From deb9305f5658fd0796bd4ceffc2f997ce79e6529 Mon Sep 17 00:00:00 2001 From: vagrant Date: Sat, 4 Jul 2020 20:57:54 +0000 Subject: [PATCH] add diagnostics command --- cmd/diagnostics.go | 226 +++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 22 +++-- 2 files changed, 238 insertions(+), 10 deletions(-) create mode 100644 cmd/diagnostics.go diff --git a/cmd/diagnostics.go b/cmd/diagnostics.go new file mode 100644 index 0000000..5509100 --- /dev/null +++ b/cmd/diagnostics.go @@ -0,0 +1,226 @@ +package cmd + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os/exec" + "path" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" + "github.com/docker/cli/components/engine/pkg/parsers/operatingsystem" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/parsers/kernel" + "github.com/pterodactyl/wings/config" + "github.com/pterodactyl/wings/system" + "github.com/spf13/cobra" +) + +const DefaultHastebinUrl = "https://hastebin.com" + +var ( + diagnosticsArgs struct { + IncludeEndpoints bool + IncludeLogs bool + ReviewBeforeUpload bool + HastebinURL string + } +) + +var diagnosticsCmd = &cobra.Command{ + Use: "diagnostics", + Short: "Collect diagnostics information.", + Run: diagnosticsCmdRun, +} + +func init() { + diagnosticsCmd.PersistentFlags().StringVar(&diagnosticsArgs.HastebinURL, "hastebin-url", DefaultHastebinUrl, "The url of the hastebin instance to use.") +} + +// diagnosticsCmdRun collects diagnostics about wings, it's configuration and the node. +// We collect: +// - wings and docker versions +// - relevant parts of daemon configuration +// - the docker debug output +// - running docker containers +// - logs +func diagnosticsCmdRun(cmd *cobra.Command, args []string) { + questions := []*survey.Question{ + { + Name: "IncludeEndpoints", + Prompt: &survey.Confirm{Message: "Do you want to include endpoints (i.e. the FQDN/IP of your panel)?", Default: false}, + }, + { + Name: "IncludeLogs", + Prompt: &survey.Confirm{Message: "Do you want to include the latest logs?", Default: true}, + }, + { + Name: "ReviewBeforeUpload", + Prompt: &survey.Confirm{ + Message: "Do you want to review the collected data before uploading to hastebin.com?", + Help: "The data, especially the logs, might contain sensitive information, so you should review it. You will be asked again if you want to uplaod.", + Default: true, + }, + }, + } + if err := survey.Ask(questions, &diagnosticsArgs); err != nil { + if err == terminal.InterruptErr { + return + } + panic(err) + } + + dockerVersion, dockerInfo, dockerErr := getDockerInfo() + _ = dockerInfo + + output := &strings.Builder{} + fmt.Fprintln(output, "Pterodactly Wings - Diagnostics Report") + printHeader(output, "Versions") + fmt.Fprintln(output, "wings:", system.Version) + if dockerErr == nil { + fmt.Fprintln(output, "Docker", dockerVersion.Version) + } + if v, err := kernel.GetKernelVersion(); err == nil { + fmt.Fprintln(output, "Kernel:", v) + } + if os, err := operatingsystem.GetOperatingSystem(); err == nil { + fmt.Fprintln(output, "OS:", os) + } + + printHeader(output, "Wings Configuration") + if cfg, err := config.ReadConfiguration(config.DefaultLocation); cfg != nil { + fmt.Fprintln(output, "Panel Location:", redact(cfg.PanelLocation)) + fmt.Fprintln(output, "Api Host:", redact(cfg.Api.Host)) + fmt.Fprintln(output, "Api Port:", cfg.Api.Port) + fmt.Fprintln(output, "Api Ssl Enabled:", cfg.Api.Ssl.Enabled) + fmt.Fprintln(output, "Api Ssl Certificate:", redact(cfg.Api.Ssl.CertificateFile)) + fmt.Fprintln(output, "Api Ssl Key:", redact(cfg.Api.Ssl.KeyFile)) + fmt.Fprintln(output, "Sftp Address:", redact(cfg.System.Sftp.Address)) + fmt.Fprintln(output, "Sftp Port:", cfg.System.Sftp.Port) + fmt.Fprintln(output, "Sftp Read Only:", cfg.System.Sftp.ReadOnly) + fmt.Fprintln(output, "Sftp Diskchecking Disabled:", cfg.System.Sftp.DisableDiskChecking) + fmt.Fprintln(output, "System Root Directory:", cfg.System.RootDirectory) + fmt.Fprintln(output, "System Logs Directory:", cfg.System.LogDirectory) + fmt.Fprintln(output, "System Data Directory:", cfg.System.Data) + fmt.Fprintln(output, "System Archive Directory:", cfg.System.ArchiveDirectory) + fmt.Fprintln(output, "System Backup Directory:", cfg.System.BackupDirectory) + fmt.Fprintln(output, "System Username:", cfg.System.Username) + fmt.Fprintln(output, "Debug Enabled:", cfg.Debug) + } else { + fmt.Println("Failed to load configuration.", err) + } + + printHeader(output, "Docker: Info") + fmt.Fprintln(output, "Server Version:", dockerInfo.ServerVersion) + fmt.Fprintln(output, "Storage Driver:", dockerInfo.Driver) + if dockerInfo.DriverStatus != nil { + for _, pair := range dockerInfo.DriverStatus { + fmt.Fprintf(output, " %s: %s\n", pair[0], pair[1]) + } + } + if dockerInfo.SystemStatus != nil { + for _, pair := range dockerInfo.SystemStatus { + fmt.Fprintf(output, " %s: %s\n", pair[0], pair[1]) + } + } + fmt.Fprintln(output, "LoggingDriver:", dockerInfo.LoggingDriver) + fmt.Fprintln(output, "CgroupDriver:", dockerInfo.CgroupDriver) + if len(dockerInfo.Warnings) > 0 { + for _, w := range dockerInfo.Warnings { + fmt.Fprintln(output, w) + } + } + + printHeader(output, "Docker: Running Containers") + c := exec.Command("docker", "ps") + if co, err := c.Output(); err == nil { + output.Write(co) + } else { + fmt.Fprint(output, "Couldn't list containers: ", err) + } + + printHeader(output, "Latest Wings Logs") + if diagnosticsArgs.IncludeLogs { + fmt.Fprintln(output, "No logs found. Probably because nobody implemented logging to files yet :(") + } else { + fmt.Fprintln(output, "Logs redacted.") + } + + fmt.Println("\n--------------- generated report ---------------") + fmt.Println(output.String()) + fmt.Print("--------------- end of report ---------------\n\n") + + upload := !diagnosticsArgs.ReviewBeforeUpload + if !upload { + survey.AskOne(&survey.Confirm{Message: "Upload to " + diagnosticsArgs.HastebinURL + "?", Default: false}, &upload) + } + if upload { + url, err := uploadToHastebin(diagnosticsArgs.HastebinURL, output.String()) + if err == nil { + fmt.Println("Your report is available here: ", url) + } + } +} + +func getDockerInfo() (types.Version, types.Info, error) { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return types.Version{}, types.Info{}, err + } + dockerVersion, err := cli.ServerVersion(context.Background()) + if err != nil { + return types.Version{}, types.Info{}, err + } + dockerInfo, err := cli.Info(context.Background()) + if err != nil { + return types.Version{}, types.Info{}, err + } + return dockerVersion, dockerInfo, nil +} + +func uploadToHastebin(hbUrl, content string) (string, error) { + r := strings.NewReader(content) + u, err := url.Parse(hbUrl) + if err != nil { + return "", err + } + u.Path = path.Join(u.Path, "documents") + res, err := http.Post(u.String(), "plain/text", r) + if err != nil || res.StatusCode != 200 { + fmt.Println("Failed to upload report to ", u.String(), err) + return "", err + } + pres := make(map[string]interface{}) + body, err := ioutil.ReadAll(res.Body) + if err != nil { + fmt.Println("Failed to parse response.", err) + return "", err + } + json.Unmarshal(body, &pres) + if key, ok := pres["key"].(string); ok { + u, _ := url.Parse(hbUrl) + u.Path = path.Join(u.Path, key) + return u.String(), nil + } + return "", errors.New("Couldn't find key in response") +} + +func redact(s string) string { + if !diagnosticsArgs.IncludeEndpoints { + return "{redacted}" + } + return s +} + +func printHeader(w io.Writer, title string) { + fmt.Fprintln(w, "\n|\n|", title) + fmt.Fprintln(w, "| ------------------------------") +} diff --git a/cmd/root.go b/cmd/root.go index 2f9bdf8..a7a8602 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,15 +3,16 @@ package cmd import ( "crypto/tls" "fmt" - "github.com/apex/log" - "github.com/mitchellh/colorstring" - "github.com/pterodactyl/wings/loggers/cli" - "golang.org/x/crypto/acme/autocert" "net/http" "os" "path" "strings" + "github.com/apex/log" + "github.com/mitchellh/colorstring" + "github.com/pterodactyl/wings/loggers/cli" + "golang.org/x/crypto/acme/autocert" + "github.com/pkg/errors" "github.com/pkg/profile" "github.com/pterodactyl/wings/config" @@ -41,7 +42,7 @@ var root = &cobra.Command{ os.Exit(1) } }, - Run: rootCmdRun, + Run: rootCmdRun, } func init() { @@ -52,6 +53,7 @@ func init() { root.PersistentFlags().StringVar(&tlsHostname, "tls-hostname", "", "required with --auto-tls, the FQDN for the generated SSL certificate") root.AddCommand(configureCmd) + root.AddCommand(diagnosticsCmd) } // Get the configuration path based on the arguments provided. @@ -229,10 +231,10 @@ func rootCmdRun(*cobra.Command, []string) { } log.WithFields(log.Fields{ - "use_ssl": c.Api.Ssl.Enabled, + "use_ssl": c.Api.Ssl.Enabled, "use_auto_tls": useAutomaticTls && len(tlsHostname) > 0, "host_address": c.Api.Host, - "host_port": c.Api.Port, + "host_port": c.Api.Port, }).Info("configuring internal webserver") r := router.Configure() @@ -240,9 +242,9 @@ func rootCmdRun(*cobra.Command, []string) { if useAutomaticTls && len(tlsHostname) > 0 { m := autocert.Manager{ - Prompt: autocert.AcceptTOS, - Cache: autocert.DirCache(path.Join(c.System.RootDirectory, "/.tls-cache")), - HostPolicy: autocert.HostWhitelist(tlsHostname), + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(path.Join(c.System.RootDirectory, "/.tls-cache")), + HostPolicy: autocert.HostWhitelist(tlsHostname), } log.WithField("hostname", tlsHostname).