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 }