github.com/pingcap/tiup@v1.15.1/components/cluster/command/root.go (about)

     1  // Copyright 2020 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package command
    15  
    16  import (
    17  	"context"
    18  	"encoding/json"
    19  	"fmt"
    20  	"os"
    21  	"path"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/fatih/color"
    26  	"github.com/google/uuid"
    27  	"github.com/joomcode/errorx"
    28  	perrs "github.com/pingcap/errors"
    29  	"github.com/pingcap/tiup/pkg/cluster/executor"
    30  	"github.com/pingcap/tiup/pkg/cluster/manager"
    31  	operator "github.com/pingcap/tiup/pkg/cluster/operation"
    32  	"github.com/pingcap/tiup/pkg/cluster/spec"
    33  	tiupmeta "github.com/pingcap/tiup/pkg/environment"
    34  	"github.com/pingcap/tiup/pkg/localdata"
    35  	"github.com/pingcap/tiup/pkg/logger"
    36  	logprinter "github.com/pingcap/tiup/pkg/logger/printer"
    37  	"github.com/pingcap/tiup/pkg/proxy"
    38  	"github.com/pingcap/tiup/pkg/repository"
    39  	"github.com/pingcap/tiup/pkg/telemetry"
    40  	"github.com/pingcap/tiup/pkg/tui"
    41  	"github.com/pingcap/tiup/pkg/utils"
    42  	"github.com/pingcap/tiup/pkg/version"
    43  	"github.com/spf13/cobra"
    44  	"go.uber.org/zap"
    45  )
    46  
    47  var (
    48  	errNS         = errorx.NewNamespace("cmd")
    49  	rootCmd       *cobra.Command
    50  	gOpt          operator.Options
    51  	skipConfirm   bool
    52  	reportEnabled bool // is telemetry report enabled
    53  	teleReport    *telemetry.Report
    54  	clusterReport *telemetry.ClusterReport
    55  	teleNodeInfos []*telemetry.NodeInfo
    56  	teleTopology  string
    57  	teleCommand   []string
    58  	log           = logprinter.NewLogger("") // init default logger
    59  )
    60  
    61  var tidbSpec *spec.SpecManager
    62  var cm *manager.Manager
    63  
    64  func scrubClusterName(n string) string {
    65  	// prepend the telemetry secret to cluster name, so that two installations
    66  	// of tiup with the same cluster name produce different hashes
    67  	return "cluster_" + telemetry.SaltedHash(n)
    68  }
    69  
    70  func getParentNames(cmd *cobra.Command) []string {
    71  	if cmd == nil {
    72  		return nil
    73  	}
    74  
    75  	p := cmd.Parent()
    76  	// always use 'cluster' as the root command name
    77  	if cmd.Parent() == nil {
    78  		return []string{"cluster"}
    79  	}
    80  
    81  	return append(getParentNames(p), cmd.Name())
    82  }
    83  
    84  func init() {
    85  	logger.InitGlobalLogger()
    86  
    87  	tui.AddColorFunctionsForCobra()
    88  
    89  	cobra.EnableCommandSorting = false
    90  
    91  	nativeEnvVar := strings.ToLower(os.Getenv(localdata.EnvNameNativeSSHClient))
    92  	if nativeEnvVar == "true" || nativeEnvVar == "1" || nativeEnvVar == "enable" {
    93  		gOpt.NativeSSH = true
    94  	}
    95  
    96  	rootCmd = &cobra.Command{
    97  		Use:           tui.OsArgs0(),
    98  		Short:         "Deploy a TiDB cluster for production",
    99  		SilenceUsage:  true,
   100  		SilenceErrors: true,
   101  		Version:       version.NewTiUPVersion().String(),
   102  		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
   103  			// populate logger
   104  			log.SetDisplayModeFromString(gOpt.DisplayMode)
   105  
   106  			var err error
   107  			var env *tiupmeta.Environment
   108  			if err = spec.Initialize("cluster"); err != nil {
   109  				return err
   110  			}
   111  
   112  			tidbSpec = spec.GetSpecManager()
   113  			cm = manager.NewManager("tidb", tidbSpec, log)
   114  			if cmd.Name() != "__complete" {
   115  				logger.EnableAuditLog(spec.AuditDir())
   116  			}
   117  
   118  			// Running in other OS/ARCH Should be fine we only download manifest file.
   119  			env, err = tiupmeta.InitEnv(repository.Options{
   120  				GOOS:   "linux",
   121  				GOARCH: "amd64",
   122  			}, repository.MirrorOptions{})
   123  			if err != nil {
   124  				return err
   125  			}
   126  			tiupmeta.SetGlobalEnv(env)
   127  
   128  			teleCommand = getParentNames(cmd)
   129  
   130  			if gOpt.NativeSSH {
   131  				gOpt.SSHType = executor.SSHTypeSystem
   132  				log.Infof(
   133  					"System ssh client will be used (%s=%s)",
   134  					localdata.EnvNameNativeSSHClient,
   135  					os.Getenv(localdata.EnvNameNativeSSHClient))
   136  				log.Infof("The --native-ssh flag has been deprecated, please use --ssh=system")
   137  			}
   138  
   139  			err = proxy.MaybeStartProxy(
   140  				gOpt.SSHProxyHost,
   141  				gOpt.SSHProxyPort,
   142  				gOpt.SSHProxyUser,
   143  				gOpt.SSHProxyUsePassword,
   144  				gOpt.SSHProxyIdentity,
   145  				log,
   146  			)
   147  			if err != nil {
   148  				return perrs.Annotate(err, "start http-proxy")
   149  			}
   150  
   151  			return nil
   152  		},
   153  		PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
   154  			proxy.MaybeStopProxy()
   155  			return tiupmeta.GlobalEnv().V1Repository().Mirror().Close()
   156  		},
   157  	}
   158  
   159  	tui.BeautifyCobraUsageAndHelp(rootCmd)
   160  
   161  	rootCmd.PersistentFlags().Uint64Var(&gOpt.SSHTimeout, "ssh-timeout", 5, "Timeout in seconds to connect host via SSH, ignored for operations that don't need an SSH connection.")
   162  	// the value of wait-timeout is also used for `systemctl` commands, as the default timeout of systemd for
   163  	// start/stop operations is 90s, the default value of this argument is better be longer than that
   164  	rootCmd.PersistentFlags().Uint64Var(&gOpt.OptTimeout, "wait-timeout", 120, "Timeout in seconds to wait for an operation to complete, ignored for operations that don't fit.")
   165  	rootCmd.PersistentFlags().BoolVarP(&skipConfirm, "yes", "y", false, "Skip all confirmations and assumes 'yes'")
   166  	rootCmd.PersistentFlags().BoolVar(&gOpt.NativeSSH, "native-ssh", gOpt.NativeSSH, "(EXPERIMENTAL) Use the native SSH client installed on local system instead of the build-in one.")
   167  	rootCmd.PersistentFlags().StringVar((*string)(&gOpt.SSHType), "ssh", "", "(EXPERIMENTAL) The executor type: 'builtin', 'system', 'none'.")
   168  	rootCmd.PersistentFlags().IntVarP(&gOpt.Concurrency, "concurrency", "c", 5, "max number of parallel tasks allowed")
   169  	rootCmd.PersistentFlags().StringVar(&gOpt.DisplayMode, "format", "default", "(EXPERIMENTAL) The format of output, available values are [default, json]")
   170  	rootCmd.PersistentFlags().StringVar(&gOpt.SSHProxyHost, "ssh-proxy-host", "", "The SSH proxy host used to connect to remote host.")
   171  	rootCmd.PersistentFlags().StringVar(&gOpt.SSHProxyUser, "ssh-proxy-user", utils.CurrentUser(), "The user name used to login the proxy host.")
   172  	rootCmd.PersistentFlags().IntVar(&gOpt.SSHProxyPort, "ssh-proxy-port", 22, "The port used to login the proxy host.")
   173  	rootCmd.PersistentFlags().StringVar(&gOpt.SSHProxyIdentity, "ssh-proxy-identity-file", path.Join(utils.UserHome(), ".ssh", "id_rsa"), "The identity file used to login the proxy host.")
   174  	rootCmd.PersistentFlags().BoolVar(&gOpt.SSHProxyUsePassword, "ssh-proxy-use-password", false, "Use password to login the proxy host.")
   175  	rootCmd.PersistentFlags().Uint64Var(&gOpt.SSHProxyTimeout, "ssh-proxy-timeout", 5, "Timeout in seconds to connect the proxy host via SSH, ignored for operations that don't need an SSH connection.")
   176  	_ = rootCmd.PersistentFlags().MarkHidden("native-ssh")
   177  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-host")
   178  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-user")
   179  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-port")
   180  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-identity-file")
   181  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-use-password")
   182  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-timeout")
   183  
   184  	rootCmd.AddCommand(
   185  		newCheckCmd(),
   186  		newDeploy(),
   187  		newStartCmd(),
   188  		newStopCmd(),
   189  		newRestartCmd(),
   190  		newScaleInCmd(),
   191  		newScaleOutCmd(),
   192  		newDestroyCmd(),
   193  		newCleanCmd(),
   194  		newUpgradeCmd(),
   195  		newDisplayCmd(),
   196  		newPruneCmd(),
   197  		newListCmd(),
   198  		newAuditCmd(),
   199  		newImportCmd(),
   200  		newEditConfigCmd(),
   201  		newShowConfigCmd(),
   202  		newReloadCmd(),
   203  		newPatchCmd(),
   204  		newRenameCmd(),
   205  		newEnableCmd(),
   206  		newDisableCmd(),
   207  		newExecCmd(),
   208  		newPullCmd(),
   209  		newPushCmd(),
   210  		newTestCmd(), // hidden command for test internally
   211  		newTelemetryCmd(),
   212  		newReplayCmd(),
   213  		newTemplateCmd(),
   214  		newTLSCmd(),
   215  		newMetaCmd(),
   216  		newRotateSSHCmd(),
   217  	)
   218  }
   219  
   220  func printErrorMessageForNormalError(err error) {
   221  	_, _ = tui.ColorErrorMsg.Fprintf(os.Stderr, "\nError: %s\n", err.Error())
   222  }
   223  
   224  func printErrorMessageForErrorX(err *errorx.Error) {
   225  	msg := ""
   226  	ident := 0
   227  	causeErrX := err
   228  	for causeErrX != nil {
   229  		if ident > 0 {
   230  			msg += strings.Repeat("  ", ident) + "caused by: "
   231  		}
   232  		currentErrMsg := causeErrX.Message()
   233  		if len(currentErrMsg) > 0 {
   234  			if ident == 0 {
   235  				// Print error code only for top level error
   236  				msg += fmt.Sprintf("%s (%s)\n", currentErrMsg, causeErrX.Type().FullName())
   237  			} else {
   238  				msg += fmt.Sprintf("%s\n", currentErrMsg)
   239  			}
   240  			ident++
   241  		}
   242  		cause := causeErrX.Cause()
   243  		if c := errorx.Cast(cause); c != nil {
   244  			causeErrX = c
   245  		} else {
   246  			if cause != nil {
   247  				if ident > 0 {
   248  					// The error may have empty message. In this case we treat it as a transparent error.
   249  					// Thus `ident == 0` can be possible.
   250  					msg += strings.Repeat("  ", ident) + "caused by: "
   251  				}
   252  				msg += fmt.Sprintf("%s\n", cause.Error())
   253  			}
   254  			break
   255  		}
   256  	}
   257  	_, _ = tui.ColorErrorMsg.Fprintf(os.Stderr, "\nError: %s", msg)
   258  }
   259  
   260  func extractSuggestionFromErrorX(err *errorx.Error) string {
   261  	cause := err
   262  	for cause != nil {
   263  		v, ok := cause.Property(utils.ErrPropSuggestion)
   264  		if ok {
   265  			if s, ok := v.(string); ok {
   266  				return s
   267  			}
   268  		}
   269  		cause = errorx.Cast(cause.Cause())
   270  	}
   271  
   272  	return ""
   273  }
   274  
   275  // Execute executes the root command
   276  func Execute() {
   277  	zap.L().Info("Execute command", zap.String("command", tui.OsArgs()))
   278  	zap.L().Debug("Environment variables", zap.Strings("env", os.Environ()))
   279  
   280  	teleReport = new(telemetry.Report)
   281  	clusterReport = new(telemetry.ClusterReport)
   282  	teleReport.EventDetail = &telemetry.Report_Cluster{Cluster: clusterReport}
   283  	reportEnabled = telemetry.Enabled()
   284  	if reportEnabled {
   285  		eventUUID := os.Getenv(localdata.EnvNameTelemetryEventUUID)
   286  		if eventUUID == "" {
   287  			eventUUID = uuid.New().String()
   288  		}
   289  		teleReport.InstallationUUID = telemetry.GetUUID()
   290  		teleReport.EventUUID = eventUUID
   291  		teleReport.EventUnixTimestamp = time.Now().Unix()
   292  		teleReport.Version = telemetry.TiUPMeta()
   293  	}
   294  
   295  	start := time.Now()
   296  	code := 0
   297  	err := rootCmd.Execute()
   298  	if err != nil {
   299  		code = 1
   300  	}
   301  
   302  	zap.L().Info("Execute command finished", zap.Int("code", code), zap.Error(err))
   303  
   304  	if reportEnabled {
   305  		f := func() {
   306  			defer func() {
   307  				if r := recover(); r != nil {
   308  					if tiupmeta.DebugMode {
   309  						log.Debugf("Recovered in telemetry report: %v", r)
   310  					}
   311  				}
   312  			}()
   313  
   314  			clusterReport.ExitCode = int32(code)
   315  			clusterReport.Nodes = teleNodeInfos
   316  			if teleTopology != "" {
   317  				if data, err := telemetry.ScrubYaml(
   318  					[]byte(teleTopology),
   319  					map[string]struct{}{
   320  						"host":       {},
   321  						"name":       {},
   322  						"user":       {},
   323  						"group":      {},
   324  						"deploy_dir": {},
   325  						"data_dir":   {},
   326  						"log_dir":    {},
   327  					}, // fields to hash
   328  					map[string]struct{}{
   329  						"config":         {},
   330  						"server_configs": {},
   331  					}, // fields to omit
   332  					telemetry.GetSecret(),
   333  				); err == nil {
   334  					clusterReport.Topology = (string(data))
   335  				}
   336  			}
   337  			clusterReport.TakeMilliseconds = uint64(time.Since(start).Milliseconds())
   338  			clusterReport.Command = strings.Join(teleCommand, " ")
   339  			ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
   340  			tele := telemetry.NewTelemetry()
   341  			err := tele.Report(ctx, teleReport)
   342  			if tiupmeta.DebugMode {
   343  				if err != nil {
   344  					log.Infof("report failed: %v", err)
   345  				}
   346  				log.Errorf("report: %s\n", teleReport.String())
   347  				if data, err := json.Marshal(teleReport); err == nil {
   348  					log.Debugf("report: %s\n", string(data))
   349  				}
   350  			}
   351  			cancel()
   352  		}
   353  
   354  		f()
   355  	}
   356  
   357  	switch log.GetDisplayMode() {
   358  	case logprinter.DisplayModeJSON:
   359  		obj := struct {
   360  			Code int    `json:"exit_code"`
   361  			Err  string `json:"error,omitempty"`
   362  		}{
   363  			Code: code,
   364  		}
   365  		if err != nil {
   366  			obj.Err = err.Error()
   367  		}
   368  		data, err := json.Marshal(obj)
   369  		if err != nil {
   370  			fmt.Printf("{\"exit_code\":%d, \"error\":\"%s\"}", code, err)
   371  		}
   372  		fmt.Fprintln(os.Stderr, string(data))
   373  	default:
   374  		if err != nil {
   375  			if errx := errorx.Cast(err); errx != nil {
   376  				printErrorMessageForErrorX(errx)
   377  			} else {
   378  				printErrorMessageForNormalError(err)
   379  			}
   380  
   381  			if !errorx.HasTrait(err, utils.ErrTraitPreCheck) {
   382  				logger.OutputDebugLog("tiup-cluster")
   383  			}
   384  
   385  			if errx := errorx.Cast(err); errx != nil {
   386  				if suggestion := extractSuggestionFromErrorX(errx); len(suggestion) > 0 {
   387  					log.Errorf("\n%s\n", suggestion)
   388  				}
   389  			}
   390  		}
   391  	}
   392  	err = logger.OutputAuditLogIfEnabled()
   393  	if err != nil {
   394  		zap.L().Warn("Write audit log file failed", zap.Error(err))
   395  		code = 1
   396  	}
   397  
   398  	color.Unset()
   399  
   400  	if code != 0 {
   401  		os.Exit(code)
   402  	}
   403  }