diff --git a/client/cmd/status.go b/client/cmd/status.go index 1582e8b90..78179c72b 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -2,7 +2,9 @@ package cmd import ( "context" + "encoding/json" "fmt" + yaml2 "gopkg.in/yaml.v2" "net" "net/netip" "sort" @@ -21,6 +23,8 @@ import ( var ( detailFlag bool ipv4Flag bool + jsonFlag bool + yamlFlag bool ipsFilter []string statusFilter string ipsFilterMap map[string]struct{} @@ -29,67 +33,92 @@ var ( var statusCmd = &cobra.Command{ Use: "status", Short: "status of the Netbird Service", - RunE: func(cmd *cobra.Command, args []string) error { - SetFlagsFromEnvVars(rootCmd) - - cmd.SetOut(cmd.OutOrStdout()) - - err := parseFilters() - if err != nil { - return err - } - - err = util.InitLog(logLevel, "console") - if err != nil { - return fmt.Errorf("failed initializing log %v", err) - } - - ctx := internal.CtxInitState(context.Background()) - - conn, err := DialClientGRPCServer(ctx, daemonAddr) - if err != nil { - return fmt.Errorf("failed to connect to daemon error: %v\n"+ - "If the daemon is not running please run: "+ - "\nnetbird service install \nnetbird service start\n", err) - } - defer conn.Close() - - resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true}) - if err != nil { - return fmt.Errorf("status failed: %v", status.Convert(err).Message()) - } - - daemonStatus := fmt.Sprintf("Daemon status: %s\n", resp.GetStatus()) - if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) { - - cmd.Printf("%s\n"+ - "Run UP command to log in with SSO (interactive login):\n\n"+ - " netbird up \n\n"+ - "If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+ - "you can use a setup-key:\n\n netbird up --management-url --setup-key \n\n"+ - "More info: https://www.netbird.io/docs/overview/setup-keys\n\n", - daemonStatus, - ) - return nil - } - - pbFullStatus := resp.GetFullStatus() - fullStatus := fromProtoFullStatus(pbFullStatus) - - cmd.Print(parseFullStatus(fullStatus, detailFlag, daemonStatus, resp.GetDaemonVersion(), ipv4Flag)) - - return nil - }, + RunE: statusFunc, } func init() { ipsFilterMap = make(map[string]struct{}) - statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information") + statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information in human-readable format") + statusCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "display detailed status information in json format") + statusCmd.PersistentFlags().BoolVar(&yamlFlag, "yaml", false, "display detailed status information in yaml format") statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33") + statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4") statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200") statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected") } +func statusFunc(cmd *cobra.Command, args []string) error { + SetFlagsFromEnvVars(rootCmd) + + cmd.SetOut(cmd.OutOrStdout()) + + err := parseFilters() + if err != nil { + return err + } + + err = util.InitLog(logLevel, "console") + if err != nil { + return fmt.Errorf("failed initializing log %v", err) + } + + ctx := internal.CtxInitState(context.Background()) + + conn, err := DialClientGRPCServer(ctx, daemonAddr) + if err != nil { + return fmt.Errorf("failed to connect to daemon error: %v\n"+ + "If the daemon is not running please run: "+ + "\nnetbird service install \nnetbird service start\n", err) + } + defer conn.Close() + + resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true}) + if err != nil { + return fmt.Errorf("status failed: %v", status.Convert(err).Message()) + } + + daemonStatus := fmt.Sprintf("Daemon status: %s\n", resp.GetStatus()) + if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) { + + cmd.Printf("%s\n"+ + "Run UP command to log in with SSO (interactive login):\n\n"+ + " netbird up \n\n"+ + "If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+ + "you can use a setup-key:\n\n netbird up --management-url --setup-key \n\n"+ + "More info: https://www.netbird.io/docs/overview/setup-keys\n\n", + daemonStatus, + ) + return nil + } + + pbFullStatus := resp.GetFullStatus() + fullStatus := fromProtoFullStatus(pbFullStatus) + + statusOutputString := "" + if detailFlag { + statusOutputString = parseToHumanReadable(fullStatus, daemonStatus, resp.GetDaemonVersion()) + } + if jsonFlag { + statusOutputString, err = parseToJson(fullStatus) + if err != nil { + return fmt.Errorf("json marshal failed") + } + } + if yamlFlag { + statusOutputString, err = parseToYaml(fullStatus) + if err != nil { + return fmt.Errorf("yaml marshal failed") + } + } + if ipv4Flag { + statusOutputString = parseInterfaceIP(fullStatus.LocalPeerState.IP) + } + + cmd.Print(statusOutputString) + + return nil +} + func parseFilters() error { switch strings.ToLower(statusFilter) { case "", "disconnected", "connected": @@ -148,40 +177,51 @@ func fromProtoFullStatus(pbFullStatus *proto.FullStatus) nbStatus.FullStatus { return fullStatus } -func parseFullStatus(fullStatus nbStatus.FullStatus, printDetail bool, daemonStatus string, daemonVersion string, flag bool) string { - - interfaceIP := fullStatus.LocalPeerState.IP - +func parseInterfaceIP(interfaceIP string) string { ip, _, err := net.ParseCIDR(interfaceIP) if err != nil { return "" } + return fmt.Sprintf("%s\n", ip) +} - if ipv4Flag { - return fmt.Sprintf("%s\n", ip) +func parseToJson(fullStatus nbStatus.FullStatus) (string, error) { + jsonBytes, err := json.Marshal(fullStatus) + return string(jsonBytes), err +} + +func parseToYaml(fullStatus nbStatus.FullStatus) (string, error) { + yamlBytes, err := yaml2.Marshal(fullStatus) + return string(yamlBytes), err +} + +func countConnectedPeers(peers []nbStatus.PeerState) int { + peersConnected := 0 + for _, peerState := range peers { + if peerState.ConnStatus == peer.StatusConnected.String() { + peersConnected = peersConnected + 1 + } } + return peersConnected +} - var ( - managementStatusURL = "" - signalStatusURL = "" - managementConnString = "Disconnected" - signalConnString = "Disconnected" - interfaceTypeString = "Userspace" - ) +func parseGeneralSummary(fullStatus nbStatus.FullStatus, daemonStatus string, daemonVersion string) string { - if printDetail { - managementStatusURL = fmt.Sprintf(" to %s", fullStatus.ManagementState.URL) - signalStatusURL = fmt.Sprintf(" to %s", fullStatus.SignalState.URL) - } + managementStatusURL := fmt.Sprintf(" to %s", fullStatus.ManagementState.URL) + signalStatusURL := fmt.Sprintf(" to %s", fullStatus.SignalState.URL) + managementConnString := "Disconnected" if fullStatus.ManagementState.Connected { managementConnString = "Connected" } + signalConnString := "Disconnected" if fullStatus.SignalState.Connected { signalConnString = "Connected" } + interfaceTypeString := "Userspace" + interfaceIP := "" if fullStatus.LocalPeerState.KernelInterface { interfaceTypeString = "Kernel" } else if fullStatus.LocalPeerState.IP == "" { @@ -189,8 +229,7 @@ func parseFullStatus(fullStatus nbStatus.FullStatus, printDetail bool, daemonSta interfaceIP = "N/A" } - parsedPeersString, peersConnected := parsePeers(fullStatus.Peers, printDetail) - + peersConnected := countConnectedPeers(fullStatus.Peers) peersCountString := fmt.Sprintf("%d/%d Connected", peersConnected, len(fullStatus.Peers)) summary := fmt.Sprintf( @@ -215,23 +254,25 @@ func parseFullStatus(fullStatus nbStatus.FullStatus, printDetail bool, daemonSta interfaceTypeString, peersCountString, ) - - if printDetail { - return fmt.Sprintf( - "Peers detail:"+ - "%s\n"+ - "%s", - parsedPeersString, - summary, - ) - } return summary } -func parsePeers(peers []nbStatus.PeerState, printDetail bool) (string, int) { +func parseToHumanReadable(fullStatus nbStatus.FullStatus, daemonStatus string, daemonVersion string) string { + parsedPeersString := parsePeers(fullStatus.Peers) + summary := parseGeneralSummary(fullStatus, daemonStatus, daemonVersion) + + return fmt.Sprintf( + "Peers detail:"+ + "%s\n"+ + "%s", + parsedPeersString, + summary, + ) +} + +func parsePeers(peers []nbStatus.PeerState) string { var ( - peersString = "" - peersConnected = 0 + peersString = "" ) if len(peers) > 0 { @@ -242,59 +283,49 @@ func parsePeers(peers []nbStatus.PeerState, printDetail bool) (string, int) { }) } - connectedStatusString := peer.StatusConnected.String() - for _, peerState := range peers { - peerConnectionStatus := false - if peerState.ConnStatus == connectedStatusString { - peersConnected = peersConnected + 1 - peerConnectionStatus = true + peerConnectionStatus := peerState.ConnStatus == peer.StatusConnected.String() + if skipDetailByFilters(peerState, peerConnectionStatus) { + continue } - if printDetail { + localICE := "-" + remoteICE := "-" + connType := "-" - if skipDetailByFilters(peerState, peerConnectionStatus) { - continue + if peerConnectionStatus { + localICE = peerState.LocalIceCandidateType + remoteICE = peerState.RemoteIceCandidateType + connType = "P2P" + if peerState.Relayed { + connType = "Relayed" } - - localICE := "-" - remoteICE := "-" - connType := "-" - - if peerConnectionStatus { - localICE = peerState.LocalIceCandidateType - remoteICE = peerState.RemoteIceCandidateType - connType = "P2P" - if peerState.Relayed { - connType = "Relayed" - } - } - - peerString := fmt.Sprintf( - "\n %s:\n"+ - " NetBird IP: %s\n"+ - " Public key: %s\n"+ - " Status: %s\n"+ - " -- detail --\n"+ - " Connection type: %s\n"+ - " Direct: %t\n"+ - " ICE candidate (Local/Remote): %s/%s\n"+ - " Last connection update: %s\n", - peerState.FQDN, - peerState.IP, - peerState.PubKey, - peerState.ConnStatus, - connType, - peerState.Direct, - localICE, - remoteICE, - peerState.ConnStatusUpdate.Format("2006-01-02 15:04:05"), - ) - - peersString = peersString + peerString } + + peerString := fmt.Sprintf( + "\n %s:\n"+ + " NetBird IP: %s\n"+ + " Public key: %s\n"+ + " Status: %s\n"+ + " -- detail --\n"+ + " Connection type: %s\n"+ + " Direct: %t\n"+ + " ICE candidate (Local/Remote): %s/%s\n"+ + " Last connection update: %s\n", + peerState.FQDN, + peerState.IP, + peerState.PubKey, + peerState.ConnStatus, + connType, + peerState.Direct, + localICE, + remoteICE, + peerState.ConnStatusUpdate.Format("2006-01-02 15:04:05"), + ) + + peersString = peersString + peerString } - return peersString, peersConnected + return peersString } func skipDetailByFilters(peerState nbStatus.PeerState, isConnected bool) bool { diff --git a/client/cmd/status_test.go b/client/cmd/status_test.go new file mode 100644 index 000000000..ea94282c0 --- /dev/null +++ b/client/cmd/status_test.go @@ -0,0 +1,134 @@ +package cmd + +import ( + nbStatus "github.com/netbirdio/netbird/client/status" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +var fullStatus = nbStatus.FullStatus{ + Peers: []nbStatus.PeerState{ + { + IP: "192.168.178.101", + PubKey: "Pubkey1", + FQDN: "peer-1.awesome-domain.com", + ConnStatus: "Connected", + ConnStatusUpdate: time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC), + Relayed: false, + LocalIceCandidateType: "-", + RemoteIceCandidateType: "-", + }, + { + IP: "192.168.178.102", + PubKey: "Pubkey2", + FQDN: "peer-2.awesome-domain.com", + ConnStatus: "Connected", + ConnStatusUpdate: time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC), + Relayed: false, + LocalIceCandidateType: "-", + RemoteIceCandidateType: "-", + }, + }, + ManagementState: nbStatus.ManagementState{ + URL: "my-awesome-management.com:443", + Connected: true, + }, + SignalState: nbStatus.SignalState{ + URL: "my-awesome-signal.com:443", + Connected: true, + }, + LocalPeerState: nbStatus.LocalPeerState{ + IP: "192.168.178.2", + PubKey: "Some-Pub-Key", + KernelInterface: false, + FQDN: "some-localhost.awesome-domain.com", + }, +} + +// @formatter:off +func TestParsingToJson(t *testing.T) { + json, _ := parseToJson(fullStatus) + + expectedJson := "{" + + "\"Peers\":" + + "[" + + "{" + + "\"IP\":\"192.168.178.101\"," + + "\"PubKey\":\"Pubkey1\"," + + "\"FQDN\":\"peer-1.awesome-domain.com\"," + + "\"ConnStatus\":\"Connected\"," + + "\"ConnStatusUpdate\":\"2001-01-01T01:01:01Z\"," + + "\"Relayed\":false," + + "\"Direct\":false," + + "\"LocalIceCandidateType\":\"-\"," + + "\"RemoteIceCandidateType\":\"-\"" + + "}," + + "{" + + "\"IP\":\"192.168.178.102\"," + + "\"PubKey\":\"Pubkey2\"," + + "\"FQDN\":\"peer-2.awesome-domain.com\"," + + "\"ConnStatus\":\"Connected\"," + + "\"ConnStatusUpdate\":\"2002-02-02T02:02:02Z\"," + + "\"Relayed\":false," + + "\"Direct\":false," + + "\"LocalIceCandidateType\":\"-\"," + + "\"RemoteIceCandidateType\":\"-\"" + + "}" + + "]," + + "\"ManagementState\":" + + "{" + + "\"URL\":\"my-awesome-management.com:443\"," + + "\"Connected\":true" + + "}," + + "\"SignalState\":" + + "{" + + "\"URL\":\"my-awesome-signal.com:443\"," + + "\"Connected\":true" + + "}," + + "\"LocalPeerState\":" + + "{" + + "\"IP\":\"192.168.178.2\"," + + "\"PubKey\":\"Some-Pub-Key\"," + + "\"KernelInterface\":false," + + "\"FQDN\":\"some-localhost.awesome-domain.com\"" + + "}" + + "}" + assert.Equal(t, expectedJson, json) +} + +func TestParsingToYaml(t *testing.T) { + yaml, _ := parseToYaml(fullStatus) + + expectedYaml := "peers:\n" + + "- ip: 192.168.178.101\n" + + " pubkey: Pubkey1\n" + + " fqdn: peer-1.awesome-domain.com\n" + + " connstatus: Connected\n" + + " connstatusupdate: 2001-01-01T01:01:01Z\n" + + " relayed: false\n" + + " direct: false\n" + + " localicecandidatetype: '-'\n" + + " remoteicecandidatetype: '-'\n" + + "- ip: 192.168.178.102\n" + + " pubkey: Pubkey2\n" + + " fqdn: peer-2.awesome-domain.com\n" + + " connstatus: Connected\n" + + " connstatusupdate: 2002-02-02T02:02:02Z\n" + + " relayed: false\n" + + " direct: false\n" + + " localicecandidatetype: '-'\n" + + " remoteicecandidatetype: '-'\n" + + "managementstate:\n" + + " url: my-awesome-management.com:443\n" + + " connected: true\n" + + "signalstate:\n" + + " url: my-awesome-signal.com:443\n" + + " connected: true\n" + + "localpeerstate:\n" + + " ip: 192.168.178.2\n" + + " pubkey: Some-Pub-Key\n" + + " kernelinterface: false\n" + + " fqdn: some-localhost.awesome-domain.com\n" + assert.Equal(t, expectedYaml, yaml) +}