diff --git a/cmd/bss-boot-params-add.go b/cmd/bss-boot-params-add.go index cbdefbe..eb22c11 100644 --- a/cmd/bss-boot-params-add.go +++ b/cmd/bss-boot-params-add.go @@ -18,9 +18,10 @@ var bootParamsAddCmd = &cobra.Command{ Args: cobra.NoArgs, Short: "Add new boot parameters for one or more components", Long: `Add new boot parameters for one or more components. At least one of --kernel, ---initrd, or --params must be specified as well as at least one of --xname, --mac, or --nid. -Alternatively, pass -f to pass a file (optionally specifying --payload-format, JSON by default), -but the rules above still apply for the payload. +--initrd, or --params must be specified as well as at least one of --xname, +--mac, or --nid. Alternatively, pass -f to pass a file (optionally specifying +--payload-format, JSON by default), but the rules above still apply for the +payload. If the specified file path is -, the data is read from standard input. This command sends a POST to BSS. An access token is required.`, Example: ` ochami bss boot params add \ @@ -31,7 +32,9 @@ This command sends a POST to BSS. An access token is required.`, ochami bss boot params add --mac 00:de:ad:be:ef:00,00:c0:ff:ee:00:00 --params 'quiet nosplash' ochami bss boot params add --mac 00:de:ad:be:ef:00 --mac 00:c0:ff:ee:00:00 --kernel https://example.com/kernel ochami bss boot params add -f payload.json - ochami bss boot params add -f payload.yaml --payload-format yaml`, + ochami bss boot params add -f payload.yaml --payload-format yaml + echo '' | ochami bss boot params add -f - + echo '' | ochami bss boot params add -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // cmd.LocalFlags().NFlag() doesn't seem to work, so we check every flag if len(args) == 0 && @@ -70,15 +73,7 @@ This command sends a POST to BSS. An access token is required.`, bp := bssTypes.BootParams{} // Read payload from file first, allowing overwrites from flags - if cmd.Flag("payload").Changed { - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &bp) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } - } + handlePayload(cmd, &bp) // Set the hosts the boot parameters are for if cmd.Flag("xname").Changed { diff --git a/cmd/bss-boot-params-delete.go b/cmd/bss-boot-params-delete.go index 273f93e..19d192e 100644 --- a/cmd/bss-boot-params-delete.go +++ b/cmd/bss-boot-params-delete.go @@ -18,17 +18,22 @@ var bootParamsDeleteCmd = &cobra.Command{ Args: cobra.NoArgs, Short: "Delete boot parameters for one or more components", Long: `Delete boot parameters for one or more components. At least one of --kernel, ---initrd, --params, --xname, --mac, or --nid must be specified. This command can delete -boot parameters by config (kernel URI, initrd URI, or kernel command line) or by component -(--xname, --mac, or --nid). The user will be asked for confirmation before deletion unless ---force is passed. Alternatively, pass -f to pass a file (optionally specifying --payload-format, -JSON by default), but the rules above still apply for the payload. +--initrd, --params, --xname, --mac, or --nid must be specified. +This command can delete boot parameters by config (kernel URI, +initrd URI, or kernel command line) or by component (--xname, +--mac, or --nid). The user will be asked for confirmation before +deletion unless --force is passed. Alternatively, pass -f to pass +a file (optionally specifying --payload-format, JSON by default), +but the rules above still apply for the payload. If the specified +file path is -, the data is read from standard input. This command sends a DELETE to BSS. An access token is required.`, Example: ` ochami bss boot params delete --kernel https://example.com/kernel ochami bss boot params delete --kernel https://example.com/kernel --initrd https://example.com/initrd ochami bss boot params delete -f payload.json - ochami bss boot params delete -f payload.yaml --payload-format yaml`, + ochami bss boot params delete -f payload.yaml --payload-format yaml + echo '' | ochami bss boot params delete -f - + echo '' | ochami bss boot params delete -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // cmd.LocalFlags().NFlag() doesn't seem to work, so we check every flag if len(args) == 0 && @@ -67,15 +72,7 @@ This command sends a DELETE to BSS. An access token is required.`, bp := bssTypes.BootParams{} // Read payload from file first, allowing overwrites from flags - if cmd.Flag("payload").Changed { - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &bp) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } - } + handlePayload(cmd, &bp) // Set the hosts the boot parameters are for if cmd.Flag("xname").Changed { diff --git a/cmd/bss-boot-params-set.go b/cmd/bss-boot-params-set.go index 1bde7e0..6b4840f 100644 --- a/cmd/bss-boot-params-set.go +++ b/cmd/bss-boot-params-set.go @@ -18,11 +18,13 @@ var bootParamsSetCmd = &cobra.Command{ Args: cobra.NoArgs, Short: "Set boot parameters for one or more components, overwriting any previous", Long: `Set boot parameters for one or mote components, overwriting any previously-set -parameters. At least one of --kernel, --initrd, or --params is required to -tell ochami which boot data to set. Also, at least one of --xname, --mac, -or --nid is required to tell ochami which components need modification. Alternatively, pass -f -to pass a file (optionally specifying --payload-format, JSON by default), but the rules above -still apply for the payload. +parameters. At least one of --kernel, --initrd, or --params is +required to tell ochami which boot data to set. Also, at least +one of --xname, --mac, or --nid is required to tell ochami which +components need modification. Alternatively, pass -f to pass a +file (optionally specifying --payload-format, JSON by default), +but the rules above still apply for the payload. If the specified +file path is -, the data is read from standard input. This command sends a PUT to BSS. An access token is required.`, Example: ` ochami bss boot params set --xname x1000c1s7b0 --kernel https://example.com/kernel @@ -30,7 +32,9 @@ This command sends a PUT to BSS. An access token is required.`, ochami bss boot params set --xname x1000c1s7b0 --xname x1000c1s7b1 --kernel https://example.com/kernel ochami bss boot params set --xname x1000c1s7b0 --nid 1 --mac 00:c0:ff:ee:00:00 --params 'quiet nosplash' ochami bss boot params set -f payload.json - ochami bss boot params set -f payload.yaml --payload-format yaml`, + ochami bss boot params set -f payload.yaml --payload-format yaml + echo | ochami bss boot params set -f - + echo | ochami bss boot params set -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // cmd.LocalFlags().NFlag() doesn't seem to work, so we check every flag if len(args) == 0 && @@ -69,15 +73,7 @@ This command sends a PUT to BSS. An access token is required.`, bp := bssTypes.BootParams{} // Read payload from file first, allowing overwrites from flags - if cmd.Flag("payload").Changed { - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &bp) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } - } + handlePayload(cmd, &bp) // Set the hosts the boot parameters are for if cmd.Flag("xname").Changed { diff --git a/cmd/bss-boot-params-update.go b/cmd/bss-boot-params-update.go index 3332d72..2bf7a65 100644 --- a/cmd/bss-boot-params-update.go +++ b/cmd/bss-boot-params-update.go @@ -18,9 +18,11 @@ var bootParamsUpdateCmd = &cobra.Command{ Args: cobra.NoArgs, Short: "Update some or all boot parameters for one or more components", Long: `Update some or all boot parameters for one or more components. At least one of ---kernel, initrd, or --params must be specified as well as at least one of --xname, --mac, or ---nid. Alternatively, pass -f to pass a file (optionally specifying --payload-format, JSON by -default), but the rules above still apply for the payload. +--kernel, initrd, or --params must be specified as well as at least +one of --xname, --mac, or --nid. Alternatively, pass -f to pass a +file (optionally specifying --payload-format, JSON by default), but +the rules above still apply for the payload. If the specified file +path is -, the data is read from standard input. This command sends a PATCH to BSS. An access token is required.`, Example: ` ochami bss boot params update --xname x1000c1s7b0 --kernel https://example.com/kernel @@ -28,7 +30,9 @@ This command sends a PATCH to BSS. An access token is required.`, ochami bss boot params update --xname x1000c1s7b0 --xname x1000c1s7b1 --kernel https://example.com/kernel ochami bss boot params update --xname x1000c1s7b0 --nid 1 --mac 00:c0:ff:ee:00:00 --params 'quiet nosplash' ochami bss boot params update -f payload.json - ochami bss boot params update -f payload.yaml --payload-format yaml`, + ochami bss boot params update -f payload.yaml --payload-format yaml + echo '' | ochami bss boot params update -f - + echo '' | ochami bss boot params update -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // cmd.LocalFlags().NFlag() doesn't seem to work, so we check every flag if len(args) == 0 && @@ -67,15 +71,7 @@ This command sends a PATCH to BSS. An access token is required.`, bp := bssTypes.BootParams{} // Read payload from file first, allowing overwrites from flags - if cmd.Flag("payload").Changed { - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &bp) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } - } + handlePayload(cmd, &bp) // Set the hosts the boot parameters are for if cmd.Flag("xname").Changed { diff --git a/cmd/cloud_init-config-add.go b/cmd/cloud_init-config-add.go index 34a77e3..209063c 100644 --- a/cmd/cloud_init-config-add.go +++ b/cmd/cloud_init-config-add.go @@ -21,12 +21,12 @@ var cloudInitConfigAddCmd = &cobra.Command{ Long: `Add one or more new cloud-init configs. Either a payload file containing the data or the JSON data itself must be passed. Data is represented by a JSON array of cloud-init configs, -even if only one is being passed. +even if only one is being passed. An alternative to using +-d would be to use -f and passing -, which will cause ochami +to read the data from standard input. This command sends a POST to cloud-init.`, - Example: ` ochami cloud-init config add -f payload.json - ochami cloud-init config add -f payload.yaml --payload-format yaml - ochami cloud-init config add -d \ + Example: ` ochami cloud-init config add -d \ '[ \ { \ "name": "compute", \ @@ -44,7 +44,11 @@ This command sends a POST to cloud-init.`, } \ } \ } \ - ]'`, + ]' + ochami cloud-init config add -f payload.json + ochami cloud-init config add -f payload.yaml --payload-format yaml + echo '' | ochami cloud-init config add -f - + echo '' | ochami cloud-init config add -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // Without a base URI, we cannot do anything cloudInitBaseURI, err := getBaseURI(cmd) @@ -70,13 +74,7 @@ This command sends a POST to cloud-init.`, var ciData []citypes.CI if cmd.Flag("payload").Changed { // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &ciData) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } + handlePayload(cmd, &ciData) } else if cmd.Flag("data").Changed { // ...otherwise try to read raw JSON from CLI rawJSON, err := cmd.Flags().GetString("data") diff --git a/cmd/cloud_init-config-set.go b/cmd/cloud_init-config-set.go index 5b905ce..47a6ea1 100644 --- a/cmd/cloud_init-config-set.go +++ b/cmd/cloud_init-config-set.go @@ -21,12 +21,12 @@ var cloudInitConfigSetCmd = &cobra.Command{ Long: `Set cloud-init config for one or more ids, overwriting any previous. Either a payload file containing the data or the JSON data itself must be passed. Data is represented by a JSON array of cloud-init -configs, even if only one is being passed. +configs, even if only one is being passed. An alternative to using +-d would be to use -f and passing -, which will cause ochami +to read the data from standard input. This command sends a PUT to cloud-init.`, - Example: ` ochami cloud-init config set -f payload.json - ochami cloud-init config set -f payload.yaml --payload-format yaml - ochami cloud-init config set -d \ + Example: ` ochami cloud-init config set -d \ '[ \ { \ "name": "compute", \ @@ -41,7 +41,11 @@ This command sends a PUT to cloud-init.`, } \ } \ } \ - ]'`, + ]' + ochami cloud-init config set -f payload.json + ochami cloud-init config set -f payload.yaml --payload-format yaml + echo '' | ochami cloud-init config set -f - + echo '' | ochami cloud-init config set -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // Without a base URI, we cannot do anything cloudInitBaseURI, err := getBaseURI(cmd) @@ -67,13 +71,7 @@ This command sends a PUT to cloud-init.`, var ciData []citypes.CI if cmd.Flag("payload").Changed { // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &ciData) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } + handlePayload(cmd, &ciData) } else if cmd.Flag("data").Changed { // ...otherwise try to read raw JSON from CLI rawJSON, err := cmd.Flags().GetString("data") diff --git a/cmd/discover.go b/cmd/discover.go index 9aa5c06..4717bd9 100644 --- a/cmd/discover.go +++ b/cmd/discover.go @@ -21,7 +21,8 @@ var discoverCmd = &cobra.Command{ Long: `Populate SMD with data. Currently, this command performs "fake" discovery, whereby data from a payload file is used to create the SMD structures. In this way, the command does not perform dynamic discovery like Magellan, -but statically populates SMD using a file. +but statically populates SMD using a file. If - is used as the argument to +-f, the payload data is read from standard input. The format of the payload file is an array of node specifications. In YAML, each node entry would look something like: @@ -82,13 +83,7 @@ nodes: // Read data from payload file nodes := discover.NodeList{} - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err = client.ReadPayload(dFile, dFormat, &nodes) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } + handlePayload(cmd, &nodes) log.Logger.Debug().Msgf("read %d nodes", len(nodes.Nodes)) log.Logger.Debug().Msgf("nodes: %s", nodes) diff --git a/cmd/root.go b/cmd/root.go index 2dced6c..eb24561 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -398,3 +398,17 @@ func setTokenFromEnvVar(cmd *cobra.Command) { log.Logger.Error().Msgf("Environment variable %s unset for reading token for cluster %q", envVarToRead, clusterName) os.Exit(1) } + +// handlePayload unmarshals a payload file into data for command cmd if +// --payload and, optionally, --payload-format, are passed. +func handlePayload(cmd *cobra.Command, data any) { + if cmd.Flag("payload").Changed { + dFile := cmd.Flag("payload").Value.String() + dFormat := cmd.Flag("payload-format").Value.String() + err := client.ReadPayload(dFile, dFormat, data) + if err != nil { + log.Logger.Error().Err(err).Msg("unable to read payload for request") + os.Exit(1) + } + } +} diff --git a/cmd/smd-compep-delete.go b/cmd/smd-compep-delete.go index 4aa0e3a..ae1cf60 100644 --- a/cmd/smd-compep-delete.go +++ b/cmd/smd-compep-delete.go @@ -17,12 +17,17 @@ var compepDeleteCmd = &cobra.Command{ Use: "delete -f | --all | ...", Short: "Delete one or more component endpoints", Long: `Delete one or more component endpoints. These can be specified by one or more xnames. +Alternatively, pass the xnames in an array of component endpoint data +via a file with -f. If - is passed to -f, the data is read from standard +input. This command sends a DELETE to SMD. An access token is required.`, Example: ` ochami smd compep delete x3000c1s7b56n0 x3000c1s7b56n1 ochami smd compep delete --all ochami smd compep delete -f payload.json - ochami smd compep delete -f payload.yaml --payload-format yaml`, + ochami smd compep delete -f payload.yaml --payload-format yaml + echo '' | ochami smd compep delete -f - + echo '' | ochami smd compep delete -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // With options, only one of: // - A payload file with -f @@ -83,16 +88,7 @@ This command sends a DELETE to SMD. An access token is required.`, var xnameSlice []string if cmd.Flag("payload").Changed { // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &ceSlice) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } - for _, ce := range ceSlice { - xnameSlice = append(xnameSlice, ce.ID) - } + handlePayload(cmd, &ceSlice) } else { // ...otherwise, use passed CLI arguments xnameSlice = args diff --git a/cmd/smd-component-add.go b/cmd/smd-component-add.go index 153d644..ed54b31 100644 --- a/cmd/smd-component-add.go +++ b/cmd/smd-component-add.go @@ -16,14 +16,17 @@ var componentAddCmd = &cobra.Command{ Use: "add -f | ( )", Short: "Add new component(s)", Long: `Add new component(s). A name (xname) and node ID (int64) are required unless --f is passed to read from a payload file. Specifying -f also is mutually exclusive with the -other flags of this command. +-f is passed to read from a payload file. Specifying -f also is +mutually exclusive with the other flags of this command. If - is +used as the argument to -f, the data is read from standard input. This command sends a POST to SMD. An access token is required.`, Example: ` ochami smd component add x3000c1s7b56n0 56 ochami smd component add --state Ready --enabled --role Compute --arch X86 x3000c1s7b56n0 56 ochami smd component add -f payload.json - ochami smd component add -f payload.yaml --payload-format yaml`, + ochami smd component add -f payload.yaml --payload-format yaml + echo '' | ochami smd component add -f - + echo '' | ochami smd component add -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // Check that all required args are passed if len(args) == 0 && !cmd.Flag("payload").Changed { @@ -61,14 +64,7 @@ This command sends a POST to SMD. An access token is required.`, var compSlice client.ComponentSlice if cmd.Flag("payload").Changed { - // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &compSlice) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } + handlePayload(cmd, &compSlice) } else { // ...otherwise use CLI options comp := client.Component{ diff --git a/cmd/smd-component-delete.go b/cmd/smd-component-delete.go index 7d02a38..419184e 100644 --- a/cmd/smd-component-delete.go +++ b/cmd/smd-component-delete.go @@ -16,14 +16,18 @@ var componentDeleteCmd = &cobra.Command{ Use: "delete -f | --all | ...", Short: "Delete one or more components", Long: `Delete one or more components. These can be specified by one or more xnames, one -or more NIDs, or a combination of both. +or more NIDs, or a combination of both. Alternatively, specify the xnames in +an array of component structures within a payload file and pass it to -f. If +- is passed to -f, the data is read from standard input. This command sends a DELETE to SMD. An access token is required.`, Example: ` ochami smd component delete x3000c1s7b56n0 ochami smd component delete x3000c1s7b56n0 x3000c1s7b56n1 ochami smd component delete --all ochami smd component delete -f payload.json - ochami smd component delete -f payload.yaml --payload-format yaml`, + ochami smd component delete -f payload.yaml --payload-format yaml + echo '' | ochami smd component delete -f - + echo '' | ochami smd component delete -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // With options, only one of: // - A payload file with -f @@ -84,16 +88,7 @@ This command sends a DELETE to SMD. An access token is required.`, var xnameSlice []string if cmd.Flag("payload").Changed { // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &compSlice.Components) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } - for _, comp := range compSlice.Components { - xnameSlice = append(xnameSlice, comp.ID) - } + handlePayload(cmd, &compSlice) } else { // ...otherwise, use passed CLI arguments xnameSlice = args diff --git a/cmd/smd-group-add.go b/cmd/smd-group-add.go index 89bce0a..a1b7146 100644 --- a/cmd/smd-group-add.go +++ b/cmd/smd-group-add.go @@ -17,7 +17,8 @@ var groupAddCmd = &cobra.Command{ Short: "Add new group", Long: `Add new group. A group name is required unless -f is passed to read the payload file. Specifying -f also is mutually exclusive with the other flags of this commands -and its arguments. +and its arguments. If - is used as the argument to -f, the data is read from +standard input. This command sends a POST to SMD. An access token is required.`, Example: ` ochami smd group add computes @@ -30,7 +31,9 @@ This command sends a POST to SMD. An access token is required.`, --exclusive-group amd64 \ arm64 ochami smd group add -f payload.json - ochami smd group add -f payload.yaml --payload-format yaml`, + ochami smd group add -f payload.yaml --payload-format yaml + echo '' | ochami smd group add -f - + echo '' | ochami smd group add -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // Check that all required args are passed if len(args) == 0 && !cmd.Flag("payload").Changed { @@ -69,13 +72,7 @@ This command sends a POST to SMD. An access token is required.`, var groups []client.Group if cmd.Flag("payload").Changed { // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &groups) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } + handlePayload(cmd, &groups) } else { // ...otherwise use CLI options/args group := client.Group{Label: args[0]} diff --git a/cmd/smd-group-delete.go b/cmd/smd-group-delete.go index 7ef903d..835765e 100644 --- a/cmd/smd-group-delete.go +++ b/cmd/smd-group-delete.go @@ -16,8 +16,16 @@ var groupDeleteCmd = &cobra.Command{ Use: "delete -f | ...", Short: "Delete one or more groups", Long: `Delete one or more groups. These can be specified by one or more group labels. +Alternatively, pass the group labels in an array of group structures +within a payload file and specify that file via -f. If - is used as +the argument to -f, the data is read from standard input. This command sends a DELETE to SMD. An access token is required.`, + Example: ` ochami smd group delete compute + ochami smd group delete -f payload.json + ochami smd group delete -f payload.yaml --payload-format yaml + echo '' | ochami smd group delete -f - + echo '' | ochami smd group delete -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // With options, only one of: // - A payload file with -f @@ -72,16 +80,7 @@ This command sends a DELETE to SMD. An access token is required.`, var gLabelSlice []string if cmd.Flag("payload").Changed { // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &groups) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } - for _, group := range groups { - gLabelSlice = append(gLabelSlice, group.Label) - } + handlePayload(cmd, &groups) } else { // ...otherwise, use passed CLI arguments gLabelSlice = args diff --git a/cmd/smd-group-update.go b/cmd/smd-group-update.go index f4c2261..336353e 100644 --- a/cmd/smd-group-update.go +++ b/cmd/smd-group-update.go @@ -13,12 +13,13 @@ import ( // groupUpdateCmd represents the smd-group-update command var groupUpdateCmd = &cobra.Command{ - Use: "update [-f ] | ([--description ] [--tag ]... )", + Use: "update -f | ([--description ] [--tag ]... )", Short: "Update the description and/or tags of a group", Long: `Update the description and/or tags of a group. At least one of --description or --tag must be specified. Alternatively, pass -f to pass a file (optionally specifying --payload-format, JSON by default), but the -rules above still apply for the payload. +rules above still apply for the payload. If - is used as the +argument to -f, the data is read from standard input. This command sends a PATCH to SMD. An access token is required.`, Example: ` ochami smd group update --description "New description for compute" compute @@ -26,7 +27,9 @@ This command sends a PATCH to SMD. An access token is required.`, ochami smd group update --tag existing_tag,new_tag compute ochami smd group update --tag existing_tag,new_tag -d "New description for compute" compute ochami smd group update -f payload.json - ochami smd group update -f payload.yaml --payload-format yaml`, + ochami smd group update -f payload.yaml --payload-format yaml + echo '' | ochami smd group update -f - + echo '' | ochami smd group update -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // cmd.LocalFlags().NFlag() doesn't seem to work, so we check every flag if len(args) == 0 && !cmd.Flag("description").Changed && !cmd.Flag("tag").Changed { @@ -64,13 +67,7 @@ This command sends a PATCH to SMD. An access token is required.`, // Read payload from file first, allowing overwrites from flags if cmd.Flag("payload").Changed { - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &groups) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } + handlePayload(cmd, &groups) } else { // ...otherwise use CLI options/args group := client.Group{Label: args[0]} diff --git a/cmd/smd-iface-add.go b/cmd/smd-iface-add.go index 3689e67..93ce0b6 100644 --- a/cmd/smd-iface-add.go +++ b/cmd/smd-iface-add.go @@ -18,16 +18,20 @@ var ifaceAddCmd = &cobra.Command{ Use: "add -f | ( (,)...)", Short: "Add new ethernet interface(s)", Long: `Add new ethernet interface(s). A component ID (usually an xname), MAC address, and -one or more pairs of network name and IP address (delimited by a comma) are required unless -f is -passed to read from a payload file. Specifying -f also is mutually exclusive with the other flags -of this command and its arguments. +one or more pairs of network name and IP address (delimited by a comma) +are required unless -f is passed to read from a payload file. Specifying +-f also is mutually exclusive with the other flags of this command and +its arguments. If - is used as the argument to -f, the data is read +from standard input. This command sends a POST to SMD. An access token is required.`, Example: ` ochami smd iface add x3000c1s7b55n0 de:ca:fc:0f:fe:ee NMN,172.16.0.55 ochami smd iface add -d "Node Management for n55" x3000c1s7b55n0 de:ca:fc:0f:fe:ee NMN,172.16.0.55 ochami smd iface add x3000c1s7b55n0 de:ca:fc:0f:fe:ee external,10.1.0.55 internal,172.16.0.55 ochami smd iface add -f payload.json - ochami smd iface add -f payload.yaml --payload-format yaml`, + ochami smd iface add -f payload.yaml --payload-format yaml + echo '' | ochami smd iface add -f - + echo '' | ochami smd iface add -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // Check that all required args are passed if len(args) == 0 && !cmd.Flag("payload").Changed { @@ -66,13 +70,7 @@ This command sends a POST to SMD. An access token is required.`, var eis []client.EthernetInterface if cmd.Flag("payload").Changed { // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &eis) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } + handlePayload(cmd, &eis) } else { // ...otherwise use CLI options/args var nets []client.EthernetIP diff --git a/cmd/smd-iface-delete.go b/cmd/smd-iface-delete.go index 61504a5..1bf9454 100644 --- a/cmd/smd-iface-delete.go +++ b/cmd/smd-iface-delete.go @@ -16,14 +16,19 @@ var ifaceDeleteCmd = &cobra.Command{ Use: "delete -f | --all | ...", Short: "Delete one or more ethernet interfaces", Long: `Delete one or more ethernet interfaces. These can be specified by one or more ethernet -interface IDs (note this is not the same as a component xname). +interface IDs (note this is not the same as a component xname). Alternatively, +pass -f to pass a file (optionally specifying --payload-format, JSON by default) +containing the payload data. If - is used as the argument to -f, the data is +read from standard input. This command sends a DELETE to SMD. An access token is required.`, Example: ` ochami smd iface delete decafc0ffeee ochami smd iface delete decafc0ffeee de:ad:be:ee:ee:ef ochami smd iface delete --all ochami smd iface delete -f payload.json - ochami smd iface delete -f payload.yaml --payload-format yaml`, + ochami smd iface delete -f payload.yaml --payload-format yaml + echo '' | ochami smd iface delete -f - + echo '' | ochami smd iface delete -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // With options, only one of: // - A payload file with -f @@ -84,16 +89,7 @@ This command sends a DELETE to SMD. An access token is required.`, var eIdSlice []string if cmd.Flag("payload").Changed { // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &eiSlice) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } - for _, ei := range eiSlice { - eIdSlice = append(eIdSlice, ei.ID) - } + handlePayload(cmd, &eiSlice) } else { // ...otherwise, use passed CLI arguments eIdSlice = args diff --git a/cmd/smd-rfe-add.go b/cmd/smd-rfe-add.go index ccc52df..df5cb7d 100644 --- a/cmd/smd-rfe-add.go +++ b/cmd/smd-rfe-add.go @@ -17,13 +17,16 @@ var rfeAddCmd = &cobra.Command{ Use: "add -f | ( )", Short: "Add new redfish endpoint(s)", Long: `Add new redfish endpoint(s). An xname, name, IP address, and MAC address are required -unless -f is passed to read from a payload file. Specifying -f also is mutually exclusive with the other -flags of this command and its arguments. +unless -f is passed to read from a payload file. Specifying -f also is +mutually exclusive with the other flags of this command and its arguments. +If - is used as the argument to -f, the data is read from standard input. This command sends a POST to SMD. An access token is required.`, Example: ` ochami smd rfe add x3000c1s7b56 bmc-node56 172.16.0.156 de:ca:fc:0f:fe:ee ochami smd rfe add -f payload.json - ochami smd rfe add -f payload.yaml --payload-format yaml`, + ochami smd rfe add -f payload.yaml --payload-format yaml + echo '' | ochami smd rfe add -f - + echo '' | ochami smd rfe add -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // Check that all required args are passed if len(args) == 0 && !cmd.Flag("payload").Changed { @@ -62,13 +65,7 @@ This command sends a POST to SMD. An access token is required.`, var rfes client.RedfishEndpointSlice if cmd.Flag("payload").Changed { // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &rfes.RedfishEndpoints) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } + handlePayload(cmd, &rfes.RedfishEndpoints) } else { // ...otherwise use CLI options/args rfe := csm.RedfishEndpoint{ diff --git a/cmd/smd-rfe-delete.go b/cmd/smd-rfe-delete.go index 885bdfe..21d9878 100644 --- a/cmd/smd-rfe-delete.go +++ b/cmd/smd-rfe-delete.go @@ -16,13 +16,18 @@ var rfeDeleteCmd = &cobra.Command{ Use: "delete -f | --all | ...", Short: "Delete one or more redfish endpoints", Long: `Delete one or more redfish endpoints. These can be specified by one or more xnames. +Alternatively, use -f to read the payload data from a file (optionally +specifying --payload-format, JSON by default). If - is used as the +argument to -f, the data is read from standard input. This command sends a DELETE to SMD. An access token is required.`, Example: ` ochami smd rfe delete x3000c1s7b56 ochami smd rfe delete x3000c1s7b56 x3000c1s7b56 ochami smd rfe delete --all ochami smd rfe delete -f payload.json - ochami smd rfe delete -f payload.yaml --payload-format yaml`, + ochami smd rfe delete -f payload.yaml --payload-format yaml + echo '' | ochami smd rfe delete -f - + echo '' | ochami smd rfe delete -f - --payload-format yaml`, Run: func(cmd *cobra.Command, args []string) { // With options, only one of: // - A payload file with -f @@ -83,16 +88,7 @@ This command sends a DELETE to SMD. An access token is required.`, var xnameSlice []string if cmd.Flag("payload").Changed { // Use payload file if passed - dFile := cmd.Flag("payload").Value.String() - dFormat := cmd.Flag("payload-format").Value.String() - err := client.ReadPayload(dFile, dFormat, &rfeSlice.RedfishEndpoints) - if err != nil { - log.Logger.Error().Err(err).Msg("unable to read payload for request") - os.Exit(1) - } - for _, rfe := range rfeSlice.RedfishEndpoints { - xnameSlice = append(xnameSlice, rfe.ID) - } + handlePayload(cmd, rfeSlice.RedfishEndpoints) } else { // ...otherwise, use passed CLI arguments xnameSlice = args diff --git a/internal/client/client.go b/internal/client/client.go index 863458e..d1e14bd 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -16,6 +16,7 @@ import ( "strings" "time" + oio "github.com/OpenCHAMI/ochami/internal/io" "github.com/OpenCHAMI/ochami/internal/log" "github.com/OpenCHAMI/ochami/internal/version" ) @@ -340,6 +341,48 @@ func (oc *OchamiClient) UseCACert(caCertPath string) error { return nil } +// BytesToHTTPBody takes byte slice and string representing the format of the +// data, and tries to marshal it into an HTTPBody (byte array) in JSON form, +// returning it. If an unmarshalling error occurs or either of the arguments are +// empty, nil and an error are returned. Current file formats supported are JSON +// and YAML. +func BytesToHTTPBody(data []byte, format string) (HTTPBody, error) { + if len(data) == 0 { + return nil, fmt.Errorf("byte slice is empty") + } + if format == "" { + return nil, fmt.Errorf("format is empty") + } + + var b HTTPBody + var err error + switch strings.ToLower(format) { + case "json": + var j interface{} + err = json.Unmarshal(data, &j) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) + } + b, err = json.Marshal(j) + if err != nil { + err = fmt.Errorf("failed to marshal JSON: %w", err) + } + case "yaml": + var y interface{} + err = yaml.Unmarshal(data, &y) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal YAML: %w", err) + } + y = CanonicalizeInterface(y) + b, err = json.Marshal(y) + if err != nil { + err = fmt.Errorf("failed to marshal JSON (converted from YAML): %w", err) + } + } + + return b, err +} + // FileToHTTPBody takes a file path and string representing the format of the // file, reads the file, and tries to marshal it into an HTTPBody (byte array) // in JSON form, returning it. If an unmarshalling error occurs or either of the @@ -391,14 +434,30 @@ func FileToHTTPBody(path, format string) (HTTPBody, error) { // FileToHTTPBody supports), such as YAML. If a marshalling/unmarshalling error // occurs or either path or format are empty, an error is returned. func ReadPayload(path, format string, v any) error { - log.Logger.Debug().Msgf("Payload file: %s", path) - log.Logger.Debug().Msgf("Payload file format: %s", format) - - body, err := FileToHTTPBody(path, format) - if err != nil { - return fmt.Errorf("unable to create HTTP body from file: %w", err) + log.Logger.Debug().Msgf("payload file: %s", path) + log.Logger.Debug().Msgf("payload file format: %s", format) + + var body HTTPBody + var err error + if path == "-" { + log.Logger.Debug().Msg("payload file was -, reading from stdin") + var data []byte + data, err = oio.ReadStdin() + if err != nil { + return fmt.Errorf("unable to read payload data: %w", err) + } + log.Logger.Debug().Msgf("bytes read: %q", data) + body, err = BytesToHTTPBody(data, format) + if err != nil { + return fmt.Errorf("unable to create HTTP body from payload bytes: %w", err) + } + } else { + body, err = FileToHTTPBody(path, format) + if err != nil { + return fmt.Errorf("unable to create HTTP body from file: %w", err) + } } - log.Logger.Debug().Msgf("Body bytes: %s", string(body)) + log.Logger.Debug().Msgf("body bytes: %q", body) err = json.Unmarshal(body, v) if err != nil { diff --git a/internal/io/io.go b/internal/io/io.go new file mode 100644 index 0000000..4483355 --- /dev/null +++ b/internal/io/io.go @@ -0,0 +1,22 @@ +package io + +import ( + "bufio" + "fmt" + "os" +) + +// ReadStdin reads all of standard input and returns the bytes. If an error +// occurs during scanning, it is returned. +func ReadStdin() ([]byte, error) { + var b []byte + input := bufio.NewScanner(os.Stdin) + for input.Scan() { + b = append(b, input.Bytes()...) + b = append(b, byte('\n')) + } + if err := input.Err(); err != nil { + return b, fmt.Errorf("failed to read stdin: %w", err) + } + return b, nil +}