From 9bc7d788f03b228fdc94c10e16ab968cccfd4cca Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 1 May 2025 00:48:31 +0200 Subject: [PATCH] [client] Add debug upload option to netbird ui (#3768) --- client/cmd/root.go | 3 +- client/ui/client_ui.go | 43 +++--- client/ui/debug.go | 268 +++++++++++++++++++++++++++++++--- upload-server/types/upload.go | 2 + 4 files changed, 269 insertions(+), 47 deletions(-) diff --git a/client/cmd/root.go b/client/cmd/root.go index b4f067078..b57bee230 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -42,7 +42,6 @@ const ( blockLANAccessFlag = "block-lan-access" uploadBundle = "upload-bundle" uploadBundleURL = "upload-bundle-url" - defaultBundleURL = "https://upload.debug.netbird.io" + types.GetURLPath ) var ( @@ -188,7 +187,7 @@ func init() { debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", true, "Adds system information to the debug bundle") debugCmd.PersistentFlags().BoolVarP(&debugUploadBundle, uploadBundle, "U", false, fmt.Sprintf("Uploads the debug bundle to a server from URL defined by %s", uploadBundleURL)) - debugCmd.PersistentFlags().StringVar(&debugUploadBundleURL, uploadBundleURL, defaultBundleURL, "Service URL to get an URL to upload the debug bundle") + debugCmd.PersistentFlags().StringVar(&debugUploadBundleURL, uploadBundleURL, types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle") } // SetupCloseHandler handles SIGTERM signal and exits with success diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index d0b1bacf6..d8c1ee7a2 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -51,7 +51,7 @@ const ( ) func main() { - daemonAddr, showSettings, showNetworks, errorMsg, saveLogsInFile := parseFlags() + daemonAddr, showSettings, showNetworks, showDebug, errorMsg, saveLogsInFile := parseFlags() // Initialize file logging if needed. if saveLogsInFile { @@ -72,13 +72,13 @@ func main() { } // Create the service client (this also builds the settings or networks UI if requested). - client := newServiceClient(daemonAddr, a, showSettings, showNetworks) + client := newServiceClient(daemonAddr, a, showSettings, showNetworks, showDebug) // Watch for theme/settings changes to update the icon. go watchSettingsChanges(a, client) // Run in window mode if any UI flag was set. - if showSettings || showNetworks { + if showSettings || showNetworks || showDebug { a.Run() return } @@ -99,7 +99,7 @@ func main() { } // parseFlags reads and returns all needed command-line flags. -func parseFlags() (daemonAddr string, showSettings, showNetworks bool, errorMsg string, saveLogsInFile bool) { +func parseFlags() (daemonAddr string, showSettings, showNetworks, showDebug bool, errorMsg string, saveLogsInFile bool) { defaultDaemonAddr := "unix:///var/run/netbird.sock" if runtime.GOOS == "windows" { defaultDaemonAddr = "tcp://127.0.0.1:41731" @@ -107,24 +107,16 @@ func parseFlags() (daemonAddr string, showSettings, showNetworks bool, errorMsg flag.StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") flag.BoolVar(&showSettings, "settings", false, "run settings window") flag.BoolVar(&showNetworks, "networks", false, "run networks window") + flag.BoolVar(&showDebug, "debug", false, "run debug window") flag.StringVar(&errorMsg, "error-msg", "", "displays an error message window") - - tmpDir := "/tmp" - if runtime.GOOS == "windows" { - tmpDir = os.TempDir() - } - flag.BoolVar(&saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", tmpDir)) + flag.BoolVar(&saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir())) flag.Parse() return } // initLogFile initializes logging into a file. func initLogFile() error { - tmpDir := "/tmp" - if runtime.GOOS == "windows" { - tmpDir = os.TempDir() - } - logFile := path.Join(tmpDir, fmt.Sprintf("netbird-ui-%d.log", os.Getpid())) + logFile := path.Join(os.TempDir(), fmt.Sprintf("netbird-ui-%d.log", os.Getpid())) return util.InitLog("trace", logFile) } @@ -231,7 +223,7 @@ type serviceClient struct { daemonVersion string updateIndicationLock sync.Mutex isUpdateIconActive bool - showRoutes bool + showNetworks bool wRoutes fyne.Window eventManager *event.Manager @@ -248,7 +240,7 @@ type menuHandler struct { // newServiceClient instance constructor // // This constructor also builds the UI elements for the settings window. -func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes bool) *serviceClient { +func newServiceClient(addr string, a fyne.App, showSettings bool, showNetworks bool, showDebug bool) *serviceClient { s := &serviceClient{ ctx: context.Background(), addr: addr, @@ -256,17 +248,21 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo sendNotification: false, showAdvancedSettings: showSettings, - showRoutes: showRoutes, + showNetworks: showNetworks, update: version.NewUpdate(), } s.setNewIcons() - if showSettings { + switch { + case showSettings: + s.showSettingsUI() return s - } else if showRoutes { + case showNetworks: s.showNetworksUI() + case showDebug: + s.showDebugUI() } return s @@ -743,11 +739,10 @@ func (s *serviceClient) onTrayReady() { s.runSelfCommand("settings", "true") }() case <-s.mCreateDebugBundle.ClickedCh: + s.mCreateDebugBundle.Disable() go func() { - if err := s.createAndOpenDebugBundle(); err != nil { - log.Errorf("Failed to create debug bundle: %v", err) - s.app.SendNotification(fyne.NewNotification("Error", "Failed to create debug bundle")) - } + defer s.mCreateDebugBundle.Enable() + s.runSelfCommand("debug", "true") }() case <-s.mQuit.ClickedCh: systray.Quit() diff --git a/client/ui/debug.go b/client/ui/debug.go index 845ea284c..e950e6d1e 100644 --- a/client/ui/debug.go +++ b/client/ui/debug.go @@ -7,44 +7,270 @@ import ( "path/filepath" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/widget" + log "github.com/sirupsen/logrus" "github.com/skratchdot/open-golang/open" "github.com/netbirdio/netbird/client/proto" nbstatus "github.com/netbirdio/netbird/client/status" + uptypes "github.com/netbirdio/netbird/upload-server/types" ) -func (s *serviceClient) createAndOpenDebugBundle() error { +func (s *serviceClient) showDebugUI() { + w := s.app.NewWindow("NetBird Debug") + w.Resize(fyne.NewSize(600, 400)) + w.SetFixedSize(true) + + anonymizeCheck := widget.NewCheck("Anonymize sensitive information (Public IPs, domains, ...)", nil) + systemInfoCheck := widget.NewCheck("Include system information", nil) + systemInfoCheck.SetChecked(true) + uploadCheck := widget.NewCheck("Upload bundle automatically after creation", nil) + uploadCheck.SetChecked(true) + + uploadURLLabel := widget.NewLabel("Debug upload URL:") + uploadURL := widget.NewEntry() + uploadURL.SetText(uptypes.DefaultBundleURL) + uploadURL.SetPlaceHolder("Enter upload URL") + + statusLabel := widget.NewLabel("") + statusLabel.Hide() + + createButton := widget.NewButton("Create Debug Bundle", nil) + + uploadURLContainer := container.NewVBox( + uploadURLLabel, + uploadURL, + ) + + uploadCheck.OnChanged = func(checked bool) { + if checked { + uploadURLContainer.Show() + } else { + uploadURLContainer.Hide() + } + } + + createButton.OnTapped = s.getCreateHandler(createButton, statusLabel, uploadCheck, uploadURL, anonymizeCheck, systemInfoCheck, w) + + content := container.NewVBox( + widget.NewLabel("Create a debug bundle to help troubleshoot issues with NetBird"), + widget.NewLabel(""), + anonymizeCheck, + systemInfoCheck, + uploadCheck, + uploadURLContainer, + widget.NewLabel(""), + statusLabel, + createButton, + ) + + paddedContent := container.NewPadded(content) + w.SetContent(paddedContent) + + w.Show() +} + +func (s *serviceClient) getCreateHandler( + createButton *widget.Button, + statusLabel *widget.Label, + uploadCheck *widget.Check, + uploadURL *widget.Entry, + anonymizeCheck *widget.Check, + systemInfoCheck *widget.Check, + w fyne.Window, +) func() { + return func() { + createButton.Disable() + statusLabel.SetText("Creating debug bundle...") + statusLabel.Show() + + var url string + if uploadCheck.Checked { + url = uploadURL.Text + if url == "" { + statusLabel.SetText("Error: Upload URL is required when upload is enabled") + createButton.Enable() + return + } + } + + go s.handleDebugCreation(anonymizeCheck.Checked, systemInfoCheck.Checked, uploadCheck.Checked, url, statusLabel, createButton, w) + } +} + +func (s *serviceClient) handleDebugCreation( + anonymize bool, + systemInfo bool, + upload bool, + uploadURL string, + statusLabel *widget.Label, + createButton *widget.Button, + w fyne.Window, +) { + log.Infof("Creating debug bundle (Anonymized: %v, System Info: %v, Upload Attempt: %v)...", + anonymize, systemInfo, upload) + + resp, err := s.createDebugBundle(anonymize, systemInfo, uploadURL) + if err != nil { + log.Errorf("Failed to create debug bundle: %v", err) + statusLabel.SetText(fmt.Sprintf("Error creating bundle: %v", err)) + createButton.Enable() + return + } + + localPath := resp.GetPath() + uploadFailureReason := resp.GetUploadFailureReason() + uploadedKey := resp.GetUploadedKey() + + if upload { + if uploadFailureReason != "" { + showUploadFailedDialog(w, localPath, uploadFailureReason) + } else { + showUploadSuccessDialog(w, localPath, uploadedKey) + } + } else { + showBundleCreatedDialog(w, localPath) + } + + createButton.Enable() + statusLabel.SetText("Bundle created successfully") +} + +func (s *serviceClient) createDebugBundle(anonymize bool, systemInfo bool, uploadURL string) (*proto.DebugBundleResponse, error) { conn, err := s.getSrvClient(failFastTimeout) if err != nil { - return fmt.Errorf("get client: %v", err) + return nil, fmt.Errorf("get client: %v", err) } statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true}) if err != nil { - return fmt.Errorf("failed to get status: %v", err) + log.Warnf("failed to get status for debug bundle: %v", err) } - overview := nbstatus.ConvertToStatusOutputOverview(statusResp, true, "", nil, nil, nil) - statusOutput := nbstatus.ParseToFullDetailSummary(overview) + var statusOutput string + if statusResp != nil { + overview := nbstatus.ConvertToStatusOutputOverview(statusResp, anonymize, "", nil, nil, nil) + statusOutput = nbstatus.ParseToFullDetailSummary(overview) + } - resp, err := conn.DebugBundle(s.ctx, &proto.DebugBundleRequest{ - Anonymize: true, + request := &proto.DebugBundleRequest{ + Anonymize: anonymize, Status: statusOutput, - SystemInfo: true, - }) + SystemInfo: systemInfo, + } + + if uploadURL != "" { + request.UploadURL = uploadURL + } + + resp, err := conn.DebugBundle(s.ctx, request) if err != nil { - return fmt.Errorf("failed to create debug bundle: %v", err) + return nil, fmt.Errorf("failed to create debug bundle via daemon: %v", err) } - bundleDir := filepath.Dir(resp.GetPath()) - if err := open.Start(bundleDir); err != nil { - return fmt.Errorf("failed to open debug bundle directory: %v", err) - } - - s.app.SendNotification(fyne.NewNotification( - "Debug Bundle", - fmt.Sprintf("Debug bundle created at %s. Administrator privileges are required to access it.", resp.GetPath()), - )) - - return nil + return resp, nil +} + +// showUploadFailedDialog displays a dialog when upload fails +func showUploadFailedDialog(parent fyne.Window, localPath, failureReason string) { + content := container.NewVBox( + widget.NewLabel(fmt.Sprintf("Bundle upload failed:\n%s\n\n"+ + "A local copy was saved at:\n%s", failureReason, localPath)), + ) + + customDialog := dialog.NewCustom("Upload Failed", "Cancel", content, parent) + + buttonBox := container.NewHBox( + widget.NewButton("Open File", func() { + log.Infof("Attempting to open local file: %s", localPath) + if openErr := open.Start(localPath); openErr != nil { + log.Errorf("Failed to open local file '%s': %v", localPath, openErr) + dialog.ShowError(fmt.Errorf("Failed to open the local file:\n%s\n\nError: %v", localPath, openErr), parent) + } + customDialog.Hide() + }), + widget.NewButton("Open Folder", func() { + folderPath := filepath.Dir(localPath) + log.Infof("Attempting to open local folder: %s", folderPath) + if openErr := open.Start(folderPath); openErr != nil { + log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr) + dialog.ShowError(fmt.Errorf("Failed to open the local folder:\n%s\n\nError: %v", folderPath, openErr), parent) + } + customDialog.Hide() + }), + ) + + content.Add(buttonBox) + customDialog.Show() +} + +// showUploadSuccessDialog displays a dialog when upload succeeds +func showUploadSuccessDialog(parent fyne.Window, localPath, uploadedKey string) { + keyEntry := widget.NewEntry() + keyEntry.SetText(uploadedKey) + keyEntry.Disable() + + content := container.NewVBox( + widget.NewLabel("Bundle uploaded successfully!"), + widget.NewLabel(""), + widget.NewLabel("Upload Key:"), + keyEntry, + widget.NewLabel(""), + widget.NewLabel(fmt.Sprintf("Local copy saved at:\n%s", localPath)), + ) + + customDialog := dialog.NewCustom("Upload Successful", "OK", content, parent) + + buttonBox := container.NewHBox( + widget.NewButton("Copy Key", func() { + parent.Clipboard().SetContent(uploadedKey) + log.Info("Upload key copied to clipboard") + }), + widget.NewButton("Open Local Folder", func() { + folderPath := filepath.Dir(localPath) + log.Infof("Attempting to open local folder: %s", folderPath) + if openErr := open.Start(folderPath); openErr != nil { + log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr) + dialog.ShowError(fmt.Errorf("Failed to open the local folder:\n%s\n\nError: %v", folderPath, openErr), parent) + } + }), + ) + + content.Add(buttonBox) + customDialog.Show() +} + +// showBundleCreatedDialog displays a dialog when bundle is created without upload +func showBundleCreatedDialog(parent fyne.Window, localPath string) { + content := container.NewVBox( + widget.NewLabel(fmt.Sprintf("Bundle created locally at:\n%s\n\n"+ + "Administrator privileges may be required to access the file.", localPath)), + ) + + customDialog := dialog.NewCustom("Debug Bundle Created", "Cancel", content, parent) + + buttonBox := container.NewHBox( + widget.NewButton("Open File", func() { + log.Infof("Attempting to open local file: %s", localPath) + if openErr := open.Start(localPath); openErr != nil { + log.Errorf("Failed to open local file '%s': %v", localPath, openErr) + dialog.ShowError(fmt.Errorf("Failed to open the local file:\n%s\n\nError: %v", localPath, openErr), parent) + } + customDialog.Hide() + }), + widget.NewButton("Open Folder", func() { + folderPath := filepath.Dir(localPath) + log.Infof("Attempting to open local folder: %s", folderPath) + if openErr := open.Start(folderPath); openErr != nil { + log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr) + dialog.ShowError(fmt.Errorf("Failed to open the local folder:\n%s\n\nError: %v", folderPath, openErr), parent) + } + customDialog.Hide() + }), + ) + + content.Add(buttonBox) + customDialog.Show() } diff --git a/upload-server/types/upload.go b/upload-server/types/upload.go index 35d003582..327c28e75 100644 --- a/upload-server/types/upload.go +++ b/upload-server/types/upload.go @@ -7,6 +7,8 @@ const ( ClientHeaderValue = "netbird" // GetURLPath is the path for the GetURL request GetURLPath = "/upload-url" + + DefaultBundleURL = "https://upload.debug.netbird.io" + GetURLPath ) // GetURLResponse is the response for the GetURL request