mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 19:49:56 +00:00
* Restores behavior to create profile if not there on Up * Allows to restore nerbird status showing of the profile name * [client] Reduce upFunc cognitive complexity Extract the profile switch/auto-create logic from upFunc into a dedicated switchOrCreateProfile helper. The inlined NotFound-retry branch pushed upFunc over SonarCloud's cognitive complexity threshold (S3776). No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * [client] Make up --profile auto-create idempotent under concurrent runs Don't fail switchOrCreateProfile on a createProfile error: a concurrent run may create the profile between the NotFound check and our create call. Retry the switch regardless and only surface the create error if the switch also fails. Addresses CodeRabbit race-condition feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Share createProfile with addProfileFunc * But allow conn reusage * moves switchOrCreateProfile to where it's used --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
317 lines
9.9 KiB
Go
317 lines
9.9 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
nbstatus "github.com/netbirdio/netbird/client/status"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
var (
|
|
detailFlag bool
|
|
ipv4Flag bool
|
|
ipv6Flag bool
|
|
jsonFlag bool
|
|
yamlFlag bool
|
|
ipsFilter []string
|
|
prefixNamesFilter []string
|
|
statusFilter string
|
|
ipsFilterMap map[string]struct{}
|
|
prefixNamesFilterMap map[string]struct{}
|
|
connectionTypeFilter string
|
|
checkFlag string
|
|
)
|
|
|
|
var statusCmd = &cobra.Command{
|
|
Use: "status",
|
|
Short: "Display NetBird client status",
|
|
Long: "Display the current status of the NetBird client, including connection status, peer information, and network details.",
|
|
RunE: statusFunc,
|
|
}
|
|
|
|
func init() {
|
|
ipsFilterMap = make(map[string]struct{})
|
|
prefixNamesFilterMap = make(map[string]struct{})
|
|
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information in human-readable format")
|
|
statusCmd.PersistentFlags().BoolVarP(&jsonFlag, "json", "j", false, "display detailed status information in json format")
|
|
statusCmd.PersistentFlags().BoolVarP(&yamlFlag, "yaml", "y", false, "display detailed status information in yaml format")
|
|
statusCmd.PersistentFlags().BoolVarP(&ipv4Flag, "ipv4", "4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
|
|
statusCmd.PersistentFlags().BoolVarP(&ipv6Flag, "ipv6", "6", false, "display only NetBird IPv6 of this peer")
|
|
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4", "ipv6")
|
|
statusCmd.PersistentFlags().StringSliceVarP(&ipsFilter, "filter-by-ips", "I", []string{}, "filters the detailed output by a list of one or more IPs (v4 or v6), e.g., --filter-by-ips 100.64.0.100,fd00::1")
|
|
statusCmd.PersistentFlags().StringSliceVarP(&prefixNamesFilter, "filter-by-names", "N", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud")
|
|
statusCmd.PersistentFlags().StringVarP(&statusFilter, "filter-by-status", "S", "", "filters the detailed output by connection status(idle|connecting|connected), e.g., --filter-by-status connected")
|
|
statusCmd.PersistentFlags().StringVarP(&connectionTypeFilter, "filter-by-connection-type", "T", "", "filters the detailed output by connection type (P2P|Relayed), e.g., --filter-by-connection-type P2P")
|
|
statusCmd.PersistentFlags().StringVarP(&checkFlag, "check", "C", "", "run a health check and exit with code 0 on success, 1 on failure (live|ready|startup)")
|
|
}
|
|
|
|
func statusFunc(cmd *cobra.Command, args []string) error {
|
|
SetFlagsFromEnvVars(rootCmd)
|
|
|
|
cmd.SetOut(cmd.OutOrStdout())
|
|
|
|
if checkFlag != "" {
|
|
return runHealthCheck(cmd)
|
|
}
|
|
|
|
err := parseFilters()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = util.InitLog(logLevel, util.LogConsole)
|
|
if err != nil {
|
|
return fmt.Errorf("failed initializing log %v", err)
|
|
}
|
|
|
|
ctx := internal.CtxInitState(cmd.Context())
|
|
|
|
resp, err := getStatus(ctx, true, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
status := resp.GetStatus()
|
|
|
|
needsAuth := status == string(internal.StatusNeedsLogin) || status == string(internal.StatusLoginFailed) ||
|
|
status == string(internal.StatusSessionExpired)
|
|
|
|
if needsAuth && !jsonFlag && !yamlFlag {
|
|
cmd.Printf("Daemon status: %s\n\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://docs.netbird.io/how-to/register-machines-using-setup-keys\n\n",
|
|
resp.GetStatus(),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
if ipv4Flag {
|
|
cmd.Print(parseInterfaceIP(resp.GetFullStatus().GetLocalPeerState().GetIP()))
|
|
return nil
|
|
}
|
|
|
|
if ipv6Flag {
|
|
ipv6 := resp.GetFullStatus().GetLocalPeerState().GetIpv6()
|
|
if ipv6 != "" {
|
|
cmd.Print(parseInterfaceIP(ipv6))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Resolve the active profile's display name via the daemon, which runs
|
|
// as root and can read the per-user profile files. The local profile
|
|
// manager only knows the active profile ID, not its display name.
|
|
profName := getActiveProfileName(ctx)
|
|
|
|
var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp.GetFullStatus(), nbstatus.ConvertOptions{
|
|
Anonymize: anonymizeFlag,
|
|
DaemonVersion: resp.GetDaemonVersion(),
|
|
DaemonStatus: nbstatus.ParseDaemonStatus(status),
|
|
StatusFilter: statusFilter,
|
|
PrefixNamesFilter: prefixNamesFilter,
|
|
PrefixNamesFilterMap: prefixNamesFilterMap,
|
|
IPsFilter: ipsFilterMap,
|
|
ConnectionTypeFilter: connectionTypeFilter,
|
|
ProfileName: profName,
|
|
})
|
|
var statusOutputString string
|
|
switch {
|
|
case detailFlag:
|
|
statusOutputString = outputInformationHolder.FullDetailSummary()
|
|
case jsonFlag:
|
|
statusOutputString, err = outputInformationHolder.JSON()
|
|
case yamlFlag:
|
|
statusOutputString, err = outputInformationHolder.YAML()
|
|
default:
|
|
statusOutputString = outputInformationHolder.GeneralSummary(false, false, false, false)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Print(statusOutputString)
|
|
|
|
return nil
|
|
}
|
|
|
|
func getStatus(ctx context.Context, fullPeerStatus bool, shouldRunProbes bool) (*proto.StatusResponse, error) {
|
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
if err != nil {
|
|
//nolint
|
|
return nil, 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(ctx, &proto.StatusRequest{GetFullPeerStatus: fullPeerStatus, ShouldRunProbes: shouldRunProbes})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// getActiveProfileName asks the daemon for the active profile's display
|
|
// name. The daemon runs as root and can read the per-user profile files to
|
|
// resolve the ID to its human-readable name. Returns an empty string on any
|
|
// error so status output degrades gracefully.
|
|
func getActiveProfileName(ctx context.Context) string {
|
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer conn.Close()
|
|
|
|
resp, err := proto.NewDaemonServiceClient(conn).GetActiveProfile(ctx, &proto.GetActiveProfileRequest{})
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return resp.GetProfileName()
|
|
}
|
|
|
|
func parseFilters() error {
|
|
switch strings.ToLower(statusFilter) {
|
|
case "", "idle", "connecting", "connected":
|
|
if strings.ToLower(statusFilter) != "" {
|
|
enableDetailFlagWhenFilterFlag()
|
|
}
|
|
default:
|
|
return fmt.Errorf("wrong status filter, should be one of connected|connecting|idle, got: %s", statusFilter)
|
|
}
|
|
|
|
if len(ipsFilter) > 0 {
|
|
for _, addr := range ipsFilter {
|
|
_, err := netip.ParseAddr(addr)
|
|
if err != nil {
|
|
return fmt.Errorf("got an invalid IP address in the filter: address %s, error %s", addr, err)
|
|
}
|
|
ipsFilterMap[addr] = struct{}{}
|
|
enableDetailFlagWhenFilterFlag()
|
|
}
|
|
}
|
|
|
|
if len(prefixNamesFilter) > 0 {
|
|
for _, name := range prefixNamesFilter {
|
|
prefixNamesFilterMap[strings.ToLower(name)] = struct{}{}
|
|
}
|
|
enableDetailFlagWhenFilterFlag()
|
|
}
|
|
|
|
switch strings.ToLower(connectionTypeFilter) {
|
|
case "", "p2p", "relayed":
|
|
if strings.ToLower(connectionTypeFilter) != "" {
|
|
enableDetailFlagWhenFilterFlag()
|
|
}
|
|
default:
|
|
return fmt.Errorf("wrong connection-type filter, should be one of P2P|Relayed, got: %s", connectionTypeFilter)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func enableDetailFlagWhenFilterFlag() {
|
|
if !detailFlag && !jsonFlag && !yamlFlag {
|
|
detailFlag = true
|
|
}
|
|
}
|
|
|
|
func runHealthCheck(cmd *cobra.Command) error {
|
|
check := strings.ToLower(checkFlag)
|
|
switch check {
|
|
case "live", "ready", "startup":
|
|
default:
|
|
return fmt.Errorf("unknown check %q, must be one of: live, ready, startup", checkFlag)
|
|
}
|
|
|
|
if err := util.InitLog(logLevel, util.LogConsole); err != nil {
|
|
return fmt.Errorf("init log: %w", err)
|
|
}
|
|
|
|
ctx := internal.CtxInitState(cmd.Context())
|
|
|
|
isStartup := check == "startup"
|
|
resp, err := getStatus(ctx, isStartup, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch check {
|
|
case "live":
|
|
return nil
|
|
case "ready":
|
|
return checkReadiness(resp)
|
|
case "startup":
|
|
return checkStartup(resp)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func checkReadiness(resp *proto.StatusResponse) error {
|
|
daemonStatus := internal.StatusType(resp.GetStatus())
|
|
switch daemonStatus {
|
|
case internal.StatusIdle, internal.StatusConnecting, internal.StatusConnected:
|
|
return nil
|
|
case internal.StatusNeedsLogin, internal.StatusLoginFailed, internal.StatusSessionExpired:
|
|
return fmt.Errorf("readiness check: daemon status is %s", daemonStatus)
|
|
default:
|
|
return fmt.Errorf("readiness check: unexpected daemon status %q", daemonStatus)
|
|
}
|
|
}
|
|
|
|
func checkStartup(resp *proto.StatusResponse) error {
|
|
fullStatus := resp.GetFullStatus()
|
|
if fullStatus == nil {
|
|
return fmt.Errorf("startup check: no full status available")
|
|
}
|
|
|
|
if !fullStatus.GetManagementState().GetConnected() {
|
|
return fmt.Errorf("startup check: management not connected")
|
|
}
|
|
|
|
if !fullStatus.GetSignalState().GetConnected() {
|
|
return fmt.Errorf("startup check: signal not connected")
|
|
}
|
|
|
|
var relayCount, relaysConnected int
|
|
for _, r := range fullStatus.GetRelays() {
|
|
uri := r.GetURI()
|
|
if !strings.HasPrefix(uri, "rel://") && !strings.HasPrefix(uri, "rels://") {
|
|
continue
|
|
}
|
|
relayCount++
|
|
if r.GetAvailable() {
|
|
relaysConnected++
|
|
}
|
|
}
|
|
|
|
if relayCount > 0 && relaysConnected == 0 {
|
|
return fmt.Errorf("startup check: no relay servers available (0/%d connected)", relayCount)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseInterfaceIP(interfaceIP string) string {
|
|
ip, _, err := net.ParseCIDR(interfaceIP)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%s\n", ip)
|
|
}
|