From 17db4cb9703bcaac3a18ff1c2bbc43c57af9d6b6 Mon Sep 17 00:00:00 2001 From: mmetc <92726601+mmetc@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:22:52 +0100 Subject: [PATCH] refact "cscli machines" (#2777) --- cmd/crowdsec-cli/bouncers.go | 5 +- cmd/crowdsec-cli/flag.go | 28 +++ cmd/crowdsec-cli/items.go | 2 - cmd/crowdsec-cli/machines.go | 442 ++++++++++++++++++----------------- cmd/crowdsec-cli/main.go | 22 +- cmd/crowdsec-cli/support.go | 5 +- test/bats/30_machines.bats | 11 +- 7 files changed, 279 insertions(+), 236 deletions(-) create mode 100644 cmd/crowdsec-cli/flag.go diff --git a/cmd/crowdsec-cli/bouncers.go b/cmd/crowdsec-cli/bouncers.go index 410827b31..d2685901e 100644 --- a/cmd/crowdsec-cli/bouncers.go +++ b/cmd/crowdsec-cli/bouncers.go @@ -16,7 +16,6 @@ import ( "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1" - "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/types" ) @@ -38,10 +37,10 @@ func askYesNo(message string, defaultAnswer bool) (bool, error) { type cliBouncers struct { db *database.Client - cfg func() *csconfig.Config + cfg configGetter } -func NewCLIBouncers(getconfig func() *csconfig.Config) *cliBouncers { +func NewCLIBouncers(getconfig configGetter) *cliBouncers { return &cliBouncers{ cfg: getconfig, } diff --git a/cmd/crowdsec-cli/flag.go b/cmd/crowdsec-cli/flag.go new file mode 100644 index 000000000..402302a1f --- /dev/null +++ b/cmd/crowdsec-cli/flag.go @@ -0,0 +1,28 @@ +package main + +// Custom types for flag validation and conversion. + +import ( + "errors" +) + +type MachinePassword string + +func (p *MachinePassword) String() string { + return string(*p) +} + +func (p *MachinePassword) Set(v string) error { + // a password can't be more than 72 characters + // due to bcrypt limitations + if len(v) > 72 { + return errors.New("password too long (max 72 characters)") + } + *p = MachinePassword(v) + + return nil +} + +func (p *MachinePassword) Type() string { + return "string" +} diff --git a/cmd/crowdsec-cli/items.go b/cmd/crowdsec-cli/items.go index a1d079747..851be553f 100644 --- a/cmd/crowdsec-cli/items.go +++ b/cmd/crowdsec-cli/items.go @@ -138,8 +138,6 @@ func listItems(out io.Writer, itemTypes []string, items map[string][]*cwhub.Item } csvwriter.Flush() - default: - return fmt.Errorf("unknown output format '%s'", csConfig.Cscli.Output) } return nil diff --git a/cmd/crowdsec-cli/machines.go b/cmd/crowdsec-cli/machines.go index 581683baa..0cabccf76 100644 --- a/cmd/crowdsec-cli/machines.go +++ b/cmd/crowdsec-cli/machines.go @@ -5,7 +5,6 @@ import ( "encoding/csv" "encoding/json" "fmt" - "io" "math/big" "os" "strings" @@ -101,53 +100,18 @@ func getLastHeartbeat(m *ent.Machine) (string, bool) { return hb, true } -func getAgents(out io.Writer, dbClient *database.Client) error { - machines, err := dbClient.ListMachines() - if err != nil { - return fmt.Errorf("unable to list machines: %s", err) - } - - switch csConfig.Cscli.Output { - case "human": - getAgentsTable(out, machines) - case "json": - enc := json.NewEncoder(out) - enc.SetIndent("", " ") - if err := enc.Encode(machines); err != nil { - return fmt.Errorf("failed to marshal") - } - return nil - case "raw": - csvwriter := csv.NewWriter(out) - err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat"}) - if err != nil { - return fmt.Errorf("failed to write header: %s", err) - } - for _, m := range machines { - validated := "false" - if m.IsValidated { - validated = "true" - } - hb, _ := getLastHeartbeat(m) - err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb}) - if err != nil { - return fmt.Errorf("failed to write raw output: %w", err) - } - } - csvwriter.Flush() - default: - return fmt.Errorf("unknown output '%s'", csConfig.Cscli.Output) - } - return nil +type cliMachines struct{ + db *database.Client + cfg configGetter } -type cliMachines struct{} - -func NewCLIMachines() *cliMachines { - return &cliMachines{} +func NewCLIMachines(getconfig configGetter) *cliMachines { + return &cliMachines{ + cfg: getconfig, + } } -func (cli cliMachines) NewCommand() *cobra.Command { +func (cli *cliMachines) NewCommand() *cobra.Command { cmd := &cobra.Command{ Use: "machines [action]", Short: "Manage local API machines [requires local API]", @@ -159,10 +123,10 @@ Note: This command requires database direct access, so is intended to be run on Aliases: []string{"machine"}, PersistentPreRunE: func(_ *cobra.Command, _ []string) error { var err error - if err = require.LAPI(csConfig); err != nil { + if err = require.LAPI(cli.cfg()); err != nil { return err } - dbClient, err = database.NewClient(csConfig.DbConfig) + cli.db, err = database.NewClient(cli.cfg().DbConfig) if err != nil { return fmt.Errorf("unable to create new database client: %s", err) } @@ -170,16 +134,63 @@ Note: This command requires database direct access, so is intended to be run on }, } - cmd.AddCommand(cli.NewListCmd()) - cmd.AddCommand(cli.NewAddCmd()) - cmd.AddCommand(cli.NewDeleteCmd()) - cmd.AddCommand(cli.NewValidateCmd()) - cmd.AddCommand(cli.NewPruneCmd()) + cmd.AddCommand(cli.newListCmd()) + cmd.AddCommand(cli.newAddCmd()) + cmd.AddCommand(cli.newDeleteCmd()) + cmd.AddCommand(cli.newValidateCmd()) + cmd.AddCommand(cli.newPruneCmd()) return cmd } -func (cli cliMachines) NewListCmd() *cobra.Command { +func (cli *cliMachines) list() error { + out := color.Output + + machines, err := cli.db.ListMachines() + if err != nil { + return fmt.Errorf("unable to list machines: %s", err) + } + + switch cli.cfg().Cscli.Output { + case "human": + getAgentsTable(out, machines) + case "json": + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + + if err := enc.Encode(machines); err != nil { + return fmt.Errorf("failed to marshal") + } + + return nil + case "raw": + csvwriter := csv.NewWriter(out) + + err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat"}) + if err != nil { + return fmt.Errorf("failed to write header: %s", err) + } + + for _, m := range machines { + validated := "false" + if m.IsValidated { + validated = "true" + } + + hb, _ := getLastHeartbeat(m) + + if err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb}); err != nil { + return fmt.Errorf("failed to write raw output: %w", err) + } + } + + csvwriter.Flush() + } + + return nil +} + +func (cli *cliMachines) newListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "list all machines in the database", @@ -188,84 +199,60 @@ func (cli cliMachines) NewListCmd() *cobra.Command { Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { - err := getAgents(color.Output, dbClient) - if err != nil { - return fmt.Errorf("unable to list machines: %s", err) - } - - return nil + return cli.list() }, } return cmd } -func (cli cliMachines) NewAddCmd() *cobra.Command { +func (cli *cliMachines) newAddCmd() *cobra.Command { + var ( + password MachinePassword + dumpFile string + apiURL string + interactive bool + autoAdd bool + force bool + ) + cmd := &cobra.Command{ Use: "add", Short: "add a single machine to the database", DisableAutoGenTag: true, Long: `Register a new machine in the database. cscli should be on the same machine as LAPI.`, - Example: ` -cscli machines add --auto + Example: `cscli machines add --auto cscli machines add MyTestMachine --auto cscli machines add MyTestMachine --password MyPassword -`, - RunE: cli.add, +cscli machines add -f- --auto > /tmp/mycreds.yaml`, + RunE: func(_ *cobra.Command, args []string) error { + return cli.add(args, string(password), dumpFile, apiURL, interactive, autoAdd, force) + }, } flags := cmd.Flags() - flags.StringP("password", "p", "", "machine password to login to the API") - flags.StringP("file", "f", "", "output file destination (defaults to "+csconfig.DefaultConfigPath("local_api_credentials.yaml")+")") - flags.StringP("url", "u", "", "URL of the local API") - flags.BoolP("interactive", "i", false, "interfactive mode to enter the password") - flags.BoolP("auto", "a", false, "automatically generate password (and username if not provided)") - flags.Bool("force", false, "will force add the machine if it already exist") + flags.VarP(&password, "password", "p", "machine password to login to the API") + flags.StringVarP(&dumpFile, "file", "f", "", "output file destination (defaults to "+csconfig.DefaultConfigPath("local_api_credentials.yaml")+")") + flags.StringVarP(&apiURL, "url", "u", "", "URL of the local API") + flags.BoolVarP(&interactive, "interactive", "i", false, "interfactive mode to enter the password") + flags.BoolVarP(&autoAdd, "auto", "a", false, "automatically generate password (and username if not provided)") + flags.BoolVar(&force, "force", false, "will force add the machine if it already exist") return cmd } -func (cli cliMachines) add(cmd *cobra.Command, args []string) error { - flags := cmd.Flags() - - machinePassword, err := flags.GetString("password") - if err != nil { - return err - } - - dumpFile, err := flags.GetString("file") - if err != nil { - return err - } - - apiURL, err := flags.GetString("url") - if err != nil { - return err - } - - interactive, err := flags.GetBool("interactive") - if err != nil { - return err - } - - autoAdd, err := flags.GetBool("auto") - if err != nil { - return err - } - - force, err := flags.GetBool("force") - if err != nil { - return err - } - - var machineID string +func (cli *cliMachines) add(args []string, machinePassword string, dumpFile string, apiURL string, interactive bool, autoAdd bool, force bool) error { + var ( + err error + machineID string + ) // create machineID if not specified by user if len(args) == 0 { if !autoAdd { - printHelp(cmd) - return nil + return fmt.Errorf("please specify a machine name to add, or use --auto") } + machineID, err = generateID("") if err != nil { return fmt.Errorf("unable to generate machine id: %s", err) @@ -274,15 +261,18 @@ func (cli cliMachines) add(cmd *cobra.Command, args []string) error { machineID = args[0] } + clientCfg := cli.cfg().API.Client + serverCfg := cli.cfg().API.Server + /*check if file already exists*/ - if dumpFile == "" && csConfig.API.Client != nil && csConfig.API.Client.CredentialsFilePath != "" { - credFile := csConfig.API.Client.CredentialsFilePath + if dumpFile == "" && clientCfg != nil && clientCfg.CredentialsFilePath != "" { + credFile := clientCfg.CredentialsFilePath // use the default only if the file does not exist _, err = os.Stat(credFile) switch { case os.IsNotExist(err) || force: - dumpFile = csConfig.API.Client.CredentialsFilePath + dumpFile = credFile case err != nil: return fmt.Errorf("unable to stat '%s': %s", credFile, err) default: @@ -302,78 +292,74 @@ func (cli cliMachines) add(cmd *cobra.Command, args []string) error { machinePassword = generatePassword(passwordLength) } else if machinePassword == "" && interactive { qs := &survey.Password{ - Message: "Please provide a password for the machine", + Message: "Please provide a password for the machine:", } survey.AskOne(qs, &machinePassword) } + password := strfmt.Password(machinePassword) - _, err = dbClient.CreateMachine(&machineID, &password, "", true, force, types.PasswordAuthType) + + _, err = cli.db.CreateMachine(&machineID, &password, "", true, force, types.PasswordAuthType) if err != nil { return fmt.Errorf("unable to create machine: %s", err) } - fmt.Printf("Machine '%s' successfully added to the local API.\n", machineID) + + fmt.Fprintf(os.Stderr, "Machine '%s' successfully added to the local API.\n", machineID) if apiURL == "" { - if csConfig.API.Client != nil && csConfig.API.Client.Credentials != nil && csConfig.API.Client.Credentials.URL != "" { - apiURL = csConfig.API.Client.Credentials.URL - } else if csConfig.API.Server != nil && csConfig.API.Server.ListenURI != "" { - apiURL = "http://" + csConfig.API.Server.ListenURI + if clientCfg != nil && clientCfg.Credentials != nil && clientCfg.Credentials.URL != "" { + apiURL = clientCfg.Credentials.URL + } else if serverCfg != nil && serverCfg.ListenURI != "" { + apiURL = "http://" + serverCfg.ListenURI } else { return fmt.Errorf("unable to dump an api URL. Please provide it in your configuration or with the -u parameter") } } + apiCfg := csconfig.ApiCredentialsCfg{ Login: machineID, Password: password.String(), URL: apiURL, } + apiConfigDump, err := yaml.Marshal(apiCfg) if err != nil { return fmt.Errorf("unable to marshal api credentials: %s", err) } + if dumpFile != "" && dumpFile != "-" { err = os.WriteFile(dumpFile, apiConfigDump, 0o600) if err != nil { return fmt.Errorf("write api credentials in '%s' failed: %s", dumpFile, err) } - fmt.Printf("API credentials written to '%s'.\n", dumpFile) + fmt.Fprintf(os.Stderr, "API credentials written to '%s'.\n", dumpFile) } else { - fmt.Printf("%s\n", string(apiConfigDump)) + fmt.Print(string(apiConfigDump)) } return nil } -func (cli cliMachines) NewDeleteCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "delete [machine_name]...", - Short: "delete machine(s) by name", - Example: `cscli machines delete "machine1" "machine2"`, - Args: cobra.MinimumNArgs(1), - Aliases: []string{"remove"}, - DisableAutoGenTag: true, - ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - machines, err := dbClient.ListMachines() - if err != nil { - cobra.CompError("unable to list machines " + err.Error()) - } - ret := make([]string, 0) - for _, machine := range machines { - if strings.Contains(machine.MachineId, toComplete) && !slices.Contains(args, machine.MachineId) { - ret = append(ret, machine.MachineId) - } - } - return ret, cobra.ShellCompDirectiveNoFileComp - }, - RunE: cli.delete, +func (cli *cliMachines) deleteValid(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + machines, err := cli.db.ListMachines() + if err != nil { + cobra.CompError("unable to list machines " + err.Error()) } - return cmd + ret := []string{} + + for _, machine := range machines { + if strings.Contains(machine.MachineId, toComplete) && !slices.Contains(args, machine.MachineId) { + ret = append(ret, machine.MachineId) + } + } + + return ret, cobra.ShellCompDirectiveNoFileComp } -func (cli cliMachines) delete(_ *cobra.Command, args []string) error { - for _, machineID := range args { - err := dbClient.DeleteWatcher(machineID) +func (cli *cliMachines) delete(machines []string) error { + for _, machineID := range machines { + err := cli.db.DeleteWatcher(machineID) if err != nil { log.Errorf("unable to delete machine '%s': %s", machineID, err) return nil @@ -384,8 +370,83 @@ func (cli cliMachines) delete(_ *cobra.Command, args []string) error { return nil } -func (cli cliMachines) NewPruneCmd() *cobra.Command { - var parsedDuration time.Duration +func (cli *cliMachines) newDeleteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete [machine_name]...", + Short: "delete machine(s) by name", + Example: `cscli machines delete "machine1" "machine2"`, + Args: cobra.MinimumNArgs(1), + Aliases: []string{"remove"}, + DisableAutoGenTag: true, + ValidArgsFunction: cli.deleteValid, + RunE: func(_ *cobra.Command, args []string) error { + return cli.delete(args) + }, + } + + return cmd +} + +func (cli *cliMachines) prune(duration time.Duration, notValidOnly bool, force bool) error { + if duration < 2*time.Minute && !notValidOnly { + if yes, err := askYesNo( + "The duration you provided is less than 2 minutes. " + + "This can break installations if the machines are only temporarily disconnected. Continue?", false); err != nil { + return err + } else if !yes { + fmt.Println("User aborted prune. No changes were made.") + return nil + } + } + + machines := []*ent.Machine{} + if pending, err := cli.db.QueryPendingMachine(); err == nil { + machines = append(machines, pending...) + } + + if !notValidOnly { + if pending, err := cli.db.QueryLastValidatedHeartbeatLT(time.Now().UTC().Add(duration)); err == nil { + machines = append(machines, pending...) + } + } + + if len(machines) == 0 { + fmt.Println("no machines to prune") + return nil + } + + getAgentsTable(color.Output, machines) + + if !force { + if yes, err := askYesNo( + "You are about to PERMANENTLY remove the above machines from the database. " + + "These will NOT be recoverable. Continue?", false); err != nil { + return err + } else if !yes { + fmt.Println("User aborted prune. No changes were made.") + return nil + } + } + + deleted, err := cli.db.BulkDeleteWatchers(machines) + if err != nil { + return fmt.Errorf("unable to prune machines: %s", err) + } + + fmt.Fprintf(os.Stderr, "successfully delete %d machines\n", deleted) + + return nil +} + +func (cli *cliMachines) newPruneCmd() *cobra.Command { + var ( + duration time.Duration + notValidOnly bool + force bool + ) + + const defaultDuration = 10 * time.Minute + cmd := &cobra.Command{ Use: "prune", Short: "prune multiple machines from the database", @@ -395,76 +456,29 @@ cscli machines prune --duration 1h cscli machines prune --not-validated-only --force`, Args: cobra.NoArgs, DisableAutoGenTag: true, - PreRunE: func(cmd *cobra.Command, _ []string) error { - dur, _ := cmd.Flags().GetString("duration") - var err error - parsedDuration, err = time.ParseDuration(fmt.Sprintf("-%s", dur)) - if err != nil { - return fmt.Errorf("unable to parse duration '%s': %s", dur, err) - } - return nil - }, - RunE: func(cmd *cobra.Command, _ []string) error { - notValidOnly, _ := cmd.Flags().GetBool("not-validated-only") - force, _ := cmd.Flags().GetBool("force") - if parsedDuration >= 0-60*time.Second && !notValidOnly { - var answer bool - prompt := &survey.Confirm{ - Message: "The duration you provided is less than or equal 60 seconds this can break installations do you want to continue ?", - Default: false, - } - if err := survey.AskOne(prompt, &answer); err != nil { - return fmt.Errorf("unable to ask about prune check: %s", err) - } - if !answer { - fmt.Println("user aborted prune no changes were made") - return nil - } - } - machines := make([]*ent.Machine, 0) - if pending, err := dbClient.QueryPendingMachine(); err == nil { - machines = append(machines, pending...) - } - if !notValidOnly { - if pending, err := dbClient.QueryLastValidatedHeartbeatLT(time.Now().UTC().Add(parsedDuration)); err == nil { - machines = append(machines, pending...) - } - } - if len(machines) == 0 { - fmt.Println("no machines to prune") - return nil - } - getAgentsTable(color.Output, machines) - if !force { - var answer bool - prompt := &survey.Confirm{ - Message: "You are about to PERMANENTLY remove the above machines from the database these will NOT be recoverable, continue ?", - Default: false, - } - if err := survey.AskOne(prompt, &answer); err != nil { - return fmt.Errorf("unable to ask about prune check: %s", err) - } - if !answer { - fmt.Println("user aborted prune no changes were made") - return nil - } - } - nbDeleted, err := dbClient.BulkDeleteWatchers(machines) - if err != nil { - return fmt.Errorf("unable to prune machines: %s", err) - } - fmt.Printf("successfully delete %d machines\n", nbDeleted) - return nil + RunE: func(_ *cobra.Command, _ []string) error { + return cli.prune(duration, notValidOnly, force) }, } - cmd.Flags().StringP("duration", "d", "10m", "duration of time since validated machine last heartbeat") - cmd.Flags().Bool("not-validated-only", false, "only prune machines that are not validated") - cmd.Flags().Bool("force", false, "force prune without asking for confirmation") + + flags := cmd.Flags() + flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since validated machine last heartbeat") + flags.BoolVar(¬ValidOnly, "not-validated-only", false, "only prune machines that are not validated") + flags.BoolVar(&force, "force", false, "force prune without asking for confirmation") return cmd } -func (cli cliMachines) NewValidateCmd() *cobra.Command { +func (cli *cliMachines) validate(machineID string) error { + if err := cli.db.ValidateMachine(machineID); err != nil { + return fmt.Errorf("unable to validate machine '%s': %s", machineID, err) + } + log.Infof("machine '%s' validated successfully", machineID) + + return nil +} + +func (cli *cliMachines) newValidateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "validate", Short: "validate a machine to access the local API", @@ -472,14 +486,8 @@ func (cli cliMachines) NewValidateCmd() *cobra.Command { Example: `cscli machines validate "machine_name"`, Args: cobra.ExactArgs(1), DisableAutoGenTag: true, - RunE: func(_ *cobra.Command, args []string) error { - machineID := args[0] - if err := dbClient.ValidateMachine(machineID); err != nil { - return fmt.Errorf("unable to validate machine '%s': %s", machineID, err) - } - log.Infof("machine '%s' validated successfully", machineID) - - return nil + RunE: func(cmd *cobra.Command, args []string) error { + return cli.validate(args[0]) }, } diff --git a/cmd/crowdsec-cli/main.go b/cmd/crowdsec-cli/main.go index fda4cddc2..91e31a977 100644 --- a/cmd/crowdsec-cli/main.go +++ b/cmd/crowdsec-cli/main.go @@ -21,7 +21,7 @@ var ConfigFilePath string var csConfig *csconfig.Config var dbClient *database.Client -var OutputFormat string +var outputFormat string var OutputColor string var mergedConfig string @@ -29,6 +29,8 @@ var mergedConfig string // flagBranch overrides the value in csConfig.Cscli.HubBranch var flagBranch = "" +type configGetter func() *csconfig.Config + func initConfig() { var err error @@ -64,16 +66,18 @@ func initConfig() { csConfig.Cscli.HubBranch = flagBranch } - if OutputFormat != "" { - csConfig.Cscli.Output = OutputFormat - - if OutputFormat != "json" && OutputFormat != "raw" && OutputFormat != "human" { - log.Fatalf("output format %s unknown", OutputFormat) - } + if outputFormat != "" { + csConfig.Cscli.Output = outputFormat } + if csConfig.Cscli.Output == "" { csConfig.Cscli.Output = "human" } + + if csConfig.Cscli.Output != "human" && csConfig.Cscli.Output != "json" && csConfig.Cscli.Output != "raw" { + log.Fatalf("output format '%s' not supported: must be one of human, json, raw", csConfig.Cscli.Output) + } + if csConfig.Cscli.Output == "json" { log.SetFormatter(&log.JSONFormatter{}) log.SetLevel(log.ErrorLevel) @@ -146,7 +150,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall cmd.SetOut(color.Output) cmd.PersistentFlags().StringVarP(&ConfigFilePath, "config", "c", csconfig.DefaultConfigPath("config.yaml"), "path to crowdsec config file") - cmd.PersistentFlags().StringVarP(&OutputFormat, "output", "o", "", "Output format: human, json, raw") + cmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "", "Output format: human, json, raw") cmd.PersistentFlags().StringVarP(&OutputColor, "color", "", "auto", "Output color: yes, no, auto") cmd.PersistentFlags().BoolVar(&dbg_lvl, "debug", false, "Set logging to debug") cmd.PersistentFlags().BoolVar(&nfo_lvl, "info", false, "Set logging to info") @@ -197,7 +201,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall cmd.AddCommand(NewCLIAlerts().NewCommand()) cmd.AddCommand(NewCLISimulation().NewCommand()) cmd.AddCommand(NewCLIBouncers(getconfig).NewCommand()) - cmd.AddCommand(NewCLIMachines().NewCommand()) + cmd.AddCommand(NewCLIMachines(getconfig).NewCommand()) cmd.AddCommand(NewCLICapi().NewCommand()) cmd.AddCommand(NewLapiCmd()) cmd.AddCommand(NewCompletionCmd()) diff --git a/cmd/crowdsec-cli/support.go b/cmd/crowdsec-cli/support.go index 47768e7c2..ed7f7cf2f 100644 --- a/cmd/crowdsec-cli/support.go +++ b/cmd/crowdsec-cli/support.go @@ -159,10 +159,11 @@ func collectBouncers(dbClient *database.Client) ([]byte, error) { func collectAgents(dbClient *database.Client) ([]byte, error) { out := bytes.NewBuffer(nil) - err := getAgents(out, dbClient) + machines, err := dbClient.ListMachines() if err != nil { - return nil, err + return nil, fmt.Errorf("unable to list machines: %s", err) } + getAgentsTable(out, machines) return out.Bytes(), nil } diff --git a/test/bats/30_machines.bats b/test/bats/30_machines.bats index c7a72c334..f32c376e5 100644 --- a/test/bats/30_machines.bats +++ b/test/bats/30_machines.bats @@ -34,13 +34,18 @@ teardown() { rune -0 jq -r '.msg' <(stderr) assert_output --partial 'already exists: please remove it, use "--force" or specify a different file with "-f"' rune -0 cscli machines add local -a --force - assert_output --partial "Machine 'local' successfully added to the local API." + assert_stderr --partial "Machine 'local' successfully added to the local API." +} + +@test "passwords have a size limit" { + rune -1 cscli machines add local --password "$(printf '%73s' '' | tr ' ' x)" + assert_stderr --partial "password too long (max 72 characters)" } @test "add a new machine and delete it" { rune -0 cscli machines add -a -f /dev/null CiTestMachine -o human - assert_output --partial "Machine 'CiTestMachine' successfully added to the local API" - assert_output --partial "API credentials written to '/dev/null'" + assert_stderr --partial "Machine 'CiTestMachine' successfully added to the local API" + assert_stderr --partial "API credentials written to '/dev/null'" # we now have two machines rune -0 cscli machines list -o json