package cmd import ( "context" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "os/exec" "path" "strconv" "strings" "time" "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" "github.com/apex/log" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/parsers/kernel" "github.com/docker/docker/pkg/parsers/operatingsystem" "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/loggers/cli" "github.com/pterodactyl/wings/system" "github.com/spf13/cobra" ) const DefaultHastebinUrl = "https://ptero.co" const DefaultLogLines = 200 var ( diagnosticsArgs struct { IncludeEndpoints bool IncludeLogs bool ReviewBeforeUpload bool HastebinURL string LogLines int } ) func newDiagnosticsCommand() *cobra.Command { command := &cobra.Command{ Use: "diagnostics", Short: "Collect and report information about this Wings instance to assist in debugging.", PreRun: func(cmd *cobra.Command, args []string) { initConfig() log.SetHandler(cli.Default) }, Run: diagnosticsCmdRun, } command.Flags().StringVar(&diagnosticsArgs.HastebinURL, "hastebin-url", DefaultHastebinUrl, "the url of the hastebin instance to use") command.Flags().IntVar(&diagnosticsArgs.LogLines, "log-lines", DefaultLogLines, "the number of log lines to include in the report") return command } // 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 upload.", Default: true, }, }, } if err := survey.Ask(questions, &diagnosticsArgs); err != nil { if err == terminal.InterruptErr { return } panic(err) } dockerVersion, dockerInfo, dockerErr := getDockerInfo() output := &strings.Builder{} fmt.Fprintln(output, "Pterodactyl 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 err := config.FromFile(config.DefaultLocation); err != nil { } cfg := config.Get() fmt.Fprintln(output, " Panel Location:", redact(cfg.PanelLocation)) fmt.Fprintln(output, "") fmt.Fprintln(output, " Internal Webserver:", redact(cfg.Api.Host), ":", cfg.Api.Port) fmt.Fprintln(output, " SSL Enabled:", cfg.Api.Ssl.Enabled) fmt.Fprintln(output, " SSL Certificate:", redact(cfg.Api.Ssl.CertificateFile)) fmt.Fprintln(output, " SSL Key:", redact(cfg.Api.Ssl.KeyFile)) fmt.Fprintln(output, "") fmt.Fprintln(output, " SFTP Server:", redact(cfg.System.Sftp.Address), ":", cfg.System.Sftp.Port) fmt.Fprintln(output, " SFTP Read-Only:", cfg.System.Sftp.ReadOnly) fmt.Fprintln(output, "") fmt.Fprintln(output, " Root Directory:", cfg.System.RootDirectory) fmt.Fprintln(output, " Logs Directory:", cfg.System.LogDirectory) fmt.Fprintln(output, " Data Directory:", cfg.System.Data) fmt.Fprintln(output, " Archive Directory:", cfg.System.ArchiveDirectory) fmt.Fprintln(output, " Backup Directory:", cfg.System.BackupDirectory) fmt.Fprintln(output, "") fmt.Fprintln(output, " Username:", cfg.System.Username) fmt.Fprintln(output, " Server Time:", time.Now().Format(time.RFC1123Z)) fmt.Fprintln(output, " Debug Mode:", cfg.Debug) printHeader(output, "Docker: Info") if dockerErr == nil { 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) } } } else { fmt.Fprintln(output, dockerErr.Error()) } 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 { p := "/var/log/pterodactyl/wings.log" if cfg != nil { p = path.Join(cfg.System.LogDirectory, "wings.log") } if c, err := exec.Command("tail", "-n", strconv.Itoa(diagnosticsArgs.LogLines), p).Output(); err != nil { fmt.Fprintln(output, "No logs found or an error occurred.") } else { fmt.Fprintf(output, "%s\n", string(c)) } } 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 { u, err := uploadToHastebin(diagnosticsArgs.HastebinURL, output.String()) if err == nil { fmt.Println("Your report is available here: ", u) } } } func getDockerInfo() (types.Version, types.Info, error) { client, err := environment.Docker() if err != nil { return types.Version{}, types.Info{}, err } dockerVersion, err := client.ServerVersion(context.Background()) if err != nil { return types.Version{}, types.Info{}, err } dockerInfo, err := client.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("failed to 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, "| ------------------------------") }