From 8c44cdd3f25f49bc0606f365e669bb0e6045cdff Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Wed, 26 Apr 2023 21:05:14 +0000 Subject: [PATCH] DAOS-13471 control: Add structured version info to utilities Allow consumers to grab structured build/version information. Centralizes JSON output logic to a single implementation in the cmdutil package. Includes updates to control/version.py to use the structured version information instead of scraping stdout. Includes changes made for DAOS-13236 to include build info in version output. Required-githooks: true Signed-off-by: Michael MacDonald --- ci/unit/required_packages.sh | 37 ++++--- site_scons/site_tools/go_builder.py | 2 +- src/control/SConscript | 2 + src/control/build/info.go | 39 +++++++ src/control/build/string.go | 57 ++++++++++ src/control/build/variables.go | 19 +++- src/control/cmd/daos/acl.go | 12 +- src/control/cmd/daos/container.go | 38 +++---- src/control/cmd/daos/filesystem.go | 8 +- src/control/cmd/daos/main.go | 103 +++--------------- src/control/cmd/daos/object.go | 4 +- src/control/cmd/daos/pool.go | 20 ++-- src/control/cmd/daos/snapshot.go | 8 +- src/control/cmd/daos/system.go | 4 +- src/control/cmd/daos/util.go | 2 +- src/control/cmd/daos_agent/attachinfo.go | 14 +-- src/control/cmd/daos_agent/main.go | 59 ++++------ src/control/cmd/daos_agent/network.go | 14 +-- src/control/cmd/daos_server/main.go | 30 ++++- src/control/cmd/dmg/auto.go | 7 +- src/control/cmd/dmg/cont.go | 7 +- src/control/cmd/dmg/firmware.go | 13 ++- src/control/cmd/dmg/main.go | 100 +++-------------- src/control/cmd/dmg/network.go | 7 +- src/control/cmd/dmg/pool.go | 47 ++++---- src/control/cmd/dmg/server.go | 7 +- src/control/cmd/dmg/storage.go | 25 +++-- src/control/cmd/dmg/storage_query.go | 21 ++-- src/control/cmd/dmg/system.go | 67 ++++++------ src/control/cmd/dmg/telemetry.go | 23 ++-- src/control/common/cmdutil/json.go | 92 ++++++++++++++++ src/control/lib/atm/bool.go | 7 ++ src/control/lib/control/network.go | 4 +- .../lib/hardware/hwprov/topology_cmd.go | 14 +-- src/tests/ftest/control/version.py | 70 +++--------- src/tests/ftest/util/daos_utils.py | 7 +- src/tests/ftest/util/dmg_utils.py | 7 +- src/tests/ftest/util/server_utils_base.py | 6 +- 38 files changed, 526 insertions(+), 477 deletions(-) create mode 100644 src/control/build/info.go create mode 100644 src/control/build/string.go create mode 100644 src/control/common/cmdutil/json.go diff --git a/ci/unit/required_packages.sh b/ci/unit/required_packages.sh index 0f3f3068ab18..4275551d8619 100755 --- a/ci/unit/required_packages.sh +++ b/ci/unit/required_packages.sh @@ -12,19 +12,30 @@ elif [[ "$distro" = *8 ]]; then OPENMPI_VER="" PY_MINOR_VER="" fi -pkgs="gotestsum openmpi$OPENMPI_VER \ - hwloc-devel argobots \ - fuse3-libs fuse3 \ - boost-python3$PY_MINOR_VER-devel \ - libisa-l-devel libpmem \ - libpmemobj protobuf-c \ - spdk-devel libfabric-devel \ - pmix numactl-devel \ - libipmctl-devel python3$PY_MINOR_VER-pyxattr \ - python3$PY_MINOR_VER-junit_xml \ - python3$PY_MINOR_VER-tabulate numactl \ - libyaml-devel \ - valgrind-devel patchelf capstone" +pkgs="argobots \ + boost-python3$PY_MINOR_VER-devel \ + capstone \ + fuse3 \ + fuse3-libs \ + gotestsum \ + hwloc-devel \ + libipmctl-devel \ + libisa-l-devel \ + libfabric-devel \ + libpmem \ + libpmemobj \ + libyaml-devel \ + numactl \ + numactl-devel \ + openmpi$OPENMPI_VER \ + patchelf \ + pmix \ + protobuf-c \ + python3$PY_MINOR_VER-junit_xml \ + python3$PY_MINOR_VER-pyxattr \ + python3$PY_MINOR_VER-tabulate \ + spdk-devel \ + valgrind-devel" if $quick_build; then if ! read -r mercury_version < "$distro"-required-mercury-rpm-version; then diff --git a/site_scons/site_tools/go_builder.py b/site_scons/site_tools/go_builder.py index ed7a8fceef0c..6829f688f06f 100644 --- a/site_scons/site_tools/go_builder.py +++ b/site_scons/site_tools/go_builder.py @@ -8,7 +8,7 @@ from SCons.Script import Configure, GetOption, Scanner, Glob, Exit, File GO_COMPILER = 'go' -MIN_GO_VERSION = '1.17.0' +MIN_GO_VERSION = '1.18.0' include_re = re.compile(r'\#include [<"](\S+[>"])', re.M) diff --git a/src/control/SConscript b/src/control/SConscript index fa3209d563f7..a3fe2c802689 100644 --- a/src/control/SConscript +++ b/src/control/SConscript @@ -18,6 +18,8 @@ def get_build_tags(benv): tags.append("firmware") if not is_release_build(benv): tags.append("pprof") + else: + tags.append("release") return f"-tags {','.join(tags)}" diff --git a/src/control/build/info.go b/src/control/build/info.go new file mode 100644 index 000000000000..657074d16592 --- /dev/null +++ b/src/control/build/info.go @@ -0,0 +1,39 @@ +// +// (C) Copyright 2023 Intel Corporation. +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +//go:build go1.18 + +package build + +import ( + "runtime/debug" + "strings" + "time" +) + +func init() { + info, ok := debug.ReadBuildInfo() + if !ok { + return + } + + for _, setting := range info.Settings { + switch setting.Key { + case "vcs": + VCS = setting.Value + case "vcs.revision": + Revision = setting.Value + case "vcs.modified": + DirtyBuild = setting.Value == "true" + case "vcs.time": + LastCommit, _ = time.Parse(time.RFC3339, setting.Value) + case "-tags": + if strings.Contains(setting.Value, "release") { + ReleaseBuild = true + } + } + } +} diff --git a/src/control/build/string.go b/src/control/build/string.go new file mode 100644 index 000000000000..7f8c6a97fdef --- /dev/null +++ b/src/control/build/string.go @@ -0,0 +1,57 @@ +// +// (C) Copyright 2023 Intel Corporation. +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package build + +import ( + "encoding/json" + "fmt" + "strings" +) + +func revString(version string) string { + if ReleaseBuild { + return version + } + + revParts := []string{version} + if Revision != "" { + switch VCS { + case "git": + revParts = append(revParts, fmt.Sprintf("g%7s", Revision)[0:7]) + default: + revParts = append(revParts, Revision) + } + if DirtyBuild { + revParts = append(revParts, "dirty") + } + } + return strings.Join(revParts, "-") +} + +// String returns a string containing the name, version, and for non-release builds, +// the revision of the binary. +func String(name string) string { + return fmt.Sprintf("%s version %s", name, revString(DaosVersion)) +} + +// MarshalJSON returns a JSON string containing a structured representation of +// the binary build info. +func MarshalJSON(name string) ([]byte, error) { + return json.Marshal(&struct { + Name string `json:"name"` + Version string `json:"version"` + Revision string `json:"revision,omitempty"` + Dirty bool `json:"dirty,omitempty"` + Release bool `json:"release,omitempty"` + }{ + Name: name, + Version: DaosVersion, + Revision: Revision, + Dirty: DirtyBuild, + Release: ReleaseBuild, + }) +} diff --git a/src/control/build/variables.go b/src/control/build/variables.go index 738b042c99ea..f915306098fe 100644 --- a/src/control/build/variables.go +++ b/src/control/build/variables.go @@ -1,5 +1,5 @@ // -// (C) Copyright 2020-2021 Intel Corporation. +// (C) Copyright 2020-2023 Intel Corporation. // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -7,6 +7,8 @@ // Package build provides an importable repository of variables set at build time. package build +import "time" + var ( // ConfigDir should be set via linker flag using the value of CONF_DIR. ConfigDir string = "./" @@ -20,10 +22,25 @@ var ( ManagementServiceName = "DAOS Management Service" // AgentName defines a consistent name for the compute node agent. AgentName = "DAOS Agent" + // CLIUtilName defines a consistent name for the daos CLI utility. + CLIUtilName = "DAOS CLI" + // AdminUtilName defines a consistent name for the dmg utility. + AdminUtilName = "DAOS Admin Tool" // DefaultControlPort defines the default control plane listener port. DefaultControlPort = 10001 // DefaultSystemName defines the default DAOS system name. DefaultSystemName = "daos_server" + + // VCS is the version control system used to build the binary. + VCS = "" + // Revision is the VCS revision of the binary. + Revision = "" + // LastCommit is the time of the last commit. + LastCommit time.Time + // ReleaseBuild is true if the binary was built with the release tag. + ReleaseBuild bool + // DirtyBuild is true if the binary was built with uncommitted changes. + DirtyBuild bool ) diff --git a/src/control/cmd/daos/acl.go b/src/control/cmd/daos/acl.go index 33e14ce3fcff..9085d01e0910 100644 --- a/src/control/cmd/daos/acl.go +++ b/src/control/cmd/daos/acl.go @@ -121,12 +121,11 @@ func (cmd *aclCmd) getACL(ap *C.struct_cmd_args_s) (*control.AccessControlList, } func (cmd *aclCmd) outputACL(out io.Writer, acl *control.AccessControlList, verbose bool) error { - if cmd.jsonOutputEnabled() { - cmd.wroteJSON.SetTrue() - return outputJSON(out, acl, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(acl, nil) } - _, err := fmt.Fprintf(out, control.FormatACL(acl, verbose)) + _, err := fmt.Fprint(out, control.FormatACL(acl, verbose)) return err } @@ -400,9 +399,8 @@ func (cmd *containerSetOwnerCmd) Execute(args []string) error { cmd.ContainerID()) } - if cmd.jsonOutputEnabled() { - cmd.wroteJSON.SetTrue() - return outputJSON(os.Stdout, nil, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(nil, nil) } var contID string diff --git a/src/control/cmd/daos/container.go b/src/control/cmd/daos/container.go index a5494dc2ea3f..48a3da8bd097 100644 --- a/src/control/cmd/daos/container.go +++ b/src/control/cmd/daos/container.go @@ -314,8 +314,8 @@ func (cmd *containerCreateCmd) Execute(_ []string) (err error) { ci.ContainerLabel = cmd.Args.Label } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(ci, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(ci, nil) } var bld strings.Builder @@ -750,8 +750,8 @@ func (cmd *containerListCmd) Execute(_ []string) error { "unable to list containers for pool %s", cmd.PoolID()) } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(contIDs, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(contIDs, nil) } var bld strings.Builder @@ -903,7 +903,7 @@ func (cmd *containerListObjectsCmd) Execute(_ []string) error { for i := C.uint32_t(0); i < readOids; i++ { oid := fmt.Sprintf("%d.%d", oidArr[i].hi, oidArr[i].lo) - if !cmd.jsonOutputEnabled() { + if !cmd.JSONOutputEnabled() { cmd.Infof("%s", oid) continue } @@ -911,8 +911,8 @@ func (cmd *containerListObjectsCmd) Execute(_ []string) error { } } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(oids, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(oids, nil) } return nil @@ -1035,8 +1035,8 @@ func (cmd *containerQueryCmd) Execute(_ []string) error { cmd.contUUID) } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(ci, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(ci, nil) } var bld strings.Builder @@ -1080,8 +1080,8 @@ func (cmd *containerCloneCmd) Execute(_ []string) error { return errors.Wrapf(err, "failed to clone container %s", cmd.Source) } - if cmd.shouldEmitJSON { - return cmd.outputJSON(struct { + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(struct { SourcePool string `json:"src_pool"` SourceCont string `json:"src_cont"` DestPool string `json:"dst_pool"` @@ -1160,11 +1160,11 @@ func (cmd *containerListAttrsCmd) Execute(args []string) error { cmd.ContainerID()) } - if cmd.jsonOutputEnabled() { + if cmd.JSONOutputEnabled() { if cmd.Verbose { - return cmd.outputJSON(attrs.asMap(), nil) + return cmd.OutputJSON(attrs.asMap(), nil) } - return cmd.outputJSON(attrs.asList(), nil) + return cmd.OutputJSON(attrs.asList(), nil) } var bld strings.Builder @@ -1258,12 +1258,12 @@ func (cmd *containerGetAttrCmd) Execute(args []string) error { return errors.Wrapf(err, "failed to get attributes from container %s", cmd.ContainerID()) } - if cmd.jsonOutputEnabled() { + if cmd.JSONOutputEnabled() { // Maintain compatibility with older behavior. if len(cmd.Args.Attrs.ParsedProps) == 1 && len(attrs) == 1 { - return cmd.outputJSON(attrs[0], nil) + return cmd.OutputJSON(attrs[0], nil) } - return cmd.outputJSON(attrs, nil) + return cmd.OutputJSON(attrs, nil) } var bld strings.Builder @@ -1398,8 +1398,8 @@ func (cmd *containerGetPropCmd) Execute(args []string) error { } } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(props, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(props, nil) } title := fmt.Sprintf("Properties for container %s", cmd.ContainerID()) diff --git a/src/control/cmd/daos/filesystem.go b/src/control/cmd/daos/filesystem.go index 6e683574152d..1b1ad9e6e594 100644 --- a/src/control/cmd/daos/filesystem.go +++ b/src/control/cmd/daos/filesystem.go @@ -69,14 +69,14 @@ func (cmd *fsCopyCmd) Execute(_ []string) error { return errors.Wrapf(err, "failed to copy %s -> %s", cmd.Source, cmd.Dest) } - if cmd.shouldEmitJSON { + if cmd.JSONOutputEnabled() { type CopyStats struct { NumDirs uint64 `json:"num_dirs"` NumFiles uint64 `json:"num_files"` NumLinks uint64 `json:"num_links"` } - return cmd.outputJSON(struct { + return cmd.OutputJSON(struct { SourcePool string `json:"src_pool"` SourceCont string `json:"src_cont"` DestPool string `json:"dst_pool"` @@ -262,7 +262,7 @@ func (cmd *fsGetAttrCmd) Execute(_ []string) error { var oclassName [16]C.char C.daos_oclass_id2name(attrs.doi_oclass_id, &oclassName[0]) - if cmd.jsonOutputEnabled() { + if cmd.JSONOutputEnabled() { jsonAttrs := &struct { ObjClass string `json:"oclass"` ChunkSize uint64 `json:"chunk_size"` @@ -270,7 +270,7 @@ func (cmd *fsGetAttrCmd) Execute(_ []string) error { ObjClass: C.GoString(&oclassName[0]), ChunkSize: uint64(attrs.doi_chunk_size), } - return cmd.outputJSON(jsonAttrs, nil) + return cmd.OutputJSON(jsonAttrs, nil) } cmd.Infof("Object Class = %s", C.GoString(&oclassName[0])) diff --git a/src/control/cmd/daos/main.go b/src/control/cmd/daos/main.go index 0c33a7911669..377aa22ad1eb 100644 --- a/src/control/cmd/daos/main.go +++ b/src/control/cmd/daos/main.go @@ -9,7 +9,6 @@ package main import ( "encoding/json" "fmt" - "io" "os" "path" "runtime/debug" @@ -21,85 +20,9 @@ import ( "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/fault" "github.com/daos-stack/daos/src/control/lib/atm" - "github.com/daos-stack/daos/src/control/lib/daos" "github.com/daos-stack/daos/src/control/logging" ) -type ( - jsonOutputter interface { - enableJsonOutput(bool, io.Writer, *atm.Bool) - jsonOutputEnabled() bool - outputJSON(interface{}, error) error - errorJSON(error) error - } - - jsonOutputCmd struct { - wroteJSON *atm.Bool - writer io.Writer - shouldEmitJSON bool - } -) - -func (cmd *jsonOutputCmd) enableJsonOutput(emitJson bool, w io.Writer, wj *atm.Bool) { - cmd.shouldEmitJSON = emitJson - cmd.writer = w - cmd.wroteJSON = wj -} - -func (cmd *jsonOutputCmd) jsonOutputEnabled() bool { - return cmd.shouldEmitJSON -} - -func outputJSON(out io.Writer, in interface{}, cmdErr error) error { - status := 0 - var errStr *string - if cmdErr != nil { - errStr = func() *string { str := cmdErr.Error(); return &str }() - if s, ok := errors.Cause(cmdErr).(daos.Status); ok { - status = int(s) - } else { - status = int(daos.MiscError) - } - } - - data, err := json.MarshalIndent(struct { - Response interface{} `json:"response"` - Error *string `json:"error"` - Status int `json:"status"` - }{in, errStr, status}, "", " ") - if err != nil { - return err - } - - if _, err = out.Write(append(data, []byte("\n")...)); err != nil { - return err - } - - return cmdErr -} - -func (cmd *jsonOutputCmd) outputJSON(in interface{}, cmdErr error) error { - if cmd.wroteJSON.IsTrue() { - return cmdErr - } - cmd.wroteJSON.SetTrue() - return outputJSON(cmd.writer, in, cmdErr) -} - -func errorJSON(err error) error { - return outputJSON(os.Stdout, nil, err) -} - -func (cmd *jsonOutputCmd) errorJSON(err error) error { - return cmd.outputJSON(nil, err) -} - -var _ jsonOutputter = (*jsonOutputCmd)(nil) - -type cmdLogger interface { - setLog(*logging.LeveledLogger) -} - type cliOptions struct { Debug bool `long:"debug" description:"enable debug output"` Verbose bool `long:"verbose" description:"enable verbose output (when applicable)"` @@ -113,10 +36,20 @@ type cliOptions struct { ManPage cmdutil.ManCmd `command:"manpage" hidden:"true"` } -type versionCmd struct{} +type versionCmd struct { + cmdutil.JSONOutputCmd +} func (cmd *versionCmd) Execute(_ []string) error { - fmt.Printf("daos version %s, libdaos %s\n", build.DaosVersion, apiVersion()) + if cmd.JSONOutputEnabled() { + buf, err := build.MarshalJSON(build.CLIUtilName) + if err != nil { + return err + } + return cmd.OutputJSON(json.RawMessage(buf), nil) + } + + fmt.Printf("%s, libdaos v%s\n", build.String(build.CLIUtilName), apiVersion()) os.Exit(0) return nil } @@ -161,12 +94,10 @@ or query/manage an object inside a container.` log.Debug("debug output enabled") } - if jsonCmd, ok := cmd.(jsonOutputter); ok { - jsonCmd.enableJsonOutput(opts.JSON, os.Stdout, &wroteJSON) - if opts.JSON { - // disable output on stdout other than JSON - log.ClearLevel(logging.LogLevelInfo) - } + if jsonCmd, ok := cmd.(cmdutil.JSONOutputter); ok && opts.JSON { + jsonCmd.EnableJSONOutput(os.Stdout, &wroteJSON) + // disable output on stdout other than JSON + log.ClearLevel(logging.LogLevelInfo) } if logCmd, ok := cmd.(cmdutil.LogSetter); ok { @@ -232,7 +163,7 @@ or query/manage an object inside a container.` _, err = p.ParseArgs(args) if opts.JSON && wroteJSON.IsFalse() { - return errorJSON(err) + return cmdutil.OutputJSON(os.Stdout, nil, err) } return err } diff --git a/src/control/cmd/daos/object.go b/src/control/cmd/daos/object.go index 542edd894d31..79d143379170 100644 --- a/src/control/cmd/daos/object.go +++ b/src/control/cmd/daos/object.go @@ -149,8 +149,8 @@ func (cmd *objQueryCmd) Execute(_ []string) error { defer C.daos_obj_layout_free(cLayout) layout := newObjLayout(oid, cLayout) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(layout, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(layout, nil) } // TODO: Revisit this output to make it more non-developer friendly. diff --git a/src/control/cmd/daos/pool.go b/src/control/cmd/daos/pool.go index b06b8e47288a..aab4fb9c4a7a 100644 --- a/src/control/cmd/daos/pool.go +++ b/src/control/cmd/daos/pool.go @@ -338,8 +338,8 @@ func (cmd *poolQueryCmd) Execute(_ []string) error { } } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(pqr, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(pqr, nil) } var bld strings.Builder @@ -410,8 +410,8 @@ func (cmd *poolQueryTargetsCmd) Execute(_ []string) error { } } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(infoResp, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(infoResp, nil) } var bld strings.Builder @@ -443,11 +443,11 @@ func (cmd *poolListAttrsCmd) Execute(_ []string) error { "failed to list attributes for pool %s", cmd.poolUUID) } - if cmd.jsonOutputEnabled() { + if cmd.JSONOutputEnabled() { if cmd.Verbose { - return cmd.outputJSON(attrs.asMap(), nil) + return cmd.OutputJSON(attrs.asMap(), nil) } - return cmd.outputJSON(attrs.asList(), nil) + return cmd.OutputJSON(attrs.asList(), nil) } var bld strings.Builder @@ -484,12 +484,12 @@ func (cmd *poolGetAttrCmd) Execute(_ []string) error { return errors.Wrapf(err, "failed to get attributes for pool %s", cmd.PoolID()) } - if cmd.jsonOutputEnabled() { + if cmd.JSONOutputEnabled() { // Maintain compatibility with older behavior. if len(cmd.Args.Attrs.ParsedProps) == 1 && len(attrs) == 1 { - return cmd.outputJSON(attrs[0], nil) + return cmd.OutputJSON(attrs[0], nil) } - return cmd.outputJSON(attrs, nil) + return cmd.OutputJSON(attrs, nil) } var bld strings.Builder diff --git a/src/control/cmd/daos/snapshot.go b/src/control/cmd/daos/snapshot.go index 6e5bda3d0be4..f06e517e1175 100644 --- a/src/control/cmd/daos/snapshot.go +++ b/src/control/cmd/daos/snapshot.go @@ -56,8 +56,8 @@ func (cmd *containerSnapCreateCmd) Execute(args []string) error { return errors.Wrapf(err, "failed to create snapshot of container %s", cmd.ContainerID()) } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(snapshot{ + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(snapshot{ Epoch: uint64(cEpoch), Timestamp: common.FormatTime(daos.HLC(cEpoch).ToTime()), Name: cmd.Name, @@ -229,8 +229,8 @@ func (cmd *containerSnapListCmd) Execute(args []string) error { return err } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(snaps, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(snaps, nil) } cmd.Info("Container's snapshots :") diff --git a/src/control/cmd/daos/system.go b/src/control/cmd/daos/system.go index 9d8008173ead..8856c285214c 100644 --- a/src/control/cmd/daos/system.go +++ b/src/control/cmd/daos/system.go @@ -56,8 +56,8 @@ func (cmd *systemQueryCmd) Execute(_ []string) error { }) } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(sysInfo, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(sysInfo, nil) } cmd.Infof("connected to DAOS system:") diff --git a/src/control/cmd/daos/util.go b/src/control/cmd/daos/util.go index b5ebb7d90e0d..4297fdd32eb9 100644 --- a/src/control/cmd/daos/util.go +++ b/src/control/cmd/daos/util.go @@ -253,7 +253,7 @@ type daosCaller interface { type daosCmd struct { cmdutil.NoArgsCmd - jsonOutputCmd + cmdutil.JSONOutputCmd cmdutil.LogCmd } diff --git a/src/control/cmd/daos_agent/attachinfo.go b/src/control/cmd/daos_agent/attachinfo.go index 50aedd33daa4..76e42e53974c 100644 --- a/src/control/cmd/daos_agent/attachinfo.go +++ b/src/control/cmd/daos_agent/attachinfo.go @@ -8,12 +8,12 @@ package main import ( "context" - "encoding/json" "fmt" "os" "github.com/pkg/errors" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" "github.com/daos-stack/daos/src/control/lib/txtfmt" ) @@ -21,8 +21,8 @@ import ( type dumpAttachInfoCmd struct { configCmd ctlInvokerCmd + cmdutil.JSONOutputCmd Output string `short:"o" long:"output" default:"stdout" description:"Dump output to this location"` - JSON bool `short:"j" long:"json" description:"Enable JSON output"` } func (cmd *dumpAttachInfoCmd) Execute(_ []string) error { @@ -46,14 +46,8 @@ func (cmd *dumpAttachInfoCmd) Execute(_ []string) error { return errors.Wrap(err, "GetAttachInfo failed") } - if cmd.JSON { - data, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return err - } - - _, err = out.Write(append(data, []byte("\n")...)) - return err + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } system := cmd.cfg.SystemName diff --git a/src/control/cmd/daos_agent/main.go b/src/control/cmd/daos_agent/main.go index d81abfee9c74..f8e5664936b6 100644 --- a/src/control/cmd/daos_agent/main.go +++ b/src/control/cmd/daos_agent/main.go @@ -9,7 +9,6 @@ package main import ( "encoding/json" "fmt" - "io" "os" "path" @@ -19,6 +18,7 @@ import ( "github.com/daos-stack/daos/src/control/build" "github.com/daos-stack/daos/src/control/common" "github.com/daos-stack/daos/src/control/common/cmdutil" + "github.com/daos-stack/daos/src/control/lib/atm" "github.com/daos-stack/daos/src/control/lib/control" "github.com/daos-stack/daos/src/control/lib/hardware/hwprov" "github.com/daos-stack/daos/src/control/logging" @@ -68,43 +68,23 @@ func (cmd *configCmd) setConfig(cfg *Config) { cmd.cfg = cfg } -type ( - jsonOutputter interface { - enableJsonOutput(bool) - jsonOutputEnabled() bool - outputJSON(io.Writer, interface{}) error - } - - jsonOutputCmd struct { - shouldEmitJSON bool - } -) - -func (cmd *jsonOutputCmd) enableJsonOutput(emitJson bool) { - cmd.shouldEmitJSON = emitJson +func versionString() string { + return build.String(build.AgentName) } -func (cmd *jsonOutputCmd) jsonOutputEnabled() bool { - return cmd.shouldEmitJSON +type versionCmd struct { + cmdutil.JSONOutputCmd } -func (cmd *jsonOutputCmd) outputJSON(out io.Writer, in interface{}) error { - data, err := json.MarshalIndent(in, "", " ") - if err != nil { - return err +func (cmd *versionCmd) Execute(_ []string) error { + if cmd.JSONOutputEnabled() { + buf, err := build.MarshalJSON(build.AgentName) + if err != nil { + return err + } + return cmd.OutputJSON(json.RawMessage(buf), nil) } - _, err = out.Write(append(data, []byte("\n")...)) - return err -} - -func versionString() string { - return fmt.Sprintf("%s v%s", build.AgentName, build.DaosVersion) -} - -type versionCmd struct{} - -func (cmd *versionCmd) Execute(_ []string) error { _, err := fmt.Println(versionString()) return err } @@ -115,10 +95,10 @@ func exitWithError(log logging.Logger, err error) { } func parseOpts(args []string, opts *cliOptions, invoker control.Invoker, log *logging.LeveledLogger) error { + var wroteJSON atm.Bool p := flags.NewParser(opts, flags.Default) p.Options ^= flags.PrintErrors // Don't allow the library to print errors p.SubcommandsOptional = true - p.CommandHandler = func(cmd flags.Commander, args []string) error { if len(args) > 0 { exitWithError(log, errors.Errorf("unknown command %q", args[0])) @@ -132,8 +112,10 @@ func parseOpts(args []string, opts *cliOptions, invoker control.Invoker, log *lo logCmd.SetLog(log) } - if jsonCmd, ok := cmd.(jsonOutputter); ok { - jsonCmd.enableJsonOutput(opts.JSON) + if jsonCmd, ok := cmd.(cmdutil.JSONOutputter); ok && opts.JSON { + jsonCmd.EnableJSONOutput(os.Stdout, &wroteJSON) + // disable output on stdout other than JSON + log.ClearLevel(logging.LogLevelInfo) } if opts.Debug { @@ -233,11 +215,12 @@ func parseOpts(args []string, opts *cliOptions, invoker control.Invoker, log *lo ctlCmd.setInvoker(invoker) } - if err := cmd.Execute(args); err != nil { - return err + err = cmd.Execute(args) + if opts.JSON && wroteJSON.IsFalse() { + cmdutil.OutputJSON(os.Stdout, nil, err) } - return nil + return err } _, err := p.Parse() diff --git a/src/control/cmd/daos_agent/network.go b/src/control/cmd/daos_agent/network.go index 2c5b3a55dde7..42c3407ca49a 100644 --- a/src/control/cmd/daos_agent/network.go +++ b/src/control/cmd/daos_agent/network.go @@ -8,7 +8,6 @@ package main import ( "context" - "os" "strings" "github.com/daos-stack/daos/src/control/cmd/dmg/pretty" @@ -21,17 +20,10 @@ import ( type netScanCmd struct { cmdutil.LogCmd - jsonOutputCmd + cmdutil.JSONOutputCmd FabricProvider string `short:"p" long:"provider" description:"Filter device list to those that support the given OFI provider or 'all' for all available (default is all local providers)"` } -func (cmd *netScanCmd) printUnlessJson(fmtStr string, args ...interface{}) { - if cmd.jsonOutputEnabled() { - return - } - cmd.Infof(fmtStr, args...) -} - func (cmd *netScanCmd) Execute(_ []string) error { var prov string if !strings.EqualFold(cmd.FabricProvider, "all") { @@ -51,8 +43,8 @@ func (cmd *netScanCmd) Execute(_ []string) error { return err } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(os.Stdout, hfm) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(hfm, nil) } var bld strings.Builder diff --git a/src/control/cmd/daos_server/main.go b/src/control/cmd/daos_server/main.go index 62056936293f..0de6f55bd6b1 100644 --- a/src/control/cmd/daos_server/main.go +++ b/src/control/cmd/daos_server/main.go @@ -8,6 +8,7 @@ package main import ( "context" + "encoding/json" "fmt" "os" "path" @@ -19,6 +20,7 @@ import ( "github.com/daos-stack/daos/src/control/common" "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/fault" + "github.com/daos-stack/daos/src/control/lib/atm" "github.com/daos-stack/daos/src/control/lib/hardware/hwprov" "github.com/daos-stack/daos/src/control/logging" "github.com/daos-stack/daos/src/control/pbin" @@ -35,6 +37,7 @@ type mainOpts struct { // TODO(DAOS-3129): This should be -d, but it conflicts with the start // subcommand's -d flag when we default to running it. Debug bool `short:"b" long:"debug" description:"Enable debug output"` + JSON bool `long:"json" short:"j" description:"enable JSON output"` JSONLog bool `short:"J" long:"json-logging" description:"Enable JSON-formatted log output"` Syslog bool `long:"syslog" description:"Enable logging to syslog"` @@ -53,10 +56,20 @@ type mainOpts struct { preExecTests []execTestFn } -type versionCmd struct{} +type versionCmd struct { + cmdutil.JSONOutputCmd +} func (cmd *versionCmd) Execute(_ []string) error { - fmt.Printf("%s v%s\n", build.ControlPlaneName, build.DaosVersion) + if cmd.JSONOutputEnabled() { + buf, err := build.MarshalJSON(build.ControlPlaneName) + if err != nil { + return err + } + return cmd.OutputJSON(json.RawMessage(buf), nil) + } + + fmt.Println(build.String(build.ControlPlaneName)) return nil } @@ -70,6 +83,7 @@ func exitWithError(log *logging.LeveledLogger, err error) { } func parseOpts(args []string, opts *mainOpts, log *logging.LeveledLogger) error { + var wroteJSON atm.Bool p := flags.NewParser(opts, flags.HelpFlag|flags.PassDoubleDash) p.SubcommandsOptional = false p.CommandHandler = func(cmd flags.Commander, cmdArgs []string) error { @@ -78,6 +92,12 @@ func parseOpts(args []string, opts *mainOpts, log *logging.LeveledLogger) error return errors.Errorf("unexpected commandline arguments: %v", cmdArgs) } + if jsonCmd, ok := cmd.(cmdutil.JSONOutputter); ok && opts.JSON { + jsonCmd.EnableJSONOutput(os.Stdout, &wroteJSON) + // disable output on stdout other than JSON + log.ClearLevel(logging.LogLevelInfo) + } + switch cmd.(type) { case *versionCmd: // No pre-exec tests or setup needed for these commands; just @@ -143,11 +163,11 @@ func parseOpts(args []string, opts *mainOpts, log *logging.LeveledLogger) error // Parse commandline flags which override options loaded from config. _, err := p.ParseArgs(args) - if err != nil { - return err + if opts.JSON && wroteJSON.IsFalse() { + cmdutil.OutputJSON(os.Stdout, nil, err) } - return nil + return err } func main() { diff --git a/src/control/cmd/dmg/auto.go b/src/control/cmd/dmg/auto.go index 98a7d9973ff8..ccfe54de575b 100644 --- a/src/control/cmd/dmg/auto.go +++ b/src/control/cmd/dmg/auto.go @@ -14,6 +14,7 @@ import ( "gopkg.in/yaml.v2" "github.com/daos-stack/daos/src/control/cmd/dmg/pretty" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" "github.com/daos-stack/daos/src/control/lib/hardware" "github.com/daos-stack/daos/src/control/server/config" @@ -32,7 +33,7 @@ type configGenCmd struct { cfgCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd AccessPoints string `default:"localhost" short:"a" long:"access-points" description:"Comma separated list of access point addresses "` NrEngines int `short:"e" long:"num-engines" description:"Set the number of DAOS Engine sections to be populated in the config file output. If unset then the value will be set to the number of NUMA nodes on storage hosts in the DAOS system."` @@ -87,8 +88,8 @@ func (cmd *configGenCmd) confGen(ctx context.Context) (*config.Server, error) { req.HostList = hl // TODO: decide whether we want meaningful JSON output - if cmd.jsonOutputEnabled() { - return nil, cmd.outputJSON(nil, errors.New("JSON output not supported")) + if cmd.JSONOutputEnabled() { + return nil, cmd.OutputJSON(nil, errors.New("JSON output not supported")) } cmd.Debugf("control API ConfGenerateRemote called with req: %+v", req) diff --git a/src/control/cmd/dmg/cont.go b/src/control/cmd/dmg/cont.go index 4e2aba52e2ba..5ad087b0676f 100644 --- a/src/control/cmd/dmg/cont.go +++ b/src/control/cmd/dmg/cont.go @@ -12,6 +12,7 @@ import ( "github.com/jessevdk/go-flags" "github.com/pkg/errors" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" "github.com/daos-stack/daos/src/control/lib/ui" ) @@ -25,7 +26,7 @@ type ContCmd struct { type ContSetOwnerCmd struct { baseCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd GroupName ui.ACLPrincipalFlag `short:"g" long:"group" description:"New owner-group for the container, format name@domain"` UserName ui.ACLPrincipalFlag `short:"u" long:"user" description:"New owner-user for the container, format name@domain"` ContUUID string `short:"c" long:"cont" required:"1" description:"UUID of the DAOS container"` @@ -54,6 +55,10 @@ func (c *ContSetOwnerCmd) Execute(args []string) error { msg = errors.WithMessage(err, "FAILED").Error() } + if c.JSONOutputEnabled() { + return c.OutputJSON(nil, err) + } + c.Infof("Container-set-owner command %s\n", msg) return err diff --git a/src/control/cmd/dmg/firmware.go b/src/control/cmd/dmg/firmware.go index c7baa1eef662..16748d37b32e 100644 --- a/src/control/cmd/dmg/firmware.go +++ b/src/control/cmd/dmg/firmware.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/daos-stack/daos/src/control/cmd/dmg/pretty" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" ) @@ -31,7 +32,7 @@ type firmwareQueryCmd struct { baseCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd DeviceType string `short:"t" long:"type" choice:"nvme" choice:"scm" choice:"all" default:"all" description:"Type of storage devices to query"` Devices string `short:"d" long:"devices" description:"Comma-separated list of device identifiers to query"` ModelID string `short:"m" long:"model" description:"Model ID to filter results by"` @@ -57,8 +58,8 @@ func (cmd *firmwareQueryCmd) Execute(args []string) error { req.SetHostList(cmd.getHostList()) resp, err := control.FirmwareQuery(ctx, cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -115,7 +116,7 @@ type firmwareUpdateCmd struct { baseCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd DeviceType string `short:"t" long:"type" choice:"nvme" choice:"scm" required:"1" description:"Type of storage devices to update"` FilePath string `short:"p" long:"path" required:"1" description:"Path to the firmware file accessible from all nodes"` Devices string `short:"d" long:"devices" description:"Comma-separated list of device identifiers to update"` @@ -147,8 +148,8 @@ func (cmd *firmwareUpdateCmd) Execute(args []string) error { req.SetHostList(cmd.getHostList()) resp, err := control.FirmwareUpdate(ctx, cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { diff --git a/src/control/cmd/dmg/main.go b/src/control/cmd/dmg/main.go index eec44cbb4712..6ba39eded6de 100644 --- a/src/control/cmd/dmg/main.go +++ b/src/control/cmd/dmg/main.go @@ -9,7 +9,6 @@ package main import ( "encoding/json" "fmt" - "io" "os" "path" @@ -22,7 +21,6 @@ import ( "github.com/daos-stack/daos/src/control/fault" "github.com/daos-stack/daos/src/control/lib/atm" "github.com/daos-stack/daos/src/control/lib/control" - "github.com/daos-stack/daos/src/control/lib/daos" "github.com/daos-stack/daos/src/control/lib/hostlist" "github.com/daos-stack/daos/src/control/lib/ui" "github.com/daos-stack/daos/src/control/logging" @@ -54,19 +52,6 @@ type ( ctlInvokerCmd struct { ctlInvoker control.Invoker } - - jsonOutputter interface { - enableJsonOutput(bool, io.Writer, *atm.Bool) - jsonOutputEnabled() bool - outputJSON(interface{}, error) error - errorJSON(error) error - } - - jsonOutputCmd struct { - wroteJSON *atm.Bool - writer io.Writer - shouldEmitJSON bool - } ) func (cmd *ctlInvokerCmd) setInvoker(c control.Invoker) { @@ -99,65 +84,6 @@ func (cmd *singleHostCmd) setHostList(newList *hostlist.HostSet) { cmd.HostList.Replace(newList) } -func (cmd *jsonOutputCmd) enableJsonOutput(emitJson bool, w io.Writer, wj *atm.Bool) { - cmd.shouldEmitJSON = emitJson - cmd.writer = w - cmd.wroteJSON = wj -} - -func (cmd *jsonOutputCmd) jsonOutputEnabled() bool { - return cmd.shouldEmitJSON -} - -func outputJSON(out io.Writer, in interface{}, cmdErr error) error { - status := 0 - var errStr *string - if cmdErr != nil { - errStr = new(string) - *errStr = cmdErr.Error() - if s, ok := errors.Cause(cmdErr).(daos.Status); ok { - status = int(s) - } else { - status = int(daos.MiscError) - } - } - - data, err := json.MarshalIndent(struct { - Response interface{} `json:"response"` - Error *string `json:"error"` - Status int `json:"status"` - }{in, errStr, status}, "", " ") - if err != nil { - fmt.Fprintf(out, "unable to marshal json: %s\n", err.Error()) - return err - } - - if _, err = out.Write(append(data, []byte("\n")...)); err != nil { - fmt.Fprintf(out, "unable to write json: %s\n", err.Error()) - return err - } - - return cmdErr -} - -func (cmd *jsonOutputCmd) outputJSON(in interface{}, cmdErr error) error { - if cmd.wroteJSON.IsTrue() { - return cmdErr - } - cmd.wroteJSON.SetTrue() - return outputJSON(cmd.writer, in, cmdErr) -} - -func errorJSON(err error) error { - return outputJSON(os.Stdout, nil, err) -} - -func (cmd *jsonOutputCmd) errorJSON(err error) error { - return cmd.outputJSON(nil, err) -} - -var _ jsonOutputter = (*jsonOutputCmd)(nil) - type cmdLogger interface { setLog(*logging.LeveledLogger) } @@ -204,10 +130,20 @@ type cliOptions struct { ManPage cmdutil.ManCmd `command:"manpage" hidden:"true"` } -type versionCmd struct{} +type versionCmd struct { + cmdutil.JSONOutputCmd +} func (cmd *versionCmd) Execute(_ []string) error { - fmt.Printf("dmg version %s\n", build.DaosVersion) + if cmd.JSONOutputEnabled() { + buf, err := build.MarshalJSON(build.AdminUtilName) + if err != nil { + return err + } + return cmd.OutputJSON(json.RawMessage(buf), nil) + } + + fmt.Println(build.String(build.AdminUtilName)) os.Exit(0) return nil } @@ -276,12 +212,10 @@ and access control settings, along with system wide operations.` log.WithJSONOutput() } - if jsonCmd, ok := cmd.(jsonOutputter); ok { - jsonCmd.enableJsonOutput(opts.JSON, os.Stdout, &wroteJSON) - if opts.JSON { - // disable output on stdout other than JSON - log.ClearLevel(logging.LogLevelInfo) - } + if jsonCmd, ok := cmd.(cmdutil.JSONOutputter); ok && opts.JSON { + jsonCmd.EnableJSONOutput(os.Stdout, &wroteJSON) + // disable output on stdout other than JSON + log.ClearLevel(logging.LogLevelInfo) } if logCmd, ok := cmd.(cmdutil.LogSetter); ok { @@ -350,7 +284,7 @@ and access control settings, along with system wide operations.` _, err := p.ParseArgs(args) if opts.JSON && wroteJSON.IsFalse() { - return errorJSON(err) + return cmdutil.OutputJSON(os.Stdout, nil, err) } return err } diff --git a/src/control/cmd/dmg/network.go b/src/control/cmd/dmg/network.go index 219c67e4dd77..e55ddadb6356 100644 --- a/src/control/cmd/dmg/network.go +++ b/src/control/cmd/dmg/network.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/daos-stack/daos/src/control/cmd/dmg/pretty" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" ) @@ -26,7 +27,7 @@ type networkScanCmd struct { cfgCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd FabricProvider string `short:"p" long:"provider" description:"Filter device list to those that support the given OFI provider or 'all' for all available (default is the provider specified in daos_server.yml)"` } @@ -42,8 +43,8 @@ func (cmd *networkScanCmd) Execute(_ []string) error { resp, err := control.NetworkScan(ctx, cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { diff --git a/src/control/cmd/dmg/pool.go b/src/control/cmd/dmg/pool.go index f33f2f335dbd..c229b40256ff 100644 --- a/src/control/cmd/dmg/pool.go +++ b/src/control/cmd/dmg/pool.go @@ -19,6 +19,7 @@ import ( "github.com/daos-stack/daos/src/control/cmd/dmg/pretty" "github.com/daos-stack/daos/src/control/common" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" "github.com/daos-stack/daos/src/control/lib/ranklist" "github.com/daos-stack/daos/src/control/lib/ui" @@ -187,7 +188,7 @@ type PoolCreateCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd GroupName ui.ACLPrincipalFlag `short:"g" long:"group" description:"DAOS pool to be owned by given group, format name@domain"` UserName ui.ACLPrincipalFlag `short:"u" long:"user" description:"DAOS pool to be owned by given user, format name@domain"` Properties PoolSetPropsFlag `short:"P" long:"properties" description:"Pool properties to be set"` @@ -309,8 +310,8 @@ func (cmd *PoolCreateCmd) Execute(args []string) error { resp, err := control.PoolCreate(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -355,7 +356,7 @@ type PoolListCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Verbose bool `short:"v" long:"verbose" description:"Add pool UUIDs and service replica lists to display"` NoQuery bool `short:"n" long:"no-query" description:"Disable query of listed pools"` } @@ -379,8 +380,8 @@ func (cmd *PoolListCmd) Execute(_ []string) (errOut error) { return err // control api returned an error, disregard response } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, nil) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, nil) } var out, outErr strings.Builder @@ -406,7 +407,7 @@ type poolCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Args struct { Pool PoolID `positional-arg-name:"" required:"1"` @@ -598,8 +599,8 @@ func (cmd *PoolQueryCmd) Execute(args []string) error { resp, err := control.PoolQuery(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -638,8 +639,8 @@ func (cmd *PoolQueryTargetsCmd) Execute(args []string) error { resp, err := control.PoolQueryTargets(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -703,8 +704,8 @@ func (cmd *PoolSetPropCmd) Execute(_ []string) error { } err := control.PoolSetProp(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(nil, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(nil, err) } if err != nil { @@ -731,8 +732,8 @@ func (cmd *PoolGetPropCmd) Execute(_ []string) error { } resp, err := control.PoolGetProp(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -760,8 +761,8 @@ func (cmd *PoolGetACLCmd) Execute(args []string) error { req := &control.PoolGetACLReq{ID: cmd.PoolID().String()} resp, err := control.PoolGetACL(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -830,8 +831,8 @@ func (cmd *PoolOverwriteACLCmd) Execute(args []string) error { } resp, err := control.PoolOverwriteACL(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -878,8 +879,8 @@ func (cmd *PoolUpdateACLCmd) Execute(args []string) error { } resp, err := control.PoolUpdateACL(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -908,8 +909,8 @@ func (cmd *PoolDeleteACLCmd) Execute(args []string) error { } resp, err := control.PoolDeleteACL(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { diff --git a/src/control/cmd/dmg/server.go b/src/control/cmd/dmg/server.go index 463f99877b6e..aa4a6e91f220 100644 --- a/src/control/cmd/dmg/server.go +++ b/src/control/cmd/dmg/server.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/daos-stack/daos/src/control/cmd/dmg/pretty" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" ) @@ -27,7 +28,7 @@ type serverSetLogMasksCmd struct { baseCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Masks *string `short:"m" long:"masks" description:"Set log masks for a set of facilities to a given level. The input string should look like PREFIX1=LEVEL1,PREFIX2=LEVEL2,... where the syntax is identical to what is expected by 'D_LOG_MASK' environment variable. If the 'PREFIX=' part is omitted, then the level applies to all defined facilities (e.g. a value of 'WARN' sets everything to WARN). If unset then reset engine log masks to use the 'log_mask' value set in the server config file (for each engine) at the time of DAOS system format. Supported levels are FATAL, CRIT, ERR, WARN, NOTE, INFO, DEBUG"` Streams *string `short:"d" long:"streams" description:"Employ finer grained control over debug streams. Mask bits are set as the first argument passed in D_DEBUG(mask, ...) and this input string (DD_MASK) can be set to enable different debug streams. The expected syntax is a comma separated list of stream identifiers and accepted DAOS Debug Streams are md,pl,mgmt,epc,df,rebuild,daos_default and Common Debug Streams (GURT) are any,trace,mem,net,io. If not set, streams will be read from server config file and if set to an empty string then all debug streams will be enabled"` Subsystems *string `short:"s" long:"subsystems" description:"This input string is equivalent to the use of the DD_SUBSYS environment variable and can be set to enable logging for specific subsystems or facilities. The expected syntax is a comma separated list of facility identifiers. Accepted DAOS facilities are common,tree,vos,client,server,rdb,pool,container,object,placement,rebuild,tier,mgmt,bio,tests, Common facilities (GURT) are MISC,MEM and CaRT facilities RPC,BULK,CORPC,GRP,LM,HG,ST,IV If not set, subsystems to enable will be read from server config file and if set to an empty string then logging all subsystems will be enabled"` @@ -55,8 +56,8 @@ func (cmd *serverSetLogMasksCmd) Execute(_ []string) (errOut error) { cmd.Debugf("set log masks response: %+v", resp) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, resp.Errors()) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, resp.Errors()) } var out, outErr strings.Builder diff --git a/src/control/cmd/dmg/storage.go b/src/control/cmd/dmg/storage.go index 818bc59ae35e..fea3160f74ca 100644 --- a/src/control/cmd/dmg/storage.go +++ b/src/control/cmd/dmg/storage.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/daos-stack/daos/src/control/cmd/dmg/pretty" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" ) @@ -33,7 +34,7 @@ type storageScanCmd struct { baseCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Verbose bool `short:"v" long:"verbose" description:"List SCM & NVMe device details"` NvmeHealth bool `short:"n" long:"nvme-health" description:"Display NVMe device health statistics"` NvmeMeta bool `short:"m" long:"nvme-meta" description:"Display server meta data held on NVMe storage"` @@ -67,8 +68,8 @@ func (cmd *storageScanCmd) Execute(_ []string) error { cmd.Debugf("storage scan response: %+v", resp.HostStorage) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, resp.Errors()) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, resp.Errors()) } var outErr strings.Builder @@ -105,7 +106,7 @@ type storageFormatCmd struct { baseCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Verbose bool `short:"v" long:"verbose" description:"Show results of each SCM & NVMe device format operation"` Force bool `long:"force" description:"Force storage format on a host, stopping any running engines (CAUTION: destructive operation)"` } @@ -124,8 +125,8 @@ func (cmd *storageFormatCmd) Execute(args []string) (err error) { return err } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, resp.Errors()) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, resp.Errors()) } return cmd.printFormatResp(resp) @@ -155,7 +156,7 @@ type nvmeRebindCmd struct { baseCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd PCIAddr string `short:"a" long:"pci-address" required:"1" description:"NVMe SSD PCI address to rebind."` } @@ -177,8 +178,8 @@ func (cmd *nvmeRebindCmd) Execute(args []string) error { return err } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, resp.Errors()) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, resp.Errors()) } var outErr strings.Builder @@ -202,7 +203,7 @@ type nvmeAddDeviceCmd struct { baseCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd PCIAddr string `short:"a" long:"pci-address" required:"1" description:"NVMe SSD PCI address to add."` EngineIndex uint32 `short:"e" long:"engine-index" required:"1" description:"Index of DAOS engine to add NVMe device to."` StorageTierIndex int32 `short:"t" long:"tier-index" default:"-1" description:"Index of storage tier on DAOS engine to add NVMe device to."` @@ -231,8 +232,8 @@ func (cmd *nvmeAddDeviceCmd) Execute(args []string) error { return err } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, resp.Errors()) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, resp.Errors()) } var outErr strings.Builder diff --git a/src/control/cmd/dmg/storage_query.go b/src/control/cmd/dmg/storage_query.go index 86e179f06443..4f30b1b5af9c 100644 --- a/src/control/cmd/dmg/storage_query.go +++ b/src/control/cmd/dmg/storage_query.go @@ -14,6 +14,7 @@ import ( "github.com/daos-stack/daos/src/control/cmd/dmg/pretty" "github.com/daos-stack/daos/src/control/common" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" "github.com/daos-stack/daos/src/control/lib/ranklist" ) @@ -33,15 +34,15 @@ type smdQueryCmd struct { baseCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd } func (cmd *smdQueryCmd) makeRequest(ctx context.Context, req *control.SmdQueryReq, opts ...pretty.PrintConfigOption) error { req.SetHostList(cmd.getHostList()) resp, err := control.SmdQuery(ctx, cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -127,7 +128,7 @@ type usageQueryCmd struct { baseCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd } // Execute is run when usageQueryCmd activates. @@ -139,8 +140,8 @@ func (cmd *usageQueryCmd) Execute(_ []string) error { req.SetHostList(cmd.getHostList()) resp, err := control.StorageScan(ctx, cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -165,15 +166,15 @@ type smdManageCmd struct { baseCmd ctlInvokerCmd hostListCmd - jsonOutputCmd + cmdutil.JSONOutputCmd } func (cmd *smdManageCmd) makeRequest(ctx context.Context, req *control.SmdManageReq, opts ...pretty.PrintConfigOption) error { req.SetHostList(cmd.getHostList()) resp, err := control.SmdManage(ctx, cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -206,7 +207,7 @@ type nvmeSetFaultyCmd struct { // Set the SMD device state of the given device to "FAULTY" func (cmd *nvmeSetFaultyCmd) Execute(_ []string) error { cmd.Notice("This command will permanently mark the device as unusable!") - if !cmd.Force && !cmd.jsonOutputEnabled() { + if !cmd.Force && !cmd.JSONOutputEnabled() { if !common.GetConsent(cmd.Logger) { return errors.New("consent not given") } diff --git a/src/control/cmd/dmg/system.go b/src/control/cmd/dmg/system.go index ea1d911cf60a..58834554081a 100644 --- a/src/control/cmd/dmg/system.go +++ b/src/control/cmd/dmg/system.go @@ -16,6 +16,7 @@ import ( "github.com/pkg/errors" "github.com/daos-stack/daos/src/control/cmd/dmg/pretty" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" "github.com/daos-stack/daos/src/control/lib/daos" "github.com/daos-stack/daos/src/control/lib/ranklist" @@ -45,7 +46,7 @@ type leaderQueryCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd } func (cmd *leaderQueryCmd) Execute(_ []string) (errOut error) { @@ -65,8 +66,8 @@ func (cmd *leaderQueryCmd) Execute(_ []string) (errOut error) { return err // control api returned an error, disregard response } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } cmd.Infof("Current Leader: %s\n Replica Set: %s\n", resp.Leader, @@ -98,7 +99,7 @@ type systemQueryCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd rankListCmd Verbose bool `long:"verbose" short:"v" description:"Display more member details"` } @@ -121,8 +122,8 @@ func (cmd *systemQueryCmd) Execute(_ []string) (errOut error) { return err // control api returned an error, disregard response } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, resp.Errors()) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, resp.Errors()) } var out, outErr strings.Builder @@ -157,7 +158,7 @@ type systemStopCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd rankListCmd Force bool `long:"force" description:"Force stop DAOS system members"` } @@ -182,8 +183,8 @@ func (cmd *systemStopCmd) Execute(_ []string) (errOut error) { return err // control api returned an error, disregard response } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, resp.Errors()) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, resp.Errors()) } var out, outErr strings.Builder @@ -202,7 +203,7 @@ type baseExcludeCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd rankListCmd } @@ -223,8 +224,8 @@ func (cmd *baseExcludeCmd) execute(clear bool) error { return err // control api returned an error, disregard response } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, resp.Errors()) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, resp.Errors()) } updated := ranklist.NewRankSet() @@ -261,7 +262,7 @@ type systemStartCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd rankListCmd } @@ -283,8 +284,8 @@ func (cmd *systemStartCmd) Execute(_ []string) (errOut error) { return err // control api returned an error, disregard response } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, resp.Errors()) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, resp.Errors()) } var out, outErr strings.Builder @@ -303,7 +304,7 @@ type systemCleanupCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Args struct { Machine string `positional-arg-name:""` @@ -327,8 +328,8 @@ func (cmd *systemCleanupCmd) Execute(_ []string) (errOut error) { return err // control api returned an error, disregard response } - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } var out, outErr strings.Builder @@ -351,7 +352,7 @@ type systemSetAttrCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Args struct { Attrs ui.SetPropertiesFlag `positional-arg-name:"system attributes to set (key:val[,key:val...])" required:"1"` @@ -365,8 +366,8 @@ func (cmd *systemSetAttrCmd) Execute(_ []string) error { } err := control.SystemSetAttr(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(nil, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(nil, err) } if err != nil { @@ -382,7 +383,7 @@ type systemGetAttrCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Args struct { Attrs ui.GetPropertiesFlag `positional-arg-name:"system attributes to get (key[,key...])"` @@ -417,8 +418,8 @@ func (cmd *systemGetAttrCmd) Execute(_ []string) error { } resp, err := control.SystemGetAttr(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } if err != nil { @@ -437,7 +438,7 @@ type systemDelAttrCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Args struct { Attrs ui.GetPropertiesFlag `positional-arg-name:"system attributes to delete (key[,key...])" required:"1"` @@ -454,8 +455,8 @@ func (cmd *systemDelAttrCmd) Execute(_ []string) error { } err := control.SystemSetAttr(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(nil, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(nil, err) } if err != nil { @@ -516,7 +517,7 @@ type systemSetPropCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Args struct { Props systemSetPropsFlag `positional-arg-name:"system properties to set (key:val[,key:val...])" required:"1"` @@ -530,8 +531,8 @@ func (cmd *systemSetPropCmd) Execute(_ []string) error { } err := control.SystemSetProp(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(nil, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(nil, err) } if err != nil { @@ -588,7 +589,7 @@ type systemGetPropCmd struct { baseCmd cfgCmd ctlInvokerCmd - jsonOutputCmd + cmdutil.JSONOutputCmd Args struct { Props systemGetPropsFlag `positional-arg-name:"system properties to get (key[,key...])"` @@ -623,8 +624,8 @@ func (cmd *systemGetPropCmd) Execute(_ []string) error { } resp, err := control.SystemGetProp(context.Background(), cmd.ctlInvoker, req) - if cmd.jsonOutputEnabled() { - return cmd.outputJSON(resp.Properties, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp.Properties, err) } if err != nil { diff --git a/src/control/cmd/dmg/telemetry.go b/src/control/cmd/dmg/telemetry.go index ca6e748f743f..9351496addec 100644 --- a/src/control/cmd/dmg/telemetry.go +++ b/src/control/cmd/dmg/telemetry.go @@ -29,6 +29,7 @@ import ( "github.com/daos-stack/daos/src/control/cmd/dmg/pretty" "github.com/daos-stack/daos/src/control/common" + "github.com/daos-stack/daos/src/control/common/cmdutil" "github.com/daos-stack/daos/src/control/lib/control" ) @@ -40,7 +41,7 @@ type telemCmd struct { type telemConfigCmd struct { baseCmd cfgCmd - jsonOutputCmd + cmdutil.JSONOutputCmd InstallDir string `long:"install-dir" short:"i" required:"1" description:"Install directory for telemetry binary"` System string `long:"system" short:"s" default:"prometheus" description:"Telemetry system to configure"` } @@ -303,7 +304,7 @@ type metricsCmd struct { // metricsListCmd provides a list of metrics available from the requested DAOS servers. type metricsListCmd struct { baseCmd - jsonOutputCmd + cmdutil.JSONOutputCmd singleHostCmd Port uint32 `short:"p" long:"port" default:"9191" description:"Telemetry port on the host"` } @@ -319,7 +320,7 @@ func (cmd *metricsListCmd) Execute(args []string) error { req.Port = cmd.Port req.Host = host - if !cmd.shouldEmitJSON { + if !cmd.JSONOutputEnabled() { cmd.Info(getConnectingMsg(req.Host, req.Port)) } @@ -328,11 +329,11 @@ func (cmd *metricsListCmd) Execute(args []string) error { return err } - if cmd.shouldEmitJSON { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } - err = pretty.PrintMetricsListResp(cmd.writer, resp) + err = pretty.PrintMetricsListResp(os.Stdout, resp) if err != nil { return err } @@ -357,7 +358,7 @@ func getConnectingMsg(host string, port uint32) string { // metricsQueryCmd collects the requested metrics from the requested DAOS servers. type metricsQueryCmd struct { baseCmd - jsonOutputCmd + cmdutil.JSONOutputCmd singleHostCmd Port uint32 `short:"p" long:"port" default:"9191" description:"Telemetry port on the host"` Metrics string `short:"m" long:"metrics" default:"" description:"Comma-separated list of metric names"` @@ -375,7 +376,7 @@ func (cmd *metricsQueryCmd) Execute(args []string) error { req.Host = host req.MetricNames = common.TokenizeCommaSeparatedString(cmd.Metrics) - if !cmd.shouldEmitJSON { + if !cmd.JSONOutputEnabled() { cmd.Info(getConnectingMsg(req.Host, req.Port)) } @@ -384,11 +385,11 @@ func (cmd *metricsQueryCmd) Execute(args []string) error { return err } - if cmd.shouldEmitJSON { - return cmd.outputJSON(resp, err) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(resp, err) } - err = pretty.PrintMetricsQueryResp(cmd.writer, resp) + err = pretty.PrintMetricsQueryResp(os.Stdout, resp) if err != nil { return err } diff --git a/src/control/common/cmdutil/json.go b/src/control/common/cmdutil/json.go new file mode 100644 index 000000000000..6af30f2c1ff1 --- /dev/null +++ b/src/control/common/cmdutil/json.go @@ -0,0 +1,92 @@ +// +// (C) Copyright 2023 Intel Corporation. +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package cmdutil + +import ( + "encoding/json" + "io" + + "github.com/pkg/errors" + + "github.com/daos-stack/daos/src/control/lib/atm" + "github.com/daos-stack/daos/src/control/lib/daos" +) + +var _ JSONOutputter = (*JSONOutputCmd)(nil) + +type ( + // JSONOutputter is an interface for commands that can output JSON. + JSONOutputter interface { + EnableJSONOutput(io.Writer, *atm.Bool) + JSONOutputEnabled() bool + OutputJSON(interface{}, error) error + } +) + +// OutputJSON writes the given data or error to the given writer as JSON. +func OutputJSON(writer io.Writer, in interface{}, inErr error) error { + status := 0 + var errStr *string + if inErr != nil { + errStr = func() *string { str := inErr.Error(); return &str }() + if s, ok := errors.Cause(inErr).(daos.Status); ok { + status = int(s) + } else { + status = int(daos.MiscError) + } + } + + data, err := json.MarshalIndent(struct { + Response interface{} `json:"response"` + Error *string `json:"error"` + Status int `json:"status"` + }{in, errStr, status}, "", " ") + if err != nil { + return err + } + + if _, err = writer.Write(append(data, []byte("\n")...)); err != nil { + return err + } + + return inErr +} + +// JSONOutputCmd is a struct that implements JSONOutputter and +// can be embedded in a command struct to provide JSON output. +type JSONOutputCmd struct { + writer io.Writer + jsonEnabled atm.Bool + wroteJSON *atm.Bool +} + +// EnableJSONOutput enables JSON output to the given writer. The +// wroteJSON parameter is optional and is used to track whether +// JSON has been written to the writer. +func (cmd *JSONOutputCmd) EnableJSONOutput(writer io.Writer, wroteJSON *atm.Bool) { + cmd.wroteJSON = wroteJSON + if cmd.wroteJSON == nil { + cmd.wroteJSON = atm.NewBoolRef(false) + } + cmd.writer = writer + cmd.jsonEnabled.SetTrue() +} + +// JSONOutputEnabled returns true if JSON output is enabled. +func (cmd *JSONOutputCmd) JSONOutputEnabled() bool { + return cmd.jsonEnabled.IsTrue() +} + +// OutputJSON writes the given data or error to the command's writer as JSON. +func (cmd *JSONOutputCmd) OutputJSON(in interface{}, err error) error { + if cmd.JSONOutputEnabled() && cmd.wroteJSON.IsFalse() { + cmd.wroteJSON.SetTrue() + return OutputJSON(cmd.writer, in, err) + } + + return nil +} diff --git a/src/control/lib/atm/bool.go b/src/control/lib/atm/bool.go index eb6e046fef4d..b68e5c081fcd 100644 --- a/src/control/lib/atm/bool.go +++ b/src/control/lib/atm/bool.go @@ -21,6 +21,13 @@ func NewBool(in bool) Bool { return b } +// NewBoolRef returns a reference to a Bool set to the +// provided starting value. +func NewBoolRef(in bool) *Bool { + b := NewBool(in) + return &b +} + // SetTrue sets the Bool to true. func (b *Bool) SetTrue() { atomic.StoreUint32((*uint32)(b), 1) diff --git a/src/control/lib/control/network.go b/src/control/lib/control/network.go index e9901b210184..30496860b98b 100644 --- a/src/control/lib/control/network.go +++ b/src/control/lib/control/network.go @@ -207,8 +207,8 @@ type ( // PrimaryServiceRank provides a rank->uri mapping for a DAOS // Primary Service Rank (PSR). PrimaryServiceRank struct { - Rank uint32 - Uri string + Rank uint32 `json:"rank"` + Uri string `json:"uri"` } ClientNetworkHint struct { diff --git a/src/control/lib/hardware/hwprov/topology_cmd.go b/src/control/lib/hardware/hwprov/topology_cmd.go index 10f69944658b..cf7b5d85a0dd 100644 --- a/src/control/lib/hardware/hwprov/topology_cmd.go +++ b/src/control/lib/hardware/hwprov/topology_cmd.go @@ -8,7 +8,6 @@ package hwprov import ( "context" - "encoding/json" "os" "github.com/pkg/errors" @@ -20,9 +19,9 @@ import ( // DumpTopologyCmd implements a go-flags Commander that dumps // the system topology to stdout or to a file. type DumpTopologyCmd struct { + cmdutil.JSONOutputCmd cmdutil.LogCmd Output string `short:"o" long:"output" default:"stdout" description:"Dump output to this location"` - JSON bool `short:"j" long:"json" description:"Enable JSON output"` } func (cmd *DumpTopologyCmd) Execute(_ []string) error { @@ -42,14 +41,9 @@ func (cmd *DumpTopologyCmd) Execute(_ []string) error { return err } - if !cmd.JSON { - return hardware.PrintTopology(topo, out) + if cmd.JSONOutputEnabled() { + return cmd.OutputJSON(topo, err) } - data, err := json.MarshalIndent(topo, "", " ") - if err != nil { - return err - } - _, err = out.Write(append(data, []byte("\n")...)) - return err + return hardware.PrintTopology(topo, out) } diff --git a/src/tests/ftest/control/version.py b/src/tests/ftest/control/version.py index a93f20be0bab..ce74549d03e0 100644 --- a/src/tests/ftest/control/version.py +++ b/src/tests/ftest/control/version.py @@ -4,6 +4,7 @@ SPDX-License-Identifier: BSD-2-Clause-Patent ''' import re +import json from apricot import TestWithServers from general_utils import run_pcmd, report_errors @@ -17,6 +18,14 @@ class DAOSVersion(TestWithServers): :avocado: recursive """ + + def __init__(self, *args, **kwargs): + """Initialize a DAOSVersion object.""" + super().__init__(*args, **kwargs) + # Don't waste time starting servers and agents. + self.setup_start_servers = False + self.setup_start_agents = False + def test_version(self): """Verify version number for dmg, daos, daos_server, and daos_agent against RPM. @@ -41,66 +50,23 @@ def test_version(self): self.log.info("RPM version = %s", rpm_version) # Get dmg version. - dmg_cmd = self.get_dmg_command() - output = dmg_cmd.version().stdout.decode("utf-8") - - # Verify that "dmg version" is in the output. - if "dmg version" not in output: - errors.append("dmg version is not in the output! {}".format(output)) - - result = re.findall(r"dmg version ([\d.]+)", output) - if not result: - errors.append("Failed to obtain dmg version! {}".format(output)) - else: - dmg_version = result[0] - self.log.info("dmg version = %s", dmg_version) + dmg_version = self.get_dmg_command().version()["response"]["version"] + self.log.info("dmg version = %s", dmg_version) # Get daos version. - daos_cmd = self.get_daos_command() - output = daos_cmd.version().stdout.decode("utf-8") - - # Verify that "daos version" is in the output. - if "daos version" not in output: - errors.append("daos version is not in the output! {}".format(output)) - - result = re.findall(r"daos version ([\d.]+)", output) - if not result: - errors.append("Failed to obtain daos version! {}".format(output)) - else: - daos_version = result[0] - self.log.info("daos version = %s", daos_version) + daos_version = self.get_daos_command().version()["response"]["version"] + self.log.info("daos version = %s", daos_version) # Get daos_agent version. - daos_agent_cmd = "daos_agent version" + daos_agent_cmd = "daos_agent --json version" output = run_pcmd(hosts=self.hostlist_servers, command=daos_agent_cmd) - stdout = output[0]["stdout"][0] - - # Verify that "DAOS Agent" is in the output. - if "DAOS Agent" not in stdout: - errors.append("DAOS Agent is not in the output! {}".format(stdout)) - - result = re.findall(r"DAOS Agent v([\d.]+)", stdout) - if not result: - errors.append("Failed to obtain daos_agent version! {}".format(output)) - else: - daos_agent_version = result[0] - self.log.info("daos_agent version = %s", daos_agent_version) + daos_agent_version = json.loads("".join(output[0]["stdout"]))["response"]["version"] + self.log.info("daos_agent version = %s", daos_agent_version) # Get daos_server version daos_server_cmd = DaosServerCommandRunner(path=self.bin) - output = daos_server_cmd.version() - stdout = output.stdout.decode("utf-8") - - # Verify that "DAOS Control Server" is in the output. - if "DAOS Control Server" not in stdout: - errors.append("DAOS Control Server is not in the output! {}".format(stdout)) - - result = re.findall(r"DAOS Control Server v([\d.]+)", stdout) - if not result: - errors.append("Failed to obtain daos_server version! {}".format(output)) - else: - daos_server_version = result[0] - self.log.info("daos_server version = %s", daos_server_version) + daos_server_version = daos_server_cmd.version()["response"]["version"] + self.log.info("daos_server version = %s", daos_server_version) # Verify the tool versions against the RPM. tool_versions = [ diff --git a/src/tests/ftest/util/daos_utils.py b/src/tests/ftest/util/daos_utils.py index 7bafe2dee0b0..c1d9b30d5eac 100644 --- a/src/tests/ftest/util/daos_utils.py +++ b/src/tests/ftest/util/daos_utils.py @@ -830,11 +830,10 @@ def version(self): """Call daos version. Returns: - CmdResult: an avocado CmdResult object containing the dmg command - information, e.g. exit status, stdout, stderr, etc. + dict: JSON output Raises: - CommandFailure: if the dmg storage query command fails. + CommandFailure: if the daos version command fails. """ - return self._get_result(["version"]) + return self._get_json_result(("version",)) diff --git a/src/tests/ftest/util/dmg_utils.py b/src/tests/ftest/util/dmg_utils.py index 354dca4de03b..34c2ca8a5afd 100644 --- a/src/tests/ftest/util/dmg_utils.py +++ b/src/tests/ftest/util/dmg_utils.py @@ -1293,14 +1293,13 @@ def version(self): """Call dmg version. Returns: - CmdResult: an avocado CmdResult object containing the dmg command - information, e.g. exit status, stdout, stderr, etc. + dict: the dmg json command output converted to a python dictionary Raises: - CommandFailure: if the dmg storage query command fails. + CommandFailure: if the dmg version command fails. """ - return self._get_result(["version"]) + return self._get_json_result(("version",)) def check_system_query_status(data): diff --git a/src/tests/ftest/util/server_utils_base.py b/src/tests/ftest/util/server_utils_base.py index dd1c4832d63c..eac7251a7088 100644 --- a/src/tests/ftest/util/server_utils_base.py +++ b/src/tests/ftest/util/server_utils_base.py @@ -59,6 +59,7 @@ def __init__(self, path="", yaml_cfg=None, timeout=45): # -o, --config-path= Path to agent configuration file self.debug = FormattedParameter("--debug", True) self.json_logs = FormattedParameter("--json-logging", False) + self.json = FormattedParameter("--json", False) self.config = FormattedParameter("--config={}", default_yaml_file) # Additional daos_server command line parameters: @@ -892,11 +893,10 @@ def version(self): """Call daos_server version. Returns: - CmdResult: an avocado CmdResult object containing the daos_server command - information, e.g. exit status, stdout, stderr, etc. + dict: JSON output Raises: CommandFailure: if the daos_server version command fails. """ - return self._get_result(["version"]) + return self._get_json_result(("version",))