diff --git a/go.mod b/go.mod index 6c46b66..af00404 100644 --- a/go.mod +++ b/go.mod @@ -14,9 +14,9 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/mod v0.16.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect golang.org/x/sys v0.18.0 // indirect - golang.org/x/tools v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index 415b252..39b427d 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,11 @@ github.com/Showmax/go-fqdn v1.0.0 h1:0rG5IbmVliNT5O19Mfuvna9LL7zlHyRfsSvBPZmF9tM= github.com/Showmax/go-fqdn v1.0.0/go.mod h1:SfrFBzmDCtCGrnHhoDjuvFnKsWjEQX/Q9ARZvOrJAko= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= @@ -11,23 +14,21 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/arguments/arguments.go b/internal/arguments/arguments.go index dd96700..35a6358 100644 --- a/internal/arguments/arguments.go +++ b/internal/arguments/arguments.go @@ -3,40 +3,120 @@ package arguments import ( "bufio" "fmt" + flag "github.com/spf13/pflag" "os" "strings" ) -func NewStringVar(callback *string, value string, usage string) { - scanner := bufio.NewScanner(os.Stdin) +var ( + NonInteractive bool + validBooleanInputs = map[string]bool{ + "y": true, + "yes": true, + "n": false, + "no": false, + } +) + +type Handler struct { + scanner *bufio.Scanner + prompts []func() +} + +func NewHandler() Handler { + return Handler{ + scanner: bufio.NewScanner(os.Stdin), + } +} + +func (h *Handler) ReadArgumentsFromStdin() { + for _, prompt := range h.prompts { + prompt() + } +} + +func (h *Handler) NewPromptStringVar(callback *string, name, defaultValue, usage string, required bool) { + flag.StringVar(callback, name, defaultValue, usage) + + h.prompts = append(h.prompts, func() { + if *callback != "" { + defaultValue = *callback + } + + h.newStringPrompt(callback, defaultValue, usage, required) + }) +} + +func (h *Handler) NewPromptStringSliceVar(callback *[]string, name string, defaultValue []string, usage string, required bool) { + flag.StringSliceVar(callback, name, defaultValue, usage) + + h.prompts = append(h.prompts, func() { + if len(*callback) > 0 { + defaultValue = *callback + } + + var input string + + h.newStringPrompt(&input, strings.Join(defaultValue, ","), usage, required) + *callback = strings.Split(input, ",") + }) +} + +func (h *Handler) NewPromptBoolVar(callback *bool, name string, defaultValue bool, usage string) { + flag.BoolVar(callback, name, defaultValue, usage) + h.prompts = append(h.prompts, func() { + h.newBoolPrompt(callback, defaultValue, usage) + }) +} + +func (h *Handler) newStringPrompt(callback *string, defaultValue, usage string, required bool) { for { - fmt.Printf("%s - (Default: %s): ", usage, value) - if scanner.Scan() { - input := scanner.Text() + fmt.Printf("%s - (Preselection: '%s'): ", usage, defaultValue) + if h.scanner.Scan() { + input := h.scanner.Text() if input != "" { *callback = input break - } else { - *callback = value + } else if input == "" && defaultValue != "" { + *callback = defaultValue + break + } else if input == "" && !required { break } } else { - if err := scanner.Err(); err != nil { + if err := h.scanner.Err(); err != nil { _, _ = fmt.Fprintln(os.Stderr, "reading standard input:", err) break } } } + + return } -func NewStringSliceVar(callback *[]string, value []string, usage string) { - var input string - NewStringVar(&input, strings.Join(value, ","), usage) +func (h *Handler) newBoolPrompt(callback *bool, defaultValue bool, usage string) { + for { + fmt.Printf("%s [y/n] - (Preselection: '%t'): ", usage, defaultValue) + + if h.scanner.Scan() { + input := strings.ToLower(h.scanner.Text()) - *callback = strings.Split(input, ",") + if input != "" && isValidBoolString(input) { + *callback = validBooleanInputs[input] + break + } else if input == "" { + *callback = defaultValue + break + } + } + } } -func NewBoolVar() { +func isValidBoolString(input string) bool { + if _, ok := validBooleanInputs[input]; !ok { + return false + } + return true } diff --git a/main.go b/main.go index 9a8fe3c..f8b2c28 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/NETWAYS/support-collector/internal/arguments" "github.com/NETWAYS/support-collector/internal/metrics" + flag "github.com/spf13/pflag" "os" "path/filepath" "strings" @@ -36,16 +37,15 @@ import ( "github.com/mattn/go-colorable" "github.com/sirupsen/logrus" - flag "github.com/spf13/pflag" ) -const Product = "NETWAYS support collector" +const Product = "NETWAYS support-collector" // FilePrefix for the outfile file. -const FilePrefix = "netways-support" +const FilePrefix = "support-collector" const Readme = ` -The support collector allows our customers to collect relevant information from +The support-collector allows our customers to collect relevant information from their servers. A resulting ZIP file can then be provided to our support team for further inspection. @@ -108,22 +108,79 @@ var ( ) var ( - verbose, printVersion bool - enabledModules, disabledModules []string - extraObfuscators []string - outputFile string - commandTimeout = 60 * time.Second - noDetailedCollection bool - startTime = time.Now() - metric *metrics.Metrics + verbose, printVersion, noDetailedCollection bool + enabledModules, disabledModules, extraObfuscators []string + outputFile string + commandTimeout = 60 * time.Second + startTime = time.Now() + metric *metrics.Metrics ) -func main() { - handleArguments() - +func init() { // Set locale to C, to avoid translations in command output _ = os.Setenv("LANG", "C") + args := arguments.NewHandler() + + // General arguments without interactive prompt + flag.BoolVar(&arguments.NonInteractive, "nonInteractive", false, "Enable non-interactive mode") + flag.BoolVar(&printVersion, "version", false, "Print version and exit") + flag.BoolVar(&verbose, "verbose", false, "Enable verbose logging") + + // TODO + //flag.DurationVar(&commandTimeout, "command-timeout", commandTimeout, "Timeout for command execution in modules") + + // Run specific arguments + args.NewPromptStringVar(&outputFile, "output", buildFileName(), "Output file for the ZIP content", true) + args.NewPromptStringSliceVar(&enabledModules, "enable", moduleOrder, "Comma separated list of enabled modules", false) + args.NewPromptStringSliceVar(&disabledModules, "disable", []string{}, "Comma separated list of disabled modules", false) + args.NewPromptBoolVar(&noDetailedCollection, "nodetails", false, "Disable detailed collection including logs and more") + + // Icinga 2 specific arguments + args.NewPromptStringVar(&icinga2.APICred.Username, "icinga2-api-user", "", "Username of global Icinga 2 API user to collect data about Icinga 2 Infrastructure", false) + args.NewPromptStringVar(&icinga2.APICred.Password, "icinga2-api-pass", "", "Password for global Icinga 2 API user to collect data about Icinga 2 Infrastructure", false) + args.NewPromptStringSliceVar(&icinga2.APIEndpoints, "icinga2-api-endpoints", []string{}, "Comma separated list of Icinga 2 API Endpoints (including port) to collect data from. FQDN or IP address must be reachable. (Example: i2-master01.local:5665)", false) + + flag.CommandLine.SortFlags = false + + // Output a proper help message with details + flag.Usage = func() { + _, _ = fmt.Fprintf(os.Stderr, "%s\n\n%s\n\n", Product, strings.Trim(Readme, "\n")) + + _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + + flag.PrintDefaults() + } + + flag.Parse() + + if printVersion { + fmt.Println(Product, "version", getBuildInfo()) //nolint:forbidigo + os.Exit(0) + } + + if !arguments.NonInteractive { + args.ReadArgumentsFromStdin() + } + + // Verify enabled modules + for _, name := range enabledModules { + if _, ok := modules[name]; !ok { + logrus.Fatal("Unknown module to enable: ", name) + } + } + + fmt.Println(noDetailedCollection) + + os.Exit(1) +} + +// buildFileName returns a filename to store the output of support collector. +func buildFileName() string { + return FilePrefix + "_" + util.GetHostnameWithoutDomain() + "_" + time.Now().Format("20060102-1504") + ".zip" +} + +func main() { c, closeCollection := NewCollection(outputFile) // Close collection defer closeCollection() @@ -184,59 +241,6 @@ func main() { c.Log.Infof("Generated ZIP file located at %s", path) } -func handleArguments() { - // TODO only a prototype - arguments.NewStringVar(&outputFile, buildFileName(), "Output file for the ZIP content") - arguments.NewStringSliceVar(&enabledModules, moduleOrder, "Comma seperated list of enabled module") - - // arguments for collection handling - //flag.StringSliceVar(&enabledModules, "enable", moduleOrder, "List of enabled module") - flag.StringSliceVar(&disabledModules, "disable", []string{}, "List of disabled module") - //flag.StringVarP(&outputFile, "output", "o", buildFileName(), "Output file for the ZIP content") - flag.BoolVar(&noDetailedCollection, "nodetails", false, "Disable detailed collection including logs and more") - flag.StringArrayVar(&extraObfuscators, "hide", []string{}, "List of additional strings to obfuscate. Can be used multiple times and supports regex.") //nolint:lll - flag.DurationVar(&commandTimeout, "command-timeout", commandTimeout, "Timeout for command execution in modules") - - // api credentials for icinga 2 modules - flag.StringVar(&icinga2.APICred.Username, "icinga2-api-user", "", "Username of global Icinga 2 API user to collect data about Icinga 2 Infrastructure") //nolint:lll - flag.StringVar(&icinga2.APICred.Password, "icinga2-api-pass", "", "Password for global Icinga 2 API user to collect data about Icinga 2 Infrastructure") //nolint:lll - flag.StringSliceVar(&icinga2.APIEndpoints, "icinga2-api-endpoints", []string{}, "List of Icinga 2 API Endpoints (including port) to collect data from. FQDN or IP address must be reachable. (Example: i2-master01.local:5665)") //nolint:lll - - // basic arguments - flag.BoolVarP(&printVersion, "version", "V", false, "Print version and exit") - flag.BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") - - flag.CommandLine.SortFlags = false - - // Output a proper help message with details - flag.Usage = func() { - _, _ = fmt.Fprintf(os.Stderr, "%s\n\n%s\n\n", Product, strings.Trim(Readme, "\n")) - - _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) - - flag.PrintDefaults() - } - - flag.Parse() - - if printVersion { - fmt.Println(Product, "version", getBuildInfo()) //nolint:forbidigo - os.Exit(0) - } - - // Verify enabled modules - for _, name := range enabledModules { - if _, ok := modules[name]; !ok { - logrus.Fatal("Unknown module to enable: ", name) - } - } -} - -// buildFileName returns a filename to store the output of support collector. -func buildFileName() string { - return util.GetHostnameWithoutDomain() + "-" + FilePrefix + "-" + time.Now().Format("20060102-1504") + ".zip" -} - // NewCollection starts a new collection. outputFile will be created. // // Collection and cleanup function to defer are returned