refact "cscli machines" (#2777)

This commit is contained in:
mmetc 2024-02-01 17:22:52 +01:00 committed by GitHub
parent 4192af30d5
commit 17db4cb970
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 279 additions and 236 deletions

View file

@ -16,7 +16,6 @@ import (
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1" 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/database"
"github.com/crowdsecurity/crowdsec/pkg/types" "github.com/crowdsecurity/crowdsec/pkg/types"
) )
@ -38,10 +37,10 @@ func askYesNo(message string, defaultAnswer bool) (bool, error) {
type cliBouncers struct { type cliBouncers struct {
db *database.Client db *database.Client
cfg func() *csconfig.Config cfg configGetter
} }
func NewCLIBouncers(getconfig func() *csconfig.Config) *cliBouncers { func NewCLIBouncers(getconfig configGetter) *cliBouncers {
return &cliBouncers{ return &cliBouncers{
cfg: getconfig, cfg: getconfig,
} }

28
cmd/crowdsec-cli/flag.go Normal file
View file

@ -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"
}

View file

@ -138,8 +138,6 @@ func listItems(out io.Writer, itemTypes []string, items map[string][]*cwhub.Item
} }
csvwriter.Flush() csvwriter.Flush()
default:
return fmt.Errorf("unknown output format '%s'", csConfig.Cscli.Output)
} }
return nil return nil

View file

@ -5,7 +5,6 @@ import (
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"math/big" "math/big"
"os" "os"
"strings" "strings"
@ -101,53 +100,18 @@ func getLastHeartbeat(m *ent.Machine) (string, bool) {
return hb, true return hb, true
} }
func getAgents(out io.Writer, dbClient *database.Client) error { type cliMachines struct{
machines, err := dbClient.ListMachines() db *database.Client
if err != nil { cfg configGetter
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{} func NewCLIMachines(getconfig configGetter) *cliMachines {
return &cliMachines{
func NewCLIMachines() *cliMachines { cfg: getconfig,
return &cliMachines{} }
} }
func (cli cliMachines) NewCommand() *cobra.Command { func (cli *cliMachines) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "machines [action]", Use: "machines [action]",
Short: "Manage local API machines [requires local API]", 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"}, Aliases: []string{"machine"},
PersistentPreRunE: func(_ *cobra.Command, _ []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
var err error var err error
if err = require.LAPI(csConfig); err != nil { if err = require.LAPI(cli.cfg()); err != nil {
return err return err
} }
dbClient, err = database.NewClient(csConfig.DbConfig) cli.db, err = database.NewClient(cli.cfg().DbConfig)
if err != nil { if err != nil {
return fmt.Errorf("unable to create new database client: %s", err) 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.newListCmd())
cmd.AddCommand(cli.NewAddCmd()) cmd.AddCommand(cli.newAddCmd())
cmd.AddCommand(cli.NewDeleteCmd()) cmd.AddCommand(cli.newDeleteCmd())
cmd.AddCommand(cli.NewValidateCmd()) cmd.AddCommand(cli.newValidateCmd())
cmd.AddCommand(cli.NewPruneCmd()) cmd.AddCommand(cli.newPruneCmd())
return cmd 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{ cmd := &cobra.Command{
Use: "list", Use: "list",
Short: "list all machines in the database", Short: "list all machines in the database",
@ -188,84 +199,60 @@ func (cli cliMachines) NewListCmd() *cobra.Command {
Args: cobra.NoArgs, Args: cobra.NoArgs,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
err := getAgents(color.Output, dbClient) return cli.list()
if err != nil {
return fmt.Errorf("unable to list machines: %s", err)
}
return nil
}, },
} }
return cmd 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{ cmd := &cobra.Command{
Use: "add", Use: "add",
Short: "add a single machine to the database", Short: "add a single machine to the database",
DisableAutoGenTag: true, DisableAutoGenTag: true,
Long: `Register a new machine in the database. cscli should be on the same machine as LAPI.`, Long: `Register a new machine in the database. cscli should be on the same machine as LAPI.`,
Example: ` Example: `cscli machines add --auto
cscli machines add --auto
cscli machines add MyTestMachine --auto cscli machines add MyTestMachine --auto
cscli machines add MyTestMachine --password MyPassword cscli machines add MyTestMachine --password MyPassword
`, cscli machines add -f- --auto > /tmp/mycreds.yaml`,
RunE: cli.add, RunE: func(_ *cobra.Command, args []string) error {
return cli.add(args, string(password), dumpFile, apiURL, interactive, autoAdd, force)
},
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringP("password", "p", "", "machine password to login to the API") flags.VarP(&password, "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.StringVarP(&dumpFile, "file", "f", "", "output file destination (defaults to "+csconfig.DefaultConfigPath("local_api_credentials.yaml")+")")
flags.StringP("url", "u", "", "URL of the local API") flags.StringVarP(&apiURL, "url", "u", "", "URL of the local API")
flags.BoolP("interactive", "i", false, "interfactive mode to enter the password") flags.BoolVarP(&interactive, "interactive", "i", false, "interfactive mode to enter the password")
flags.BoolP("auto", "a", false, "automatically generate password (and username if not provided)") flags.BoolVarP(&autoAdd, "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.BoolVar(&force, "force", false, "will force add the machine if it already exist")
return cmd return cmd
} }
func (cli cliMachines) add(cmd *cobra.Command, args []string) error { func (cli *cliMachines) add(args []string, machinePassword string, dumpFile string, apiURL string, interactive bool, autoAdd bool, force bool) error {
flags := cmd.Flags() var (
err error
machinePassword, err := flags.GetString("password") machineID string
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
// create machineID if not specified by user // create machineID if not specified by user
if len(args) == 0 { if len(args) == 0 {
if !autoAdd { if !autoAdd {
printHelp(cmd) return fmt.Errorf("please specify a machine name to add, or use --auto")
return nil
} }
machineID, err = generateID("") machineID, err = generateID("")
if err != nil { if err != nil {
return fmt.Errorf("unable to generate machine id: %s", err) 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] machineID = args[0]
} }
clientCfg := cli.cfg().API.Client
serverCfg := cli.cfg().API.Server
/*check if file already exists*/ /*check if file already exists*/
if dumpFile == "" && csConfig.API.Client != nil && csConfig.API.Client.CredentialsFilePath != "" { if dumpFile == "" && clientCfg != nil && clientCfg.CredentialsFilePath != "" {
credFile := csConfig.API.Client.CredentialsFilePath credFile := clientCfg.CredentialsFilePath
// use the default only if the file does not exist // use the default only if the file does not exist
_, err = os.Stat(credFile) _, err = os.Stat(credFile)
switch { switch {
case os.IsNotExist(err) || force: case os.IsNotExist(err) || force:
dumpFile = csConfig.API.Client.CredentialsFilePath dumpFile = credFile
case err != nil: case err != nil:
return fmt.Errorf("unable to stat '%s': %s", credFile, err) return fmt.Errorf("unable to stat '%s': %s", credFile, err)
default: default:
@ -302,78 +292,74 @@ func (cli cliMachines) add(cmd *cobra.Command, args []string) error {
machinePassword = generatePassword(passwordLength) machinePassword = generatePassword(passwordLength)
} else if machinePassword == "" && interactive { } else if machinePassword == "" && interactive {
qs := &survey.Password{ qs := &survey.Password{
Message: "Please provide a password for the machine", Message: "Please provide a password for the machine:",
} }
survey.AskOne(qs, &machinePassword) survey.AskOne(qs, &machinePassword)
} }
password := strfmt.Password(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 { if err != nil {
return fmt.Errorf("unable to create machine: %s", err) 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 apiURL == "" {
if csConfig.API.Client != nil && csConfig.API.Client.Credentials != nil && csConfig.API.Client.Credentials.URL != "" { if clientCfg != nil && clientCfg.Credentials != nil && clientCfg.Credentials.URL != "" {
apiURL = csConfig.API.Client.Credentials.URL apiURL = clientCfg.Credentials.URL
} else if csConfig.API.Server != nil && csConfig.API.Server.ListenURI != "" { } else if serverCfg != nil && serverCfg.ListenURI != "" {
apiURL = "http://" + csConfig.API.Server.ListenURI apiURL = "http://" + serverCfg.ListenURI
} else { } else {
return fmt.Errorf("unable to dump an api URL. Please provide it in your configuration or with the -u parameter") return fmt.Errorf("unable to dump an api URL. Please provide it in your configuration or with the -u parameter")
} }
} }
apiCfg := csconfig.ApiCredentialsCfg{ apiCfg := csconfig.ApiCredentialsCfg{
Login: machineID, Login: machineID,
Password: password.String(), Password: password.String(),
URL: apiURL, URL: apiURL,
} }
apiConfigDump, err := yaml.Marshal(apiCfg) apiConfigDump, err := yaml.Marshal(apiCfg)
if err != nil { if err != nil {
return fmt.Errorf("unable to marshal api credentials: %s", err) return fmt.Errorf("unable to marshal api credentials: %s", err)
} }
if dumpFile != "" && dumpFile != "-" { if dumpFile != "" && dumpFile != "-" {
err = os.WriteFile(dumpFile, apiConfigDump, 0o600) err = os.WriteFile(dumpFile, apiConfigDump, 0o600)
if err != nil { if err != nil {
return fmt.Errorf("write api credentials in '%s' failed: %s", dumpFile, err) 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 { } else {
fmt.Printf("%s\n", string(apiConfigDump)) fmt.Print(string(apiConfigDump))
} }
return nil return nil
} }
func (cli cliMachines) NewDeleteCmd() *cobra.Command { func (cli *cliMachines) deleteValid(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
cmd := &cobra.Command{ machines, err := cli.db.ListMachines()
Use: "delete [machine_name]...", if err != nil {
Short: "delete machine(s) by name", cobra.CompError("unable to list machines " + err.Error())
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,
} }
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 { func (cli *cliMachines) delete(machines []string) error {
for _, machineID := range args { for _, machineID := range machines {
err := dbClient.DeleteWatcher(machineID) err := cli.db.DeleteWatcher(machineID)
if err != nil { if err != nil {
log.Errorf("unable to delete machine '%s': %s", machineID, err) log.Errorf("unable to delete machine '%s': %s", machineID, err)
return nil return nil
@ -384,8 +370,83 @@ func (cli cliMachines) delete(_ *cobra.Command, args []string) error {
return nil return nil
} }
func (cli cliMachines) NewPruneCmd() *cobra.Command { func (cli *cliMachines) newDeleteCmd() *cobra.Command {
var parsedDuration time.Duration 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{ cmd := &cobra.Command{
Use: "prune", Use: "prune",
Short: "prune multiple machines from the database", Short: "prune multiple machines from the database",
@ -395,76 +456,29 @@ cscli machines prune --duration 1h
cscli machines prune --not-validated-only --force`, cscli machines prune --not-validated-only --force`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
DisableAutoGenTag: true, DisableAutoGenTag: true,
PreRunE: func(cmd *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
dur, _ := cmd.Flags().GetString("duration") return cli.prune(duration, notValidOnly, force)
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
}, },
} }
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") flags := cmd.Flags()
cmd.Flags().Bool("force", false, "force prune without asking for confirmation") flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since validated machine last heartbeat")
flags.BoolVar(&notValidOnly, "not-validated-only", false, "only prune machines that are not validated")
flags.BoolVar(&force, "force", false, "force prune without asking for confirmation")
return cmd 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{ cmd := &cobra.Command{
Use: "validate", Use: "validate",
Short: "validate a machine to access the local API", 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"`, Example: `cscli machines validate "machine_name"`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
machineID := args[0] return cli.validate(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
}, },
} }

View file

@ -21,7 +21,7 @@ var ConfigFilePath string
var csConfig *csconfig.Config var csConfig *csconfig.Config
var dbClient *database.Client var dbClient *database.Client
var OutputFormat string var outputFormat string
var OutputColor string var OutputColor string
var mergedConfig string var mergedConfig string
@ -29,6 +29,8 @@ var mergedConfig string
// flagBranch overrides the value in csConfig.Cscli.HubBranch // flagBranch overrides the value in csConfig.Cscli.HubBranch
var flagBranch = "" var flagBranch = ""
type configGetter func() *csconfig.Config
func initConfig() { func initConfig() {
var err error var err error
@ -64,16 +66,18 @@ func initConfig() {
csConfig.Cscli.HubBranch = flagBranch csConfig.Cscli.HubBranch = flagBranch
} }
if OutputFormat != "" { if outputFormat != "" {
csConfig.Cscli.Output = OutputFormat csConfig.Cscli.Output = outputFormat
if OutputFormat != "json" && OutputFormat != "raw" && OutputFormat != "human" {
log.Fatalf("output format %s unknown", OutputFormat)
}
} }
if csConfig.Cscli.Output == "" { if csConfig.Cscli.Output == "" {
csConfig.Cscli.Output = "human" 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" { if csConfig.Cscli.Output == "json" {
log.SetFormatter(&log.JSONFormatter{}) log.SetFormatter(&log.JSONFormatter{})
log.SetLevel(log.ErrorLevel) 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.SetOut(color.Output)
cmd.PersistentFlags().StringVarP(&ConfigFilePath, "config", "c", csconfig.DefaultConfigPath("config.yaml"), "path to crowdsec config file") 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().StringVarP(&OutputColor, "color", "", "auto", "Output color: yes, no, auto")
cmd.PersistentFlags().BoolVar(&dbg_lvl, "debug", false, "Set logging to debug") cmd.PersistentFlags().BoolVar(&dbg_lvl, "debug", false, "Set logging to debug")
cmd.PersistentFlags().BoolVar(&nfo_lvl, "info", false, "Set logging to info") 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(NewCLIAlerts().NewCommand())
cmd.AddCommand(NewCLISimulation().NewCommand()) cmd.AddCommand(NewCLISimulation().NewCommand())
cmd.AddCommand(NewCLIBouncers(getconfig).NewCommand()) cmd.AddCommand(NewCLIBouncers(getconfig).NewCommand())
cmd.AddCommand(NewCLIMachines().NewCommand()) cmd.AddCommand(NewCLIMachines(getconfig).NewCommand())
cmd.AddCommand(NewCLICapi().NewCommand()) cmd.AddCommand(NewCLICapi().NewCommand())
cmd.AddCommand(NewLapiCmd()) cmd.AddCommand(NewLapiCmd())
cmd.AddCommand(NewCompletionCmd()) cmd.AddCommand(NewCompletionCmd())

View file

@ -159,10 +159,11 @@ func collectBouncers(dbClient *database.Client) ([]byte, error) {
func collectAgents(dbClient *database.Client) ([]byte, error) { func collectAgents(dbClient *database.Client) ([]byte, error) {
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
err := getAgents(out, dbClient) machines, err := dbClient.ListMachines()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("unable to list machines: %s", err)
} }
getAgentsTable(out, machines)
return out.Bytes(), nil return out.Bytes(), nil
} }

View file

@ -34,13 +34,18 @@ teardown() {
rune -0 jq -r '.msg' <(stderr) rune -0 jq -r '.msg' <(stderr)
assert_output --partial 'already exists: please remove it, use "--force" or specify a different file with "-f"' 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 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" { @test "add a new machine and delete it" {
rune -0 cscli machines add -a -f /dev/null CiTestMachine -o human rune -0 cscli machines add -a -f /dev/null CiTestMachine -o human
assert_output --partial "Machine 'CiTestMachine' successfully added to the local API" assert_stderr --partial "Machine 'CiTestMachine' successfully added to the local API"
assert_output --partial "API credentials written to '/dev/null'" assert_stderr --partial "API credentials written to '/dev/null'"
# we now have two machines # we now have two machines
rune -0 cscli machines list -o json rune -0 cscli machines list -o json