Refactor status functions and add first tests

This commit is contained in:
Pascal Fischer 2023-02-23 20:13:19 +01:00
parent c3ed08c249
commit e75535d30b
2 changed files with 295 additions and 130 deletions

View File

@ -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 <YOUR_MANAGEMENT_URL> --setup-key <YOUR_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 <YOUR_MANAGEMENT_URL> --setup-key <YOUR_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 {

134
client/cmd/status_test.go Normal file
View File

@ -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)
}