diff --git a/components/cluster/command/audit.go b/components/cluster/command/audit.go index bc6ace1d93..047cd53015 100644 --- a/components/cluster/command/audit.go +++ b/components/cluster/command/audit.go @@ -14,20 +14,8 @@ package command import ( - "bufio" - "fmt" - "io/ioutil" - "os" - "sort" - "strings" - "time" - - "github.com/fatih/color" - "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/base52" - "github.com/pingcap/tiup/pkg/cliutil" + "github.com/pingcap/tiup/pkg/cluster/audit" "github.com/pingcap/tiup/pkg/cluster/spec" - tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) @@ -38,9 +26,9 @@ func newAuditCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { switch len(args) { case 0: - return showAuditList() + return audit.ShowAuditList(spec.AuditDir()) case 1: - return showAuditLog(args[0]) + return audit.ShowAuditLog(spec.AuditDir(), args[0]) default: return cmd.Help() } @@ -48,77 +36,3 @@ func newAuditCmd() *cobra.Command { } return cmd } - -func showAuditList() error { - firstLine := func(fileName string) (string, error) { - file, err := os.Open(spec.ProfilePath(spec.TiOpsAuditDir, fileName)) - if err != nil { - return "", errors.Trace(err) - } - defer file.Close() - - scanner := bufio.NewScanner(file) - if scanner.Scan() { - return scanner.Text(), nil - } - return "", errors.New("unknown audit log format") - } - - auditDir := spec.ProfilePath(spec.TiOpsAuditDir) - // Header - clusterTable := [][]string{{"ID", "Time", "Command"}} - fileInfos, err := ioutil.ReadDir(auditDir) - if err != nil && !os.IsNotExist(err) { - return err - } - for _, fi := range fileInfos { - if fi.IsDir() { - continue - } - ts, err := base52.Decode(fi.Name()) - if err != nil { - continue - } - t := time.Unix(ts, 0) - cmd, err := firstLine(fi.Name()) - if err != nil { - continue - } - clusterTable = append(clusterTable, []string{ - fi.Name(), - t.Format(time.RFC3339), - cmd, - }) - } - - sort.Slice(clusterTable[1:], func(i, j int) bool { - return clusterTable[i+1][1] > clusterTable[j+1][1] - }) - - cliutil.PrintTable(clusterTable, true) - return nil -} - -func showAuditLog(auditID string) error { - path := spec.ProfilePath(spec.TiOpsAuditDir, auditID) - if tiuputils.IsNotExist(path) { - return errors.Errorf("cannot find the audit log '%s'", auditID) - } - - ts, err := base52.Decode(auditID) - if err != nil { - return errors.Annotatef(err, "unrecognized audit id '%s'", auditID) - } - - content, err := ioutil.ReadFile(path) - if err != nil { - return errors.Trace(err) - } - - t := time.Unix(ts, 0) - hint := fmt.Sprintf("- OPERATION TIME: %s -", t.Format("2006-01-02T15:04:05")) - line := strings.Repeat("-", len(hint)) - _, _ = os.Stdout.WriteString(color.MagentaString("%s\n%s\n%s\n", line, hint, line)) - _, _ = os.Stdout.Write(content) - return nil -} diff --git a/components/cluster/command/check.go b/components/cluster/command/check.go index ed17c48a2c..91587a09a9 100644 --- a/components/cluster/command/check.go +++ b/components/cluster/command/check.go @@ -29,7 +29,6 @@ import ( operator "github.com/pingcap/tiup/pkg/cluster/operation" "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" "github.com/pingcap/tiup/pkg/logger/log" "github.com/pingcap/tiup/pkg/meta" tiuputils "github.com/pingcap/tiup/pkg/utils" @@ -63,8 +62,6 @@ conflict checks with other clusters`, return cmd.Help() } - logger.EnableAuditLog() - var topo spec.Specification if opt.existCluster { // check for existing cluster clusterName := args[0] diff --git a/components/cluster/command/deploy.go b/components/cluster/command/deploy.go index 98aa00f3cb..1b93b0613f 100644 --- a/components/cluster/command/deploy.go +++ b/components/cluster/command/deploy.go @@ -15,28 +15,16 @@ package command import ( "context" - "fmt" "io/ioutil" - "os" "path" - "path/filepath" - "strings" - "github.com/fatih/color" - "github.com/joomcode/errorx" - "github.com/pingcap/errors" "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cliutil/prepare" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" + "github.com/pingcap/tiup/pkg/cluster" operator "github.com/pingcap/tiup/pkg/cluster/operation" "github.com/pingcap/tiup/pkg/cluster/report" "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/cluster/task" "github.com/pingcap/tiup/pkg/errutil" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" - "github.com/pingcap/tiup/pkg/set" telemetry2 "github.com/pingcap/tiup/pkg/telemetry" tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" @@ -55,30 +43,9 @@ var ( errDeployNameDuplicate = errNSDeploy.NewType("name_dup", errutil.ErrTraitPreCheck) ) -type ( - componentInfo struct { - component string - version string - } - - deployOptions struct { - user string // username to login to the SSH server - identityFile string // path to the private key file - usePassword bool // use password instead of identity file for ssh connection - ignoreConfigCheck bool // ignore config check result - } - - hostInfo struct { - ssh int // ssh port of host - os string // operating system - arch string // cpu architecture - // vendor string - } -) - func newDeploy() *cobra.Command { - opt := deployOptions{ - identityFile: path.Join(tiuputils.UserHome(), ".ssh", "id_rsa"), + opt := cluster.DeployOptions{ + IdentityFile: path.Join(tiuputils.UserHome(), ".ssh", "id_rsa"), } cmd := &cobra.Command{ Use: "deploy ", @@ -94,361 +61,47 @@ func newDeploy() *cobra.Command { return nil } - logger.EnableAuditLog() clusterName := args[0] version := args[1] teleCommand = append(teleCommand, scrubClusterName(clusterName)) teleCommand = append(teleCommand, version) - return deploy(clusterName, version, args[2], opt) - }, - } - - cmd.Flags().StringVarP(&opt.user, "user", "u", tiuputils.CurrentUser(), "The user name to login via SSH. The user must has root (or sudo) privilege.") - cmd.Flags().StringVarP(&opt.identityFile, "identity_file", "i", opt.identityFile, "The path of the SSH identity file. If specified, public key authentication will be used.") - cmd.Flags().BoolVarP(&opt.usePassword, "password", "p", false, "Use password of target hosts. If specified, password authentication will be used.") - cmd.Flags().BoolVarP(&opt.ignoreConfigCheck, "ignore-config-check", "", opt.ignoreConfigCheck, "Ignore the config check result") - - return cmd -} - -func confirmTopology(clusterName, version string, topo *spec.Specification, patchedRoles set.StringSet) error { - log.Infof("Please confirm your topology:") - - cyan := color.New(color.FgCyan, color.Bold) - fmt.Printf("TiDB Cluster: %s\n", cyan.Sprint(clusterName)) - fmt.Printf("TiDB Version: %s\n", cyan.Sprint(version)) - - clusterTable := [][]string{ - // Header - {"Type", "Host", "Ports", "OS/Arch", "Directories"}, - } - - topo.IterInstance(func(instance spec.Instance) { - comp := instance.ComponentName() - if patchedRoles.Exist(comp) { - comp = comp + " (patched)" - } - clusterTable = append(clusterTable, []string{ - comp, - instance.GetHost(), - clusterutil.JoinInt(instance.UsedPorts(), "/"), - cliutil.OsArch(instance.OS(), instance.Arch()), - strings.Join(instance.UsedDirs(), ","), - }) - }) - - cliutil.PrintTable(clusterTable, true) - - log.Warnf("Attention:") - log.Warnf(" 1. If the topology is not what you expected, check your yaml file.") - log.Warnf(" 2. Please confirm there is no port/directory conflicts in same host.") - if len(patchedRoles) != 0 { - log.Errorf(" 3. The component marked as `patched` has been replaced by previours patch command.") - } - - if len(topo.TiSparkMasters) > 0 || len(topo.TiSparkWorkers) > 0 { - log.Warnf("There are TiSpark nodes defined in the topology, please note that you'll need to manually install Java Runtime Environment (JRE) 8 on the host, other wise the TiSpark nodes will fail to start.") - log.Warnf("You may read the OpenJDK doc for a reference: https://openjdk.java.net/install/") - } - - return cliutil.PromptForConfirmOrAbortError("Do you want to continue? [y/N]: ") -} - -func deploy(clusterName, clusterVersion, topoFile string, opt deployOptions) error { - if err := clusterutil.ValidateClusterNameOrError(clusterName); err != nil { - return err - } - - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return errors.AddStack(err) - } - - if exist { - // FIXME: When change to use args, the suggestion text need to be updated. - return errDeployNameDuplicate. - New("Cluster name '%s' is duplicated", clusterName). - WithProperty(cliutil.SuggestionFromFormat("Please specify another cluster name")) - } - - var topo spec.Specification - if err := clusterutil.ParseTopologyYaml(topoFile, &topo); err != nil { - return err - } - - if data, err := ioutil.ReadFile(topoFile); err == nil { - teleTopology = string(data) - } - - if err := prepare.CheckClusterPortConflict(tidbSpec, clusterName, &topo); err != nil { - return err - } - if err := prepare.CheckClusterDirConflict(tidbSpec, clusterName, &topo); err != nil { - return err - } - if !skipConfirm { - if err := confirmTopology(clusterName, clusterVersion, &topo, set.NewStringSet()); err != nil { - return err - } - } - - sshConnProps, err := cliutil.ReadIdentityFileOrPassword(opt.identityFile, opt.usePassword) - if err != nil { - return err - } - - if err := os.MkdirAll(spec.ClusterPath(clusterName), 0755); err != nil { - return errorx.InitializationFailed. - Wrap(err, "Failed to create cluster metadata directory '%s'", spec.ClusterPath(clusterName)). - WithProperty(cliutil.SuggestionFromString("Please check file system permissions and try again.")) - } - - var ( - envInitTasks []*task.StepDisplay // tasks which are used to initialize environment - downloadCompTasks []*task.StepDisplay // tasks which are used to download components - deployCompTasks []*task.StepDisplay // tasks which are used to copy components to remote host - ) - - // Initialize environment - uniqueHosts := make(map[string]hostInfo) // host -> ssh-port, os, arch - globalOptions := topo.GlobalOptions - var iterErr error // error when itering over instances - iterErr = nil - topo.IterInstance(func(inst spec.Instance) { - if _, found := uniqueHosts[inst.GetHost()]; !found { - // check for "imported" parameter, it can not be true when scaling out - if inst.IsImported() { - iterErr = errors.New( - "'imported' is set to 'true' for new instance, this is only used " + - "for instances imported from tidb-ansible and make no sense when " + - "deploying new instances, please delete the line or set it to 'false' for new instances") - return // skip the host to avoid issues + topoFile := args[2] + if data, err := ioutil.ReadFile(topoFile); err == nil { + teleTopology = string(data) } - uniqueHosts[inst.GetHost()] = hostInfo{ - ssh: inst.GetSSHPort(), - os: inst.OS(), - arch: inst.Arch(), - } - var dirs []string - for _, dir := range []string{globalOptions.DeployDir, globalOptions.LogDir} { - if dir == "" { - continue - } - dirs = append(dirs, clusterutil.Abs(globalOptions.User, dir)) - } - // the default, relative path of data dir is under deploy dir - if strings.HasPrefix(globalOptions.DataDir, "/") { - dirs = append(dirs, globalOptions.DataDir) - } - t := task.NewBuilder(). - RootSSH( - inst.GetHost(), - inst.GetSSHPort(), - opt.user, - sshConnProps.Password, - sshConnProps.IdentityFile, - sshConnProps.IdentityFilePassphrase, - gOpt.SSHTimeout, - gOpt.NativeSSH, - ). - EnvInit(inst.GetHost(), globalOptions.User). - Mkdir(globalOptions.User, inst.GetHost(), dirs...). - BuildAsStep(fmt.Sprintf(" - Prepare %s:%d", inst.GetHost(), inst.GetSSHPort())) - envInitTasks = append(envInitTasks, t) - } - }) - - if iterErr != nil { - return iterErr - } - - // Download missing component - downloadCompTasks = prepare.BuildDownloadCompTasks(clusterVersion, &topo) - - // Deploy components to remote - topo.IterInstance(func(inst spec.Instance) { - version := spec.ComponentVersion(inst.ComponentName(), clusterVersion) - deployDir := clusterutil.Abs(globalOptions.User, inst.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(globalOptions.User, inst.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(globalOptions.User, inst.LogDir()) - // Deploy component - // prepare deployment server - t := task.NewBuilder(). - UserSSH(inst.GetHost(), inst.GetSSHPort(), globalOptions.User, gOpt.SSHTimeout, gOpt.NativeSSH). - Mkdir(globalOptions.User, inst.GetHost(), - deployDir, logDir, - filepath.Join(deployDir, "bin"), - filepath.Join(deployDir, "conf"), - filepath.Join(deployDir, "scripts")). - Mkdir(globalOptions.User, inst.GetHost(), dataDirs...) - - // copy dependency component if needed - switch inst.ComponentName() { - case spec.ComponentTiSpark: - t = t.DeploySpark(inst, version, "" /* default srcPath */, deployDir) - default: - t = t.CopyComponent( - inst.ComponentName(), - inst.OS(), - inst.Arch(), + return manager.Deploy( + clusterName, version, - "", // use default srcPath - inst.GetHost(), - deployDir, + topoFile, + opt, + postDeployHook, + skipConfirm, + gOpt.OptTimeout, + gOpt.SSHTimeout, + gOpt.NativeSSH, ) - } + }, + } - // generate configs for the component - t = t.InitConfig( - clusterName, - clusterVersion, - inst, - globalOptions.User, - opt.ignoreConfigCheck, - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - Cache: spec.ClusterPath(clusterName, spec.TempConfigPath), - }, - ) + cmd.Flags().StringVarP(&opt.User, "user", "u", tiuputils.CurrentUser(), "The user name to login via SSH. The user must has root (or sudo) privilege.") + cmd.Flags().StringVarP(&opt.IdentityFile, "identity_file", "i", opt.IdentityFile, "The path of the SSH identity file. If specified, public key authentication will be used.") + cmd.Flags().BoolVarP(&opt.UsePassword, "password", "p", false, "Use password of target hosts. If specified, password authentication will be used.") + cmd.Flags().BoolVarP(&opt.IgnoreConfigCheck, "ignore-config-check", "", opt.IgnoreConfigCheck, "Ignore the config check result") - deployCompTasks = append(deployCompTasks, - t.BuildAsStep(fmt.Sprintf(" - Copy %s -> %s", inst.ComponentName(), inst.GetHost())), - ) - }) + return cmd +} +func postDeployHook(builder *task.Builder, topo spec.Topology) { nodeInfoTask := task.NewBuilder().Func("Check status", func(ctx *task.Context) error { var err error - teleNodeInfos, err = operator.GetNodeInfo(context.Background(), ctx, &topo) + teleNodeInfos, err = operator.GetNodeInfo(context.Background(), ctx, topo) _ = err // intend to never return error return nil }).BuildAsStep("Check status").SetHidden(true) - - // Deploy monitor relevant components to remote - dlTasks, dpTasks := buildMonitoredDeployTask( - clusterName, - uniqueHosts, - globalOptions, - topo.MonitoredOptions, - clusterVersion, - ) - downloadCompTasks = append(downloadCompTasks, dlTasks...) - deployCompTasks = append(deployCompTasks, dpTasks...) - if report.Enable() { - deployCompTasks = append(deployCompTasks, nodeInfoTask) - } - - builder := task.NewBuilder(). - Step("+ Generate SSH keys", - task.NewBuilder().SSHKeyGen(spec.ClusterPath(clusterName, "ssh", "id_rsa")).Build()). - ParallelStep("+ Download TiDB components", downloadCompTasks...). - ParallelStep("+ Initialize target host environments", envInitTasks...). - ParallelStep("+ Copy files", deployCompTasks...) - if report.Enable() { builder.ParallelStep("+ Check status", nodeInfoTask) } - - t := builder.Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return errors.Trace(err) - } - - err = spec.SaveClusterMeta(clusterName, &spec.ClusterMeta{ - User: globalOptions.User, - Version: clusterVersion, - Topology: &topo, - }) - if err != nil { - return errors.Trace(err) - } - - hint := color.New(color.Bold).Sprintf("%s start %s", cliutil.OsArgs0(), clusterName) - log.Infof("Deployed cluster `%s` successfully, you can start the cluster via `%s`", clusterName, hint) - return nil -} - -func buildMonitoredDeployTask( - clusterName string, - uniqueHosts map[string]hostInfo, // host -> ssh-port, os, arch - globalOptions spec.GlobalOptions, - monitoredOptions spec.MonitoredOptions, - version string, -) (downloadCompTasks []*task.StepDisplay, deployCompTasks []*task.StepDisplay) { - uniqueCompOSArch := make(map[string]struct{}) // comp-os-arch -> {} - // monitoring agents - for _, comp := range []string{spec.ComponentNodeExporter, spec.ComponentBlackboxExporter} { - version := spec.ComponentVersion(comp, version) - - for host, info := range uniqueHosts { - // FIXME: as the uniqueHosts list is built with os-arch as part of the key, - // for platform independent packages, it will be downloaded multiple times - // and be saved with different file names in the packages dir, the tarballs - // are identical and the only difference is platform in filename. - - // populate unique os/arch set - key := fmt.Sprintf("%s-%s-%s", comp, info.os, info.arch) - if _, found := uniqueCompOSArch[key]; !found { - uniqueCompOSArch[key] = struct{}{} - downloadCompTasks = append(downloadCompTasks, task.NewBuilder(). - Download(comp, info.os, info.arch, version). - BuildAsStep(fmt.Sprintf(" - Download %s:%s (%s/%s)", comp, version, info.os, info.arch))) - } - - deployDir := clusterutil.Abs(globalOptions.User, monitoredOptions.DeployDir) - // data dir would be empty for components which don't need it - dataDir := monitoredOptions.DataDir - // the default data_dir is relative to deploy_dir - if dataDir != "" && !strings.HasPrefix(dataDir, "/") { - dataDir = filepath.Join(deployDir, dataDir) - } - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(globalOptions.User, monitoredOptions.LogDir) - // Deploy component - t := task.NewBuilder(). - UserSSH(host, info.ssh, globalOptions.User, gOpt.SSHTimeout, gOpt.NativeSSH). - Mkdir(globalOptions.User, host, - deployDir, dataDir, logDir, - filepath.Join(deployDir, "bin"), - filepath.Join(deployDir, "conf"), - filepath.Join(deployDir, "scripts")). - CopyComponent( - comp, - info.os, - info.arch, - version, - "", // use default srcPath - host, - deployDir, - ). - MonitoredConfig( - clusterName, - comp, - host, - globalOptions.ResourceControl, - monitoredOptions, - globalOptions.User, - meta.DirPaths{ - Deploy: deployDir, - Data: []string{dataDir}, - Log: logDir, - Cache: spec.ClusterPath(clusterName, spec.TempConfigPath), - }, - ). - BuildAsStep(fmt.Sprintf(" - Copy %s -> %s", comp, host)) - deployCompTasks = append(deployCompTasks, t) - } - } - return } diff --git a/components/cluster/command/destroy.go b/components/cluster/command/destroy.go index b86f35bca3..89a1e7ca86 100644 --- a/components/cluster/command/destroy.go +++ b/components/cluster/command/destroy.go @@ -14,19 +14,9 @@ package command import ( - "errors" - "os" - - "github.com/fatih/color" - "github.com/joomcode/errorx" perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" operator "github.com/pingcap/tiup/pkg/cluster/operation" "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" "github.com/pingcap/tiup/pkg/set" "github.com/spf13/cobra" ) @@ -45,6 +35,9 @@ You can retain some nodes and roles data when destroy cluster, eg: return cmd.Help() } + clusterName := args[0] + teleCommand = append(teleCommand, scrubClusterName(clusterName)) + // Validate the retained roles to prevent unexpected deleting data if len(destoyOpt.RetainDataRoles) > 0 { validRoles := set.NewStringSet(spec.AllComponentNames()...) @@ -55,57 +48,7 @@ You can retain some nodes and roles data when destroy cluster, eg: } } - clusterName := args[0] - teleCommand = append(teleCommand, scrubClusterName(clusterName)) - - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot destroy non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) && - !errors.Is(perrs.Cause(err), spec.ErrNoTiSparkMaster) { - return err - } - - if !skipConfirm { - if err := cliutil.PromptForConfirmOrAbortError( - "This operation will destroy TiDB %s cluster %s and its data.\nDo you want to continue? [y/N]:", - color.HiYellowString(metadata.Version), - color.HiYellowString(clusterName)); err != nil { - return err - } - log.Infof("Destroying cluster...") - } - - t := task.NewBuilder(). - SSHKeySet( - spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - ClusterOperate(metadata.Topology, operator.StopOperation, operator.Options{}). - ClusterOperate(metadata.Topology, operator.DestroyOperation, destoyOpt). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - if err := os.RemoveAll(spec.ClusterPath(clusterName)); err != nil { - return perrs.Trace(err) - } - log.Infof("Destroyed cluster `%s` successfully", clusterName) - return nil + return manager.DestroyCluster(clusterName, gOpt, destoyOpt, skipConfirm) }, } diff --git a/components/cluster/command/display.go b/components/cluster/command/display.go index 3c95cf6ede..fa0aacd6f3 100644 --- a/components/cluster/command/display.go +++ b/components/cluster/command/display.go @@ -17,21 +17,15 @@ import ( "errors" "fmt" "net/url" - "sort" - "strings" "time" - "github.com/fatih/color" perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" "github.com/pingcap/tiup/pkg/cluster/api" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" operator "github.com/pingcap/tiup/pkg/cluster/operation" "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/cluster/task" "github.com/pingcap/tiup/pkg/logger/log" "github.com/pingcap/tiup/pkg/meta" - "github.com/pingcap/tiup/pkg/set" "github.com/spf13/cobra" ) @@ -64,11 +58,9 @@ func newDisplayCmd() *cobra.Command { return displayDashboardInfo(clusterName) } - if err := displayClusterMeta(clusterName, &gOpt); err != nil { - return err - } - if err := displayClusterTopology(clusterName, &gOpt); err != nil { - return err + err = manager.Display(clusterName, gOpt) + if err != nil { + return perrs.AddStack(err) } metadata, err := spec.ClusterMetadata(clusterName) @@ -121,21 +113,6 @@ func displayDashboardInfo(clusterName string) error { return nil } -func displayClusterMeta(clusterName string, opt *operator.Options) error { - clsMeta, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) && - !errors.Is(perrs.Cause(err), spec.ErrNoTiSparkMaster) { - return err - } - - cyan := color.New(color.FgCyan, color.Bold) - - fmt.Printf("TiDB Cluster: %s\n", cyan.Sprint(clusterName)) - fmt.Printf("TiDB Version: %s\n", cyan.Sprint(clsMeta.Version)) - - return nil -} - func destroyTombstoneIfNeed(clusterName string, metadata *spec.ClusterMeta, opt operator.Options) error { topo := metadata.Topology @@ -175,122 +152,3 @@ func destroyTombstoneIfNeed(clusterName string, metadata *spec.ClusterMeta, opt return spec.SaveClusterMeta(clusterName, metadata) } - -func displayClusterTopology(clusterName string, opt *operator.Options) error { - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) && - !errors.Is(perrs.Cause(err), spec.ErrNoTiSparkMaster) { - return err - } - - topo := metadata.Topology - - clusterTable := [][]string{ - // Header - {"ID", "Role", "Host", "Ports", "OS/Arch", "Status", "Data Dir", "Deploy Dir"}, - } - - ctx := task.NewContext() - err = ctx.SetSSHKeySet(spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")) - if err != nil { - return perrs.AddStack(err) - } - - err = ctx.SetClusterSSH(topo, metadata.User, gOpt.SSHTimeout, gOpt.NativeSSH) - if err != nil { - return perrs.AddStack(err) - } - - filterRoles := set.NewStringSet(opt.Roles...) - filterNodes := set.NewStringSet(opt.Nodes...) - pdList := topo.GetPDList() - for _, comp := range topo.ComponentsByStartOrder() { - for _, ins := range comp.Instances() { - // apply role filter - if len(filterRoles) > 0 && !filterRoles.Exist(ins.Role()) { - continue - } - // apply node filter - if len(filterNodes) > 0 && !filterNodes.Exist(ins.ID()) { - continue - } - - dataDir := "-" - insDirs := ins.UsedDirs() - deployDir := insDirs[0] - if len(insDirs) > 1 { - dataDir = insDirs[1] - } - - status := ins.Status(pdList...) - // Query the service status - if status == "-" { - e, found := ctx.GetExecutor(ins.GetHost()) - if found { - active, _ := operator.GetServiceStatus(e, ins.ServiceName()) - if parts := strings.Split(strings.TrimSpace(active), " "); len(parts) > 2 { - if parts[1] == "active" { - status = "Up" - } else { - status = parts[1] - } - } - } - } - clusterTable = append(clusterTable, []string{ - color.CyanString(ins.ID()), - ins.Role(), - ins.GetHost(), - clusterutil.JoinInt(ins.UsedPorts(), "/"), - cliutil.OsArch(ins.OS(), ins.Arch()), - formatInstanceStatus(status), - dataDir, - deployDir, - }) - - } - } - - // Sort by role,host,ports - sort.Slice(clusterTable[1:], func(i, j int) bool { - lhs, rhs := clusterTable[i+1], clusterTable[j+1] - // column: 1 => role, 2 => host, 3 => ports - for _, col := range []int{1, 2} { - if lhs[col] != rhs[col] { - return lhs[col] < rhs[col] - } - } - return lhs[3] < rhs[3] - }) - - cliutil.PrintTable(clusterTable, true) - - return nil -} - -func formatInstanceStatus(status string) string { - lowercaseStatus := strings.ToLower(status) - - startsWith := func(prefixs ...string) bool { - for _, prefix := range prefixs { - if strings.HasPrefix(lowercaseStatus, prefix) { - return true - } - } - return false - } - - switch { - case startsWith("up|l"): // up|l, up|l|ui - return color.HiGreenString(status) - case startsWith("up"): - return color.GreenString(status) - case startsWith("down", "err"): // down, down|ui - return color.RedString(status) - case startsWith("tombstone", "disconnected"), strings.Contains(status, "offline"): - return color.YellowString(status) - default: - return status - } -} diff --git a/components/cluster/command/edit_config.go b/components/cluster/command/edit_config.go index a586a8c825..18ed3a5261 100644 --- a/components/cluster/command/edit_config.go +++ b/components/cluster/command/edit_config.go @@ -14,23 +14,7 @@ package command import ( - "bytes" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - - "github.com/fatih/color" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" - tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" ) func newEditConfigCmd() *cobra.Command { @@ -45,126 +29,9 @@ func newEditConfigCmd() *cobra.Command { clusterName := args[0] teleCommand = append(teleCommand, scrubClusterName(clusterName)) - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot start non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return err - } - - // do marshal and unmarshal outside editTopo() to avoid vadation inside - data, err := yaml.Marshal(metadata.Topology) - if err != nil { - return perrs.AddStack(err) - } - - newTopo, err := editTopo(clusterName, metadata.Topology, data) - if err != nil { - return err - } - if newTopo == nil { - return nil - } - - log.Infof("Apply the change...") - metadata.Topology = newTopo - err = spec.SaveClusterMeta(clusterName, metadata) - if err != nil { - return perrs.Annotate(err, "failed to save") - } - - log.Infof("Apply change successfully, please use `%s reload %s [-N ] [-R ]` to reload config.", cliutil.OsArgs0(), clusterName) - return nil + return manager.EditConfig(clusterName, skipConfirm) }, } return cmd } - -// 1. Write Topology to a temporary file. -// 2. Open file in editor. -// 3. Check and update Topology. -// 4. Save meta file. -func editTopo(clusterName string, origTopo *spec.Specification, data []byte) (*spec.Specification, error) { - file, err := ioutil.TempFile(os.TempDir(), "*") - if err != nil { - return nil, perrs.AddStack(err) - } - - name := file.Name() - - _, err = io.Copy(file, bytes.NewReader(data)) - if err != nil { - return nil, perrs.AddStack(err) - } - - err = file.Close() - if err != nil { - return nil, perrs.AddStack(err) - } - - err = tiuputils.OpenFileInEditor(name) - if err != nil { - return nil, perrs.AddStack(err) - } - - // Now user finish editing the file. - newData, err := ioutil.ReadFile(name) - if err != nil { - return nil, perrs.AddStack(err) - } - - newTopo := new(spec.Specification) - err = yaml.UnmarshalStrict(newData, newTopo) - if err != nil { - fmt.Print(color.RedString("New topology could not be saved: ")) - log.Infof("Failed to parse topology file: %v", err) - if cliutil.PromptForConfirmReverse("Do you want to continue editing? [Y/n]: ") { - return editTopo(clusterName, origTopo, newData) - } - log.Infof("Nothing changed.") - return nil, nil - } - - // report error if immutable field has been changed - if err := tiuputils.ValidateSpecDiff(origTopo, newTopo); err != nil { - fmt.Print(color.RedString("New topology could not be saved: ")) - log.Errorf("%s", err) - if cliutil.PromptForConfirmReverse("Do you want to continue editing? [Y/n]: ") { - return editTopo(clusterName, origTopo, newData) - } - log.Infof("Nothing changed.") - return nil, nil - - } - - origData, err := yaml.Marshal(origTopo) - if err != nil { - return nil, perrs.AddStack(err) - } - - if bytes.Equal(origData, newData) { - log.Infof("The file has nothing changed") - return nil, nil - } - - tiuputils.ShowDiff(string(origData), string(newData), os.Stdout) - - if !skipConfirm { - if err := cliutil.PromptForConfirmOrAbortError( - color.HiYellowString("Please check change highlight above, do you want to apply the change? [y/N]:"), - ); err != nil { - return nil, err - } - } - - return newTopo, nil -} diff --git a/components/cluster/command/exec.go b/components/cluster/command/exec.go index 7b75ef33b7..e9c8802afd 100644 --- a/components/cluster/command/exec.go +++ b/components/cluster/command/exec.go @@ -14,27 +14,12 @@ package command import ( - "errors" - - "github.com/fatih/color" - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" - "github.com/pingcap/tiup/pkg/set" + "github.com/pingcap/tiup/pkg/cluster" "github.com/spf13/cobra" ) -type execOptions struct { - command string - sudo bool -} - func newExecCmd() *cobra.Command { - opt := execOptions{} + opt := cluster.ExecOptions{} cmd := &cobra.Command{ Use: "exec ", Short: "Run shell command on host in the tidb cluster", @@ -46,87 +31,12 @@ func newExecCmd() *cobra.Command { clusterName := args[0] teleCommand = append(teleCommand, scrubClusterName(clusterName)) - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot execute command on non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return err - } - - filterRoles := set.NewStringSet(gOpt.Roles...) - filterNodes := set.NewStringSet(gOpt.Nodes...) - - var shellTasks []task.Task - uniqueHosts := map[string]int{} // host -> ssh-port - metadata.Topology.IterInstance(func(inst spec.Instance) { - if _, found := uniqueHosts[inst.GetHost()]; !found { - if len(gOpt.Roles) > 0 && !filterRoles.Exist(inst.Role()) { - return - } - - if len(gOpt.Nodes) > 0 && !filterNodes.Exist(inst.GetHost()) { - return - } - - uniqueHosts[inst.GetHost()] = inst.GetSSHPort() - } - }) - - for host := range uniqueHosts { - shellTasks = append(shellTasks, - task.NewBuilder(). - Shell(host, opt.command, opt.sudo). - Build()) - } - - t := task.NewBuilder(). - SSHKeySet( - spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - Parallel(shellTasks...). - Build() - - execCtx := task.NewContext() - if err := t.Execute(execCtx); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - // print outputs - for host := range uniqueHosts { - stdout, stderr, ok := execCtx.GetOutputs(host) - if !ok { - continue - } - log.Infof("Outputs of %s on %s:", - color.CyanString(opt.command), - color.CyanString(host)) - if len(stdout) > 0 { - log.Infof("%s:\n%s", color.GreenString("stdout"), stdout) - } - if len(stderr) > 0 { - log.Infof("%s:\n%s", color.RedString("stderr"), stderr) - } - } - - return nil + return manager.Exec(clusterName, opt, gOpt) }, } - cmd.Flags().StringVar(&opt.command, "command", "ls", "the command run on cluster host") - cmd.Flags().BoolVar(&opt.sudo, "sudo", false, "use root permissions (default false)") + cmd.Flags().StringVar(&opt.Command, "command", "ls", "the command run on cluster host") + cmd.Flags().BoolVar(&opt.Sudo, "sudo", false, "use root permissions (default false)") cmd.Flags().StringSliceVarP(&gOpt.Roles, "role", "R", nil, "Only exec on host with specified roles") cmd.Flags().StringSliceVarP(&gOpt.Nodes, "node", "N", nil, "Only exec on host with specified nodes") diff --git a/components/cluster/command/list.go b/components/cluster/command/list.go index 0834f47645..363badb33e 100644 --- a/components/cluster/command/list.go +++ b/components/cluster/command/list.go @@ -14,12 +14,6 @@ package command import ( - "errors" - - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/meta" "github.com/spf13/cobra" ) @@ -28,39 +22,8 @@ func newListCmd() *cobra.Command { Use: "list", Short: "List all clusters", RunE: func(cmd *cobra.Command, args []string) error { - return listCluster() + return manager.ListCluster() }, } return cmd } - -func listCluster() error { - names, err := tidbSpec.List() - if err != nil { - return perrs.AddStack(err) - } - - clusterTable := [][]string{ - // Header - {"Name", "User", "Version", "Path", "PrivateKey"}, - } - - for _, name := range names { - metadata := new(spec.ClusterMeta) - err := tidbSpec.Metadata(name, metadata) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return perrs.Trace(err) - } - - clusterTable = append(clusterTable, []string{ - name, - metadata.User, - metadata.Version, - tidbSpec.Path(name), - tidbSpec.Path(name, "ssh", "id_rsa"), - }) - } - - cliutil.PrintTable(clusterTable, true) - return nil -} diff --git a/components/cluster/command/patch.go b/components/cluster/command/patch.go index b20c187a00..47a6b28b89 100644 --- a/components/cluster/command/patch.go +++ b/components/cluster/command/patch.go @@ -14,22 +14,7 @@ package command import ( - "errors" - "fmt" - "os" - "os/exec" - "path" - - "github.com/joomcode/errorx" perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/meta" - "github.com/pingcap/tiup/pkg/set" - "github.com/pingcap/tiup/pkg/utils" - tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) @@ -54,7 +39,8 @@ func newPatchCmd() *cobra.Command { } clusterName := args[0] teleCommand = append(teleCommand, scrubClusterName(clusterName)) - return patch(args[0], args[1], gOpt, overwrite) + + return manager.Patch(clusterName, args[1], gOpt, overwrite) }, } @@ -64,150 +50,3 @@ func newPatchCmd() *cobra.Command { cmd.Flags().Int64Var(&gOpt.APITimeout, "transfer-timeout", 300, "Timeout in seconds when transferring PD and TiKV store leaders") return cmd } - -func patch(clusterName, packagePath string, options operator.Options, overwrite bool) error { - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot patch non-exists cluster %s", clusterName) - } - - if exist := tiuputils.IsExist(packagePath); !exist { - return perrs.New("specified package not exists") - } - - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return err - } - - insts, err := instancesToPatch(metadata, options) - if err != nil { - return err - } - if err := checkPackage(clusterName, insts[0].ComponentName(), insts[0].OS(), insts[0].Arch(), packagePath); err != nil { - return err - } - - var replacePackageTasks []task.Task - for _, inst := range insts { - deployDir := clusterutil.Abs(metadata.User, inst.DeployDir()) - tb := task.NewBuilder() - tb.BackupComponent(inst.ComponentName(), metadata.Version, inst.GetHost(), deployDir). - InstallPackage(packagePath, inst.GetHost(), deployDir) - replacePackageTasks = append(replacePackageTasks, tb.Build()) - } - - t := task.NewBuilder(). - SSHKeySet( - spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - Parallel(replacePackageTasks...). - ClusterOperate(metadata.Topology, operator.UpgradeOperation, options). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - if overwrite { - if err := overwritePatch(clusterName, insts[0].ComponentName(), packagePath); err != nil { - return err - } - } - - return nil -} - -func instancesToPatch(metadata *spec.ClusterMeta, options operator.Options) ([]spec.Instance, error) { - roleFilter := set.NewStringSet(options.Roles...) - nodeFilter := set.NewStringSet(options.Nodes...) - components := metadata.Topology.ComponentsByStartOrder() - components = operator.FilterComponent(components, roleFilter) - - instances := []spec.Instance{} - comps := []string{} - for _, com := range components { - insts := operator.FilterInstance(com.Instances(), nodeFilter) - if len(insts) > 0 { - comps = append(comps, com.Name()) - } - instances = append(instances, insts...) - } - if len(comps) > 1 { - return nil, fmt.Errorf("can't patch more than one component at once: %v", comps) - } - - if len(instances) == 0 { - return nil, fmt.Errorf("no instance found on specifid role(%v) and nodes(%v)", options.Roles, options.Nodes) - } - - return instances, nil -} - -func checkPackage(clusterName, comp, nodeOS, arch, packagePath string) error { - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return err - } - - ver := spec.ComponentVersion(comp, metadata.Version) - repo, err := clusterutil.NewRepository(nodeOS, arch) - if err != nil { - return err - } - entry, err := repo.ComponentBinEntry(comp, ver) - if err != nil { - return err - } - - checksum, err := tiuputils.Checksum(packagePath) - if err != nil { - return err - } - cacheDir := spec.ClusterPath(clusterName, "cache", comp+"-"+checksum[:7]) - if err := os.MkdirAll(cacheDir, 0755); err != nil { - return err - } - if err := exec.Command("tar", "-xvf", packagePath, "-C", cacheDir).Run(); err != nil { - return err - } - - if exists := tiuputils.IsExist(path.Join(cacheDir, entry)); !exists { - return fmt.Errorf("entry %s not found in package %s", entry, packagePath) - } - - return nil -} - -func overwritePatch(clusterName, comp, packagePath string) error { - if err := os.MkdirAll(spec.ClusterPath(clusterName, spec.PatchDirName), 0755); err != nil { - return err - } - - checksum, err := tiuputils.Checksum(packagePath) - if err != nil { - return err - } - - tg := spec.ClusterPath(clusterName, spec.PatchDirName, comp+"-"+checksum[:7]+".tar.gz") - if !utils.IsExist(tg) { - if err := tiuputils.CopyFile(packagePath, tg); err != nil { - return err - } - } - - symlink := spec.ClusterPath(clusterName, spec.PatchDirName, comp+".tar.gz") - if utils.IsSymExist(symlink) { - os.Remove(symlink) - } - return os.Symlink(tg, symlink) -} diff --git a/components/cluster/command/reload.go b/components/cluster/command/reload.go index c3df7b3715..5dc960d6f0 100644 --- a/components/cluster/command/reload.go +++ b/components/cluster/command/reload.go @@ -14,20 +14,8 @@ package command import ( - "errors" - "fmt" - "path/filepath" - "strings" - - "github.com/joomcode/errorx" perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" - operator "github.com/pingcap/tiup/pkg/cluster/operation" "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" "github.com/spf13/cobra" ) @@ -48,37 +36,7 @@ func newReloadCmd() *cobra.Command { clusterName := args[0] teleCommand = append(teleCommand, scrubClusterName(clusterName)) - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot start non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return err - } - - t, err := buildReloadTask(clusterName, metadata, gOpt, skipRestart) - if err != nil { - return err - } - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - log.Infof("Reloaded cluster `%s` successfully", clusterName) - - return nil + return manager.Reload(clusterName, gOpt, skipRestart) }, } @@ -92,134 +50,6 @@ func newReloadCmd() *cobra.Command { return cmd } -func buildReloadTask( - clusterName string, - metadata *spec.ClusterMeta, - options operator.Options, - skipRestart bool, -) (task.Task, error) { - - var refreshConfigTasks []*task.StepDisplay - - topo := metadata.Topology - hasImported := false - uniqueHosts := make(map[string]hostInfo) // host -> ssh-port, os, arch - - topo.IterInstance(func(inst spec.Instance) { - if _, found := uniqueHosts[inst.GetHost()]; !found { - uniqueHosts[inst.GetHost()] = hostInfo{ - ssh: inst.GetSSHPort(), - os: inst.OS(), - arch: inst.Arch(), - } - } - - deployDir := clusterutil.Abs(metadata.User, inst.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(metadata.User, inst.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(metadata.User, inst.LogDir()) - - // Download and copy the latest component to remote if the cluster is imported from Ansible - tb := task.NewBuilder().UserSSH(inst.GetHost(), inst.GetSSHPort(), metadata.User, gOpt.SSHTimeout, gOpt.NativeSSH) - if inst.IsImported() { - switch compName := inst.ComponentName(); compName { - case spec.ComponentGrafana, spec.ComponentPrometheus, spec.ComponentAlertManager: - version := spec.ComponentVersion(compName, metadata.Version) - tb.Download(compName, inst.OS(), inst.Arch(), version). - CopyComponent( - compName, - inst.OS(), - inst.Arch(), - version, - "", // use default srcPath - inst.GetHost(), - deployDir, - ) - } - hasImported = true - } - - // Refresh all configuration - t := tb.InitConfig(clusterName, - metadata.Version, - inst, metadata.User, - options.IgnoreConfigCheck, - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - Cache: spec.ClusterPath(clusterName, spec.TempConfigPath), - }). - BuildAsStep(fmt.Sprintf(" - Refresh config %s -> %s", inst.ComponentName(), inst.ID())) - refreshConfigTasks = append(refreshConfigTasks, t) - }) - - monitorConfigTasks := refreshMonitoredConfigTask(clusterName, uniqueHosts, topo.GlobalOptions, topo.MonitoredOptions) - - // handle dir scheme changes - if hasImported { - if err := spec.HandleImportPathMigration(clusterName); err != nil { - return task.NewBuilder().Build(), err - } - } - - tb := task.NewBuilder(). - SSHKeySet( - spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - ParallelStep("+ Refresh instance configs", refreshConfigTasks...). - ParallelStep("+ Refresh monitor configs", monitorConfigTasks...) - if !skipRestart { - tb = tb.ClusterOperate(metadata.Topology, operator.UpgradeOperation, options) - } - return tb.Build(), nil -} - -func refreshMonitoredConfigTask( - clusterName string, - uniqueHosts map[string]hostInfo, // host -> ssh-port, os, arch - globalOptions spec.GlobalOptions, - monitoredOptions spec.MonitoredOptions, -) []*task.StepDisplay { - tasks := []*task.StepDisplay{} - // monitoring agents - for _, comp := range []string{spec.ComponentNodeExporter, spec.ComponentBlackboxExporter} { - for host, info := range uniqueHosts { - deployDir := clusterutil.Abs(globalOptions.User, monitoredOptions.DeployDir) - // data dir would be empty for components which don't need it - dataDir := monitoredOptions.DataDir - // the default data_dir is relative to deploy_dir - if dataDir != "" && !strings.HasPrefix(dataDir, "/") { - dataDir = filepath.Join(deployDir, dataDir) - } - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(globalOptions.User, monitoredOptions.LogDir) - // Generate configs - t := task.NewBuilder(). - UserSSH(host, info.ssh, globalOptions.User, gOpt.SSHTimeout, gOpt.NativeSSH). - MonitoredConfig( - clusterName, - comp, - host, - globalOptions.ResourceControl, - monitoredOptions, - globalOptions.User, - meta.DirPaths{ - Deploy: deployDir, - Data: []string{dataDir}, - Log: logDir, - Cache: spec.ClusterPath(clusterName, spec.TempConfigPath), - }, - ). - BuildAsStep(fmt.Sprintf(" - Refresh config %s -> %s", comp, host)) - tasks = append(tasks, t) - } - } - return tasks -} - func validRoles(roles []string) error { for _, r := range roles { match := false diff --git a/components/cluster/command/restart.go b/components/cluster/command/restart.go index d058ac4fba..7f5b5491ff 100644 --- a/components/cluster/command/restart.go +++ b/components/cluster/command/restart.go @@ -14,16 +14,6 @@ package command import ( - "errors" - - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" "github.com/spf13/cobra" ) @@ -43,40 +33,7 @@ func newRestartCmd() *cobra.Command { clusterName := args[0] teleCommand = append(teleCommand, scrubClusterName(clusterName)) - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot restart non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return err - } - - t := task.NewBuilder(). - SSHKeySet( - spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - ClusterOperate(metadata.Topology, operator.RestartOperation, gOpt). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - log.Infof("Restarted cluster `%s` successfully", clusterName) - - return nil + return manager.RestartCluster(clusterName, gOpt) }, } diff --git a/components/cluster/command/root.go b/components/cluster/command/root.go index 0713c6e129..295377adec 100644 --- a/components/cluster/command/root.go +++ b/components/cluster/command/root.go @@ -25,6 +25,7 @@ import ( "github.com/google/uuid" "github.com/joomcode/errorx" "github.com/pingcap/tiup/pkg/cliutil" + "github.com/pingcap/tiup/pkg/cluster" "github.com/pingcap/tiup/pkg/cluster/flags" operator "github.com/pingcap/tiup/pkg/cluster/operation" "github.com/pingcap/tiup/pkg/cluster/report" @@ -35,7 +36,6 @@ import ( "github.com/pingcap/tiup/pkg/localdata" "github.com/pingcap/tiup/pkg/logger" "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" "github.com/pingcap/tiup/pkg/repository" "github.com/pingcap/tiup/pkg/telemetry" "github.com/pingcap/tiup/pkg/version" @@ -50,7 +50,8 @@ var ( skipConfirm bool ) -var tidbSpec *meta.SpecManager +var tidbSpec *spec.SpecManager +var manager *cluster.Manager func scrubClusterName(n string) string { return "cluster_" + telemetry.HashReport(n) @@ -98,6 +99,8 @@ func init() { } tidbSpec = spec.GetSpecManager() + manager = cluster.NewManager("tidb", tidbSpec) + logger.EnableAuditLog(spec.AuditDir()) // Running in other OS/ARCH Should be fine we only download manifest file. env, err = tiupmeta.InitEnv(repository.Options{ diff --git a/components/cluster/command/scale_in.go b/components/cluster/command/scale_in.go index d01db7a968..ec12a333bc 100644 --- a/components/cluster/command/scale_in.go +++ b/components/cluster/command/scale_in.go @@ -14,21 +14,9 @@ package command import ( - "errors" - "strings" - - "github.com/fatih/color" - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" operator "github.com/pingcap/tiup/pkg/cluster/operation" "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" - "github.com/pingcap/tiup/pkg/set" "github.com/spf13/cobra" ) @@ -43,27 +31,28 @@ func newScaleInCmd() *cobra.Command { clusterName := args[0] teleCommand = append(teleCommand, scrubClusterName(clusterName)) - if !skipConfirm { - if err := cliutil.PromptForConfirmOrAbortError( - "This operation will delete the %s nodes in `%s` and all their data.\nDo you want to continue? [y/N]:", - strings.Join(gOpt.Nodes, ","), - color.HiYellowString(clusterName)); err != nil { - return err - } - if gOpt.Force { - if err := cliutil.PromptForConfirmOrAbortError( - "Forcing scale in is unsafe and may result in data lost for stateful components.\nDo you want to continue? [y/N]:", - ); err != nil { - return err - } + scale := func(b *task.Builder, imetadata spec.Metadata) { + metadata := imetadata.(*spec.ClusterMeta) + if !gOpt.Force { + b.ClusterOperate(metadata.Topology, operator.ScaleInOperation, gOpt). + UpdateMeta(clusterName, metadata, operator.AsyncNodes(metadata.Topology, gOpt.Nodes, false)). + UpdateTopology(clusterName, metadata, operator.AsyncNodes(metadata.Topology, gOpt.Nodes, false)) + } else { + b.ClusterOperate(metadata.Topology, operator.ScaleInOperation, gOpt). + UpdateMeta(clusterName, metadata, gOpt.Nodes). + UpdateTopology(clusterName, metadata, gOpt.Nodes) } - - log.Infof("Scale-in nodes...") } - logger.EnableAuditLog() - return scaleIn(clusterName, gOpt) + return manager.ScaleIn( + clusterName, + skipConfirm, + gOpt.SSHTimeout, + gOpt.Force, + gOpt.Nodes, + scale, + ) }, } @@ -75,109 +64,3 @@ func newScaleInCmd() *cobra.Command { return cmd } - -func scaleIn(clusterName string, options operator.Options) error { - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot scale-in non-exists cluster %s", clusterName) - } - - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - // ignore conflict check error, node may be deployed by former version - // that lack of some certain conflict checks - return err - } - - // Regenerate configuration - var regenConfigTasks []task.Task - hasImported := false - deletedNodes := set.NewStringSet(options.Nodes...) - for _, component := range metadata.Topology.ComponentsByStartOrder() { - for _, instance := range component.Instances() { - if deletedNodes.Exist(instance.ID()) { - continue - } - deployDir := clusterutil.Abs(metadata.User, instance.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(metadata.User, instance.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(metadata.User, instance.LogDir()) - - // Download and copy the latest component to remote if the cluster is imported from Ansible - tb := task.NewBuilder() - if instance.IsImported() { - switch compName := instance.ComponentName(); compName { - case spec.ComponentGrafana, spec.ComponentPrometheus, spec.ComponentAlertManager: - version := spec.ComponentVersion(compName, metadata.Version) - tb.Download(compName, instance.OS(), instance.Arch(), version). - CopyComponent( - compName, - instance.OS(), - instance.Arch(), - version, - "", // use default srcPath - instance.GetHost(), - deployDir, - ) - } - hasImported = true - } - - t := tb.InitConfig(clusterName, - metadata.Version, - instance, - metadata.User, - true, // always ignore config check result in scale in - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - Cache: spec.ClusterPath(clusterName, spec.TempConfigPath), - }, - ).Build() - regenConfigTasks = append(regenConfigTasks, t) - } - } - - // handle dir scheme changes - if hasImported { - if err := spec.HandleImportPathMigration(clusterName); err != nil { - return err - } - } - - b := task.NewBuilder(). - SSHKeySet( - spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout) - - if !options.Force { - b.ClusterOperate(metadata.Topology, operator.ScaleInOperation, options). - UpdateMeta(clusterName, metadata, operator.AsyncNodes(metadata.Topology, options.Nodes, false)). - UpdateTopology(clusterName, metadata, operator.AsyncNodes(metadata.Topology, options.Nodes, false)) - } else { - b.ClusterOperate(metadata.Topology, operator.ScaleInOperation, options). - UpdateMeta(clusterName, metadata, options.Nodes). - UpdateTopology(clusterName, metadata, options.Nodes) - } - - t := b.Parallel(regenConfigTasks...).Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - log.Infof("Scaled cluster `%s` in successfully", clusterName) - - return nil -} diff --git a/components/cluster/command/scale_out.go b/components/cluster/command/scale_out.go index dcf4e418b2..581e151373 100644 --- a/components/cluster/command/scale_out.go +++ b/components/cluster/command/scale_out.go @@ -15,37 +15,21 @@ package command import ( "context" - "errors" "io/ioutil" "path/filepath" - "strings" - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cliutil/prepare" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" + "github.com/pingcap/tiup/pkg/cluster" operator "github.com/pingcap/tiup/pkg/cluster/operation" "github.com/pingcap/tiup/pkg/cluster/report" "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" - "github.com/pingcap/tiup/pkg/set" tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) -type scaleOutOptions struct { - user string // username to login to the SSH server - identityFile string // path to the private key file - usePassword bool // use password instead of identity file for ssh connection -} - func newScaleOutCmd() *cobra.Command { - opt := scaleOutOptions{ - identityFile: filepath.Join(tiuputils.UserHome(), ".ssh", "id_rsa"), + opt := cluster.ScaleOutOptions{ + IdentityFile: filepath.Join(tiuputils.UserHome(), ".ssh", "id_rsa"), } cmd := &cobra.Command{ Use: "scale-out ", @@ -56,108 +40,33 @@ func newScaleOutCmd() *cobra.Command { return cmd.Help() } - logger.EnableAuditLog() clusterName := args[0] teleCommand = append(teleCommand, scrubClusterName(clusterName)) - return scaleOut(args[0], args[1], opt) - }, - } - - cmd.Flags().StringVarP(&opt.user, "user", "u", tiuputils.CurrentUser(), "The user name to login via SSH. The user must has root (or sudo) privilege.") - cmd.Flags().StringVarP(&opt.identityFile, "identity_file", "i", opt.identityFile, "The path of the SSH identity file. If specified, public key authentication will be used.") - cmd.Flags().BoolVarP(&opt.usePassword, "password", "p", false, "Use password of target hosts. If specified, password authentication will be used.") - return cmd -} - -func scaleOut(clusterName, topoFile string, opt scaleOutOptions) error { - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot scale-out non-exists cluster %s", clusterName) - } - - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil { // not allowing validation errors - return err - } - - // Inherit existing global configuration. We must assign the inherited values before unmarshalling - // because some default value rely on the global options and monitored options. - var newPart = spec.Specification{ - GlobalOptions: metadata.Topology.GlobalOptions, - MonitoredOptions: metadata.Topology.MonitoredOptions, - ServerConfigs: metadata.Topology.ServerConfigs, - } - - // The no tispark master error is ignored, as if the tispark master is removed from the topology - // file for some reason (manual edit, for example), it is still possible to scale-out it to make - // the whole topology back to normal state. - if err := clusterutil.ParseTopologyYaml(topoFile, &newPart); err != nil && - !errors.Is(perrs.Cause(err), spec.ErrNoTiSparkMaster) { - return err - } - - if err := validateNewTopo(&newPart); err != nil { - return err - } - - if data, err := ioutil.ReadFile(topoFile); err == nil { - teleTopology = string(data) - } - - // Abort scale out operation if the merged topology is invalid - mergedTopo := metadata.Topology.Merge(&newPart) - if err := mergedTopo.Validate(); err != nil { - return err - } - - if err := prepare.CheckClusterPortConflict(tidbSpec, clusterName, mergedTopo); err != nil { - return err - } - if err := prepare.CheckClusterDirConflict(tidbSpec, clusterName, mergedTopo); err != nil { - return err - } - - patchedComponents := set.NewStringSet() - newPart.IterInstance(func(instance spec.Instance) { - if tiuputils.IsExist(spec.ClusterPath(clusterName, spec.PatchDirName, instance.ComponentName()+".tar.gz")) { - patchedComponents.Insert(instance.ComponentName()) - } - }) - - if !skipConfirm { - // patchedComponents are components that have been patched and overwrited - if err := confirmTopology(clusterName, metadata.Version, &newPart, patchedComponents); err != nil { - return err - } - } - - sshConnProps, err := cliutil.ReadIdentityFileOrPassword(opt.identityFile, opt.usePassword) - if err != nil { - return err - } - - // Build the scale out tasks - t, err := buildScaleOutTask(clusterName, metadata, mergedTopo, opt, sshConnProps, &newPart, patchedComponents, gOpt.OptTimeout) - if err != nil { - return err - } + topoFile := args[1] + if data, err := ioutil.ReadFile(topoFile); err == nil { + teleTopology = string(data) + } - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) + return manager.ScaleOut( + clusterName, + topoFile, + postScaleOutHook, + final, + opt, + skipConfirm, + gOpt.OptTimeout, + gOpt.SSHTimeout, + gOpt.NativeSSH, + ) + }, } - log.Infof("Scaled cluster `%s` out successfully", clusterName) + cmd.Flags().StringVarP(&opt.User, "user", "u", tiuputils.CurrentUser(), "The user name to login via SSH. The user must has root (or sudo) privilege.") + cmd.Flags().StringVarP(&opt.IdentityFile, "identity_file", "i", opt.IdentityFile, "The path of the SSH identity file. If specified, public key authentication will be used.") + cmd.Flags().BoolVarP(&opt.UsePassword, "password", "p", false, "Use password of target hosts. If specified, password authentication will be used.") - return nil + return cmd } // Deprecated @@ -169,179 +78,11 @@ func convertStepDisplaysToTasks(t []*task.StepDisplay) []task.Task { return tasks } -func buildScaleOutTask( - clusterName string, - metadata *spec.ClusterMeta, - mergedTopo *spec.Specification, - opt scaleOutOptions, - sshConnProps *cliutil.SSHConnectionProps, - newPart *spec.Specification, - patchedComponents set.StringSet, - timeout int64, -) (task.Task, error) { - var ( - envInitTasks []task.Task // tasks which are used to initialize environment - downloadCompTasks []task.Task // tasks which are used to download components - deployCompTasks []task.Task // tasks which are used to copy components to remote host - refreshConfigTasks []task.Task // tasks which are used to refresh configuration - ) - - // Initialize the environments - initializedHosts := set.NewStringSet() - metadata.Topology.IterInstance(func(instance spec.Instance) { - initializedHosts.Insert(instance.GetHost()) - }) - // uninitializedHosts are hosts which haven't been initialized yet - uninitializedHosts := make(map[string]hostInfo) // host -> ssh-port, os, arch - newPart.IterInstance(func(instance spec.Instance) { - if host := instance.GetHost(); !initializedHosts.Exist(host) { - if _, found := uninitializedHosts[host]; found { - return - } - - uninitializedHosts[host] = hostInfo{ - ssh: instance.GetSSHPort(), - os: instance.OS(), - arch: instance.Arch(), - } - - var dirs []string - globalOptions := metadata.Topology.GlobalOptions - for _, dir := range []string{globalOptions.DeployDir, globalOptions.DataDir, globalOptions.LogDir} { - for _, dirname := range strings.Split(dir, ",") { - if dirname == "" { - continue - } - dirs = append(dirs, clusterutil.Abs(globalOptions.User, dirname)) - } - } - t := task.NewBuilder(). - RootSSH( - instance.GetHost(), - instance.GetSSHPort(), - opt.user, - sshConnProps.Password, - sshConnProps.IdentityFile, - sshConnProps.IdentityFilePassphrase, - gOpt.SSHTimeout, - gOpt.NativeSSH, - ). - EnvInit(instance.GetHost(), metadata.User). - Mkdir(globalOptions.User, instance.GetHost(), dirs...). - Build() - envInitTasks = append(envInitTasks, t) - } - }) - - // Download missing component - downloadCompTasks = convertStepDisplaysToTasks(prepare.BuildDownloadCompTasks(metadata.Version, newPart)) - - // Deploy the new topology and refresh the configuration - newPart.IterInstance(func(inst spec.Instance) { - version := spec.ComponentVersion(inst.ComponentName(), metadata.Version) - deployDir := clusterutil.Abs(metadata.User, inst.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(metadata.User, inst.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(metadata.User, inst.LogDir()) - - // Deploy component - tb := task.NewBuilder(). - UserSSH(inst.GetHost(), inst.GetSSHPort(), metadata.User, gOpt.SSHTimeout, gOpt.NativeSSH). - Mkdir(metadata.User, inst.GetHost(), - deployDir, logDir, - filepath.Join(deployDir, "bin"), - filepath.Join(deployDir, "conf"), - filepath.Join(deployDir, "scripts")). - Mkdir(metadata.User, inst.GetHost(), dataDirs...) - - srcPath := "" - if patchedComponents.Exist(inst.ComponentName()) { - srcPath = spec.ClusterPath(clusterName, spec.PatchDirName, inst.ComponentName()+".tar.gz") - } - - // copy dependency component if needed - switch inst.ComponentName() { - case spec.ComponentTiSpark: - tb = tb.DeploySpark(inst, version, srcPath, deployDir) - default: - tb.CopyComponent( - inst.ComponentName(), - inst.OS(), - inst.Arch(), - version, - srcPath, - inst.GetHost(), - deployDir, - ) - } - - t := tb.ScaleConfig(clusterName, - metadata.Version, - metadata.Topology, - inst, - metadata.User, - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - }, - ).Build() - deployCompTasks = append(deployCompTasks, t) - }) - - hasImported := false - - mergedTopo.IterInstance(func(inst spec.Instance) { - deployDir := clusterutil.Abs(metadata.User, inst.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(metadata.User, inst.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(metadata.User, inst.LogDir()) - - // Download and copy the latest component to remote if the cluster is imported from Ansible - tb := task.NewBuilder() - if inst.IsImported() { - switch compName := inst.ComponentName(); compName { - case spec.ComponentGrafana, spec.ComponentPrometheus, spec.ComponentAlertManager: - version := spec.ComponentVersion(compName, metadata.Version) - tb.Download(compName, inst.OS(), inst.Arch(), version). - CopyComponent( - compName, - inst.OS(), - inst.Arch(), - version, - "", // use default srcPath - inst.GetHost(), - deployDir, - ) - } - hasImported = true - } - - // Refresh all configuration - t := tb.InitConfig(clusterName, - metadata.Version, - inst, - metadata.User, - true, // always ignore config check result in scale out - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - Cache: spec.ClusterPath(clusterName, spec.TempConfigPath), - }, - ).Build() - refreshConfigTasks = append(refreshConfigTasks, t) - }) - - // handle dir scheme changes - if hasImported { - if err := spec.HandleImportPathMigration(clusterName); err != nil { - return task.NewBuilder().Build(), err - } - } +func final(builder *task.Builder, name string, meta spec.Metadata) { + builder.UpdateTopology(name, meta.(*spec.ClusterMeta), nil) +} +func postScaleOutHook(builder *task.Builder, newPart spec.Topology) { nodeInfoTask := task.NewBuilder().Func("Check status", func(ctx *task.Context) error { var err error teleNodeInfos, err = operator.GetNodeInfo(context.Background(), ctx, newPart) @@ -350,61 +91,7 @@ func buildScaleOutTask( return nil }).BuildAsStep("Check status").SetHidden(true) - // Deploy monitor relevant components to remote - dlTasks, dpTasks := buildMonitoredDeployTask( - clusterName, - uninitializedHosts, - metadata.Topology.GlobalOptions, - metadata.Topology.MonitoredOptions, - metadata.Version, - ) - downloadCompTasks = append(downloadCompTasks, convertStepDisplaysToTasks(dlTasks)...) - deployCompTasks = append(deployCompTasks, convertStepDisplaysToTasks(dpTasks)...) - - builder := task.NewBuilder(). - SSHKeySet( - spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - Parallel(downloadCompTasks...). - Parallel(envInitTasks...). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - Parallel(deployCompTasks...) - if report.Enable() { builder.Parallel(convertStepDisplaysToTasks([]*task.StepDisplay{nodeInfoTask})...) } - - // TODO: find another way to make sure current cluster started - builder.ClusterOperate(metadata.Topology, operator.StartOperation, operator.Options{OptTimeout: timeout}). - ClusterSSH(newPart, metadata.User, gOpt.SSHTimeout). - Func("save meta", func(_ *task.Context) error { - metadata.Topology = mergedTopo - return spec.SaveClusterMeta(clusterName, metadata) - }). - ClusterOperate(newPart, operator.StartOperation, operator.Options{OptTimeout: timeout}). - Parallel(refreshConfigTasks...). - ClusterOperate(metadata.Topology, operator.RestartOperation, operator.Options{ - Roles: []string{spec.ComponentPrometheus}, - OptTimeout: timeout, - }). - UpdateTopology(clusterName, metadata, nil) - - return builder.Build(), nil - -} - -// validateNewTopo checks the new part of scale-out topology to make sure it's supported -func validateNewTopo(topo *spec.Specification) (err error) { - err = nil - topo.IterInstance(func(instance spec.Instance) { - // check for "imported" parameter, it can not be true when scaling out - if instance.IsImported() { - err = perrs.New( - "'imported' is set to 'true' for new instance, this is only used " + - "for instances imported from tidb-ansible and make no sense when " + - "scaling out, please delete the line or set it to 'false' for new instances") - return - } - }) - return err } diff --git a/components/cluster/command/scale_out_test.go b/components/cluster/command/scale_out_test.go deleted file mode 100644 index 06e12775ad..0000000000 --- a/components/cluster/command/scale_out_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package command - -import ( - "github.com/pingcap/check" - "github.com/pingcap/tiup/pkg/cluster/spec" - "gopkg.in/yaml.v2" -) - -type scaleOutSuite struct{} - -var _ = check.Suite(&scaleOutSuite{}) - -func (s *scaleOutSuite) TestValidateNewTopo(c *check.C) { - topo := spec.Specification{} - err := yaml.Unmarshal([]byte(` -global: - user: "test1" - ssh_port: 220 - deploy_dir: "test-deploy" - data_dir: "test-data" -tidb_servers: - - host: 172.16.5.138 - deploy_dir: "tidb-deploy" -pd_servers: - - host: 172.16.5.53 - data_dir: "pd-data" -`), &topo) - c.Assert(err, check.IsNil) - err = validateNewTopo(&topo) - c.Assert(err, check.IsNil) - - topo = spec.Specification{} - err = yaml.Unmarshal([]byte(` -tidb_servers: - - host: 172.16.5.138 - imported: true - deploy_dir: "tidb-deploy" -pd_servers: - - host: 172.16.5.53 - data_dir: "pd-data" -`), &topo) - c.Assert(err, check.IsNil) - err = validateNewTopo(&topo) - c.Assert(err, check.NotNil) - - topo = spec.Specification{} - err = yaml.Unmarshal([]byte(` -global: - user: "test3" - deploy_dir: "test-deploy" - data_dir: "test-data" -pd_servers: - - host: 172.16.5.53 - imported: true -`), &topo) - c.Assert(err, check.IsNil) - err = validateNewTopo(&topo) - c.Assert(err, check.NotNil) -} diff --git a/components/cluster/command/start.go b/components/cluster/command/start.go index 3b4f78f982..38ade84148 100644 --- a/components/cluster/command/start.go +++ b/components/cluster/command/start.go @@ -14,16 +14,8 @@ package command import ( - "errors" - - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - operator "github.com/pingcap/tiup/pkg/cluster/operation" "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" "github.com/spf13/cobra" ) @@ -43,16 +35,10 @@ func newStartCmd() *cobra.Command { clusterName := args[0] teleCommand = append(teleCommand, scrubClusterName(clusterName)) - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot start non-exists cluster %s", clusterName) - } - - return startCluster(clusterName, gOpt) + return manager.StartCluster(clusterName, gOpt, func(b *task.Builder, metadata spec.Metadata) { + tidbMeta := metadata.(*spec.ClusterMeta) + b.UpdateTopology(clusterName, tidbMeta, nil) + }) }, } @@ -61,33 +47,3 @@ func newStartCmd() *cobra.Command { return cmd } - -func startCluster(clusterName string, options operator.Options) error { - logger.EnableAuditLog() - log.Infof("Starting cluster %s...", clusterName) - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return err - } - - t := task.NewBuilder(). - SSHKeySet( - spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - ClusterOperate(metadata.Topology, operator.StartOperation, options). - UpdateTopology(clusterName, metadata, nil). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - log.Infof("Started cluster `%s` successfully", clusterName) - - return nil -} diff --git a/components/cluster/command/stop.go b/components/cluster/command/stop.go index ba1f1c7cca..9a03e40d65 100644 --- a/components/cluster/command/stop.go +++ b/components/cluster/command/stop.go @@ -14,16 +14,6 @@ package command import ( - "errors" - - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" "github.com/spf13/cobra" ) @@ -43,40 +33,7 @@ func newStopCmd() *cobra.Command { clusterName := args[0] teleCommand = append(teleCommand, scrubClusterName(clusterName)) - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot stop non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return err - } - - t := task.NewBuilder(). - SSHKeySet( - spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - ClusterOperate(metadata.Topology, operator.StopOperation, gOpt). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - log.Infof("Stopped cluster `%s` successfully", clusterName) - - return nil + return manager.StopCluster(clusterName, gOpt) }, } diff --git a/components/cluster/command/upgrade.go b/components/cluster/command/upgrade.go index 45152b7216..ad81fcb9d5 100644 --- a/components/cluster/command/upgrade.go +++ b/components/cluster/command/upgrade.go @@ -14,22 +14,7 @@ package command import ( - "errors" - "fmt" - "os" - - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" - "github.com/pingcap/tiup/pkg/version" "github.com/spf13/cobra" - "golang.org/x/mod/semver" ) func newUpgradeCmd() *cobra.Command { @@ -41,12 +26,12 @@ func newUpgradeCmd() *cobra.Command { return cmd.Help() } - logger.EnableAuditLog() clusterName := args[0] version := args[1] teleCommand = append(teleCommand, scrubClusterName(clusterName)) teleCommand = append(teleCommand, version) - return upgrade(clusterName, version, gOpt) + + return manager.Upgrade(clusterName, version, gOpt) }, } cmd.Flags().BoolVar(&gOpt.Force, "force", false, "Force upgrade without transferring PD leader") @@ -55,165 +40,3 @@ func newUpgradeCmd() *cobra.Command { return cmd } - -func versionCompare(curVersion, newVersion string) error { - // Can always upgrade to 'nightly' event the current version is 'nightly' - if newVersion == version.NightlyVersion { - return nil - } - - switch semver.Compare(curVersion, newVersion) { - case -1: - return nil - case 0, 1: - return perrs.Errorf("please specify a higher version than %s", curVersion) - default: - return perrs.Errorf("unreachable") - } -} - -func upgrade(clusterName, clusterVersion string, opt operator.Options) error { - exist, err := tidbSpec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot upgrade non-exists cluster %s", clusterName) - } - - metadata, err := spec.ClusterMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return err - } - - var ( - downloadCompTasks []task.Task // tasks which are used to download components - copyCompTasks []task.Task // tasks which are used to copy components to remote host - - uniqueComps = map[string]struct{}{} - ) - - if err := versionCompare(metadata.Version, clusterVersion); err != nil { - return err - } - - hasImported := false - for _, comp := range metadata.Topology.ComponentsByUpdateOrder() { - for _, inst := range comp.Instances() { - version := spec.ComponentVersion(inst.ComponentName(), clusterVersion) - if version == "" { - return perrs.Errorf("unsupported component: %v", inst.ComponentName()) - } - compInfo := componentInfo{ - component: inst.ComponentName(), - version: version, - } - - // Download component from repository - key := fmt.Sprintf("%s-%s-%s-%s", compInfo.component, compInfo.version, inst.OS(), inst.Arch()) - if _, found := uniqueComps[key]; !found { - uniqueComps[key] = struct{}{} - t := task.NewBuilder(). - Download(inst.ComponentName(), inst.OS(), inst.Arch(), version). - Build() - downloadCompTasks = append(downloadCompTasks, t) - } - - deployDir := clusterutil.Abs(metadata.User, inst.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(metadata.User, inst.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(metadata.User, inst.LogDir()) - - // Deploy component - tb := task.NewBuilder() - if inst.IsImported() { - switch inst.ComponentName() { - case spec.ComponentPrometheus, spec.ComponentGrafana, spec.ComponentAlertManager: - tb.CopyComponent( - inst.ComponentName(), - inst.OS(), - inst.Arch(), - version, - "", // use default srcPath - inst.GetHost(), - deployDir, - ) - } - hasImported = true - } - - // backup files of the old version - tb = tb.BackupComponent(inst.ComponentName(), metadata.Version, inst.GetHost(), deployDir) - - // copy dependency component if needed - switch inst.ComponentName() { - case spec.ComponentTiSpark: - tb = tb.DeploySpark(inst, version, "" /* default srcPath */, deployDir) - default: - tb = tb.CopyComponent( - inst.ComponentName(), - inst.OS(), - inst.Arch(), - version, - "", // use default srcPath - inst.GetHost(), - deployDir, - ) - } - - tb.InitConfig( - clusterName, - clusterVersion, - inst, - metadata.User, - opt.IgnoreConfigCheck, - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - Cache: spec.ClusterPath(clusterName, spec.TempConfigPath), - }, - ) - copyCompTasks = append(copyCompTasks, tb.Build()) - } - } - - // handle dir scheme changes - if hasImported { - if err := spec.HandleImportPathMigration(clusterName); err != nil { - return err - } - } - - t := task.NewBuilder(). - SSHKeySet( - spec.ClusterPath(clusterName, "ssh", "id_rsa"), - spec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - Parallel(downloadCompTasks...). - Parallel(copyCompTasks...). - ClusterOperate(metadata.Topology, operator.UpgradeOperation, opt). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - metadata.Version = clusterVersion - if err := spec.SaveClusterMeta(clusterName, metadata); err != nil { - return perrs.Trace(err) - } - if err := os.RemoveAll(spec.ClusterPath(clusterName, "patch")); err != nil { - return perrs.Trace(err) - } - - log.Infof("Upgraded cluster `%s` successfully", clusterName) - - return nil -} diff --git a/components/cluster/command/upgrade_test.go b/components/cluster/command/upgrade_test.go deleted file mode 100644 index db9e8980c5..0000000000 --- a/components/cluster/command/upgrade_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package command - -import ( - "github.com/pingcap/check" -) - -type upgradeSuite struct{} - -var _ = check.Suite(&upgradeSuite{}) - -func (s *upgradeSuite) TestVersionCompare(c *check.C) { - var err error - - err = versionCompare("v4.0.0", "v4.0.1") - c.Assert(err, check.IsNil) - - err = versionCompare("v4.0.1", "v4.0.0") - c.Assert(err, check.NotNil) - - err = versionCompare("v4.0.0", "nightly") - c.Assert(err, check.IsNil) - - err = versionCompare("nightly", "nightly") - c.Assert(err, check.IsNil) -} diff --git a/components/dm/command/audit.go b/components/dm/command/audit.go index 4b0f87f445..6b0a682ec4 100644 --- a/components/dm/command/audit.go +++ b/components/dm/command/audit.go @@ -13,22 +13,9 @@ package command -/* import ( - "bufio" - "fmt" - "io/ioutil" - "os" - "sort" - "strings" - "time" - - "github.com/fatih/color" - "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/base52" - "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cluster/meta" - tiuputils "github.com/pingcap/tiup/pkg/utils" + "github.com/pingcap/tiup/pkg/cluster/audit" + cspec "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/spf13/cobra" ) @@ -39,9 +26,9 @@ func newAuditCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { switch len(args) { case 0: - return showAuditList() + return audit.ShowAuditList(cspec.AuditDir()) case 1: - return showAuditLog(args[0]) + return audit.ShowAuditLog(cspec.AuditDir(), args[0]) default: return cmd.Help() } @@ -49,78 +36,3 @@ func newAuditCmd() *cobra.Command { } return cmd } - -func showAuditList() error { - firstLine := func(fileName string) (string, error) { - file, err := os.Open(meta.ProfilePath(meta.TiOpsAuditDir, fileName)) - if err != nil { - return "", errors.Trace(err) - } - defer file.Close() - - scanner := bufio.NewScanner(file) - if scanner.Scan() { - return scanner.Text(), nil - } - return "", errors.New("unknown audit log format") - } - - auditDir := meta.ProfilePath(meta.TiOpsAuditDir) - // Header - clusterTable := [][]string{{"ID", "Time", "Command"}} - fileInfos, err := ioutil.ReadDir(auditDir) - if err != nil && !os.IsNotExist(err) { - return err - } - for _, fi := range fileInfos { - if fi.IsDir() { - continue - } - ts, err := base52.Decode(fi.Name()) - if err != nil { - continue - } - t := time.Unix(ts, 0) - cmd, err := firstLine(fi.Name()) - if err != nil { - continue - } - clusterTable = append(clusterTable, []string{ - fi.Name(), - t.Format(time.RFC3339), - cmd, - }) - } - - sort.Slice(clusterTable[1:], func(i, j int) bool { - return clusterTable[i+1][1] > clusterTable[j+1][1] - }) - - cliutil.PrintTable(clusterTable, true) - return nil -} - -func showAuditLog(auditID string) error { - path := meta.ProfilePath(meta.TiOpsAuditDir, auditID) - if tiuputils.IsNotExist(path) { - return errors.Errorf("cannot find the audit log '%s'", auditID) - } - - ts, err := base52.Decode(auditID) - if err != nil { - return errors.Annotatef(err, "unrecognized audit id '%s'", auditID) - } - - content, err := ioutil.ReadFile(path) - if err != nil { - return errors.Trace(err) - } - - t := time.Unix(ts, 0) - hint := fmt.Sprintf("- OPERATION TIME: %s -", t.Format("2006-01-02T15:04:05")) - line := strings.Repeat("-", len(hint)) - _, _ = os.Stdout.WriteString(color.MagentaString("%s\n%s\n%s\n", line, hint, line)) - _, _ = os.Stdout.Write(content) - return nil -} -*/ diff --git a/components/dm/command/deploy.go b/components/dm/command/deploy.go index 3ea00fba89..eb6a512cff 100644 --- a/components/dm/command/deploy.go +++ b/components/dm/command/deploy.go @@ -14,54 +14,17 @@ package command import ( - "fmt" - "os" "path" - "path/filepath" - "strings" - cspec "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/dm/spec" - "github.com/pingcap/tiup/pkg/meta" - - "github.com/fatih/color" - "github.com/joomcode/errorx" - "github.com/pingcap/errors" "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cliutil/prepare" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/errutil" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/set" + "github.com/pingcap/tiup/pkg/cluster" tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) -var ( - errNSDeploy = errNS.NewSubNamespace("deploy") - errDeployNameDuplicate = errNSDeploy.NewType("name_dup", errutil.ErrTraitPreCheck) -) - -type ( - deployOptions struct { - user string // username to login to the SSH server - identityFile string // path to the private key file - usePassword bool // use password instead of identity file for ssh connection - } - - hostInfo struct { - ssh int // ssh port of host - os string // operating system - arch string // cpu architecture - // vendor string - } -) - func newDeploy() *cobra.Command { - opt := deployOptions{ - identityFile: path.Join(tiuputils.UserHome(), ".ssh", "id_rsa"), + opt := cluster.DeployOptions{ + IdentityFile: path.Join(tiuputils.UserHome(), ".ssh", "id_rsa"), } cmd := &cobra.Command{ Use: "deploy ", @@ -77,222 +40,28 @@ func newDeploy() *cobra.Command { return nil } - logger.EnableAuditLog() - return deploy(args[0], args[1], args[2], opt) - }, - } - - cmd.Flags().StringVar(&opt.user, "user", tiuputils.CurrentUser(), "The user name to login via SSH. The user must has root (or sudo) privilege.") - cmd.Flags().StringVarP(&opt.identityFile, "identity_file", "i", opt.identityFile, "The path of the SSH identity file. If specified, public key authentication will be used.") - cmd.Flags().BoolVarP(&opt.usePassword, "password", "p", false, "Use password of target hosts. If specified, password authentication will be used.") - - return cmd -} + clusterName := args[0] + version := args[1] + topoFile := args[2] -func confirmTopology(clusterName, version string, topo *spec.DMTopologySpecification, patchedRoles set.StringSet) error { - log.Infof("Please confirm your topology:") - - cyan := color.New(color.FgCyan, color.Bold) - fmt.Printf("DM Cluster: %s\n", cyan.Sprint(clusterName)) - fmt.Printf("DM Version: %s\n", cyan.Sprint(version)) - - clusterTable := [][]string{ - // Header - {"Type", "Host", "Ports", "OS/Arch", "Directories"}, - } - - topo.IterInstance(func(instance spec.Instance) { - comp := instance.ComponentName() - if patchedRoles.Exist(comp) { - comp = comp + " (patched)" - } - clusterTable = append(clusterTable, []string{ - comp, - instance.GetHost(), - clusterutil.JoinInt(instance.UsedPorts(), "/"), - cliutil.OsArch(instance.OS(), instance.Arch()), - strings.Join(instance.UsedDirs(), ","), - }) - }) - - cliutil.PrintTable(clusterTable, true) - - log.Warnf("Attention:") - log.Warnf(" 1. If the topology is not what you expected, check your yaml file.") - log.Warnf(" 2. Please confirm there is no port/directory conflicts in same host.") - if len(patchedRoles) != 0 { - log.Errorf(" 3. The component marked as `patched` has been replaced by previours patch command.") - } - - return cliutil.PromptForConfirmOrAbortError("Do you want to continue? [y/N]: ") -} - -func deploy(clusterName, clusterVersion, topoFile string, opt deployOptions) error { - if err := clusterutil.ValidateClusterNameOrError(clusterName); err != nil { - return err - } - - exist, err := dmspec.Exist(clusterName) - if err != nil { - return errors.AddStack(err) - } - - if exist { - // FIXME: When change to use args, the suggestion text need to be updated. - return errDeployNameDuplicate. - New("Cluster name '%s' is duplicated", clusterName). - WithProperty(cliutil.SuggestionFromFormat("Please specify another cluster name")) - } - - var topo spec.DMTopologySpecification - if err := clusterutil.ParseTopologyYaml(topoFile, &topo); err != nil { - return err - } - - /* - if err := prepare.CheckClusterPortConflict(clusterName, &topo); err != nil { - return err - } - if err := prepare.CheckClusterDirConflict(clusterName, &topo); err != nil { - return err - } - */ - - if !skipConfirm { - if err := confirmTopology(clusterName, clusterVersion, &topo, set.NewStringSet()); err != nil { - return err - } - } - - sshConnProps, err := cliutil.ReadIdentityFileOrPassword(opt.identityFile, opt.usePassword) - if err != nil { - return err - } - - if err := os.MkdirAll(cspec.ClusterPath(clusterName), 0755); err != nil { - return errorx.InitializationFailed. - Wrap(err, "Failed to create cluster metadata directory '%s'", cspec.ClusterPath(clusterName)). - WithProperty(cliutil.SuggestionFromString("Please check file system permissions and try again.")) - } - - var ( - envInitTasks []*task.StepDisplay // tasks which are used to initialize environment - downloadCompTasks []*task.StepDisplay // tasks which are used to download components - deployCompTasks []*task.StepDisplay // tasks which are used to copy components to remote host - ) - - // Initialize environment - uniqueHosts := make(map[string]hostInfo) // host -> ssh-port, os, arch - globalOptions := topo.GlobalOptions - topo.IterInstance(func(inst spec.Instance) { - if _, found := uniqueHosts[inst.GetHost()]; !found { - uniqueHosts[inst.GetHost()] = hostInfo{ - ssh: inst.GetSSHPort(), - os: inst.OS(), - arch: inst.Arch(), - } - var dirs []string - for _, dir := range []string{globalOptions.DeployDir, globalOptions.LogDir} { - if dir == "" { - continue - } - dirs = append(dirs, clusterutil.Abs(globalOptions.User, dir)) - } - // the default, relative path of data dir is under deploy dir - if strings.HasPrefix(globalOptions.DataDir, "/") { - dirs = append(dirs, globalOptions.DataDir) - } - t := task.NewBuilder(). - RootSSH( - inst.GetHost(), - inst.GetSSHPort(), - opt.user, - sshConnProps.Password, - sshConnProps.IdentityFile, - sshConnProps.IdentityFilePassphrase, - gOpt.SSHTimeout, - gOpt.NativeSSH, - ). - EnvInit(inst.GetHost(), globalOptions.User). - Mkdir(globalOptions.User, inst.GetHost(), dirs...). - BuildAsStep(fmt.Sprintf(" - Prepare %s:%d", inst.GetHost(), inst.GetSSHPort())) - envInitTasks = append(envInitTasks, t) - } - }) - - // Download missing component - downloadCompTasks = prepare.BuildDownloadCompTasks(clusterVersion, &topo) - - // Deploy components to remote - topo.IterInstance(func(inst spec.Instance) { - version := cspec.ComponentVersion(inst.ComponentName(), clusterVersion) - deployDir := clusterutil.Abs(globalOptions.User, inst.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(globalOptions.User, inst.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(globalOptions.User, inst.LogDir()) - // Deploy component - t := task.NewBuilder(). - UserSSH(inst.GetHost(), inst.GetSSHPort(), globalOptions.User, gOpt.SSHTimeout, gOpt.NativeSSH). - Mkdir(globalOptions.User, inst.GetHost(), - deployDir, logDir, - filepath.Join(deployDir, "bin"), - filepath.Join(deployDir, "conf"), - filepath.Join(deployDir, "scripts")). - Mkdir(globalOptions.User, inst.GetHost(), dataDirs...). - CopyComponent( - inst.ComponentName(), - inst.OS(), - inst.Arch(), - version, - "", // use default srcPath - inst.GetHost(), - deployDir, - ). - InitConfig( + return manager.Deploy( clusterName, - clusterVersion, - inst, - globalOptions.User, - false, - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - Cache: cspec.ClusterPath(clusterName, cspec.TempConfigPath), - }, - ). - BuildAsStep(fmt.Sprintf(" - Copy %s -> %s", inst.ComponentName(), inst.GetHost())) - deployCompTasks = append(deployCompTasks, t) - }) - - builder := task.NewBuilder(). - Step("+ Generate SSH keys", - task.NewBuilder().SSHKeyGen(cspec.ClusterPath(clusterName, "ssh", "id_rsa")).Build()). - ParallelStep("+ Download DM components", downloadCompTasks...). - ParallelStep("+ Initialize target host environments", envInitTasks...). - ParallelStep("+ Copy files", deployCompTasks...) - - t := builder.Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return errors.Trace(err) + version, + topoFile, + opt, + nil, + skipConfirm, + gOpt.OptTimeout, + gOpt.SSHTimeout, + gOpt.NativeSSH, + ) + }, } - err = dmspec.SaveMeta(clusterName, &spec.DMMeta{ - User: globalOptions.User, - Version: clusterVersion, - Topology: &topo, - }) - if err != nil { - return errors.Trace(err) - } + cmd.Flags().StringVarP(&opt.User, "user", "u", tiuputils.CurrentUser(), "The user name to login via SSH. The user must has root (or sudo) privilege.") + cmd.Flags().StringVarP(&opt.IdentityFile, "identity_file", "i", opt.IdentityFile, "The path of the SSH identity file. If specified, public key authentication will be used.") + cmd.Flags().BoolVarP(&opt.UsePassword, "password", "p", false, "Use password of target hosts. If specified, password authentication will be used.") + cmd.Flags().BoolVarP(&opt.IgnoreConfigCheck, "ignore-config-check", "", opt.IgnoreConfigCheck, "Ignore the config check result") - hint := color.New(color.Bold).Sprintf("tiup dm start %s", clusterName) - log.Infof("Deployed cluster `%s` successfully, you can start the cluster via `%s`", clusterName, hint) - return nil + return cmd } diff --git a/components/dm/command/destroy.go b/components/dm/command/destroy.go index 66573982d0..3571db677a 100644 --- a/components/dm/command/destroy.go +++ b/components/dm/command/destroy.go @@ -13,24 +13,12 @@ package command -/* import ( - "errors" - "os" - - "github.com/fatih/color" - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cluster/meta" operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) +// TODO support retain data like cluster? func newDestroyCmd() *cobra.Command { cmd := &cobra.Command{ Use: "destroy ", @@ -41,51 +29,10 @@ func newDestroyCmd() *cobra.Command { } clusterName := args[0] - if tiuputils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return perrs.Errorf("cannot destroy non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - - if !skipConfirm { - if err := cliutil.PromptForConfirmOrAbortError( - "This operation will destroy DM %s cluster %s and its data.\nDo you want to continue? [y/N]:", - color.HiYellowString(metadata.Version), - color.HiYellowString(clusterName)); err != nil { - return err - } - log.Infof("Destroying cluster...") - } - - t := task.NewBuilder(). - SSHKeySet( - meta.ClusterPath(clusterName, "ssh", "id_rsa"), - meta.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - ClusterOperate(metadata.Topology, operator.StopOperation, operator.Options{}). - ClusterOperate(metadata.Topology, operator.DestroyOperation, operator.Options{}). - Build() - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - if err := os.RemoveAll(meta.ClusterPath(clusterName)); err != nil { - return perrs.Trace(err) - } - log.Infof("Destroyed DM cluster `%s` successfully", clusterName) - return nil + return manager.DestroyCluster(clusterName, gOpt, operator.Options{}, skipConfirm) }, } return cmd } -*/ diff --git a/components/dm/command/display.go b/components/dm/command/display.go index 18648efb59..a9af4ad2ed 100644 --- a/components/dm/command/display.go +++ b/components/dm/command/display.go @@ -13,25 +13,15 @@ package command -/* import ( - "errors" - "fmt" - "sort" - "strings" "sync" "time" - "github.com/fatih/color" perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" + "github.com/pingcap/tiup/components/dm/spec" "github.com/pingcap/tiup/pkg/cluster/api" "github.com/pingcap/tiup/pkg/cluster/clusterutil" - "github.com/pingcap/tiup/pkg/cluster/meta" operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/set" - tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" "go.uber.org/zap" ) @@ -49,17 +39,18 @@ func newDisplayCmd() *cobra.Command { } clusterName = args[0] - if err := displayDMMeta(clusterName, &gOpt); err != nil { - return err - } - if err := displayClusterTopology(clusterName, &gOpt); err != nil { - return err + + err := manager.Display(clusterName, gOpt) + if err != nil { + return perrs.AddStack(err) } - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { + metadata := new(spec.Metadata) + err = dmspec.Metadata(clusterName, metadata) + if err != nil { return perrs.AddStack(err) } + return clearOutDatedEtcdInfo(clusterName, metadata, gOpt) }, } @@ -70,25 +61,7 @@ func newDisplayCmd() *cobra.Command { return cmd } -func displayDMMeta(clusterName string, opt *operator.Options) error { - if tiuputils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return perrs.Errorf("cannot display non-exists cluster %s", clusterName) - } - - clsMeta, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - - cyan := color.New(color.FgCyan, color.Bold) - - fmt.Printf("DM Cluster: %s\n", cyan.Sprint(clusterName)) - fmt.Printf("DM Version: %s\n", cyan.Sprint(clsMeta.Version)) - - return nil -} - -func clearOutDatedEtcdInfo(clusterName string, metadata *meta.DMMeta, opt operator.Options) error { +func clearOutDatedEtcdInfo(clusterName string, metadata *spec.Metadata, opt operator.Options) error { topo := metadata.Topology existedMasters := make(map[string]struct{}) @@ -154,121 +127,3 @@ func clearOutDatedEtcdInfo(clusterName string, metadata *meta.DMMeta, opt operat // return any one error return <-errCh } - -func displayClusterTopology(clusterName string, opt *operator.Options) error { - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - - topo := metadata.Topology - - clusterTable := [][]string{ - // Header - {"ID", "Role", "Host", "Ports", "Status", "Data Dir", "Deploy Dir"}, - } - - ctx := task.NewContext() - err = ctx.SetSSHKeySet(meta.ClusterPath(clusterName, "ssh", "id_rsa"), - meta.ClusterPath(clusterName, "ssh", "id_rsa.pub")) - if err != nil { - return perrs.AddStack(err) - } - - err = ctx.SetClusterSSH(topo, metadata.User, gOpt.SSHTimeout) - if err != nil { - return perrs.AddStack(err) - } - - filterRoles := set.NewStringSet(opt.Roles...) - filterNodes := set.NewStringSet(opt.Nodes...) - masterList := topo.GetMasterList() - for _, comp := range topo.ComponentsByStartOrder() { - for _, ins := range comp.Instances() { - // apply role filter - if len(filterRoles) > 0 && !filterRoles.Exist(ins.Role()) { - continue - } - // apply node filter - if len(filterNodes) > 0 && !filterNodes.Exist(ins.ID()) { - continue - } - - dataDir := "-" - insDirs := ins.UsedDirs() - deployDir := insDirs[0] - if len(insDirs) > 1 { - dataDir = insDirs[1] - } - - status := ins.Status(masterList...) - // Query the service status - if status == "-" { - e, found := ctx.GetExecutor(ins.GetHost()) - if found { - active, _ := operator.GetServiceStatus(e, ins.ServiceName()) - if parts := strings.Split(strings.TrimSpace(active), " "); len(parts) > 2 { - if parts[1] == "active" { - status = "Up" - } else { - status = parts[1] - } - } - } - } - clusterTable = append(clusterTable, []string{ - color.CyanString(ins.ID()), - ins.Role(), - ins.GetHost(), - clusterutil.JoinInt(ins.UsedPorts(), "/"), - formatInstanceStatus(status), - dataDir, - deployDir, - }) - - } - } - - // Sort by role,host,ports - sort.Slice(clusterTable[1:], func(i, j int) bool { - lhs, rhs := clusterTable[i+1], clusterTable[j+1] - // column: 1 => role, 2 => host, 3 => ports - for _, col := range []int{1, 2} { - if lhs[col] != rhs[col] { - return lhs[col] < rhs[col] - } - } - return lhs[3] < rhs[3] - }) - - cliutil.PrintTable(clusterTable, true) - - return nil -} - -func formatInstanceStatus(status string) string { - lowercaseStatus := strings.ToLower(status) - - startsWith := func(prefixs ...string) bool { - for _, prefix := range prefixs { - if strings.HasPrefix(lowercaseStatus, prefix) { - return true - } - } - return false - } - - switch { - case startsWith("up|l"): // up|l, up|l|ui - return color.HiGreenString(status) - case startsWith("up"): - return color.GreenString(status) - case startsWith("down", "err"): // down, down|ui - return color.RedString(status) - case startsWith("tombstone", "disconnected"), strings.Contains(status, "offline"): - return color.YellowString(status) - default: - return status - } -} -*/ diff --git a/components/dm/command/edit_config.go b/components/dm/command/edit_config.go index 70ca6ecf36..d5c2a467f1 100644 --- a/components/dm/command/edit_config.go +++ b/components/dm/command/edit_config.go @@ -13,23 +13,8 @@ package command -/* import ( - "bytes" - "errors" - "io" - "io/ioutil" - "os" - - "github.com/fatih/color" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cluster/meta" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" ) func newEditConfigCmd() *cobra.Command { @@ -42,93 +27,10 @@ func newEditConfigCmd() *cobra.Command { } clusterName := args[0] - if tiuputils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return perrs.Errorf("cannot start non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - return editTopo(clusterName, metadata) + return manager.EditConfig(clusterName, skipConfirm) }, } return cmd } - -// 1. Write Topology to a temporary file. -// 2. Open file in editor. -// 3. Check and update Topology. -// 4. Save meta file. -func editTopo(clusterName string, metadata *meta.DMMeta) error { - data, err := yaml.Marshal(metadata.Topology) - if err != nil { - return perrs.AddStack(err) - } - - file, err := ioutil.TempFile(os.TempDir(), "*") - if err != nil { - return perrs.AddStack(err) - } - - name := file.Name() - - _, err = io.Copy(file, bytes.NewReader(data)) - if err != nil { - return perrs.AddStack(err) - } - - err = file.Close() - if err != nil { - return perrs.AddStack(err) - } - - err = tiuputils.OpenFileInEditor(name) - if err != nil { - return perrs.AddStack(err) - } - - // Now user finish editing the file. - newData, err := ioutil.ReadFile(name) - if err != nil { - return perrs.AddStack(err) - } - - newTopo := new(meta.DMSTopologySpecification) - err = yaml.UnmarshalStrict(newData, newTopo) - if err != nil { - log.Infof("Failed to parse topology file: %v", err) - return perrs.AddStack(err) - } - - if bytes.Equal(data, newData) { - log.Infof("The file has nothing changed") - return nil - } - - tiuputils.ShowDiff(string(data), string(newData), os.Stdout) - - if !skipConfirm { - if err := cliutil.PromptForConfirmOrAbortError( - color.HiYellowString("Please check change highlight above, do you want to apply the change? [y/N]:"), - ); err != nil { - return err - } - } - - log.Infof("Apply the change...") - - metadata.Topology = newTopo - err = meta.SaveDMMeta(clusterName, metadata) - if err != nil { - return perrs.Annotate(err, "failed to save") - } - - log.Infof("Apply change successfully, please use `%s reload %s [-N ] [-R ]` to reload config.", cliutil.OsArgs0(), clusterName) - - return nil -} -*/ diff --git a/components/dm/command/exec.go b/components/dm/command/exec.go index 842aadbf50..2c78bb2eb1 100644 --- a/components/dm/command/exec.go +++ b/components/dm/command/exec.go @@ -13,29 +13,13 @@ package command -/* import ( - "errors" - - "github.com/fatih/color" - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/meta" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/set" - tiuputils "github.com/pingcap/tiup/pkg/utils" + "github.com/pingcap/tiup/pkg/cluster" "github.com/spf13/cobra" ) -type execOptions struct { - command string - sudo bool -} - func newExecCmd() *cobra.Command { - opt := execOptions{} + opt := cluster.ExecOptions{} cmd := &cobra.Command{ Use: "exec ", Short: "Run shell command on host in the dm cluster", @@ -45,85 +29,15 @@ func newExecCmd() *cobra.Command { } clusterName := args[0] - if tiuputils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return perrs.Errorf("cannot execute command on non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - - filterRoles := set.NewStringSet(gOpt.Roles...) - filterNodes := set.NewStringSet(gOpt.Nodes...) - - var shellTasks []task.Task - uniqueHosts := map[string]int{} // host -> ssh-port - metadata.Topology.IterInstance(func(inst meta.Instance) { - if _, found := uniqueHosts[inst.GetHost()]; !found { - if len(gOpt.Roles) > 0 && !filterRoles.Exist(inst.Role()) { - return - } - - if len(gOpt.Nodes) > 0 && !filterNodes.Exist(inst.GetHost()) { - return - } - - uniqueHosts[inst.GetHost()] = inst.GetSSHPort() - } - }) - - for host := range uniqueHosts { - shellTasks = append(shellTasks, - task.NewBuilder(). - Shell(host, opt.command, opt.sudo). - Build()) - } - - t := task.NewBuilder(). - SSHKeySet( - meta.ClusterPath(clusterName, "ssh", "id_rsa"), - meta.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - Parallel(shellTasks...). - Build() - - execCtx := task.NewContext() - if err := t.Execute(execCtx); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - // print outputs - for host := range uniqueHosts { - stdout, stderr, ok := execCtx.GetOutputs(host) - if !ok { - continue - } - log.Infof("Outputs of %s on %s:", - color.CyanString(opt.command), - color.CyanString(host)) - if len(stdout) > 0 { - log.Infof("%s:\n%s", color.GreenString("stdout"), stdout) - } - if len(stderr) > 0 { - log.Infof("%s:\n%s", color.RedString("stderr"), stderr) - } - } - return nil + return manager.Exec(clusterName, opt, gOpt) }, } - cmd.Flags().StringVar(&opt.command, "command", "ls", "the command run on cluster host") - cmd.Flags().BoolVar(&opt.sudo, "sudo", false, "use root permissions (default false)") + cmd.Flags().StringVar(&opt.Command, "command", "ls", "the command run on cluster host") + cmd.Flags().BoolVar(&opt.Sudo, "sudo", false, "use root permissions (default false)") cmd.Flags().StringSliceVarP(&gOpt.Roles, "role", "R", nil, "Only exec on host with specified roles") cmd.Flags().StringSliceVarP(&gOpt.Nodes, "node", "N", nil, "Only exec on host with specified nodes") return cmd } -*/ diff --git a/components/dm/command/list.go b/components/dm/command/list.go index 2432422b7d..363badb33e 100644 --- a/components/dm/command/list.go +++ b/components/dm/command/list.go @@ -13,16 +13,7 @@ package command -/* import ( - "errors" - "io/ioutil" - "os" - - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cluster/meta" - tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) @@ -31,41 +22,8 @@ func newListCmd() *cobra.Command { Use: "list", Short: "List all clusters", RunE: func(cmd *cobra.Command, args []string) error { - return listCluster() + return manager.ListCluster() }, } return cmd } - -func listCluster() error { - clusterDir := meta.ProfilePath(meta.TiOpsClusterDir) - clusterTable := [][]string{ - // Header - {"Name", "User", "Version", "Path", "PrivateKey"}, - } - fileInfos, err := ioutil.ReadDir(clusterDir) - if err != nil && !os.IsNotExist(err) { - return err - } - for _, fi := range fileInfos { - if tiuputils.IsNotExist(meta.ClusterPath(fi.Name(), meta.MetaFileName)) { - continue - } - metadata, err := meta.DMMetadata(fi.Name()) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return perrs.Trace(err) - } - - clusterTable = append(clusterTable, []string{ - fi.Name(), - metadata.User, - metadata.Version, - meta.ClusterPath(fi.Name()), - meta.ClusterPath(fi.Name(), "ssh", "id_rsa"), - }) - } - - cliutil.PrintTable(clusterTable, true) - return nil -} -*/ diff --git a/components/dm/command/patch.go b/components/dm/command/patch.go index d07c5e32d6..f111b70f98 100644 --- a/components/dm/command/patch.go +++ b/components/dm/command/patch.go @@ -13,24 +13,8 @@ package command -/* import ( - "errors" - "fmt" - "os" - "os/exec" - "path" - - "github.com/joomcode/errorx" perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" - "github.com/pingcap/tiup/pkg/cluster/meta" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/task" - tiupmeta "github.com/pingcap/tiup/pkg/environment" - "github.com/pingcap/tiup/pkg/repository/v0manifest" - "github.com/pingcap/tiup/pkg/set" - tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) @@ -53,7 +37,10 @@ func newPatchCmd() *cobra.Command { if len(gOpt.Nodes) == 0 && len(gOpt.Roles) == 0 { return perrs.New("the flag -R or -N must be specified at least one") } - return patch(args[0], args[1], gOpt, overwrite) + + clusterName := args[0] + + return manager.Patch(clusterName, args[1], gOpt, overwrite) }, } @@ -63,136 +50,3 @@ func newPatchCmd() *cobra.Command { cmd.Flags().Int64Var(&gOpt.APITimeout, "transfer-timeout", 300, "Timeout in seconds when transferring dm-master leaders") return cmd } - -func patch(clusterName, packagePath string, options operator.Options, overwrite bool) error { - if tiuputils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return perrs.Errorf("cannot patch non-exists cluster %s", clusterName) - } - - if exist := tiuputils.IsExist(packagePath); !exist { - return perrs.New("specified package not exists") - } - - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - - insts, err := instancesToPatch(metadata, options) - if err != nil { - return err - } - if err := checkPackage(clusterName, insts[0].ComponentName(), packagePath); err != nil { - return err - } - - var replacePackageTasks []task.Task - for _, inst := range insts { - deployDir := clusterutil.Abs(metadata.User, inst.DeployDir()) - tb := task.NewBuilder() - tb.BackupComponent(inst.ComponentName(), metadata.Version, inst.GetHost(), deployDir). - InstallPackage(packagePath, inst.GetHost(), deployDir) - replacePackageTasks = append(replacePackageTasks, tb.Build()) - } - - t := task.NewBuilder(). - SSHKeySet( - meta.ClusterPath(clusterName, "ssh", "id_rsa"), - meta.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - Parallel(replacePackageTasks...). - ClusterOperate(metadata.Topology, operator.UpgradeOperation, options). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - if overwrite { - if err := overwritePatch(clusterName, insts[0].ComponentName(), packagePath); err != nil { - return err - } - } - - return nil -} - -func instancesToPatch(metadata *meta.DMMeta, options operator.Options) ([]meta.Instance, error) { - roleFilter := set.NewStringSet(options.Roles...) - nodeFilter := set.NewStringSet(options.Nodes...) - components := metadata.Topology.ComponentsByStartOrder() - components = operator.FilterComponent(components, roleFilter) - - instances := []meta.Instance{} - comps := []string{} - for _, com := range components { - insts := operator.FilterInstance(com.Instances(), nodeFilter) - if len(insts) > 0 { - comps = append(comps, com.Name()) - } - instances = append(instances, insts...) - } - if len(comps) > 1 { - return nil, fmt.Errorf("can't patch more than one component at once: %v", comps) - } - - if len(instances) == 0 { - return nil, fmt.Errorf("no instance found on specifid role(%v) and nodes(%v)", options.Roles, options.Nodes) - } - - return instances, nil -} - -func checkPackage(clusterName, comp, packagePath string) error { - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - manifest, err := tiupmeta.GlobalEnv().Repository().ComponentVersions(comp) - if err != nil { - return err - } - ver := meta.ComponentVersion(comp, metadata.Version) - versionInfo, found := manifest.FindVersion(v0manifest.Version(ver)) - if !found { - return fmt.Errorf("cannot found version %v in %s manifest", ver, comp) - } - - checksum, err := tiuputils.Checksum(packagePath) - if err != nil { - return err - } - cacheDir := meta.ClusterPath(clusterName, "cache", comp+"-"+checksum[:7]) - if err := os.MkdirAll(cacheDir, 0755); err != nil { - return err - } - if err := exec.Command("tar", "-xvf", packagePath, "-C", cacheDir).Run(); err != nil { - return err - } - - if exists := tiuputils.IsExist(path.Join(cacheDir, versionInfo.Entry)); !exists { - return fmt.Errorf("entry %s not found in package %s", versionInfo.Entry, packagePath) - } - - return nil -} - -func overwritePatch(clusterName, comp, packagePath string) error { - if err := os.MkdirAll(meta.ClusterPath(clusterName, meta.PatchDirName), 0755); err != nil { - return err - } - checksum, err := tiuputils.Checksum(packagePath) - if err != nil { - return err - } - tg := meta.ClusterPath(clusterName, meta.PatchDirName, comp+"-"+checksum[:7]+".tar.gz") - if err := tiuputils.CopyFile(packagePath, tg); err != nil { - return err - } - return os.Symlink(tg, meta.ClusterPath(clusterName, meta.PatchDirName, comp+".tar.gz")) -} -*/ diff --git a/components/dm/command/reload.go b/components/dm/command/reload.go index c5a99505cb..da8465882b 100644 --- a/components/dm/command/reload.go +++ b/components/dm/command/reload.go @@ -13,23 +13,14 @@ package command -/* import ( - "errors" - - "github.com/joomcode/errorx" perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" - "github.com/pingcap/tiup/pkg/cluster/meta" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/utils" + "github.com/pingcap/tiup/components/dm/spec" "github.com/spf13/cobra" ) func newReloadCmd() *cobra.Command { + var skipRestart bool cmd := &cobra.Command{ Use: "reload ", Short: "Reload a DM cluster's config and restart if needed", @@ -43,32 +34,8 @@ func newReloadCmd() *cobra.Command { } clusterName := args[0] - if utils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return perrs.Errorf("cannot start non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - - t, err := buildReloadTask(clusterName, metadata, gOpt) - if err != nil { - return err - } - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - log.Infof("Reloaded cluster `%s` successfully", clusterName) - return nil + return manager.Reload(clusterName, gOpt, skipRestart) }, } @@ -76,68 +43,15 @@ func newReloadCmd() *cobra.Command { cmd.Flags().StringSliceVarP(&gOpt.Nodes, "node", "N", nil, "Only start specified nodes") cmd.Flags().Int64Var(&gOpt.APITimeout, "transfer-timeout", 300, "Timeout in seconds when transferring dm-master leaders") cmd.Flags().BoolVarP(&gOpt.IgnoreConfigCheck, "ignore-config-check", "", false, "Ignore the config check result") + cmd.Flags().BoolVar(&skipRestart, "skip-restart", false, "Only refresh configuration to remote and do not restart services") return cmd } -func buildReloadTask( - clusterName string, - metadata *meta.DMMeta, - options operator.Options, -) (task.Task, error) { - - var refreshConfigTasks []task.Task - - topo := metadata.Topology - - topo.IterInstance(func(inst meta.Instance) { - deployDir := clusterutil.Abs(metadata.User, inst.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(metadata.User, inst.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(metadata.User, inst.LogDir()) - - // Download and copy the latest component to remote if the cluster is imported from Ansible - tb := task.NewBuilder().UserSSH(inst.GetHost(), inst.GetSSHPort(), metadata.User, gOpt.SSHTimeout) - if inst.IsImported() { - switch compName := inst.ComponentName(); compName { - case meta.ComponentGrafana, meta.ComponentPrometheus, meta.ComponentAlertManager: - version := meta.ComponentVersion(compName, metadata.Version) - tb.Download(compName, inst.OS(), inst.Arch(), version). - CopyComponent(compName, inst.OS(), inst.Arch(), version, inst.GetHost(), deployDir) - } - } - - // Refresh all configuration - t := tb.InitConfig(clusterName, - metadata.Version, - inst, metadata.User, - options.IgnoreConfigCheck, - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - Cache: meta.ClusterPath(clusterName, meta.TempConfigPath), - }).Build() - refreshConfigTasks = append(refreshConfigTasks, t) - }) - - t := task.NewBuilder(). - SSHKeySet( - meta.ClusterPath(clusterName, "ssh", "id_rsa"), - meta.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - Parallel(refreshConfigTasks...). - ClusterOperate(metadata.Topology, operator.UpgradeOperation, options). - Build() - - return t, nil -} - func validRoles(roles []string) error { for _, r := range roles { match := false - for _, has := range meta.AllDMComponentNames() { + for _, has := range spec.AllDMComponentNames() { if r == has { match = true break @@ -145,10 +59,9 @@ func validRoles(roles []string) error { } if !match { - return perrs.Errorf("not valid role: %s, should be one of: %v", r, meta.AllDMComponentNames()) + return perrs.Errorf("not valid role: %s, should be one of: %v", r, spec.AllDMComponentNames()) } } return nil } -*/ diff --git a/components/dm/command/restart.go b/components/dm/command/restart.go index 7988fc79f0..87cfd6be1c 100644 --- a/components/dm/command/restart.go +++ b/components/dm/command/restart.go @@ -13,18 +13,7 @@ package command -/* import ( - "errors" - - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/meta" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) @@ -37,40 +26,9 @@ func newRestartCmd() *cobra.Command { return cmd.Help() } - if err := validRoles(gOpt.Roles); err != nil { - return err - } - clusterName := args[0] - if tiuputils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return perrs.Errorf("cannot restart non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - - t := task.NewBuilder(). - SSHKeySet( - meta.ClusterPath(clusterName, "ssh", "id_rsa"), - meta.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - ClusterOperate(metadata.Topology, operator.RestartOperation, gOpt). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - log.Infof("Restarted cluster `%s` successfully", clusterName) - return nil + return manager.RestartCluster(clusterName, gOpt) }, } @@ -79,4 +37,3 @@ func newRestartCmd() *cobra.Command { return cmd } -*/ diff --git a/components/dm/command/root.go b/components/dm/command/root.go index fd740fa787..114968a168 100644 --- a/components/dm/command/root.go +++ b/components/dm/command/root.go @@ -20,12 +20,13 @@ import ( "github.com/fatih/color" "github.com/joomcode/errorx" + "github.com/pingcap/tiup/components/dm/spec" "github.com/pingcap/tiup/pkg/cliutil" + "github.com/pingcap/tiup/pkg/cluster" "github.com/pingcap/tiup/pkg/cluster/flags" operator "github.com/pingcap/tiup/pkg/cluster/operation" cspec "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/colorutil" - "github.com/pingcap/tiup/pkg/dm/spec" tiupmeta "github.com/pingcap/tiup/pkg/environment" "github.com/pingcap/tiup/pkg/errutil" "github.com/pingcap/tiup/pkg/localdata" @@ -37,13 +38,15 @@ import ( ) var ( + // nolint errNS = errorx.NewNamespace("cmd") rootCmd *cobra.Command gOpt operator.Options skipConfirm bool ) -var dmspec = spec.GetSpecManager() +var dmspec *cspec.SpecManager +var manager *cluster.Manager func init() { logger.InitGlobalLogger() @@ -54,6 +57,11 @@ func init() { flags.ShowBacktrace = len(os.Getenv("TIUP_BACKTRACE")) > 0 cobra.EnableCommandSorting = false + nativeEnvVar := strings.ToLower(os.Getenv(localdata.EnvNameNativeSSHClient)) + if nativeEnvVar == "true" || nativeEnvVar == "1" || nativeEnvVar == "enable" { + gOpt.NativeSSH = true + } + rootCmd = &cobra.Command{ Use: cliutil.OsArgs0(), Short: "Deploy a DM cluster for production", @@ -63,10 +71,14 @@ func init() { PersistentPreRunE: func(cmd *cobra.Command, args []string) error { var err error var env *tiupmeta.Environment - if err = cspec.Initialize("cluster"); err != nil { + if err = cspec.Initialize("dm"); err != nil { return err } + dmspec = spec.GetSpecManager() + logger.EnableAuditLog(cspec.AuditDir()) + manager = cluster.NewManager("dm", spec.GetSpecManager()) + // Running in other OS/ARCH Should be fine we only download manifest file. env, err = tiupmeta.InitEnv(repository.Options{ GOOS: "linux", @@ -76,6 +88,12 @@ func init() { return err } tiupmeta.SetGlobalEnv(env) + + if gOpt.NativeSSH { + zap.L().Info("Native ssh client will be used", + zap.String(localdata.EnvNameNativeSSHClient, os.Getenv(localdata.EnvNameNativeSSHClient))) + } + return nil }, RunE: func(cmd *cobra.Command, args []string) error { @@ -91,25 +109,26 @@ func init() { rootCmd.PersistentFlags().Int64Var(&gOpt.SSHTimeout, "ssh-timeout", 5, "Timeout in seconds to connect host via SSH, ignored for operations that don't need an SSH connection.") rootCmd.PersistentFlags().Int64Var(&gOpt.OptTimeout, "wait-timeout", 60, "Timeout in seconds to wait for an operation to complete, ignored for operations that don't fit.") rootCmd.PersistentFlags().BoolVarP(&skipConfirm, "yes", "y", false, "Skip all confirmations and assumes 'yes'") + rootCmd.PersistentFlags().BoolVar(&gOpt.NativeSSH, "native-ssh", gOpt.NativeSSH, "Use the native SSH client installed on local system instead of the build-in one.") rootCmd.AddCommand( newDeploy(), newStartCmd(), - /* - newCheckCmd(), newStopCmd(), newRestartCmd(), - newScaleInCmd(), - newScaleOutCmd(), - newDestroyCmd(), - newUpgradeCmd(), - newExecCmd(), - newDisplayCmd(), newListCmd(), + newDestroyCmd(), newAuditCmd(), + newExecCmd(), newEditConfigCmd(), + newDisplayCmd(), newReloadCmd(), + newUpgradeCmd(), newPatchCmd(), + newScaleOutCmd(), + newScaleInCmd(), + /* + newCheckCmd(), newTestCmd(), // hidden command for test internally */ ) diff --git a/components/dm/command/scale_in.go b/components/dm/command/scale_in.go index 60ccc7e9ee..00a67f03e8 100644 --- a/components/dm/command/scale_in.go +++ b/components/dm/command/scale_in.go @@ -13,23 +13,13 @@ package command -/* import ( - "errors" - "strings" + "fmt" - "github.com/fatih/color" - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" - "github.com/pingcap/tiup/pkg/cluster/meta" + dm "github.com/pingcap/tiup/components/dm/spec" operator "github.com/pingcap/tiup/pkg/cluster/operation" + "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/set" - tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) @@ -43,18 +33,25 @@ func newScaleInCmd() *cobra.Command { } clusterName := args[0] - if !skipConfirm { - if err := cliutil.PromptForConfirmOrAbortError( - "This operation will delete the %s nodes in `%s` and all their data.\nDo you want to continue? [y/N]:", - strings.Join(gOpt.Nodes, ","), - color.HiYellowString(clusterName)); err != nil { - return err - } - log.Infof("Scale-in nodes...") + + scale := func(b *task.Builder, imetadata spec.Metadata) { + metadata := imetadata.(*dm.Metadata) + b.Func( + fmt.Sprintf("ScaleInCluster: options=%+v", gOpt), + func(ctx *task.Context) error { + return operator.ScaleInDMCluster(ctx, metadata.Topology, gOpt) + }, + ).UpdateDMMeta(clusterName, metadata, gOpt.Nodes) } - logger.EnableAuditLog() - return scaleIn(clusterName, gOpt) + return manager.ScaleIn( + clusterName, + skipConfirm, + gOpt.SSHTimeout, + gOpt.Force, + gOpt.Nodes, + scale, + ) }, } @@ -66,78 +63,3 @@ func newScaleInCmd() *cobra.Command { return cmd } - -func scaleIn(clusterName string, options operator.Options) error { - if tiuputils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return perrs.Errorf("cannot scale-in non-exists cluster %s", clusterName) - } - - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(err, meta.ValidateErr) { - // ignore conflict check error, node may be deployed by former version - // that lack of some certain conflict checks - return err - } - - // Regenerate configuration - var regenConfigTasks []task.Task - deletedNodes := set.NewStringSet(options.Nodes...) - for _, component := range metadata.Topology.ComponentsByStartOrder() { - for _, instance := range component.Instances() { - if deletedNodes.Exist(instance.ID()) { - continue - } - deployDir := clusterutil.Abs(metadata.User, instance.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(metadata.User, instance.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(metadata.User, instance.LogDir()) - - // Download and copy the latest component to remote if the cluster is imported from Ansible - tb := task.NewBuilder() - - t := tb.InitConfig(clusterName, - metadata.Version, - instance, - metadata.User, - true, - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - Cache: meta.ClusterPath(clusterName, meta.TempConfigPath), - }, - ).Build() - regenConfigTasks = append(regenConfigTasks, t) - } - } - - b := task.NewBuilder(). - SSHKeySet( - meta.ClusterPath(clusterName, "ssh", "id_rsa"), - meta.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout) - - if !options.Force { - b.ClusterOperate(metadata.Topology, operator.ScaleInOperation, options). - UpdateDMMeta(clusterName, metadata, operator.AsyncNodes(metadata.Topology, options.Nodes, false)) - } else { - b.ClusterOperate(metadata.Topology, operator.ScaleInOperation, options). - UpdateDMMeta(clusterName, metadata, options.Nodes) - } - - t := b.Parallel(regenConfigTasks...).Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - log.Infof("Scaled cluster `%s` in successfully", clusterName) - - return nil -} -*/ diff --git a/components/dm/command/scale_out.go b/components/dm/command/scale_out.go index 6cf2cfac55..8fe7c0cddd 100644 --- a/components/dm/command/scale_out.go +++ b/components/dm/command/scale_out.go @@ -13,38 +13,17 @@ package command -/* import ( - "context" - "io/ioutil" "path/filepath" - "strings" - "github.com/joomcode/errorx" - "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cliutil" - "github.com/pingcap/tiup/pkg/cliutil/prepare" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" - "github.com/pingcap/tiup/pkg/cluster/meta" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/report" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/set" + "github.com/pingcap/tiup/pkg/cluster" tiuputils "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) -type scaleOutOptions struct { - user string // username to login to the SSH server - identityFile string // path to the private key file - usePassword bool // use password instead of identity file for ssh connection -} - func newScaleOutCmd() *cobra.Command { - opt := scaleOutOptions{ - identityFile: filepath.Join(tiuputils.UserHome(), ".ssh", "id_rsa"), + opt := cluster.ScaleOutOptions{ + IdentityFile: filepath.Join(tiuputils.UserHome(), ".ssh", "id_rsa"), } cmd := &cobra.Command{ Use: "scale-out ", @@ -55,274 +34,26 @@ func newScaleOutCmd() *cobra.Command { return cmd.Help() } - logger.EnableAuditLog() - return scaleOut(args[0], args[1], opt) + clusterName := args[0] + topoFile := args[1] + + return manager.ScaleOut( + clusterName, + topoFile, + nil, + nil, + opt, + skipConfirm, + gOpt.OptTimeout, + gOpt.SSHTimeout, + gOpt.NativeSSH, + ) }, } - cmd.Flags().StringVar(&opt.user, "user", tiuputils.CurrentUser(), "The user name to login via SSH. The user must has root (or sudo) privilege.") - cmd.Flags().StringVarP(&opt.identityFile, "identity_file", "i", opt.identityFile, "The path of the SSH identity file. If specified, public key authentication will be used.") - cmd.Flags().BoolVarP(&opt.usePassword, "password", "p", false, "Use password of target hosts. If specified, password authentication will be used.") + cmd.Flags().StringVarP(&opt.User, "user", "u", tiuputils.CurrentUser(), "The user name to login via SSH. The user must has root (or sudo) privilege.") + cmd.Flags().StringVarP(&opt.IdentityFile, "identity_file", "i", opt.IdentityFile, "The path of the SSH identity file. If specified, public key authentication will be used.") + cmd.Flags().BoolVarP(&opt.UsePassword, "password", "p", false, "Use password of target hosts. If specified, password authentication will be used.") return cmd } - -func scaleOut(clusterName, topoFile string, opt scaleOutOptions) error { - if tiuputils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return errors.Errorf("cannot scale-out non-exists cluster %s", clusterName) - } - - var newPart meta.DMSTopologySpecification - if err := clusterutil.ParseTopologyYaml(topoFile, &newPart); err != nil { - return err - } - - if data, err := ioutil.ReadFile(topoFile); err == nil { - teleTopology = string(data) - } - - metadata, err := meta.DMMetadata(clusterName) - if err != nil { - return err - } - - // Abort scale out operation if the merged topology is invalid - mergedTopo := metadata.Topology.Merge(&newPart) - if err := mergedTopo.Validate(); err != nil { - return err - } - - if err := prepare.CheckClusterPortConflict(clusterName, mergedTopo); err != nil { - return err - } - if err := prepare.CheckClusterDirConflict(clusterName, mergedTopo); err != nil { - return err - } - - patchedComponents := set.NewStringSet() - newPart.IterInstance(func(instance meta.Instance) { - if exists := tiuputils.IsExist(meta.ClusterPath(clusterName, meta.PatchDirName, instance.ComponentName()+".tar.gz")); exists { - patchedComponents.Insert(instance.ComponentName()) - } - }) - if !skipConfirm { - // patchedComponents are components that have been patched and overwrited - if err := confirmTopology(clusterName, metadata.Version, &newPart, patchedComponents); err != nil { - return err - } - } - - // Inherit existing global configuration - newPart.GlobalOptions = metadata.Topology.GlobalOptions - newPart.MonitoredOptions = metadata.Topology.MonitoredOptions - newPart.ServerConfigs = metadata.Topology.ServerConfigs - - sshConnProps, err := cliutil.ReadIdentityFileOrPassword(opt.identityFile, opt.usePassword) - if err != nil { - return err - } - - // Build the scale out tasks - t, err := buildScaleOutTask(clusterName, metadata, mergedTopo, opt, sshConnProps, &newPart, patchedComponents, gOpt.OptTimeout) - if err != nil { - return err - } - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return errors.Trace(err) - } - - log.Infof("Scaled cluster `%s` out successfully", clusterName) - - return nil -} - -// Deprecated -func convertStepDisplaysToTasks(t []*task.StepDisplay) []task.Task { - tasks := make([]task.Task, 0, len(t)) - for _, sd := range t { - tasks = append(tasks, sd) - } - return tasks -} - -func buildScaleOutTask( - clusterName string, - metadata *meta.DMMeta, - mergedTopo *meta.DMSSpecification, - opt scaleOutOptions, - sshConnProps *cliutil.SSHConnectionProps, - newPart meta.Specification, - patchedComponents set.StringSet, - timeout int64, -) (task.Task, error) { - var ( - envInitTasks []task.Task // tasks which are used to initialize environment - downloadCompTasks []task.Task // tasks which are used to download components - deployCompTasks []task.Task // tasks which are used to copy components to remote host - refreshConfigTasks []task.Task // tasks which are used to refresh configuration - ) - - // Initialize the environments - initializedHosts := set.NewStringSet() - metadata.Topology.IterInstance(func(instance meta.Instance) { - initializedHosts.Insert(instance.GetHost()) - }) - // uninitializedHosts are hosts which haven't been initialized yet - uninitializedHosts := make(map[string]hostInfo) // host -> ssh-port, os, arch - newPart.IterInstance(func(instance meta.Instance) { - if host := instance.GetHost(); !initializedHosts.Exist(host) { - if _, found := uninitializedHosts[host]; found { - return - } - - uninitializedHosts[host] = hostInfo{ - ssh: instance.GetSSHPort(), - os: instance.OS(), - arch: instance.Arch(), - } - - var dirs []string - globalOptions := metadata.Topology.GlobalOptions - for _, dir := range []string{globalOptions.DeployDir, globalOptions.DataDir, globalOptions.LogDir} { - for _, dirname := range strings.Split(dir, ",") { - if dirname == "" { - continue - } - dirs = append(dirs, clusterutil.Abs(globalOptions.User, dirname)) - } - } - t := task.NewBuilder(). - RootSSH( - instance.GetHost(), - instance.GetSSHPort(), - opt.user, - sshConnProps.Password, - sshConnProps.IdentityFile, - sshConnProps.IdentityFilePassphrase, - gOpt.SSHTimeout, - ). - EnvInit(instance.GetHost(), metadata.User). - Mkdir(globalOptions.User, instance.GetHost(), dirs...). - Build() - envInitTasks = append(envInitTasks, t) - } - }) - - // Download missing component - downloadCompTasks = convertStepDisplaysToTasks(prepare.BuildDownloadCompTasks(metadata.Version, newPart)) - - // Deploy the new topology and refresh the configuration - newPart.IterInstance(func(inst meta.Instance) { - version := meta.ComponentVersion(inst.ComponentName(), metadata.Version) - deployDir := clusterutil.Abs(metadata.User, inst.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(metadata.User, inst.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(metadata.User, inst.LogDir()) - - // Deploy component - tb := task.NewBuilder(). - UserSSH(inst.GetHost(), inst.GetSSHPort(), metadata.User, gOpt.SSHTimeout). - Mkdir(metadata.User, inst.GetHost(), - deployDir, logDir, - filepath.Join(deployDir, "bin"), - filepath.Join(deployDir, "conf"), - filepath.Join(deployDir, "scripts")). - Mkdir(metadata.User, inst.GetHost(), dataDirs...) - - srcPath := "" - if patchedComponents.Exist(inst.ComponentName()) { - srcPath = spec.ClusterPath(clusterName, spec.PatchDirName, inst.ComponentName()+".tar.gz") - } - - t := tb.CopyComponent( - inst.ComponentName(), - inst.OS(), - inst.Arch(), - version, - srcPath, - inst.GetHost(), - deployDir, - ).ScaleConfig(clusterName, - metadata.Version, - metadata.Topology, - inst, - metadata.User, - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - }, - ).Build() - deployCompTasks = append(deployCompTasks, t) - }) - - mergedTopo.IterInstance(func(inst meta.Instance) { - deployDir := clusterutil.Abs(metadata.User, inst.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(metadata.User, inst.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(metadata.User, inst.LogDir()) - - // Download and copy the latest component to remote if the cluster is imported from Ansible - tb := task.NewBuilder() - - // Refresh all configuration - t := tb.InitConfig(clusterName, - metadata.Version, - inst, - metadata.User, - true, - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - Cache: meta.ClusterPath(clusterName, meta.TempConfigPath), - }, - ).Build() - refreshConfigTasks = append(refreshConfigTasks, t) - }) - - nodeInfoTask := task.NewBuilder().Func("Check status", func(ctx *task.Context) error { - var err error - teleNodeInfos, err = operator.GetNodeInfo(context.Background(), ctx, newPart) - _ = err - // intend to never return error - return nil - }).BuildAsStep("Check status").SetHidden(true) - - builder := task.NewBuilder(). - SSHKeySet( - meta.ClusterPath(clusterName, "ssh", "id_rsa"), - meta.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - Parallel(downloadCompTasks...). - Parallel(envInitTasks...). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - Parallel(deployCompTasks...) - - if report.Enable() { - builder.Parallel(convertStepDisplaysToTasks([]*task.StepDisplay{nodeInfoTask})...) - } - - // TODO: find another way to make sure current cluster started - builder.ClusterOperate(metadata.Topology, operator.StartOperation, operator.Options{OptTimeout: timeout}). - ClusterSSH(newPart, metadata.User, gOpt.SSHTimeout). - Func("save meta", func(_ *task.Context) error { - metadata.Topology = mergedTopo - return meta.SaveDMMeta(clusterName, metadata) - }). - ClusterOperate(newPart, operator.StartOperation, operator.Options{OptTimeout: timeout}). - Parallel(refreshConfigTasks...). - ClusterOperate(metadata.Topology, operator.RestartOperation, operator.Options{ - Roles: []string{meta.ComponentPrometheus}, - OptTimeout: timeout, - }) - - return builder.Build(), nil -} -*/ diff --git a/components/dm/command/start.go b/components/dm/command/start.go index 61eba3a5d4..c3ad9cb44c 100644 --- a/components/dm/command/start.go +++ b/components/dm/command/start.go @@ -14,17 +14,6 @@ package command import ( - "errors" - - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - cspec "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/dm/spec" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/meta" "github.com/spf13/cobra" ) @@ -39,16 +28,7 @@ func newStartCmd() *cobra.Command { clusterName := args[0] - exist, err := dmspec.Exist(clusterName) - if err != nil { - return perrs.AddStack(err) - } - - if !exist { - return perrs.Errorf("cannot start non-exists cluster %s", clusterName) - } - - return startCluster(clusterName, gOpt) + return manager.StartCluster(clusterName, gOpt) }, } @@ -57,36 +37,3 @@ func newStartCmd() *cobra.Command { return cmd } - -func startCluster(clusterName string, options operator.Options) error { - logger.EnableAuditLog() - log.Infof("Starting cluster %s...", clusterName) - metadata := new(spec.DMMeta) - err := dmspec.Metadata(clusterName, metadata) - if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { - return err - } - - t := task.NewBuilder(). - SSHKeySet( - cspec.ClusterPath(clusterName, "ssh", "id_rsa"), - cspec.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - // ClusterOperate(metadata.Topology, operator.StartOperation, options). - Serial(task.NewFunc("start", func(ctx *task.Context) error { - return operator.Start(ctx, metadata.Topology, options) - })). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - log.Infof("Started cluster `%s` successfully", clusterName) - - return nil -} diff --git a/components/dm/command/stop.go b/components/dm/command/stop.go index 29b30fb569..14cb3961a7 100644 --- a/components/dm/command/stop.go +++ b/components/dm/command/stop.go @@ -13,18 +13,7 @@ package command -/* import ( - "errors" - - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/meta" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" ) @@ -38,35 +27,8 @@ func newStopCmd() *cobra.Command { } clusterName := args[0] - if utils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return perrs.Errorf("cannot stop non-exists cluster %s", clusterName) - } - - logger.EnableAuditLog() - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - - t := task.NewBuilder(). - SSHKeySet( - meta.ClusterPath(clusterName, "ssh", "id_rsa"), - meta.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - ClusterOperate(metadata.Topology, operator.StopOperation, gOpt). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - log.Infof("Stopped cluster `%s` successfully", clusterName) - return nil + return manager.StopCluster(clusterName, gOpt) }, } @@ -75,4 +37,3 @@ func newStopCmd() *cobra.Command { return cmd } -*/ diff --git a/components/dm/command/upgrade.go b/components/dm/command/upgrade.go index a5c24a983a..8a78fc50fc 100644 --- a/components/dm/command/upgrade.go +++ b/components/dm/command/upgrade.go @@ -13,24 +13,8 @@ package command -/* import ( - "errors" - "fmt" - "os" - - "github.com/joomcode/errorx" - perrs "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" - "github.com/pingcap/tiup/pkg/cluster/meta" - operator "github.com/pingcap/tiup/pkg/cluster/operation" - "github.com/pingcap/tiup/pkg/cluster/task" - "github.com/pingcap/tiup/pkg/logger" - "github.com/pingcap/tiup/pkg/logger/log" - "github.com/pingcap/tiup/pkg/repository/v0manifest" - "github.com/pingcap/tiup/pkg/utils" "github.com/spf13/cobra" - "golang.org/x/mod/semver" ) func newUpgradeCmd() *cobra.Command { @@ -42,8 +26,7 @@ func newUpgradeCmd() *cobra.Command { return cmd.Help() } - logger.EnableAuditLog() - return upgrade(args[0], args[1], gOpt) + return manager.Upgrade(args[0], args[1], gOpt) }, } cmd.Flags().BoolVar(&gOpt.Force, "force", false, "Force upgrade won't transfer leader") @@ -52,130 +35,3 @@ func newUpgradeCmd() *cobra.Command { return cmd } - -func versionCompare(curVersion, newVersion string) error { - // Can always upgrade to 'nightly' event the current version is 'nightly' - if v0manifest.Version(newVersion).IsNightly() { - return nil - } - - switch semver.Compare(curVersion, newVersion) { - case -1: - return nil - case 0, 1: - return perrs.Errorf("please specify a higher version than %s", curVersion) - default: - return perrs.Errorf("unreachable") - } -} - -func upgrade(clusterName, clusterVersion string, opt operator.Options) error { - if utils.IsNotExist(meta.ClusterPath(clusterName, meta.MetaFileName)) { - return perrs.Errorf("cannot upgrade non-exists cluster %s", clusterName) - } - - metadata, err := meta.DMMetadata(clusterName) - if err != nil && !errors.Is(perrs.Cause(err), meta.ValidateErr) { - return err - } - - var ( - downloadCompTasks []task.Task // tasks which are used to download components - copyCompTasks []task.Task // tasks which are used to copy components to remote host - - uniqueComps = map[string]struct{}{} - ) - - if err := versionCompare(metadata.Version, clusterVersion); err != nil { - return err - } - - for _, comp := range metadata.Topology.ComponentsByStartOrder() { - for _, inst := range comp.Instances() { - version := meta.ComponentVersion(inst.ComponentName(), clusterVersion) - if version == "" { - return perrs.Errorf("unsupported component: %v", inst.ComponentName()) - } - compInfo := componentInfo{ - component: inst.ComponentName(), - version: v0manifest.Version(version), - } - - // Download component from repository - key := fmt.Sprintf("%s-%s-%s-%s", compInfo.component, compInfo.version, inst.OS(), inst.Arch()) - if _, found := uniqueComps[key]; !found { - uniqueComps[key] = struct{}{} - t := task.NewBuilder(). - Download(inst.ComponentName(), inst.OS(), inst.Arch(), version). - Build() - downloadCompTasks = append(downloadCompTasks, t) - } - - deployDir := clusterutil.Abs(metadata.User, inst.DeployDir()) - // data dir would be empty for components which don't need it - dataDirs := clusterutil.MultiDirAbs(metadata.User, inst.DataDir()) - // log dir will always be with values, but might not used by the component - logDir := clusterutil.Abs(metadata.User, inst.LogDir()) - - // Deploy component - tb := task.NewBuilder() - if inst.IsImported() { - switch inst.ComponentName() { - case meta.ComponentPrometheus, meta.ComponentGrafana, meta.ComponentAlertManager: - tb.CopyComponent(inst.ComponentName(), inst.OS(), inst.Arch(), version, inst.GetHost(), deployDir) - default: - tb.BackupComponent(inst.ComponentName(), metadata.Version, inst.GetHost(), deployDir). - CopyComponent(inst.ComponentName(), inst.OS(), inst.Arch(), version, inst.GetHost(), deployDir) - } - tb.InitConfig( - clusterName, - clusterVersion, - inst, - metadata.User, - opt.IgnoreConfigCheck, - meta.DirPaths{ - Deploy: deployDir, - Data: dataDirs, - Log: logDir, - Cache: meta.ClusterPath(clusterName, meta.TempConfigPath), - }, - ) - } else { - tb.BackupComponent(inst.ComponentName(), metadata.Version, inst.GetHost(), deployDir). - CopyComponent(inst.ComponentName(), inst.OS(), inst.Arch(), version, inst.GetHost(), deployDir) - } - copyCompTasks = append(copyCompTasks, tb.Build()) - } - } - - t := task.NewBuilder(). - SSHKeySet( - meta.ClusterPath(clusterName, "ssh", "id_rsa"), - meta.ClusterPath(clusterName, "ssh", "id_rsa.pub")). - ClusterSSH(metadata.Topology, metadata.User, gOpt.SSHTimeout). - Parallel(downloadCompTasks...). - Parallel(copyCompTasks...). - ClusterOperate(metadata.Topology, operator.UpgradeOperation, opt). - Build() - - if err := t.Execute(task.NewContext()); err != nil { - if errorx.Cast(err) != nil { - // FIXME: Map possible task errors and give suggestions. - return err - } - return perrs.Trace(err) - } - - metadata.Version = clusterVersion - if err := meta.SaveDMMeta(clusterName, metadata); err != nil { - return perrs.Trace(err) - } - if err := os.RemoveAll(meta.ClusterPath(clusterName, "patch")); err != nil { - return perrs.Trace(err) - } - - log.Infof("Upgraded cluster `%s` successfully", clusterName) - - return nil -} -*/ diff --git a/components/dm/spec/cluster.go b/components/dm/spec/cluster.go new file mode 100644 index 0000000000..3a2e45b258 --- /dev/null +++ b/components/dm/spec/cluster.go @@ -0,0 +1,81 @@ +// Copyright 2020 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package spec + +import ( + "fmt" + "path/filepath" + "reflect" + + cspec "github.com/pingcap/tiup/pkg/cluster/spec" +) + +var specManager *cspec.SpecManager + +// Metadata is the specification of generic cluster metadata +type Metadata struct { + User string `yaml:"user"` // the user to run and manage cluster on remote + Version string `yaml:"dm_version"` // the version of TiDB cluster + //EnableTLS bool `yaml:"enable_tls"` + //EnableFirewall bool `yaml:"firewall"` + + Topology *Topology `yaml:"topology"` +} + +var _ cspec.UpgradableMetadata = &Metadata{} + +// SetVersion implement UpgradableMetadata interface. +func (m *Metadata) SetVersion(s string) { + m.Version = s +} + +// SetUser implement UpgradableMetadata interface. +func (m *Metadata) SetUser(s string) { + m.User = s +} + +// GetTopology implements Metadata interface. +func (m *Metadata) GetTopology() cspec.Topology { + return m.Topology +} + +// SetTopology implements Metadata interface. +func (m *Metadata) SetTopology(topo cspec.Topology) { + dmTopo, ok := topo.(*Topology) + if !ok { + panic(fmt.Sprintln("wrong type: ", reflect.TypeOf(topo))) + } + + m.Topology = dmTopo +} + +// GetBaseMeta implements Metadata interface. +func (m *Metadata) GetBaseMeta() *cspec.BaseMeta { + return &cspec.BaseMeta{ + Version: m.Version, + User: m.User, + } +} + +// GetSpecManager return the spec manager of dm cluster. +func GetSpecManager() *cspec.SpecManager { + if specManager == nil { + specManager = cspec.NewSpec(filepath.Join(cspec.ProfileDir(), cspec.TiOpsClusterDir), func() cspec.Metadata { + return &Metadata{ + Topology: new(Topology), + } + }) + } + return specManager +} diff --git a/pkg/dm/spec/logic.go b/components/dm/spec/logic.go similarity index 83% rename from pkg/dm/spec/logic.go rename to components/dm/spec/logic.go index a217055e79..078d969dd2 100644 --- a/pkg/dm/spec/logic.go +++ b/components/dm/spec/logic.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "github.com/pingcap/tiup/pkg/logger/log" "github.com/pingcap/tiup/pkg/meta" "github.com/pingcap/tiup/pkg/utils" @@ -93,7 +94,7 @@ type instance struct { host string port int sshp int - topo *DMSSpecification + topo *Topology usedPorts []int usedDirs []string @@ -139,7 +140,7 @@ func (i *instance) InitConfig(e executor.Executor, _, _, user string, paths meta } // ScaleConfig deploy temporary config on scaling -func (i *instance) ScaleConfig(e executor.Executor, _ *DMSSpecification, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { +func (i *instance) ScaleConfig(e executor.Executor, _ *Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } @@ -248,11 +249,8 @@ func (i *instance) Status(masterList ...string) string { return i.statusFn(masterList...) } -// DMSSpecification of cluster -type DMSSpecification = DMTopologySpecification - // DMMasterComponent represents TiDB component. -type DMMasterComponent struct{ *DMSSpecification } +type DMMasterComponent struct{ *Topology } // Name implements Component interface. func (c *DMMasterComponent) Name() string { @@ -272,7 +270,7 @@ func (c *DMMasterComponent) Instances() []Instance { host: s.Host, port: s.Port, sshp: s.SSHPort, - topo: c.DMSSpecification, + topo: c.Topology, usedPorts: []int{ s.Port, @@ -326,42 +324,41 @@ func (i *DMMasterInstance) InitConfig(e executor.Executor, clusterName, clusterV } // ScaleConfig deploy temporary config on scaling -func (i *DMMasterInstance) ScaleConfig(e executor.Executor, b *spec.Specification, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { - panic("TODO") - /* - if err := i.instance.InitConfig(e, clusterName, clusterVersion, deployUser, paths); err != nil { - return err - } +func (i *DMMasterInstance) ScaleConfig(e executor.Executor, topo spec.Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { + if err := i.InitConfig(e, clusterName, clusterVersion, deployUser, paths); err != nil { + return err + } - c := b - spec := i.InstanceSpec.(MasterSpec) - cfg := scripts.NewDMMasterScaleScript( - spec.Name, - i.GetHost(), - paths.Deploy, - paths.Data[0], - paths.Log, - ).WithPort(spec.Port).WithNumaNode(spec.NumaNode).WithPeerPort(spec.PeerPort).AppendEndpoints(c.Endpoints(deployUser)...) - - fp := filepath.Join(paths.Cache, fmt.Sprintf("run_dm-master_%s_%d.sh", i.GetHost(), i.GetPort())) - log.Infof("script path: %s", fp) - if err := cfg.ConfigToFile(fp); err != nil { - return err - } + c := topo.(*Topology) + spec := i.InstanceSpec.(MasterSpec) + cfg := scripts.NewDMMasterScaleScript( + spec.Name, + i.GetHost(), + paths.Deploy, + paths.Data[0], + paths.Log, + ).WithPort(spec.Port).WithNumaNode(spec.NumaNode).WithPeerPort(spec.PeerPort).AppendEndpoints(c.Endpoints(deployUser)...) - dst := filepath.Join(paths.Deploy, "scripts", "run_dm-master.sh") - if err := e.Transfer(fp, dst, false); err != nil { - return err - } - if _, _, err := e.Execute("chmod +x "+dst, false); err != nil { - return err - } - */ + fp := filepath.Join(paths.Cache, fmt.Sprintf("run_dm-master_%s_%d.sh", i.GetHost(), i.GetPort())) + log.Infof("script path: %s", fp) + if err := cfg.ConfigToFile(fp); err != nil { + return err + } + + dst := filepath.Join(paths.Deploy, "scripts", "run_dm-master.sh") + if err := e.Transfer(fp, dst, false); err != nil { + return err + } + if _, _, err := e.Execute("chmod +x "+dst, false); err != nil { + return err + } + + return nil } // DMWorkerComponent represents DM worker component. type DMWorkerComponent struct { - *DMSSpecification + *Topology } // Name implements Component interface. @@ -382,7 +379,7 @@ func (c *DMWorkerComponent) Instances() []Instance { host: s.Host, port: s.Port, sshp: s.SSHPort, - topo: c.DMSSpecification, + topo: c.Topology, usedPorts: []int{ s.Port, @@ -432,25 +429,22 @@ func (i *DMWorkerInstance) InitConfig(e executor.Executor, clusterName, clusterV } specConfig := spec.Config - return i.mergeServerConfig(e, i.topo.ServerConfigs.Master, specConfig, paths) + return i.mergeServerConfig(e, i.topo.ServerConfigs.Worker, specConfig, paths) } // ScaleConfig deploy temporary config on scaling -func (i *DMWorkerInstance) ScaleConfig(e executor.Executor, b *spec.Specification, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { - panic("TODO") - /* - s := i.instance.topo - defer func() { - i.instance.topo = s - }() - i.instance.topo = b - return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) - */ +func (i *DMWorkerInstance) ScaleConfig(e executor.Executor, topo spec.Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { + s := i.instance.topo + defer func() { + i.instance.topo = s + }() + i.instance.topo = topo.(*Topology) + return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } // DMPortalComponent represents DM portal component. type DMPortalComponent struct { - *DMSSpecification + *Topology } // Name implements Component interface. @@ -470,7 +464,7 @@ func (c *DMPortalComponent) Instances() []Instance { host: s.Host, port: s.Port, sshp: s.SSHPort, - topo: c.DMSSpecification, + topo: c.Topology, usedPorts: []int{ s.Port, @@ -536,34 +530,31 @@ func (i *DMPortalInstance) InitConfig(e executor.Executor, clusterName, clusterV } specConfig := spec.Config - return i.mergeServerConfig(e, i.topo.ServerConfigs.Master, specConfig, paths) + return i.mergeServerConfig(e, i.topo.ServerConfigs.Portal, specConfig, paths) } // ScaleConfig deploy temporary config on scaling -func (i *DMPortalInstance) ScaleConfig(e executor.Executor, b *spec.Specification, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { - panic("TODO") - /* - s := i.instance.topo - defer func() { - i.instance.topo = s - }() - i.instance.topo = b - return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) - */ +func (i *DMPortalInstance) ScaleConfig(e executor.Executor, topo spec.Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { + s := i.instance.topo + defer func() { + i.instance.topo = s + }() + i.instance.topo = topo.(*Topology) + return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } // GetGlobalOptions returns cluster topology -func (topo *DMSSpecification) GetGlobalOptions() spec.GlobalOptions { +func (topo *Topology) GetGlobalOptions() spec.GlobalOptions { return topo.GlobalOptions } // GetMonitoredOptions returns MonitoredOptions -func (topo *DMSSpecification) GetMonitoredOptions() *spec.MonitoredOptions { +func (topo *Topology) GetMonitoredOptions() *spec.MonitoredOptions { return nil } // ComponentsByStopOrder return component in the order need to stop. -func (topo *DMSSpecification) ComponentsByStopOrder() (comps []Component) { +func (topo *Topology) ComponentsByStopOrder() (comps []Component) { comps = topo.ComponentsByStartOrder() // revert order i := 0 @@ -577,7 +568,7 @@ func (topo *DMSSpecification) ComponentsByStopOrder() (comps []Component) { } // ComponentsByStartOrder return component in the order need to start. -func (topo *DMSSpecification) ComponentsByStartOrder() (comps []Component) { +func (topo *Topology) ComponentsByStartOrder() (comps []Component) { // "dm-master", "dm-worker", "dm-portal" comps = append(comps, &DMMasterComponent{topo}) comps = append(comps, &DMWorkerComponent{topo}) @@ -586,7 +577,7 @@ func (topo *DMSSpecification) ComponentsByStartOrder() (comps []Component) { } // ComponentsByUpdateOrder return component in the order need to be updated. -func (topo *DMSSpecification) ComponentsByUpdateOrder() (comps []Component) { +func (topo *Topology) ComponentsByUpdateOrder() (comps []Component) { // "dm-master", "dm-worker", "dm-portal" comps = append(comps, &DMMasterComponent{topo}) comps = append(comps, &DMWorkerComponent{topo}) @@ -595,14 +586,14 @@ func (topo *DMSSpecification) ComponentsByUpdateOrder() (comps []Component) { } // IterComponent iterates all components in component starting order -func (topo *DMSSpecification) IterComponent(fn func(comp Component)) { +func (topo *Topology) IterComponent(fn func(comp Component)) { for _, comp := range topo.ComponentsByStartOrder() { fn(comp) } } // IterInstance iterates all instances in component starting order -func (topo *DMSSpecification) IterInstance(fn func(instance Instance)) { +func (topo *Topology) IterInstance(fn func(instance Instance)) { for _, comp := range topo.ComponentsByStartOrder() { for _, inst := range comp.Instances() { fn(inst) @@ -611,7 +602,7 @@ func (topo *DMSSpecification) IterInstance(fn func(instance Instance)) { } // IterHost iterates one instance for each host -func (topo *DMSSpecification) IterHost(fn func(instance Instance)) { +func (topo *Topology) IterHost(fn func(instance Instance)) { hostMap := make(map[string]bool) for _, comp := range topo.ComponentsByStartOrder() { for _, inst := range comp.Instances() { @@ -626,7 +617,7 @@ func (topo *DMSSpecification) IterHost(fn func(instance Instance)) { } // Endpoints returns the PD endpoints configurations -func (topo *DMSSpecification) Endpoints(user string) []*scripts.DMMasterScript { +func (topo *Topology) Endpoints(user string) []*scripts.DMMasterScript { var ends []*scripts.DMMasterScript for _, spec := range topo.Masters { deployDir := clusterutil.Abs(user, spec.DeployDir) diff --git a/pkg/dm/spec/topology_dm.go b/components/dm/spec/topology_dm.go similarity index 93% rename from pkg/dm/spec/topology_dm.go rename to components/dm/spec/topology_dm.go index 468d5bb072..352f02a9e9 100644 --- a/pkg/dm/spec/topology_dm.go +++ b/components/dm/spec/topology_dm.go @@ -84,11 +84,11 @@ type ( Portal map[string]interface{} `yaml:"dm_portal"` } - // DMTopologySpecification represents the specification of topology.yaml - DMTopologySpecification struct { - GlobalOptions GlobalOptions `yaml:"global,omitempty"` - // MonitoredOptions MonitoredOptions `yaml:"monitored,omitempty"` - ServerConfigs DMServerConfigs `yaml:"server_configs,omitempty"` + // Topology represents the specification of topology.yaml + Topology struct { + GlobalOptions GlobalOptions `yaml:"global,omitempty" validate:"global:editable"` + // MonitoredOptions MonitoredOptions `yaml:"monitored,omitempty" validate:"monitored:editable"` + ServerConfigs DMServerConfigs `yaml:"server_configs,omitempty" validate:"server_configs:ignore"` Masters []MasterSpec `yaml:"dm_master_servers"` Workers []WorkerSpec `yaml:"dm_worker_servers"` Portals []PortalSpec `yaml:"dm_portal_servers"` @@ -101,7 +101,7 @@ type ( // AllDMComponentNames contains the names of all dm components. // should include all components in ComponentsByStartOrder func AllDMComponentNames() (roles []string) { - tp := &DMTopologySpecification{} + tp := &Topology{} tp.IterComponent(func(c Component) { roles = append(roles, c.Name()) }) @@ -263,8 +263,8 @@ func (s PortalSpec) IsImported() bool { } // UnmarshalYAML sets default values when unmarshaling the topology file -func (topo *DMTopologySpecification) UnmarshalYAML(unmarshal func(interface{}) error) error { - type topology DMTopologySpecification +func (topo *Topology) UnmarshalYAML(unmarshal func(interface{}) error) error { + type topology Topology if err := unmarshal((*topology)(topo)); err != nil { return err } @@ -282,7 +282,7 @@ func (topo *DMTopologySpecification) UnmarshalYAML(unmarshal func(interface{}) e // platformConflictsDetect checks for conflicts in topology for different OS / Arch // for set to the same host / IP -func (topo *DMTopologySpecification) platformConflictsDetect() error { +func (topo *Topology) platformConflictsDetect() error { type ( conflict struct { os string @@ -343,7 +343,7 @@ func (topo *DMTopologySpecification) platformConflictsDetect() error { return nil } -func (topo *DMTopologySpecification) portConflictsDetect() error { +func (topo *Topology) portConflictsDetect() error { type ( usedPort struct { host string @@ -421,7 +421,7 @@ func (topo *DMTopologySpecification) portConflictsDetect() error { return nil } -func (topo *DMTopologySpecification) dirConflictsDetect() error { +func (topo *Topology) dirConflictsDetect() error { type ( usedDir struct { host string @@ -505,7 +505,7 @@ func (topo *DMTopologySpecification) dirConflictsDetect() error { // CountDir counts for dir paths used by any instance in the cluster with the same // prefix, useful to find potential path conflicts -func (topo *DMTopologySpecification) CountDir(targetHost, dirPrefix string) int { +func (topo *Topology) CountDir(targetHost, dirPrefix string) int { dirTypes := []string{ "DataDir", "DeployDir", @@ -571,7 +571,7 @@ func (topo *DMTopologySpecification) CountDir(targetHost, dirPrefix string) int // Validate validates the topology specification and produce error if the // specification invalid (e.g: port conflicts or directory conflicts) -func (topo *DMTopologySpecification) Validate() error { +func (topo *Topology) Validate() error { if err := topo.platformConflictsDetect(); err != nil { return err } @@ -583,8 +583,35 @@ func (topo *DMTopologySpecification) Validate() error { return topo.dirConflictsDetect() } +// BaseTopo implements Topology interface. +func (topo *Topology) BaseTopo() *spec.BaseTopo { + return &spec.BaseTopo{ + GlobalOptions: &topo.GlobalOptions, + MonitoredOptions: topo.GetMonitoredOptions(), + MasterList: topo.GetMasterList(), + } +} + +// NewPart implements ScaleOutTopology interface. +func (topo *Topology) NewPart() spec.Topology { + return &Topology{ + GlobalOptions: topo.GlobalOptions, + ServerConfigs: topo.ServerConfigs, + } +} + +// MergeTopo implements ScaleOutTopology interface. +func (topo *Topology) MergeTopo(rhs spec.Topology) spec.Topology { + other, ok := rhs.(*Topology) + if !ok { + panic("topo should be DM Topology") + } + + return topo.Merge(other) +} + // GetMasterList returns a list of Master API hosts of the current cluster -func (topo *DMTopologySpecification) GetMasterList() []string { +func (topo *Topology) GetMasterList() []string { var masterList []string for _, master := range topo.Masters { @@ -594,9 +621,9 @@ func (topo *DMTopologySpecification) GetMasterList() []string { return masterList } -// Merge returns a new DMTopologySpecification which sum old ones -func (topo *DMTopologySpecification) Merge(that *DMTopologySpecification) *DMTopologySpecification { - return &DMTopologySpecification{ +// Merge returns a new Topology which sum old ones +func (topo *Topology) Merge(that *Topology) *Topology { + return &Topology{ GlobalOptions: topo.GlobalOptions, // MonitoredOptions: topo.MonitoredOptions, ServerConfigs: topo.ServerConfigs, diff --git a/pkg/dm/spec/topology_dm_test.go b/components/dm/spec/topology_dm_test.go similarity index 94% rename from pkg/dm/spec/topology_dm_test.go rename to components/dm/spec/topology_dm_test.go index cfd85bb51b..3a689cd92a 100644 --- a/pkg/dm/spec/topology_dm_test.go +++ b/components/dm/spec/topology_dm_test.go @@ -25,14 +25,14 @@ var _ = Suite(&metaSuiteDM{}) func (s *metaSuiteDM) TestDefaultDataDir(c *C) { // Test with without global DataDir. - topo := new(DMTopologySpecification) + topo := new(Topology) topo.Masters = append(topo.Masters, MasterSpec{Host: "1.1.1.1", Port: 1111}) topo.Workers = append(topo.Workers, WorkerSpec{Host: "1.1.2.1", Port: 2221}) data, err := yaml.Marshal(topo) c.Assert(err, IsNil) // Check default value. - topo = new(DMTopologySpecification) + topo = new(Topology) err = yaml.Unmarshal(data, topo) c.Assert(err, IsNil) c.Assert(topo.GlobalOptions.DataDir, Equals, "data") @@ -42,7 +42,7 @@ func (s *metaSuiteDM) TestDefaultDataDir(c *C) { // Can keep the default value. data, err = yaml.Marshal(topo) c.Assert(err, IsNil) - topo = new(DMTopologySpecification) + topo = new(Topology) err = yaml.Unmarshal(data, topo) c.Assert(err, IsNil) c.Assert(topo.GlobalOptions.DataDir, Equals, "data") @@ -50,7 +50,7 @@ func (s *metaSuiteDM) TestDefaultDataDir(c *C) { c.Assert(topo.Workers[0].DataDir, Equals, "data") // Test with global DataDir. - topo = new(DMTopologySpecification) + topo = new(Topology) topo.GlobalOptions.DataDir = "/gloable_data" topo.Masters = append(topo.Masters, MasterSpec{Host: "1.1.1.1", Port: 1111}) topo.Masters = append(topo.Masters, MasterSpec{Host: "1.1.1.2", Port: 1112, DataDir: "/my_data"}) @@ -59,7 +59,7 @@ func (s *metaSuiteDM) TestDefaultDataDir(c *C) { data, err = yaml.Marshal(topo) c.Assert(err, IsNil) - topo = new(DMTopologySpecification) + topo = new(Topology) err = yaml.Unmarshal(data, topo) c.Assert(err, IsNil) c.Assert(topo.GlobalOptions.DataDir, Equals, "/gloable_data") @@ -70,7 +70,7 @@ func (s *metaSuiteDM) TestDefaultDataDir(c *C) { } func (s *metaSuiteDM) TestGlobalOptions(c *C) { - topo := DMTopologySpecification{} + topo := Topology{} err := yaml.Unmarshal([]byte(` global: user: "test1" @@ -96,7 +96,7 @@ dm-worker_servers: } func (s *metaSuiteDM) TestDirectoryConflicts(c *C) { - topo := DMTopologySpecification{} + topo := Topology{} err := yaml.Unmarshal([]byte(` global: user: "test1" @@ -130,7 +130,7 @@ dm-worker_servers: } func (s *metaSuiteDM) TestPortConflicts(c *C) { - topo := DMTopologySpecification{} + topo := Topology{} err := yaml.Unmarshal([]byte(` global: user: "test1" @@ -147,7 +147,7 @@ dm-worker_servers: c.Assert(err, NotNil) c.Assert(err.Error(), Equals, "port conflict for '1234' between 'dm-master_servers:172.16.5.138.peer_port' and 'dm-worker_servers:172.16.5.138.port'") - topo = DMTopologySpecification{} + topo = Topology{} err = yaml.Unmarshal([]byte(` monitored: node_exporter_port: 1234 @@ -165,7 +165,7 @@ dm-worker_servers: func (s *metaSuiteDM) TestPlatformConflicts(c *C) { // aarch64 and arm64 are equal - topo := DMTopologySpecification{} + topo := Topology{} err := yaml.Unmarshal([]byte(` global: os: "linux" @@ -179,7 +179,7 @@ dm-worker_servers: c.Assert(err, IsNil) // different arch defined for the same host - topo = DMTopologySpecification{} + topo = Topology{} err = yaml.Unmarshal([]byte(` global: os: "linux" @@ -193,7 +193,7 @@ dm-worker_servers: c.Assert(err.Error(), Equals, "platform mismatch for '172.16.5.138' between 'dm-master_servers:linux/arm64' and 'dm-worker_servers:linux/amd64'") // different os defined for the same host - topo = DMTopologySpecification{} + topo = Topology{} err = yaml.Unmarshal([]byte(` global: os: "linux" @@ -210,7 +210,7 @@ dm-worker_servers: } func (s *metaSuiteDM) TestCountDir(c *C) { - topo := DMTopologySpecification{} + topo := Topology{} err := yaml.Unmarshal([]byte(` global: diff --git a/pkg/cliutil/prepare/prepare.go b/pkg/cliutil/prepare/prepare.go index cb5e1120bd..a994bbb332 100644 --- a/pkg/cliutil/prepare/prepare.go +++ b/pkg/cliutil/prepare/prepare.go @@ -25,7 +25,6 @@ import ( "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/cluster/task" "github.com/pingcap/tiup/pkg/errutil" - "github.com/pingcap/tiup/pkg/meta" "go.uber.org/zap" ) @@ -36,36 +35,48 @@ var ( errDeployPortConflict = errNSDeploy.NewType("port_conflict", errutil.ErrTraitPreCheck) ) -func fixDir(topo *spec.Specification) func(string) string { +func fixDir(topo spec.Topology) func(string) string { return func(dir string) string { if dir != "" { - return clusterutil.Abs(topo.GlobalOptions.User, dir) + return clusterutil.Abs(topo.BaseTopo().GlobalOptions.User, dir) } return dir } } // CheckClusterDirConflict checks cluster dir conflict -func CheckClusterDirConflict(clusterSpec *meta.SpecManager, clusterName string, topo *spec.Specification) error { +func CheckClusterDirConflict(clusterSpec *spec.SpecManager, clusterName string, topo spec.Topology) error { type DirAccessor struct { dirKind string - accessor func(spec.Instance, *spec.Specification) string + accessor func(spec.Instance, spec.Topology) string } instanceDirAccessor := []DirAccessor{ - {dirKind: "deploy directory", accessor: func(instance spec.Instance, topo *spec.Specification) string { return instance.DeployDir() }}, - {dirKind: "data directory", accessor: func(instance spec.Instance, topo *spec.Specification) string { return instance.DataDir() }}, - {dirKind: "log directory", accessor: func(instance spec.Instance, topo *spec.Specification) string { return instance.LogDir() }}, + {dirKind: "deploy directory", accessor: func(instance spec.Instance, topo spec.Topology) string { return instance.DeployDir() }}, + {dirKind: "data directory", accessor: func(instance spec.Instance, topo spec.Topology) string { return instance.DataDir() }}, + {dirKind: "log directory", accessor: func(instance spec.Instance, topo spec.Topology) string { return instance.LogDir() }}, } hostDirAccessor := []DirAccessor{ - {dirKind: "monitor deploy directory", accessor: func(instance spec.Instance, topo *spec.Specification) string { - return topo.MonitoredOptions.DeployDir + {dirKind: "monitor deploy directory", accessor: func(instance spec.Instance, topo spec.Topology) string { + m := topo.BaseTopo().MonitoredOptions + if m == nil { + return "" + } + return topo.BaseTopo().MonitoredOptions.DeployDir }}, - {dirKind: "monitor data directory", accessor: func(instance spec.Instance, topo *spec.Specification) string { - return topo.MonitoredOptions.DataDir + {dirKind: "monitor data directory", accessor: func(instance spec.Instance, topo spec.Topology) string { + m := topo.BaseTopo().MonitoredOptions + if m == nil { + return "" + } + return topo.BaseTopo().MonitoredOptions.DataDir }}, - {dirKind: "monitor log directory", accessor: func(instance spec.Instance, topo *spec.Specification) string { - return topo.MonitoredOptions.LogDir + {dirKind: "monitor log directory", accessor: func(instance spec.Instance, topo spec.Topology) string { + m := topo.BaseTopo().MonitoredOptions + if m == nil { + return "" + } + return topo.BaseTopo().MonitoredOptions.LogDir }}, } @@ -89,16 +100,18 @@ func CheckClusterDirConflict(clusterSpec *meta.SpecManager, clusterName string, continue } - metadata := new(spec.ClusterMeta) + metadata := clusterSpec.NewMetadata() err := clusterSpec.Metadata(name, metadata) if err != nil { return errors.Trace(err) } - f := fixDir(metadata.Topology) - metadata.Topology.IterInstance(func(inst spec.Instance) { + topo := metadata.GetTopology() + + f := fixDir(topo) + topo.IterInstance(func(inst spec.Instance) { for _, dirAccessor := range instanceDirAccessor { - for _, dir := range strings.Split(f(dirAccessor.accessor(inst, metadata.Topology)), ",") { + for _, dir := range strings.Split(f(dirAccessor.accessor(inst, topo)), ",") { existingEntries = append(existingEntries, Entry{ clusterName: name, dirKind: dirAccessor.dirKind, @@ -108,9 +121,9 @@ func CheckClusterDirConflict(clusterSpec *meta.SpecManager, clusterName string, } } }) - metadata.Topology.IterHost(func(inst spec.Instance) { + spec.IterHost(topo, func(inst spec.Instance) { for _, dirAccessor := range hostDirAccessor { - for _, dir := range strings.Split(f(dirAccessor.accessor(inst, metadata.Topology)), ",") { + for _, dir := range strings.Split(f(dirAccessor.accessor(inst, topo)), ",") { existingEntries = append(existingEntries, Entry{ clusterName: name, dirKind: dirAccessor.dirKind, @@ -134,7 +147,8 @@ func CheckClusterDirConflict(clusterSpec *meta.SpecManager, clusterName string, } } }) - topo.IterHost(func(inst spec.Instance) { + + spec.IterHost(topo, func(inst spec.Instance) { for _, dirAccessor := range hostDirAccessor { for _, dir := range strings.Split(f(dirAccessor.accessor(inst, topo)), ",") { currentEntries = append(currentEntries, Entry{ @@ -190,7 +204,7 @@ Please change to use another directory or another host. } // CheckClusterPortConflict checks cluster dir conflict -func CheckClusterPortConflict(clusterSpec *meta.SpecManager, clusterName string, topo *spec.Specification) error { +func CheckClusterPortConflict(clusterSpec *spec.SpecManager, clusterName string, topo spec.Topology) error { type Entry struct { clusterName string instance spec.Instance @@ -210,13 +224,13 @@ func CheckClusterPortConflict(clusterSpec *meta.SpecManager, clusterName string, continue } - metadata := new(spec.ClusterMeta) + metadata := clusterSpec.NewMetadata() err := clusterSpec.Metadata(name, metadata) if err != nil { return errors.Trace(err) } - metadata.Topology.IterInstance(func(inst spec.Instance) { + metadata.GetTopology().IterInstance(func(inst spec.Instance) { for _, port := range inst.UsedPorts() { existingEntries = append(existingEntries, Entry{ clusterName: name, diff --git a/pkg/cluster/audit/audit.go b/pkg/cluster/audit/audit.go new file mode 100644 index 0000000000..906fe15ad9 --- /dev/null +++ b/pkg/cluster/audit/audit.go @@ -0,0 +1,112 @@ +// Copyright 2020 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package audit + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/fatih/color" + "github.com/juju/errors" + "github.com/pingcap/tiup/pkg/base52" + "github.com/pingcap/tiup/pkg/cliutil" + tiuputils "github.com/pingcap/tiup/pkg/utils" +) + +// ShowAuditList show the audit list. +func ShowAuditList(dir string) error { + firstLine := func(fileName string) (string, error) { + file, err := os.Open(filepath.Join(dir, fileName)) + if err != nil { + return "", errors.Trace(err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + if scanner.Scan() { + return scanner.Text(), nil + } + return "", errors.New("unknown audit log format") + } + + // Header + clusterTable := [][]string{{"ID", "Time", "Command"}} + fileInfos, err := ioutil.ReadDir(dir) + if err != nil && !os.IsNotExist(err) { + return err + } + for _, fi := range fileInfos { + if fi.IsDir() { + continue + } + ts, err := base52.Decode(fi.Name()) + if err != nil { + continue + } + t := time.Unix(ts, 0) + cmd, err := firstLine(fi.Name()) + if err != nil { + continue + } + clusterTable = append(clusterTable, []string{ + fi.Name(), + t.Format(time.RFC3339), + cmd, + }) + } + + sort.Slice(clusterTable[1:], func(i, j int) bool { + return clusterTable[i+1][1] > clusterTable[j+1][1] + }) + + cliutil.PrintTable(clusterTable, true) + return nil +} + +// OutputAuditLog outputs audit log. +func OutputAuditLog(dir string, data []byte) error { + fname := filepath.Join(dir, base52.Encode(time.Now().Unix())) + return ioutil.WriteFile(fname, data, 0644) +} + +// ShowAuditLog show the audit with the specified auditID +func ShowAuditLog(dir string, auditID string) error { + path := filepath.Join(dir, auditID) + if tiuputils.IsNotExist(path) { + return errors.Errorf("cannot find the audit log '%s'", auditID) + } + + ts, err := base52.Decode(auditID) + if err != nil { + return errors.Annotatef(err, "unrecognized audit id '%s'", auditID) + } + + content, err := ioutil.ReadFile(path) + if err != nil { + return errors.Trace(err) + } + + t := time.Unix(ts, 0) + hint := fmt.Sprintf("- OPERATION TIME: %s -", t.Format("2006-01-02T15:04:05")) + line := strings.Repeat("-", len(hint)) + _, _ = os.Stdout.WriteString(color.MagentaString("%s\n%s\n%s\n", line, hint, line)) + _, _ = os.Stdout.Write(content) + return nil +} diff --git a/pkg/cluster/manager.go b/pkg/cluster/manager.go new file mode 100644 index 0000000000..9e3aa7dab3 --- /dev/null +++ b/pkg/cluster/manager.go @@ -0,0 +1,1906 @@ +// Copyright 2020 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package cluster + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/fatih/color" + "github.com/joomcode/errorx" + perrs "github.com/pingcap/errors" + "github.com/pingcap/tiup/pkg/cliutil" + "github.com/pingcap/tiup/pkg/cliutil/prepare" + "github.com/pingcap/tiup/pkg/cluster/clusterutil" + operator "github.com/pingcap/tiup/pkg/cluster/operation" + "github.com/pingcap/tiup/pkg/cluster/spec" + "github.com/pingcap/tiup/pkg/cluster/task" + "github.com/pingcap/tiup/pkg/errutil" + "github.com/pingcap/tiup/pkg/logger/log" + "github.com/pingcap/tiup/pkg/meta" + "github.com/pingcap/tiup/pkg/set" + "github.com/pingcap/tiup/pkg/utils" + "github.com/pingcap/tiup/pkg/version" + "golang.org/x/mod/semver" + "gopkg.in/yaml.v2" +) + +var ( + errNSDeploy = errorx.NewNamespace("deploy") + errDeployNameDuplicate = errNSDeploy.NewType("name_dup", errutil.ErrTraitPreCheck) +) + +// Manager to deploy a cluster. +type Manager struct { + sysName string + specManager *spec.SpecManager +} + +// NewManager create a Manager. +func NewManager(sysName string, specManager *spec.SpecManager) *Manager { + return &Manager{ + sysName: sysName, + specManager: specManager, + } +} + +// StartCluster start the cluster with specified name. +func (m *Manager) StartCluster(name string, options operator.Options, fn ...func(b *task.Builder, metadata spec.Metadata)) error { + log.Infof("Starting cluster %s...", name) + + metadata, err := m.meta(name) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + b := task.NewBuilder(). + SSHKeySet( + m.specManager.Path(name, "ssh", "id_rsa"), + m.specManager.Path(name, "ssh", "id_rsa.pub")). + ClusterSSH(topo, base.User, options.SSHTimeout). + Func("StartCluster", func(ctx *task.Context) error { + return operator.Start(ctx, topo, options) + }) + + for _, f := range fn { + f(b, metadata) + } + + t := b.Build() + + if err := t.Execute(task.NewContext()); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.Trace(err) + } + + log.Infof("Started cluster `%s` successfully", name) + return nil +} + +// StopCluster stop the cluster. +func (m *Manager) StopCluster(clusterName string, options operator.Options) error { + metadata, err := m.meta(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + t := task.NewBuilder(). + SSHKeySet( + m.specManager.Path(clusterName, "ssh", "id_rsa"), + m.specManager.Path(clusterName, "ssh", "id_rsa.pub")). + ClusterSSH(metadata.GetTopology(), base.User, options.SSHTimeout). + Func("StopCluster", func(ctx *task.Context) error { + return operator.Stop(ctx, topo, options) + }). + Build() + + if err := t.Execute(task.NewContext()); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.Trace(err) + } + + log.Infof("Stopped cluster `%s` successfully", clusterName) + return nil +} + +// RestartCluster restart the cluster. +func (m *Manager) RestartCluster(clusterName string, options operator.Options) error { + metadata, err := m.meta(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + t := task.NewBuilder(). + SSHKeySet( + m.specManager.Path(clusterName, "ssh", "id_rsa"), + m.specManager.Path(clusterName, "ssh", "id_rsa.pub")). + ClusterSSH(topo, base.User, options.SSHTimeout). + Func("RestartCluster", func(ctx *task.Context) error { + return operator.Restart(ctx, topo, options) + }). + Build() + + if err := t.Execute(task.NewContext()); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.Trace(err) + } + + log.Infof("Restarted cluster `%s` successfully", clusterName) + return nil +} + +// ListCluster list the clusters. +func (m *Manager) ListCluster() error { + names, err := m.specManager.List() + if err != nil { + return perrs.AddStack(err) + } + + clusterTable := [][]string{ + // Header + {"Name", "User", "Version", "Path", "PrivateKey"}, + } + + for _, name := range names { + metadata, err := m.meta(name) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + return perrs.Trace(err) + } + + base := metadata.GetBaseMeta() + + clusterTable = append(clusterTable, []string{ + name, + base.User, + base.Version, + m.specManager.Path(name), + m.specManager.Path(name, "ssh", "id_rsa"), + }) + } + + cliutil.PrintTable(clusterTable, true) + return nil +} + +// DestroyCluster destroy the cluster. +func (m *Manager) DestroyCluster(clusterName string, gOpt operator.Options, destroyOpt operator.Options, skipConfirm bool) error { + metadata, err := m.meta(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) && + !errors.Is(perrs.Cause(err), spec.ErrNoTiSparkMaster) { + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + if !skipConfirm { + if err := cliutil.PromptForConfirmOrAbortError( + "This operation will destroy %s %s cluster %s and its data.\nDo you want to continue? [y/N]:", + m.sysName, + color.HiYellowString(base.Version), + color.HiYellowString(clusterName)); err != nil { + return err + } + log.Infof("Destroying cluster...") + } + + t := task.NewBuilder(). + SSHKeySet( + m.specManager.Path(clusterName, "ssh", "id_rsa"), + m.specManager.Path(clusterName, "ssh", "id_rsa.pub")). + ClusterSSH(topo, base.User, gOpt.SSHTimeout). + Func("StopCluster", func(ctx *task.Context) error { + return operator.Stop(ctx, topo, operator.Options{}) + }). + Func("DestroyCluster", func(ctx *task.Context) error { + return operator.Destroy(ctx, topo, destroyOpt) + }). + Build() + + if err := t.Execute(task.NewContext()); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.Trace(err) + } + + if err := m.specManager.Remove(clusterName); err != nil { + return perrs.Trace(err) + } + + log.Infof("Destroyed cluster `%s` successfully", clusterName) + return nil + +} + +// ExecOptions for exec shell commanm. +type ExecOptions struct { + Command string + Sudo bool +} + +// Exec shell command on host in the tidb cluster. +func (m *Manager) Exec(clusterName string, opt ExecOptions, gOpt operator.Options) error { + metadata, err := m.meta(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + filterRoles := set.NewStringSet(gOpt.Roles...) + filterNodes := set.NewStringSet(gOpt.Nodes...) + + var shellTasks []task.Task + uniqueHosts := map[string]int{} // host -> ssh-port + topo.IterInstance(func(inst spec.Instance) { + if _, found := uniqueHosts[inst.GetHost()]; !found { + if len(gOpt.Roles) > 0 && !filterRoles.Exist(inst.Role()) { + return + } + + if len(gOpt.Nodes) > 0 && !filterNodes.Exist(inst.GetHost()) { + return + } + + uniqueHosts[inst.GetHost()] = inst.GetSSHPort() + } + }) + + for host := range uniqueHosts { + shellTasks = append(shellTasks, + task.NewBuilder(). + Shell(host, opt.Command, opt.Sudo). + Build()) + } + + t := task.NewBuilder(). + SSHKeySet( + m.specManager.Path(clusterName, "ssh", "id_rsa"), + m.specManager.Path(clusterName, "ssh", "id_rsa.pub")). + ClusterSSH(topo, base.User, gOpt.SSHTimeout). + Parallel(shellTasks...). + Build() + + execCtx := task.NewContext() + if err := t.Execute(execCtx); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.Trace(err) + } + + // print outputs + for host := range uniqueHosts { + stdout, stderr, ok := execCtx.GetOutputs(host) + if !ok { + continue + } + log.Infof("Outputs of %s on %s:", + color.CyanString(opt.Command), + color.CyanString(host)) + if len(stdout) > 0 { + log.Infof("%s:\n%s", color.GreenString("stdout"), stdout) + } + if len(stderr) > 0 { + log.Infof("%s:\n%s", color.RedString("stderr"), stderr) + } + } + + return nil +} + +// Display cluster meta and topology. +func (m *Manager) Display(clusterName string, opt operator.Options) error { + metadata, err := m.meta(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) && + !errors.Is(perrs.Cause(err), spec.ErrNoTiSparkMaster) { + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + // display cluster meta + cyan := color.New(color.FgCyan, color.Bold) + fmt.Printf("%s Cluster: %s\n", m.sysName, cyan.Sprint(clusterName)) + fmt.Printf("%s Version: %s\n", m.sysName, cyan.Sprint(base.Version)) + + // display topology + clusterTable := [][]string{ + // Header + {"ID", "Role", "Host", "Ports", "OS/Arch", "Status", "Data Dir", "Deploy Dir"}, + } + + ctx := task.NewContext() + err = ctx.SetSSHKeySet(m.specManager.Path(clusterName, "ssh", "id_rsa"), + m.specManager.Path(clusterName, "ssh", "id_rsa.pub")) + if err != nil { + return perrs.AddStack(err) + } + + err = ctx.SetClusterSSH(topo, base.User, opt.SSHTimeout, opt.NativeSSH) + if err != nil { + return perrs.AddStack(err) + } + + filterRoles := set.NewStringSet(opt.Roles...) + filterNodes := set.NewStringSet(opt.Nodes...) + pdList := topo.BaseTopo().MasterList + for _, comp := range topo.ComponentsByStartOrder() { + for _, ins := range comp.Instances() { + // apply role filter + if len(filterRoles) > 0 && !filterRoles.Exist(ins.Role()) { + continue + } + // apply node filter + if len(filterNodes) > 0 && !filterNodes.Exist(ins.ID()) { + continue + } + + dataDir := "-" + insDirs := ins.UsedDirs() + deployDir := insDirs[0] + if len(insDirs) > 1 { + dataDir = insDirs[1] + } + + status := ins.Status(pdList...) + // Query the service status + if status == "-" { + e, found := ctx.GetExecutor(ins.GetHost()) + if found { + active, _ := operator.GetServiceStatus(e, ins.ServiceName()) + if parts := strings.Split(strings.TrimSpace(active), " "); len(parts) > 2 { + if parts[1] == "active" { + status = "Up" + } else { + status = parts[1] + } + } + } + } + clusterTable = append(clusterTable, []string{ + color.CyanString(ins.ID()), + ins.Role(), + ins.GetHost(), + clusterutil.JoinInt(ins.UsedPorts(), "/"), + cliutil.OsArch(ins.OS(), ins.Arch()), + formatInstanceStatus(status), + dataDir, + deployDir, + }) + + } + } + + // Sort by role,host,ports + sort.Slice(clusterTable[1:], func(i, j int) bool { + lhs, rhs := clusterTable[i+1], clusterTable[j+1] + // column: 1 => role, 2 => host, 3 => ports + for _, col := range []int{1, 2} { + if lhs[col] != rhs[col] { + return lhs[col] < rhs[col] + } + } + return lhs[3] < rhs[3] + }) + + cliutil.PrintTable(clusterTable, true) + + return nil +} + +// EditConfig let the user edit the config. +func (m *Manager) EditConfig(clusterName string, skipConfirm bool) error { + metadata, err := m.meta(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + + data, err := yaml.Marshal(topo) + if err != nil { + return perrs.AddStack(err) + } + + newTopo, err := m.editTopo(topo, data, skipConfirm) + if err != nil { + return perrs.AddStack(err) + } + + if newTopo == nil { + return nil + } + + log.Infof("Apply the change...") + metadata.SetTopology(newTopo) + err = m.specManager.SaveMeta(clusterName, metadata) + if err != nil { + return perrs.Annotate(err, "failed to save meta") + } + + log.Infof("Apply change successfully, please use `%s reload %s [-N ] [-R ]` to reload config.", cliutil.OsArgs0(), clusterName) + return nil +} + +// Reload the cluster. +func (m *Manager) Reload(clusterName string, opt operator.Options, skipRestart bool) error { + sshTimeout := opt.SSHTimeout + nativeSSH := opt.NativeSSH + + metadata, err := m.meta(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + var refreshConfigTasks []*task.StepDisplay + + hasImported := false + uniqueHosts := make(map[string]hostInfo) // host -> ssh-port, os, arch + + topo.IterInstance(func(inst spec.Instance) { + if _, found := uniqueHosts[inst.GetHost()]; !found { + uniqueHosts[inst.GetHost()] = hostInfo{ + ssh: inst.GetSSHPort(), + os: inst.OS(), + arch: inst.Arch(), + } + } + + deployDir := clusterutil.Abs(base.User, inst.DeployDir()) + // data dir would be empty for components which don't need it + dataDirs := clusterutil.MultiDirAbs(base.User, inst.DataDir()) + // log dir will always be with values, but might not used by the component + logDir := clusterutil.Abs(base.User, inst.LogDir()) + + // Download and copy the latest component to remote if the cluster is imported from Ansible + tb := task.NewBuilder().UserSSH(inst.GetHost(), inst.GetSSHPort(), base.User, opt.SSHTimeout, opt.NativeSSH) + if inst.IsImported() { + switch compName := inst.ComponentName(); compName { + case spec.ComponentGrafana, spec.ComponentPrometheus, spec.ComponentAlertManager: + version := spec.ComponentVersion(compName, base.Version) + tb.Download(compName, inst.OS(), inst.Arch(), version). + CopyComponent(compName, inst.OS(), inst.Arch(), version, "", inst.GetHost(), deployDir) + } + hasImported = true + } + + // Refresh all configuration + t := tb.InitConfig(clusterName, + base.Version, + m.specManager, + inst, base.User, + opt.IgnoreConfigCheck, + meta.DirPaths{ + Deploy: deployDir, + Data: dataDirs, + Log: logDir, + Cache: m.specManager.Path(clusterName, spec.TempConfigPath), + }). + BuildAsStep(fmt.Sprintf(" - Refresh config %s -> %s", inst.ComponentName(), inst.ID())) + refreshConfigTasks = append(refreshConfigTasks, t) + }) + + monitorConfigTasks := refreshMonitoredConfigTask( + m.specManager, + clusterName, + uniqueHosts, + *topo.BaseTopo().GlobalOptions, + topo.GetMonitoredOptions(), + sshTimeout, + nativeSSH) + + // handle dir scheme changes + if hasImported { + if err := spec.HandleImportPathMigration(clusterName); err != nil { + return perrs.AddStack(err) + } + } + + tb := task.NewBuilder(). + SSHKeySet( + m.specManager.Path(clusterName, "ssh", "id_rsa"), + m.specManager.Path(clusterName, "ssh", "id_rsa.pub")). + ClusterSSH(topo, base.User, opt.SSHTimeout). + ParallelStep("+ Refresh instance configs", refreshConfigTasks...) + + if len(monitorConfigTasks) > 0 { + tb = tb.ParallelStep("+ Refresh monitor configs", monitorConfigTasks...) + } + + if !skipRestart { + tb = tb.Func("UpgradeCluster", func(ctx *task.Context) error { + return operator.Upgrade(ctx, topo, opt) + }) + } + + t := tb.Build() + + if err := t.Execute(task.NewContext()); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.Trace(err) + } + + log.Infof("Reloaded cluster `%s` successfully", clusterName) + + return nil +} + +// Upgrade the cluster. +func (m *Manager) Upgrade(clusterName string, clusterVersion string, opt operator.Options) error { + metadata, err := m.meta(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + var ( + downloadCompTasks []task.Task // tasks which are used to download components + copyCompTasks []task.Task // tasks which are used to copy components to remote host + + uniqueComps = map[string]struct{}{} + ) + + if err := versionCompare(base.Version, clusterVersion); err != nil { + return err + } + + hasImported := false + for _, comp := range topo.ComponentsByUpdateOrder() { + for _, inst := range comp.Instances() { + version := spec.ComponentVersion(inst.ComponentName(), clusterVersion) + if version == "" { + return perrs.Errorf("unsupported component: %v", inst.ComponentName()) + } + compInfo := componentInfo{ + component: inst.ComponentName(), + version: version, + } + + // Download component from repository + key := fmt.Sprintf("%s-%s-%s-%s", compInfo.component, compInfo.version, inst.OS(), inst.Arch()) + if _, found := uniqueComps[key]; !found { + uniqueComps[key] = struct{}{} + t := task.NewBuilder(). + Download(inst.ComponentName(), inst.OS(), inst.Arch(), version). + Build() + downloadCompTasks = append(downloadCompTasks, t) + } + + deployDir := clusterutil.Abs(base.User, inst.DeployDir()) + // data dir would be empty for components which don't need it + dataDirs := clusterutil.MultiDirAbs(base.User, inst.DataDir()) + // log dir will always be with values, but might not used by the component + logDir := clusterutil.Abs(base.User, inst.LogDir()) + + // Deploy component + tb := task.NewBuilder() + if inst.IsImported() { + switch inst.ComponentName() { + case spec.ComponentPrometheus, spec.ComponentGrafana, spec.ComponentAlertManager: + tb.CopyComponent( + inst.ComponentName(), + inst.OS(), + inst.Arch(), + version, + "", // use default srcPath + inst.GetHost(), + deployDir, + ) + } + hasImported = true + } + + // backup files of the old version + tb = tb.BackupComponent(inst.ComponentName(), base.Version, inst.GetHost(), deployDir) + + // copy dependency component if needed + switch inst.ComponentName() { + case spec.ComponentTiSpark: + tb = tb.DeploySpark(inst, version, "" /* default srcPath */, deployDir) + default: + tb = tb.CopyComponent( + inst.ComponentName(), + inst.OS(), + inst.Arch(), + version, + "", // use default srcPath + inst.GetHost(), + deployDir, + ) + } + + tb.InitConfig( + clusterName, + clusterVersion, + m.specManager, + inst, + base.User, + opt.IgnoreConfigCheck, + meta.DirPaths{ + Deploy: deployDir, + Data: dataDirs, + Log: logDir, + Cache: m.specManager.Path(clusterName, spec.TempConfigPath), + }, + ) + copyCompTasks = append(copyCompTasks, tb.Build()) + } + } + + // handle dir scheme changes + if hasImported { + if err := spec.HandleImportPathMigration(clusterName); err != nil { + return err + } + } + + t := task.NewBuilder(). + SSHKeySet( + m.specManager.Path(clusterName, "ssh", "id_rsa"), + m.specManager.Path(clusterName, "ssh", "id_rsa.pub")). + ClusterSSH(topo, base.User, opt.SSHTimeout). + Parallel(downloadCompTasks...). + Parallel(copyCompTasks...). + Func("UpgradeCluster", func(ctx *task.Context) error { + return operator.Upgrade(ctx, topo, opt) + }). + Build() + + if err := t.Execute(task.NewContext()); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.Trace(err) + } + + metadata.SetVersion(clusterVersion) + + if err := m.specManager.SaveMeta(clusterName, metadata); err != nil { + return perrs.Trace(err) + } + + if err := os.RemoveAll(m.specManager.Path(clusterName, "patch")); err != nil { + return perrs.Trace(err) + } + + log.Infof("Upgraded cluster `%s` successfully", clusterName) + + return nil +} + +// Patch the cluster. +func (m *Manager) Patch(clusterName string, packagePath string, opt operator.Options, overwrite bool) error { + metadata, err := m.meta(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + if exist := utils.IsExist(packagePath); !exist { + return perrs.New("specified package not exists") + } + + insts, err := instancesToPatch(topo, opt) + if err != nil { + return err + } + if err := checkPackage(m.specManager, clusterName, insts[0].ComponentName(), insts[0].OS(), insts[0].Arch(), packagePath); err != nil { + return err + } + + var replacePackageTasks []task.Task + for _, inst := range insts { + deployDir := clusterutil.Abs(base.User, inst.DeployDir()) + tb := task.NewBuilder() + tb.BackupComponent(inst.ComponentName(), base.Version, inst.GetHost(), deployDir). + InstallPackage(packagePath, inst.GetHost(), deployDir) + replacePackageTasks = append(replacePackageTasks, tb.Build()) + } + + t := task.NewBuilder(). + SSHKeySet( + m.specManager.Path(clusterName, "ssh", "id_rsa"), + m.specManager.Path(clusterName, "ssh", "id_rsa.pub")). + ClusterSSH(topo, base.User, opt.SSHTimeout). + Parallel(replacePackageTasks...). + Func("UpgradeCluster", func(ctx *task.Context) error { + return operator.Upgrade(ctx, topo, opt) + }). + Build() + + if err := t.Execute(task.NewContext()); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.Trace(err) + } + + if overwrite { + if err := overwritePatch(m.specManager, clusterName, insts[0].ComponentName(), packagePath); err != nil { + return err + } + } + + return nil +} + +// ScaleOutOptions contains the options for scale out. +type ScaleOutOptions struct { + User string // username to login to the SSH server + IdentityFile string // path to the private key file + UsePassword bool // use password instead of identity file for ssh connection +} + +// DeployOptions contains the options for scale out. +// TODO: merge ScaleOutOptions, should check config too when scale out. +type DeployOptions struct { + User string // username to login to the SSH server + IdentityFile string // path to the private key file + UsePassword bool // use password instead of identity file for ssh connection + IgnoreConfigCheck bool // ignore config check result +} + +// Deploy a cluster. +func (m *Manager) Deploy( + clusterName string, + clusterVersion string, + topoFile string, + opt DeployOptions, + afterDeploy func(b *task.Builder, newPart spec.Topology), + skipConfirm bool, + optTimeout int64, + sshTimeout int64, + nativeSSH bool, +) error { + if err := clusterutil.ValidateClusterNameOrError(clusterName); err != nil { + return err + } + + exist, err := m.specManager.Exist(clusterName) + if err != nil { + return perrs.AddStack(err) + } + + if exist { + // FIXME: When change to use args, the suggestion text need to be updatem. + return errDeployNameDuplicate. + New("Cluster name '%s' is duplicated", clusterName). + WithProperty(cliutil.SuggestionFromFormat("Please specify another cluster name")) + } + + metadata := m.specManager.NewMetadata() + topo := metadata.GetTopology() + + // The no tispark master error is ignored, as if the tispark master is removed from the topology + // file for some reason (manual edit, for example), it is still possible to scale-out it to make + // the whole topology back to normal state. + if err := clusterutil.ParseTopologyYaml(topoFile, topo); err != nil && + !errors.Is(perrs.Cause(err), spec.ErrNoTiSparkMaster) { + return err + } + + base := topo.BaseTopo() + + if err := prepare.CheckClusterPortConflict(m.specManager, clusterName, topo); err != nil { + return err + } + if err := prepare.CheckClusterDirConflict(m.specManager, clusterName, topo); err != nil { + return err + } + + if !skipConfirm { + if err := m.confirmTopology(clusterName, clusterVersion, topo, set.NewStringSet()); err != nil { + return err + } + } + + sshConnProps, err := cliutil.ReadIdentityFileOrPassword(opt.IdentityFile, opt.UsePassword) + if err != nil { + return err + } + + if err := os.MkdirAll(m.specManager.Path(clusterName), 0755); err != nil { + return errorx.InitializationFailed. + Wrap(err, "Failed to create cluster metadata directory '%s'", m.specManager.Path(clusterName)). + WithProperty(cliutil.SuggestionFromString("Please check file system permissions and try again.")) + } + + var ( + envInitTasks []*task.StepDisplay // tasks which are used to initialize environment + downloadCompTasks []*task.StepDisplay // tasks which are used to download components + deployCompTasks []*task.StepDisplay // tasks which are used to copy components to remote host + ) + + // Initialize environment + uniqueHosts := make(map[string]hostInfo) // host -> ssh-port, os, arch + globalOptions := base.GlobalOptions + var iterErr error // error when itering over instances + iterErr = nil + topo.IterInstance(func(inst spec.Instance) { + if _, found := uniqueHosts[inst.GetHost()]; !found { + // check for "imported" parameter, it can not be true when scaling out + if inst.IsImported() { + iterErr = errors.New( + "'imported' is set to 'true' for new instance, this is only used " + + "for instances imported from tidb-ansible and make no sense when " + + "deploying new instances, please delete the line or set it to 'false' for new instances") + return // skip the host to avoid issues + } + + uniqueHosts[inst.GetHost()] = hostInfo{ + ssh: inst.GetSSHPort(), + os: inst.OS(), + arch: inst.Arch(), + } + var dirs []string + for _, dir := range []string{globalOptions.DeployDir, globalOptions.LogDir} { + if dir == "" { + continue + } + dirs = append(dirs, clusterutil.Abs(globalOptions.User, dir)) + } + // the default, relative path of data dir is under deploy dir + if strings.HasPrefix(globalOptions.DataDir, "/") { + dirs = append(dirs, globalOptions.DataDir) + } + t := task.NewBuilder(). + RootSSH( + inst.GetHost(), + inst.GetSSHPort(), + opt.User, + sshConnProps.Password, + sshConnProps.IdentityFile, + sshConnProps.IdentityFilePassphrase, + sshTimeout, + nativeSSH, + ). + EnvInit(inst.GetHost(), globalOptions.User). + Mkdir(globalOptions.User, inst.GetHost(), dirs...). + BuildAsStep(fmt.Sprintf(" - Prepare %s:%d", inst.GetHost(), inst.GetSSHPort())) + envInitTasks = append(envInitTasks, t) + } + }) + + if iterErr != nil { + return iterErr + } + + // Download missing component + downloadCompTasks = prepare.BuildDownloadCompTasks(clusterVersion, topo) + + // Deploy components to remote + topo.IterInstance(func(inst spec.Instance) { + version := spec.ComponentVersion(inst.ComponentName(), clusterVersion) + deployDir := clusterutil.Abs(globalOptions.User, inst.DeployDir()) + // data dir would be empty for components which don't need it + dataDirs := clusterutil.MultiDirAbs(globalOptions.User, inst.DataDir()) + // log dir will always be with values, but might not used by the component + logDir := clusterutil.Abs(globalOptions.User, inst.LogDir()) + // Deploy component + // prepare deployment server + t := task.NewBuilder(). + UserSSH(inst.GetHost(), inst.GetSSHPort(), globalOptions.User, sshTimeout, nativeSSH). + Mkdir(globalOptions.User, inst.GetHost(), + deployDir, logDir, + filepath.Join(deployDir, "bin"), + filepath.Join(deployDir, "conf"), + filepath.Join(deployDir, "scripts")). + Mkdir(globalOptions.User, inst.GetHost(), dataDirs...) + + // copy dependency component if needed + switch inst.ComponentName() { + case spec.ComponentTiSpark: + t = t.DeploySpark(inst, version, "" /* default srcPath */, deployDir) + default: + t = t.CopyComponent( + inst.ComponentName(), + inst.OS(), + inst.Arch(), + version, + "", // use default srcPath + inst.GetHost(), + deployDir, + ) + } + + // generate configs for the component + t = t.InitConfig( + clusterName, + clusterVersion, + m.specManager, + inst, + globalOptions.User, + opt.IgnoreConfigCheck, + meta.DirPaths{ + Deploy: deployDir, + Data: dataDirs, + Log: logDir, + Cache: m.specManager.Path(clusterName, spec.TempConfigPath), + }, + ) + + deployCompTasks = append(deployCompTasks, + t.BuildAsStep(fmt.Sprintf(" - Copy %s -> %s", inst.ComponentName(), inst.GetHost())), + ) + }) + + // Deploy monitor relevant components to remote + dlTasks, dpTasks := buildMonitoredDeployTask( + m.specManager, + clusterName, + uniqueHosts, + globalOptions, + topo.GetMonitoredOptions(), + clusterVersion, + sshTimeout, + nativeSSH, + ) + downloadCompTasks = append(downloadCompTasks, dlTasks...) + deployCompTasks = append(deployCompTasks, dpTasks...) + + builder := task.NewBuilder(). + Step("+ Generate SSH keys", + task.NewBuilder().SSHKeyGen(m.specManager.Path(clusterName, "ssh", "id_rsa")).Build()). + ParallelStep("+ Download TiDB components", downloadCompTasks...). + ParallelStep("+ Initialize target host environments", envInitTasks...). + ParallelStep("+ Copy files", deployCompTasks...) + + if afterDeploy != nil { + afterDeploy(builder, topo) + } + + t := builder.Build() + + if err := t.Execute(task.NewContext()); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.AddStack(err) + } + + metadata.SetUser(globalOptions.User) + metadata.SetVersion(clusterVersion) + err = m.specManager.SaveMeta(clusterName, metadata) + + if err != nil { + return perrs.AddStack(err) + } + + hint := color.New(color.Bold).Sprintf("%s start %s", cliutil.OsArgs0(), clusterName) + log.Infof("Deployed cluster `%s` successfully, you can start the cluster via `%s`", clusterName, hint) + return nil +} + +// ScaleIn the cluster. +func (m *Manager) ScaleIn( + clusterName string, + skipConfirm bool, + sshTimeout int64, + force bool, + nodes []string, + scale func(builer *task.Builder, metadata spec.Metadata), +) error { + if !skipConfirm { + if err := cliutil.PromptForConfirmOrAbortError( + "This operation will delete the %s nodes in `%s` and all their data.\nDo you want to continue? [y/N]:", + strings.Join(nodes, ","), + color.HiYellowString(clusterName)); err != nil { + return err + } + + if force { + if err := cliutil.PromptForConfirmOrAbortError( + "Forcing scale in is unsafe and may result in data lost for stateful components.\nDo you want to continue? [y/N]:", + ); err != nil { + return err + } + } + + log.Infof("Scale-in nodes...") + } + + metadata, err := m.meta(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + // ignore conflict check error, node may be deployed by former version + // that lack of some certain conflict checks + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + // Regenerate configuration + var regenConfigTasks []task.Task + hasImported := false + deletedNodes := set.NewStringSet(nodes...) + for _, component := range topo.ComponentsByStartOrder() { + for _, instance := range component.Instances() { + if deletedNodes.Exist(instance.ID()) { + continue + } + deployDir := clusterutil.Abs(base.User, instance.DeployDir()) + // data dir would be empty for components which don't need it + dataDirs := clusterutil.MultiDirAbs(base.User, instance.DataDir()) + // log dir will always be with values, but might not used by the component + logDir := clusterutil.Abs(base.User, instance.LogDir()) + + // Download and copy the latest component to remote if the cluster is imported from Ansible + tb := task.NewBuilder() + if instance.IsImported() { + switch compName := instance.ComponentName(); compName { + case spec.ComponentGrafana, spec.ComponentPrometheus, spec.ComponentAlertManager: + version := spec.ComponentVersion(compName, base.Version) + tb.Download(compName, instance.OS(), instance.Arch(), version). + CopyComponent( + compName, + instance.OS(), + instance.Arch(), + version, + "", // use default srcPath + instance.GetHost(), + deployDir, + ) + } + hasImported = true + } + + t := tb.InitConfig(clusterName, + base.Version, + m.specManager, + instance, + base.User, + true, // always ignore config check result in scale in + meta.DirPaths{ + Deploy: deployDir, + Data: dataDirs, + Log: logDir, + Cache: m.specManager.Path(clusterName, spec.TempConfigPath), + }, + ).Build() + regenConfigTasks = append(regenConfigTasks, t) + } + } + + // handle dir scheme changes + if hasImported { + if err := spec.HandleImportPathMigration(clusterName); err != nil { + return err + } + } + + b := task.NewBuilder(). + SSHKeySet( + m.specManager.Path(clusterName, "ssh", "id_rsa"), + m.specManager.Path(clusterName, "ssh", "id_rsa.pub")). + ClusterSSH(topo, base.User, sshTimeout) + + // TODO: support command scale in operation. + scale(b, metadata) + + t := b.Parallel(regenConfigTasks...).Build() + + if err := t.Execute(task.NewContext()); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.Trace(err) + } + + log.Infof("Scaled cluster `%s` in successfully", clusterName) + + return nil +} + +// ScaleOut scale out the cluster. +func (m *Manager) ScaleOut( + clusterName string, + topoFile string, + afterDeploy func(b *task.Builder, newPart spec.Topology), + final func(b *task.Builder, name string, meta spec.Metadata), + opt ScaleOutOptions, + skipConfirm bool, + optTimeout int64, + sshTimeout int64, + nativeSSH bool, +) error { + metadata, err := m.meta(clusterName) + if err != nil { // not allowing validation errors + return perrs.AddStack(err) + } + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + + // not allowing validation errors + if err := topo.Validate(); err != nil { + return err + } + + // Inherit existing global configuration. We must assign the inherited values before unmarshalling + // because some default value rely on the global options and monitored options. + newPart := topo.NewPart() + + // The no tispark master error is ignored, as if the tispark master is removed from the topology + // file for some reason (manual edit, for example), it is still possible to scale-out it to make + // the whole topology back to normal state. + if err := clusterutil.ParseTopologyYaml(topoFile, newPart); err != nil && + !errors.Is(perrs.Cause(err), spec.ErrNoTiSparkMaster) { + return err + } + + if err := validateNewTopo(newPart); err != nil { + return err + } + + // Abort scale out operation if the merged topology is invalid + mergedTopo := topo.MergeTopo(newPart) + if err := mergedTopo.Validate(); err != nil { + return err + } + + if err := prepare.CheckClusterPortConflict(m.specManager, clusterName, mergedTopo); err != nil { + return err + } + if err := prepare.CheckClusterDirConflict(m.specManager, clusterName, mergedTopo); err != nil { + return err + } + + patchedComponents := set.NewStringSet() + newPart.IterInstance(func(instance spec.Instance) { + if utils.IsExist(m.specManager.Path(clusterName, spec.PatchDirName, instance.ComponentName()+".tar.gz")) { + patchedComponents.Insert(instance.ComponentName()) + } + }) + + if !skipConfirm { + // patchedComponents are components that have been patched and overwrited + if err := m.confirmTopology(clusterName, base.Version, newPart, patchedComponents); err != nil { + return err + } + } + + sshConnProps, err := cliutil.ReadIdentityFileOrPassword(opt.IdentityFile, opt.UsePassword) + if err != nil { + return err + } + + // Build the scale out tasks + t, err := buildScaleOutTask(m, clusterName, metadata, mergedTopo, opt, sshConnProps, newPart, patchedComponents, optTimeout, sshTimeout, nativeSSH, afterDeploy, final) + if err != nil { + return err + } + + if err := t.Execute(task.NewContext()); err != nil { + if errorx.Cast(err) != nil { + // FIXME: Map possible task errors and give suggestions. + return err + } + return perrs.Trace(err) + } + + log.Infof("Scaled cluster `%s` out successfully", clusterName) + + return nil +} + +func (m *Manager) meta(name string) (metadata spec.Metadata, err error) { + exist, err := m.specManager.Exist(name) + if err != nil { + return nil, perrs.AddStack(err) + } + + if !exist { + return nil, perrs.Errorf("%s cluster `%s` not exists", m.sysName, name) + } + + metadata = m.specManager.NewMetadata() + err = m.specManager.Metadata(name, metadata) + if err != nil { + return metadata, perrs.AddStack(err) + } + + return metadata, nil +} + +// 1. Write Topology to a temporary file. +// 2. Open file in editor. +// 3. Check and update Topology. +// 4. Save meta file. +func (m *Manager) editTopo(origTopo spec.Topology, data []byte, skipConfirm bool) (spec.Topology, error) { + file, err := ioutil.TempFile(os.TempDir(), "*") + if err != nil { + return nil, perrs.AddStack(err) + } + + name := file.Name() + + _, err = io.Copy(file, bytes.NewReader(data)) + if err != nil { + return nil, perrs.AddStack(err) + } + + err = file.Close() + if err != nil { + return nil, perrs.AddStack(err) + } + + err = utils.OpenFileInEditor(name) + if err != nil { + return nil, perrs.AddStack(err) + } + + // Now user finish editing the file. + newData, err := ioutil.ReadFile(name) + if err != nil { + return nil, perrs.AddStack(err) + } + + newTopo := m.specManager.NewMetadata().GetTopology() + err = yaml.UnmarshalStrict(newData, newTopo) + if err != nil { + fmt.Print(color.RedString("New topology could not be saved: ")) + log.Infof("Failed to parse topology file: %v", err) + if cliutil.PromptForConfirmReverse("Do you want to continue editing? [Y/n]: ") { + return m.editTopo(origTopo, newData, skipConfirm) + } + log.Infof("Nothing changem.") + return nil, nil + } + + // report error if immutable field has been changed + if err := utils.ValidateSpecDiff(origTopo, newTopo); err != nil { + fmt.Print(color.RedString("New topology could not be saved: ")) + log.Errorf("%s", err) + if cliutil.PromptForConfirmReverse("Do you want to continue editing? [Y/n]: ") { + return m.editTopo(origTopo, newData, skipConfirm) + } + log.Infof("Nothing changem.") + return nil, nil + + } + + origData, err := yaml.Marshal(origTopo) + if err != nil { + return nil, perrs.AddStack(err) + } + + if bytes.Equal(origData, newData) { + log.Infof("The file has nothing changed") + return nil, nil + } + + utils.ShowDiff(string(origData), string(newData), os.Stdout) + + if !skipConfirm { + if err := cliutil.PromptForConfirmOrAbortError( + color.HiYellowString("Please check change highlight above, do you want to apply the change? [y/N]:"), + ); err != nil { + return nil, err + } + } + + return newTopo, nil +} + +func formatInstanceStatus(status string) string { + lowercaseStatus := strings.ToLower(status) + + startsWith := func(prefixs ...string) bool { + for _, prefix := range prefixs { + if strings.HasPrefix(lowercaseStatus, prefix) { + return true + } + } + return false + } + + switch { + case startsWith("up|l"): // up|l, up|l|ui + return color.HiGreenString(status) + case startsWith("up"): + return color.GreenString(status) + case startsWith("down", "err"): // down, down|ui + return color.RedString(status) + case startsWith("tombstone", "disconnected"), strings.Contains(status, "offline"): + return color.YellowString(status) + default: + return status + } +} + +func versionCompare(curVersion, newVersion string) error { + // Can always upgrade to 'nightly' event the current version is 'nightly' + if newVersion == version.NightlyVersion { + return nil + } + + switch semver.Compare(curVersion, newVersion) { + case -1: + return nil + case 0, 1: + return perrs.Errorf("please specify a higher version than %s", curVersion) + default: + return perrs.Errorf("unreachable") + } +} + +type componentInfo struct { + component string + version string +} + +func instancesToPatch(topo spec.Topology, options operator.Options) ([]spec.Instance, error) { + roleFilter := set.NewStringSet(options.Roles...) + nodeFilter := set.NewStringSet(options.Nodes...) + components := topo.ComponentsByStartOrder() + components = operator.FilterComponent(components, roleFilter) + + instances := []spec.Instance{} + comps := []string{} + for _, com := range components { + insts := operator.FilterInstance(com.Instances(), nodeFilter) + if len(insts) > 0 { + comps = append(comps, com.Name()) + } + instances = append(instances, insts...) + } + if len(comps) > 1 { + return nil, fmt.Errorf("can't patch more than one component at once: %v", comps) + } + + if len(instances) == 0 { + return nil, fmt.Errorf("no instance found on specifid role(%v) and nodes(%v)", options.Roles, options.Nodes) + } + + return instances, nil +} + +func checkPackage(specManager *spec.SpecManager, clusterName, comp, nodeOS, arch, packagePath string) error { + metadata, err := spec.ClusterMetadata(clusterName) + if err != nil && !errors.Is(perrs.Cause(err), meta.ErrValidate) { + return err + } + + ver := spec.ComponentVersion(comp, metadata.Version) + repo, err := clusterutil.NewRepository(nodeOS, arch) + if err != nil { + return err + } + entry, err := repo.ComponentBinEntry(comp, ver) + if err != nil { + return err + } + + checksum, err := utils.Checksum(packagePath) + if err != nil { + return err + } + cacheDir := specManager.Path(clusterName, "cache", comp+"-"+checksum[:7]) + if err := os.MkdirAll(cacheDir, 0755); err != nil { + return err + } + if err := exec.Command("tar", "-xvf", packagePath, "-C", cacheDir).Run(); err != nil { + return err + } + + if exists := utils.IsExist(path.Join(cacheDir, entry)); !exists { + return fmt.Errorf("entry %s not found in package %s", entry, packagePath) + } + + return nil +} + +func overwritePatch(specManager *spec.SpecManager, clusterName, comp, packagePath string) error { + if err := os.MkdirAll(specManager.Path(clusterName, spec.PatchDirName), 0755); err != nil { + return err + } + + checksum, err := utils.Checksum(packagePath) + if err != nil { + return err + } + + tg := specManager.Path(clusterName, spec.PatchDirName, comp+"-"+checksum[:7]+".tar.gz") + if !utils.IsExist(tg) { + if err := utils.CopyFile(packagePath, tg); err != nil { + return err + } + } + + symlink := specManager.Path(clusterName, spec.PatchDirName, comp+".tar.gz") + if utils.IsSymExist(symlink) { + os.Remove(symlink) + } + return os.Symlink(tg, symlink) +} + +// validateNewTopo checks the new part of scale-out topology to make sure it's supported +func validateNewTopo(topo spec.Topology) (err error) { + topo.IterInstance(func(instance spec.Instance) { + // check for "imported" parameter, it can not be true when scaling out + if instance.IsImported() { + err = errors.New( + "'imported' is set to 'true' for new instance, this is only used " + + "for instances imported from tidb-ansible and make no sense when " + + "scaling out, please delete the line or set it to 'false' for new instances") + return + } + }) + return err +} + +func (m *Manager) confirmTopology(clusterName, version string, topo spec.Topology, patchedRoles set.StringSet) error { + log.Infof("Please confirm your topology:") + + cyan := color.New(color.FgCyan, color.Bold) + fmt.Printf("%s Cluster: %s\n", m.sysName, cyan.Sprint(clusterName)) + fmt.Printf("%s Version: %s\n", m.sysName, cyan.Sprint(version)) + + clusterTable := [][]string{ + // Header + {"Type", "Host", "Ports", "OS/Arch", "Directories"}, + } + + topo.IterInstance(func(instance spec.Instance) { + comp := instance.ComponentName() + if patchedRoles.Exist(comp) { + comp = comp + " (patched)" + } + clusterTable = append(clusterTable, []string{ + comp, + instance.GetHost(), + clusterutil.JoinInt(instance.UsedPorts(), "/"), + cliutil.OsArch(instance.OS(), instance.Arch()), + strings.Join(instance.UsedDirs(), ","), + }) + }) + + cliutil.PrintTable(clusterTable, true) + + log.Warnf("Attention:") + log.Warnf(" 1. If the topology is not what you expected, check your yaml file.") + log.Warnf(" 2. Please confirm there is no port/directory conflicts in same host.") + if len(patchedRoles) != 0 { + log.Errorf(" 3. The component marked as `patched` has been replaced by previous patch commanm.") + } + + if spec, ok := topo.(*spec.Specification); ok { + if len(spec.TiSparkMasters) > 0 || len(spec.TiSparkWorkers) > 0 { + log.Warnf("There are TiSpark nodes defined in the topology, please note that you'll need to manually install Java Runtime Environment (JRE) 8 on the host, other wise the TiSpark nodes will fail to start.") + log.Warnf("You may read the OpenJDK doc for a reference: https://openjdk.java.net/install/") + } + } + + return cliutil.PromptForConfirmOrAbortError("Do you want to continue? [y/N]: ") +} + +func buildScaleOutTask( + m *Manager, + clusterName string, + metadata spec.Metadata, + mergedTopo spec.Topology, + opt ScaleOutOptions, + sshConnProps *cliutil.SSHConnectionProps, + newPart spec.Topology, + patchedComponents set.StringSet, + optTimeout int64, + sshTimeout int64, + nativeSSH bool, + afterDeploy func(b *task.Builder, newPart spec.Topology), + final func(b *task.Builder, name string, meta spec.Metadata), +) (task.Task, error) { + var ( + envInitTasks []task.Task // tasks which are used to initialize environment + downloadCompTasks []task.Task // tasks which are used to download components + deployCompTasks []task.Task // tasks which are used to copy components to remote host + refreshConfigTasks []task.Task // tasks which are used to refresh configuration + ) + + topo := metadata.GetTopology() + base := metadata.GetBaseMeta() + specManager := m.specManager + + // Initialize the environments + initializedHosts := set.NewStringSet() + metadata.GetTopology().IterInstance(func(instance spec.Instance) { + initializedHosts.Insert(instance.GetHost()) + }) + // uninitializedHosts are hosts which haven't been initialized yet + uninitializedHosts := make(map[string]hostInfo) // host -> ssh-port, os, arch + newPart.IterInstance(func(instance spec.Instance) { + if host := instance.GetHost(); !initializedHosts.Exist(host) { + if _, found := uninitializedHosts[host]; found { + return + } + + uninitializedHosts[host] = hostInfo{ + ssh: instance.GetSSHPort(), + os: instance.OS(), + arch: instance.Arch(), + } + + var dirs []string + globalOptions := metadata.GetTopology().BaseTopo().GlobalOptions + for _, dir := range []string{globalOptions.DeployDir, globalOptions.DataDir, globalOptions.LogDir} { + for _, dirname := range strings.Split(dir, ",") { + if dirname == "" { + continue + } + dirs = append(dirs, clusterutil.Abs(globalOptions.User, dirname)) + } + } + t := task.NewBuilder(). + RootSSH( + instance.GetHost(), + instance.GetSSHPort(), + opt.User, + sshConnProps.Password, + sshConnProps.IdentityFile, + sshConnProps.IdentityFilePassphrase, + sshTimeout, + nativeSSH, + ). + EnvInit(instance.GetHost(), base.User). + Mkdir(globalOptions.User, instance.GetHost(), dirs...). + Build() + envInitTasks = append(envInitTasks, t) + } + }) + + // Download missing component + downloadCompTasks = convertStepDisplaysToTasks(prepare.BuildDownloadCompTasks(base.Version, newPart)) + + // Deploy the new topology and refresh the configuration + newPart.IterInstance(func(inst spec.Instance) { + version := spec.ComponentVersion(inst.ComponentName(), base.Version) + deployDir := clusterutil.Abs(base.User, inst.DeployDir()) + // data dir would be empty for components which don't need it + dataDirs := clusterutil.MultiDirAbs(base.User, inst.DataDir()) + // log dir will always be with values, but might not used by the component + logDir := clusterutil.Abs(base.User, inst.LogDir()) + + // Deploy component + tb := task.NewBuilder(). + UserSSH(inst.GetHost(), inst.GetSSHPort(), base.User, sshTimeout, nativeSSH). + Mkdir(base.User, inst.GetHost(), + deployDir, logDir, + filepath.Join(deployDir, "bin"), + filepath.Join(deployDir, "conf"), + filepath.Join(deployDir, "scripts")). + Mkdir(base.User, inst.GetHost(), dataDirs...) + + srcPath := "" + if patchedComponents.Exist(inst.ComponentName()) { + srcPath = specManager.Path(clusterName, spec.PatchDirName, inst.ComponentName()+".tar.gz") + } + + // copy dependency component if needed + switch inst.ComponentName() { + case spec.ComponentTiSpark: + tb = tb.DeploySpark(inst, version, srcPath, deployDir) + default: + tb.CopyComponent( + inst.ComponentName(), + inst.OS(), + inst.Arch(), + version, + srcPath, + inst.GetHost(), + deployDir, + ) + } + + t := tb.ScaleConfig(clusterName, + base.Version, + m.specManager, + topo, + inst, + base.User, + meta.DirPaths{ + Deploy: deployDir, + Data: dataDirs, + Log: logDir, + }, + ).Build() + deployCompTasks = append(deployCompTasks, t) + }) + + hasImported := false + + mergedTopo.IterInstance(func(inst spec.Instance) { + deployDir := clusterutil.Abs(base.User, inst.DeployDir()) + // data dir would be empty for components which don't need it + dataDirs := clusterutil.MultiDirAbs(base.User, inst.DataDir()) + // log dir will always be with values, but might not used by the component + logDir := clusterutil.Abs(base.User, inst.LogDir()) + + // Download and copy the latest component to remote if the cluster is imported from Ansible + tb := task.NewBuilder() + if inst.IsImported() { + switch compName := inst.ComponentName(); compName { + case spec.ComponentGrafana, spec.ComponentPrometheus, spec.ComponentAlertManager: + version := spec.ComponentVersion(compName, base.Version) + tb.Download(compName, inst.OS(), inst.Arch(), version). + CopyComponent(compName, inst.OS(), inst.Arch(), version, "", inst.GetHost(), deployDir) + } + hasImported = true + } + + // Refresh all configuration + t := tb.InitConfig(clusterName, + base.Version, + m.specManager, + inst, + base.User, + true, // always ignore config check result in scale out + meta.DirPaths{ + Deploy: deployDir, + Data: dataDirs, + Log: logDir, + Cache: specManager.Path(clusterName, spec.TempConfigPath), + }, + ).Build() + refreshConfigTasks = append(refreshConfigTasks, t) + }) + + // handle dir scheme changes + if hasImported { + if err := spec.HandleImportPathMigration(clusterName); err != nil { + return task.NewBuilder().Build(), err + } + } + + // Deploy monitor relevant components to remote + dlTasks, dpTasks := buildMonitoredDeployTask( + specManager, + clusterName, + uninitializedHosts, + topo.BaseTopo().GlobalOptions, + topo.BaseTopo().MonitoredOptions, + base.Version, + sshTimeout, + nativeSSH, + ) + downloadCompTasks = append(downloadCompTasks, convertStepDisplaysToTasks(dlTasks)...) + deployCompTasks = append(deployCompTasks, convertStepDisplaysToTasks(dpTasks)...) + + builder := task.NewBuilder(). + SSHKeySet( + specManager.Path(clusterName, "ssh", "id_rsa"), + specManager.Path(clusterName, "ssh", "id_rsa.pub")). + Parallel(downloadCompTasks...). + Parallel(envInitTasks...). + ClusterSSH(topo, base.User, sshTimeout). + Parallel(deployCompTasks...) + + if afterDeploy != nil { + afterDeploy(builder, newPart) + } + + // TODO: find another way to make sure current cluster started + builder. + Func("StartCluster", func(ctx *task.Context) error { + return operator.Start(ctx, metadata.GetTopology(), operator.Options{OptTimeout: optTimeout}) + }). + ClusterSSH(newPart, base.User, sshTimeout). + Func("Save meta", func(_ *task.Context) error { + metadata.SetTopology(mergedTopo) + return m.specManager.SaveMeta(clusterName, metadata) + }). + Func("StartCluster", func(ctx *task.Context) error { + return operator.Start(ctx, newPart, operator.Options{OptTimeout: optTimeout}) + }). + Parallel(refreshConfigTasks...). + Func("RestartCluster", func(ctx *task.Context) error { + return operator.Restart(ctx, metadata.GetTopology(), operator.Options{ + Roles: []string{spec.ComponentPrometheus}, + OptTimeout: optTimeout, + }) + }) + + if final != nil { + final(builder, clusterName, metadata) + } + + return builder.Build(), nil +} + +type hostInfo struct { + ssh int // ssh port of host + os string // operating system + arch string // cpu architecture + // vendor string +} + +// Deprecated +func convertStepDisplaysToTasks(t []*task.StepDisplay) []task.Task { + tasks := make([]task.Task, 0, len(t)) + for _, sd := range t { + tasks = append(tasks, sd) + } + return tasks +} + +func buildMonitoredDeployTask( + specManager *spec.SpecManager, + clusterName string, + uniqueHosts map[string]hostInfo, // host -> ssh-port, os, arch + globalOptions *spec.GlobalOptions, + monitoredOptions *spec.MonitoredOptions, + version string, + sshTimeout int64, + nativeSSH bool, +) (downloadCompTasks []*task.StepDisplay, deployCompTasks []*task.StepDisplay) { + if monitoredOptions == nil { + return + } + + uniqueCompOSArch := make(map[string]struct{}) // comp-os-arch -> {} + // monitoring agents + for _, comp := range []string{spec.ComponentNodeExporter, spec.ComponentBlackboxExporter} { + version := spec.ComponentVersion(comp, version) + + for host, info := range uniqueHosts { + // populate unique os/arch set + key := fmt.Sprintf("%s-%s-%s", comp, info.os, info.arch) + if _, found := uniqueCompOSArch[key]; !found { + uniqueCompOSArch[key] = struct{}{} + downloadCompTasks = append(downloadCompTasks, task.NewBuilder(). + Download(comp, info.os, info.arch, version). + BuildAsStep(fmt.Sprintf(" - Download %s:%s (%s/%s)", comp, version, info.os, info.arch))) + } + + deployDir := clusterutil.Abs(globalOptions.User, monitoredOptions.DeployDir) + // data dir would be empty for components which don't need it + dataDir := monitoredOptions.DataDir + // the default data_dir is relative to deploy_dir + if dataDir != "" && !strings.HasPrefix(dataDir, "/") { + dataDir = filepath.Join(deployDir, dataDir) + } + // log dir will always be with values, but might not used by the component + logDir := clusterutil.Abs(globalOptions.User, monitoredOptions.LogDir) + // Deploy component + t := task.NewBuilder(). + UserSSH(host, info.ssh, globalOptions.User, sshTimeout, nativeSSH). + Mkdir(globalOptions.User, host, + deployDir, dataDir, logDir, + filepath.Join(deployDir, "bin"), + filepath.Join(deployDir, "conf"), + filepath.Join(deployDir, "scripts")). + CopyComponent( + comp, + info.os, + info.arch, + version, + "", + host, + deployDir, + ). + MonitoredConfig( + clusterName, + comp, + host, + globalOptions.ResourceControl, + monitoredOptions, + globalOptions.User, + meta.DirPaths{ + Deploy: deployDir, + Data: []string{dataDir}, + Log: logDir, + Cache: specManager.Path(clusterName, spec.TempConfigPath), + }, + ). + BuildAsStep(fmt.Sprintf(" - Copy %s -> %s", comp, host)) + deployCompTasks = append(deployCompTasks, t) + } + } + return +} + +func refreshMonitoredConfigTask( + specManager *spec.SpecManager, + clusterName string, + uniqueHosts map[string]hostInfo, // host -> ssh-port, os, arch + globalOptions spec.GlobalOptions, + monitoredOptions *spec.MonitoredOptions, + sshTimeout int64, + nativeSSH bool, +) []*task.StepDisplay { + if monitoredOptions == nil { + return nil + } + + tasks := []*task.StepDisplay{} + // monitoring agents + for _, comp := range []string{spec.ComponentNodeExporter, spec.ComponentBlackboxExporter} { + for host, info := range uniqueHosts { + deployDir := clusterutil.Abs(globalOptions.User, monitoredOptions.DeployDir) + // data dir would be empty for components which don't need it + dataDir := monitoredOptions.DataDir + // the default data_dir is relative to deploy_dir + if dataDir != "" && !strings.HasPrefix(dataDir, "/") { + dataDir = filepath.Join(deployDir, dataDir) + } + // log dir will always be with values, but might not used by the component + logDir := clusterutil.Abs(globalOptions.User, monitoredOptions.LogDir) + // Generate configs + t := task.NewBuilder(). + UserSSH(host, info.ssh, globalOptions.User, sshTimeout, nativeSSH). + MonitoredConfig( + clusterName, + comp, + host, + globalOptions.ResourceControl, + monitoredOptions, + globalOptions.User, + meta.DirPaths{ + Deploy: deployDir, + Data: []string{dataDir}, + Log: logDir, + Cache: specManager.Path(clusterName, spec.TempConfigPath), + }, + ). + BuildAsStep(fmt.Sprintf(" - Refresh config %s -> %s", comp, host)) + tasks = append(tasks, t) + } + } + return tasks +} diff --git a/pkg/cluster/manager_test.go b/pkg/cluster/manager_test.go new file mode 100644 index 0000000000..13a36d41a5 --- /dev/null +++ b/pkg/cluster/manager_test.go @@ -0,0 +1,88 @@ +// Copyright 2020 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package cluster + +import ( + "testing" + + "github.com/pingcap/tiup/pkg/cluster/spec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestVersionCompare(t *testing.T) { + var err error + + err = versionCompare("v4.0.0", "v4.0.1") + assert.Nil(t, err) + + err = versionCompare("v4.0.1", "v4.0.0") + assert.NotNil(t, err) + + err = versionCompare("v4.0.0", "nightly") + assert.Nil(t, err) + + err = versionCompare("nightly", "nightly") + assert.Nil(t, err) +} + +func TestValidateNewTopo(t *testing.T) { + topo := spec.Specification{} + err := yaml.Unmarshal([]byte(` +global: + user: "test1" + ssh_port: 220 + deploy_dir: "test-deploy" + data_dir: "test-data" +tidb_servers: + - host: 172.16.5.138 + deploy_dir: "tidb-deploy" +pd_servers: + - host: 172.16.5.53 + data_dir: "pd-data" +`), &topo) + assert := require.New(t) + assert.Nil(err) + err = validateNewTopo(&topo) + assert.Nil(err) + + topo = spec.Specification{} + err = yaml.Unmarshal([]byte(` +tidb_servers: + - host: 172.16.5.138 + imported: true + deploy_dir: "tidb-deploy" +pd_servers: + - host: 172.16.5.53 + data_dir: "pd-data" +`), &topo) + assert.Nil(err) + err = validateNewTopo(&topo) + assert.NotNil(err) + + topo = spec.Specification{} + err = yaml.Unmarshal([]byte(` +global: + user: "test3" + deploy_dir: "test-deploy" + data_dir: "test-data" +pd_servers: + - host: 172.16.5.53 + imported: true +`), &topo) + assert.Nil(err) + err = validateNewTopo(&topo) + assert.NotNil(err) +} diff --git a/pkg/cluster/operation/action.go b/pkg/cluster/operation/action.go index a834505f84..fed4301a51 100644 --- a/pkg/cluster/operation/action.go +++ b/pkg/cluster/operation/action.go @@ -32,7 +32,7 @@ import ( // Start the cluster. func Start( getter ExecutorGetter, - cluster spec.Spec, + cluster spec.Topology, options Options, ) error { uniqueHosts := set.NewStringSet() @@ -65,7 +65,7 @@ func Start( // Stop the cluster. func Stop( getter ExecutorGetter, - cluster *spec.Specification, + cluster spec.Topology, options Options, ) error { roleFilter := set.NewStringSet(options.Roles...) @@ -87,8 +87,10 @@ func Stop( for _, inst := range insts { instCount[inst.GetHost()]-- if instCount[inst.GetHost()] == 0 { - if err := StopMonitored(getter, inst, cluster.MonitoredOptions, options.OptTimeout); err != nil { - return err + if cluster.GetMonitoredOptions() != nil { + if err := StopMonitored(getter, inst, cluster.GetMonitoredOptions(), options.OptTimeout); err != nil { + return err + } } } } @@ -323,7 +325,7 @@ func DestroyClusterTombstone( // Restart the cluster. func Restart( getter ExecutorGetter, - cluster *spec.Specification, + cluster spec.Topology, options Options, ) error { err := Stop(getter, cluster, options) @@ -382,6 +384,44 @@ func StartMonitored(getter ExecutorGetter, instance spec.Instance, options *spec return nil } +func restartInstance(getter ExecutorGetter, ins spec.Instance, timeout int64) error { + e := getter.Get(ins.GetHost()) + log.Infof("\tRestarting instance %s", ins.GetHost()) + + // Restart by systemd. + c := module.SystemdModuleConfig{ + Unit: ins.ServiceName(), + ReloadDaemon: true, + Action: "restart", + Timeout: time.Second * time.Duration(timeout), + } + systemd := module.NewSystemdModule(c) + stdout, stderr, err := systemd.Execute(e) + + if len(stdout) > 0 { + fmt.Println(string(stdout)) + } + if len(stderr) > 0 { + log.Errorf(string(stderr)) + } + + if err != nil { + return errors.Annotatef(err, "failed to restart: %s", ins.GetHost()) + } + + // Check ready. + err = ins.Ready(e, timeout) + if err != nil { + str := fmt.Sprintf("\t%s failed to restart: %s", ins.GetHost(), err) + log.Errorf(str) + return errors.Annotatef(err, str) + } + + log.Infof("\tRestart %s success", ins.GetHost()) + + return nil +} + // RestartComponent restarts the component. func RestartComponent(getter ExecutorGetter, instances []spec.Instance, timeout int64) error { if len(instances) <= 0 { @@ -392,39 +432,10 @@ func RestartComponent(getter ExecutorGetter, instances []spec.Instance, timeout log.Infof("Restarting component %s", name) for _, ins := range instances { - e := getter.Get(ins.GetHost()) - log.Infof("\tRestarting instance %s", ins.GetHost()) - - // Restart by systemd. - c := module.SystemdModuleConfig{ - Unit: ins.ServiceName(), - ReloadDaemon: true, - Action: "restart", - Timeout: time.Second * time.Duration(timeout), - } - systemd := module.NewSystemdModule(c) - stdout, stderr, err := systemd.Execute(e) - - if len(stdout) > 0 { - fmt.Println(string(stdout)) - } - if len(stderr) > 0 { - log.Errorf(string(stderr)) - } - + err := restartInstance(getter, ins, timeout) if err != nil { - return errors.Annotatef(err, "failed to restart: %s", ins.GetHost()) + return errors.AddStack(err) } - - // Check ready. - err = ins.Ready(e, timeout) - if err != nil { - str := fmt.Sprintf("\t%s failed to restart: %s", ins.GetHost(), err) - log.Errorf(str) - return errors.Annotatef(err, str) - } - - log.Infof("\tRestart %s success", ins.GetHost()) } return nil @@ -511,7 +522,7 @@ func StartComponent(getter ExecutorGetter, instances []spec.Instance, options Op } // StopMonitored stop BlackboxExporter and NodeExporter -func StopMonitored(getter ExecutorGetter, instance spec.Instance, options spec.MonitoredOptions, timeout int64) error { +func StopMonitored(getter ExecutorGetter, instance spec.Instance, options *spec.MonitoredOptions, timeout int64) error { ports := map[string]int{ spec.ComponentNodeExporter: options.NodeExporterPort, spec.ComponentBlackboxExporter: options.BlackboxExporterPort, diff --git a/pkg/cluster/operation/destroy.go b/pkg/cluster/operation/destroy.go index dccc179516..185e6812b0 100644 --- a/pkg/cluster/operation/destroy.go +++ b/pkg/cluster/operation/destroy.go @@ -29,7 +29,7 @@ import ( // Destroy the cluster. func Destroy( getter ExecutorGetter, - cluster *spec.Specification, + cluster spec.Topology, options Options, ) error { uniqueHosts := set.NewStringSet() @@ -49,8 +49,10 @@ func Destroy( for _, inst := range insts { instCount[inst.GetHost()]-- if instCount[inst.GetHost()] == 0 { - if err := DestroyMonitored(getter, inst, cluster.MonitoredOptions, options.OptTimeout); err != nil { - return err + if cluster.GetMonitoredOptions() != nil { + if err := DestroyMonitored(getter, inst, cluster.GetMonitoredOptions(), options.OptTimeout); err != nil { + return err + } } } } @@ -58,7 +60,7 @@ func Destroy( // Delete all global deploy directory for host := range uniqueHosts { - if err := DeleteGlobalDirs(getter, host, cluster.GlobalOptions); err != nil { + if err := DeleteGlobalDirs(getter, host, cluster.BaseTopo().GlobalOptions); err != nil { return nil } } @@ -67,7 +69,11 @@ func Destroy( } // DeleteGlobalDirs deletes all global directories if they are empty -func DeleteGlobalDirs(getter ExecutorGetter, host string, options spec.GlobalOptions) error { +func DeleteGlobalDirs(getter ExecutorGetter, host string, options *spec.GlobalOptions) error { + if options == nil { + return nil + } + e := getter.Get(host) log.Infof("Clean global directories %s", host) for _, dir := range []string{options.LogDir, options.DeployDir, options.DataDir} { @@ -103,7 +109,7 @@ func DeleteGlobalDirs(getter ExecutorGetter, host string, options spec.GlobalOpt } // DestroyMonitored destroy the monitored service. -func DestroyMonitored(getter ExecutorGetter, inst spec.Instance, options spec.MonitoredOptions, timeout int64) error { +func DestroyMonitored(getter ExecutorGetter, inst spec.Instance, options *spec.MonitoredOptions, timeout int64) error { e := getter.Get(inst.GetHost()) log.Infof("Destroying monitored %s", inst.GetHost()) @@ -164,13 +170,8 @@ func DestroyMonitored(getter ExecutorGetter, inst spec.Instance, options spec.Mo return nil } -// topologySpecification is an interface used to get essential information of a cluster -type topologySpecification interface { - CountDir(string, string) int // count how many time a path is used by instances in cluster -} - // DestroyComponent destroy the instances. -func DestroyComponent(getter ExecutorGetter, instances []spec.Instance, cls topologySpecification, options Options) error { +func DestroyComponent(getter ExecutorGetter, instances []spec.Instance, cls spec.Topology, options Options) error { if len(instances) <= 0 { return nil } @@ -184,22 +185,14 @@ func DestroyComponent(getter ExecutorGetter, instances []spec.Instance, cls topo for _, ins := range instances { // Some data of instances will be retained - dataRetained := (len(retainDataRoles) > 0 && retainDataRoles.Exist(ins.ComponentName())) || - (len(retainDataNodes) > 0 && retainDataNodes.Exist(ins.ID())) + dataRetained := retainDataRoles.Exist(ins.ComponentName()) || + retainDataNodes.Exist(ins.ID()) e := getter.Get(ins.GetHost()) log.Infof("Destroying instance %s", ins.GetHost()) var dataDirs []string - switch name { - case spec.ComponentTiKV, - spec.ComponentPD, - spec.ComponentPump, - spec.ComponentDrainer, - spec.ComponentPrometheus, - spec.ComponentAlertManager: - dataDirs = []string{ins.DataDir()} - case spec.ComponentTiFlash: + if len(ins.DataDir()) > 0 { dataDirs = strings.Split(ins.DataDir(), ",") } diff --git a/pkg/cluster/operation/scale_in.go b/pkg/cluster/operation/scale_in.go index e5c5ecfcf8..55a6b8e939 100644 --- a/pkg/cluster/operation/scale_in.go +++ b/pkg/cluster/operation/scale_in.go @@ -19,7 +19,9 @@ import ( "strconv" "time" + "github.com/fatih/color" "github.com/pingcap/errors" + dm "github.com/pingcap/tiup/components/dm/spec" "github.com/pingcap/tiup/pkg/cluster/api" "github.com/pingcap/tiup/pkg/cluster/clusterutil" "github.com/pingcap/tiup/pkg/cluster/spec" @@ -233,8 +235,8 @@ func ScaleInCluster( return errors.Annotatef(err, "failed to destroy %s", component.Name()) } } else { - log.Warnf("The component `%s` will be destroyed when display cluster info when it become tombstone, maybe exists in several minutes or hours", - component.Name()) + log.Warnf(color.YellowString("The component `%s` will be destroyed when display cluster info when it become tombstone, maybe exists in several minutes or hours", + component.Name())) } } } @@ -325,15 +327,14 @@ func deleteMember( return nil } -/* -// ScaleInDMCluster scales in the cluster +// ScaleInDMCluster scale in dm cluster. func ScaleInDMCluster( getter ExecutorGetter, - spec *meta.DMSSpecification, + spec *dm.Topology, options Options, ) error { // instances by uuid - instances := map[string]meta.Instance{} + instances := map[string]dm.Instance{} // make sure all nodeIds exists in topology for _, component := range spec.ComponentsByStartOrder() { @@ -343,7 +344,7 @@ func ScaleInDMCluster( } // Clean components - deletedDiff := map[string][]meta.Instance{} + deletedDiff := map[string][]dm.Instance{} deletedNodes := set.NewStringSet(options.Nodes...) for nodeID := range deletedNodes { inst, found := instances[nodeID] @@ -354,7 +355,7 @@ func ScaleInDMCluster( } // Cannot delete all DM DMMaster servers - if len(deletedDiff[meta.ComponentDMMaster]) == len(spec.Masters) { + if len(deletedDiff[dm.ComponentDMMaster]) == len(spec.Masters) { return errors.New("cannot delete all dm-master servers") } @@ -365,10 +366,10 @@ func ScaleInDMCluster( continue } // just try stop and destroy - if err := StopComponent(getter, []meta.Instance{instance}, options.OptTimeout); err != nil { + if err := StopComponent(getter, []dm.Instance{instance}, options.OptTimeout); err != nil { log.Warnf("failed to stop %s: %v", component.Name(), err) } - if err := DestroyComponent(getter, []meta.Instance{instance}, spec, options); err != nil { + if err := DestroyComponent(getter, []dm.Instance{instance}, spec, options); err != nil { log.Warnf("failed to destroy %s: %v", component.Name(), err) } @@ -381,7 +382,7 @@ func ScaleInDMCluster( // At least a DMMaster server exists var dmMasterClient *api.DMMasterClient var dmMasterEndpoint []string - for _, instance := range (&meta.DMMasterComponent{DMSSpecification: spec}).Instances() { + for _, instance := range (&dm.DMMasterComponent{Topology: spec}).Instances() { if !deletedNodes.Exist(instance.ID()) { dmMasterEndpoint = append(dmMasterEndpoint, addr(instance)) } @@ -404,22 +405,22 @@ func ScaleInDMCluster( continue } - if err := StopComponent(getter, []meta.Instance{instance}, options.OptTimeout); err != nil { + if err := StopComponent(getter, []dm.Instance{instance}, options.OptTimeout); err != nil { return errors.Annotatef(err, "failed to stop %s", component.Name()) } - if err := DestroyComponent(getter, []meta.Instance{instance}, spec, options); err != nil { + if err := DestroyComponent(getter, []dm.Instance{instance}, spec, options); err != nil { return errors.Annotatef(err, "failed to destroy %s", component.Name()) } switch component.Name() { - case meta.ComponentDMMaster: - name := instance.(*meta.DMMasterInstance).Name + case dm.ComponentDMMaster: + name := instance.(*dm.DMMasterInstance).Name err := dmMasterClient.OfflineMaster(name, retryOpt) if err != nil { return errors.AddStack(err) } - case meta.ComponentDMWorker: - name := instance.(*meta.DMWorkerInstance).Name + case dm.ComponentDMWorker: + name := instance.(*dm.DMWorkerInstance).Name err := dmMasterClient.OfflineWorker(name, retryOpt) if err != nil { return errors.AddStack(err) @@ -430,4 +431,3 @@ func ScaleInDMCluster( return nil } -*/ diff --git a/pkg/cluster/operation/telemetry.go b/pkg/cluster/operation/telemetry.go index 443e95c6af..3baf594452 100644 --- a/pkg/cluster/operation/telemetry.go +++ b/pkg/cluster/operation/telemetry.go @@ -32,7 +32,7 @@ import ( func GetNodeInfo( ctx context.Context, getter ExecutorGetter, - topo *spec.Specification, + topo spec.Topology, ) (nodes []*telemetry.NodeInfo, err error) { ver := version.NewTiUPVersion().String() dir := "/tmp/_cluster" diff --git a/pkg/cluster/operation/upgrade.go b/pkg/cluster/operation/upgrade.go index 9f8383978b..2b06098dcb 100644 --- a/pkg/cluster/operation/upgrade.go +++ b/pkg/cluster/operation/upgrade.go @@ -15,11 +15,8 @@ package operator import ( "strconv" - "time" "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/cluster/api" - "github.com/pingcap/tiup/pkg/cluster/clusterutil" "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/logger/log" "github.com/pingcap/tiup/pkg/set" @@ -28,21 +25,14 @@ import ( // Upgrade the cluster. func Upgrade( getter ExecutorGetter, - cluster *spec.Specification, + topo spec.Topology, options Options, ) error { roleFilter := set.NewStringSet(options.Roles...) nodeFilter := set.NewStringSet(options.Nodes...) - components := cluster.ComponentsByUpdateOrder() + components := topo.ComponentsByUpdateOrder() components = FilterComponent(components, roleFilter) - leaderAware := set.NewStringSet(spec.ComponentPD, spec.ComponentTiKV) - - timeoutOpt := &clusterutil.RetryOption{ - Timeout: time.Second * time.Duration(options.APITimeout), - Delay: time.Second * 2, - } - for _, component := range components { instances := FilterInstance(component.Instances(), nodeFilter) if len(instances) < 1 { @@ -50,69 +40,34 @@ func Upgrade( } // Transfer leader of evict leader if the component is TiKV/PD in non-force mode - if !options.Force && leaderAware.Exist(component.Name()) { - pdClient := api.NewPDClient(cluster.GetPDList(), 5*time.Second, nil) - switch component.Name() { - case spec.ComponentPD: - log.Infof("Restarting component %s", component.Name()) - for _, instance := range instances { - leader, err := pdClient.GetLeader() - if err != nil { - return errors.Annotatef(err, "failed to get PD leader %s", instance.GetHost()) - } + log.Infof("Restarting component %s", component.Name()) - if len(cluster.PDServers) > 1 && leader.Name == instance.(*spec.PDInstance).Name { - if err := pdClient.EvictPDLeader(timeoutOpt); err != nil { - return errors.Annotatef(err, "failed to evict PD leader %s", instance.GetHost()) - } - } + for _, instance := range instances { + var rollingInstance spec.RollingUpdateInstance + var isRollingInstance bool - if err := stopInstance(getter, instance, options.OptTimeout); err != nil { - return errors.Annotatef(err, "failed to stop %s", instance.GetHost()) - } - if err := startInstance(getter, instance, options.OptTimeout); err != nil { - return errors.Annotatef(err, "failed to start %s", instance.GetHost()) - } - } + if !options.Force { + rollingInstance, isRollingInstance = instance.(spec.RollingUpdateInstance) + } - case spec.ComponentTiKV: - log.Infof("Restarting component %s", component.Name()) - pdClient := api.NewPDClient(cluster.GetPDList(), 5*time.Second, nil) - // Make sure there's leader of PD. - // Although we evict pd leader when restart pd, - // But when there's only one PD instance the pd might not serve request right away after restart. - err := pdClient.WaitLeader(timeoutOpt) + if isRollingInstance { + err := rollingInstance.PreRestart(topo, int(options.APITimeout)) if err != nil { - return errors.Annotate(err, "failed to wait leader") + return errors.AddStack(err) } + } - for _, instance := range instances { - if err := pdClient.EvictStoreLeader(addr(instance), timeoutOpt); err != nil { - if clusterutil.IsTimeoutOrMaxRetry(err) { - log.Warnf("Ignore evicting store leader from %s, %v", instance.ID(), err) - } else { - return errors.Annotatef(err, "failed to evict store leader %s", instance.GetHost()) - } - } + if err := restartInstance(getter, instance, options.OptTimeout); err != nil { + return errors.AddStack(err) + } - if err := stopInstance(getter, instance, options.OptTimeout); err != nil { - return errors.Annotatef(err, "failed to stop %s", instance.GetHost()) - } - if err := startInstance(getter, instance, options.OptTimeout); err != nil { - return errors.Annotatef(err, "failed to start %s", instance.GetHost()) - } - // remove store leader evict scheduler after restart - if err := pdClient.RemoveStoreEvict(addr(instance)); err != nil { - return errors.Annotatef(err, "failed to remove evict store scheduler for %s", instance.GetHost()) - } + if isRollingInstance { + err := rollingInstance.PostRestart(topo) + if err != nil { + return errors.AddStack(err) } } - continue - } - - if err := RestartComponent(getter, instances, options.OptTimeout); err != nil { - return errors.Annotatef(err, "failed to restart %s", component.Name()) } } diff --git a/pkg/cluster/spec/alertmanager.go b/pkg/cluster/spec/alertmanager.go index 4f1c0b86b0..0d2459f554 100644 --- a/pkg/cluster/spec/alertmanager.go +++ b/pkg/cluster/spec/alertmanager.go @@ -136,10 +136,10 @@ func (i *AlertManagerInstance) InitConfig(e executor.Executor, clusterName, clus } // ScaleConfig deploy temporary config on scaling -func (i *AlertManagerInstance) ScaleConfig(e executor.Executor, cluster *Specification, +func (i *AlertManagerInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName string, clusterVersion string, deployUser string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() - i.instance.topo = cluster + i.instance.topo = mustBeClusterTopo(topo) return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } diff --git a/pkg/cluster/spec/cdc.go b/pkg/cluster/spec/cdc.go index f4d689fca3..ddf335b4ad 100644 --- a/pkg/cluster/spec/cdc.go +++ b/pkg/cluster/spec/cdc.go @@ -100,12 +100,12 @@ type CDCInstance struct { } // ScaleConfig deploy temporary config on scaling -func (i *CDCInstance) ScaleConfig(e executor.Executor, cluster *Specification, clusterName, clusterVersion, user string, paths meta.DirPaths) error { +func (i *CDCInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName, clusterVersion, user string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() - i.instance.topo = cluster + i.instance.topo = mustBeClusterTopo(topo) return i.InitConfig(e, clusterName, clusterVersion, user, paths) } diff --git a/pkg/cluster/spec/drainer.go b/pkg/cluster/spec/drainer.go index f7763a89d3..df7a542ce1 100644 --- a/pkg/cluster/spec/drainer.go +++ b/pkg/cluster/spec/drainer.go @@ -105,12 +105,12 @@ type DrainerInstance struct { } // ScaleConfig deploy temporary config on scaling -func (i *DrainerInstance) ScaleConfig(e executor.Executor, cluster *Specification, clusterName, clusterVersion, user string, paths meta.DirPaths) error { +func (i *DrainerInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName, clusterVersion, user string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() - i.instance.topo = cluster + i.instance.topo = mustBeClusterTopo(topo) return i.InitConfig(e, clusterName, clusterVersion, user, paths) } diff --git a/pkg/cluster/spec/grafana.go b/pkg/cluster/spec/grafana.go index 24ffbe7057..8d54cb47c3 100644 --- a/pkg/cluster/spec/grafana.go +++ b/pkg/cluster/spec/grafana.go @@ -154,10 +154,11 @@ func (i *GrafanaInstance) InitConfig(e executor.Executor, clusterName, clusterVe } // ScaleConfig deploy temporary config on scaling -func (i *GrafanaInstance) ScaleConfig(e executor.Executor, cluster *Specification, +func (i *GrafanaInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName string, clusterVersion string, deployUser string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() + cluster := mustBeClusterTopo(topo) i.instance.topo = cluster.Merge(i.instance.topo) return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } diff --git a/pkg/cluster/spec/instance.go b/pkg/cluster/spec/instance.go index 11969e429b..d80218186f 100644 --- a/pkg/cluster/spec/instance.go +++ b/pkg/cluster/spec/instance.go @@ -56,6 +56,13 @@ type Component interface { Instances() []Instance } +// RollingUpdateInstance represent a instance need to transfer state when restart. +// e.g transfer leader. +type RollingUpdateInstance interface { + PreRestart(topo Topology, apiTimeoutSeconds int) error + PostRestart(topo Topology) error +} + // Instance represents the instance. type Instance interface { InstanceSpec @@ -63,7 +70,7 @@ type Instance interface { Ready(executor.Executor, int64) error WaitForDown(executor.Executor, int64) error InitConfig(e executor.Executor, clusterName string, clusterVersion string, deployUser string, paths meta.DirPaths) error - ScaleConfig(e executor.Executor, cluster *Specification, clusterName string, clusterVersion string, deployUser string, paths meta.DirPaths) error + ScaleConfig(e executor.Executor, topo Topology, clusterName string, clusterVersion string, deployUser string, paths meta.DirPaths) error PrepareStart() error ComponentName() string InstanceName() string diff --git a/pkg/cluster/spec/pd.go b/pkg/cluster/spec/pd.go index dc4e6afc0a..eabde86d57 100644 --- a/pkg/cluster/spec/pd.go +++ b/pkg/cluster/spec/pd.go @@ -18,9 +18,11 @@ import ( "io/ioutil" "path/filepath" "strings" + "time" "github.com/pingcap/errors" "github.com/pingcap/tiup/pkg/cluster/api" + "github.com/pingcap/tiup/pkg/cluster/clusterutil" "github.com/pingcap/tiup/pkg/cluster/executor" "github.com/pingcap/tiup/pkg/cluster/template/scripts" "github.com/pingcap/tiup/pkg/logger/log" @@ -217,13 +219,15 @@ func (i *PDInstance) InitConfig(e executor.Executor, clusterName, clusterVersion } // ScaleConfig deploy temporary config on scaling -func (i *PDInstance) ScaleConfig(e executor.Executor, cluster *Specification, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { +func (i *PDInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { // We need pd.toml here, but we don't need to check it if err := i.InitConfig(e, clusterName, clusterVersion, deployUser, paths); err != nil && errors.Cause(err) != ErrorCheckConfig { return err } + cluster := mustBeClusterTopo(topo) + spec := i.InstanceSpec.(PDSpec) cfg := scripts.NewPDScaleScript( i.Name, @@ -252,3 +256,39 @@ func (i *PDInstance) ScaleConfig(e executor.Executor, cluster *Specification, cl } return nil } + +var _ RollingUpdateInstance = &PDInstance{} + +// PreRestart implements RollingUpdateInstance interface. +func (i *PDInstance) PreRestart(topo Topology, apiTimeoutSeconds int) error { + timeoutOpt := &clusterutil.RetryOption{ + Timeout: time.Second * time.Duration(apiTimeoutSeconds), + Delay: time.Second * 2, + } + + tidbTopo, ok := topo.(*Specification) + if !ok { + panic("topo should be type of tidb topology") + } + + pdClient := api.NewPDClient(tidbTopo.GetPDList(), 5*time.Second, nil) + + leader, err := pdClient.GetLeader() + if err != nil { + return errors.Annotatef(err, "failed to get PD leader %s", i.GetHost()) + } + + if len(tidbTopo.PDServers) > 1 && leader.Name == i.Name { + if err := pdClient.EvictPDLeader(timeoutOpt); err != nil { + return errors.Annotatef(err, "failed to evict PD leader %s", i.GetHost()) + } + } + + return nil +} + +// PostRestart implements RollingUpdateInstance interface. +func (i *PDInstance) PostRestart(topo Topology) error { + // intend to do nothing + return nil +} diff --git a/pkg/cluster/spec/profile.go b/pkg/cluster/spec/profile.go index d1817ececc..a11e0e5867 100644 --- a/pkg/cluster/spec/profile.go +++ b/pkg/cluster/spec/profile.go @@ -19,7 +19,6 @@ import ( "path" "path/filepath" - "github.com/pingcap/tiup/pkg/meta" utils2 "github.com/pingcap/tiup/pkg/utils" "github.com/pingcap/errors" @@ -64,7 +63,11 @@ func Initialize(base string) error { } clusterBaseDir := filepath.Join(profileDir, TiOpsClusterDir) - tidbSpec = meta.NewSpec(clusterBaseDir) + tidbSpec = NewSpec(clusterBaseDir, func() Metadata { + return &ClusterMeta{ + Topology: new(Specification), + } + }) initialized = true // make sure the dir exist return utils2.CreateDir(profileDir) diff --git a/pkg/cluster/spec/prometheus.go b/pkg/cluster/spec/prometheus.go index 47c9a513b1..f601f86e31 100644 --- a/pkg/cluster/spec/prometheus.go +++ b/pkg/cluster/spec/prometheus.go @@ -185,10 +185,10 @@ func (i *MonitorInstance) InitConfig(e executor.Executor, clusterName, clusterVe } // ScaleConfig deploy temporary config on scaling -func (i *MonitorInstance) ScaleConfig(e executor.Executor, cluster *Specification, +func (i *MonitorInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName string, clusterVersion string, deployUser string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() - i.instance.topo = cluster + i.instance.topo = mustBeClusterTopo(topo) return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } diff --git a/pkg/cluster/spec/pump.go b/pkg/cluster/spec/pump.go index 3e5581774c..3a9b0e4676 100644 --- a/pkg/cluster/spec/pump.go +++ b/pkg/cluster/spec/pump.go @@ -104,12 +104,12 @@ type PumpInstance struct { } // ScaleConfig deploy temporary config on scaling -func (i *PumpInstance) ScaleConfig(e executor.Executor, cluster *Specification, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { +func (i *PumpInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() - i.instance.topo = cluster + i.instance.topo = mustBeClusterTopo(topo) return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } diff --git a/pkg/cluster/spec/spec.go b/pkg/cluster/spec/spec.go index cebbd61e27..fa82c3b86b 100644 --- a/pkg/cluster/spec/spec.go +++ b/pkg/cluster/spec/spec.go @@ -109,18 +109,96 @@ type ( } ) -// Spec represents specification of the cluster. -type Spec interface { +// BaseTopo is the base info to topology. +type BaseTopo struct { + GlobalOptions *GlobalOptions + MonitoredOptions *MonitoredOptions + MasterList []string +} + +// Topology represents specification of the cluster. +type Topology interface { + BaseTopo() *BaseTopo + // Validate validates the topology specification and produce error if the + // specification invalid (e.g: port conflicts or directory conflicts) + Validate() error + // Instances() []Instance ComponentsByStartOrder() []Component + ComponentsByStopOrder() []Component + ComponentsByUpdateOrder() []Component + IterInstance(fn func(instance Instance)) GetMonitoredOptions() *MonitoredOptions + // count how many time a path is used by instances in cluster + CountDir(host string, dir string) int + + ScaleOutTopology +} + +// BaseMeta is the base info of metadata. +type BaseMeta struct { + User string + Version string + OpsVer *string `yaml:"last_ops_ver,omitempty"` // the version of ourself that updated the meta last time +} + +// Metadata of a cluster. +type Metadata interface { + GetTopology() Topology + SetTopology(topo Topology) + GetBaseMeta() *BaseMeta + + UpgradableMetadata +} + +// ScaleOutTopology represents a scale out metadata. +type ScaleOutTopology interface { + // Inherit existing global configuration. We must assign the inherited values before unmarshalling + // because some default value rely on the global options and monitored options. + // TODO: we should separate the unmarshal and setting default value. + NewPart() Topology + MergeTopo(topo Topology) Topology +} + +// UpgradableMetadata represents a upgradable Metadata. +type UpgradableMetadata interface { + SetVersion(s string) + SetUser(u string) +} + +// NewPart implements ScaleOutTopology interface. +func (s *Specification) NewPart() Topology { + return &Specification{ + GlobalOptions: s.GlobalOptions, + MonitoredOptions: s.MonitoredOptions, + ServerConfigs: s.ServerConfigs, + } +} + +// MergeTopo implements ScaleOutTopology interface. +func (s *Specification) MergeTopo(topo Topology) Topology { + other, ok := topo.(*Specification) + if !ok { + panic("topo should be Specification") + } + + return s.Merge(other) } -// GetMonitoredOptions implements Spec interface. +// GetMonitoredOptions implements Topology interface. func (s *Specification) GetMonitoredOptions() *MonitoredOptions { return &s.MonitoredOptions } +// BaseTopo implements Topology interface. +func (s *Specification) BaseTopo() *BaseTopo { + return &BaseTopo{ + GlobalOptions: &s.GlobalOptions, + MonitoredOptions: s.GetMonitoredOptions(), + MasterList: s.GetPDList(), + } +} + // AllComponentNames contains the names of all components. // should include all components in ComponentsByStartOrder func AllComponentNames() (roles []string) { @@ -811,9 +889,9 @@ func (s *Specification) IterInstance(fn func(instance Instance)) { } // IterHost iterates one instance for each host -func (s *Specification) IterHost(fn func(instance Instance)) { +func IterHost(topo Topology, fn func(instance Instance)) { hostMap := make(map[string]bool) - for _, comp := range s.ComponentsByStartOrder() { + for _, comp := range topo.ComponentsByStartOrder() { for _, inst := range comp.Instances() { host := inst.GetHost() _, ok := hostMap[host] diff --git a/pkg/meta/spec.go b/pkg/cluster/spec/spec_manager.go similarity index 86% rename from pkg/meta/spec.go rename to pkg/cluster/spec/spec_manager.go index dc204317b2..cb0cf13419 100644 --- a/pkg/meta/spec.go +++ b/pkg/cluster/spec/spec_manager.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package meta +package spec import ( "io/ioutil" @@ -23,6 +23,7 @@ import ( "github.com/pingcap/tiup/pkg/cliutil" "github.com/pingcap/tiup/pkg/file" "github.com/pingcap/tiup/pkg/utils" + "github.com/pingcap/tiup/pkg/version" "gopkg.in/yaml.v2" ) @@ -43,18 +44,27 @@ const ( BackupDirName = "backup" ) +//revive:disable + // SpecManager control management of spec meta data. type SpecManager struct { - base string + base string + newMeta func() Metadata } // NewSpec create a spec instance. -func NewSpec(base string) *SpecManager { +func NewSpec(base string, newMeta func() Metadata) *SpecManager { return &SpecManager{ - base: base, + base: base, + newMeta: newMeta, } } +// NewMetadata alloc a Metadata according the type. +func (s *SpecManager) NewMetadata() Metadata { + return s.newMeta() +} + // Path returns the full path to a subpath (file or directory) of a // cluster, it is a subdir in the profile dir of the user, with the cluster name // as its name. @@ -72,7 +82,7 @@ func (s *SpecManager) Path(cluster string, subpath ...string) string { } // SaveMeta save the meta with specified cluster name. -func (s *SpecManager) SaveMeta(clusterName string, meta interface{}) error { +func (s *SpecManager) SaveMeta(clusterName string, meta Metadata) error { wrapError := func(err error) *errorx.Error { return ErrSaveMetaFailed.Wrap(err, "Failed to save cluster metadata") } @@ -93,6 +103,11 @@ func (s *SpecManager) SaveMeta(clusterName string, meta interface{}) error { return wrapError(err) } + opsVer := meta.GetBaseMeta().OpsVer + if opsVer != nil { + *opsVer = version.NewTiUPVersion().String() + } + err = file.SaveFileWithBackup(metaFile, data, backupDir) if err != nil { return wrapError(err) @@ -133,6 +148,11 @@ func (s *SpecManager) Exist(name string) (exist bool, err error) { return true, nil } +// Remove remove the data with specified cluster name. +func (s *SpecManager) Remove(name string) error { + return os.RemoveAll(s.Path(name)) +} + // List return the cluster names. func (s *SpecManager) List() (names []string, err error) { fileInfos, err := ioutil.ReadDir(s.base) diff --git a/pkg/cluster/spec/spec_manager_test.go b/pkg/cluster/spec/spec_manager_test.go new file mode 100644 index 0000000000..5e178bfda5 --- /dev/null +++ b/pkg/cluster/spec/spec_manager_test.go @@ -0,0 +1,174 @@ +// Copyright 2020 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package spec + +import ( + "io/ioutil" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +type TestMetadata struct { + BaseMeta + Topo *TestTopology +} + +func (m *TestMetadata) SetVersion(s string) { + m.BaseMeta.Version = s +} + +func (m *TestMetadata) SetUser(s string) { + m.BaseMeta.User = s +} + +func (m *TestMetadata) GetTopology() Topology { + return m.Topo +} + +func (m *TestMetadata) GetBaseMeta() *BaseMeta { + return &m.BaseMeta +} + +func (m *TestMetadata) SetTopology(topo Topology) { + testTopo, ok := topo.(*TestTopology) + if !ok { + panic("wrong toplogy type") + } + + m.Topo = testTopo +} + +type TestTopology struct { + base BaseTopo +} + +func (t *TestTopology) Validate() error { + return nil +} + +func (t *TestTopology) NewPart() Topology { + panic("not support") +} + +func (t *TestTopology) MergeTopo(topo Topology) Topology { + panic("not support") +} + +func (t *TestTopology) BaseTopo() *BaseTopo { + return &t.base +} + +func (t *TestTopology) ComponentsByStartOrder() []Component { + return nil +} + +func (t *TestTopology) ComponentsByStopOrder() []Component { + return nil +} + +func (t *TestTopology) ComponentsByUpdateOrder() []Component { + return nil +} + +func (t *TestTopology) IterInstance(fn func(instance Instance)) { +} + +func (t *TestTopology) GetMonitoredOptions() *MonitoredOptions { + return nil +} + +func (t *TestTopology) CountDir(host string, dir string) int { + return 0 +} + +func TestSpec(t *testing.T) { + dir, err := ioutil.TempDir("", "test-*") + assert.Nil(t, err) + + spec := NewSpec(dir, func() Metadata { + return new(TestMetadata) + }) + names, err := spec.List() + assert.Nil(t, err) + assert.Len(t, names, 0) + + // Should ignore directory without meta file. + err = os.Mkdir(filepath.Join(dir, "dummy"), 0755) + assert.Nil(t, err) + names, err = spec.List() + assert.Nil(t, err) + assert.Len(t, names, 0) + + exist, err := spec.Exist("dummy") + assert.Nil(t, err) + assert.False(t, exist) + + var meta1 = &TestMetadata{ + BaseMeta: BaseMeta{ + Version: "1.1.1", + }, + Topo: &TestTopology{}, + } + var meta2 = &TestMetadata{ + BaseMeta: BaseMeta{ + Version: "2.2.2", + }, + Topo: &TestTopology{}, + } + + err = spec.SaveMeta("name1", meta1) + assert.Nil(t, err) + + err = spec.SaveMeta("name2", meta2) + assert.Nil(t, err) + + getMeta := new(TestMetadata) + err = spec.Metadata("name1", getMeta) + assert.Nil(t, err) + assert.Equal(t, meta1, getMeta) + + err = spec.Metadata("name2", getMeta) + assert.Nil(t, err) + assert.Equal(t, meta2, getMeta) + + names, err = spec.List() + assert.Nil(t, err) + assert.Len(t, names, 2) + sort.Strings(names) + assert.Equal(t, "name1", names[0]) + assert.Equal(t, "name2", names[1]) + + exist, err = spec.Exist("name1") + assert.Nil(t, err) + assert.True(t, exist) + + exist, err = spec.Exist("name2") + assert.Nil(t, err) + assert.True(t, exist) + + // remove name1 and check again. + err = spec.Remove("name1") + assert.Nil(t, err) + exist, err = spec.Exist("name1") + assert.Nil(t, err) + assert.False(t, exist) + + // remove a not exist cluster should be fine + err = spec.Remove("name1") + assert.Nil(t, err) +} diff --git a/pkg/cluster/spec/tidb.go b/pkg/cluster/spec/tidb.go index c193d396ba..dca862a7df 100644 --- a/pkg/cluster/spec/tidb.go +++ b/pkg/cluster/spec/tidb.go @@ -182,9 +182,17 @@ func (i *TiDBInstance) InitConfig(e executor.Executor, clusterName, clusterVersi } // ScaleConfig deploy temporary config on scaling -func (i *TiDBInstance) ScaleConfig(e executor.Executor, cluster *Specification, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { +func (i *TiDBInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() - i.instance.topo = cluster + i.instance.topo = mustBeClusterTopo(topo) return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } + +func mustBeClusterTopo(topo Topology) *Specification { + spec, ok := topo.(*Specification) + if !ok { + panic("must be cluster spec") + } + return spec +} diff --git a/pkg/cluster/spec/tiflash.go b/pkg/cluster/spec/tiflash.go index 52f6997006..95c82a8575 100644 --- a/pkg/cluster/spec/tiflash.go +++ b/pkg/cluster/spec/tiflash.go @@ -386,12 +386,12 @@ func (i *TiFlashInstance) InitConfig(e executor.Executor, clusterName, clusterVe } // ScaleConfig deploy temporary config on scaling -func (i *TiFlashInstance) ScaleConfig(e executor.Executor, cluster *Specification, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { +func (i *TiFlashInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() - i.instance.topo = cluster + i.instance.topo = mustBeClusterTopo(topo) return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } diff --git a/pkg/cluster/spec/tikv.go b/pkg/cluster/spec/tikv.go index 7051db9e95..90ffd8e02a 100644 --- a/pkg/cluster/spec/tikv.go +++ b/pkg/cluster/spec/tikv.go @@ -17,12 +17,17 @@ import ( "fmt" "io/ioutil" "path/filepath" + "strconv" "strings" + "time" + "github.com/pingcap/errors" pdserverapi "github.com/pingcap/pd/v4/server/api" "github.com/pingcap/tiup/pkg/cluster/api" + "github.com/pingcap/tiup/pkg/cluster/clusterutil" "github.com/pingcap/tiup/pkg/cluster/executor" "github.com/pingcap/tiup/pkg/cluster/template/scripts" + "github.com/pingcap/tiup/pkg/logger/log" "github.com/pingcap/tiup/pkg/meta" ) @@ -211,11 +216,69 @@ func (i *TiKVInstance) InitConfig(e executor.Executor, clusterName, clusterVersi } // ScaleConfig deploy temporary config on scaling -func (i *TiKVInstance) ScaleConfig(e executor.Executor, cluster *Specification, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { +func (i *TiKVInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() - i.instance.topo = cluster + i.instance.topo = mustBeClusterTopo(topo) return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } + +var _ RollingUpdateInstance = &TiKVInstance{} + +// PreRestart implements RollingUpdateInstance interface. +func (i *TiKVInstance) PreRestart(topo Topology, apiTimeoutSeconds int) error { + timeoutOpt := &clusterutil.RetryOption{ + Timeout: time.Second * time.Duration(apiTimeoutSeconds), + Delay: time.Second * 2, + } + + tidbTopo, ok := topo.(*Specification) + if !ok { + panic("should be type of tidb topology") + } + + pdClient := api.NewPDClient(tidbTopo.GetPDList(), 5*time.Second, nil) + + // Make sure there's leader of PD. + // Although we evict pd leader when restart pd, + // But when there's only one PD instance the pd might not serve request right away after restart. + err := pdClient.WaitLeader(timeoutOpt) + if err != nil { + return errors.AddStack(err) + } + + if err := pdClient.EvictStoreLeader(addr(i), timeoutOpt); err != nil { + if clusterutil.IsTimeoutOrMaxRetry(err) { + log.Warnf("Ignore evicting store leader from %s, %v", i.ID(), err) + } else { + return errors.Annotatef(err, "failed to evict store leader %s", i.GetHost()) + } + } + return nil +} + +// PostRestart implements RollingUpdateInstance interface. +func (i *TiKVInstance) PostRestart(topo Topology) error { + tidbTopo, ok := topo.(*Specification) + if !ok { + panic("should be type of tidb topology") + } + + pdClient := api.NewPDClient(tidbTopo.GetPDList(), 5*time.Second, nil) + + // remove store leader evict scheduler after restart + if err := pdClient.RemoveStoreEvict(addr(i)); err != nil { + return errors.Annotatef(err, "failed to remove evict store scheduler for %s", i.GetHost()) + } + + return nil +} + +func addr(ins Instance) string { + if ins.GetPort() == 0 || ins.GetPort() == 80 { + panic(ins) + } + return ins.GetHost() + ":" + strconv.Itoa(ins.GetPort()) +} diff --git a/pkg/cluster/spec/tispark.go b/pkg/cluster/spec/tispark.go index 21eda6ff13..b1981ca810 100644 --- a/pkg/cluster/spec/tispark.go +++ b/pkg/cluster/spec/tispark.go @@ -247,10 +247,11 @@ func (i *TiSparkMasterInstance) InitConfig(e executor.Executor, clusterName, clu } // ScaleConfig deploy temporary config on scaling -func (i *TiSparkMasterInstance) ScaleConfig(e executor.Executor, cluster *Specification, +func (i *TiSparkMasterInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() + cluster := mustBeClusterTopo(topo) i.instance.topo = cluster.Merge(i.instance.topo) return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } @@ -388,10 +389,11 @@ func (i *TiSparkWorkerInstance) InitConfig(e executor.Executor, clusterName, clu } // ScaleConfig deploy temporary config on scaling -func (i *TiSparkWorkerInstance) ScaleConfig(e executor.Executor, cluster *Specification, +func (i *TiSparkWorkerInstance) ScaleConfig(e executor.Executor, topo Topology, clusterName, clusterVersion, deployUser string, paths meta.DirPaths) error { s := i.instance.topo defer func() { i.instance.topo = s }() + cluster := mustBeClusterTopo(topo) i.instance.topo = cluster.Merge(i.instance.topo) return i.InitConfig(e, clusterName, clusterVersion, deployUser, paths) } diff --git a/pkg/cluster/spec/util.go b/pkg/cluster/spec/util.go index 05c66dd44b..4bb489aaef 100644 --- a/pkg/cluster/spec/util.go +++ b/pkg/cluster/spec/util.go @@ -14,20 +14,18 @@ package spec import ( + "fmt" + "path/filepath" + "reflect" + "github.com/pingcap/errors" - "github.com/pingcap/tiup/pkg/meta" "github.com/pingcap/tiup/pkg/version" ) -const ( - // PatchDirName is the directory to store patch file eg. {PatchDirName}/tidb-hotfix.tar.gz - PatchDirName = "patch" -) - -var tidbSpec *meta.SpecManager +var tidbSpec *SpecManager // GetSpecManager return the spec manager of tidb cluster. -func GetSpecManager() *meta.SpecManager { +func GetSpecManager() *SpecManager { if !initialized { panic("must Initialize profile first") } @@ -45,6 +43,47 @@ type ClusterMeta struct { Topology *Specification `yaml:"topology"` } +var _ UpgradableMetadata = &ClusterMeta{} + +// SetVersion implement UpgradableMetadata interface. +func (m *ClusterMeta) SetVersion(s string) { + m.Version = s +} + +// SetUser implement UpgradableMetadata interface. +func (m *ClusterMeta) SetUser(s string) { + m.User = s +} + +// GetTopology implement Metadata interface. +func (m *ClusterMeta) GetTopology() Topology { + return m.Topology +} + +// SetTopology implement Metadata interface. +func (m *ClusterMeta) SetTopology(topo Topology) { + tidbTopo, ok := topo.(*Specification) + if !ok { + panic(fmt.Sprintln("wrong type: ", reflect.TypeOf(topo))) + } + + m.Topology = tidbTopo +} + +// GetBaseMeta implements Metadata interface. +func (m *ClusterMeta) GetBaseMeta() *BaseMeta { + return &BaseMeta{ + Version: m.Version, + User: m.User, + OpsVer: &m.OpsVer, + } +} + +// AuditDir return the directory for saving audit log. +func AuditDir() string { + return filepath.Join(profileDir, TiOpsAuditDir) +} + // SaveClusterMeta saves the cluster meta information to profile directory func SaveClusterMeta(clusterName string, cmeta *ClusterMeta) error { // set the cmd version diff --git a/pkg/cluster/task/builder.go b/pkg/cluster/task/builder.go index 75eecac986..d378b4aab6 100644 --- a/pkg/cluster/task/builder.go +++ b/pkg/cluster/task/builder.go @@ -17,6 +17,7 @@ import ( "fmt" "path/filepath" + dm "github.com/pingcap/tiup/components/dm/spec" operator "github.com/pingcap/tiup/pkg/cluster/operation" "github.com/pingcap/tiup/pkg/cluster/spec" "github.com/pingcap/tiup/pkg/meta" @@ -75,7 +76,7 @@ func (b *Builder) Func(name string, fn func(ctx *Context) error) *Builder { } // ClusterSSH init all UserSSH need for the cluster. -func (b *Builder) ClusterSSH(spec spec.Spec, deployUser string, sshTimeout int64) *Builder { +func (b *Builder) ClusterSSH(spec spec.Topology, deployUser string, sshTimeout int64) *Builder { var tasks []Task for _, com := range spec.ComponentsByStartOrder() { for _, in := range com.Instances() { @@ -103,9 +104,8 @@ func (b *Builder) UpdateMeta(cluster string, metadata *spec.ClusterMeta, deleted return b } -/* // UpdateDMMeta maintain the dm meta information -func (b *Builder) UpdateDMMeta(cluster string, metadata *meta.DMMeta, deletedNodeIds []string) *Builder { +func (b *Builder) UpdateDMMeta(cluster string, metadata *dm.Metadata, deletedNodeIds []string) *Builder { b.tasks = append(b.tasks, &UpdateDMMeta{ cluster: cluster, metadata: metadata, @@ -113,7 +113,6 @@ func (b *Builder) UpdateDMMeta(cluster string, metadata *meta.DMMeta, deletedNod }) return b } -*/ // UpdateTopology maintain the topology information func (b *Builder) UpdateTopology(cluster string, metadata *spec.ClusterMeta, deletedNodeIds []string) *Builder { @@ -177,8 +176,9 @@ func (b *Builder) BackupComponent(component, fromVer string, host, deployDir str } // InitConfig appends a CopyComponent task to the current task collection -func (b *Builder) InitConfig(clusterName, clusterVersion string, inst spec.Instance, deployUser string, ignoreCheck bool, paths meta.DirPaths) *Builder { +func (b *Builder) InitConfig(clusterName, clusterVersion string, specManager *spec.SpecManager, inst spec.Instance, deployUser string, ignoreCheck bool, paths meta.DirPaths) *Builder { b.tasks = append(b.tasks, &InitConfig{ + specManager: specManager, clusterName: clusterName, clusterVersion: clusterVersion, instance: inst, @@ -190,11 +190,12 @@ func (b *Builder) InitConfig(clusterName, clusterVersion string, inst spec.Insta } // ScaleConfig generate temporary config on scaling -func (b *Builder) ScaleConfig(clusterName, clusterVersion string, cluster *spec.Specification, inst spec.Instance, deployUser string, paths meta.DirPaths) *Builder { +func (b *Builder) ScaleConfig(clusterName, clusterVersion string, specManager *spec.SpecManager, topo spec.Topology, inst spec.Instance, deployUser string, paths meta.DirPaths) *Builder { b.tasks = append(b.tasks, &ScaleConfig{ + specManager: specManager, clusterName: clusterName, clusterVersion: clusterVersion, - base: cluster, + base: topo, instance: inst, deployUser: deployUser, paths: paths, @@ -203,7 +204,7 @@ func (b *Builder) ScaleConfig(clusterName, clusterVersion string, cluster *spec. } // MonitoredConfig appends a CopyComponent task to the current task collection -func (b *Builder) MonitoredConfig(name, comp, host string, globResCtl meta.ResourceControl, options spec.MonitoredOptions, deployUser string, paths meta.DirPaths) *Builder { +func (b *Builder) MonitoredConfig(name, comp, host string, globResCtl meta.ResourceControl, options *spec.MonitoredOptions, deployUser string, paths meta.DirPaths) *Builder { b.tasks = append(b.tasks, &MonitoredConfig{ name: name, component: comp, diff --git a/pkg/cluster/task/context.go b/pkg/cluster/task/context.go index f42f48107d..57c4570284 100644 --- a/pkg/cluster/task/context.go +++ b/pkg/cluster/task/context.go @@ -29,7 +29,7 @@ func (ctx *Context) SetSSHKeySet(privateKeyPath string, publicKeyPath string) er } // SetClusterSSH set cluster user ssh executor in context. -func (ctx *Context) SetClusterSSH(topo *spec.Specification, deployUser string, sshTimeout int64, nativeClient bool) error { +func (ctx *Context) SetClusterSSH(topo spec.Topology, deployUser string, sshTimeout int64, nativeClient bool) error { if len(ctx.PrivateKeyPath) == 0 { return errors.Errorf("context has no PrivateKeyPath") } diff --git a/pkg/cluster/task/init_config.go b/pkg/cluster/task/init_config.go index d579a56025..3826f55d2b 100644 --- a/pkg/cluster/task/init_config.go +++ b/pkg/cluster/task/init_config.go @@ -25,6 +25,7 @@ import ( // InitConfig is used to copy all configurations to the target directory of path type InitConfig struct { + specManager *spec.SpecManager clusterName string clusterVersion string instance spec.Instance @@ -64,5 +65,5 @@ func (c *InitConfig) Rollback(ctx *Context) error { func (c *InitConfig) String() string { return fmt.Sprintf("InitConfig: cluster=%s, user=%s, host=%s, path=%s, %s", c.clusterName, c.deployUser, c.instance.GetHost(), - filepath.Join(spec.ClusterPath(c.clusterName, spec.TempConfigPath, c.instance.ServiceName())), c.paths) + filepath.Join(c.specManager.Path(c.clusterName, spec.TempConfigPath, c.instance.ServiceName())), c.paths) } diff --git a/pkg/cluster/task/monitored_config.go b/pkg/cluster/task/monitored_config.go index e4732f2f08..93c3131aed 100644 --- a/pkg/cluster/task/monitored_config.go +++ b/pkg/cluster/task/monitored_config.go @@ -35,7 +35,7 @@ type MonitoredConfig struct { component string host string globResCtl meta.ResourceControl - options spec.MonitoredOptions + options *spec.MonitoredOptions deployUser string paths meta.DirPaths } diff --git a/pkg/cluster/task/scale_config.go b/pkg/cluster/task/scale_config.go index 3ded3fb08b..bfa15d6e52 100644 --- a/pkg/cluster/task/scale_config.go +++ b/pkg/cluster/task/scale_config.go @@ -23,10 +23,11 @@ import ( // ScaleConfig is used to copy all configurations to the target directory of path type ScaleConfig struct { + specManager *spec.SpecManager clusterName string clusterVersion string instance spec.Instance - base *spec.Specification + base spec.Topology deployUser string paths meta.DirPaths } @@ -39,7 +40,7 @@ func (c *ScaleConfig) Execute(ctx *Context) error { return ErrNoExecutor } - c.paths.Cache = spec.ClusterPath(c.clusterName, spec.TempConfigPath) + c.paths.Cache = c.specManager.Path(c.clusterName, spec.TempConfigPath) if err := os.MkdirAll(c.paths.Cache, 0755); err != nil { return err } diff --git a/pkg/cluster/task/update_dm_meta.go b/pkg/cluster/task/update_dm_meta.go index e7361cd1b4..9bea0b2276 100644 --- a/pkg/cluster/task/update_dm_meta.go +++ b/pkg/cluster/task/update_dm_meta.go @@ -13,12 +13,11 @@ package task -/* import ( "fmt" "strings" - "github.com/pingcap/tiup/pkg/dms/meta" + dmspec "github.com/pingcap/tiup/components/dm/spec" "github.com/pingcap/tiup/pkg/set" ) @@ -26,52 +25,51 @@ import ( // UpdateDMMeta is used to maintain the DM meta information type UpdateDMMeta struct { cluster string - metadata *meta.DMMeta + metadata *dmspec.Metadata deletedNodesID []string } // Execute implements the Task interface func (u *UpdateDMMeta) Execute(ctx *Context) error { // make a copy - newMeta := &meta.DMMeta{} + newMeta := &dmspec.Metadata{} *newMeta = *u.metadata - newMeta.Topology = &meta.DMSTopologySpecification{ - GlobalOptions: u.metadata.Topology.GlobalOptions, - MonitoredOptions: u.metadata.Topology.MonitoredOptions, - ServerConfigs: u.metadata.Topology.ServerConfigs, + newMeta.Topology = &dmspec.Topology{ + GlobalOptions: u.metadata.Topology.GlobalOptions, + // MonitoredOptions: u.metadata.Topology.MonitoredOptions, + ServerConfigs: u.metadata.Topology.ServerConfigs, } deleted := set.NewStringSet(u.deletedNodesID...) topo := u.metadata.Topology - for i, instance := range (&meta.DMMasterComponent{DMSSpecification: topo}).Instances() { + for i, instance := range (&dmspec.DMMasterComponent{Topology: topo}).Instances() { if deleted.Exist(instance.ID()) { continue } newMeta.Topology.Masters = append(newMeta.Topology.Masters, topo.Masters[i]) } - for i, instance := range (&meta.DMWorkerComponent{DMSSpecification: topo}).Instances() { + for i, instance := range (&dmspec.DMWorkerComponent{Topology: topo}).Instances() { if deleted.Exist(instance.ID()) { continue } newMeta.Topology.Workers = append(newMeta.Topology.Workers, topo.Workers[i]) } - for i, instance := range (&meta.DMPortalComponent{DMSSpecification: topo}).Instances() { + for i, instance := range (&dmspec.DMPortalComponent{Topology: topo}).Instances() { if deleted.Exist(instance.ID()) { continue } newMeta.Topology.Portals = append(newMeta.Topology.Portals, topo.Portals[i]) } - return meta.SaveDMMeta(u.cluster, newMeta) + return dmspec.GetSpecManager().SaveMeta(u.cluster, newMeta) } // Rollback implements the Task interface func (u *UpdateDMMeta) Rollback(ctx *Context) error { - return meta.SaveDMMeta(u.cluster, u.metadata) + return dmspec.GetSpecManager().SaveMeta(u.cluster, u.metadata) } // String implements the fmt.Stringer interface func (u *UpdateDMMeta) String() string { return fmt.Sprintf("UpdateMeta: cluster=%s, deleted=`'%s'`", u.cluster, strings.Join(u.deletedNodesID, "','")) } -*/ diff --git a/pkg/dm/spec/cluster.go b/pkg/dm/spec/cluster.go deleted file mode 100644 index 595a9065f1..0000000000 --- a/pkg/dm/spec/cluster.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2020 PingCAP, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// See the License for the specific language governing permissions and -// limitations under the License. - -package spec - -import ( - "path/filepath" - - cspec "github.com/pingcap/tiup/pkg/cluster/spec" - "github.com/pingcap/tiup/pkg/meta" -) - -const dmDir = "dm" - -var specManager *meta.SpecManager - -func init() { - specManager = meta.NewSpec(filepath.Join(cspec.ProfileDir(), dmDir)) -} - -// DMMeta is the specification of generic cluster metadata -type DMMeta struct { - User string `yaml:"user"` // the user to run and manage cluster on remote - Version string `yaml:"dm_version"` // the version of TiDB cluster - //EnableTLS bool `yaml:"enable_tls"` - //EnableFirewall bool `yaml:"firewall"` - - Topology *DMTopologySpecification `yaml:"topology"` -} - -// GetSpecManager return the spec manager of dm cluster. -func GetSpecManager() *meta.SpecManager { - return specManager -} diff --git a/pkg/logger/audit.go b/pkg/logger/audit.go index efd68ba346..01e751da1f 100644 --- a/pkg/logger/audit.go +++ b/pkg/logger/audit.go @@ -15,15 +15,11 @@ package logger import ( "bytes" - "io/ioutil" "os" "strings" - "time" + "github.com/pingcap/tiup/pkg/cluster/audit" utils2 "github.com/pingcap/tiup/pkg/utils" - - "github.com/pingcap/tiup/pkg/base52" - "github.com/pingcap/tiup/pkg/cluster/spec" "go.uber.org/atomic" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -31,9 +27,11 @@ import ( var auditEnabled atomic.Bool var auditBuffer *bytes.Buffer +var auditDir string // EnableAuditLog enables audit log. -func EnableAuditLog() { +func EnableAuditLog(dir string) { + auditDir = dir auditEnabled.Store(true) } @@ -53,15 +51,15 @@ func OutputAuditLogIfEnabled() { if !auditEnabled.Load() { return } - auditDir := spec.ProfilePath(spec.TiOpsAuditDir) + if err := utils2.CreateDir(auditDir); err != nil { zap.L().Warn("Create audit directory failed", zap.Error(err)) - } else { - auditFilePath := spec.ProfilePath(spec.TiOpsAuditDir, base52.Encode(time.Now().Unix())) - err := ioutil.WriteFile(auditFilePath, auditBuffer.Bytes(), os.ModePerm) - if err != nil { - zap.L().Warn("Write audit log file failed", zap.Error(err)) - } - auditBuffer.Reset() + return + } + + err := audit.OutputAuditLog(auditDir, auditBuffer.Bytes()) + if err != nil { + zap.L().Warn("Write audit log file failed", zap.Error(err)) } + auditBuffer.Reset() } diff --git a/pkg/meta/spec_test.go b/pkg/meta/spec_test.go deleted file mode 100644 index 5c0339c9bc..0000000000 --- a/pkg/meta/spec_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2020 PingCAP, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// See the License for the specific language governing permissions and -// limitations under the License. - -package meta - -import ( - "io/ioutil" - "os" - "path/filepath" - "sort" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSpec(t *testing.T) { - dir, err := ioutil.TempDir("", "test-*") - assert.Nil(t, err) - - spec := NewSpec(dir) - names, err := spec.List() - assert.Nil(t, err) - assert.Len(t, names, 0) - - // Should ignore directory without meta file. - err = os.Mkdir(filepath.Join(dir, "dummy"), 0755) - assert.Nil(t, err) - names, err = spec.List() - assert.Nil(t, err) - assert.Len(t, names, 0) - - exist, err := spec.Exist("dummy") - assert.Nil(t, err) - assert.False(t, exist) - - type Meta struct { - A string - B int - } - var meta1 = &Meta{ - A: "a", - B: 1, - } - var meta2 = &Meta{ - A: "b", - B: 2, - } - - err = spec.SaveMeta("name1", meta1) - assert.Nil(t, err) - - err = spec.SaveMeta("name2", meta2) - assert.Nil(t, err) - - getMeta := new(Meta) - err = spec.Metadata("name1", getMeta) - assert.Nil(t, err) - assert.Equal(t, meta1, getMeta) - - err = spec.Metadata("name2", getMeta) - assert.Nil(t, err) - assert.Equal(t, meta2, getMeta) - - names, err = spec.List() - assert.Nil(t, err) - assert.Len(t, names, 2) - sort.Strings(names) - assert.Equal(t, "name1", names[0]) - assert.Equal(t, "name2", names[1]) - - exist, err = spec.Exist("name1") - assert.Nil(t, err) - assert.True(t, exist) - - exist, err = spec.Exist("name2") - assert.Nil(t, err) - assert.True(t, exist) -} diff --git a/tools/check/go.mod b/tools/check/go.mod index 91fc1c9d7b..6c2cbb97c2 100644 --- a/tools/check/go.mod +++ b/tools/check/go.mod @@ -9,11 +9,10 @@ require ( github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf // indirect github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc github.com/kisielk/errcheck v1.2.0 - github.com/mgechev/revive v0.0.0-20181210140514-b4cc152955fb + github.com/mgechev/revive v1.0.2 github.com/nicksnyder/go-i18n v1.10.0 // indirect github.com/pelletier/go-toml v1.2.0 // indirect github.com/securego/gosec v0.0.0-20181211171558-12400f9a1ca7 - golang.org/x/tools v0.0.0-20190925020647-22afafe3322a // indirect gopkg.in/alecthomas/gometalinter.v2 v2.0.12 // indirect gopkg.in/alecthomas/gometalinter.v3 v3.0.0 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect diff --git a/tools/check/go.sum b/tools/check/go.sum index f713df72d4..1d593c8b83 100644 --- a/tools/check/go.sum +++ b/tools/check/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/chzchzchz/goword v0.0.0-20170907005317-a9744cb52b03/go.mod h1:uFE9hX+zXEwvyUThZ4gDb9vkAwc5DoHUnRSEpH0VrOs= @@ -8,8 +10,13 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dnephin/govet v0.0.0-20171012192244-4a96d43e39d3/go.mod h1:pPTX0MEEoAnfbrAGFj4nSVNhl6YbugRj6eardUZdtGo= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structtag v1.0.0 h1:pTHj65+u3RKWYPSGaU290FpI/dXxTaHdVwVwbcPKmEc= github.com/fatih/structtag v1.0.0/go.mod h1:IKitwq45uXL/yqi5mYghiD3w9H6eTOvI9vnk8tXMphA= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= @@ -21,35 +28,58 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mgechev/dots v0.0.0-20180605013149-8e09d8ea2757 h1:KTwJ7Lo3KDKMknRYN5JEFRGIM4IkG59QjFFM2mxsMEU= github.com/mgechev/dots v0.0.0-20180605013149-8e09d8ea2757/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= +github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81 h1:QASJXOGm2RZ5Ardbc86qNFvby9AqkLDibfChMtAg5QM= +github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= github.com/mgechev/revive v0.0.0-20181210140514-b4cc152955fb h1:bLiKpCHe+ngBsF1o7DjZTmoffHEy2gdQ/+9NunuJ4ZY= github.com/mgechev/revive v0.0.0-20181210140514-b4cc152955fb/go.mod h1:pVHj2KvxEhotJ6Lmr7zb3YgNMX1QKt8cyp6fdPHOrzU= +github.com/mgechev/revive v1.0.2 h1:v0NxxQ7fSFz/u1NQydPo6EGdq7va0J1BtsZmae6kzUg= +github.com/mgechev/revive v1.0.2/go.mod h1:rb0dQy1LVAxW9SWy5R3LPUjevzUbUS316U5MFySA2lo= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= github.com/olekukonko/tablewriter v0.0.0-20180912035003-be2c049b30cc h1:rQ1O4ZLYR2xXHXgBCCfIIGnuZ0lidMQw2S5n1oOv+Wg= github.com/olekukonko/tablewriter v0.0.0-20180912035003-be2c049b30cc/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/securego/gosec v0.0.0-20181211171558-12400f9a1ca7/go.mod h1:m3KbCTwh9vLhm6AKBjE+ALesKilKcQHezI1uVOti0Ks= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -57,7 +87,10 @@ golang.org/x/tools v0.0.0-20180911133044-677d2ff680c1/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190925020647-22afafe3322a h1:3GxqzBPBt1O2dIiPnzldQ5d25CAMWJFBZTpqxLPfjs8= golang.org/x/tools v0.0.0-20190925020647-22afafe3322a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200225230052-807dcd883420 h1:4RJNOV+2rLxMEfr6QIpC7GEv9MjD6ApGXTCLrNF9+eA= +golang.org/x/tools v0.0.0-20200225230052-807dcd883420/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/alecthomas/gometalinter.v2 v2.0.12/go.mod h1:NDRytsqEZyolNuAgTzJkZMkSQM7FIKyzVzGhjB/qfYo= gopkg.in/alecthomas/gometalinter.v3 v3.0.0/go.mod h1:sE0aqUDPY4ibZWdfOxx4ZVG9CD+Y5I1H+Snwv8a3r/s= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -67,3 +100,4 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=