command cscli [machines|bouncers] inspect (#3103)

* command cscli [machines|bouncers] inspect

* lint
This commit is contained in:
mmetc 2024-07-03 17:20:20 +02:00 committed by GitHub
parent 4635d04b22
commit 9cebcf96b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 527 additions and 147 deletions

View file

@ -143,6 +143,8 @@ linters-settings:
disabled: true
- name: struct-tag
disabled: true
- name: redundant-import-alias
disabled: true
- name: time-equal
disabled: true
- name: var-naming

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
@ -12,12 +13,16 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/database/ent/bouncer"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
@ -79,13 +84,92 @@ Note: This command requires database direct access, so is intended to be run on
cmd.AddCommand(cli.newAddCmd())
cmd.AddCommand(cli.newDeleteCmd())
cmd.AddCommand(cli.newPruneCmd())
cmd.AddCommand(cli.newInspectCmd())
return cmd
}
func (cli *cliBouncers) list() error {
out := color.Output
func (cli *cliBouncers) listHuman(out io.Writer, bouncers ent.Bouncers) {
t := newLightTable(out).Writer
t.AppendHeader(table.Row{"Name", "IP Address", "Valid", "Last API pull", "Type", "Version", "Auth Type"})
for _, b := range bouncers {
revoked := emoji.CheckMark
if b.Revoked {
revoked = emoji.Prohibited
}
lastPull := ""
if b.LastPull != nil {
lastPull = b.LastPull.Format(time.RFC3339)
}
t.AppendRow(table.Row{b.Name, b.IPAddress, revoked, lastPull, b.Type, b.Version, b.AuthType})
}
fmt.Fprintln(out, t.Render())
}
// bouncerInfo contains only the data we want for inspect/list
type bouncerInfo struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
Revoked bool `json:"revoked"`
IPAddress string `json:"ip_address"`
Type string `json:"type"`
Version string `json:"version"`
LastPull *time.Time `json:"last_pull"`
AuthType string `json:"auth_type"`
OS string `json:"os,omitempty"`
Featureflags []string `json:"featureflags,omitempty"`
}
func newBouncerInfo(b *ent.Bouncer) bouncerInfo {
return bouncerInfo{
CreatedAt: b.CreatedAt,
UpdatedAt: b.UpdatedAt,
Name: b.Name,
Revoked: b.Revoked,
IPAddress: b.IPAddress,
Type: b.Type,
Version: b.Version,
LastPull: b.LastPull,
AuthType: b.AuthType,
OS: b.GetOSNameAndVersion(),
Featureflags: b.GetFeatureFlagList(),
}
}
func (cli *cliBouncers) listCSV(out io.Writer, bouncers ent.Bouncers) error {
csvwriter := csv.NewWriter(out)
if err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"}); err != nil {
return fmt.Errorf("failed to write raw header: %w", err)
}
for _, b := range bouncers {
valid := "validated"
if b.Revoked {
valid = "pending"
}
lastPull := ""
if b.LastPull != nil {
lastPull = b.LastPull.Format(time.RFC3339)
}
if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, lastPull, b.Type, b.Version, b.AuthType}); err != nil {
return fmt.Errorf("failed to write raw: %w", err)
}
}
csvwriter.Flush()
return nil
}
func (cli *cliBouncers) list(out io.Writer) error {
bouncers, err := cli.db.ListBouncers()
if err != nil {
return fmt.Errorf("unable to list bouncers: %w", err)
@ -93,40 +177,23 @@ func (cli *cliBouncers) list() error {
switch cli.cfg().Cscli.Output {
case "human":
getBouncersTable(out, bouncers)
cli.listHuman(out, bouncers)
case "json":
info := make([]bouncerInfo, 0, len(bouncers))
for _, b := range bouncers {
info = append(info, newBouncerInfo(b))
}
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(bouncers); err != nil {
return fmt.Errorf("failed to marshal: %w", err)
if err := enc.Encode(info); err != nil {
return errors.New("failed to marshal")
}
return nil
case "raw":
csvwriter := csv.NewWriter(out)
if err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"}); err != nil {
return fmt.Errorf("failed to write raw header: %w", err)
}
for _, b := range bouncers {
valid := "validated"
if b.Revoked {
valid = "pending"
}
lastPull := ""
if b.LastPull != nil {
lastPull = b.LastPull.Format(time.RFC3339)
}
if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, lastPull, b.Type, b.Version, b.AuthType}); err != nil {
return fmt.Errorf("failed to write raw: %w", err)
}
}
csvwriter.Flush()
return cli.listCSV(out, bouncers)
}
return nil
@ -140,7 +207,7 @@ func (cli *cliBouncers) newListCmd() *cobra.Command {
Args: cobra.ExactArgs(0),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.list()
return cli.list(color.Output)
},
}
@ -206,13 +273,14 @@ cscli bouncers add MyBouncerName --key <random-key>`,
return cmd
}
func (cli *cliBouncers) deleteValid(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// need to load config and db because PersistentPreRunE is not called for completions
// validBouncerID returns a list of bouncer IDs for command completion
func (cli *cliBouncers) validBouncerID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var err error
cfg := cli.cfg()
// need to load config and db because PersistentPreRunE is not called for completions
if err = require.LAPI(cfg); err != nil {
cobra.CompError("unable to list bouncers " + err.Error())
return nil, cobra.ShellCompDirectiveNoFileComp
@ -261,7 +329,7 @@ func (cli *cliBouncers) newDeleteCmd() *cobra.Command {
Args: cobra.MinimumNArgs(1),
Aliases: []string{"remove"},
DisableAutoGenTag: true,
ValidArgsFunction: cli.deleteValid,
ValidArgsFunction: cli.validBouncerID,
RunE: func(_ *cobra.Command, args []string) error {
return cli.delete(args)
},
@ -292,7 +360,7 @@ func (cli *cliBouncers) prune(duration time.Duration, force bool) error {
return nil
}
getBouncersTable(color.Output, bouncers)
cli.listHuman(color.Output, bouncers)
if !force {
if yes, err := askYesNo(
@ -341,3 +409,84 @@ cscli bouncers prune -d 45m --force`,
return cmd
}
func (cli *cliBouncers) inspectHuman(out io.Writer, bouncer *ent.Bouncer) {
t := newTable(out).Writer
t.SetTitle("Bouncer: " + bouncer.Name)
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true},
})
lastPull := ""
if bouncer.LastPull != nil {
lastPull = bouncer.LastPull.String()
}
t.AppendRows([]table.Row{
{"Created At", bouncer.CreatedAt},
{"Last Update", bouncer.UpdatedAt},
{"Revoked?", bouncer.Revoked},
{"IP Address", bouncer.IPAddress},
{"Type", bouncer.Type},
{"Version", bouncer.Version},
{"Last Pull", lastPull},
{"Auth type", bouncer.AuthType},
{"OS", bouncer.GetOSNameAndVersion()},
})
for _, ff := range bouncer.GetFeatureFlagList() {
t.AppendRow(table.Row{"Feature Flags", ff})
}
fmt.Fprintln(out, t.Render())
}
func (cli *cliBouncers) inspect(bouncer *ent.Bouncer) error {
out := color.Output
outputFormat := cli.cfg().Cscli.Output
switch outputFormat {
case "human":
cli.inspectHuman(out, bouncer)
case "json":
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(newBouncerInfo(bouncer)); err != nil {
return errors.New("failed to marshal")
}
return nil
default:
return fmt.Errorf("output format '%s' not supported for this command", outputFormat)
}
return nil
}
func (cli *cliBouncers) newInspectCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "inspect [bouncer_name]",
Short: "inspect a bouncer by name",
Example: `cscli bouncers inspect "bouncer1"`,
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
ValidArgsFunction: cli.validBouncerID,
RunE: func(cmd *cobra.Command, args []string) error {
bouncerName := args[0]
b, err := cli.db.Ent.Bouncer.Query().
Where(bouncer.Name(bouncerName)).
Only(cmd.Context())
if err != nil {
return fmt.Errorf("unable to read bouncer data '%s': %w", bouncerName, err)
}
return cli.inspect(b)
},
}
return cmd
}

View file

@ -1,33 +0,0 @@
package main
import (
"io"
"time"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/table"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
)
func getBouncersTable(out io.Writer, bouncers []*ent.Bouncer) {
t := newLightTable(out)
t.SetHeaders("Name", "IP Address", "Valid", "Last API pull", "Type", "Version", "Auth Type")
t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
for _, b := range bouncers {
revoked := emoji.CheckMark
if b.Revoked {
revoked = emoji.Prohibited
}
lastPull := ""
if b.LastPull != nil {
lastPull = b.LastPull.Format(time.RFC3339)
}
t.AddRow(b.Name, b.IPAddress, revoked, lastPull, b.Type, b.Version, b.AuthType)
}
t.Render()
}

View file

@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"os"
"slices"
@ -16,6 +17,7 @@ import (
"github.com/fatih/color"
"github.com/go-openapi/strfmt"
"github.com/google/uuid"
"github.com/jedib0t/go-pretty/v6/table"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
@ -24,8 +26,10 @@ import (
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
@ -147,13 +151,126 @@ Note: This command requires database direct access, so is intended to be run on
cmd.AddCommand(cli.newDeleteCmd())
cmd.AddCommand(cli.newValidateCmd())
cmd.AddCommand(cli.newPruneCmd())
cmd.AddCommand(cli.newInspectCmd())
return cmd
}
func (cli *cliMachines) list() error {
out := color.Output
func (*cliMachines) inspectHubHuman(out io.Writer, machine *ent.Machine) {
state := machine.Hubstate
if len(state) == 0 {
fmt.Println("No hub items found for this machine")
return
}
// group state rows by type for multiple tables
rowsByType := make(map[string][]table.Row)
for itemType, items := range state {
for _, item := range items {
if _, ok := rowsByType[itemType]; !ok {
rowsByType[itemType] = make([]table.Row, 0)
}
row := table.Row{item.Name, item.Status, item.Version}
rowsByType[itemType] = append(rowsByType[itemType], row)
}
}
for itemType, rows := range rowsByType {
t := newTable(out).Writer
t.AppendHeader(table.Row{"Name", "Status", "Version"})
t.SetTitle(itemType)
t.AppendRows(rows)
fmt.Fprintln(out, t.Render())
}
}
func (cli *cliMachines) listHuman(out io.Writer, machines ent.Machines) {
t := newLightTable(out).Writer
t.AppendHeader(table.Row{"Name", "IP Address", "Last Update", "Status", "Version", "OS", "Auth Type", "Last Heartbeat"})
for _, m := range machines {
validated := emoji.Prohibited
if m.IsValidated {
validated = emoji.CheckMark
}
hb, active := getLastHeartbeat(m)
if !active {
hb = emoji.Warning + " " + hb
}
t.AppendRow(table.Row{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.GetOSNameAndVersion(), m.AuthType, hb})
}
fmt.Fprintln(out, t.Render())
}
// machineInfo contains only the data we want for inspect/list: no hub status, scenarios, edges, etc.
type machineInfo struct {
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
LastPush *time.Time `json:"last_push,omitempty"`
LastHeartbeat *time.Time `json:"last_heartbeat,omitempty"`
MachineId string `json:"machineId,omitempty"`
IpAddress string `json:"ipAddress,omitempty"`
Version string `json:"version,omitempty"`
IsValidated bool `json:"isValidated,omitempty"`
AuthType string `json:"auth_type"`
OS string `json:"os,omitempty"`
Featureflags []string `json:"featureflags,omitempty"`
Datasources map[string]int64 `json:"datasources,omitempty"`
}
func newMachineInfo(m *ent.Machine) machineInfo {
return machineInfo{
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
LastPush: m.LastPush,
LastHeartbeat: m.LastHeartbeat,
MachineId: m.MachineId,
IpAddress: m.IpAddress,
Version: m.Version,
IsValidated: m.IsValidated,
AuthType: m.AuthType,
OS: m.GetOSNameAndVersion(),
Featureflags: m.GetFeatureFlagList(),
Datasources: m.Datasources,
}
}
func (cli *cliMachines) listCSV(out io.Writer, machines ent.Machines) error {
csvwriter := csv.NewWriter(out)
err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat", "os"})
if err != nil {
return fmt.Errorf("failed to write header: %w", err)
}
for _, m := range machines {
validated := "false"
if m.IsValidated {
validated = "true"
}
hb := "-"
if m.LastHeartbeat != nil {
hb = m.LastHeartbeat.Format(time.RFC3339)
}
if err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb, fmt.Sprintf("%s/%s", m.Osname, m.Osversion)}); err != nil {
return fmt.Errorf("failed to write raw output: %w", err)
}
}
csvwriter.Flush()
return nil
}
func (cli *cliMachines) list(out io.Writer) error {
machines, err := cli.db.ListMachines()
if err != nil {
return fmt.Errorf("unable to list machines: %w", err)
@ -161,38 +278,23 @@ func (cli *cliMachines) list() error {
switch cli.cfg().Cscli.Output {
case "human":
getAgentsTable(out, machines)
cli.listHuman(out, machines)
case "json":
info := make([]machineInfo, 0, len(machines))
for _, m := range machines {
info = append(info, newMachineInfo(m))
}
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(machines); err != nil {
if err := enc.Encode(info); err != nil {
return errors.New("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: %w", 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 cli.listCSV(out, machines)
}
return nil
@ -207,7 +309,7 @@ func (cli *cliMachines) newListCmd() *cobra.Command {
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.list()
return cli.list(color.Output)
},
}
@ -349,13 +451,14 @@ func (cli *cliMachines) add(args []string, machinePassword string, dumpFile stri
return nil
}
func (cli *cliMachines) deleteValid(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// need to load config and db because PersistentPreRunE is not called for completions
// validMachineID returns a list of machine IDs for command completion
func (cli *cliMachines) validMachineID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var err error
cfg := cli.cfg()
// need to load config and db because PersistentPreRunE is not called for completions
if err = require.LAPI(cfg); err != nil {
cobra.CompError("unable to list machines " + err.Error())
return nil, cobra.ShellCompDirectiveNoFileComp
@ -405,7 +508,7 @@ func (cli *cliMachines) newDeleteCmd() *cobra.Command {
Args: cobra.MinimumNArgs(1),
Aliases: []string{"remove"},
DisableAutoGenTag: true,
ValidArgsFunction: cli.deleteValid,
ValidArgsFunction: cli.validMachineID,
RunE: func(_ *cobra.Command, args []string) error {
return cli.delete(args)
},
@ -417,7 +520,7 @@ func (cli *cliMachines) newDeleteCmd() *cobra.Command {
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. " +
"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 {
@ -442,11 +545,11 @@ func (cli *cliMachines) prune(duration time.Duration, notValidOnly bool, force b
return nil
}
getAgentsTable(color.Output, machines)
cli.listHuman(color.Output, machines)
if !force {
if yes, err := askYesNo(
"You are about to PERMANENTLY remove the above machines from the database. " +
"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 {
@ -460,7 +563,7 @@ func (cli *cliMachines) prune(duration time.Duration, notValidOnly bool, force b
return fmt.Errorf("unable to prune machines: %w", err)
}
fmt.Fprintf(os.Stderr, "successfully delete %d machines\n", deleted)
fmt.Fprintf(os.Stderr, "successfully deleted %d machines\n", deleted)
return nil
}
@ -521,3 +624,136 @@ func (cli *cliMachines) newValidateCmd() *cobra.Command {
return cmd
}
func (*cliMachines) inspectHuman(out io.Writer, machine *ent.Machine) {
t := newTable(out).Writer
t.SetTitle("Machine: " + machine.MachineId)
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true},
})
t.AppendRows([]table.Row{
{"IP Address", machine.IpAddress},
{"Created At", machine.CreatedAt},
{"Last Update", machine.UpdatedAt},
{"Last Heartbeat", machine.LastHeartbeat},
{"Validated?", machine.IsValidated},
{"CrowdSec version", machine.Version},
{"OS", machine.GetOSNameAndVersion()},
{"Auth type", machine.AuthType},
})
for dsName, dsCount := range machine.Datasources {
t.AppendRow(table.Row{"Datasources", fmt.Sprintf("%s: %d", dsName, dsCount)})
}
for _, ff := range machine.GetFeatureFlagList() {
t.AppendRow(table.Row{"Feature Flags", ff})
}
for _, coll := range machine.Hubstate[cwhub.COLLECTIONS] {
t.AppendRow(table.Row{"Collections", coll.Name})
}
fmt.Fprintln(out, t.Render())
}
func (cli *cliMachines) inspect(machine *ent.Machine) error {
out := color.Output
outputFormat := cli.cfg().Cscli.Output
switch outputFormat {
case "human":
cli.inspectHuman(out, machine)
case "json":
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(newMachineInfo(machine)); err != nil {
return errors.New("failed to marshal")
}
return nil
default:
return fmt.Errorf("output format '%s' not supported for this command", outputFormat)
}
return nil
}
func (cli *cliMachines) inspectHub(machine *ent.Machine) error {
out := color.Output
switch cli.cfg().Cscli.Output {
case "human":
cli.inspectHubHuman(out, machine)
case "json":
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(machine.Hubstate); err != nil {
return errors.New("failed to marshal")
}
return nil
case "raw":
csvwriter := csv.NewWriter(out)
err := csvwriter.Write([]string{"type", "name", "status", "version"})
if err != nil {
return fmt.Errorf("failed to write header: %w", err)
}
rows := make([][]string, 0)
for itemType, items := range machine.Hubstate {
for _, item := range items {
rows = append(rows, []string{itemType, item.Name, item.Status, item.Version})
}
}
for _, row := range rows {
if err := csvwriter.Write(row); err != nil {
return fmt.Errorf("failed to write raw output: %w", err)
}
}
csvwriter.Flush()
}
return nil
}
func (cli *cliMachines) newInspectCmd() *cobra.Command {
var showHub bool
cmd := &cobra.Command{
Use: "inspect [machine_name]",
Short: "inspect a machine by name",
Example: `cscli machines inspect "machine1"`,
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
ValidArgsFunction: cli.validMachineID,
RunE: func(_ *cobra.Command, args []string) error {
machineID := args[0]
machine, err := cli.db.QueryMachineByID(machineID)
if err != nil {
return fmt.Errorf("unable to read machine data '%s': %w", machineID, err)
}
if showHub {
return cli.inspectHub(machine)
}
return cli.inspect(machine)
},
}
flags := cmd.Flags()
flags.BoolVarP(&showHub, "hub", "H", false, "show hub state")
return cmd
}

View file

@ -1,33 +0,0 @@
package main
import (
"io"
"time"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/table"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
)
func getAgentsTable(out io.Writer, machines []*ent.Machine) {
t := newLightTable(out)
t.SetHeaders("Name", "IP Address", "Last Update", "Status", "Version", "Auth Type", "Last Heartbeat")
t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
for _, m := range machines {
validated := emoji.Prohibited
if m.IsValidated {
validated = emoji.CheckMark
}
hb, active := getLastHeartbeat(m)
if !active {
hb = emoji.Warning + " " + hb
}
t.AddRow(m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb)
}
t.Render()
}

View file

@ -193,12 +193,9 @@ func (cli *cliSupport) dumpBouncers(zw *zip.Writer, db *database.Client) error {
out := new(bytes.Buffer)
bouncers, err := db.ListBouncers()
if err != nil {
return fmt.Errorf("unable to list bouncers: %w", err)
}
getBouncersTable(out, bouncers)
// call the "cscli bouncers list" command directly, skip any preRun
cm := cliBouncers{db: db, cfg: cli.cfg}
cm.list(out)
stripped := stripAnsiString(out.String())
@ -216,12 +213,9 @@ func (cli *cliSupport) dumpAgents(zw *zip.Writer, db *database.Client) error {
out := new(bytes.Buffer)
machines, err := db.ListMachines()
if err != nil {
return fmt.Errorf("unable to list machines: %w", err)
}
getAgentsTable(out, machines)
// call the "cscli machines list" command directly, skip any preRun
cm := cliMachines{db: db, cfg: cli.cfg}
cm.list(out)
stripped := stripAnsiString(out.String())
@ -617,6 +611,10 @@ cscli support dump -f /tmp/crowdsec-support.zip
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
output := cli.cfg().Cscli.Output
if output != "human" {
return fmt.Errorf("output format %s not supported for this command", output)
}
return cli.dump(cmd.Context(), outFile)
},
}

View file

@ -0,0 +1,55 @@
package ent
import (
"strings"
)
func (m *Machine) GetOSNameAndVersion() string {
ret := m.Osname
if m.Osversion != "" {
if ret != "" {
ret += "/"
}
ret += m.Osversion
}
if ret == "" {
return "?"
}
return ret
}
func (b *Bouncer) GetOSNameAndVersion() string {
ret := b.Osname
if b.Osversion != "" {
if ret != "" {
ret += "/"
}
ret += b.Osversion
}
if ret == "" {
return "?"
}
return ret
}
func (m *Machine) GetFeatureFlagList() []string {
if m.Featureflags == "" {
return nil
}
return strings.Split(m.Featureflags, ",")
}
func (b *Bouncer) GetFeatureFlagList() []string {
if b.Featureflags == "" {
return nil
}
return strings.Split(b.Featureflags, ",")
}

View file

@ -10,6 +10,7 @@ import (
// ItemState is defined here instead of using pkg/models/HubItem to avoid introducing a dependency
type ItemState struct {
Name string `json:"name,omitempty"`
Status string `json:"status,omitempty"`
Version string `json:"version,omitempty"`
}

View file

@ -62,7 +62,7 @@ teardown() {
assert_output 1
}
@test "machines delete has autocompletion" {
@test "machines [delete|inspect] has autocompletion" {
rune -0 cscli machines add -a -f /dev/null foo1
rune -0 cscli machines add -a -f /dev/null foo2
rune -0 cscli machines add -a -f /dev/null bar
@ -72,6 +72,11 @@ teardown() {
assert_line --index 1 'foo2'
refute_line 'bar'
refute_line 'baz'
rune -0 cscli __complete machines inspect 'foo'
assert_line --index 0 'foo1'
assert_line --index 1 'foo2'
refute_line 'bar'
refute_line 'baz'
}
@test "heartbeat is initially null" {