Skip to content

Commit 9f222a2

Browse files
committed
CLI: show all supported feature-flags and descriptions (usability)
* cluster scope and bucket scope * set and show operations * color current (set) features * update readme Signed-off-by: Alex Aizman <alex.aizman@gmail.com>
1 parent 5dc44e1 commit 9f222a2

File tree

9 files changed

+297
-117
lines changed

9 files changed

+297
-117
lines changed

cmd/cli/cli/bucket.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,9 @@ func showBucketProps(c *cli.Context) (err error) {
373373
return headBckTable(c, p, defProps, section)
374374
}
375375

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

427429
if flagIsSet(c, noHeaderFlag) {
428-
return teb.Print(propList, teb.PropValTmplNoHdr)
430+
err = teb.Print(propList, teb.PropValTmplNoHdr)
431+
} else {
432+
err = teb.Print(propList, teb.PropValTmpl)
433+
}
434+
if err != nil {
435+
return err
429436
}
430-
return teb.Print(propList, teb.PropValTmpl)
437+
438+
// feature flags: show all w/ descriptions
439+
if section == featureFlagsJname {
440+
err = printFeatVerbose(c, props.Features, true /*bucket scope*/)
441+
}
442+
return err
431443
}

cmd/cli/cli/bucket_hdlr.go

Lines changed: 98 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"github.com/NVIDIA/aistore/cmn"
1818
"github.com/NVIDIA/aistore/cmn/archive"
1919
"github.com/NVIDIA/aistore/cmn/cos"
20+
"github.com/NVIDIA/aistore/cmn/debug"
21+
jsoniter "github.com/json-iterator/go"
2022
"github.com/urfave/cli"
2123
)
2224

@@ -300,10 +302,10 @@ var (
300302
}
301303
)
302304

303-
func createBucketHandler(c *cli.Context) (err error) {
305+
func createBucketHandler(c *cli.Context) error {
304306
var props *cmn.BpropsToSet
305307
if flagIsSet(c, bucketPropsFlag) {
306-
propSingleBck, err := parseBpropsFromContext(c)
308+
propSingleBck, _, err := _parseBprops(c)
307309
if err != nil {
308310
return err
309311
}
@@ -400,24 +402,38 @@ func toggleLRU(c *cli.Context, bck cmn.Bck, p *cmn.Bprops, toggle bool) (err err
400402
return updateBckProps(c, bck, p, toggledProps)
401403
}
402404

403-
func setPropsHandler(c *cli.Context) (err error) {
404-
var currProps *cmn.Bprops
405-
bck, err := parseBckURI(c, c.Args().Get(0), false)
405+
func setPropsHandler(c *cli.Context) error {
406+
var (
407+
currBprops *cmn.Bprops
408+
nvs cos.StrKVs // user specified
409+
newBprops *cmn.BpropsToSet // API structure to set
410+
bck, err = parseBckURI(c, c.Args().Get(0), false)
411+
)
406412
if err != nil {
407413
return err
408414
}
415+
409416
dontHeadRemote := flagIsSet(c, dontHeadRemoteFlag)
410417
if !dontHeadRemote {
411-
if currProps, err = headBucket(bck, false /* don't add */); err != nil {
418+
if currBprops, err = headBucket(bck, false /* don't add */); err != nil {
412419
return err
413420
}
414421
}
415-
newProps, err := parseBpropsFromContext(c)
416-
422+
newBprops, nvs, err = _parseBprops(c)
417423
if err == nil {
418-
newProps.Force = flagIsSet(c, forceFlag)
419-
return updateBckProps(c, bck, currProps, newProps)
424+
newBprops.Force = flagIsSet(c, forceFlag)
425+
err = updateBckProps(c, bck, currBprops, newBprops)
426+
if err != nil {
427+
return err
428+
}
429+
// feature flags: show all w/ descriptions
430+
if _, ok := nvs[featureFlagsJname]; ok && newBprops.Features != nil {
431+
err = printFeatVerbose(c, *newBprops.Features, true /*bucket scope*/)
432+
}
433+
return err
420434
}
435+
436+
// [usability] try to help
421437
var (
422438
section = c.Args().Get(1)
423439
isValid bool
@@ -435,35 +451,100 @@ func setPropsHandler(c *cli.Context) (err error) {
435451
return nil
436452
}
437453
}
454+
438455
return fmt.Errorf("%v%s", err, examplesBckSetProps)
439456
}
440457

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

447463
// check for changes
448-
if allNewProps.Equal(currProps) {
464+
if allNewBprops.Equal(currBprops) {
449465
displayPropsEqMsg(c, bck)
450466
return nil
451467
}
452468

453469
// do
454-
if _, err = api.SetBucketProps(apiBP, bck, updateProps); err != nil {
470+
if _, err := api.SetBucketProps(apiBP, bck, updateProps); err != nil {
455471
if herr, ok := err.(*cmn.ErrHTTP); ok && herr.Status == http.StatusNotFound {
456472
return herr
457473
}
458474
helpMsg := fmt.Sprintf("To show bucket properties, run '%s %s %s %s'",
459475
cliName, commandShow, cmdBucket, bck.Cname(""))
460476
return newAdditionalInfoError(err, helpMsg)
461477
}
462-
showDiff(c, currProps, allNewProps)
478+
479+
_showDiff(c, currBprops, allNewBprops)
480+
463481
actionDone(c, "\nBucket props successfully updated.")
464482
return nil
465483
}
466484

485+
func _showDiff(c *cli.Context, currBprops, newBprops *cmn.Bprops) {
486+
var (
487+
newPropList = bckPropList(newBprops, true)
488+
origPropList = bckPropList(currBprops, true)
489+
)
490+
for _, np := range newPropList {
491+
var found bool
492+
for _, op := range origPropList {
493+
if np.Name != op.Name {
494+
continue
495+
}
496+
found = true
497+
if np.Value != op.Value {
498+
fmt.Fprintf(c.App.Writer, "%q set to: %q (was: %q)\n", np.Name, _clearFmt(np.Value), _clearFmt(op.Value))
499+
}
500+
}
501+
if !found && np.Value != "" {
502+
fmt.Fprintf(c.App.Writer, "%q set to: %q (was: n/a)\n", np.Name, _clearFmt(np.Value))
503+
}
504+
}
505+
506+
// feature flags: show all w/ descriptions
507+
if len(newPropList) == 1 && newPropList[0].Name == featureFlagsJname {
508+
err := printFeatVerbose(c, newBprops.Features, true /*bucket scope*/)
509+
debug.AssertNoErr(err)
510+
}
511+
}
512+
513+
func _parseBprops(c *cli.Context) (props *cmn.BpropsToSet, nvs cos.StrKVs, err error) {
514+
propArgs := c.Args().Tail()
515+
516+
if c.Command.Name == commandCreate {
517+
inputProps := parseStrFlag(c, bucketPropsFlag)
518+
if isJSON(inputProps) {
519+
err = jsoniter.Unmarshal([]byte(inputProps), &props)
520+
return
521+
}
522+
propArgs = strings.Split(inputProps, " ")
523+
}
524+
525+
if len(propArgs) == 1 && isJSON(propArgs[0]) {
526+
err = jsoniter.Unmarshal([]byte(propArgs[0]), &props)
527+
return
528+
}
529+
530+
if len(propArgs) == 0 {
531+
return nil, nil, missingArgumentsError(c, "property key-value pairs")
532+
}
533+
534+
// command line => key/val pairs
535+
nvs, err = makeBckPropPairs(propArgs)
536+
if err != nil {
537+
return nil, nil, err
538+
}
539+
if err = reformatBackendProps(c, nvs); err != nil {
540+
return nil, nvs, err
541+
}
542+
543+
// key/val pairs => cmn.BpropsToSet
544+
props, err = cmn.NewBpropsToSet(nvs)
545+
return props, nvs, err
546+
}
547+
467548
func displayPropsEqMsg(c *cli.Context, bck cmn.Bck) {
468549
args := c.Args().Tail()
469550
if len(args) == 1 && !isJSON(args[0]) {
@@ -485,28 +566,6 @@ func _clearFmt(v string) string {
485566
return strings.ReplaceAll(nv, "\t", "")
486567
}
487568

488-
func showDiff(c *cli.Context, currProps, newProps *cmn.Bprops) {
489-
var (
490-
origKV = bckPropList(currProps, true)
491-
newKV = bckPropList(newProps, true)
492-
)
493-
for _, np := range newKV {
494-
var found bool
495-
for _, op := range origKV {
496-
if np.Name != op.Name {
497-
continue
498-
}
499-
found = true
500-
if np.Value != op.Value {
501-
fmt.Fprintf(c.App.Writer, "%q set to: %q (was: %q)\n", np.Name, _clearFmt(np.Value), _clearFmt(op.Value))
502-
}
503-
}
504-
if !found && np.Value != "" {
505-
fmt.Fprintf(c.App.Writer, "%q set to: %q (was: n/a)\n", np.Name, _clearFmt(np.Value))
506-
}
507-
}
508-
}
509-
510569
func listAnyHandler(c *cli.Context) error {
511570
var (
512571
opts = cmn.ParseURIOpts{IsQuery: true}

cmd/cli/cli/completions.go

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Package cli provides easy-to-use commands to manage, monitor, and utilize AIS clusters.
2-
// This file handles bash completions for the CLI.
32
/*
4-
* Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
3+
* Copyright (c) 2018-2025, NVIDIA CORPORATION. All rights reserved.
54
*/
65
package cli
76

@@ -24,12 +23,10 @@ import (
2423
"github.com/urfave/cli"
2524
)
2625

27-
//////////////////////
28-
// Cluster / Daemon //
29-
//////////////////////
26+
// This source handles Bash and zsh completions for the CLI.
3027

31-
// Log level doubles as level per se and (s)modules, the latter enumerated
3228
const (
29+
// Log level doubles as level per se and (s)modules, the latter enumerated
3330
confLogLevel = "log.level"
3431
confLogModules = "log.modules"
3532
)
@@ -44,8 +41,8 @@ var (
4441
// access
4542
cmn.PropBucketAccessAttrs: apc.SupportedPermissions(),
4643
// feature flags
47-
"cluster.features": append(feat.Cluster[:], apc.NilValue),
48-
"bucket.features": append(feat.Bucket[:], apc.NilValue),
44+
clusterFeatures: append(feat.Cluster[:], apc.NilValue),
45+
bucketFeatures: append(feat.Bucket[:], apc.NilValue),
4946
// rest
5047
"write_policy.data": apc.SupportedWritePolicy[:],
5148
"write_policy.md": apc.SupportedWritePolicy[:],
@@ -94,9 +91,9 @@ func lastIsFeature(c *cli.Context, bucketScope bool) bool {
9491
return true
9592
}
9693
if bucketScope {
97-
return _lastv(c, propCmpls["bucket.features"])
94+
return _lastv(c, propCmpls[bucketFeatures])
9895
}
99-
return _lastv(c, propCmpls["cluster.features"])
96+
return _lastv(c, propCmpls[clusterFeatures])
10097
}
10198

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

124121
func featureCompletions(c *cli.Context, bucketScope bool) {
125122
if bucketScope {
126-
remaining(c, propCmpls["bucket.features"])
123+
remaining(c, propCmpls[bucketFeatures])
127124
} else {
128-
remaining(c, propCmpls["cluster.features"])
125+
remaining(c, propCmpls[clusterFeatures])
129126
}
130127
}
131128

cmd/cli/cli/feat.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Package cli provides easy-to-use commands to manage, monitor, and utilize AIS clusters.
2+
/*
3+
* Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
4+
*/
5+
package cli
6+
7+
import (
8+
"fmt"
9+
10+
"github.com/NVIDIA/aistore/cmd/cli/teb"
11+
"github.com/NVIDIA/aistore/cmn/feat"
12+
"github.com/urfave/cli"
13+
)
14+
15+
// Features feat.Flags `json:"features,string" as per (bucket: cmn/api and cluster: cmn/config)
16+
17+
const (
18+
featureFlagsJname = "features"
19+
20+
clusterFeatures = "cluster." + featureFlagsJname
21+
bucketFeatures = "bucket." + featureFlagsJname
22+
)
23+
24+
// NOTE:
25+
// - `Bucket` features are a strict subset of all `Cluster` features, and can be changed for individual buckets;
26+
// - for any changes, check server-side cmn/feat/feat
27+
28+
var clusterFeatDesc = [...]string{
29+
"enforce intra-cluster access",
30+
"(*) skip loading existing object's metadata, Version and Checksum (VC) in particular",
31+
"do not auto-detect file share (NFS, SMB) when _promoting_ shared files to AIS",
32+
"handle s3 requests via `aistore-hostname/` (default: `aistore-hostname/s3`)",
33+
"(*) when finalizing PUT(object): fflush prior to (close, rename) sequence",
34+
".tar.lz4 format, lz4 compression: max uncompressed block size=1MB (default: 256K)",
35+
"checksum lz4 frames (default: don't)",
36+
"do not allow passing fully-qualified name of a locally stored object to (local) ETL containers",
37+
"run in presence of _limited coexistence_ type conflicts (same as e.g. CopyBckMsg.Force but globally)",
38+
"(*) pass-through client-signed (presigned) S3 requests for subsequent authentication by S3",
39+
"when prefix doesn't end with '/' and is a subdirectory: don't assume there are no _prefixed_ obj names",
40+
"disable cold-GET (from remote bucket)",
41+
"write and transmit cold-GET content back to user in parallel, without _finalizing_ in-cluster object",
42+
"intra-cluster communications: instead of regular HTTP redirects reverse-proxy S3 API calls to designated targets",
43+
"use older path-style addressing (as opposed to virtual-hosted style), e.g., https://s3.amazonaws.com/BUCKET/KEY",
44+
"when objects get _rebalanced_ to their proper locations, do not delete their respective _misplaced_ sources",
45+
"intra-cluster control plane: do not set IPv4 ToS field (to low-latency)",
46+
"when checking whether objects are identical trust only cryptographically secure checksums",
47+
48+
// "none" ====================
49+
}
50+
51+
// common (cluster, bucket) feature-flags (set, show) helper
52+
func printFeatVerbose(c *cli.Context, flags feat.Flags, scopeBucket bool) error {
53+
fmt.Fprintln(c.App.Writer)
54+
flat := _flattenFeat(flags, scopeBucket)
55+
return teb.Print(flat, teb.FeatDescTmplHdr+teb.PropValTmplNoHdr)
56+
}
57+
58+
func _flattenFeat(flags feat.Flags, scopeBucket bool) (flat nvpairList) {
59+
for i, f := range feat.Cluster {
60+
if scopeBucket && !feat.IsBucketScope(f) {
61+
continue
62+
}
63+
nv := nvpair{Name: f, Value: clusterFeatDesc[i]}
64+
if flags.IsSet(1 << i) {
65+
nv.Value = fcyan(nv.Value)
66+
}
67+
flat = append(flat, nv)
68+
}
69+
return flat
70+
}

cmd/cli/cli/show_hdlr.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,16 +785,27 @@ func showClusterConfig(c *cli.Context, section string) error {
785785
flat = flattenBackends(backends)
786786
}
787787

788+
// compare w/ headBckTable using the same generic template for bucket props
788789
if flagIsSet(c, noHeaderFlag) {
789790
err = teb.Print(flat, teb.PropValTmplNoHdr)
790791
} else {
791792
err = teb.Print(flat, teb.PropValTmpl)
792793
}
793-
if err == nil && section == "" {
794+
if err != nil {
795+
return err
796+
}
797+
if section == "" {
794798
msg := fmt.Sprintf("(Tip: use '[SECTION] %s' to show config section(s), see %s for details)",
795799
flprn(jsonFlag), qflprn(cli.HelpFlag))
796800
actionDone(c, msg)
801+
return nil
802+
}
803+
804+
// feature flags: show all w/ descriptions
805+
if section == featureFlagsJname {
806+
err = printFeatVerbose(c, cluConfig.Features, false /*bucket scope*/)
797807
}
808+
798809
return err
799810
}
800811

0 commit comments

Comments
 (0)