From f5e0bd6390fc0e8416af6552e2ee78ac4b3c42e4 Mon Sep 17 00:00:00 2001 From: anyingiit <49945850+anyingiit@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:39:00 +0800 Subject: [PATCH 1/3] docs(smd): document iface add and delete in ochami-smd(1) The iface section of ochami-smd(1) only described "get". Add entries for "add" and "delete", following the compep and rfe sections: their forms, the payload input options, --all and --no-confirm for delete, and the rule that delete only uses each payload item's ID (blank or path-like IDs are a usage error, and no ID is derived from MACAddress). Fixes #206 Signed-off-by: anyingiit <49945850+anyingiit@users.noreply.github.com> --- man/ochami-smd.1.sc | 100 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/man/ochami-smd.1.sc b/man/ochami-smd.1.sc index f6da21db..01dd5d74 100644 --- a/man/ochami-smd.1.sc +++ b/man/ochami-smd.1.sc @@ -451,6 +451,106 @@ Subcommands for this command are as follows: Manage Ethernet interfaces. +Subcommands for this command are as follows: + +*add* [-D _description_] _comp_id_ _mac_addr_ _net_name_,_ip_addr_++ +*add* [-f _format_] -d _data_++ +*add* [-f _format_] -d @_path_++ +*add* [-f _format_] -d @- + Add one or more new Ethernet interfaces to SMD. + + In the first form of the command, the ID of the component the interface + belongs to (_comp_id_, usually an xname), the interface's MAC address + (_mac_addr_), and a network name and IPv4 address delimited by a comma + (_net_name_,_ip_addr_) are required. + + In the second form of the command, raw data is passed as an argument to be + the payload. + + In the third form of the command, a file containing the payload data (see + the *EthernetInterface* data structure above) is passed. This is convenient + in cases of dealing with many Ethernet interfaces at once. + + In the fourth form of the command, the payload data is read from standard + input. + + This command sends a POST request to SMD's /Inventory/EthernetInterfaces + endpoint. An access token is required. + + This command accepts the following options: + + *-d, --data* (_data_ | @_path_ | @-) + Specify raw _data_ to send, the _path_ to a file to read payload data + from, or to read the data from standard input (@-). The format of data + read in any of these forms is JSON by default unless *-f* is specified + to change it. + + *-D, --description* _description_ + Description of the Ethernet interface. Default: *Undescribed Ethernet + Interface*. Cannot be used with *-d*. + + *-f, --format-input* _format_ + Format of input payload data used by *-d*. Supported formats are: + + - _json_ (default) + - _json-pretty_ + - _yaml_ + +*delete* [--no-confirm] --all++ +*delete* [--no-confirm] _iface_id_...++ +*delete* [--no-confirm] [-f _format_] -d _data_++ +*delete* [--no-confirm] [-f _format_] -d @_path_++ +*delete* [--no-confirm] [-f _format_] -d @- + Delete one or more Ethernet interfaces in SMD. Unless *--no-confirm* is + passed, the user is asked to confirm deletion. + + In the first form of the command, all Ethernet interfaces are deleted. *BE + CAREFUL!* + + In the second form of the command, one or more Ethernet interface IDs + (_iface_id_) identifying the interface(s) to delete are specified. Note that + an Ethernet interface ID is not the same as a component xname. + + In the third form of the command, raw data is passed as an argument to be + the payload. + + In the fourth form of the command, a file containing the payload data (see + the *EthernetInterface* data structure above) is passed. This is convenient + in cases of dealing with many Ethernet interfaces at once. + + In the fifth form of the command, the payload data is read from standard + input. + + Only the ID of each payload item is used. If any ID, whether from the + payload or the command line, is blank, is "." or "..", or contains "/", + nothing is deleted and the command exits with a usage error. A missing ID + is not derived from *MACAddress*, so a payload item with only *MACAddress* + set also causes a usage error. + + This command sends one or more DELETE requests to SMD's + /Inventory/EthernetInterfaces endpoint. An access token is required. + + This command accepts the following options: + + *-a, --all* + Delete *all* Ethernet interfaces in SMD. *BE CAREFUL!* + + *-d, --data* (_data_ | @_path_ | @-) + Specify raw _data_ to send, the _path_ to a file to read payload data + from, or to read the data from standard input (@-). The format of data + read in any of these forms is JSON by default unless *-f* is specified + to change it. + + *-f, --format-input* _format_ + Format of input payload data used by *-d*. Supported formats are: + + - _json_ (default) + - _json-pretty_ + - _yaml_ + + *--no-confirm* + Do not ask the user to confirm deletion. Use with caution. + *get* [-F _format_] [--by-ip] [--comp-id _xname_,...] [-i _id_] [--ip _ip_,...] [-m _mac_,...] [--net _network_,...] [--newer-than _timestamp_] [--older-than _timestamp_] [--type _type_,...] Aliases: *list* From 2d30dae25765af7109f549be336f32753c149c8f Mon Sep 17 00:00:00 2001 From: Devon Bautista <17506592+synackd@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:20:07 -0600 Subject: [PATCH 2/3] fix(smd): accept multiple network pairs when adding an interface Accept at least three positional arguments so iface add can consume every network,IPv4 pair advertised by its help text and examples. Keep all pairs on one Ethernet interface in the request rather than rejecting commands that specify more than one address. Reject pairs without a comma or network name before indexing their fields to report a usage error instead of panicking. Preserve IPv4 validation and validate every pair before sending the interface to SMD. Update the man page to describe repeated pair arguments. Add command tests that verify the complete multi-address request and reject malformed pairs, including invalid later pairs, without sending a request. Follow the test/organize conventions for test names, files, comments, and runtimes. Signed-off-by: Devon Bautista <17506592+synackd@users.noreply.github.com> --- cmd/smd/iface/add.go | 7 ++++-- cmd/smd_iface_errors_test.go | 48 ++++++++++++++++++++++++++++++++++++ cmd/smd_iface_test.go | 47 +++++++++++++++++++++++++++++++++++ man/ochami-smd.1.sc | 7 +++--- 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/cmd/smd/iface/add.go b/cmd/smd/iface/add.go index f041eacc..fbcf9930 100644 --- a/cmd/smd/iface/add.go +++ b/cmd/smd/iface/add.go @@ -49,8 +49,8 @@ See ochami-smd(1) for more details.`, PreRunE: func(cmd *cobra.Command, args []string) error { // Check that all required args are passed if !cmd.Flag("data").Changed { - if len(args) != 3 { - return cli.Errorf(cli.CodeUsage, "expected -d or >= 3 arguments (component id, mac address, network name, ip address), got %d", len(args)) + if len(args) < 3 { + return cli.Errorf(cli.CodeUsage, "expected -d or >= 3 arguments (component id, mac address, network,ip pairs), got %d", len(args)) } } else { if len(args) > 0 { @@ -89,6 +89,9 @@ See ochami-smd(1) for more details.`, var nets []smd.EthernetIP for i := 2; i < len(args); i++ { tokens := strings.SplitN(args[i], ",", 2) + if len(tokens) != 2 || tokens[0] == "" { + return cli.Errorf(cli.CodeUsage, "invalid network,ip pair %q: expected a network name and IPv4 address separated by a comma", args[i]) + } if ip := net.ParseIP(tokens[1]); ip.To4() == nil { return cli.Errorf(cli.CodeUsage, "invalid IP address: %s", tokens[1]) } diff --git a/cmd/smd_iface_errors_test.go b/cmd/smd_iface_errors_test.go index 513a4823..362b6b7b 100644 --- a/cmd/smd_iface_errors_test.go +++ b/cmd/smd_iface_errors_test.go @@ -7,6 +7,7 @@ package cmd import ( "net/http" "net/http/httptest" + "strings" "testing" "github.com/openchami/ochami/internal/cli" @@ -77,6 +78,53 @@ func TestSMDIfaceAdd_InvalidIP(t *testing.T) { } } +// TestSMDIfaceAdd_InvalidNetworkPairs verifies malformed pairs and missing +// positional arguments return usage errors without sending a request. +func TestSMDIfaceAdd_InvalidNetworkPairs(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + args []string + want string + }{ + {"no arguments", nil, "expected -d or >= 3 arguments"}, + {"component only", []string{"node"}, "expected -d or >= 3 arguments"}, + {"no network", []string{"node", "de:ca:fc:0f:fe:ee"}, "expected -d or >= 3 arguments"}, + {"missing comma", []string{"node", "de:ca:fc:0f:fe:ee", "external"}, "invalid network,ip pair"}, + {"empty network", []string{"node", "de:ca:fc:0f:fe:ee", ",10.1.0.55"}, "invalid network,ip pair"}, + {"empty IP", []string{"node", "de:ca:fc:0f:fe:ee", "external,"}, "invalid IP address"}, + {"extra comma", []string{"node", "de:ca:fc:0f:fe:ee", "external,10.1.0.55,internal"}, "invalid IP address"}, + {"IPv6", []string{"node", "de:ca:fc:0f:fe:ee", "external,2001:db8::1"}, "invalid IP address"}, + {"malformed second pair", []string{"node", "de:ca:fc:0f:fe:ee", "external,10.1.0.55", "internal"}, "invalid network,ip pair"}, + {"invalid second IP", []string{"node", "de:ca:fc:0f:fe:ee", "external,10.1.0.55", "internal,not-an-ip"}, "invalid IP address"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var requests int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + w.WriteHeader(http.StatusCreated) + })) + defer srv.Close() + + args := append([]string{"smd", "--ignore-config", "iface", "add", "--uri", srv.URL, "--token", "t"}, tc.args...) + res := runOchamiWithRuntime(t, args...) + if res.err == nil || !strings.Contains(res.err.Error(), tc.want) { + t.Errorf("error = %v, want %q", res.err, tc.want) + } + if res.exitCode != cli.CodeUsage { + t.Errorf("exit code = %d, want %d (%s)", res.exitCode, cli.CodeUsage, cli.CodeName(cli.CodeUsage)) + } + if requests != 0 { + t.Errorf("request count = %d, want 0", requests) + } + }) + } +} + // TestSMDIfaceAdd_HTTPError verifies a failing POST resolves to CodeHTTP via the // per-item aggregation. func TestSMDIfaceAdd_HTTPError(t *testing.T) { diff --git a/cmd/smd_iface_test.go b/cmd/smd_iface_test.go index 04650624..f7212d56 100644 --- a/cmd/smd_iface_test.go +++ b/cmd/smd_iface_test.go @@ -12,11 +12,14 @@ package cmd // smd_iface_errors_test.go. import ( + "encoding/json" "net/http" "net/http/httptest" "net/url" "strings" "testing" + + "github.com/openchami/ochami/pkg/client/smd" ) // TestSMDIfaceGet_Filters verifies the query builder emits the expected filter @@ -188,6 +191,50 @@ func TestSMDIfaceAdd_ByFlags(t *testing.T) { } } +// TestSMDIfaceAdd_MultipleNetworks verifies multiple network,IP arguments produce +// one POST /Inventory/EthernetInterfaces with all addresses on one interface. +func TestSMDIfaceAdd_MultipleNetworks(t *testing.T) { + t.Parallel() + + var got smd.EthernetInterface + var requests int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + if r.Method != http.MethodPost || r.URL.Path != "/Inventory/EthernetInterfaces" { + t.Errorf("request = %s %s, want POST /Inventory/EthernetInterfaces", r.Method, r.URL.Path) + } + if err := json.NewDecoder(r.Body).Decode(&got); err != nil { + t.Errorf("decode request: %v", err) + } + w.WriteHeader(http.StatusCreated) + })) + defer srv.Close() + + res := runOchamiWithRuntime(t, "smd", "--ignore-config", "iface", "add", "--uri", srv.URL, "--token", "t", + "-D", "Node networks", "x3000c1s7b55n0", "de:ca:fc:0f:fe:ee", "external,10.1.0.55", "internal,172.16.0.55") + if res.err != nil { + t.Fatalf("unexpected error: %v (exit %d)", res.err, res.exitCode) + } + if requests != 1 { + t.Errorf("request count = %d, want 1", requests) + } + if got.ComponentID != "x3000c1s7b55n0" || got.MACAddress != "de:ca:fc:0f:fe:ee" || got.Description != "Node networks" { + t.Errorf("interface = %+v, want specified component, MAC and description", got) + } + want := []smd.EthernetIP{ + {Network: "external", IPAddress: "10.1.0.55"}, + {Network: "internal", IPAddress: "172.16.0.55"}, + } + if len(got.IPAddresses) != len(want) { + t.Fatalf("IPAddresses = %+v, want %+v", got.IPAddresses, want) + } + for i := range want { + if got.IPAddresses[i] != want[i] { + t.Errorf("IPAddresses[%d] = %+v, want %+v", i, got.IPAddresses[i], want[i]) + } + } +} + // TestSMDIfaceAdd_ByData verifies "add -d " issues a POST. func TestSMDIfaceAdd_ByData(t *testing.T) { t.Parallel() diff --git a/man/ochami-smd.1.sc b/man/ochami-smd.1.sc index 01dd5d74..8d2eef3b 100644 --- a/man/ochami-smd.1.sc +++ b/man/ochami-smd.1.sc @@ -453,7 +453,7 @@ Manage Ethernet interfaces. Subcommands for this command are as follows: -*add* [-D _description_] _comp_id_ _mac_addr_ _net_name_,_ip_addr_++ +*add* [-D _description_] _comp_id_ _mac_addr_ _net_name_,_ip_addr_...++ *add* [-f _format_] -d _data_++ *add* [-f _format_] -d @_path_++ *add* [-f _format_] -d @- @@ -461,8 +461,9 @@ Subcommands for this command are as follows: In the first form of the command, the ID of the component the interface belongs to (_comp_id_, usually an xname), the interface's MAC address - (_mac_addr_), and a network name and IPv4 address delimited by a comma - (_net_name_,_ip_addr_) are required. + (_mac_addr_), and one or more pairs of network name and IPv4 address + delimited by a comma (_net_name_,_ip_addr_) are required. Pass each pair + as a separate argument to add multiple IP addresses to the interface. In the second form of the command, raw data is passed as an argument to be the payload. From 911a0bf960b46b9c28a622b754c7d13481b04e5a Mon Sep 17 00:00:00 2001 From: Devon Bautista <17506592+synackd@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:22:43 -0600 Subject: [PATCH 3/3] docs(smd): clarify iface payload shape and ignored arguments State that iface add and delete payloads must be an array of EthernetInterface objects, even for a single interface. Both commands unmarshal -d data directly into a slice, so a bare object is rejected, and the data structure example already showed an array while calling it a single EthernetInterface. Correct the delete validation rule to cover only the IDs used to select interfaces. With --all or -d, positional arguments are ignored with a warning and never validated, so the previous wording promised a usage error that --all never raised. Document that --all takes precedence over -d after the payload is validated, and that add also ignores positional arguments when -d is passed. Signed-off-by: Devon Bautista <17506592+synackd@users.noreply.github.com> --- man/ochami-smd.1.sc | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/man/ochami-smd.1.sc b/man/ochami-smd.1.sc index 8d2eef3b..4940dbb3 100644 --- a/man/ochami-smd.1.sc +++ b/man/ochami-smd.1.sc @@ -152,7 +152,9 @@ set*, then the form uses _label_ and _ids_ as: The *EthernetInterface* contains information on a network interface for a *Component*. -Below is an example of a single *EthernetInterface* in JSON form. +Below is an example of an array containing a single *EthernetInterface* in +JSON form. Payload data passed to *iface add* and *iface delete* must be such +an array, even when it contains only one *EthernetInterface*. ``` [ @@ -475,6 +477,10 @@ Subcommands for this command are as follows: In the fourth form of the command, the payload data is read from standard input. + In the second, third, and fourth forms, the payload is an array of + *EthernetInterface* objects, even when adding a single interface. If + positional arguments are also passed, they are ignored with a warning. + This command sends a POST request to SMD's /Inventory/EthernetInterfaces endpoint. An access token is required. @@ -522,11 +528,20 @@ Subcommands for this command are as follows: In the fifth form of the command, the payload data is read from standard input. - Only the ID of each payload item is used. If any ID, whether from the - payload or the command line, is blank, is "." or "..", or contains "/", - nothing is deleted and the command exits with a usage error. A missing ID - is not derived from *MACAddress*, so a payload item with only *MACAddress* - set also causes a usage error. + In the third, fourth, and fifth forms, the payload is an array of + *EthernetInterface* objects, even when deleting a single interface. Only + the ID of each payload item is used. + + If any ID used to select interfaces, whether from the payload or the + command line, is blank, is "." or "..", or contains "/", nothing is deleted + and the command exits with a usage error. A missing ID is not derived from + *MACAddress*, so a payload item with only *MACAddress* set also causes a + usage error. + + When *--all* or *-d* is passed, positional _iface_id_ arguments are + ignored with a warning and are not validated. When both *--all* and *-d* + are passed, the payload is still read and validated, but *--all* takes + precedence and all Ethernet interfaces are deleted. This command sends one or more DELETE requests to SMD's /Inventory/EthernetInterfaces endpoint. An access token is required.