Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cluster/dm: support backup and restore cluster meta #1801

Merged
merged 10 commits into from
Apr 15, 2022
65 changes: 65 additions & 0 deletions components/cluster/command/meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2022 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 command

import (
"fmt"
"time"

"github.com/spf13/cobra"
)

func newMetaCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "meta",
Short: "backup/restore meta information",
}

var filePath string

var metaBackupCmd = &cobra.Command{
Use: "backup <cluster-name>",
Short: "backup topology and other information of cluster",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("please input cluster-name")
}
if filePath == "" {
filePath = "tiup-cluster_" + args[0] + "_metabackup_" + time.Now().Format(time.RFC3339) + ".tar.gz"
}
err := cm.BackupClusterMeta(args[0], filePath)
if err == nil {
log.Infof("successfully backup meta of cluster %s on %s", args[0], filePath)
}
return err
},
}
metaBackupCmd.Flags().StringVar(&filePath, "file", "", "filepath of output tarball")

var metaRestoreCmd = &cobra.Command{
Use: "restore <cluster-name> <backup-file>",
Short: "restore topology and other information of cluster",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("please input cluster-name and backup-file")
}
return cm.RestoreClusterMeta(args[0], args[1], skipConfirm)
},
}

cmd.AddCommand(metaBackupCmd)
cmd.AddCommand(metaRestoreCmd)

return cmd
}
1 change: 1 addition & 0 deletions components/cluster/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ func init() {
newReplayCmd(),
newTemplateCmd(),
newTLSCmd(),
newMetaCmd(),
)
}

Expand Down
65 changes: 65 additions & 0 deletions components/dm/command/meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2022 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 command

import (
"fmt"
"time"

"github.com/spf13/cobra"
)

func newMetaCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "meta",
Short: "backup/restore meta information",
}

var filePath string

var metaBackupCmd = &cobra.Command{
Use: "backup <cluster-name>",
Short: "backup topology and other information of a cluster",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("please input cluster name")
}
if filePath == "" {
filePath = "tiup-cluster_" + args[0] + "_metabackup_" + time.Now().Format(time.RFC3339) + ".tar.gz"
}
err := cm.BackupClusterMeta(args[0], filePath)
if err == nil {
log.Infof("successfully backup meta of cluster %s on %s", args[0], filePath)
}
return err
},
}
metaBackupCmd.Flags().StringVar(&filePath, "file", "", "filepath of output tarball")

var metaRestoreCmd = &cobra.Command{
Use: "restore <cluster-name> <backup-file>",
Short: "restore topology and other information of a cluster",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("please input cluster name and path to the backup file")
}
return cm.RestoreClusterMeta(args[0], args[1], skipConfirm)
},
}

cmd.AddCommand(metaBackupCmd)
cmd.AddCommand(metaRestoreCmd)

return cmd
}
1 change: 1 addition & 0 deletions components/dm/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ please backup your data before process.`,
newDisableCmd(),
newReplayCmd(),
newTemplateCmd(),
newMetaCmd(),
)
}

Expand Down
81 changes: 81 additions & 0 deletions pkg/cluster/manager/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"

"github.com/fatih/color"
"github.com/joomcode/errorx"
perrs "github.com/pingcap/errors"
"github.com/pingcap/tiup/pkg/cluster/clusterutil"
"github.com/pingcap/tiup/pkg/cluster/ctxt"
operator "github.com/pingcap/tiup/pkg/cluster/operation"
"github.com/pingcap/tiup/pkg/cluster/spec"
Expand Down Expand Up @@ -302,3 +305,81 @@ func checkTiFlashWithTLS(topo spec.Topology, version string) error {
}
return nil
}

// BackupClusterMeta backup cluster meta to given filepath
func (m *Manager) BackupClusterMeta(clusterName, filePath string) error {
exist, err := m.specManager.Exist(clusterName)
if err != nil {
return err
}
if !exist {
return fmt.Errorf("cluster %s does not exist", clusterName)
}
// check locked
if err := m.specManager.ScaleOutLockedErr(clusterName); err != nil {
return err
}
f, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
nexustar marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
return utils.Tar(f, m.specManager.Path(clusterName))
}

// RestoreClusterMeta restore cluster meta by given filepath
func (m *Manager) RestoreClusterMeta(clusterName, filePath string, skipConfirm bool) error {
if err := clusterutil.ValidateClusterNameOrError(clusterName); err != nil {
return err
}

fi, err := os.Stat(m.specManager.Path(clusterName))
if err != nil {
if !os.IsNotExist(err) {
return perrs.AddStack(err)
}
m.logger.Infof(fmt.Sprintf("meta of cluster %s didn't exist before restore", clusterName))
skipConfirm = true
} else {
m.logger.Warnf(color.HiRedString(tui.ASCIIArtWarning))

exist, err := m.specManager.Exist(clusterName)
if err != nil {
return err
}
if exist {
m.logger.Infof(fmt.Sprintf("the exist meta.yaml of cluster %s was last modified at %s", clusterName, color.HiYellowString(fi.ModTime().Format(time.RFC3339))))
} else {
m.logger.Infof(fmt.Sprintf("the meta.yaml of cluster %s does not exist", clusterName))
}
}
fi, err = os.Stat(filePath)
if err != nil {
return err
}
m.logger.Warnf(fmt.Sprintf("the given tarball was last modified at %s", color.HiYellowString(fi.ModTime().Format(time.RFC3339))))
if !skipConfirm {
if err := tui.PromptForAnswerOrAbortError(
"Yes, I know my cluster meta will be be overridden.",
fmt.Sprintf("This operation will override topology file and other meta file of %s cluster %s .",
m.sysName,
color.HiYellowString(clusterName),
)+"\nAre you sure to continue?",
); err != nil {
return err
}
m.logger.Infof("Destroying cluster...")
}
err = os.RemoveAll(m.specManager.Path(clusterName))
if err != nil {
return err
}
f, err := os.Open(filePath)
if err != nil {
return err
}
err = utils.Untar(f, m.specManager.Path(clusterName))
if err == nil {
m.logger.Infof(fmt.Sprintf("restore meta of cluster %s successfully.", clusterName))
}
return err
}
36 changes: 36 additions & 0 deletions pkg/utils/ioutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"crypto/sha1"
"encoding/hex"
"io"
"io/fs"
"os"
"path"
"path/filepath"
Expand Down Expand Up @@ -92,6 +93,41 @@ func IsSubDir(parent, sub string) bool {
return false
}

// Tar compresses the folder to tarball with gzip
func Tar(writer io.Writer, from string) error {
compressW := gzip.NewWriter(writer)
defer compressW.Close()
tarW := tar.NewWriter(compressW)
defer tarW.Close()

return filepath.Walk(from, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
header, _ := tar.FileInfoHeader(info, "")
header.Name, _ = filepath.Rel(from, path)
// skip "."
if header.Name == "." {
return nil
}

err = tarW.WriteHeader(header)
if err != nil {
return err
}
if !info.IsDir() {
fd, err := os.Open(path)
if err != nil {
return err
}
defer fd.Close()
_, err = io.Copy(tarW, fd)
return err
}
return nil
})
}

// Untar decompresses the tarball
func Untar(reader io.Reader, to string) error {
gr, err := gzip.NewReader(reader)
Expand Down