Skip to content

Commit

Permalink
CLI: show all supported feature-flags and descriptions (usability)
Browse files Browse the repository at this point in the history
* cluster scope and bucket scope
* set and show operations
* color current (set) features
* update readme

Signed-off-by: Alex Aizman <[email protected]>
  • Loading branch information
alex-aizman committed Feb 8, 2025
1 parent 5dc44e1 commit 9f222a2
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 117 deletions.
18 changes: 15 additions & 3 deletions cmd/cli/cli/bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,9 @@ func showBucketProps(c *cli.Context) (err error) {
return headBckTable(c, p, defProps, section)
}

func headBckTable(c *cli.Context, props, defProps *cmn.Bprops, section string) error {
// compare w/ showClusterConfig using the same generic template
// for "flattened" cluster config
func headBckTable(c *cli.Context, props, defProps *cmn.Bprops, section string) (err error) {
var (
defList nvpairList
colored = !cfg.NoColor
Expand Down Expand Up @@ -425,7 +427,17 @@ func headBckTable(c *cli.Context, props, defProps *cmn.Bprops, section string) e
}

if flagIsSet(c, noHeaderFlag) {
return teb.Print(propList, teb.PropValTmplNoHdr)
err = teb.Print(propList, teb.PropValTmplNoHdr)
} else {
err = teb.Print(propList, teb.PropValTmpl)
}
if err != nil {
return err
}
return teb.Print(propList, teb.PropValTmpl)

// feature flags: show all w/ descriptions
if section == featureFlagsJname {
err = printFeatVerbose(c, props.Features, true /*bucket scope*/)
}
return err
}
137 changes: 98 additions & 39 deletions cmd/cli/cli/bucket_hdlr.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/NVIDIA/aistore/cmn"
"github.com/NVIDIA/aistore/cmn/archive"
"github.com/NVIDIA/aistore/cmn/cos"
"github.com/NVIDIA/aistore/cmn/debug"
jsoniter "github.com/json-iterator/go"
"github.com/urfave/cli"
)

Expand Down Expand Up @@ -300,10 +302,10 @@ var (
}
)

func createBucketHandler(c *cli.Context) (err error) {
func createBucketHandler(c *cli.Context) error {
var props *cmn.BpropsToSet
if flagIsSet(c, bucketPropsFlag) {
propSingleBck, err := parseBpropsFromContext(c)
propSingleBck, _, err := _parseBprops(c)
if err != nil {
return err
}
Expand Down Expand Up @@ -400,24 +402,38 @@ func toggleLRU(c *cli.Context, bck cmn.Bck, p *cmn.Bprops, toggle bool) (err err
return updateBckProps(c, bck, p, toggledProps)
}

func setPropsHandler(c *cli.Context) (err error) {
var currProps *cmn.Bprops
bck, err := parseBckURI(c, c.Args().Get(0), false)
func setPropsHandler(c *cli.Context) error {
var (
currBprops *cmn.Bprops
nvs cos.StrKVs // user specified
newBprops *cmn.BpropsToSet // API structure to set
bck, err = parseBckURI(c, c.Args().Get(0), false)
)
if err != nil {
return err
}

dontHeadRemote := flagIsSet(c, dontHeadRemoteFlag)
if !dontHeadRemote {
if currProps, err = headBucket(bck, false /* don't add */); err != nil {
if currBprops, err = headBucket(bck, false /* don't add */); err != nil {
return err
}
}
newProps, err := parseBpropsFromContext(c)

newBprops, nvs, err = _parseBprops(c)
if err == nil {
newProps.Force = flagIsSet(c, forceFlag)
return updateBckProps(c, bck, currProps, newProps)
newBprops.Force = flagIsSet(c, forceFlag)
err = updateBckProps(c, bck, currBprops, newBprops)
if err != nil {
return err
}
// feature flags: show all w/ descriptions
if _, ok := nvs[featureFlagsJname]; ok && newBprops.Features != nil {
err = printFeatVerbose(c, *newBprops.Features, true /*bucket scope*/)
}
return err
}

// [usability] try to help
var (
section = c.Args().Get(1)
isValid bool
Expand All @@ -435,35 +451,100 @@ func setPropsHandler(c *cli.Context) (err error) {
return nil
}
}

return fmt.Errorf("%v%s", err, examplesBckSetProps)
}

// TODO: more validation; e.g. `validate_warm_get = true` is only supported for buckets with Cloud and remais backends
func updateBckProps(c *cli.Context, bck cmn.Bck, currProps *cmn.Bprops, updateProps *cmn.BpropsToSet) (err error) {
func updateBckProps(c *cli.Context, bck cmn.Bck, currBprops *cmn.Bprops, updateProps *cmn.BpropsToSet) error {
// apply updated props
allNewProps := currProps.Clone()
allNewProps.Apply(updateProps)
allNewBprops := currBprops.Clone()
allNewBprops.Apply(updateProps)

// check for changes
if allNewProps.Equal(currProps) {
if allNewBprops.Equal(currBprops) {
displayPropsEqMsg(c, bck)
return nil
}

// do
if _, err = api.SetBucketProps(apiBP, bck, updateProps); err != nil {
if _, err := api.SetBucketProps(apiBP, bck, updateProps); err != nil {
if herr, ok := err.(*cmn.ErrHTTP); ok && herr.Status == http.StatusNotFound {
return herr
}
helpMsg := fmt.Sprintf("To show bucket properties, run '%s %s %s %s'",
cliName, commandShow, cmdBucket, bck.Cname(""))
return newAdditionalInfoError(err, helpMsg)
}
showDiff(c, currProps, allNewProps)

_showDiff(c, currBprops, allNewBprops)

actionDone(c, "\nBucket props successfully updated.")
return nil
}

func _showDiff(c *cli.Context, currBprops, newBprops *cmn.Bprops) {
var (
newPropList = bckPropList(newBprops, true)
origPropList = bckPropList(currBprops, true)
)
for _, np := range newPropList {
var found bool
for _, op := range origPropList {
if np.Name != op.Name {
continue
}
found = true
if np.Value != op.Value {
fmt.Fprintf(c.App.Writer, "%q set to: %q (was: %q)\n", np.Name, _clearFmt(np.Value), _clearFmt(op.Value))
}
}
if !found && np.Value != "" {
fmt.Fprintf(c.App.Writer, "%q set to: %q (was: n/a)\n", np.Name, _clearFmt(np.Value))
}
}

// feature flags: show all w/ descriptions
if len(newPropList) == 1 && newPropList[0].Name == featureFlagsJname {
err := printFeatVerbose(c, newBprops.Features, true /*bucket scope*/)
debug.AssertNoErr(err)
}
}

func _parseBprops(c *cli.Context) (props *cmn.BpropsToSet, nvs cos.StrKVs, err error) {
propArgs := c.Args().Tail()

if c.Command.Name == commandCreate {
inputProps := parseStrFlag(c, bucketPropsFlag)
if isJSON(inputProps) {
err = jsoniter.Unmarshal([]byte(inputProps), &props)
return
}
propArgs = strings.Split(inputProps, " ")
}

if len(propArgs) == 1 && isJSON(propArgs[0]) {
err = jsoniter.Unmarshal([]byte(propArgs[0]), &props)
return
}

if len(propArgs) == 0 {
return nil, nil, missingArgumentsError(c, "property key-value pairs")
}

// command line => key/val pairs
nvs, err = makeBckPropPairs(propArgs)
if err != nil {
return nil, nil, err
}
if err = reformatBackendProps(c, nvs); err != nil {
return nil, nvs, err
}

// key/val pairs => cmn.BpropsToSet
props, err = cmn.NewBpropsToSet(nvs)
return props, nvs, err
}

func displayPropsEqMsg(c *cli.Context, bck cmn.Bck) {
args := c.Args().Tail()
if len(args) == 1 && !isJSON(args[0]) {
Expand All @@ -485,28 +566,6 @@ func _clearFmt(v string) string {
return strings.ReplaceAll(nv, "\t", "")
}

func showDiff(c *cli.Context, currProps, newProps *cmn.Bprops) {
var (
origKV = bckPropList(currProps, true)
newKV = bckPropList(newProps, true)
)
for _, np := range newKV {
var found bool
for _, op := range origKV {
if np.Name != op.Name {
continue
}
found = true
if np.Value != op.Value {
fmt.Fprintf(c.App.Writer, "%q set to: %q (was: %q)\n", np.Name, _clearFmt(np.Value), _clearFmt(op.Value))
}
}
if !found && np.Value != "" {
fmt.Fprintf(c.App.Writer, "%q set to: %q (was: n/a)\n", np.Name, _clearFmt(np.Value))
}
}
}

func listAnyHandler(c *cli.Context) error {
var (
opts = cmn.ParseURIOpts{IsQuery: true}
Expand Down
21 changes: 9 additions & 12 deletions cmd/cli/cli/completions.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Package cli provides easy-to-use commands to manage, monitor, and utilize AIS clusters.
// This file handles bash completions for the CLI.
/*
* Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
* Copyright (c) 2018-2025, NVIDIA CORPORATION. All rights reserved.
*/
package cli

Expand All @@ -24,12 +23,10 @@ import (
"github.com/urfave/cli"
)

//////////////////////
// Cluster / Daemon //
//////////////////////
// This source handles Bash and zsh completions for the CLI.

// Log level doubles as level per se and (s)modules, the latter enumerated
const (
// Log level doubles as level per se and (s)modules, the latter enumerated
confLogLevel = "log.level"
confLogModules = "log.modules"
)
Expand All @@ -44,8 +41,8 @@ var (
// access
cmn.PropBucketAccessAttrs: apc.SupportedPermissions(),
// feature flags
"cluster.features": append(feat.Cluster[:], apc.NilValue),
"bucket.features": append(feat.Bucket[:], apc.NilValue),
clusterFeatures: append(feat.Cluster[:], apc.NilValue),
bucketFeatures: append(feat.Bucket[:], apc.NilValue),
// rest
"write_policy.data": apc.SupportedWritePolicy[:],
"write_policy.md": apc.SupportedWritePolicy[:],
Expand Down Expand Up @@ -94,9 +91,9 @@ func lastIsFeature(c *cli.Context, bucketScope bool) bool {
return true
}
if bucketScope {
return _lastv(c, propCmpls["bucket.features"])
return _lastv(c, propCmpls[bucketFeatures])
}
return _lastv(c, propCmpls["cluster.features"])
return _lastv(c, propCmpls[clusterFeatures])
}

// Returns true if the last arg is any of the enumerated constants
Expand All @@ -123,9 +120,9 @@ func accessCompletions(c *cli.Context) { remaining(c, propCmpls[cmn.PropBucketA

func featureCompletions(c *cli.Context, bucketScope bool) {
if bucketScope {
remaining(c, propCmpls["bucket.features"])
remaining(c, propCmpls[bucketFeatures])
} else {
remaining(c, propCmpls["cluster.features"])
remaining(c, propCmpls[clusterFeatures])
}
}

Expand Down
70 changes: 70 additions & 0 deletions cmd/cli/cli/feat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Package cli provides easy-to-use commands to manage, monitor, and utilize AIS clusters.
/*
* Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
*/
package cli

import (
"fmt"

"github.com/NVIDIA/aistore/cmd/cli/teb"
"github.com/NVIDIA/aistore/cmn/feat"
"github.com/urfave/cli"
)

// Features feat.Flags `json:"features,string" as per (bucket: cmn/api and cluster: cmn/config)

const (
featureFlagsJname = "features"

clusterFeatures = "cluster." + featureFlagsJname
bucketFeatures = "bucket." + featureFlagsJname
)

// NOTE:
// - `Bucket` features are a strict subset of all `Cluster` features, and can be changed for individual buckets;
// - for any changes, check server-side cmn/feat/feat

var clusterFeatDesc = [...]string{
"enforce intra-cluster access",
"(*) skip loading existing object's metadata, Version and Checksum (VC) in particular",
"do not auto-detect file share (NFS, SMB) when _promoting_ shared files to AIS",
"handle s3 requests via `aistore-hostname/` (default: `aistore-hostname/s3`)",
"(*) when finalizing PUT(object): fflush prior to (close, rename) sequence",
".tar.lz4 format, lz4 compression: max uncompressed block size=1MB (default: 256K)",
"checksum lz4 frames (default: don't)",
"do not allow passing fully-qualified name of a locally stored object to (local) ETL containers",
"run in presence of _limited coexistence_ type conflicts (same as e.g. CopyBckMsg.Force but globally)",
"(*) pass-through client-signed (presigned) S3 requests for subsequent authentication by S3",
"when prefix doesn't end with '/' and is a subdirectory: don't assume there are no _prefixed_ obj names",
"disable cold-GET (from remote bucket)",
"write and transmit cold-GET content back to user in parallel, without _finalizing_ in-cluster object",
"intra-cluster communications: instead of regular HTTP redirects reverse-proxy S3 API calls to designated targets",
"use older path-style addressing (as opposed to virtual-hosted style), e.g., https://s3.amazonaws.com/BUCKET/KEY",
"when objects get _rebalanced_ to their proper locations, do not delete their respective _misplaced_ sources",
"intra-cluster control plane: do not set IPv4 ToS field (to low-latency)",
"when checking whether objects are identical trust only cryptographically secure checksums",

// "none" ====================
}

// common (cluster, bucket) feature-flags (set, show) helper
func printFeatVerbose(c *cli.Context, flags feat.Flags, scopeBucket bool) error {
fmt.Fprintln(c.App.Writer)
flat := _flattenFeat(flags, scopeBucket)
return teb.Print(flat, teb.FeatDescTmplHdr+teb.PropValTmplNoHdr)
}

func _flattenFeat(flags feat.Flags, scopeBucket bool) (flat nvpairList) {
for i, f := range feat.Cluster {
if scopeBucket && !feat.IsBucketScope(f) {
continue
}
nv := nvpair{Name: f, Value: clusterFeatDesc[i]}
if flags.IsSet(1 << i) {
nv.Value = fcyan(nv.Value)
}
flat = append(flat, nv)
}
return flat
}
13 changes: 12 additions & 1 deletion cmd/cli/cli/show_hdlr.go
Original file line number Diff line number Diff line change
Expand Up @@ -785,16 +785,27 @@ func showClusterConfig(c *cli.Context, section string) error {
flat = flattenBackends(backends)
}

// compare w/ headBckTable using the same generic template for bucket props
if flagIsSet(c, noHeaderFlag) {
err = teb.Print(flat, teb.PropValTmplNoHdr)
} else {
err = teb.Print(flat, teb.PropValTmpl)
}
if err == nil && section == "" {
if err != nil {
return err
}
if section == "" {
msg := fmt.Sprintf("(Tip: use '[SECTION] %s' to show config section(s), see %s for details)",
flprn(jsonFlag), qflprn(cli.HelpFlag))
actionDone(c, msg)
return nil
}

// feature flags: show all w/ descriptions
if section == featureFlagsJname {
err = printFeatVerbose(c, cluConfig.Features, false /*bucket scope*/)
}

return err
}

Expand Down
Loading

0 comments on commit 9f222a2

Please sign in to comment.