diff --git a/canonical_facts_cmd.go b/canonical_facts_cmd.go new file mode 100644 index 0000000..1b192bf --- /dev/null +++ b/canonical_facts_cmd.go @@ -0,0 +1,23 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/urfave/cli/v2" +) + +// canonicalFactAction tries to gather canonical facts about system, +// and it prints JSON with facts to stdout. +func canonicalFactAction(_ *cli.Context) error { + // NOTE: CLI context is not useful for anything + facts, err := GetCanonicalFacts() + if err != nil { + return cli.Exit(err, 1) + } + data, err := json.MarshalIndent(facts, "", " ") + if err != nil { + return err + } + fmt.Println(string(data)) + return nil +} diff --git a/conf.go b/conf.go index 6d10040..fbe5544 100644 --- a/conf.go +++ b/conf.go @@ -1,13 +1,5 @@ package main -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "os" - "path/filepath" -) - const ( cliLogLevel = "log-level" cliCertFile = "cert-file" @@ -22,49 +14,4 @@ type Conf struct { CADir string } -// Create a TLSConfig using the rhsm certificates -func (conf *Conf) CreateTLSClientConfig() (*tls.Config, error) { - var certData, keyData []byte - var err error - rootCAs := make([][]byte, 0) - - KeyDir := conf.KeyFile - CertDir := conf.CertFile - - certData, err = os.ReadFile(CertDir) - if err != nil { - return nil, fmt.Errorf("cannot read cert-file: %w", err) - } - - keyData, err = os.ReadFile(KeyDir) - if err != nil { - return nil, fmt.Errorf("cannot read key-file: %w", err) - } - CAFiles, err := os.ReadDir(conf.CADir) - if err != nil { - return nil, fmt.Errorf("cannot read ca files: %w", err) - } - for _, file := range CAFiles { - fPath := filepath.Join(conf.CADir, file.Name()) - data, err := os.ReadFile(fPath) - if err != nil { - return nil, fmt.Errorf("cannot read ca-file %s : %w", fPath, err) - } - rootCAs = append(rootCAs, data) - } - - tlsConfig := &tls.Config{} - cert, err := tls.X509KeyPair(certData, keyData) - if err != nil { - return nil, fmt.Errorf("cannot create key pair: %w", err) - } - tlsConfig.Certificates = []tls.Certificate{cert} - - // Create a pool with CAcerts from rhsm CA directory - pool := x509.NewCertPool() - for _, data := range rootCAs { - pool.AppendCertsFromPEM(data) - } - - return tlsConfig, nil -} +var config = Conf{} diff --git a/connect_cmd.go b/connect_cmd.go new file mode 100644 index 0000000..ee65cec --- /dev/null +++ b/connect_cmd.go @@ -0,0 +1,215 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/subpop/go-log" + "github.com/urfave/cli/v2" + "os" + "time" +) + +// ConnectResult is structure holding information about results +// of connect command. The result could be printed in machine-readable format. +type ConnectResult struct { + Hostname string `json:"hostname"` + HostnameError string `json:"hostname_error,omitempty"` + UID int `json:"uid"` + UIDError string `json:"uid_error,omitempty"` + RHSMConnected bool `json:"rhsm_connected"` + RHSMConnectError string `json:"rhsm_connect_error,omitempty"` + InsightsConnected bool `json:"insights_connected"` + InsightsError string `json:"insights_connect_error,omitempty"` + YggdrasilStarted bool `json:"yggdrasil_started"` + YggdrasilStartedError string `json:"yggdrasil_started_error,omitempty"` + format string +} + +// Error implement error interface for structure ConnectResult +func (connectResult ConnectResult) Error() string { + var result string + switch connectResult.format { + case "json": + data, err := json.MarshalIndent(connectResult, "", " ") + if err != nil { + return err.Error() + } + result = string(data) + case "": + break + default: + result = "error: unsupported document format: " + connectResult.format + } + return result +} + +// beforeConnectAction ensures that user has supplied a correct CLI options +// and there is no conflict between provided options +func beforeConnectAction(ctx *cli.Context) error { + // First check if machine-readable format is used + err := setupFormatOption(ctx) + if err != nil { + return err + } + + username := ctx.String("username") + password := ctx.String("password") + organization := ctx.String("organization") + activationKeys := ctx.StringSlice("activation-key") + + if len(activationKeys) > 0 { + if username != "" { + return fmt.Errorf("--username and --activation-key can not be used together") + } + if organization == "" { + return fmt.Errorf("--organization is required, when --activation-key is used") + } + } + + // When machine-readable format is used, then additional requirements have to be met + if uiSettings.isMachineReadable { + if username == "" || password == "" { + return fmt.Errorf("--username/--password or --organization/--activation-key are required when a machine-readable format is used") + } + } + + return checkForUnknownArgs(ctx) +} + +// connectAction tries to register system against Red Hat Subscription Management, +// gather the profile information that the system will configure +// connect system to Red Hat Insights, and it also tries to start rhcd service +func connectAction(ctx *cli.Context) error { + var connectResult ConnectResult + connectResult.format = ctx.String("format") + + uid := os.Getuid() + if uid != 0 { + errMsg := "non-root user cannot connect system" + exitCode := 1 + if uiSettings.isMachineReadable { + connectResult.UID = uid + connectResult.UIDError = errMsg + return cli.Exit(connectResult, exitCode) + } else { + return cli.Exit(fmt.Errorf("error: %s", errMsg), exitCode) + } + } + + hostname, err := os.Hostname() + if uiSettings.isMachineReadable { + connectResult.Hostname = hostname + } + if err != nil { + exitCode := 1 + if uiSettings.isMachineReadable { + connectResult.HostnameError = err.Error() + return cli.Exit(connectResult, exitCode) + } else { + return cli.Exit(err, exitCode) + } + } + + interactivePrintf("Connecting %v to %v.\nThis might take a few seconds.\n\n", hostname, Provider) + + var start time.Time + durations := make(map[string]time.Duration) + errorMessages := make(map[string]LogMessage) + /* 1. Register to RHSM, because we need to get consumer certificate. This blocks following action */ + start = time.Now() + var returnedMsg string + returnedMsg, err = registerRHSM(ctx) + if err != nil { + connectResult.RHSMConnected = false + errorMessages["rhsm"] = LogMessage{ + level: log.LevelError, + message: fmt.Errorf("cannot connect to Red Hat Subscription Management: %w", + err)} + if uiSettings.isMachineReadable { + connectResult.RHSMConnectError = errorMessages["rhsm"].message.Error() + } else { + fmt.Printf( + "%v Cannot connect to Red Hat Subscription Management\n", + uiSettings.iconError, + ) + } + } else { + connectResult.RHSMConnected = true + interactivePrintf("%v %v\n", uiSettings.iconOK, returnedMsg) + } + durations["rhsm"] = time.Since(start) + + /* 2. Register insights-client */ + if errors, exist := errorMessages["rhsm"]; exist { + if errors.level == log.LevelError { + interactivePrintf( + "%v Skipping connection to Red Hat Insights\n", + uiSettings.iconError, + ) + } + } else { + start = time.Now() + err = showProgress(" Connecting to Red Hat Insights...", registerInsights) + if err != nil { + connectResult.InsightsConnected = false + errorMessages["insights"] = LogMessage{ + level: log.LevelError, + message: fmt.Errorf("cannot connect to Red Hat Insights: %w", err)} + if uiSettings.isMachineReadable { + connectResult.InsightsError = errorMessages["insights"].message.Error() + } else { + fmt.Printf("%v Cannot connect to Red Hat Insights\n", uiSettings.iconError) + } + } else { + connectResult.InsightsConnected = true + interactivePrintf("%v Connected to Red Hat Insights\n", uiSettings.iconOK) + } + durations["insights"] = time.Since(start) + } + + /* 3. Start yggdrasil (rhcd) service */ + if rhsmErrMsg, exist := errorMessages["rhsm"]; exist && rhsmErrMsg.level == log.LevelError { + connectResult.YggdrasilStarted = false + interactivePrintf( + "%v Skipping activation of %v service\n", + uiSettings.iconError, + ServiceName, + ) + } else { + start = time.Now() + progressMessage := fmt.Sprintf(" Activating the %v service", ServiceName) + err = showProgress(progressMessage, activateService) + if err != nil { + connectResult.YggdrasilStarted = false + errorMessages[ServiceName] = LogMessage{ + level: log.LevelError, + message: fmt.Errorf("cannot activate %s service: %w", + ServiceName, err)} + if uiSettings.isMachineReadable { + connectResult.YggdrasilStartedError = errorMessages[ServiceName].message.Error() + } else { + fmt.Printf("%v Cannot activate the %v service\n", uiSettings.iconError, ServiceName) + } + } else { + connectResult.YggdrasilStarted = true + interactivePrintf("%v Activated the %v service\n", uiSettings.iconOK, ServiceName) + } + durations[ServiceName] = time.Since(start) + interactivePrintf("\nSuccessfully connected to Red Hat!\n") + } + + if !uiSettings.isMachineReadable { + /* 5. Show footer message */ + fmt.Printf("\nManage your connected systems: https://red.ht/connector\n") + + /* 6. Optionally display duration time of each sub-action */ + showTimeDuration(durations) + } + + err = showErrorMessages("connect", errorMessages) + if err != nil { + return err + } + + return cli.Exit(connectResult, 0) +} diff --git a/disconnect_cmd.go b/disconnect_cmd.go new file mode 100644 index 0000000..f9df4a4 --- /dev/null +++ b/disconnect_cmd.go @@ -0,0 +1,160 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/subpop/go-log" + "github.com/urfave/cli/v2" + "os" + "time" +) + +// DisconnectResult is structure holding information about result of +// disconnect command. The result could be printed in machine-readable format. +type DisconnectResult struct { + Hostname string `json:"hostname"` + HostnameError string `json:"hostname_error,omitempty"` + UID int `json:"uid"` + UIDError string `json:"uid_error,omitempty"` + RHSMDisconnected bool `json:"rhsm_disconnected"` + RHSMDisconnectedError string `json:"rhsm_disconnect_error,omitempty"` + InsightsDisconnected bool `json:"insights_disconnected"` + InsightsDisconnectedError string `json:"insights_disconnected_error,omitempty"` + YggdrasilStopped bool `json:"yggdrasil_stopped"` + YggdrasilStoppedError string `json:"yggdrasil_stopped_error,omitempty"` + format string +} + +// Error implement error interface for structure DisconnectResult +func (disconnectResult DisconnectResult) Error() string { + var result string + switch disconnectResult.format { + case "json": + data, err := json.MarshalIndent(disconnectResult, "", " ") + if err != nil { + return err.Error() + } + result = string(data) + case "": + break + default: + result = "error: unsupported document format: " + disconnectResult.format + } + return result +} + +// beforeDisconnectAction ensures the used has supplied a correct `--format` flag +func beforeDisconnectAction(ctx *cli.Context) error { + err := setupFormatOption(ctx) + if err != nil { + return err + } + + return checkForUnknownArgs(ctx) +} + +// disconnectAction tries to stop (yggdrasil) rhcd service, disconnect from Red Hat Insights, +// and finally it unregisters system from Red Hat Subscription Management +func disconnectAction(ctx *cli.Context) error { + var disconnectResult DisconnectResult + disconnectResult.format = ctx.String("format") + + uid := os.Getuid() + if uid != 0 { + errMsg := "non-root user cannot disconnect system" + exitCode := 1 + if uiSettings.isMachineReadable { + disconnectResult.UID = uid + disconnectResult.UIDError = errMsg + return cli.Exit(disconnectResult, exitCode) + } else { + return cli.Exit(fmt.Errorf("error: %s", errMsg), exitCode) + } + } + + hostname, err := os.Hostname() + if uiSettings.isMachineReadable { + disconnectResult.Hostname = hostname + } + if err != nil { + exitCode := 1 + if uiSettings.isMachineReadable { + disconnectResult.HostnameError = err.Error() + return cli.Exit(disconnectResult, exitCode) + } else { + return cli.Exit(err, exitCode) + } + } + + interactivePrintf("Disconnecting %v from %v.\nThis might take a few seconds.\n\n", hostname, Provider) + + var start time.Time + durations := make(map[string]time.Duration) + errorMessages := make(map[string]LogMessage) + + /* 1. Deactivate yggdrasil (rhcd) service */ + start = time.Now() + progressMessage := fmt.Sprintf(" Deactivating the %v service", ServiceName) + err = showProgress(progressMessage, deactivateService) + if err != nil { + errMsg := fmt.Sprintf("Cannot deactivate %s service: %v", ServiceName, err) + errorMessages[ServiceName] = LogMessage{ + level: log.LevelError, + message: fmt.Errorf("%v", errMsg)} + disconnectResult.YggdrasilStopped = false + disconnectResult.YggdrasilStoppedError = errMsg + interactivePrintf("%v %v\n", uiSettings.iconError, errMsg) + } else { + disconnectResult.YggdrasilStopped = true + interactivePrintf("%v Deactivated the %v service\n", uiSettings.iconOK, ServiceName) + } + durations[ServiceName] = time.Since(start) + + /* 2. Disconnect from Red Hat Insights */ + start = time.Now() + err = showProgress(" Disconnecting from Red Hat Insights...", unregisterInsights) + if err != nil { + errMsg := fmt.Sprintf("Cannot disconnect from Red Hat Insights: %v", err) + errorMessages["insights"] = LogMessage{ + level: log.LevelError, + message: fmt.Errorf("%v", errMsg)} + disconnectResult.InsightsDisconnected = false + disconnectResult.InsightsDisconnectedError = errMsg + interactivePrintf("%v %v\n", uiSettings.iconError, errMsg) + } else { + disconnectResult.InsightsDisconnected = true + interactivePrintf("%v Disconnected from Red Hat Insights\n", uiSettings.iconOK) + } + durations["insights"] = time.Since(start) + + /* 3. Unregister system from Red Hat Subscription Management */ + err = showProgress( + " Disconnecting from Red Hat Subscription Management...", unregister, + ) + if err != nil { + errMsg := fmt.Sprintf("Cannot disconnect from Red Hat Subscription Management: %v", err) + errorMessages["rhsm"] = LogMessage{ + level: log.LevelError, + message: fmt.Errorf("%v", errMsg)} + + disconnectResult.RHSMDisconnected = false + disconnectResult.RHSMDisconnectedError = errMsg + interactivePrintf("%v %v\n", uiSettings.iconError, errMsg) + } else { + disconnectResult.RHSMDisconnected = true + interactivePrintf("%v Disconnected from Red Hat Subscription Management\n", uiSettings.iconOK) + } + durations["rhsm"] = time.Since(start) + + if !uiSettings.isMachineReadable { + fmt.Printf("\nManage your connected systems: https://red.ht/connector\n") + showTimeDuration(durations) + + err = showErrorMessages("disconnect", errorMessages) + if err != nil { + return err + } + } + + return cli.Exit(disconnectResult, 0) +} diff --git a/go.mod b/go.mod index 2a2984f..a34334e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/godbus/dbus/v5 v5.1.0 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 - github.com/subpop/go-ini v0.1.5 github.com/subpop/go-log v0.1.2 github.com/urfave/cli/v2 v2.27.5 golang.org/x/sys v0.26.0 diff --git a/go.sum b/go.sum index bb663dc..304e242 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,6 @@ github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/subpop/go-ini v0.1.5 h1:08hu8slz5c4KBXJWYOycyuDQ0E7xHr2vV2rH9x79FrY= -github.com/subpop/go-ini v0.1.5/go.mod h1:mM5HCw/1+q92kIf4Btd7Oorioa8rAXb4NSBT2ldZNiU= github.com/subpop/go-log v0.1.2 h1:NgbZR6frmeDtC+96d+UxOkt4X/JxO626fokwL56Dff0= github.com/subpop/go-log v0.1.2/go.mod h1:uAEovif98swmWm/8qYGrzGFahUIRQ/KwlBUpJuoOdds= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= diff --git a/interactive.go b/interactive.go new file mode 100644 index 0000000..47ae1f9 --- /dev/null +++ b/interactive.go @@ -0,0 +1,119 @@ +package main + +import ( + "fmt" + "github.com/briandowns/spinner" + "github.com/subpop/go-log" + "github.com/urfave/cli/v2" + "os" + "text/tabwriter" + "time" +) + +const ( + colorGreen = "\u001B[32m" + colorYellow = "\u001B[33m" + colorRed = "\u001B[31m" + colorReset = "\u001B[0m" +) + +// userInterfaceSettings manages standard output preference. +// It tracks colors, icons and machine-readable output (e.g. json). +// +// It is instantiated via uiSettings by calling configureUISettings. +type userInterfaceSettings struct { + // isMachineReadable describes the machine-readable mode (e.g., `--format json`) + isMachineReadable bool + // isRich describes the ability to display colors and animations + isRich bool + iconOK string + iconInfo string + iconError string +} + +// uiSettings is an instance that keeps actual data of output preference. +// +// It is managed by calling the configureUISettings method. +var uiSettings = userInterfaceSettings{} + +// configureUISettings is called by the CLI library when it loads up. +// It sets up the uiSettings object. +func configureUISettings(ctx *cli.Context) { + if ctx.Bool("no-color") { + uiSettings = userInterfaceSettings{ + isRich: false, + isMachineReadable: false, + iconOK: "โœ“", + iconInfo: "ยท", + iconError: "๐„‚", + } + } else { + uiSettings = userInterfaceSettings{ + isRich: true, + isMachineReadable: false, + iconOK: colorGreen + "โ—" + colorReset, + iconInfo: colorYellow + "โ—" + colorReset, + iconError: colorRed + "โ—" + colorReset, + } + } +} + +// showProgress calls function and, when it is possible display spinner with +// some progress message. +func showProgress( + progressMessage string, + function func() error, +) error { + var s *spinner.Spinner + if uiSettings.isRich { + s = spinner.New(spinner.CharSets[9], 100*time.Millisecond) + s.Suffix = progressMessage + s.Start() + // Stop spinner after running function + defer func() { s.Stop() }() + } + return function() +} + +// showTimeDuration shows table with duration of each sub-action +func showTimeDuration(durations map[string]time.Duration) { + if log.CurrentLevel() >= log.LevelDebug { + fmt.Println() + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "STEP\tDURATION\t") + for step, duration := range durations { + _, _ = fmt.Fprintf(w, "%v\t%v\t\n", step, duration.Truncate(time.Millisecond)) + } + _ = w.Flush() + } +} + +// showErrorMessages shows table with all error messages gathered during action +func showErrorMessages(action string, errorMessages map[string]LogMessage) error { + if hasPriorityErrors(errorMessages, log.CurrentLevel()) { + if !uiSettings.isMachineReadable { + fmt.Println() + fmt.Printf("The following errors were encountered during %s:\n\n", action) + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "TYPE\tSTEP\tERROR\t") + for step, logMsg := range errorMessages { + if logMsg.level <= log.CurrentLevel() { + _, _ = fmt.Fprintf(w, "%v\t%v\t%v\n", logMsg.level, step, logMsg.message) + } + } + _ = w.Flush() + if hasPriorityErrors(errorMessages, log.LevelError) { + return cli.Exit("", 1) + } + } + } + return nil +} + +// interactivePrintf is method for printing human-readable output. It suppresses output, when +// machine-readable format is used. +func interactivePrintf(format string, a ...interface{}) { + if !uiSettings.isMachineReadable { + fmt.Printf(format, a...) + } +} diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..53be989 --- /dev/null +++ b/logging.go @@ -0,0 +1,8 @@ +package main + +import "github.com/subpop/go-log" + +type LogMessage struct { + level log.Level + message error +} diff --git a/main.go b/main.go index 67804e1..18fd2e2 100644 --- a/main.go +++ b/main.go @@ -1,748 +1,13 @@ package main import ( - "bufio" - "encoding/json" "fmt" - "os" - "strings" - "text/tabwriter" - "time" - "github.com/subpop/go-log" - "golang.org/x/term" - - "github.com/briandowns/spinner" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" + "os" ) -type LogMessage struct { - level log.Level - message error -} - -const ( - colorGreen = "\u001B[32m" - colorYellow = "\u001B[33m" - colorRed = "\u001B[31m" - colorReset = "\u001B[0m" -) - -// userInterfaceSettings manages standard output preference. -// It tracks colors, icons and machine-readable output (e.g. json). -// -// It is instantiated via uiSettings by calling configureUISettings. -type userInterfaceSettings struct { - // isMachineReadable describes the machine-readable mode (e.g., `--format json`) - isMachineReadable bool - // isRich describes the ability to display colors and animations - isRich bool - iconOK string - iconInfo string - iconError string -} - -// uiSettings is an instance that keeps actual data of output preference. -// -// It is managed by calling the configureUISettings method. -var uiSettings = userInterfaceSettings{} - -// configureUISettings is called by the CLI library when it loads up. -// It sets up the uiSettings object. -func configureUISettings(ctx *cli.Context) { - if ctx.Bool("no-color") { - uiSettings = userInterfaceSettings{ - isRich: false, - isMachineReadable: false, - iconOK: "โœ“", - iconInfo: "ยท", - iconError: "๐„‚", - } - } else { - uiSettings = userInterfaceSettings{ - isRich: true, - isMachineReadable: false, - iconOK: colorGreen + "โ—" + colorReset, - iconInfo: colorYellow + "โ—" + colorReset, - iconError: colorRed + "โ—" + colorReset, - } - } -} - -// showProgress calls function and, when it is possible display spinner with -// some progress message. -func showProgress( - progressMessage string, - function func() error, -) error { - var s *spinner.Spinner - if uiSettings.isRich { - s = spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Suffix = progressMessage - s.Start() - // Stop spinner after running function - defer func() { s.Stop() }() - } - return function() -} - -// showTimeDuration shows table with duration of each sub-action -func showTimeDuration(durations map[string]time.Duration) { - if log.CurrentLevel() >= log.LevelDebug { - fmt.Println() - w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - _, _ = fmt.Fprintln(w, "STEP\tDURATION\t") - for step, duration := range durations { - _, _ = fmt.Fprintf(w, "%v\t%v\t\n", step, duration.Truncate(time.Millisecond)) - } - _ = w.Flush() - } -} - -// showErrorMessages shows table with all error messages gathered during action -func showErrorMessages(action string, errorMessages map[string]LogMessage) error { - if hasPriorityErrors(errorMessages, log.CurrentLevel()) { - if !uiSettings.isMachineReadable { - fmt.Println() - fmt.Printf("The following errors were encountered during %s:\n\n", action) - w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - _, _ = fmt.Fprintln(w, "TYPE\tSTEP\tERROR\t") - for step, logMsg := range errorMessages { - if logMsg.level <= log.CurrentLevel() { - _, _ = fmt.Fprintf(w, "%v\t%v\t%v\n", logMsg.level, step, logMsg.message) - } - } - _ = w.Flush() - if hasPriorityErrors(errorMessages, log.LevelError) { - return cli.Exit("", 1) - } - } - } - return nil -} - -// registerRHSM tries to register system against Red Hat Subscription Management server (candlepin server) -func registerRHSM(ctx *cli.Context) (string, error) { - uuid, err := getConsumerUUID() - if err != nil { - return "Unable to get consumer UUID", cli.Exit(err, 1) - } - var successMsg string - - if uuid == "" { - username := ctx.String("username") - password := ctx.String("password") - organization := ctx.String("organization") - activationKeys := ctx.StringSlice("activation-key") - - if len(activationKeys) == 0 { - if username == "" { - password = "" - scanner := bufio.NewScanner(os.Stdin) - fmt.Print("Username: ") - _ = scanner.Scan() - username = strings.TrimSpace(scanner.Text()) - } - if password == "" { - fmt.Print("Password: ") - data, err := term.ReadPassword(int(os.Stdin.Fd())) - if err != nil { - return "Unable to read password", cli.Exit(err, 1) - } - password = string(data) - fmt.Printf("\n\n") - } - } - - var s *spinner.Spinner - if uiSettings.isRich { - s = spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Suffix = " Connecting to Red Hat Subscription Management..." - s.Start() - defer s.Stop() - } - - var err error - if len(activationKeys) > 0 { - err = registerActivationKey( - organization, - ctx.StringSlice("activation-key"), - ctx.String("server")) - } else { - var orgs []string - if organization != "" { - _, err = registerUsernamePassword(username, password, organization, ctx.String("server")) - } else { - orgs, err = registerUsernamePassword(username, password, "", ctx.String("server")) - /* When organization was not specified using CLI option --organization, and it is - required, because user is member of more than one organization, then ask for - the organization. */ - if len(orgs) > 0 { - if uiSettings.isMachineReadable { - return "Unable to register system to RHSM", cli.Exit("no organization specified", 1) - } - // Stop spinner to be able to display message and ask for organization - if uiSettings.isRich { - s.Stop() - } - - // Ask for organization and display hint with list of organizations - scanner := bufio.NewScanner(os.Stdin) - fmt.Println("Available Organizations:") - writer := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0) - for i, org := range orgs { - _, _ = fmt.Fprintf(writer, "%v\t", org) - if (i+1)%4 == 0 { - _, _ = fmt.Fprint(writer, "\n") - } - } - _ = writer.Flush() - fmt.Print("\nOrganization: ") - _ = scanner.Scan() - organization = strings.TrimSpace(scanner.Text()) - fmt.Printf("\n") - - // Start spinner again - if uiSettings.isRich { - s.Start() - } - - // Try to register once again with given organization - _, err = registerUsernamePassword(username, password, organization, ctx.String("server")) - } - } - } - if err != nil { - return "Unable to register system to RHSM", cli.Exit(err, 1) - } - successMsg = "Connected to Red Hat Subscription Management" - } else { - successMsg = "This system is already connected to Red Hat Subscription Management" - } - return successMsg, nil -} - -// beforeConnectAction ensures that user has supplied a correct CLI options -// and there is no conflict between provided options -func beforeConnectAction(ctx *cli.Context) error { - // First check if machine-readable format is used - err := setupFormatOption(ctx) - if err != nil { - return err - } - - username := ctx.String("username") - password := ctx.String("password") - organization := ctx.String("organization") - activationKeys := ctx.StringSlice("activation-key") - - if len(activationKeys) > 0 { - if username != "" { - return fmt.Errorf("--username and --activation-key can not be used together") - } - if organization == "" { - return fmt.Errorf("--organization is required, when --activation-key is used") - } - } - - // When machine-readable format is used, then additional requirements have to be met - if uiSettings.isMachineReadable { - if username == "" || password == "" { - return fmt.Errorf("--username/--password or --organization/--activation-key are required when a machine-readable format is used") - } - } - - return checkForUnknownArgs(ctx) -} - -// connectAction tries to register system against Red Hat Subscription Management, -// gather the profile information that the system will configure -// connect system to Red Hat Insights, and it also tries to start rhcd service -func connectAction(ctx *cli.Context) error { - var connectResult ConnectResult - connectResult.format = ctx.String("format") - - uid := os.Getuid() - if uid != 0 { - errMsg := "non-root user cannot connect system" - exitCode := 1 - if uiSettings.isMachineReadable { - connectResult.UID = uid - connectResult.UIDError = errMsg - return cli.Exit(connectResult, exitCode) - } else { - return cli.Exit(fmt.Errorf("error: %s", errMsg), exitCode) - } - } - - hostname, err := os.Hostname() - if uiSettings.isMachineReadable { - connectResult.Hostname = hostname - } - if err != nil { - exitCode := 1 - if uiSettings.isMachineReadable { - connectResult.HostnameError = err.Error() - return cli.Exit(connectResult, exitCode) - } else { - return cli.Exit(err, exitCode) - } - } - - interactivePrintf("Connecting %v to %v.\nThis might take a few seconds.\n\n", hostname, Provider) - - var start time.Time - durations := make(map[string]time.Duration) - errorMessages := make(map[string]LogMessage) - /* 1. Register to RHSM, because we need to get consumer certificate. This blocks following action */ - start = time.Now() - var returnedMsg string - returnedMsg, err = registerRHSM(ctx) - if err != nil { - connectResult.RHSMConnected = false - errorMessages["rhsm"] = LogMessage{ - level: log.LevelError, - message: fmt.Errorf("cannot connect to Red Hat Subscription Management: %w", - err)} - if uiSettings.isMachineReadable { - connectResult.RHSMConnectError = errorMessages["rhsm"].message.Error() - } else { - fmt.Printf( - "%v Cannot connect to Red Hat Subscription Management\n", - uiSettings.iconError, - ) - } - } else { - connectResult.RHSMConnected = true - interactivePrintf("%v %v\n", uiSettings.iconOK, returnedMsg) - } - durations["rhsm"] = time.Since(start) - - /* 2. Register insights-client */ - if errors, exist := errorMessages["rhsm"]; exist { - if errors.level == log.LevelError { - interactivePrintf( - "%v Skipping connection to Red Hat Insights\n", - uiSettings.iconError, - ) - } - } else { - start = time.Now() - err = showProgress(" Connecting to Red Hat Insights...", registerInsights) - if err != nil { - connectResult.InsightsConnected = false - errorMessages["insights"] = LogMessage{ - level: log.LevelError, - message: fmt.Errorf("cannot connect to Red Hat Insights: %w", err)} - if uiSettings.isMachineReadable { - connectResult.InsightsError = errorMessages["insights"].message.Error() - } else { - fmt.Printf("%v Cannot connect to Red Hat Insights\n", uiSettings.iconError) - } - } else { - connectResult.InsightsConnected = true - interactivePrintf("%v Connected to Red Hat Insights\n", uiSettings.iconOK) - } - durations["insights"] = time.Since(start) - } - - /* 3. Start yggdrasil (rhcd) service */ - if rhsmErrMsg, exist := errorMessages["rhsm"]; exist && rhsmErrMsg.level == log.LevelError { - connectResult.YggdrasilStarted = false - interactivePrintf( - "%v Skipping activation of %v service\n", - uiSettings.iconError, - ServiceName, - ) - } else { - start = time.Now() - progressMessage := fmt.Sprintf(" Activating the %v service", ServiceName) - err = showProgress(progressMessage, activateService) - if err != nil { - connectResult.YggdrasilStarted = false - errorMessages[ServiceName] = LogMessage{ - level: log.LevelError, - message: fmt.Errorf("cannot activate %s service: %w", - ServiceName, err)} - if uiSettings.isMachineReadable { - connectResult.YggdrasilStartedError = errorMessages[ServiceName].message.Error() - } else { - fmt.Printf("%v Cannot activate the %v service\n", uiSettings.iconError, ServiceName) - } - } else { - connectResult.YggdrasilStarted = true - interactivePrintf("%v Activated the %v service\n", uiSettings.iconOK, ServiceName) - } - durations[ServiceName] = time.Since(start) - interactivePrintf("\nSuccessfully connected to Red Hat!\n") - } - - if !uiSettings.isMachineReadable { - /* 5. Show footer message */ - fmt.Printf("\nManage your connected systems: https://red.ht/connector\n") - - /* 6. Optionally display duration time of each sub-action */ - showTimeDuration(durations) - } - - err = showErrorMessages("connect", errorMessages) - if err != nil { - return err - } - - return cli.Exit(connectResult, 0) -} - -// setupFormatOption ensures the user has supplied a correct `--format` flag -// and set values in uiSettings, when JSON format is used. -func setupFormatOption(ctx *cli.Context) error { - // This is run after the `app.Before()` has been run, - // the uiSettings is already set up for us to modify. - format := ctx.String("format") - switch format { - case "": - return nil - case "json": - uiSettings.isMachineReadable = true - uiSettings.isRich = false - return nil - default: - err := fmt.Errorf( - "unsupported format: %s (supported formats: %s)", - format, - `"json"`, - ) - return cli.Exit(err, 1) - } -} - -// DisconnectResult is structure holding information about result of -// disconnect command. The result could be printed in machine-readable format. -type DisconnectResult struct { - Hostname string `json:"hostname"` - HostnameError string `json:"hostname_error,omitempty"` - UID int `json:"uid"` - UIDError string `json:"uid_error,omitempty"` - RHSMDisconnected bool `json:"rhsm_disconnected"` - RHSMDisconnectedError string `json:"rhsm_disconnect_error,omitempty"` - InsightsDisconnected bool `json:"insights_disconnected"` - InsightsDisconnectedError string `json:"insights_disconnected_error,omitempty"` - YggdrasilStopped bool `json:"yggdrasil_stopped"` - YggdrasilStoppedError string `json:"yggdrasil_stopped_error,omitempty"` - format string -} - -// Error implement error interface for structure DisconnectResult -func (disconnectResult DisconnectResult) Error() string { - var result string - switch disconnectResult.format { - case "json": - data, err := json.MarshalIndent(disconnectResult, "", " ") - if err != nil { - return err.Error() - } - result = string(data) - case "": - break - default: - result = "error: unsupported document format: " + disconnectResult.format - } - return result -} - -// ConnectResult is structure holding information about results -// of connect command. The result could be printed in machine-readable format. -type ConnectResult struct { - Hostname string `json:"hostname"` - HostnameError string `json:"hostname_error,omitempty"` - UID int `json:"uid"` - UIDError string `json:"uid_error,omitempty"` - RHSMConnected bool `json:"rhsm_connected"` - RHSMConnectError string `json:"rhsm_connect_error,omitempty"` - InsightsConnected bool `json:"insights_connected"` - InsightsError string `json:"insights_connect_error,omitempty"` - YggdrasilStarted bool `json:"yggdrasil_started"` - YggdrasilStartedError string `json:"yggdrasil_started_error,omitempty"` - format string -} - -// Error implement error interface for structure ConnectResult -func (connectResult ConnectResult) Error() string { - var result string - switch connectResult.format { - case "json": - data, err := json.MarshalIndent(connectResult, "", " ") - if err != nil { - return err.Error() - } - result = string(data) - case "": - break - default: - result = "error: unsupported document format: " + connectResult.format - } - return result -} - -// beforeDisconnectAction ensures the used has supplied a correct `--format` flag -func beforeDisconnectAction(ctx *cli.Context) error { - err := setupFormatOption(ctx) - if err != nil { - return err - } - - return checkForUnknownArgs(ctx) -} - -// interactivePrintf is method for printing human-readable output. It suppresses output, when -// machine-readable format is used. -func interactivePrintf(format string, a ...interface{}) { - if !uiSettings.isMachineReadable { - fmt.Printf(format, a...) - } -} - -// disconnectAction tries to stop (yggdrasil) rhcd service, disconnect from Red Hat Insights, -// and finally it unregisters system from Red Hat Subscription Management -func disconnectAction(ctx *cli.Context) error { - var disconnectResult DisconnectResult - disconnectResult.format = ctx.String("format") - - uid := os.Getuid() - if uid != 0 { - errMsg := "non-root user cannot disconnect system" - exitCode := 1 - if uiSettings.isMachineReadable { - disconnectResult.UID = uid - disconnectResult.UIDError = errMsg - return cli.Exit(disconnectResult, exitCode) - } else { - return cli.Exit(fmt.Errorf("error: %s", errMsg), exitCode) - } - } - - hostname, err := os.Hostname() - if uiSettings.isMachineReadable { - disconnectResult.Hostname = hostname - } - if err != nil { - exitCode := 1 - if uiSettings.isMachineReadable { - disconnectResult.HostnameError = err.Error() - return cli.Exit(disconnectResult, exitCode) - } else { - return cli.Exit(err, exitCode) - } - } - - interactivePrintf("Disconnecting %v from %v.\nThis might take a few seconds.\n\n", hostname, Provider) - - var start time.Time - durations := make(map[string]time.Duration) - errorMessages := make(map[string]LogMessage) - - /* 1. Deactivate yggdrasil (rhcd) service */ - start = time.Now() - progressMessage := fmt.Sprintf(" Deactivating the %v service", ServiceName) - err = showProgress(progressMessage, deactivateService) - if err != nil { - errMsg := fmt.Sprintf("Cannot deactivate %s service: %v", ServiceName, err) - errorMessages[ServiceName] = LogMessage{ - level: log.LevelError, - message: fmt.Errorf("%v", errMsg)} - disconnectResult.YggdrasilStopped = false - disconnectResult.YggdrasilStoppedError = errMsg - interactivePrintf("%v %v\n", uiSettings.iconError, errMsg) - } else { - disconnectResult.YggdrasilStopped = true - interactivePrintf("%v Deactivated the %v service\n", uiSettings.iconOK, ServiceName) - } - durations[ServiceName] = time.Since(start) - - /* 2. Disconnect from Red Hat Insights */ - start = time.Now() - err = showProgress(" Disconnecting from Red Hat Insights...", unregisterInsights) - if err != nil { - errMsg := fmt.Sprintf("Cannot disconnect from Red Hat Insights: %v", err) - errorMessages["insights"] = LogMessage{ - level: log.LevelError, - message: fmt.Errorf("%v", errMsg)} - disconnectResult.InsightsDisconnected = false - disconnectResult.InsightsDisconnectedError = errMsg - interactivePrintf("%v %v\n", uiSettings.iconError, errMsg) - } else { - disconnectResult.InsightsDisconnected = true - interactivePrintf("%v Disconnected from Red Hat Insights\n", uiSettings.iconOK) - } - durations["insights"] = time.Since(start) - - /* 3. Unregister system from Red Hat Subscription Management */ - err = showProgress( - " Disconnecting from Red Hat Subscription Management...", unregister, - ) - if err != nil { - errMsg := fmt.Sprintf("Cannot disconnect from Red Hat Subscription Management: %v", err) - errorMessages["rhsm"] = LogMessage{ - level: log.LevelError, - message: fmt.Errorf("%v", errMsg)} - - disconnectResult.RHSMDisconnected = false - disconnectResult.RHSMDisconnectedError = errMsg - interactivePrintf("%v %v\n", uiSettings.iconError, errMsg) - } else { - disconnectResult.RHSMDisconnected = true - interactivePrintf("%v Disconnected from Red Hat Subscription Management\n", uiSettings.iconOK) - } - durations["rhsm"] = time.Since(start) - - if !uiSettings.isMachineReadable { - fmt.Printf("\nManage your connected systems: https://red.ht/connector\n") - showTimeDuration(durations) - - err = showErrorMessages("disconnect", errorMessages) - if err != nil { - return err - } - } - - return cli.Exit(disconnectResult, 0) -} - -// canonicalFactAction tries to gather canonical facts about system, -// and it prints JSON with facts to stdout. -func canonicalFactAction(_ *cli.Context) error { - // NOTE: CLI context is not useful for anything - facts, err := GetCanonicalFacts() - if err != nil { - return cli.Exit(err, 1) - } - data, err := json.MarshalIndent(facts, "", " ") - if err != nil { - return err - } - fmt.Println(string(data)) - return nil -} - -// SystemStatus is structure holding information about system status -// When more file format is supported, then add more tags for fields -// like xml:"hostname" -type SystemStatus struct { - SystemHostname string `json:"hostname"` - HostnameError string `json:"hostname_error,omitempty"` - RHSMConnected bool `json:"rhsm_connected"` - RHSMError string `json:"rhsm_error,omitempty"` - InsightsConnected bool `json:"insights_connected"` - InsightsError string `json:"insights_error,omitempty"` - YggdrasilRunning bool `json:"yggdrasil_running"` - YggdrasilError string `json:"yggdrasil_error,omitempty"` - returnCode int -} - -// printJSONStatus tries to print the system status as JSON to stdout. -// When marshaling of systemStatus fails, then error is returned -func printJSONStatus(systemStatus *SystemStatus) error { - data, err := json.MarshalIndent(systemStatus, "", " ") - if err != nil { - return err - } - fmt.Println(string(data)) - return nil -} - -// beforeStatusAction ensures the user has supplied a correct `--format` flag. -func beforeStatusAction(ctx *cli.Context) error { - err := setupFormatOption(ctx) - if err != nil { - return err - } - - return checkForUnknownArgs(ctx) -} - -// statusAction tries to print status of system. It means that it gives -// answer on following questions: -// 1. Is system registered to Red Hat Subscription Management? -// 2. Is system connected to Red Hat Insights? -// 3. Is yggdrasil.service (rhcd.service) running? -// Status can be printed as human-readable text or machine-readable JSON document. -// Format is influenced by --format json CLI option stored in CLI context -func statusAction(ctx *cli.Context) (err error) { - var systemStatus SystemStatus - var machineReadablePrintFunc func(systemStatus *SystemStatus) error - - format := ctx.String("format") - switch format { - case "json": - machineReadablePrintFunc = printJSONStatus - default: - break - } - - // When printing of status is requested, then print machine-readable file format - // at the end of this function - if uiSettings.isMachineReadable { - defer func(systemStatus *SystemStatus) { - err = machineReadablePrintFunc(systemStatus) - // When it was not possible to print status to machine-readable format, then - // change returned error to CLI exit error to be able to set exit code to - // a non-zero value - if err != nil { - err = cli.Exit( - fmt.Errorf("unable to print status as %s document: %s", format, err.Error()), - 1) - } - // When any of status is not correct, then return 1 exit code - if systemStatus.returnCode != 0 { - err = cli.Exit("", 1) - } - }(&systemStatus) - } - - hostname, err := os.Hostname() - if err != nil { - if uiSettings.isMachineReadable { - systemStatus.HostnameError = err.Error() - } else { - return cli.Exit(err, 1) - } - } - - if uiSettings.isMachineReadable { - systemStatus.SystemHostname = hostname - } else { - fmt.Printf("Connection status for %v:\n\n", hostname) - } - - /* 1. Get Status of RHSM */ - err = rhsmStatus(&systemStatus) - if err != nil { - return cli.Exit(err, 1) - } - - /* 2. Get status of insights-client */ - insightStatus(&systemStatus) - - /* 3. Get status of yggdrasil (rhcd) service */ - err = serviceStatus(&systemStatus) - if err != nil { - return cli.Exit(err, 1) - } - - if !uiSettings.isMachineReadable { - fmt.Printf("\nManage your connected systems: https://red.ht/connector\n") - } - - // At the end check if all statuses are correct. - // If not, return 1 exit code without any message. - if systemStatus.returnCode != 0 { - return cli.Exit("", 1) - } - - return nil -} - // mainAction is triggered in the case, when no sub-command is specified func mainAction(c *cli.Context) error { type GenerationFunc func() (string, error) @@ -806,8 +71,6 @@ func beforeAction(c *cli.Context) error { return nil } -var config = Conf{} - func main() { app := cli.NewApp() app.Name = ShortName diff --git a/register.go b/rhsm.go similarity index 74% rename from register.go rename to rhsm.go index 48767ae..b0024e2 100644 --- a/register.go +++ b/rhsm.go @@ -1,11 +1,18 @@ package main import ( + "bufio" "encoding/json" "fmt" + "github.com/briandowns/spinner" + "github.com/urfave/cli/v2" + "golang.org/x/term" "io" "net/url" "os" + "strings" + "text/tabwriter" + "time" "github.com/godbus/dbus/v5" ) @@ -387,20 +394,103 @@ func configureRHSM(serverURL string) error { return nil } -func getRHSMConfigOption(name string, val interface{}) error { - conn, err := dbus.SystemBus() +// registerRHSM tries to register system against Red Hat Subscription Management server (candlepin server) +func registerRHSM(ctx *cli.Context) (string, error) { + uuid, err := getConsumerUUID() if err != nil { - return fmt.Errorf("cannot connect to system D-Bus: %w", err) - } - locale := getLocale() - obj := conn.Object("com.redhat.RHSM1", "/com/redhat/RHSM1/Config") - if err := obj.Call( - "com.redhat.RHSM1.Config.Get", - dbus.Flags(0), - name, - locale).Store(val); err != nil { - return unpackRHSMError(err) + return "Unable to get consumer UUID", cli.Exit(err, 1) } + var successMsg string - return nil + if uuid == "" { + username := ctx.String("username") + password := ctx.String("password") + organization := ctx.String("organization") + activationKeys := ctx.StringSlice("activation-key") + + if len(activationKeys) == 0 { + if username == "" { + password = "" + scanner := bufio.NewScanner(os.Stdin) + fmt.Print("Username: ") + _ = scanner.Scan() + username = strings.TrimSpace(scanner.Text()) + } + if password == "" { + fmt.Print("Password: ") + data, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "Unable to read password", cli.Exit(err, 1) + } + password = string(data) + fmt.Printf("\n\n") + } + } + + var s *spinner.Spinner + if uiSettings.isRich { + s = spinner.New(spinner.CharSets[9], 100*time.Millisecond) + s.Suffix = " Connecting to Red Hat Subscription Management..." + s.Start() + defer s.Stop() + } + + var err error + if len(activationKeys) > 0 { + err = registerActivationKey( + organization, + ctx.StringSlice("activation-key"), + ctx.String("server")) + } else { + var orgs []string + if organization != "" { + _, err = registerUsernamePassword(username, password, organization, ctx.String("server")) + } else { + orgs, err = registerUsernamePassword(username, password, "", ctx.String("server")) + /* When organization was not specified using CLI option --organization, and it is + required, because user is member of more than one organization, then ask for + the organization. */ + if len(orgs) > 0 { + if uiSettings.isMachineReadable { + return "Unable to register system to RHSM", cli.Exit("no organization specified", 1) + } + // Stop spinner to be able to display message and ask for organization + if uiSettings.isRich { + s.Stop() + } + + // Ask for organization and display hint with list of organizations + scanner := bufio.NewScanner(os.Stdin) + fmt.Println("Available Organizations:") + writer := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0) + for i, org := range orgs { + _, _ = fmt.Fprintf(writer, "%v\t", org) + if (i+1)%4 == 0 { + _, _ = fmt.Fprint(writer, "\n") + } + } + _ = writer.Flush() + fmt.Print("\nOrganization: ") + _ = scanner.Scan() + organization = strings.TrimSpace(scanner.Text()) + fmt.Printf("\n") + + // Start spinner again + if uiSettings.isRich { + s.Start() + } + + // Try to register once again with given organization + _, err = registerUsernamePassword(username, password, organization, ctx.String("server")) + } + } + } + if err != nil { + return "Unable to register system to RHSM", cli.Exit(err, 1) + } + successMsg = "Connected to Red Hat Subscription Management" + } else { + successMsg = "This system is already connected to Red Hat Subscription Management" + } + return successMsg, nil } diff --git a/status.go b/status.go deleted file mode 100644 index 8693c83..0000000 --- a/status.go +++ /dev/null @@ -1,108 +0,0 @@ -package main - -import ( - "context" - "fmt" - "time" - - "github.com/briandowns/spinner" - systemd "github.com/coreos/go-systemd/v22/dbus" -) - -// rhsmStatus tries to print status provided by RHSM D-Bus API. If we provide -// output in machine-readable format, then we only set files in SystemStatus -// structure and content of this structure will be printed later -func rhsmStatus(systemStatus *SystemStatus) error { - - uuid, err := getConsumerUUID() - if err != nil { - return fmt.Errorf("unable to get consumer UUID: %s", err) - } - if uuid == "" { - systemStatus.returnCode += 1 - if uiSettings.isMachineReadable { - systemStatus.RHSMConnected = false - } else { - fmt.Printf("%v Not connected to Red Hat Subscription Management\n", uiSettings.iconInfo) - } - } else { - if uiSettings.isMachineReadable { - systemStatus.RHSMConnected = true - } else { - fmt.Printf("%v Connected to Red Hat Subscription Management\n", uiSettings.iconOK) - } - } - return nil -} - -// insightStatus tries to print status of insights client -func insightStatus(systemStatus *SystemStatus) { - var s *spinner.Spinner - if uiSettings.isRich { - s = spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Suffix = " Checking Red Hat Insights..." - s.Start() - } - isRegistered, err := insightsIsRegistered() - if uiSettings.isRich { - s.Stop() - } - if isRegistered { - if uiSettings.isMachineReadable { - systemStatus.InsightsConnected = true - } else { - fmt.Print(uiSettings.iconOK + " Connected to Red Hat Insights\n") - } - } else { - systemStatus.returnCode += 1 - if err == nil { - if uiSettings.isMachineReadable { - systemStatus.InsightsConnected = false - } else { - fmt.Print(uiSettings.iconInfo + " Not connected to Red Hat Insights\n") - } - } else { - if uiSettings.isMachineReadable { - systemStatus.InsightsConnected = false - systemStatus.InsightsError = err.Error() - } else { - fmt.Printf(uiSettings.iconError+" Cannot detect Red Hat Insights status: %v\n", err) - } - } - } -} - -// serviceStatus tries to print status of yggdrasil.service or rhcd.service -func serviceStatus(systemStatus *SystemStatus) error { - ctx := context.Background() - conn, err := systemd.NewSystemConnectionContext(ctx) - if err != nil { - systemStatus.YggdrasilRunning = false - systemStatus.YggdrasilError = err.Error() - return fmt.Errorf("unable to connect to systemd: %s", err) - } - defer conn.Close() - unitName := ServiceName + ".service" - properties, err := conn.GetUnitPropertiesContext(ctx, unitName) - if err != nil { - systemStatus.YggdrasilRunning = false - systemStatus.YggdrasilError = err.Error() - return fmt.Errorf("unable to get properties of %s: %s", unitName, err) - } - activeState := properties["ActiveState"] - if activeState.(string) == "active" { - if uiSettings.isMachineReadable { - systemStatus.YggdrasilRunning = true - } else { - fmt.Printf(uiSettings.iconOK+" The %v service is active\n", ServiceName) - } - } else { - systemStatus.returnCode += 1 - if uiSettings.isMachineReadable { - systemStatus.YggdrasilRunning = false - } else { - fmt.Printf(uiSettings.iconInfo+" The %v service is inactive\n", ServiceName) - } - } - return nil -} diff --git a/status_cmd.go b/status_cmd.go new file mode 100644 index 0000000..4533f67 --- /dev/null +++ b/status_cmd.go @@ -0,0 +1,229 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "github.com/urfave/cli/v2" + "os" + "time" + + "github.com/briandowns/spinner" + systemd "github.com/coreos/go-systemd/v22/dbus" +) + +// rhsmStatus tries to print status provided by RHSM D-Bus API. If we provide +// output in machine-readable format, then we only set files in SystemStatus +// structure and content of this structure will be printed later +func rhsmStatus(systemStatus *SystemStatus) error { + + uuid, err := getConsumerUUID() + if err != nil { + return fmt.Errorf("unable to get consumer UUID: %s", err) + } + if uuid == "" { + systemStatus.returnCode += 1 + if uiSettings.isMachineReadable { + systemStatus.RHSMConnected = false + } else { + fmt.Printf("%v Not connected to Red Hat Subscription Management\n", uiSettings.iconInfo) + } + } else { + if uiSettings.isMachineReadable { + systemStatus.RHSMConnected = true + } else { + fmt.Printf("%v Connected to Red Hat Subscription Management\n", uiSettings.iconOK) + } + } + return nil +} + +// insightStatus tries to print status of insights client +func insightStatus(systemStatus *SystemStatus) { + var s *spinner.Spinner + if uiSettings.isRich { + s = spinner.New(spinner.CharSets[9], 100*time.Millisecond) + s.Suffix = " Checking Red Hat Insights..." + s.Start() + } + isRegistered, err := insightsIsRegistered() + if uiSettings.isRich { + s.Stop() + } + if isRegistered { + if uiSettings.isMachineReadable { + systemStatus.InsightsConnected = true + } else { + fmt.Print(uiSettings.iconOK + " Connected to Red Hat Insights\n") + } + } else { + systemStatus.returnCode += 1 + if err == nil { + if uiSettings.isMachineReadable { + systemStatus.InsightsConnected = false + } else { + fmt.Print(uiSettings.iconInfo + " Not connected to Red Hat Insights\n") + } + } else { + if uiSettings.isMachineReadable { + systemStatus.InsightsConnected = false + systemStatus.InsightsError = err.Error() + } else { + fmt.Printf(uiSettings.iconError+" Cannot detect Red Hat Insights status: %v\n", err) + } + } + } +} + +// serviceStatus tries to print status of yggdrasil.service or rhcd.service +func serviceStatus(systemStatus *SystemStatus) error { + ctx := context.Background() + conn, err := systemd.NewSystemConnectionContext(ctx) + if err != nil { + systemStatus.YggdrasilRunning = false + systemStatus.YggdrasilError = err.Error() + return fmt.Errorf("unable to connect to systemd: %s", err) + } + defer conn.Close() + unitName := ServiceName + ".service" + properties, err := conn.GetUnitPropertiesContext(ctx, unitName) + if err != nil { + systemStatus.YggdrasilRunning = false + systemStatus.YggdrasilError = err.Error() + return fmt.Errorf("unable to get properties of %s: %s", unitName, err) + } + activeState := properties["ActiveState"] + if activeState.(string) == "active" { + if uiSettings.isMachineReadable { + systemStatus.YggdrasilRunning = true + } else { + fmt.Printf(uiSettings.iconOK+" The %v service is active\n", ServiceName) + } + } else { + systemStatus.returnCode += 1 + if uiSettings.isMachineReadable { + systemStatus.YggdrasilRunning = false + } else { + fmt.Printf(uiSettings.iconInfo+" The %v service is inactive\n", ServiceName) + } + } + return nil +} + +// SystemStatus is structure holding information about system status +// When more file format is supported, then add more tags for fields +// like xml:"hostname" +type SystemStatus struct { + SystemHostname string `json:"hostname"` + HostnameError string `json:"hostname_error,omitempty"` + RHSMConnected bool `json:"rhsm_connected"` + RHSMError string `json:"rhsm_error,omitempty"` + InsightsConnected bool `json:"insights_connected"` + InsightsError string `json:"insights_error,omitempty"` + YggdrasilRunning bool `json:"yggdrasil_running"` + YggdrasilError string `json:"yggdrasil_error,omitempty"` + returnCode int +} + +// printJSONStatus tries to print the system status as JSON to stdout. +// When marshaling of systemStatus fails, then error is returned +func printJSONStatus(systemStatus *SystemStatus) error { + data, err := json.MarshalIndent(systemStatus, "", " ") + if err != nil { + return err + } + fmt.Println(string(data)) + return nil +} + +// beforeStatusAction ensures the user has supplied a correct `--format` flag. +func beforeStatusAction(ctx *cli.Context) error { + err := setupFormatOption(ctx) + if err != nil { + return err + } + + return checkForUnknownArgs(ctx) +} + +// statusAction tries to print status of system. It means that it gives +// answer on following questions: +// 1. Is system registered to Red Hat Subscription Management? +// 2. Is system connected to Red Hat Insights? +// 3. Is yggdrasil.service (rhcd.service) running? +// Status can be printed as human-readable text or machine-readable JSON document. +// Format is influenced by --format json CLI option stored in CLI context +func statusAction(ctx *cli.Context) (err error) { + var systemStatus SystemStatus + var machineReadablePrintFunc func(systemStatus *SystemStatus) error + + format := ctx.String("format") + switch format { + case "json": + machineReadablePrintFunc = printJSONStatus + default: + break + } + + // When printing of status is requested, then print machine-readable file format + // at the end of this function + if uiSettings.isMachineReadable { + defer func(systemStatus *SystemStatus) { + err = machineReadablePrintFunc(systemStatus) + // When it was not possible to print status to machine-readable format, then + // change returned error to CLI exit error to be able to set exit code to + // a non-zero value + if err != nil { + err = cli.Exit( + fmt.Errorf("unable to print status as %s document: %s", format, err.Error()), + 1) + } + // When any of status is not correct, then return 1 exit code + if systemStatus.returnCode != 0 { + err = cli.Exit("", 1) + } + }(&systemStatus) + } + + hostname, err := os.Hostname() + if err != nil { + if uiSettings.isMachineReadable { + systemStatus.HostnameError = err.Error() + } else { + return cli.Exit(err, 1) + } + } + + if uiSettings.isMachineReadable { + systemStatus.SystemHostname = hostname + } else { + fmt.Printf("Connection status for %v:\n\n", hostname) + } + + /* 1. Get Status of RHSM */ + err = rhsmStatus(&systemStatus) + if err != nil { + return cli.Exit(err, 1) + } + + /* 2. Get status of insights-client */ + insightStatus(&systemStatus) + + /* 3. Get status of yggdrasil (rhcd) service */ + err = serviceStatus(&systemStatus) + if err != nil { + return cli.Exit(err, 1) + } + + if !uiSettings.isMachineReadable { + fmt.Printf("\nManage your connected systems: https://red.ht/connector\n") + } + + // At the end check if all statuses are correct. + // If not, return 1 exit code without any message. + if systemStatus.returnCode != 0 { + return cli.Exit("", 1) + } + + return nil +} diff --git a/util.go b/util.go index cef646a..226a7b4 100644 --- a/util.go +++ b/util.go @@ -3,12 +3,10 @@ package main import ( "fmt" "io" - "net/url" "os" "path/filepath" "strings" - "github.com/subpop/go-ini" "github.com/subpop/go-log" "github.com/urfave/cli/v2" @@ -70,71 +68,6 @@ func ConfigPath() (string, error) { return filePath, nil } -// GuessAPIURL gets the API server URL based on, insights-client.conf -// and rhsm.conf. This URL may differ from prod, stage and Satellite -func GuessAPIURL() (string, error) { - var uString string - var baseURL *url.URL - - // Check if the server api is set in insights conf - // Create the structs needed to read the config file - opts := ini.Options{ - AllowNumberSignComments: true, - } - type InsightsClientConf struct { - BaseUrl string `ini:"base_url"` - } - type InsightsConf struct { - InsightsClient InsightsClientConf `ini:"insights-client"` - } - var cfg InsightsConf - // Read the config file - confFilePath := "/etc/insights-client/insights-client.conf" - data, err := os.ReadFile(confFilePath) - if err != nil { - return "", fmt.Errorf("failed to read file '%v': %v", confFilePath, err) - } - // Get the config into the struct - if err := ini.UnmarshalWithOptions(data, &cfg, opts); err != nil { - return "", fmt.Errorf("failed to parse file '%v': %v", confFilePath, err) - } - APIServer := cfg.InsightsClient.BaseUrl - - if APIServer != "" { - base, err := url.Parse("https://" + APIServer) - if err != nil { - return "", fmt.Errorf("cannot get base URL: %w", err) - } - p, _ := url.Parse("api/config-manager/v2/profiles/current") - uString = base.ResolveReference(p).String() - } else { - // Get the server hostname where this host is connected - var serverHost string - err = getRHSMConfigOption("server.hostname", &serverHost) - if err != nil { - return "", fmt.Errorf("cannot get server hostname: %w", err) - } - // Get the final api server url to make the call - // Check if it is the default api server - if strings.Contains(serverHost, "subscription.rhsm.redhat.com") { - baseURL, _ = url.Parse("https://cert.console.redhat.com") - p, _ := url.Parse("api/config-manager/v2/profiles/current") - uString = baseURL.ResolveReference(p).String() - } else { - // Otherwise it is connected to Satellite - // Generate the base URL - base, err := url.Parse("https://" + serverHost) - if err != nil { - return "", fmt.Errorf("cannot get base URL: %w", err) - } - p, _ := url.Parse("redhat_access/r/insights/platform/config-manager/v2/profiles/current") - uString = base.ResolveReference(p).String() - } - } - - return uString, nil -} - // hasPriorityErrors checks if the errorMessage map has any error // with a higher priority than the logLevel configure. func hasPriorityErrors(errorMessages map[string]LogMessage, level log.Level) bool { @@ -163,3 +96,26 @@ func checkForUnknownArgs(ctx *cli.Context) error { } return nil } + +// setupFormatOption ensures the user has supplied a correct `--format` flag +// and set values in uiSettings, when JSON format is used. +func setupFormatOption(ctx *cli.Context) error { + // This is run after the `app.Before()` has been run, + // the uiSettings is already set up for us to modify. + format := ctx.String("format") + switch format { + case "": + return nil + case "json": + uiSettings.isMachineReadable = true + uiSettings.isRich = false + return nil + default: + err := fmt.Errorf( + "unsupported format: %s (supported formats: %s)", + format, + `"json"`, + ) + return cli.Exit(err, 1) + } +}