[client] Add debug upload option to netbird ui (#3768)

This commit is contained in:
Viktor Liu
2025-05-01 00:48:31 +02:00
committed by GitHub
parent b5419ef11a
commit 9bc7d788f0
4 changed files with 269 additions and 47 deletions

View File

@ -42,7 +42,6 @@ const (
blockLANAccessFlag = "block-lan-access" blockLANAccessFlag = "block-lan-access"
uploadBundle = "upload-bundle" uploadBundle = "upload-bundle"
uploadBundleURL = "upload-bundle-url" uploadBundleURL = "upload-bundle-url"
defaultBundleURL = "https://upload.debug.netbird.io" + types.GetURLPath
) )
var ( var (
@ -188,7 +187,7 @@ func init() {
debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", true, "Adds system information to the debug bundle") 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().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 // SetupCloseHandler handles SIGTERM signal and exits with success

View File

@ -51,7 +51,7 @@ const (
) )
func main() { func main() {
daemonAddr, showSettings, showNetworks, errorMsg, saveLogsInFile := parseFlags() daemonAddr, showSettings, showNetworks, showDebug, errorMsg, saveLogsInFile := parseFlags()
// Initialize file logging if needed. // Initialize file logging if needed.
if saveLogsInFile { if saveLogsInFile {
@ -72,13 +72,13 @@ func main() {
} }
// Create the service client (this also builds the settings or networks UI if requested). // 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. // Watch for theme/settings changes to update the icon.
go watchSettingsChanges(a, client) go watchSettingsChanges(a, client)
// Run in window mode if any UI flag was set. // Run in window mode if any UI flag was set.
if showSettings || showNetworks { if showSettings || showNetworks || showDebug {
a.Run() a.Run()
return return
} }
@ -99,7 +99,7 @@ func main() {
} }
// parseFlags reads and returns all needed command-line flags. // 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" defaultDaemonAddr := "unix:///var/run/netbird.sock"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
defaultDaemonAddr = "tcp://127.0.0.1:41731" 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.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(&showSettings, "settings", false, "run settings window")
flag.BoolVar(&showNetworks, "networks", false, "run networks 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") flag.StringVar(&errorMsg, "error-msg", "", "displays an error message window")
flag.BoolVar(&saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir()))
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.Parse() flag.Parse()
return return
} }
// initLogFile initializes logging into a file. // initLogFile initializes logging into a file.
func initLogFile() error { func initLogFile() error {
tmpDir := "/tmp" logFile := path.Join(os.TempDir(), fmt.Sprintf("netbird-ui-%d.log", os.Getpid()))
if runtime.GOOS == "windows" {
tmpDir = os.TempDir()
}
logFile := path.Join(tmpDir, fmt.Sprintf("netbird-ui-%d.log", os.Getpid()))
return util.InitLog("trace", logFile) return util.InitLog("trace", logFile)
} }
@ -231,7 +223,7 @@ type serviceClient struct {
daemonVersion string daemonVersion string
updateIndicationLock sync.Mutex updateIndicationLock sync.Mutex
isUpdateIconActive bool isUpdateIconActive bool
showRoutes bool showNetworks bool
wRoutes fyne.Window wRoutes fyne.Window
eventManager *event.Manager eventManager *event.Manager
@ -248,7 +240,7 @@ type menuHandler struct {
// newServiceClient instance constructor // newServiceClient instance constructor
// //
// This constructor also builds the UI elements for the settings window. // 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{ s := &serviceClient{
ctx: context.Background(), ctx: context.Background(),
addr: addr, addr: addr,
@ -256,17 +248,21 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo
sendNotification: false, sendNotification: false,
showAdvancedSettings: showSettings, showAdvancedSettings: showSettings,
showRoutes: showRoutes, showNetworks: showNetworks,
update: version.NewUpdate(), update: version.NewUpdate(),
} }
s.setNewIcons() s.setNewIcons()
if showSettings { switch {
case showSettings:
s.showSettingsUI() s.showSettingsUI()
return s return s
} else if showRoutes { case showNetworks:
s.showNetworksUI() s.showNetworksUI()
case showDebug:
s.showDebugUI()
} }
return s return s
@ -743,11 +739,10 @@ func (s *serviceClient) onTrayReady() {
s.runSelfCommand("settings", "true") s.runSelfCommand("settings", "true")
}() }()
case <-s.mCreateDebugBundle.ClickedCh: case <-s.mCreateDebugBundle.ClickedCh:
s.mCreateDebugBundle.Disable()
go func() { go func() {
if err := s.createAndOpenDebugBundle(); err != nil { defer s.mCreateDebugBundle.Enable()
log.Errorf("Failed to create debug bundle: %v", err) s.runSelfCommand("debug", "true")
s.app.SendNotification(fyne.NewNotification("Error", "Failed to create debug bundle"))
}
}() }()
case <-s.mQuit.ClickedCh: case <-s.mQuit.ClickedCh:
systray.Quit() systray.Quit()

View File

@ -7,44 +7,270 @@ import (
"path/filepath" "path/filepath"
"fyne.io/fyne/v2" "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/skratchdot/open-golang/open"
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
nbstatus "github.com/netbirdio/netbird/client/status" 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) conn, err := s.getSrvClient(failFastTimeout)
if err != nil { 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}) statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true})
if err != nil { 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) var statusOutput string
statusOutput := nbstatus.ParseToFullDetailSummary(overview) if statusResp != nil {
overview := nbstatus.ConvertToStatusOutputOverview(statusResp, anonymize, "", nil, nil, nil)
statusOutput = nbstatus.ParseToFullDetailSummary(overview)
}
resp, err := conn.DebugBundle(s.ctx, &proto.DebugBundleRequest{ request := &proto.DebugBundleRequest{
Anonymize: true, Anonymize: anonymize,
Status: statusOutput, Status: statusOutput,
SystemInfo: true, SystemInfo: systemInfo,
}) }
if uploadURL != "" {
request.UploadURL = uploadURL
}
resp, err := conn.DebugBundle(s.ctx, request)
if err != nil { 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()) return resp, nil
if err := open.Start(bundleDir); err != nil { }
return fmt.Errorf("failed to open debug bundle directory: %v", err)
} // showUploadFailedDialog displays a dialog when upload fails
func showUploadFailedDialog(parent fyne.Window, localPath, failureReason string) {
s.app.SendNotification(fyne.NewNotification( content := container.NewVBox(
"Debug Bundle", widget.NewLabel(fmt.Sprintf("Bundle upload failed:\n%s\n\n"+
fmt.Sprintf("Debug bundle created at %s. Administrator privileges are required to access it.", resp.GetPath()), "A local copy was saved at:\n%s", failureReason, localPath)),
)) )
return nil 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()
} }

View File

@ -7,6 +7,8 @@ const (
ClientHeaderValue = "netbird" ClientHeaderValue = "netbird"
// GetURLPath is the path for the GetURL request // GetURLPath is the path for the GetURL request
GetURLPath = "/upload-url" GetURLPath = "/upload-url"
DefaultBundleURL = "https://upload.debug.netbird.io" + GetURLPath
) )
// GetURLResponse is the response for the GetURL request // GetURLResponse is the response for the GetURL request