From c720e7802b3cfb1b8d4200b0440b07cde106b3c4 Mon Sep 17 00:00:00 2001 From: yyhenryyy Date: Thu, 11 Jan 2024 20:07:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(mongodb):=20=E5=A2=9E=E5=8A=A0=E5=8E=9F?= =?UTF-8?q?=E5=AD=90=E4=BB=BB=E5=8A=A1=EF=BC=8Ccluster=E5=AE=89=E8=A3=85fl?= =?UTF-8?q?ow=20#3018?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dbm-services/go.work | 1 + .../mongo/db-tools/dbactuator/.gitignore | 31 + .../mongo/db-tools/dbactuator/LICENSE | 0 .../mongo/db-tools/dbactuator/Makefile | 9 + .../mongo/db-tools/dbactuator/README.md | 85 +++ .../mongo/db-tools/dbactuator/cmd/root.go | 126 ++++ .../example/add_shard_to_cluster.example.md | 24 + .../example/cluster_balancer.example.md | 20 + .../example/initiate_replicaset.example.md | 35 ++ .../example/mongo_add_user.example.md | 52 ++ .../example/mongo_deinstall.example.md | 22 + .../example/mongo_del_user.example.md | 34 ++ .../example/mongo_execute_script.example.md | 30 + .../example/mongo_process_restart.example.md | 42 ++ .../example/mongod_install.example.md | 60 ++ .../example/mongod_replace.example.md | 29 + .../example/mongod_step_down.example.md | 18 + .../example/mongos_install.example.md | 30 + .../example/os_mongo_init.example.md | 16 + dbm-services/mongo/db-tools/dbactuator/go.mod | 28 + dbm-services/mongo/db-tools/dbactuator/go.sum | 95 +++ .../imgs/bk-dbactuator-mongo_structur.png | Bin 0 -> 162444 bytes .../mongo/db-tools/dbactuator/main.go | 12 + .../mongo/db-tools/dbactuator/mylog/mylog.go | 29 + .../atommongodb/add_shard_to_cluster.go | 248 ++++++++ .../pkg/atomjobs/atommongodb/add_user.go | 264 ++++++++ .../pkg/atomjobs/atommongodb/atommongodb.go | 2 + .../atomjobs/atommongodb/cluster_balancer.go | 159 +++++ .../pkg/atomjobs/atommongodb/del_user.go | 204 +++++++ .../atommongodb/initiate_replicaset.go | 278 +++++++++ .../atomjobs/atommongodb/mongo_deinstall.go | 230 +++++++ .../atommongodb/mongo_execute_script.go | 331 ++++++++++ .../atommongodb/mongo_process_restart.go | 399 ++++++++++++ .../atommongodb/mongo_set_profiler.go | 186 ++++++ .../atommongodb/mongod_change_oplogsize.go | 501 +++++++++++++++ .../atomjobs/atommongodb/mongod_install.go | 492 +++++++++++++++ .../atomjobs/atommongodb/mongod_replace.go | 379 ++++++++++++ .../atomjobs/atommongodb/mongos_install.go | 441 ++++++++++++++ .../atommongodb/replicaset_stepdown.go | 130 ++++ .../pkg/atomjobs/atomsys/atomsys.go | 2 + .../pkg/atomjobs/atomsys/os_mongo_init.go | 123 ++++ .../dbactuator/pkg/backupsys/backupsys.go | 231 +++++++ .../dbactuator/pkg/common/exporter_conf.go | 46 ++ .../dbactuator/pkg/common/filelock.go | 34 ++ .../pkg/common/initiate_replicaset_conf.go | 37 ++ .../dbactuator/pkg/common/media_pkg.go | 130 ++++ .../dbactuator/pkg/common/mongo_common.go | 575 ++++++++++++++++++ .../dbactuator/pkg/common/mongo_init_shell.go | 165 +++++ .../dbactuator/pkg/common/mongo_user_conf.go | 35 ++ .../dbactuator/pkg/common/mongod_conf.go | 87 +++ .../dbactuator/pkg/common/mongos_conf.go | 46 ++ .../pkg/common/repliccaset_member_conf.go | 24 + .../db-tools/dbactuator/pkg/consts/consts.go | 277 +++++++++ .../dbactuator/pkg/consts/data_dir.go | 325 ++++++++++ .../db-tools/dbactuator/pkg/consts/dts.go | 25 + .../db-tools/dbactuator/pkg/consts/test.go | 94 +++ .../db-tools/dbactuator/pkg/consts/user.go | 80 +++ .../dbactuator/pkg/customtime/customtime.go | 76 +++ .../dbactuator/pkg/jobmanager/jobmanager.go | 153 +++++ .../dbactuator/pkg/jobruntime/jobrunner.go | 19 + .../dbactuator/pkg/jobruntime/jobruntime.go | 159 +++++ .../dbactuator/pkg/report/filereport.go | 101 +++ .../dbactuator/pkg/report/reporter.go | 58 ++ .../db-tools/dbactuator/pkg/util/bkrepo.go | 102 ++++ .../db-tools/dbactuator/pkg/util/compress.go | 206 +++++++ .../db-tools/dbactuator/pkg/util/file.go | 66 ++ .../mongo/db-tools/dbactuator/pkg/util/net.go | 67 ++ .../db-tools/dbactuator/pkg/util/osCmd.go | 107 ++++ .../dbactuator/pkg/util/proxy_tools.go | 103 ++++ .../db-tools/dbactuator/pkg/util/redisutil.go | 102 ++++ .../db-tools/dbactuator/pkg/util/reflect.go | 20 + .../db-tools/dbactuator/pkg/util/util.go | 254 ++++++++ .../db-tools/dbactuator/pkg/util/version.go | 119 ++++ .../db-tools/dbactuator/scripts/upload.sh | 182 ++++++ .../db-tools/dbactuator/tests/test_mongo.sh | 82 +++ dbm-ui/backend/flow/consts.py | 8 + .../bamboo/scene/mongodb/mongodb_install.py | 8 + .../scene/mongodb/sub_task/mongos_install.py | 2 +- .../scene/redis/redis_data_structure.py | 16 +- .../mongodb/add_relationship_to_meta.py | 1 + .../collections/mongodb/exec_actuator_job.py | 6 + .../flow/utils/mongodb/calculate_cluster.py | 287 ++++++--- .../flow/utils/mongodb/mongodb_dataclass.py | 65 +- 83 files changed, 9671 insertions(+), 106 deletions(-) create mode 100644 dbm-services/mongo/db-tools/dbactuator/.gitignore create mode 100644 dbm-services/mongo/db-tools/dbactuator/LICENSE create mode 100644 dbm-services/mongo/db-tools/dbactuator/Makefile create mode 100644 dbm-services/mongo/db-tools/dbactuator/README.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/cmd/root.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/add_shard_to_cluster.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/cluster_balancer.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/initiate_replicaset.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/mongo_add_user.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/mongo_deinstall.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/mongo_del_user.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/mongo_execute_script.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/mongo_process_restart.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/mongod_install.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/mongod_replace.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/mongod_step_down.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/mongos_install.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/example/os_mongo_init.example.md create mode 100644 dbm-services/mongo/db-tools/dbactuator/go.mod create mode 100644 dbm-services/mongo/db-tools/dbactuator/go.sum create mode 100644 dbm-services/mongo/db-tools/dbactuator/imgs/bk-dbactuator-mongo_structur.png create mode 100644 dbm-services/mongo/db-tools/dbactuator/main.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/mylog/mylog.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/add_shard_to_cluster.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/add_user.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/atommongodb.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/cluster_balancer.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/del_user.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/initiate_replicaset.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_deinstall.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_execute_script.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_process_restart.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_set_profiler.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_change_oplogsize.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_install.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_replace.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongos_install.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/replicaset_stepdown.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atomsys/atomsys.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atomsys/os_mongo_init.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/backupsys/backupsys.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/common/exporter_conf.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/common/filelock.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/common/initiate_replicaset_conf.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/common/media_pkg.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_common.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_init_shell.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_user_conf.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/common/mongod_conf.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/common/mongos_conf.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/common/repliccaset_member_conf.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/consts/consts.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/consts/data_dir.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/consts/dts.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/consts/test.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/consts/user.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/customtime/customtime.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/jobmanager/jobmanager.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime/jobrunner.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime/jobruntime.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/report/filereport.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/report/reporter.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/util/bkrepo.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/util/compress.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/util/file.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/util/net.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/util/osCmd.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/util/proxy_tools.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/util/redisutil.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/util/reflect.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/util/util.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/pkg/util/version.go create mode 100644 dbm-services/mongo/db-tools/dbactuator/scripts/upload.sh create mode 100644 dbm-services/mongo/db-tools/dbactuator/tests/test_mongo.sh diff --git a/dbm-services/go.work b/dbm-services/go.work index 5f76be8586..6d05de2342 100644 --- a/dbm-services/go.work +++ b/dbm-services/go.work @@ -27,4 +27,5 @@ use ( riak/db-tools/dbactuator riak/db-tools/riak-monitor sqlserver/db-tools/dbactuator + mongo/db-tools/dbactuator ) diff --git a/dbm-services/mongo/db-tools/dbactuator/.gitignore b/dbm-services/mongo/db-tools/dbactuator/.gitignore new file mode 100644 index 0000000000..f6b4016d07 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/.gitignore @@ -0,0 +1,31 @@ +.idea +.vscode +logs/* +bin/mongo-dbactuator +scripts/upload_media.sh +.codecc +gonote +goimports +.agent.properties +agent.zip +codecc/ +devopsAgent +devopsDaemon +install.sh +jre.zip +jre/ +latest_version.txt +preci +preci.log +preci.pid +preci_server.jar +runtime/ +start.sh +stop.sh +telegraf.conf +tmp/ +uninstall.sh +worker-agent.jar +preci.port +build.yml +tests/dbactuator-test diff --git a/dbm-services/mongo/db-tools/dbactuator/LICENSE b/dbm-services/mongo/db-tools/dbactuator/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dbm-services/mongo/db-tools/dbactuator/Makefile b/dbm-services/mongo/db-tools/dbactuator/Makefile new file mode 100644 index 0000000000..04a58a4c97 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/Makefile @@ -0,0 +1,9 @@ +SRV_NAME=mongo-dbactuator + +clean: + -rm ./bin/${SRV_NAME} + +build:clean + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/$(SRV_NAME) -v main.go + +.PHONY: init clean build diff --git a/dbm-services/mongo/db-tools/dbactuator/README.md b/dbm-services/mongo/db-tools/dbactuator/README.md new file mode 100644 index 0000000000..c34a3996ae --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/README.md @@ -0,0 +1,85 @@ +## mongo-dbactuator +mongo原子任务合集,包含mongo复制集、cluster的创建,备份,回档等等原子任务。 + +使用方式: +```go +./bin/mongo-dbactuator -h +mongo原子任务合集,包含mongo复制集、cluster的创建,备份,回档等等原子任务。 + +Usage: + mongo-dbactuator [flags] + + +Flags: + -A, --atom-job-list string 多个原子任务名用','分割,如 redis_install,redis_replicaof + -B, --backup_dir string 备份保存路径,亦可通过环境变量MONGO_BACKUP_DIR指定 + -D, --data_dir string 数据保存路径,亦可通过环境变量 MONGO_DATA_DIR 指定 + -h, --help help for mongo-dbactuator + -N, --node_id string 节点id + -p, --payload string 原子任务参数信息,base64包裹 + -f, --payload_file string 原子任务参数信息,json/yaml文件 + -R, --root_id string 流程id + -t, --toggle Help message for toggle + -U, --uid string 单据id + -V, --version_id string 运行版本id + -u, --user string db进程运行的os用户 + -g, --group string db进程运行的os用户的属主 + +//执行示例 +./bin/dbactuator_redis --uid=1111 --root_id=2222 --node_id=3333 --version_id=v1 --payload='eyJkaXIiOiIvZGF0YS9yZWRpcy8zMDAwMCIsInBvcnQiOjMwMDAwLCJwYXNzd29yZCI6InBhc3MwMSIsInZlcnNpb24iOiJyZWRpcy00LjExLjEyIiwiZGF0YWJhc2VzIjoyfQ==' --atom-job-list="mongod_install" +``` + +### 架构图 +![架构图](./imgs/bk-dbactuator-mongo_structur.png) + +### 开发规范 +go开发规范参考: [https://google.github.io/styleguide/go/decisions](https://google.github.io/styleguide/go/decisions) + +### 开发流程 +- **step1(必须):`pkg/atomJobs`目录下添加类对象,如`pkg/atomJobs/atommongodb/mongod_install.go`**; +以`type MongoDBInstall`为例。 +需实现`JobRunner`中的相关接口: +```go +//JobRunner defines a behavior of a job +type JobRunner interface { + // Init doing some operation before run a job + // such as reading parametes + Init(*JobGenericRuntime) error + + // Name return the name of the job + Name() string + + // Run run a job + Run() error + + Retry() uint + + // Rollback you can define some rollback logic here when job fails + Rollback() error +} +``` +而后实现一个New函数,该函数简单返回一个`*MongoDBInstall{}`即可,如:`func NewMongoDBInstall() jobruntime.JobRunner`; +- **step2(必须):`pkg/jobmanager/jobmanager.go`中修改`GetAtomJobInstance()`函数** +加一行代码即可。 +```go +//key名必须和./mongo-dbactuator --atom-job-list 参数中的保持一致; +//value就是step1中的New函数; +m.atomJobMapper["NewMongoDBInstall"] = atommongodb.NewMongoDBInstall +``` +- **step3(非必须):更新README.md中的“当前支持的原子任务”** + +### 注意事项 +- 第一: **`mongo-dbactuator`中每个原子任务,强烈建议可重入,即可反复执行** +虽然接口`JobRunner`中有`Rollback() error`实现需求,但其实不那么重要。 +相比可回档,实现可重入有如下优势: + - **可重入实现难度更低, 基本上每个动作前先判断该动作是否已做过即可,而回档操作难度大,如100个redis实例建立主从关系,其中1个失败,99个成功,可重入实现简单,回档操作则非常麻烦;** + - **可重入风险更低,创建的回档动作是删除,删除的回档动作是创建。回档操作代码细微bug,影响很大;** + - **可重入对DBA和用户更实用,用户执行某个操作失败,用户基本诉求是重跑,完全不执行该操作了恢复环境需求很少;** + +### 当前支持的原子任务 +```go +os_mongo_init // mongo安装前,os初始化 +mongod_install // mongod安装 +mongos_replicaof // mongos安装 +... +``` \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/cmd/root.go b/dbm-services/mongo/db-tools/dbactuator/cmd/root.go new file mode 100644 index 0000000000..71291f6cc4 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/cmd/root.go @@ -0,0 +1,126 @@ +// Package cmd 根目录 +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "encoding/base64" + "log" + "os" + + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobmanager" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/spf13/cobra" +) + +var uid string +var rootID string +var nodeID string +var versionID string +var dataDir string +var backupDir string +var payLoad string +var payLoadFormat string +var payLoadFile string +var atomJobList string +var user string +var group string + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "mongo-dbactuator", + Short: "mongo原子任务合集", + Long: `mongo原子任务合集,包含mongo复制集、cluster的创建,备份,回档等等原子任务。`, + // Uncomment the following line if your bare application + // has an action associated with it: + Run: func(cmd *cobra.Command, args []string) { + var err error + dir, _ := util.GetCurrentDirectory() + + // 优先使用payLoad。 payLoadFile 个人测试的时候使用的. + if payLoad == "" && payLoadFile != "" { + if o, err := os.ReadFile(payLoadFile); err == nil { + payLoad = base64.StdEncoding.EncodeToString(o) + log.Printf("using payload file %s", payLoadFile) + } else { + log.Printf("using payload file %s err %v", payLoadFile, err) + } + } + + // 设置mongo环境变量 + err = consts.SetMongoDataDir(dataDir) + if err != nil { + log.Println(err.Error()) + os.Exit(-1) + } + err = consts.SetMongoBackupDir(backupDir) + if err != nil { + log.Println(err.Error()) + os.Exit(-1) + } + + err = consts.SetProcessUser(user) + if err != nil { + log.Println(err.Error()) + os.Exit(-1) + } + err = consts.SetProcessUserGroup(group) + if err != nil { + log.Println(err.Error()) + os.Exit(-1) + } + + manager, err := jobmanager.NewJobGenericManager(uid, rootID, nodeID, versionID, + payLoad, payLoadFormat, atomJobList, dir) + if err != nil { + return + } + err = manager.LoadAtomJobs() + if err != nil { + os.Exit(-1) + } + err = manager.RunAtomJobs() + if err != nil { + os.Exit(-1) + } + }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := RootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // Cobra also supports local flags, which will only run + // when this action is called directly. + RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + RootCmd.PersistentFlags().StringVarP(&uid, "uid", "U", "", "单据id") + RootCmd.PersistentFlags().StringVarP(&rootID, "root_id", "R", "", "流程id") + RootCmd.PersistentFlags().StringVarP(&nodeID, "node_id", "N", "", "节点id") + RootCmd.PersistentFlags().StringVarP(&versionID, "version_id", "V", "", "运行版本id") + RootCmd.PersistentFlags().StringVarP(&dataDir, "data_dir", "D", "", + "数据保存路径,亦可通过环境变量 REDIS_DATA_DIR 指定") + RootCmd.PersistentFlags().StringVarP(&backupDir, "backup_dir", "B", "", + "备份保存路径,亦可通过环境变量REDIS_BACKUP_DIR指定") + RootCmd.PersistentFlags().StringVarP(&payLoad, "payload", "p", "", "原子任务参数信息,base64包裹") + RootCmd.PersistentFlags().StringVarP(&payLoadFormat, "payload-format", "m", "", + "command payload format, default base64, value_allowed: base64|raw") + RootCmd.PersistentFlags().StringVarP(&atomJobList, "atom-job-list", "A", "", + "多个原子任务名用','分割,如 redis_install,redis_replicaof") + RootCmd.PersistentFlags().StringVarP(&payLoadFile, "payload_file", "f", "", "原子任务参数信息,json文件") + RootCmd.PersistentFlags().StringVarP(&user, "user", "u", "", "开启进程的os用户") + RootCmd.PersistentFlags().StringVarP(&group, "group", "g", "", "开启进程的os用户属主") +} diff --git a/dbm-services/mongo/db-tools/dbactuator/example/add_shard_to_cluster.example.md b/dbm-services/mongo/db-tools/dbactuator/example/add_shard_to_cluster.example.md new file mode 100644 index 0000000000..b42c70cc4e --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/add_shard_to_cluster.example.md @@ -0,0 +1,24 @@ +### add_shard_to_cluster +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="add_shard_to_cluster" --payload='{{payload_base64}}' +``` + + +原始payload + +```json +{ + "ip":"10.1.1.1", + "port":27021, + "adminUsername":"xxx", + "adminPassword":"xxxxxxx", + "shard":{ + "test-test1-s1":"10.1.1.2:27001,10.1.1.3:27002", + "test-test1-s2":"10.1.1.2:27004,10.1.1.3:27005", + "test-test1-s3":"10.1.1.3:27001,10.1.1.4:27002", + "test-test1-s4":"10.1.1.3:27004,10.1.1.4:27005" + } +} +``` \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/example/cluster_balancer.example.md b/dbm-services/mongo/db-tools/dbactuator/example/cluster_balancer.example.md new file mode 100644 index 0000000000..579242159b --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/cluster_balancer.example.md @@ -0,0 +1,20 @@ +### mongod_replace +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="cluster_balancer" --payload='{{payload_base64}}' +``` + + +原始payload + +```json +{ + "ip":"10.1.1.1", + "port":27021, + "open": false, + "adminUsername":"xxx", + "adminPassword":"xxxxxxxxx" +} +``` +"open"字段 true:表示打开balancer false:表示关闭balancer diff --git a/dbm-services/mongo/db-tools/dbactuator/example/initiate_replicaset.example.md b/dbm-services/mongo/db-tools/dbactuator/example/initiate_replicaset.example.md new file mode 100644 index 0000000000..1ddff6024c --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/initiate_replicaset.example.md @@ -0,0 +1,35 @@ +### init_replicaset +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="init_replicaset" --payload='{{payload_base64}}' +``` +--data_dir、--backup_dir 可以留空. --user启动进程用户名,--group启动进程用户名的属组,如果为空默认都为mysql。 + +原始payload + +```json +{ + "ip":"10.1.1.1", + "port":27001, + "app":"test", + "areaId":"test1", + "setId":"s1", + "configSvr":false, + "ips":[ + "10.1.1.1:27001", + "10.1.1.2:27002", + "10.1.1.3:27003" + ], + "priority":{ + "10.1.1.1:27001":1, + "10.1.1.2:27002":1, + "10.1.1.3:27003":0 + }, + "hidden":{ + "10.1.1.1:27001":false, + "10.1.1.2:27002":false, + "10.1.1.3:27003":true + } +} +``` \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/example/mongo_add_user.example.md b/dbm-services/mongo/db-tools/dbactuator/example/mongo_add_user.example.md new file mode 100644 index 0000000000..1920afbca5 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/mongo_add_user.example.md @@ -0,0 +1,52 @@ +### add_user +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="add_user" --payload='{{payload_base64}}' +``` + + +原始payload + +创建管理员用户 +```json +{ + "ip":"10.1.1.1", + "port":27001, + "instanceType":"mongod", + "username":"xxx", + "password":"xxxxxxx", + "adminUsername":"", + "adminPassword":"", + "authDb":"admin", + "dbs":[ + + ], + "privileges":[ + "root" + ] +} +``` + +创建业务用户 +```json +{ + "ip":"10.1.1.1", + "port":27001, + "instanceType":"mongod", + "username":"xxx", + "password":"xxxxxxx", + "adminUsername":"xxx", + "adminPassword":"xxxxxxxx", + "authDb":"admin", + "dbs":[ + + ], + "privileges":[ + "xxx" + ] +} +``` + + +"instanceType"字段 "mongod":表示在复制集或者复制集单点进行创建用户 "mongos":表示cluster进行创建用户 \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/example/mongo_deinstall.example.md b/dbm-services/mongo/db-tools/dbactuator/example/mongo_deinstall.example.md new file mode 100644 index 0000000000..9374f50c74 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/mongo_deinstall.example.md @@ -0,0 +1,22 @@ +### mongo_deinstall +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="mongo_deinstall" --payload='{{payload_base64}}' +``` + +原始payload +```json +{ + "ip":"10.1.1.1", + "port":27002, + "app":"test", + "areaId":"test1", + "nodeInfo":[ + "10.1.1.1", + "10.1.1.2" + ], + "instanceType":"mongod" +} +``` + diff --git a/dbm-services/mongo/db-tools/dbactuator/example/mongo_del_user.example.md b/dbm-services/mongo/db-tools/dbactuator/example/mongo_del_user.example.md new file mode 100644 index 0000000000..d9f284ab45 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/mongo_del_user.example.md @@ -0,0 +1,34 @@ +### delete_user +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="delete_user" --payload='{{payload_base64}}' +``` + + +原始payload +mongos删除业务用户 +```json +{ + "ip":"10.1.1.1", + "port":27023, + "instanceType":"mongos", + "adminUsername":"xxx", + "adminPassword":"xxxxx", + "username":"xx", + "authDb":"admin" +} +``` + +mongod删除业务用户 +```json +{ + "ip":"10.1.1.1", + "port":27001, + "instanceType":"mongod", + "adminUsername":"xxx", + "adminPassword":"xxxx", + "username":"xx", + "authDb":"admin" +} +``` \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/example/mongo_execute_script.example.md b/dbm-services/mongo/db-tools/dbactuator/example/mongo_execute_script.example.md new file mode 100644 index 0000000000..54356f9e28 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/mongo_execute_script.example.md @@ -0,0 +1,30 @@ +### mongo_execute_script +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="mongo_execute_script" --payload='{{payload_base64}}' +``` + + +原始payload + +# 原始payload +```json +{ + "ip":"10.1.1.1", + "port":27021, + "script":"xxx", + "type":"cluster", + "secondary": false, + "adminUsername":"xxx", + "adminPassword":"xxxxxx", + "repoUrl":"url", + "repoUsername":"username", + "repoToken":"token", + "repoProject":"project", + "repoRepo":"project-package", + "repoPath":"path" +} +``` + +以repo为前缀的字段为制品库信息 \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/example/mongo_process_restart.example.md b/dbm-services/mongo/db-tools/dbactuator/example/mongo_process_restart.example.md new file mode 100644 index 0000000000..4fe5bd03f8 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/mongo_process_restart.example.md @@ -0,0 +1,42 @@ +### mongo_restart +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="mongo_restart" --payload='{{payload_base64}}' +``` + + +原始payload + +## mongod +```json +{ + "ip":"10.1.1.1", + "port":27001, + "instanceType":"mongod", + "singleNodeInstallRestart":false, + "auth":true, + "cacheSizeGB": null, + "mongoSConfDbOld":"", + "MongoSConfDbNew":"", + "adminUsername":"", + "adminPassword":"" +} +``` +"singleNodeInstallRestart"字段表示安装替换节点时mongod单节点重启 true:替换节点单节点重启 false:复制集节点重启 +"adminUsername"和"adminPassword"字段为空时表示安装时最后一步重启进程,不为空时表示提供服务期间重启 +## mongos +```json +{ + "ip":"10.1.1.1", + "port":27021, + "instanceType":"mongos", + "singleNodeInstallRestart":false, + "auth":true, + "cacheSizeGB": null, + "mongoSConfDbOld":"10.1.1.2:27001", + "MongoSConfDbNew":"10.1.1.2:27004", + "adminUsername":"", + "adminPassword":"" +} +``` \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/example/mongod_install.example.md b/dbm-services/mongo/db-tools/dbactuator/example/mongod_install.example.md new file mode 100644 index 0000000000..5aa9bfb27b --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/mongod_install.example.md @@ -0,0 +1,60 @@ +### mongod_install +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="mongod_install" --data_dir=/path/to/data --backup_dir=/path/to/backup --user="xxx" --group="xxx" --payload='{{payload_base64}}' +``` +--data_dir、--backup_dir 可以留空. --user启动进程用户名,--group启动进程用户名的属组,如果为空默认都为mysql。 + +原始payload + +## shardsvr +```json +{ + "mediapkg":{ + "pkg":"mongodb-linux-x86_64-3.4.20.tar.gz", + "pkg_md5":"e68d998d75df81b219e99795dec43ffb" + }, + "ip":"10.1.1.1", + "port":27001, + "dbVersion":"3.4.20", + "instanceType":"mongod", + "app":"test", + "areaId":"test1", + "setId":"s1", + "auth": true, + "clusterRole":"shardsvr", + "dbConfig":{ + "slowOpThresholdMs":200, + "cacheSizeGB":1, + "oplogSizeMB":500, + "destination":"file" + } +} +``` +部署复制集时"clusterRole"字段为空 + +## configsvr +```json +{ + "mediapkg":{ + "pkg":"mongodb-linux-x86_64-3.4.20.tar.gz", + "pkg_md5":"e68d998d75df81b219e99795dec43ffb" + }, + "ip":"10.1.1.1", + "port":27002, + "dbVersion":"3.4.20", + "instanceType":"mongod", + "app":"test", + "areaId":"test1", + "setId":"conf", + "auth": true, + "clusterRole":"configsvr", + "dbConfig":{ + "slowOpThresholdMs":200, + "cacheSizeGB":1, + "oplogSizeMB":500, + "destination":"file" + } +} +``` \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/example/mongod_replace.example.md b/dbm-services/mongo/db-tools/dbactuator/example/mongod_replace.example.md new file mode 100644 index 0000000000..ca568d908c --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/mongod_replace.example.md @@ -0,0 +1,29 @@ +### mongod_replace +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="mongod_replace" --payload='{{payload_base64}}' +``` + + +原始payload + +## mongod +```json +{ + "ip":"10.1.1.1", + "port":27002, + "sourceIP":"10.1.1.3", + "sourcePort":27007, + "sourceDown":true, + "adminUsername":"xxx", + "adminPassword":"xxxxxxxx", + "targetIP":"10.1.1.1", + "targetPort":27004, + "targetPriority":"", + "targetHidden":"" +} +``` +"sourceDown" 源端是否已down机 +"targetPriority"可以指定替换节点的优先级 +"targetHidden"可以指定替换节点是否为隐藏节点 \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/example/mongod_step_down.example.md b/dbm-services/mongo/db-tools/dbactuator/example/mongod_step_down.example.md new file mode 100644 index 0000000000..8730c595d1 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/mongod_step_down.example.md @@ -0,0 +1,18 @@ +### replicaset_stepdown +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="replicaset_stepdown" --payload='{{payload_base64}}' +``` + + +原始payload + +```json +{ + "ip":"10.1.1.1", + "port":27001, + "adminUsername":"xxx", + "adminPassword":"xxx" +} +``` \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/example/mongos_install.example.md b/dbm-services/mongo/db-tools/dbactuator/example/mongos_install.example.md new file mode 100644 index 0000000000..794ea00679 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/mongos_install.example.md @@ -0,0 +1,30 @@ +### mongos_install +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="mongos_install" --data_dir=/path/to/data --backup_dir=/path/to/backup --user="xxx" --group="xxx" --payload='{{payload_base64}}' +``` +--data_dir、--backup_dir 可以留空. --user启动进程用户名,--group启动进程用户名的属组,如果为空默认都为mysql。 + +原始payload + +```json +{ + "mediapkg":{ + "pkg":"mongodb-linux-x86_64-3.4.20.tar.gz", + "pkg_md5":"e68d998d75df81b219e99795dec43ffb" + }, + "ip":"10.1.1.1", + "port":27021, + "dbVersion":"3.4.20", + "instanceType":"mongos", + "app":"test", + "areaId":"test1", + "auth": true, + "configDB":["10.1.1.2:27001","10.1.1.3:27002","10.1.1.4:27003"], + "dbConfig":{ + "slowOpThresholdMs":200, + "destination":"file" + } +} +``` \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/example/os_mongo_init.example.md b/dbm-services/mongo/db-tools/dbactuator/example/os_mongo_init.example.md new file mode 100644 index 0000000000..3aa463c70e --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/example/os_mongo_init.example.md @@ -0,0 +1,16 @@ +### os_mongo_init +初始化新机器: + +```json +./dbactuator_redis --uid={{uid}} --root_id={{root_id}} --node_id={{node_id}} --version_id={{version_id}} --atom-job-list="os_mongo_init" --data_dir=/path/to/data --backup_dir=/path/to/backup --user="xxx" --group="xxx" --payload='{{payload_base64}}' +``` +--data_dir、--backup_dir 可以留空. --user启动进程用户名,--group启动进程用户名的属组,如果为空默认都为mysql。 + +原始payload + +```json +{ +"user":"xxx", +"password":"xxxxxxx" +} +``` \ No newline at end of file diff --git a/dbm-services/mongo/db-tools/dbactuator/go.mod b/dbm-services/mongo/db-tools/dbactuator/go.mod new file mode 100644 index 0000000000..3d1fc24785 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/go.mod @@ -0,0 +1,28 @@ +module dbm-services/mongo/db-tools/dbactuator + +go 1.18 + +require ( + github.com/dustin/go-humanize v1.0.0 + github.com/go-playground/validator/v10 v10.11.0 + github.com/shirou/gopsutil/v3 v3.23.1 + github.com/spf13/cobra v1.7.0 + golang.org/x/sys v0.11.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect + golang.org/x/text v0.3.7 // indirect +) diff --git a/dbm-services/mongo/db-tools/dbactuator/go.sum b/dbm-services/mongo/db-tools/dbactuator/go.sum new file mode 100644 index 0000000000..91ee4b3f96 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/go.sum @@ -0,0 +1,95 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= +github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil/v3 v3.23.1 h1:a9KKO+kGLKEvcPIs4W62v0nu3sciVDOOOPUD0Hz7z/4= +github.com/shirou/gopsutil/v3 v3.23.1/go.mod h1:NN6mnm5/0k8jw4cBfCnJtr5L7ErOTg18tMNpgFkn0hA= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/dbm-services/mongo/db-tools/dbactuator/imgs/bk-dbactuator-mongo_structur.png b/dbm-services/mongo/db-tools/dbactuator/imgs/bk-dbactuator-mongo_structur.png new file mode 100644 index 0000000000000000000000000000000000000000..4cc723ed152435c88754ba5082a00d1c262b5cb2 GIT binary patch literal 162444 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 z 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

_! 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

6Hjj&Bun&lPOTqGN{2 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�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 + +*/ +package main + +import "dbm-services/mongo/db-tools/dbactuator/cmd" + +func main() { + cmd.Execute() +} diff --git a/dbm-services/mongo/db-tools/dbactuator/mylog/mylog.go b/dbm-services/mongo/db-tools/dbactuator/mylog/mylog.go new file mode 100644 index 0000000000..8c50d50fa0 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/mylog/mylog.go @@ -0,0 +1,29 @@ +// Package mylog 便于全局日志操作 +package mylog + +import ( + "os" + + "dbm-services/common/go-pubpkg/logger" +) + +// Logger 和 jobruntime.Logger 是同一个 logger +var Logger *logger.Logger + +// SetDefaultLogger 设置默认logger +func SetDefaultLogger(log *logger.Logger) { + Logger = log +} + +// UnitTestInitLog 单元测试初始化Logger +func UnitTestInitLog() { + extMap := map[string]string{ + "uid": "1111", + "node_id": "localhost", + "root_id": "2222", + "version_id": "3333", + } + log01 := logger.New(os.Stdout, true, logger.InfoLevel, extMap) + log01.Sync() + SetDefaultLogger(log01) +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/add_shard_to_cluster.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/add_shard_to_cluster.go new file mode 100644 index 0000000000..5936df4598 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/add_shard_to_cluster.go @@ -0,0 +1,248 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// AddConfParams 参数 +type AddConfParams struct { + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + AdminUsername string `json:"adminUsername" validate:"required"` + AdminPassword string `json:"adminPassword" validate:"required"` + Shards map[string]string `json:"shards" validate:"required"` // key->clusterId,value->ip:port,ip:port 不包含隐藏节点 +} + +// AddShardToCluster 添加分片到集群 +type AddShardToCluster struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + Mongo string + OsUser string + ConfFilePath string + ConfFileContent string + ConfParams *AddConfParams +} + +// NewAddShardToCluster 实例化结构体 +func NewAddShardToCluster() jobruntime.JobRunner { + return &AddShardToCluster{} +} + +// Name 获取原子任务的名字 +func (a *AddShardToCluster) Name() string { + return "add_shard_to_cluster" +} + +// Run 运行原子任务 +func (a *AddShardToCluster) Run() error { + // 获取配置内容 + if err := a.makeConfContent(); err != nil { + return err + } + + // 生成js脚本 + if err := a.createAddShardToClusterScript(); err != nil { + return err + } + + // 执行js脚本 + if err := a.execScript(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (a *AddShardToCluster) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (a *AddShardToCluster) Rollback() error { + return nil +} + +// Init 初始化 +func (a *AddShardToCluster) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + a.runtime = runtime + a.runtime.Logger.Info("start to init") + a.BinDir = consts.UsrLocal + a.Mongo = filepath.Join(a.BinDir, "mongodb", "bin", "mongo") + a.ConfFilePath = filepath.Join("/", "tmp", "addShardToCluster.js") + a.OsUser = consts.GetProcessUser() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(a.runtime.PayloadDecoded), &a.ConfParams); err != nil { + a.runtime.Logger.Error(fmt.Sprintf( + "get parameters of initiateReplicaset fail by json.Unmarshal, error:%s", err)) + return fmt.Errorf("get parameters of initiateReplicaset fail by json.Unmarshal, error:%s", err) + } + a.runtime.Logger.Info("init successfully") + + // 进行校验 + if err := a.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (a *AddShardToCluster) checkParams() error { + // 校验重启配置参数 + validate := validator.New() + a.runtime.Logger.Info("start to validate parameters of addShardToCluster") + if err := validate.Struct(a.ConfParams); err != nil { + a.runtime.Logger.Error(fmt.Sprintf("validate parameters of addShardToCluster fail, error:%s", err)) + return fmt.Errorf("validate parameters of addShardToCluster fail, error:%s", err) + } + a.runtime.Logger.Info("validate parameters of addShardToCluster successfully") + return nil +} + +// makeConfContent 生成配置内容 +func (a *AddShardToCluster) makeConfContent() error { + a.runtime.Logger.Info("start to make config content of addShardToCluster") + var shards []string + for key, value := range a.ConfParams.Shards { + shards = append(shards, strings.Join([]string{key, "/", value}, "")) + } + + for _, v := range shards { + a.ConfFileContent += strings.Join([]string{"sh.addShard(\"", v, "\")\n"}, "") + } + a.runtime.Logger.Info("make config content of addShardToCluster successfully") + return nil +} + +// createAddShardToClusterScript 生成js脚本 +func (a *AddShardToCluster) createAddShardToClusterScript() error { + a.runtime.Logger.Info("start to create addShardToCluster script") + confFile, err := os.OpenFile(a.ConfFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, DefaultPerm) + defer confFile.Close() + if err != nil { + a.runtime.Logger.Error( + fmt.Sprintf("create script file of addShardToCluster fail, error:%s", err)) + return fmt.Errorf("create script file of addShardToCluster fail, error:%s", err) + } + + if _, err = confFile.WriteString(a.ConfFileContent); err != nil { + a.runtime.Logger.Error( + fmt.Sprintf("create script file of addShardToCluster write content fail, error:%s", + err)) + return fmt.Errorf("create script file of addShardToCluster write content fail, error:%s", + err) + } + a.runtime.Logger.Info("create addShardToCluster script successfully") + return nil +} + +// checkShard 检查shard是否已经加入到cluster中 +func (a *AddShardToCluster) checkShard() (bool, error) { + a.runtime.Logger.Info("start to check shard") + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --quiet --authenticationDatabase=admin --eval \"db.getMongo().getDB('config').shards.find()\" admin", + a.Mongo, a.ConfParams.AdminUsername, a.ConfParams.AdminPassword, a.ConfParams.IP, a.ConfParams.Port) + result, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + a.runtime.Logger.Error(fmt.Sprintf("get shard info fail, error:%s", err)) + return false, fmt.Errorf("get shard info fail, error:%s", err) + } + result = strings.Replace(result, "\n", "", -1) + if result == "" { + a.runtime.Logger.Info("shard is not existed") + return false, nil + } + + for k, _ := range a.ConfParams.Shards { + + if strings.Contains(result, k) { + continue + } + + return false, fmt.Errorf("add shard %s fail", k) + } + a.runtime.Logger.Info("check shard successfully") + return true, nil +} + +// execScript 执行脚本 +func (a *AddShardToCluster) execScript() error { + // 检查 + flag, err := a.checkShard() + if err != nil { + return err + } + if flag == true { + a.runtime.Logger.Info(fmt.Sprintf("shards have been added")) + // 删除脚本 + if err = a.removeScript(); err != nil { + return err + } + + return nil + } + + // 执行脚本 + a.runtime.Logger.Info("start to execute addShardToCluster script") + cmd := fmt.Sprintf("%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet %s", + a.Mongo, a.ConfParams.AdminUsername, a.ConfParams.AdminPassword, a.ConfParams.IP, a.ConfParams.Port, + a.ConfFilePath) + if _, err = util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + a.runtime.Logger.Error(fmt.Sprintf("execute addShardToCluster script fail, error:%s", err)) + return fmt.Errorf("execute addShardToCluster script fail, error:%s", err) + } + a.runtime.Logger.Info("execute addShardToCluster script successfully") + + time.Sleep(5 * time.Second) + + // 检查 + flag, err = a.checkShard() + if err != nil { + return err + } + if flag == false { + a.runtime.Logger.Error(fmt.Sprintf("add shard fail, error:%s", err)) + return fmt.Errorf("add shard fail, error:%s", err) + } + + // 删除脚本 + if err = a.removeScript(); err != nil { + return err + } + + return nil +} + +// removeScript 删除脚本 +func (a *AddShardToCluster) removeScript() error { + // 删除脚本 + a.runtime.Logger.Info("start to remove addShardToCluster script") + if err := common.RemoveFile(a.ConfFilePath); err != nil { + a.runtime.Logger.Error(fmt.Sprintf("remove addShardToCluster script fail, error:%s", err)) + return fmt.Errorf("remove addShardToCluster script fail, error:%s", err) + } + a.runtime.Logger.Info("remove addShardToCluster script successfully") + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/add_user.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/add_user.go new file mode 100644 index 0000000000..67ede47f53 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/add_user.go @@ -0,0 +1,264 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// AddUserConfParams 参数 +type AddUserConfParams struct { + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + InstanceType string `json:"instanceType" validate:"required"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + AdminUsername string `json:"adminUsername"` + AdminPassword string `json:"adminPassword"` + AuthDb string `json:"authDb"` // 为方便管理用户,验证库默认为admin库 + Dbs []string `json:"dbs"` // 业务库 + Privileges []string `json:"privileges"` // 权限 + +} + +// AddUser 添加分片到集群 +type AddUser struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + Mongo string + PrimaryIP string + PrimaryPort int + OsUser string + ScriptContent string + ConfParams *AddUserConfParams +} + +// NewAddUser 实例化结构体 +func NewAddUser() jobruntime.JobRunner { + return &AddUser{} +} + +// Name 获取原子任务的名字 +func (u *AddUser) Name() string { + return "add_user" +} + +// Run 运行原子任务 +func (u *AddUser) Run() error { + // 生成脚本内容 + if err := u.makeScriptContent(); err != nil { + return err + } + + // 执行js脚本 + if err := u.execScript(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (u *AddUser) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (u *AddUser) Rollback() error { + return nil +} + +// Init 初始化 +func (u *AddUser) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + u.runtime = runtime + u.runtime.Logger.Info("start to init") + u.BinDir = consts.UsrLocal + u.Mongo = filepath.Join(u.BinDir, "mongodb", "bin", "mongo") + u.OsUser = consts.GetProcessUser() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(u.runtime.PayloadDecoded), &u.ConfParams); err != nil { + u.runtime.Logger.Error(fmt.Sprintf( + "get parameters of addUser fail by json.Unmarshal, error:%s", err)) + return fmt.Errorf("get parameters of addUser fail by json.Unmarshal, error:%s", err) + } + + // 获取primary信息 + if u.ConfParams.InstanceType == "mongos" { + u.PrimaryIP = u.ConfParams.IP + u.PrimaryPort = u.ConfParams.Port + } else { + var info string + var err error + // 安装时无需密码验证。安装成功后需要密码验证 + if u.ConfParams.AdminUsername != "" && u.ConfParams.AdminPassword != "" { + info, err = common.AuthGetPrimaryInfo(u.Mongo, u.ConfParams.AdminUsername, + u.ConfParams.AdminPassword, u.ConfParams.IP, u.ConfParams.Port) + if err != nil { + u.runtime.Logger.Error(fmt.Sprintf( + "get primary db info of addUser fail, error:%s", err)) + return fmt.Errorf("get primary db info of addUser fail, error:%s", err) + } + getInfo := strings.Split(info, ":") + u.PrimaryIP = getInfo[0] + u.PrimaryPort, _ = strconv.Atoi(getInfo[1]) + } + } + u.runtime.Logger.Info("init successfully") + + // 进行校验 + if err := u.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (u *AddUser) checkParams() error { + // 校验重启配置参数 + validate := validator.New() + u.runtime.Logger.Info("start to validate parameters of addUser") + if err := validate.Struct(u.ConfParams); err != nil { + u.runtime.Logger.Error(fmt.Sprintf("validate parameters of addUser fail, error:%s", err)) + return fmt.Errorf("validate parameters of addUser fail, error:%s", err) + } + u.runtime.Logger.Info("validate parameters of addUser successfully") + return nil +} + +// makeScriptContent 生成user配置内容 +func (u *AddUser) makeScriptContent() error { + u.runtime.Logger.Info("start to make script content") + user := common.NewMongoUser() + user.User = u.ConfParams.Username + user.Pwd = u.ConfParams.Password + + // 判断验证db + if u.ConfParams.AuthDb == "" { + u.ConfParams.AuthDb = "admin" + } + + // 判断业务db是否存在 + if len(u.ConfParams.Dbs) == 0 { + u.ConfParams.Dbs = []string{"admin"} + } + + for _, db := range u.ConfParams.Dbs { + for _, privilege := range u.ConfParams.Privileges { + role := common.NewMongoRole() + role.Role = privilege + role.Db = db + user.Roles = append(user.Roles, role) + } + } + + content, err := user.GetContent() + if err != nil { + u.runtime.Logger.Error(fmt.Sprintf("make config content of addUser fail, error:%s", err)) + return fmt.Errorf("make config content of addUser fail, error:%s", err) + } + // content = strings.Replace(content, "\"", "\\\"", -1) + + // 获取mongo版本 + mongoName := "mongod" + if u.ConfParams.InstanceType == "mongos" { + mongoName = "mongos" + } + version, err := common.CheckMongoVersion(u.BinDir, mongoName) + if err != nil { + u.runtime.Logger.Error(fmt.Sprintf("check mongo version fail, error:%s", err)) + return fmt.Errorf("check mongo version fail, error:%s", err) + } + mainVersion, _ := strconv.Atoi(strings.Split(version, ".")[0]) + if mainVersion >= 3 { + u.ScriptContent = strings.Join([]string{"db", + fmt.Sprintf("createUser(%s)", content)}, ".") + u.runtime.Logger.Info("make script content successfully") + return nil + } + u.ScriptContent = strings.Join([]string{"db", + fmt.Sprintf("addUser(%s)", content)}, ".") + u.runtime.Logger.Info("make script content successfully") + + return nil +} + +// checkUser 检查用户是否存在 +func (u *AddUser) checkUser() (bool, error) { + var flag bool + var err error + time.Sleep(time.Second * 3) + // 安装时检查管理用户是否存在无需密码验证。安装后检查业务用户是否存在需密码验证 + if u.ConfParams.AdminUsername != "" && u.ConfParams.AdminPassword != "" { + flag, err = common.AuthCheckUser(u.Mongo, u.ConfParams.AdminUsername, u.ConfParams.AdminPassword, + u.PrimaryIP, u.PrimaryPort, u.ConfParams.AuthDb, u.ConfParams.Username) + } else { + flag, err = common.AuthCheckUser(u.Mongo, u.ConfParams.Username, u.ConfParams.Password, + u.ConfParams.IP, u.ConfParams.Port, u.ConfParams.AuthDb, u.ConfParams.Username) + } + return flag, err +} + +// execScript 执行脚本 +func (u *AddUser) execScript() error { + var cmd string + if u.ConfParams.AdminUsername != "" && u.ConfParams.AdminPassword != "" { + // 检查用户是否存在 + flag, err := u.checkUser() + if err != nil { + return err + } + if flag == true { + u.runtime.Logger.Info("user:%s has been existed", u.ConfParams.Username) + return nil + } + cmd = fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval '%s' %s", + u.Mongo, u.ConfParams.AdminUsername, u.ConfParams.AdminPassword, u.PrimaryIP, u.PrimaryPort, + u.ScriptContent, u.ConfParams.AuthDb) + } else if u.ConfParams.AdminUsername == "" && u.ConfParams.AdminPassword == "" { + // 复制集初始化后,马上创建db管理员用户,需要等3秒 + time.Sleep(time.Second * 3) + cmd = fmt.Sprintf( + "%s --host %s --port %d --quiet --eval '%s' %s", + u.Mongo, "127.0.0.1", u.ConfParams.Port, u.ScriptContent, u.ConfParams.AuthDb) + if u.ConfParams.AdminUsername != "" && u.ConfParams.AdminPassword != "" { + + } + } + + // 执行脚本 + u.runtime.Logger.Info("start to execute addUser script") + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + u.runtime.Logger.Error("execute addUser script fail, error:%s", err) + return fmt.Errorf("execute addUser script fail, error:%s", err) + } + u.runtime.Logger.Info("execute addUser script successfully") + + // 检查用户是否存在 + flag, err := u.checkUser() + if err != nil { + return err + } + if flag == false { + u.runtime.Logger.Error("add user:%s fail, error:%s", u.ConfParams.Username, err) + return fmt.Errorf("add user:%s fail, error:%s", u.ConfParams.Username, err) + } + + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/atommongodb.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/atommongodb.go new file mode 100644 index 0000000000..f022fb23e5 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/atommongodb.go @@ -0,0 +1,2 @@ +// Package atommongodb mongodb原子任务 +package atommongodb diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/cluster_balancer.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/cluster_balancer.go new file mode 100644 index 0000000000..95758fe350 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/cluster_balancer.go @@ -0,0 +1,159 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// BalancerConfParams 参数 +type BalancerConfParams struct { + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + Open bool `json:"open"` // true:打开 false:关闭 + AdminUsername string `json:"adminUsername" validate:"required"` + AdminPassword string `json:"adminPassword" validate:"required"` +} + +// Balancer 添加分片到集群 +type Balancer struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + Mongo string + OsUser string + ConfParams *BalancerConfParams +} + +// NewBalancer 实例化结构体 +func NewBalancer() jobruntime.JobRunner { + return &Balancer{} +} + +// Name 获取原子任务的名字 +func (b *Balancer) Name() string { + return "cluster_balancer" +} + +// Run 运行原子任务 +func (b *Balancer) Run() error { + // 执行脚本 + if err := b.execScript(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (b *Balancer) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (b *Balancer) Rollback() error { + return nil +} + +// Init 初始化 +func (b *Balancer) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + b.runtime = runtime + b.runtime.Logger.Info("start to init") + b.BinDir = consts.UsrLocal + b.Mongo = filepath.Join(b.BinDir, "mongodb", "bin", "mongo") + b.OsUser = consts.GetProcessUser() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(b.runtime.PayloadDecoded), &b.ConfParams); err != nil { + b.runtime.Logger.Error( + "get parameters of clusterBalancer fail by json.Unmarshal, error:%s", err) + return fmt.Errorf("get parameters of clusterBalancer fail by json.Unmarshal, error:%s", err) + } + b.runtime.Logger.Info("init successfully") + + // 进行校验 + if err := b.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (b *Balancer) checkParams() error { + // 校验配置参数 + validate := validator.New() + b.runtime.Logger.Info("start to validate parameters of clusterBalancer") + if err := validate.Struct(b.ConfParams); err != nil { + b.runtime.Logger.Error(fmt.Sprintf("validate parameters of clusterBalancer fail, error:%s", err)) + return fmt.Errorf("validate parameters of clusterBalancer fail, error:%s", err) + } + b.runtime.Logger.Info("validate parameters of clusterBalancer successfully") + return nil +} + +// execScript 执行脚本 +func (b *Balancer) execScript() error { + // 检查状态 + b.runtime.Logger.Info("start to get balancer status") + result, err := common.CheckBalancer(b.Mongo, b.ConfParams.IP, b.ConfParams.Port, + b.ConfParams.AdminUsername, b.ConfParams.AdminPassword) + if err != nil { + b.runtime.Logger.Error("get cluster balancer status fail, error:%s", err) + return fmt.Errorf("get cluster balancer status fail, error:%s", err) + } + flag, _ := strconv.ParseBool(result) + b.runtime.Logger.Info("get balancer status successfully") + if flag == b.ConfParams.Open { + b.runtime.Logger.Info("balancer status has been %t", b.ConfParams.Open) + os.Exit(0) + } + + // 执行脚本 + var cmd string + if b.ConfParams.Open == true { + cmd = fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"sh.startBalancer()\"", + b.Mongo, b.ConfParams.AdminUsername, b.ConfParams.AdminPassword, b.ConfParams.IP, b.ConfParams.Port) + } else { + cmd = fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"sh.stopBalancer()\"", + b.Mongo, b.ConfParams.AdminUsername, b.ConfParams.AdminPassword, b.ConfParams.IP, b.ConfParams.Port) + } + b.runtime.Logger.Info("start to execute script") + _, err = util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + b.runtime.Logger.Error("set cluster balancer status fail, error:%s", err) + return fmt.Errorf("set cluster balancer status fail, error:%s", err) + } + b.runtime.Logger.Info("execute script successfully") + + // 检查状态 + b.runtime.Logger.Info("start to check balancer status") + result, err = common.CheckBalancer(b.Mongo, b.ConfParams.IP, b.ConfParams.Port, + b.ConfParams.AdminUsername, b.ConfParams.AdminPassword) + if err != nil { + b.runtime.Logger.Error("get cluster balancer status fail, error:%s", err) + return fmt.Errorf("get cluster balancer status fail, error:%s", err) + } + flag, _ = strconv.ParseBool(result) + b.runtime.Logger.Info("check balancer status successfully") + if flag == b.ConfParams.Open { + b.runtime.Logger.Info("set balancer status:%t successfully", b.ConfParams.Open) + } + + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/del_user.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/del_user.go new file mode 100644 index 0000000000..393d3a3d6c --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/del_user.go @@ -0,0 +1,204 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// DelUserConfParams 参数 +type DelUserConfParams struct { + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + InstanceType string `json:"instanceType" validate:"required"` + AdminUsername string `json:"adminUsername" validate:"required"` + AdminPassword string `json:"adminPassword" validate:"required"` + Username string `json:"username" validate:"required"` + AuthDb string `json:"authDb"` // 为方便管理用户,验证库默认为admin库 +} + +// DelUser 添加分片到集群 +type DelUser struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + Mongo string + OsUser string + PrimaryIP string + PrimaryPort int + ScriptContent string + ConfParams *DelUserConfParams +} + +// NewDelUser 实例化结构体 +func NewDelUser() jobruntime.JobRunner { + return &DelUser{} +} + +// Name 获取原子任务的名字 +func (d *DelUser) Name() string { + return "delete_user" +} + +// Run 运行原子任务 +func (d *DelUser) Run() error { + // 生成脚本内容 + if err := d.makeScriptContent(); err != nil { + return err + } + + // 执行js脚本 + if err := d.execScript(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (d *DelUser) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (d *DelUser) Rollback() error { + return nil +} + +// Init 初始化 +func (d *DelUser) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + d.runtime = runtime + d.runtime.Logger.Info("start to init") + d.BinDir = consts.UsrLocal + d.Mongo = filepath.Join(d.BinDir, "mongodb", "bin", "mongo") + d.OsUser = consts.GetProcessUser() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(d.runtime.PayloadDecoded), &d.ConfParams); err != nil { + d.runtime.Logger.Error(fmt.Sprintf( + "get parameters of deleteUser fail by json.Unmarshal, error:%s", err)) + return fmt.Errorf("get parameters of deleteUser fail by json.Unmarshal, error:%s", err) + } + + // 获取primary信息 + if d.ConfParams.InstanceType == "mongos" { + d.PrimaryIP = d.ConfParams.IP + d.PrimaryPort = d.ConfParams.Port + } else { + info, err := common.AuthGetPrimaryInfo(d.Mongo, d.ConfParams.AdminUsername, d.ConfParams.AdminPassword, + d.ConfParams.IP, d.ConfParams.Port) + if err != nil { + d.runtime.Logger.Error(fmt.Sprintf( + "get primary db info of addUser fail, error:%s", err)) + return fmt.Errorf("get primary db info of addUser fail, error:%s", err) + } + getInfo := strings.Split(info, ":") + d.PrimaryIP = getInfo[0] + d.PrimaryPort, _ = strconv.Atoi(getInfo[1]) + } + d.runtime.Logger.Info("init successfully") + + // 进行校验 + if err := d.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (d *DelUser) checkParams() error { + // 校验重启配置参数 + validate := validator.New() + d.runtime.Logger.Info("start to validate parameters of deleteUser") + if err := validate.Struct(d.ConfParams); err != nil { + d.runtime.Logger.Error(fmt.Sprintf("validate parameters of deleteUser fail, error:%s", err)) + return fmt.Errorf("validate parameters of deleteUser fail, error:%s", err) + } + d.runtime.Logger.Info("validate parameters of deleteUser successfully") + return nil +} + +// makeScriptContent 生成user配置内容 +func (d *DelUser) makeScriptContent() error { + d.runtime.Logger.Info("start to make deleteUser script content") + // 判断验证db + if d.ConfParams.AuthDb == "" { + d.ConfParams.AuthDb = "admin" + } + + // 获取mongo版本 + mongoName := "mongod" + if d.ConfParams.InstanceType == "mongos" { + mongoName = "mongos" + } + version, err := common.CheckMongoVersion(d.BinDir, mongoName) + if err != nil { + d.runtime.Logger.Error(fmt.Sprintf("check mongo version fail, error:%s", err)) + return fmt.Errorf("check mongo version fail, error:%s", err) + } + mainVersion, _ := strconv.Atoi(strings.Split(version, ".")[0]) + if mainVersion >= 3 { + d.ScriptContent = strings.Join([]string{fmt.Sprintf("db.getMongo().getDB('%s')", d.ConfParams.AuthDb), + fmt.Sprintf("dropUser('%s')", d.ConfParams.Username)}, ".") + d.runtime.Logger.Info("make deleteUser script content successfully") + return nil + } + d.ScriptContent = strings.Join([]string{fmt.Sprintf("db.getMongo().getDB('%s')", d.ConfParams.AuthDb), + fmt.Sprintf("removeUser('%s')", d.ConfParams.Username)}, ".") + d.runtime.Logger.Info("make deleteUser script content successfully") + return nil +} + +// execScript 执行脚本 +func (d *DelUser) execScript() error { + // 检查 + flag, err := common.AuthCheckUser(d.Mongo, d.ConfParams.AdminUsername, d.ConfParams.AdminPassword, + d.PrimaryIP, d.PrimaryPort, d.ConfParams.AuthDb, d.ConfParams.Username) + if err != nil { + return err + } + if flag == false { + d.runtime.Logger.Info(fmt.Sprintf("user:%s is not existed", d.ConfParams.Username)) + return nil + } + + // 执行脚本 + d.runtime.Logger.Info("start to execute deleteUser script") + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"%s\"", + d.Mongo, d.ConfParams.AdminUsername, d.ConfParams.AdminPassword, d.PrimaryIP, + d.PrimaryPort, d.ScriptContent) + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + d.runtime.Logger.Error(fmt.Sprintf("execute addUser script fail, error:%s", err)) + return fmt.Errorf("execute addUser script fail, error:%s", err) + } + + time.Sleep(2 * time.Second) + + // 检查 + flag, err = common.AuthCheckUser(d.Mongo, d.ConfParams.AdminUsername, d.ConfParams.AdminPassword, + d.PrimaryIP, d.PrimaryPort, d.ConfParams.AuthDb, d.ConfParams.Username) + if err != nil { + return err + } + if flag == true { + d.runtime.Logger.Error(fmt.Sprintf("delete user:%s fail, error:%s", d.ConfParams.Username, err)) + return fmt.Errorf("delete user:%s fail, error:%s", d.ConfParams.Username, err) + } + d.runtime.Logger.Info("execute deleteUser script successfully") + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/initiate_replicaset.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/initiate_replicaset.go new file mode 100644 index 0000000000..f4c3587fae --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/initiate_replicaset.go @@ -0,0 +1,278 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// InitConfParams 参数 +type InitConfParams struct { + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + App string `json:"app" validate:"required"` + SetId string `json:"setId" validate:"required"` + ConfigSvr bool `json:"configSvr"` // shardsvr configsvr + Ips []string `json:"ips" validate:"required"` // ip:port + Priority map[string]int `json:"priority" validate:"required"` // key->ip:port,value->priority + Hidden map[string]bool `json:"hidden" validate:"required"` // key->ip:port,value->hidden(true or false) +} + +// InitiateReplicaset 复制集初始化 +type InitiateReplicaset struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + Mongo string + OsUser string + ConfFilePath string + ConfFileContent string + ConfParams *InitConfParams + ClusterId string + StatusChan chan int +} + +// NewInitiateReplicaset 实例化结构体 +func NewInitiateReplicaset() jobruntime.JobRunner { + return &InitiateReplicaset{} +} + +// Name 获取原子任务的名字 +func (i *InitiateReplicaset) Name() string { + return "init_replicaset" +} + +// Run 运行原子任务 +func (i *InitiateReplicaset) Run() error { + // 获取配置内容 + if err := i.makeConfContent(); err != nil { + return err + } + + // 生成js脚本 + if err := i.createInitiateReplicasetScript(); err != nil { + return err + } + + // 执行js脚本 + if err := i.execScript(); err != nil { + return err + } + + // 检查状态 + go i.checkStatus() + + // 获取状态 + if err := i.getStatus(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (i *InitiateReplicaset) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (i *InitiateReplicaset) Rollback() error { + return nil +} + +// Init 初始化 +func (i *InitiateReplicaset) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + i.runtime = runtime + i.runtime.Logger.Info("start to init") + i.BinDir = consts.UsrLocal + i.Mongo = filepath.Join(i.BinDir, "mongodb", "bin", "mongo") + i.OsUser = consts.GetProcessUser() + i.ConfFilePath = filepath.Join("/", "tmp", "initiateReplicaset.js") + i.StatusChan = make(chan int, 1) + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(i.runtime.PayloadDecoded), &i.ConfParams); err != nil { + i.runtime.Logger.Error(fmt.Sprintf( + "get parameters of initiateReplicaset fail by json.Unmarshal, error:%s", err)) + return fmt.Errorf("get parameters of initiateReplicaset fail by json.Unmarshal, error:%s", err) + } + i.ClusterId = strings.Join([]string{i.ConfParams.App, i.ConfParams.SetId}, "-") + i.runtime.Logger.Info("init successfully") + + // 进行校验 + if err := i.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (i *InitiateReplicaset) checkParams() error { + // 校验重启配置参数 + validate := validator.New() + i.runtime.Logger.Info("start to validate parameters of initiateReplicaset") + if err := validate.Struct(i.ConfParams); err != nil { + i.runtime.Logger.Error(fmt.Sprintf("validate parameters of initiateReplicaset fail, error:%s", err)) + return fmt.Errorf("validate parameters of initiateReplicaset fail, error:%s", err) + } + i.runtime.Logger.Info("validate parameters of initiateReplicaset successfully") + return nil +} + +// makeConfContent 获取配置内容 +func (i *InitiateReplicaset) makeConfContent() error { + i.runtime.Logger.Info("start to make config content of initiateReplicaset") + jsonConfReplicaset := common.NewJsonConfReplicaset() + jsonConfReplicaset.Id = i.ClusterId + for index, value := range i.ConfParams.Ips { + member := common.NewMember() + member.Id = index + member.Host = i.ConfParams.Ips[index] + member.Priority = i.ConfParams.Priority[value] + member.Hidden = i.ConfParams.Hidden[value] + jsonConfReplicaset.Members = append(jsonConfReplicaset.Members, member) + } + jsonConfReplicaset.ConfigSvr = i.ConfParams.ConfigSvr + + var err error + confJson, err := json.Marshal(jsonConfReplicaset) + if err != nil { + i.runtime.Logger.Error( + fmt.Sprintf("config content of initiateReplicaset json Marshal fial, error:%s", err)) + return fmt.Errorf("config content of initiateReplicaset json Marshal fial, error:%s", err) + } + i.ConfFileContent = strings.Join([]string{"var config=", + string(confJson), "\n", "rs.initiate(config)\n"}, "") + i.runtime.Logger.Info("make config content of initiateReplicaset successfully") + return nil +} + +// createInitiateReplicasetScript 生成js脚本 +func (i *InitiateReplicaset) createInitiateReplicasetScript() error { + i.runtime.Logger.Info("start to create initiateReplicaset script") + confFile, err := os.OpenFile(i.ConfFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, DefaultPerm) + defer confFile.Close() + if err != nil { + i.runtime.Logger.Error( + fmt.Sprintf("create script file of initiateReplicaset json Marshal fail, error:%s", err)) + return fmt.Errorf("create script file of initiateReplicaset json Marshal fail, error:%s", err) + } + + if _, err = confFile.WriteString(i.ConfFileContent); err != nil { + i.runtime.Logger.Error( + fmt.Sprintf("create script file of initiateReplicaset write content fail, error:%s", + err)) + return fmt.Errorf("create script file of initiateReplicaset write content fail, error:%s", + err) + } + i.runtime.Logger.Info("create initiateReplicaset script successfully") + return nil +} + +// getPrimaryInfo 检查状态 +func (i *InitiateReplicaset) getPrimaryInfo() (bool, error) { + i.runtime.Logger.Info("start to check replicaset status") + result, err := common.InitiateReplicasetGetPrimaryInfo(i.Mongo, i.ConfParams.IP, i.ConfParams.Port) + if err != nil { + i.runtime.Logger.Error(fmt.Sprintf("get initiateReplicaset primary info fail, error:%s", err)) + return false, fmt.Errorf("get initiateReplicaset primary info fail, error:%s", err) + } + i.runtime.Logger.Info("check replicaset status successfully") + for _, v := range i.ConfParams.Ips { + if v == result { + return true, nil + } + } + + return false, nil +} + +// checkStatus 检查复制集状态 +func (i *InitiateReplicaset) checkStatus() { + for { + result, err := common.NoAuthGetPrimaryInfo(i.Mongo, i.ConfParams.IP, i.ConfParams.Port) + if err != nil { + i.runtime.Logger.Error("check replicaset status fail, error:%s", err) + fmt.Sprintf("check replicaset status fail, error:%s\n", err) + panic(fmt.Sprintf("check replicaset status fail, error:%s\n", err.Error())) + } + if result != "" { + i.StatusChan <- 1 + } + time.Sleep(2 * time.Second) + } +} + +// execScript 执行脚本 +func (i *InitiateReplicaset) execScript() error { + // 检查 + flag, err := i.getPrimaryInfo() + if err != nil { + return err + } + if flag == true { + i.runtime.Logger.Info("replicaset has been initiated") + if err = i.removeScript(); err != nil { + return err + } + + return nil + } + + // 执行脚本 + i.runtime.Logger.Info("start to execute initiateReplicaset script") + cmd := fmt.Sprintf("%s --host %s --port %d --quiet %s", + i.Mongo, "127.0.0.1", i.ConfParams.Port, i.ConfFilePath) + if _, err = util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + i.runtime.Logger.Error("execute initiateReplicaset script fail, error:%s", err) + return fmt.Errorf("execute initiateReplicaset script fail, error:%s", err) + } + i.runtime.Logger.Info("execute initiateReplicaset script successfully") + return nil +} + +// getStatus 检查复制集状态,是否创建成功 +func (i *InitiateReplicaset) getStatus() error { + for { + select { + case status := <-i.StatusChan: + if status == 1 { + i.runtime.Logger.Info("initiate replicaset successfully") + // 删除脚本 + if err := i.removeScript(); err != nil { + return err + } + return nil + } + default: + + } + } +} + +// removeScript 删除脚本 +func (i *InitiateReplicaset) removeScript() error { + // 删除脚本 + i.runtime.Logger.Info("start to remove initiateReplicaset script") + if err := common.RemoveFile(i.ConfFilePath); err != nil { + i.runtime.Logger.Error(fmt.Sprintf("remove initiateReplicaset script fail, error:%s", err)) + return fmt.Errorf("remove initiateReplicaset script fail, error:%s", err) + } + i.runtime.Logger.Info("remove initiateReplicaset script successfully") + + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_deinstall.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_deinstall.go new file mode 100644 index 0000000000..a9e795fd95 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_deinstall.go @@ -0,0 +1,230 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// DeInstallConfParams 参数 +type DeInstallConfParams struct { + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + App string `json:"app" validate:"required"` + SetId string `json:"setId" validate:"required"` + NodeInfo []string `json:"nodeInfo" validate:"required"` // []string ip,ip 如果为复制集节点,则为复制集所有节点的ip;如果为mongos,则为mongos的ip + InstanceType string `json:"instanceType" validate:"required"` // mongod mongos +} + +// DeInstall 添加分片到集群 +type DeInstall struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + DataDir string + BackupDir string + DbpathDir string + InstallPath string + PortDir string + LogPortDir string + DbPathRenameDir string + LogPathRenameDir string + Mongo string + OsUser string + ServiceStatus bool + IPInfo string + ConfParams *DeInstallConfParams +} + +// NewDeInstall 实例化结构体 +func NewDeInstall() jobruntime.JobRunner { + return &DeInstall{} +} + +// Name 获取原子任务的名字 +func (d *DeInstall) Name() string { + return "mongo_deinstall" +} + +// Run 运行原子任务 +func (d *DeInstall) Run() error { + // 检查实例状态 + if err := d.checkMongoService(); err != nil { + return err + } + + // 关闭进程 + if err := d.shutdownProcess(); err != nil { + return err + } + + // rename目录 + if err := d.DirRename(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (d *DeInstall) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (d *DeInstall) Rollback() error { + return nil +} + +// Init 初始化 +func (d *DeInstall) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + d.runtime = runtime + d.runtime.Logger.Info("start to init") + d.BinDir = consts.UsrLocal + d.DataDir = consts.GetMongoDataDir() + d.BackupDir = consts.GetMongoBackupDir() + + d.OsUser = consts.GetProcessUser() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(d.runtime.PayloadDecoded), &d.ConfParams); err != nil { + d.runtime.Logger.Error( + "get parameters of deInstall fail by json.Unmarshal, error:%s", err) + return fmt.Errorf("get parameters of deInstall fail by json.Unmarshal, error:%s", err) + } + + // 获取各种目录 + d.InstallPath = filepath.Join(d.BinDir, "mongodb") + d.Mongo = filepath.Join(d.BinDir, "mongodb", "bin", "mongo") + strPort := strconv.Itoa(d.ConfParams.Port) + d.PortDir = filepath.Join(d.DataDir, "mongodata", strPort) + d.DbpathDir = filepath.Join(d.DataDir, "mongodata", strPort, "db") + d.DbPathRenameDir = filepath.Join(d.DataDir, "mongodata", fmt.Sprintf("%s_%s_%s_%d", + d.ConfParams.InstanceType, d.ConfParams.App, d.ConfParams.SetId, d.ConfParams.Port)) + d.IPInfo = strings.Join(d.ConfParams.NodeInfo, "|") + d.LogPortDir = filepath.Join(d.BackupDir, "mongolog", strPort) + d.LogPathRenameDir = filepath.Join(d.BackupDir, "mongolog", fmt.Sprintf("%s_%s_%s_%d", + d.ConfParams.InstanceType, d.ConfParams.App, d.ConfParams.SetId, d.ConfParams.Port)) + + // 进行校验 + if err := d.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (d *DeInstall) checkParams() error { + // 校验配置参数 + d.runtime.Logger.Info("start to validate parameters") + validate := validator.New() + d.runtime.Logger.Info("start to validate parameters of deInstall") + if err := validate.Struct(d.ConfParams); err != nil { + d.runtime.Logger.Error("validate parameters of deInstall fail, error:%s", err) + return fmt.Errorf("validate parameters of deInstall fail, error:%s", err) + } + return nil +} + +// checkMongoService 检查mongo服务 +func (d *DeInstall) checkMongoService() error { + d.runtime.Logger.Info("start to check process status") + flag, _, err := common.CheckMongoService(d.ConfParams.Port) + if err != nil { + d.runtime.Logger.Error("get mongo service status fail, error:%s", err) + return fmt.Errorf("get mongo service status fail, error:%s", err) + } + d.ServiceStatus = flag + return nil +} + +// checkConnection 检查连接 +func (d *DeInstall) checkConnection() error { + d.runtime.Logger.Info("start to check connection") + cmd := fmt.Sprintf( + "source /etc/profile;netstat -nat | grep %d |awk '{print $5}'|awk -F: '{print $1}'|sort|uniq -c|sort -nr |grep -Ewv '0.0.0.0|127.0.0.1|%s' || true", + d.ConfParams.Port, d.IPInfo) + + result, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + d.runtime.Logger.Error("check connection fail, error:%s", err) + return fmt.Errorf("check connection fail, error:%s", err) + } + result = strings.Replace(result, "\n", "", -1) + if result != "" { + d.runtime.Logger.Error("check connection fail, there are some connections") + return fmt.Errorf("check connection fail, there are some connections") + } + return nil +} + +// shutdownProcess 关闭进程 +func (d *DeInstall) shutdownProcess() error { + if d.ServiceStatus == true { + d.runtime.Logger.Info("start to shutdown service") + // 检查连接 + if err := d.checkConnection(); err != nil { + return err + } + + // 关闭进程 + if err := common.ShutdownMongoProcess(d.OsUser, d.ConfParams.InstanceType, d.BinDir, d.DbpathDir, + d.ConfParams.Port); err != nil { + d.runtime.Logger.Error("shutdown mongo service fail, error:%s", err) + return fmt.Errorf("shutdown mongo service fail, error:%s", err) + } + } + + return nil +} + +// DirRename 打包数据目录 +func (d *DeInstall) DirRename() error { + // renameDb数据目录 + flag := util.FileExists(d.PortDir) + if flag == true { + d.runtime.Logger.Info("start to rename db directory") + cmd := fmt.Sprintf( + "mv %s %s", + d.PortDir, d.DbPathRenameDir) + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + d.runtime.Logger.Error("rename db directory fail, error:%s", err) + return fmt.Errorf("rename db directory fail, error:%s", err) + } + } + + // renameDb日志目录 + flag = util.FileExists(d.LogPortDir) + if flag == true { + d.runtime.Logger.Info("start to rename log directory") + cmd := fmt.Sprintf( + "mv %s %s", + d.LogPortDir, d.LogPathRenameDir) + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + d.runtime.Logger.Error("rename log directory fail, error:%s", err) + return fmt.Errorf("rename log directory fail, error:%s", err) + } + } + + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_execute_script.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_execute_script.go new file mode 100644 index 0000000000..2f1da8b259 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_execute_script.go @@ -0,0 +1,331 @@ +package atommongodb + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// ExecScriptConfParams 参数 +type ExecScriptConfParams struct { + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + Script string `json:"script" validate:"required"` + Type string `json:"type" validate:"required"` // cluster:执行脚本为传入的mongos replicaset:执行脚本为指定节点 + Secondary bool `json:"secondary"` // 复制集是否在secondary节点执行script + AdminUsername string `json:"adminUsername" validate:"required"` + AdminPassword string `json:"adminPassword" validate:"required"` + RepoUrl string `json:"repoUrl"` // 制品库url + RepoUsername string `json:"repoUsername"` // 制品库用户名 + RepoToken string `json:"repoToken"` // 制品库token + RepoProject string `json:"repoProject"` // 制品库project + RepoRepo string `json:"repoRepo"` // 制品库repo + RepoPath string `json:"repoPath"` // 制品库路径 +} + +// ExecScript 添加分片到集群 +type ExecScript struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + Mongo string + OsUser string + OsGroup string + execIP string + execPort int + ScriptDir string + ScriptContent string + ScriptFilePath string + ResultFilePath string + ConfParams *ExecScriptConfParams +} + +// NewExecScript 实例化结构体 +func NewExecScript() jobruntime.JobRunner { + return &ExecScript{} +} + +// Name 获取原子任务的名字 +func (e *ExecScript) Name() string { + return "mongo_execute_script" +} + +// Run 运行原子任务 +func (e *ExecScript) Run() error { + // 生成script内容 + if err := e.makeScriptContent(); err != nil { + return err + } + + // 创建script文件 + if err := e.creatScriptFile(); err != nil { + return err + } + + // 执行脚本生成结果文件 + if err := e.execScript(); err != nil { + return err + } + + // 上传结果文件到制品库 + if err := e.uploadFile(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (e *ExecScript) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (e *ExecScript) Rollback() error { + return nil +} + +// Init 初始化 +func (e *ExecScript) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + e.runtime = runtime + e.runtime.Logger.Info("start to init") + e.BinDir = consts.UsrLocal + e.OsUser = consts.GetProcessUser() + e.OsGroup = consts.GetProcessUserGroup() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(e.runtime.PayloadDecoded), &e.ConfParams); err != nil { + e.runtime.Logger.Error( + "get parameters of execScript fail by json.Unmarshal, error:%s", err) + return fmt.Errorf("get parameters of execScript fail by json.Unmarshal, error:%s", err) + } + + // 获取各种目录 + e.Mongo = filepath.Join(e.BinDir, "mongodb", "bin", "mongo") + e.ScriptDir = filepath.Join("/", "home", e.OsUser, e.runtime.UID) + e.ScriptFilePath = filepath.Join(e.ScriptDir, strings.Join([]string{"script", "js"}, ".")) + e.ResultFilePath = filepath.Join(e.ScriptDir, strings.Join([]string{"result", "txt"}, ".")) + e.runtime.Logger.Info("init successfully") + + // 复制集获取执行脚本的IP端口 默认为primary节点 可以指定secondary节点 + if e.ConfParams.Type == "cluster" { + e.execIP = e.ConfParams.IP + e.execPort = e.ConfParams.Port + } + if e.ConfParams.Type == "replicaset" { + primaryInfo, err := common.AuthGetPrimaryInfo(e.Mongo, e.ConfParams.AdminUsername, + e.ConfParams.AdminPassword, + e.ConfParams.IP, e.ConfParams.Port) + if err != nil { + e.runtime.Logger.Error("init get primary info fail, error:%s", err) + return fmt.Errorf("init get primary info fail, error:%s", err) + } + e.execIP = strings.Split(primaryInfo, ":")[0] + e.execPort, _ = strconv.Atoi(strings.Split(primaryInfo, ":")[1]) + if e.ConfParams.Secondary == true { + _, _, _, _, _, memberInfo, err := common.GetNodeInfo(e.Mongo, e.ConfParams.IP, e.ConfParams.Port, + e.ConfParams.AdminUsername, e.ConfParams.AdminPassword, e.ConfParams.IP, e.ConfParams.Port) + if err != nil { + e.runtime.Logger.Error("init get member info fail, error:%s", err) + return fmt.Errorf("init get member info fail, error:%s", err) + } + for _, v := range memberInfo { + if v["state"] == "2" && v["hidden"] == "false" { + e.execIP = strings.Split(v["name"], ":")[0] + e.execPort, _ = strconv.Atoi(strings.Split(v["name"], ":")[1]) + } + } + } + } + + // 进行校验 + if err := e.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (e *ExecScript) checkParams() error { + // 校验配置参数 + e.runtime.Logger.Info("start to validate parameters") + validate := validator.New() + e.runtime.Logger.Info("start to validate parameters of deInstall") + if err := validate.Struct(e.ConfParams); err != nil { + e.runtime.Logger.Error("validate parameters of execScript fail, error:%s", err) + return fmt.Errorf("validate parameters of execScript fail, error:%s", err) + } + e.runtime.Logger.Info("validate parameters successfully") + return nil +} + +// makeScriptContent 生成script内容 +func (e *ExecScript) makeScriptContent() error { + // 复制集,判断在primary节点还是在secondary节点执行脚本 + e.runtime.Logger.Info("start to make script content") + if e.ConfParams.Type == "replicaset" && e.ConfParams.Secondary == true { + // 获取mongo版本呢 + mongoName := "mongod" + version, err := common.CheckMongoVersion(e.BinDir, mongoName) + if err != nil { + e.runtime.Logger.Error("get mongo service version fail, error:%s", err) + return fmt.Errorf("get mongo service version fail, error:%s", err) + } + splitVersion := strings.Split(version, ".") + mainVersion, _ := strconv.ParseFloat(strings.Join([]string{splitVersion[0], splitVersion[1]}, "."), 32) + + // secondary执行script + secondaryOk := "rs.slaveOk()\n" + if mainVersion >= 3.6 { + secondaryOk = "rs.secondaryOk()\n" + } + e.ScriptContent = strings.Join([]string{secondaryOk, e.ConfParams.Script}, "") + e.runtime.Logger.Info("make script content successfully") + return nil + } + e.ScriptContent = e.ConfParams.Script + e.runtime.Logger.Info("make script content successfully") + return nil +} + +// creatScriptFile 创建script文件 +func (e *ExecScript) creatScriptFile() error { + // 创建目录 + e.runtime.Logger.Info("start to make script directory") + if err := util.MkDirsIfNotExists([]string{e.ScriptDir}); err != nil { + e.runtime.Logger.Error("create script directory:%s fail, error:%s", e.ScriptDir, err) + return fmt.Errorf("create script directory:%s fail, error:%s", e.ScriptDir, err) + } + + // 创建文件 + e.runtime.Logger.Info("start to create script file") + script, err := os.OpenFile(e.ScriptFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, DefaultPerm) + defer script.Close() + if err != nil { + e.runtime.Logger.Error( + fmt.Sprintf("create script file fail, error:%s", err)) + return fmt.Errorf("create script file fail, error:%s", err) + } + if _, err = script.WriteString(e.ScriptContent); err != nil { + e.runtime.Logger.Error( + fmt.Sprintf("script file write content fail, error:%s", + err)) + return fmt.Errorf("script file write content fail, error:%s", + err) + } + e.runtime.Logger.Info("create script file successfully") + // 修改配置文件属主 + e.runtime.Logger.Info("start to execute chown command for script file") + if _, err = util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", e.OsUser, e.OsGroup, e.ScriptDir), + "", nil, + 10*time.Second); err != nil { + e.runtime.Logger.Error(fmt.Sprintf("chown script file fail, error:%s", err)) + return fmt.Errorf("chown script file fail, error:%s", err) + } + e.runtime.Logger.Info("execute chown command for script file successfully") + return nil +} + +// execScript 执行脚本 +func (e *ExecScript) execScript() error { + e.runtime.Logger.Info("start to execute script") + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet %s > %s", + e.Mongo, e.ConfParams.AdminUsername, e.ConfParams.AdminPassword, e.execIP, e.execPort, + e.ScriptFilePath, e.ResultFilePath) + cmdX := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet %s > %s", + e.Mongo, e.ConfParams.AdminUsername, "xxx", e.execIP, e.execPort, + e.ScriptFilePath, e.ResultFilePath) + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + e.runtime.Logger.Error("execute script:%s fail, error:%s", cmdX, err) + return fmt.Errorf("execute script:%s fail, error:%s", cmdX, err) + } + e.runtime.Logger.Info("execute script:%s successfully", cmdX) + return nil +} + +// Output 请求响应结构体 +type Output struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// uploadFile 上传结果文件 +func (e *ExecScript) uploadFile() error { + if e.ConfParams.RepoUrl == "" { + return nil + } + e.runtime.Logger.Info("start to upload result file") + // url + url := strings.Join([]string{e.ConfParams.RepoUrl, e.ConfParams.RepoProject, e.ConfParams.RepoRepo, + e.ConfParams.RepoPath, e.runtime.UID, "result.txt"}, "/") + + // 生成请求body内容 + file, err := ioutil.ReadFile(e.ResultFilePath) + if err != nil { + e.runtime.Logger.Error("get result file content fail, error:%s", err) + return fmt.Errorf("get result file content fail, error:%s", err) + } + + // 生成请求 + request, err := http.NewRequest("PUT", url, strings.NewReader(string(file))) + if err != nil { + e.runtime.Logger.Error("create request for uploading result file fail, error:%s", err) + return fmt.Errorf("create request for uploading result file fail, error:%s", err) + } + + // 设置请求头 + auth := base64.StdEncoding.EncodeToString([]byte(strings.Join([]string{e.ConfParams.RepoUsername, + e.ConfParams.RepoToken}, ":"))) + request.Header.Set("Authorization", "Basic "+auth) + request.Header.Set("X-BKREPO-EXPIRES", "30") + request.Header.Set("X-BKREPO-OVERWRITE", "true") + request.Header.Set("Content-Type", "multipart/form-data") + if err != nil { + e.runtime.Logger.Error("set request head for uploading result file fail, error:%s", err) + return fmt.Errorf("set request head for uploading result file fail, error:%s", err) + } + + // 执行请求 + response, err := http.DefaultClient.Do(request) + defer response.Body.Close() + if err != nil { + e.runtime.Logger.Error("request server for uploading result file fail, error:%s", err) + return fmt.Errorf("request server for uploading result file fail, error:%s", err) + } + + // 解析响应 + resp, err := ioutil.ReadAll(response.Body) + if err != nil { + e.runtime.Logger.Error("read data from response fail, error:%s", err) + return fmt.Errorf("read data from response fail, error:%s", err) + } + output := Output{} + _ = json.Unmarshal(resp, &output) + if output.Code != 0 && output.Message == "" { + e.runtime.Logger.Error("upload file fail, error:%s", output.Message) + return fmt.Errorf("upload file fail, error:%s", output.Message) + } + e.runtime.Logger.Info("upload result file successfully") + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_process_restart.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_process_restart.go new file mode 100644 index 0000000000..b802e1fc19 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_process_restart.go @@ -0,0 +1,399 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + + "github.com/go-playground/validator/v10" + "gopkg.in/yaml.v2" +) + +// RestartConfParams 重启进程参数 +type RestartConfParams struct { + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + InstanceType string `json:"instanceType" validate:"required"` // mongos mongod + SingleNodeInstallRestart bool `json:"singleNodeInstallRestart"` // mongod替换节点安装后重启 + Auth bool `json:"auth"` // true->auth false->noauth + CacheSizeGB int `json:"cacheSizeGB"` // 可选,重启mongod的参数 + MongoSConfDbOld string `json:"mongoSConfDbOld"` // 可选,ip:port + MongoSConfDbNew string `json:"MongoSConfDbNew"` // 可选,ip:port + AdminUsername string `json:"adminUsername"` + AdminPassword string `json:"adminPassword"` +} + +// MongoRestart 重启mongo进程 +type MongoRestart struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + DataDir string + DbpathDir string + Mongo string + OsUser string // MongoDB安装在哪个用户下 + OsGroup string + ConfParams *RestartConfParams + AuthConfFilePath string + NoAuthConfFilePath string +} + +// NewMongoRestart 实例化结构体 +func NewMongoRestart() jobruntime.JobRunner { + return &MongoRestart{} +} + +// Name 获取原子任务的名字 +func (r *MongoRestart) Name() string { + return "mongo_restart" +} + +// Run 运行原子任务 +func (r *MongoRestart) Run() error { + // 修改配置文件参数 + if err := r.changeParam(); err != nil { + return err + } + + // mongod的primary进行主备切换 + if err := r.RsStepDown(); err != nil { + return err + } + + // 关闭服务 + if err := r.shutdown(); err != nil { + return err + } + + // 启动服务 + if err := r.startup(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (r *MongoRestart) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (r *MongoRestart) Rollback() error { + return nil +} + +// Init 初始化 +func (r *MongoRestart) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + r.runtime = runtime + r.runtime.Logger.Info("start to init") + r.BinDir = consts.UsrLocal + r.DataDir = consts.GetRedisDataDir() + r.OsUser = consts.GetProcessUser() + r.OsGroup = consts.GetProcessUserGroup() + r.Mongo = filepath.Join(r.BinDir, "mongodb", "bin", "mongo") + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(r.runtime.PayloadDecoded), &r.ConfParams); err != nil { + r.runtime.Logger.Error(fmt.Sprintf( + "get parameters of mongo restart fail by json.Unmarshal, error:%s", err)) + return fmt.Errorf("get parameters of mongo restart fail by json.Unmarshal, error:%s", err) + } + + // 设置各种路径 + strPort := strconv.Itoa(r.ConfParams.Port) + r.DbpathDir = filepath.Join(r.DataDir, "mongodata", strPort, "db") + r.AuthConfFilePath = filepath.Join(r.DataDir, "mongodata", strPort, "mongo.conf") + r.NoAuthConfFilePath = filepath.Join(r.DataDir, "mongodata", strPort, "noauth.conf") + r.runtime.Logger.Info("init successfully") + + // 安装前进行校验 + if err := r.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (r *MongoRestart) checkParams() error { + // 校验重启配置参数 + validate := validator.New() + r.runtime.Logger.Info("start to validate parameters of restart") + if err := validate.Struct(r.ConfParams); err != nil { + r.runtime.Logger.Error(fmt.Sprintf("validate parameters of restart fail, error:%s", err)) + return fmt.Errorf("validate parameters of restart fail, error:%s", err) + } + r.runtime.Logger.Info("validate parameters of restart successfully") + return nil +} + +// changeParam 修改参数 +func (r *MongoRestart) changeParam() error { + if r.ConfParams.InstanceType == "mongos" && + r.ConfParams.MongoSConfDbOld != "" && r.ConfParams.MongoSConfDbNew != "" { + if err := r.changeConfigDb(); err != nil { + return err + } + return nil + } + if err := r.changeCacheSizeGB(); err != nil { + return err + } + return nil +} + +// changeConfigDb 修改mongoS的ConfigDb参数 +func (r *MongoRestart) changeConfigDb() error { + r.runtime.Logger.Info("start to change configDB value of config file") + // 获取配置文件内容 + readAuthConfFileContent, _ := ioutil.ReadFile(r.AuthConfFilePath) + readNoAuthConfFileContent, _ := ioutil.ReadFile(r.NoAuthConfFilePath) + + // 修改configDB配置 + yamlAuthMongoSConf := common.NewYamlMongoSConf() + yamlNoAuthMongoSConf := common.NewYamlMongoSConf() + _ = yaml.Unmarshal(readAuthConfFileContent, yamlAuthMongoSConf) + _ = yaml.Unmarshal(readNoAuthConfFileContent, yamlNoAuthMongoSConf) + yamlAuthMongoSConf.Sharding.ConfigDB = strings.Replace(yamlAuthMongoSConf.Sharding.ConfigDB, + r.ConfParams.MongoSConfDbOld, r.ConfParams.MongoSConfDbNew, -1) + yamlNoAuthMongoSConf.Sharding.ConfigDB = strings.Replace(yamlNoAuthMongoSConf.Sharding.ConfigDB, + r.ConfParams.MongoSConfDbOld, r.ConfParams.MongoSConfDbNew, -1) + authConfFileContent, _ := yamlAuthMongoSConf.GetConfContent() + noAuthConfFileContent, _ := yamlNoAuthMongoSConf.GetConfContent() + + // 修改authConfFile + authConfFile, err := os.OpenFile(r.AuthConfFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, DefaultPerm) + defer authConfFile.Close() + if err != nil { + r.runtime.Logger.Error( + fmt.Sprintf("create auth config file fail, error:%s", err)) + return fmt.Errorf("create auth config file fail, error:%s", err) + } + if _, err = authConfFile.WriteString(string(authConfFileContent)); err != nil { + r.runtime.Logger.Error( + fmt.Sprintf("change configDB value of auth config file write content fail, error:%s", + err)) + return fmt.Errorf("change configDB value of auth config file write content fail, error:%s", + err) + } + + // 修改noAuthConfFile + noAuthConfFile, err := os.OpenFile(r.NoAuthConfFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, DefaultPerm) + defer noAuthConfFile.Close() + if err != nil { + r.runtime.Logger.Error(fmt.Sprintf("create no auth config file fail, error:%s", err)) + return fmt.Errorf("create no auth config file fail, error:%s", err) + } + if _, err = noAuthConfFile.WriteString(string(noAuthConfFileContent)); err != nil { + r.runtime.Logger.Error( + fmt.Sprintf("change configDB value of no auth config file write content fail, error:%s", + err)) + return fmt.Errorf("change configDB value of no auth config file write content fail, error:%s", + err) + } + r.runtime.Logger.Info("change configDB value of config file successfully") + + return nil +} + +// changeCacheSizeGB 修改CacheSizeGB +func (r *MongoRestart) changeCacheSizeGB() error { + if r.ConfParams.CacheSizeGB == 0 { + return nil + } + + // 检查mongo版本 + r.runtime.Logger.Info("start to check mongo version") + version, err := common.CheckMongoVersion(r.BinDir, "mongod") + if err != nil { + r.runtime.Logger.Error(fmt.Sprintf("check mongo version fail, error:%s", err)) + return fmt.Errorf("check mongo version fail, error:%s", err) + } + mainVersion, _ := strconv.Atoi(strings.Split(version, ".")[0]) + r.runtime.Logger.Info("check mongo version successfully") + + if mainVersion >= 3 { + r.runtime.Logger.Info("start to change CacheSizeGB value of config file") + // 获取配置文件内容 + readAuthConfFileContent, _ := ioutil.ReadFile(r.AuthConfFilePath) + readNoAuthConfFileContent, _ := ioutil.ReadFile(r.NoAuthConfFilePath) + + // 修改CacheSizeGB大小并写入文件 + yamlAuthConfFile := common.NewYamlMongoDBConf() + yamlNoAuthConfFile := common.NewYamlMongoDBConf() + _ = yaml.Unmarshal(readAuthConfFileContent, &yamlAuthConfFile) + _ = yaml.Unmarshal(readNoAuthConfFileContent, &yamlNoAuthConfFile) + if r.ConfParams.CacheSizeGB == 0 { + return nil + } + if r.ConfParams.CacheSizeGB != yamlAuthConfFile.Storage.WiredTiger.EngineConfig.CacheSizeGB { + yamlAuthConfFile.Storage.WiredTiger.EngineConfig.CacheSizeGB = r.ConfParams.CacheSizeGB + yamlNoAuthConfFile.Storage.WiredTiger.EngineConfig.CacheSizeGB = r.ConfParams.CacheSizeGB + authConfFileContent, _ := yamlAuthConfFile.GetConfContent() + noAuthConfFileContent, _ := yamlNoAuthConfFile.GetConfContent() + + // 修改authConfFile + authConfFile, err := os.OpenFile(r.AuthConfFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, DefaultPerm) + defer authConfFile.Close() + if err != nil { + r.runtime.Logger.Error( + fmt.Sprintf("create auth config file fail, error:%s", err)) + return fmt.Errorf("create auth config file fail, error:%s", err) + } + if _, err = authConfFile.WriteString(string(authConfFileContent)); err != nil { + r.runtime.Logger.Error( + fmt.Sprintf("change CacheSizeGB value of auth config file write content fail, error:%s", + err)) + return fmt.Errorf("change CacheSizeGB value of auth config file write content fail, error:%s", + err) + } + + // 修改noAuthConfFile + noAuthConfFile, err := os.OpenFile(r.NoAuthConfFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, DefaultPerm) + defer noAuthConfFile.Close() + if err != nil { + r.runtime.Logger.Error(fmt.Sprintf("create no auth config file fail, error:%s", err)) + return fmt.Errorf("create no auth config file fail, error:%s", err) + } + if _, err = noAuthConfFile.WriteString(string(noAuthConfFileContent)); err != nil { + r.runtime.Logger.Error( + fmt.Sprintf("change CacheSizeGB value of no auth config file write content fail, error:%s", + err)) + return fmt.Errorf("change CacheSizeGB value of no auth config file write content fail, error:%s", + err) + } + } + r.runtime.Logger.Info("change CacheSizeGB value of config file successfully") + } + return nil +} + +// checkPrimary 检查该节点是否是primary +func (r *MongoRestart) checkPrimary() (bool, error) { + r.runtime.Logger.Info("start to check if it is primary") + var info string + var err error + // 安装时无需密码验证。安装成功后需要密码验证 + if r.ConfParams.AdminUsername != "" && r.ConfParams.AdminPassword != "" { + info, err = common.AuthGetPrimaryInfo(r.Mongo, r.ConfParams.AdminUsername, + r.ConfParams.AdminPassword, r.ConfParams.IP, r.ConfParams.Port) + } else { + info, err = common.NoAuthGetPrimaryInfo(r.Mongo, + r.ConfParams.IP, r.ConfParams.Port) + } + if err != nil { + r.runtime.Logger.Error("get primary info fail, error:%s", err) + return false, fmt.Errorf("get primary info fail, error:%s", err) + } + if info == fmt.Sprintf("%s:%d", r.ConfParams.IP, r.ConfParams.Port) { + return true, nil + } + r.runtime.Logger.Info("check if it is primary successfully") + return false, nil +} + +// RsStepDown 主备切换 +func (r *MongoRestart) RsStepDown() error { + if r.ConfParams.InstanceType != "mongos" { + if r.ConfParams.SingleNodeInstallRestart == true { + return nil + } + r.runtime.Logger.Info("start to check mongod service before rsStepDown") + flag, _, err := common.CheckMongoService(r.ConfParams.Port) + if err != nil { + r.runtime.Logger.Error("check mongod service fail, error:%s", err) + return fmt.Errorf("check mongod service fail, error:%s", err) + } + r.runtime.Logger.Info("check mongod service before rsStepDown successfully") + if flag == false { + return nil + } + + // 检查是否是primary + flag1, err := r.checkPrimary() + if err != nil { + return err + } + if flag1 == true { + r.runtime.Logger.Info("start to convert primary secondary db") + // 安装时无需密码验证。安装成功后需要密码验证 + var flag2 bool + if r.ConfParams.AdminUsername != "" && r.ConfParams.AdminPassword != "" { + flag2, err = common.AuthRsStepDown(r.Mongo, r.ConfParams.IP, r.ConfParams.Port, + r.ConfParams.AdminUsername, r.ConfParams.AdminPassword) + } else { + flag2, err = common.NoAuthRsStepDown(r.Mongo, r.ConfParams.IP, r.ConfParams.Port) + } + if err != nil { + r.runtime.Logger.Error("convert primary secondary db fail, error:%s", err) + return fmt.Errorf("convert primary secondary db fail, error:%s", err) + } + if flag2 == true { + r.runtime.Logger.Info("convert primary secondary db successfully") + return nil + } + } + } + + return nil +} + +// shutdown 关闭服务 +func (r *MongoRestart) shutdown() error { + // 检查服务是否存在 + r.runtime.Logger.Info("start to check %s service", r.ConfParams.InstanceType) + result, _, err := common.CheckMongoService(r.ConfParams.Port) + if err != nil { + r.runtime.Logger.Error("check %s service fail, error:%s", r.ConfParams.InstanceType, err) + return fmt.Errorf("check %s service fail, error:%s", r.ConfParams.InstanceType, err) + } + if result != true { + r.runtime.Logger.Info("%s service has been close", r.ConfParams.InstanceType) + return nil + } + r.runtime.Logger.Info("check %s service successfully", r.ConfParams.InstanceType) + + // 关闭服务 + r.runtime.Logger.Info("start to shutdown %s", r.ConfParams.InstanceType) + if err = common.ShutdownMongoProcess(r.OsUser, r.ConfParams.InstanceType, r.BinDir, r.DbpathDir, + r.ConfParams.Port); err != nil { + r.runtime.Logger.Error(fmt.Sprintf("shutdown %s fail, error:%s", r.ConfParams.InstanceType, err)) + return fmt.Errorf("shutdown %s fail, error:%s", r.ConfParams.InstanceType, err) + } + r.runtime.Logger.Info("shutdown %s successfully", r.ConfParams.InstanceType) + return nil +} + +// startup 开启服务 +func (r *MongoRestart) startup() error { + // 检查服务是否存在 + r.runtime.Logger.Info("start to check %s service", r.ConfParams.InstanceType) + result, _, err := common.CheckMongoService(r.ConfParams.Port) + if err != nil { + r.runtime.Logger.Error("check %s service fail, error:%s", r.ConfParams.InstanceType, err) + return fmt.Errorf("check %s service fail, error:%s", r.ConfParams.InstanceType, err) + } + if result == true { + r.runtime.Logger.Info("%s service has been open", r.ConfParams.InstanceType) + return nil + } + r.runtime.Logger.Info("check %s service successfully", r.ConfParams.InstanceType) + + // 开启服务 + r.runtime.Logger.Info("start to startup %s", r.ConfParams.InstanceType) + if err = common.StartMongoProcess(r.BinDir, r.ConfParams.Port, r.OsUser, r.ConfParams.Auth); err != nil { + r.runtime.Logger.Error("startup %s fail, error:%s", r.ConfParams.InstanceType, err) + return fmt.Errorf("startup %s fail, error:%s", r.ConfParams.InstanceType, err) + } + r.runtime.Logger.Info("startup %s successfully", r.ConfParams.InstanceType) + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_set_profiler.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_set_profiler.go new file mode 100644 index 0000000000..f53466fddb --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongo_set_profiler.go @@ -0,0 +1,186 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// SetProfilerConfParams 参数 +type SetProfilerConfParams struct { + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + DbName string `json:"dbName" validate:"required"` + Level int `json:"level" validate:"required"` + ProfileSize int `json:"profileSize"` // 单位:GB + AdminUsername string `json:"adminUsername" validate:"required"` + AdminPassword string `json:"adminPassword" validate:"required"` +} + +// SetProfiler 添加分片到集群 +type SetProfiler struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + Mongo string + OsUser string + PrimaryIP string + PrimaryPort int + ConfParams *SetProfilerConfParams +} + +// NewSetProfiler 实例化结构体 +func NewSetProfiler() jobruntime.JobRunner { + return &SetProfiler{} +} + +// Name 获取原子任务的名字 +func (s *SetProfiler) Name() string { + return "mongo_set_profiler" +} + +// Run 运行原子任务 +func (s *SetProfiler) Run() error { + // 生成script内容 + if err := s.setProfileSize(); err != nil { + return err + } + + // 执行脚本生成结果文件 + if err := s.setProfileLevel(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (s *SetProfiler) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (s *SetProfiler) Rollback() error { + return nil +} + +// Init 初始化 +func (s *SetProfiler) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + s.runtime = runtime + s.runtime.Logger.Info("start to init") + s.BinDir = consts.UsrLocal + s.OsUser = consts.GetProcessUser() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(s.runtime.PayloadDecoded), &s.ConfParams); err != nil { + s.runtime.Logger.Error( + "get parameters of setProfiler fail by json.Unmarshal, error:%s", err) + return fmt.Errorf("get parameters of setProfiler fail by json.Unmarshal, error:%s", err) + } + + // 获取primary信息 + info, err := common.AuthGetPrimaryInfo(s.Mongo, s.ConfParams.AdminUsername, s.ConfParams.AdminPassword, + s.ConfParams.IP, s.ConfParams.Port) + if err != nil { + s.runtime.Logger.Error("get primary db info fail, error:%s", err) + return fmt.Errorf("get primary db info fail, error:%s", err) + } + sliceInfo := strings.Split(info, ":") + s.PrimaryIP = sliceInfo[0] + s.PrimaryPort, _ = strconv.Atoi(sliceInfo[1]) + + // 获取各种目录 + s.Mongo = filepath.Join(s.BinDir, "mongodb", "bin", "mongo") + s.runtime.Logger.Info("init successfully") + + // 进行校验 + if err = s.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (s *SetProfiler) checkParams() error { + // 校验配置参数 + s.runtime.Logger.Info("start to validate parameters") + validate := validator.New() + s.runtime.Logger.Info("start to validate parameters of deInstall") + if err := validate.Struct(s.ConfParams); err != nil { + s.runtime.Logger.Error("validate parameters of setProfiler fail, error:%s", err) + return fmt.Errorf("validate parameters of setProfiler fail, error:%s", err) + } + s.runtime.Logger.Info("validate parameters successfully") + return nil +} + +// setProfileSize 设置profile大小 +func (s *SetProfiler) setProfileSize() error { + // 获取profile级别 + status, err := common.GetProfilingLevel(s.Mongo, s.ConfParams.IP, s.ConfParams.Port, + s.ConfParams.AdminUsername, s.ConfParams.AdminPassword, s.ConfParams.DbName) + if err != nil { + s.runtime.Logger.Error("get profile level fail, error:%s", err) + return fmt.Errorf("get profile level fail, error:%s", err) + } + if status != 0 { + if err = common.SetProfilingLevel(s.Mongo, s.ConfParams.IP, s.ConfParams.Port, s.ConfParams.AdminUsername, + s.ConfParams.AdminPassword, s.ConfParams.DbName, 0); err != nil { + s.runtime.Logger.Error("set profile level 0 fail, error:%s", err) + return fmt.Errorf("set profile level 0 fail, error:%s", err) + } + } + + // 删除profile.system + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"db.getMongo().getDB('%s').system.profile.drop()\"", + s.Mongo, s.ConfParams.AdminUsername, s.ConfParams.AdminPassword, s.ConfParams.IP, s.ConfParams.Port, + s.ConfParams.DbName) + if _, err = util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + s.runtime.Logger.Error("delete system.profile fail, error:%s", err) + return fmt.Errorf("set system.profile fail, error:%s", err) + } + + // 设置profile.system + s.runtime.Logger.Info("start to set system.profile size") + profileSizeBytes := s.ConfParams.ProfileSize * 1024 * 1024 * 1024 + cmd = fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"db.getMongo().getDB('%s').createCollection('system.profile',{ capped: true, size:%d })\"", + s.Mongo, s.ConfParams.AdminUsername, s.ConfParams.AdminPassword, s.ConfParams.IP, s.ConfParams.Port, + s.ConfParams.DbName, profileSizeBytes) + if _, err = util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + s.runtime.Logger.Error("set system.profile size fail, error:%s", err) + return fmt.Errorf("set system.profile size fail, error:%s", err) + } + s.runtime.Logger.Info("set system.profile size successfully") + return nil +} + +// setProfileLevel 生成脚本内容 +func (s *SetProfiler) setProfileLevel() error { + s.runtime.Logger.Info("start to set profile level") + if err := common.SetProfilingLevel(s.Mongo, s.ConfParams.IP, s.ConfParams.Port, s.ConfParams.AdminUsername, + s.ConfParams.AdminPassword, s.ConfParams.DbName, s.ConfParams.Level); err != nil { + s.runtime.Logger.Error("set profile level fail, error:%s", err) + return fmt.Errorf("set profile level fail, error:%s", err) + } + s.runtime.Logger.Info("set profile level successfully") + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_change_oplogsize.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_change_oplogsize.go new file mode 100644 index 0000000000..7948ac8191 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_change_oplogsize.go @@ -0,0 +1,501 @@ +package atommongodb + +import ( + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/go-playground/validator/v10" +) + +// MongoDChangeOplogSizeConfParams 参数 // 修改oplog大小 +type MongoDChangeOplogSizeConfParams struct { + IP string `json:"ip" validate:"required"` // 执行节点 + Port int `json:"port" validate:"required"` + AdminUsername string `json:"adminUsername" validate:"required"` + AdminPassword string `json:"adminPassword" validate:"required"` + NewOplogSize int `json:"newOplogSize"` // 单位:GB +} + +// MongoDChangeOplogSize 修改oplog大小 +type MongoDChangeOplogSize struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + Mongo string + MongoD string + OsUser string + OsGroup string + DataDir string + DbpathDir string + ConfParams *MongoDChangeOplogSizeConfParams + PrimaryIP string + PrimaryPort int + MainVersion int + Version float64 + AuthConfFilePath string + NoAuthConfFilePath string + OplogSizeMB int + NewOplogSizeMB int + ScriptDir string + ScriptFilePath string + NewPort int +} + +// NewMongoDChangeOplogSize 实例化结构体 +func NewMongoDChangeOplogSize() jobruntime.JobRunner { + return &MongoDChangeOplogSize{} +} + +// Name 获取原子任务的名字 +func (c *MongoDChangeOplogSize) Name() string { + return "mongod_change_oplogsize" +} + +// Run 运行原子任务 +func (c *MongoDChangeOplogSize) Run() error { + // 检查现有oplog大小 + if err := c.checkOplogSizeAndFreeSpace(); err != nil { + return err + } + + // 修改配置文件 + if err := c.changeConfigFile(); err != nil { + return err + } + + // 修改oplog大小 + if err := c.changeOplogSize(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (c *MongoDChangeOplogSize) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (c *MongoDChangeOplogSize) Rollback() error { + return nil +} + +// Init 初始化 +func (c *MongoDChangeOplogSize) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + c.runtime = runtime + c.runtime.Logger.Info("start to init") + c.BinDir = consts.UsrLocal + c.Mongo = filepath.Join(c.BinDir, "mongodb", "bin", "mongo") + c.MongoD = filepath.Join(c.BinDir, "mongodb", "bin", "mongod") + c.OsUser = consts.GetProcessUser() + c.OsGroup = consts.GetProcessUserGroup() + c.DataDir = consts.GetMongoDataDir() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(c.runtime.PayloadDecoded), &c.ConfParams); err != nil { + c.runtime.Logger.Error(fmt.Sprintf( + "get parameters of mongoDChangeOplogSize fail by json.Unmarshal, error:%s", err)) + return fmt.Errorf("get parameters of mongoDChangeOplogSize fail by json.Unmarshal, error:%s", err) + } + // 获取路径 + strPort := strconv.Itoa(c.ConfParams.Port) + c.AuthConfFilePath = filepath.Join(c.DataDir, "mongodata", strPort, "mongo.conf") + c.NoAuthConfFilePath = filepath.Join(c.DataDir, "mongodata", strPort, "noauth.conf") + c.DbpathDir = filepath.Join(c.DataDir, "mongodata", strPort, "db") + c.ScriptDir = filepath.Join("/", "home", c.OsUser, c.runtime.UID) + c.ScriptFilePath = filepath.Join(c.ScriptDir, strings.Join([]string{"script", "js"}, ".")) + c.NewOplogSizeMB = c.ConfParams.NewOplogSize * 1024 + c.NewPort = c.ConfParams.Port + 1000 + + // 获取primary信息 + info, err := common.AuthGetPrimaryInfo(c.Mongo, c.ConfParams.AdminUsername, c.ConfParams.AdminPassword, + c.ConfParams.IP, c.ConfParams.Port) + if err != nil { + c.runtime.Logger.Error(fmt.Sprintf( + "get primary db info of mongoDChangeOplogSize fail, error:%s", err)) + return fmt.Errorf("get primary db info of mongoDChangeOplogSize fail, error:%s", err) + } + // 判断info是否为null + if info == "" { + c.runtime.Logger.Error(fmt.Sprintf( + "get primary db info of mongoDChangeOplogSize fail, error:%s", err)) + return fmt.Errorf("get primary db info of mongoDChangeOplogSize fail, error:%s", err) + } + getInfo := strings.Split(info, ":") + c.PrimaryIP = getInfo[0] + c.PrimaryPort, _ = strconv.Atoi(getInfo[1]) + + // 获取mongo版本 + version, err := common.CheckMongoVersion(c.BinDir, "mongod") + if err != nil { + c.runtime.Logger.Error(fmt.Sprintf("check mongo version fail, error:%s", err)) + return fmt.Errorf("check mongo version fail, error:%s", err) + } + c.MainVersion, _ = strconv.Atoi(strings.Split(version, ".")[0]) + c.Version, _ = strconv.ParseFloat(version[0:3], 64) + + c.runtime.Logger.Info("init successfully") + + // 进行校验 + if err = c.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (c *MongoDChangeOplogSize) checkParams() error { + // 校验重启配置参数 + validate := validator.New() + c.runtime.Logger.Info("start to validate parameters of mongoDChangeOplogSize") + if err := validate.Struct(c.ConfParams); err != nil { + c.runtime.Logger.Error("validate parameters of mongoDChangeOplogSize fail, error:%s", err) + return fmt.Errorf("validate parameters of mongoDChangeOplogSize fail, error:%s", err) + } + c.runtime.Logger.Info("validate parameters of mongoDChangeOplogSize successfully") + return nil +} + +// checkOplogSizeAndFreeSpace 检查现有oplog大小及挂载点的剩余空间 +func (c *MongoDChangeOplogSize) checkOplogSizeAndFreeSpace() error { + // 检查现有oplog大小 + c.runtime.Logger.Info("start to check current oplogSize") + cmd := fmt.Sprintf( + "%s --host %s --port %d --authenticationDatabase=admin --quiet --eval \"%s\" %s", + c.Mongo, c.ConfParams.IP, c.ConfParams.Port, + "db.getSiblingDB('local').oplog.rs.stats(1024*1024*1024).maxSize", "admin") + oplogSize, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + c.runtime.Logger.Error("get current oplogSize fail, error:%s", err) + return fmt.Errorf("get current oplogSize fail, error:%s", err) + } + oplogSizeInt, _ := strconv.Atoi(strings.Replace(oplogSize, "\n", "", -1)) + c.OplogSizeMB = oplogSizeInt * 1024 + if oplogSizeInt == c.ConfParams.NewOplogSize { + c.runtime.Logger.Error("newOplogSize:%dGB is same as current oplogSize:%dGB", c.ConfParams.NewOplogSize, oplogSizeInt) + return fmt.Errorf("newOplogSize:%dGB is same as current oplogSize:%dGB", c.ConfParams.NewOplogSize, oplogSizeInt) + } else if oplogSizeInt > c.ConfParams.NewOplogSize { + c.runtime.Logger.Error("newOplogSize:%dGB is less than current oplogSize:%dGB", c.ConfParams.NewOplogSize, + oplogSizeInt) + return fmt.Errorf("newOplogSize:%dGB is less than current oplogSize:%dGB", c.ConfParams.NewOplogSize, oplogSizeInt) + } + c.runtime.Logger.Info("check current oplogSize successfully") + + // + c.runtime.Logger.Info("start to check free space about mountPoint") + cmd = fmt.Sprintf("df -m |grep %s | awk '{print $4}'", c.OsUser) + result, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + c.runtime.Logger.Error("check free space about mountPoint, error:%s", err) + return fmt.Errorf("check free space about mountPoint, error:%s", err) + } + result = strings.Replace(result, "\n", "", -1) + resultInt, _ := strconv.Atoi(result) + if resultInt < c.NewOplogSizeMB-c.OplogSizeMB+5120 { + c.runtime.Logger.Error("free space is not enough about mountPoint") + return fmt.Errorf("free space is not enough about mountPoint") + } + c.runtime.Logger.Info("check free space about mountPoint successfully") + return nil +} + +// changeConfigFile 修改配置文件 +func (c *MongoDChangeOplogSize) changeConfigFile() error { + c.runtime.Logger.Info("start to change config file content") + var authCmd string + var noAuthCmd string + var checkAuthCmd string + var checkNoAuthcmd string + // 修改oplog大小参数及 + if c.MainVersion >= 3 { + authCmd = fmt.Sprintf("sed -i \"s/oplogSizeMB: %d/oplogSizeMB: %d/g\" %s", c.OplogSizeMB, c.NewOplogSizeMB, + c.AuthConfFilePath) + noAuthCmd = fmt.Sprintf("sed -i \"s/oplogSizeMB: %d/oplogSizeMB: %d/g\" %s", c.OplogSizeMB, c.NewOplogSizeMB, + c.NoAuthConfFilePath) + } else { + authCmd = fmt.Sprintf("sed -i \"soplogSize=%d/oplogSize=%d/g\" %s", c.OplogSizeMB, c.NewOplogSizeMB, + c.AuthConfFilePath) + noAuthCmd = fmt.Sprintf("sed -i \"s/oplogSize=%d/oplogSize=%d/g\" %s", c.OplogSizeMB, c.NewOplogSizeMB, + c.NoAuthConfFilePath) + } + if _, err := util.RunBashCmd( + authCmd, + "", nil, + 10*time.Second); err != nil { + c.runtime.Logger.Error("change auth config file fail, error:%s", err) + return fmt.Errorf("change auth config file fail, error:%s", err) + } + if _, err := util.RunBashCmd( + noAuthCmd, + "", nil, + 10*time.Second); err != nil { + c.runtime.Logger.Error("change noAuth config file fail, error:%s", err) + return fmt.Errorf("change noAuth config file fail, error:%s", err) + } + // 检查oplog大小参数 + if c.MainVersion >= 3 { + checkAuthCmd = fmt.Sprintf("cat %s |grep oplogSizeMB", c.AuthConfFilePath) + checkNoAuthcmd = fmt.Sprintf("cat %s |grep oplogSizeMB", c.NoAuthConfFilePath) + } else { + checkAuthCmd = fmt.Sprintf("cat %s |grep oplogSize", c.AuthConfFilePath) + checkNoAuthcmd = fmt.Sprintf("cat %s |grep oplogSize", c.NoAuthConfFilePath) + } + result, err := util.RunBashCmd( + checkAuthCmd, + "", nil, + 10*time.Second) + if err != nil { + c.runtime.Logger.Error("check oplogSize parameter of auth config file fail, error:%s", err) + return fmt.Errorf("check oplogSize parameter of auth config file fail, error:%s", err) + } + if strings.Contains(result, strconv.Itoa(c.NewOplogSizeMB)) { + c.runtime.Logger.Info("change oplogSize parameter of auth config file successfully") + } + result1, err := util.RunBashCmd( + checkNoAuthcmd, + "", nil, + 10*time.Second) + if err != nil { + c.runtime.Logger.Error("check oplogSize parameter of noAuth config file fail, error:%s", err) + return fmt.Errorf("check oplogSize parameter of noAuth config file fail, error:%s", err) + } + if strings.Contains(result1, strconv.Itoa(c.NewOplogSizeMB)) { + c.runtime.Logger.Info("change oplogSize parameter of noAuth config file successfully") + } + c.runtime.Logger.Info("change config file content successfully") + return nil +} + +// createScript db版本小于3.6 创建修改脚本 +func (c *MongoDChangeOplogSize) createScript() error { + c.runtime.Logger.Info("create change oplogSize script") + scriptContent := `db = db.getSiblingDB('local'); +db.temp.drop(); +db.temp.save( db.oplog.rs.find( { }, { ts: 1, h: 1 } ).sort( {$natural : -1} ).limit(1).next() ); +db.oplog.rs.drop(); +db.runCommand( { create: "oplog.rs", capped: true, size: ({{oplogSizeGB}} * 1024 * 1024 * 1024) } ); +db.oplog.rs.save( db.temp.findOne() ); +db.temp.drop();` + scriptContent = strings.Replace(scriptContent, "{{oplogSizeGB}}", strconv.Itoa(c.ConfParams.NewOplogSize), -1) + // 创建目录 + if err := util.MkDirsIfNotExists([]string{c.ScriptDir}); err != nil { + c.runtime.Logger.Error("create script directory:%s fail, error:%s", c.ScriptDir, err) + return fmt.Errorf("create script directory:%s fail, error:%s", c.ScriptDir, err) + } + // 创建文件 + c.runtime.Logger.Info("start to create script file") + script, err := os.OpenFile(c.ScriptFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, DefaultPerm) + defer script.Close() + if err != nil { + c.runtime.Logger.Error( + fmt.Sprintf("create script file fail, error:%s", err)) + return fmt.Errorf("create script file fail, error:%s", err) + } + if _, err = script.WriteString(scriptContent); err != nil { + c.runtime.Logger.Error( + fmt.Sprintf("script file write content fail, error:%s", + err)) + return fmt.Errorf("script file write content fail, error:%s", + err) + } + // 修改配置文件属主 + if _, err = util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", c.OsUser, c.OsGroup, c.ScriptDir), + "", nil, + 10*time.Second); err != nil { + c.runtime.Logger.Error("chown script file fail, error:%s", err) + return fmt.Errorf("chown script file fail, error:%s", err) + } + return nil +} + +// standaloneStart 单机形式启动 +func (c *MongoDChangeOplogSize) standaloneStart() error { + if err := common.ShutdownMongoProcess(c.OsUser, "mongod", c.BinDir, c.DbpathDir, + c.ConfParams.Port); err != nil { + c.runtime.Logger.Error("shutdown mongod fail, error:%s", err) + return fmt.Errorf("shutdown mongod fail, error:%s", err) + } + // 检查NewPort是否使用 + for { + flag, _ := util.CheckPortIsInUse(c.ConfParams.IP, strconv.Itoa(c.NewPort)) + if flag { + c.NewPort += 1 + } + if !flag { + break + } + } + // 以单机形式启动 + cmd := fmt.Sprintf("%s --port %d --dbpath %s --fork", c.MongoD, c.NewPort, c.DbpathDir) + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + c.runtime.Logger.Error("startup mongod by port:%d fail, error:%s", c.NewPort, err) + return fmt.Errorf("startup mongod by port:%d fail, error:%s", c.NewPort, err) + } + // 检查是否启动成功 + flag, _, err := common.CheckMongoService(c.NewPort) + if err != nil || flag == false { + c.runtime.Logger.Error("check mongod fail by port:%d fail, error:%s", c.NewPort, err) + return fmt.Errorf("check mongod fail by port:%d fail, error:%s", c.NewPort, err) + } + return nil +} + +// normalStart 正常启动 +func (c *MongoDChangeOplogSize) normalStart() error { + if err := common.ShutdownMongoProcess(c.OsUser, "mongod", c.BinDir, c.DbpathDir, + c.NewPort); err != nil { + c.runtime.Logger.Error("shutdown mongod about port:%d fail, error:%s", c.NewPort, err) + return fmt.Errorf("shutdown mongod about port:%d fail, error:%s", c.NewPort, err) + } + if err := common.StartMongoProcess(c.BinDir, c.ConfParams.Port, c.OsUser, true); err != nil { + c.runtime.Logger.Error("startup mongod about port:%d fail, error:%s", c.ConfParams.Port, err) + return fmt.Errorf("startup mongod about port:%d fail, error:%s", c.ConfParams.Port, err) + } + // 检查是否启动成功 + flag, _, err := common.CheckMongoService(c.NewPort) + if err != nil || flag == false { + c.runtime.Logger.Error("check mongod fail about port:%d fail, error:%s", c.ConfParams.Port, err) + return fmt.Errorf("check mongod fail about port:%d fail, error:%s", c.ConfParams.Port, err) + } + return nil +} + +// setOplog db版本小于3.6修改oplog +func (c *MongoDChangeOplogSize) setOplog() error { + // 关闭dbmon + c.runtime.Logger.Info("stop dbmon") + cmd := fmt.Sprintf("/home/%s/dbmon/stop.sh", c.OsUser) + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + c.runtime.Logger.Error("stop dbmon fail, error:%s", err) + return fmt.Errorf("stop dbmon fail, error:%s", err) + } + + // 如果是主库进行主备切换 + if c.ConfParams.IP == c.PrimaryIP && c.ConfParams.Port == c.PrimaryPort { + c.runtime.Logger.Info("change primary to secondary") + flag, err := common.AuthRsStepDown(c.Mongo, c.ConfParams.IP, c.ConfParams.Port, c.ConfParams.AdminUsername, + c.ConfParams.AdminPassword) + if err != nil { + c.runtime.Logger.Error("change primary to secondary fail, error:%s", err) + return fmt.Errorf("change primary to secondary fail, error:%s", err) + } + if !flag { + c.runtime.Logger.Error("change primary to secondary fail") + return fmt.Errorf("change primary to secondary fail") + } + } + + // 创建修改oplog脚本 + if err := c.createScript(); err != nil { + return err + } + + // 关闭进程以单机形式启动 + if err := c.standaloneStart(); err != nil { + return err + } + + // 执行修改oplog脚本脚本 + cmd = fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet %s", + c.Mongo, c.ConfParams.AdminUsername, c.ConfParams.AdminPassword, c.ConfParams.IP, c.ConfParams.Port, + c.ScriptFilePath) + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + c.runtime.Logger.Error("execute script fail, error:%s", err) + return fmt.Errorf("execute script fail, error:%s", err) + } + // 检查新建oplog文档数量,需要等于1 + cmd = fmt.Sprintf( + "%s --host %s --port %d --authenticationDatabase=admin --quiet --eval \"%s\" %s", + c.Mongo, c.ConfParams.IP, c.ConfParams.Port, "db.getSiblingDB('local').oplog.rs.count()", "admin") + result, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + c.runtime.Logger.Error("check the number of new oplog document fail, error:%s", err) + return fmt.Errorf("check the number of new oplog document fail, error:%s", err) + } + result = strings.Replace(result, "\n", "", -1) + resultInt, _ := strconv.Atoi(result) + if resultInt != 1 { + c.runtime.Logger.Error("number of new oplog document is not equal 1, please check") + return fmt.Errorf("number of new oplog document is not equal 1, please check") + } + + // 关闭进程正常重启进程 + if err = c.normalStart(); err != nil { + return err + } + + // 开启dbmon + c.runtime.Logger.Info("start dbmon") + cmd = fmt.Sprintf("/home/%s/dbmon/start.sh", c.OsUser) + if _, err = util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + c.runtime.Logger.Error("start dbmon fail, error:%s", err) + return fmt.Errorf("start dbmon fail, error:%s", err) + } + return nil +} + +// setOplog3 db版本大于等于3.6修改oplog +func (c *MongoDChangeOplogSize) setOplog3() error { + script := fmt.Sprintf("db.adminCommand({replSetResizeOplog: 1, size: %d})", c.NewOplogSizeMB) + cmd := fmt.Sprintf( + "%s --host %s --port %d --authenticationDatabase=admin --quiet --eval \"%s\" %s", + c.Mongo, c.ConfParams.IP, c.ConfParams.Port, script, "admin") + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + c.runtime.Logger.Error("change oplogSize fail, error:%s", err) + return fmt.Errorf("change oplogSize fail, error:%s", err) + } + return nil +} + +// changeOplogSize 修改oplog大小 +func (c *MongoDChangeOplogSize) changeOplogSize() error { + c.runtime.Logger.Info("start to change oplog size") + if c.Version >= 3.6 { + if err := c.setOplog3(); err != nil { + return err + } + } else { + if err := c.setOplog(); err != nil { + return err + } + } + c.runtime.Logger.Info("change oplog size successfully") + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_install.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_install.go new file mode 100644 index 0000000000..1a444924a7 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_install.go @@ -0,0 +1,492 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +/* + +1.预检查 +检查输入的参数 检查端口是否合规 检查安装包 检查端口是否被使用(如果使用,则检查是否是mongodb服务) + +2.解压安装包 判断是否已经解压过,版本是否正确 +解压文件 做软链接 修改文件属主 + +3.安装 判断目录是否已被创建 +创建相关各级目录-判断目录是否已经创建过 修改目录属主 创建配置文件(noauth, auth) 创建dbtype文件 复制集创建key文件 + +4.启动服务 +以noauth启动服务 + +*/ + +// MongoDBPortMin MongoDB最小端口 +const MongoDBPortMin = 27000 + +// MongoDBPortMax MongoDB最大端口 +const MongoDBPortMax = 28999 + +// DefaultPerm 创建目录、文件的默认权限 +const DefaultPerm = 0755 + +// MongoDBConfParams 配置文件参数 +type MongoDBConfParams struct { + common.MediaPkg `json:"mediapkg"` + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + DbVersion string `json:"dbVersion" validate:"required"` + InstanceType string `json:"instanceType" validate:"required"` // mongos mongod + App string `json:"app" validate:"required"` + SetId string `json:"setId" validate:"required"` // 复制集为集群id,cluster为集群id+序号 + KeyFile string `json:"keyFile" validate:"required"` // keyFile的内容 app-setId + Auth bool `json:"auth"` // true:以验证方式启动mongod false:以非验证方式启动mongod + ClusterRole string `json:"clusterRole"` // 部署cluster时填写,shardsvr configsvr;部署复制集时为空 + DbConfig struct { + SlowOpThresholdMs int `json:"slowOpThresholdMs"` + CacheSizeGB int `json:"cacheSizeGB"` + OplogSizeMB int `json:"oplogSizeMB" validate:"required"` + Destination string `json:"destination"` + } `json:"dbConfig" validate:"required"` +} + +// MongoDBInstall MongoDB安装 +type MongoDBInstall struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + BackupDir string + DataDir string + OsUser string // MongoDB安装在哪个用户下 + OsGroup string + ConfParams *MongoDBConfParams + DbpathDir string + BackupPath string + AuthConfFilePath string + AuthConfFileContent []byte + NoAuthConfFilePath string + NoAuthConfFileContent []byte + DbTypeFilePath string + LogPath string + PidFilePath string + KeyFilePath string + InstallPackagePath string + LockFilePath string // 锁文件路径 +} + +// NewMongoDBInstall 实例化结构体 +func NewMongoDBInstall() jobruntime.JobRunner { + return &MongoDBInstall{} +} + +// Name 获取原子任务的名字 +func (m *MongoDBInstall) Name() string { + return "mongod_install" +} + +// Run 运行原子任务 +func (m *MongoDBInstall) Run() error { + // 进行校验 + status, err := m.checkParams() + if err != nil { + return err + } + if status { + return nil + } + + // 解压安装包并修改属主 + if err = m.unTarAndCreateSoftLink(); err != nil { + return err + } + + // 创建目录并修改属主 + if err = m.mkdir(); err != nil { + return err + } + + // 创建配置文件,key文件并修改属主 + if err = m.createFile(); err != nil { + return err + } + + // 启动服务 + if err = m.startup(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (m *MongoDBInstall) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (m *MongoDBInstall) Rollback() error { + return nil +} + +// Init 初始化 +func (m *MongoDBInstall) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + m.runtime = runtime + m.runtime.Logger.Info("start to init") + m.BinDir = consts.UsrLocal + m.BackupDir = consts.GetMongoBackupDir() + m.DataDir = consts.GetMongoDataDir() + m.OsUser = consts.GetProcessUser() + m.OsGroup = consts.GetProcessUserGroup() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(m.runtime.PayloadDecoded), &m.ConfParams); err != nil { + m.runtime.Logger.Error(fmt.Sprintf( + "get parameters of mongodb config file fail by json.Unmarshal, error:%s", err)) + return fmt.Errorf("get parameters of mongodb config file fail by json.Unmarshal, error:%s", err) + } + + // 获取信息 + m.InstallPackagePath = m.ConfParams.MediaPkg.GetAbsolutePath() + + // 设置各种路径 + strPort := strconv.Itoa(m.ConfParams.Port) + m.DbpathDir = filepath.Join(m.DataDir, "mongodata", strPort, "db") + m.BackupPath = filepath.Join(m.BackupDir, "dbbak") + m.AuthConfFilePath = filepath.Join(m.DataDir, "mongodata", strPort, "mongo.conf") + m.NoAuthConfFilePath = filepath.Join(m.DataDir, "mongodata", strPort, "noauth.conf") + m.LogPath = filepath.Join(m.BackupDir, "mongolog", strPort, "mongo.log") + PidFileName := fmt.Sprintf("pid.%s", strPort) + m.PidFilePath = filepath.Join(m.DataDir, "mongodata", strPort, PidFileName) + m.KeyFilePath = filepath.Join(m.DataDir, "mongodata", strPort, "key_of_mongo") + m.DbTypeFilePath = filepath.Join(m.DataDir, "mongodata", strPort, "dbtype") + m.LockFilePath = filepath.Join(m.DataDir, "mongoinstall.lock") + + m.runtime.Logger.Info("init successfully") + + // 生成配置文件内容 + if err := m.makeConfContent(); err != nil { + return err + } + + return nil +} + +// makeConfContent 生成配置文件内容 +func (m *MongoDBInstall) makeConfContent() error { + mainVersion, err := strconv.Atoi(strings.Split(m.ConfParams.DbVersion, ".")[0]) + if err != nil { + return err + } + + // mongodb 3.0及以上得到配置文件内容 + if mainVersion >= 3 { + m.runtime.Logger.Info("start to make mongodb config file content") + conf := common.NewYamlMongoDBConf() + conf.Storage.DbPath = m.DbpathDir + conf.Storage.Engine = "wiredTiger" + conf.Storage.WiredTiger.EngineConfig.CacheSizeGB = m.ConfParams.DbConfig.CacheSizeGB + conf.Replication.OplogSizeMB = m.ConfParams.DbConfig.OplogSizeMB + conf.Replication.ReplSetName = strings.Join([]string{m.ConfParams.App, m.ConfParams.SetId}, + "-") + conf.SystemLog.LogAppend = true + conf.SystemLog.Path = m.LogPath + conf.SystemLog.Destination = m.ConfParams.DbConfig.Destination + conf.ProcessManagement.Fork = true + conf.ProcessManagement.PidFilePath = m.PidFilePath + conf.Net.Port = m.ConfParams.Port + conf.Net.BindIp = strings.Join([]string{"127.0.0.1", m.ConfParams.IP}, ",") + conf.Net.WireObjectCheck = false + conf.OperationProfiling.SlowOpThresholdMs = m.ConfParams.DbConfig.SlowOpThresholdMs + conf.Sharding.ClusterRole = m.ConfParams.ClusterRole + // 获取非验证配置文件内容 + m.NoAuthConfFileContent, err = conf.GetConfContent() + if err != nil { + m.runtime.Logger.Error(fmt.Sprintf( + "version:%s make mongodb no auth config file content fail, error:%s", m.ConfParams.DbVersion, err)) + return fmt.Errorf("version:%s make mongodb no auth config file content fail, error:%s", + m.ConfParams.DbVersion, err) + } + conf.Security.KeyFile = m.KeyFilePath + // 获取验证配置文件内容 + m.AuthConfFileContent, err = conf.GetConfContent() + if err != nil { + m.runtime.Logger.Error(fmt.Sprintf( + "version:%s make mongodb auth config file content fail, error:%s", + m.ConfParams.DbVersion, err)) + return fmt.Errorf("version:%s make mongodb auth config file content fail, error:%s", + m.ConfParams.DbVersion, err) + } + m.runtime.Logger.Info("make mongodb config file content successfully") + return nil + } + + // mongodb 3.0以下获取配置文件内容 + // 获取非验证配置文件内容 + m.runtime.Logger.Info("start to make mongodb config file content") + NoAuthConf := common.IniNoAuthMongoDBConf + AuthConf := common.IniAuthMongoDBConf + replSet := strings.Join([]string{m.ConfParams.App, m.ConfParams.SetId}, + "-") + NoAuthConf = strings.Replace(NoAuthConf, "{{replSet}}", replSet, -1) + AuthConf = strings.Replace(AuthConf, "{{replSet}}", replSet, -1) + NoAuthConf = strings.Replace(NoAuthConf, "{{dbpath}}", m.DbpathDir, -1) + AuthConf = strings.Replace(AuthConf, "{{dbpath}}", m.DbpathDir, -1) + NoAuthConf = strings.Replace(NoAuthConf, "{{logpath}}", m.LogPath, -1) + AuthConf = strings.Replace(AuthConf, "{{logpath}}", m.LogPath, -1) + NoAuthConf = strings.Replace(NoAuthConf, "{{pidfilepath}}", m.PidFilePath, -1) + AuthConf = strings.Replace(AuthConf, "{{pidfilepath}}", m.PidFilePath, -1) + strPort := strconv.Itoa(m.ConfParams.Port) + NoAuthConf = strings.Replace(NoAuthConf, "{{port}}", strPort, -1) + AuthConf = strings.Replace(AuthConf, "{{port}}", strPort, -1) + bindIP := strings.Join([]string{"127.0.0.1", m.ConfParams.IP}, ",") + NoAuthConf = strings.Replace(NoAuthConf, "{{bind_ip}}", bindIP, -1) + AuthConf = strings.Replace(AuthConf, "{{bind_ip}}", bindIP, -1) + strOplogSize := strconv.Itoa(m.ConfParams.DbConfig.OplogSizeMB) + NoAuthConf = strings.Replace(NoAuthConf, "{{oplogSize}}", strOplogSize, -1) + AuthConf = strings.Replace(AuthConf, "{{oplogSize}}", strOplogSize, -1) + NoAuthConf = strings.Replace(NoAuthConf, "{{instanceRole}}", m.ConfParams.ClusterRole, -1) + AuthConf = strings.Replace(AuthConf, "{{instanceRole}}", m.ConfParams.ClusterRole, -1) + AuthConf = strings.Replace(AuthConf, "{{keyFile}}", m.KeyFilePath, -1) + m.NoAuthConfFileContent = []byte(NoAuthConf) + m.AuthConfFileContent = []byte(AuthConf) + m.runtime.Logger.Info("make mongodb config file content successfully") + + return nil +} + +// checkParams 校验参数 检查输入的参数 检查端口是否合规 检查安装包 检查端口是否被使用(如果使用,则检查是否是mongodb服务) +func (m *MongoDBInstall) checkParams() (bool, error) { + // 校验MongoDB配置文件 + m.runtime.Logger.Info("start to validate parameters") + validate := validator.New() + m.runtime.Logger.Info("start to validate parameters of mongodb config file") + if err := validate.Struct(m.ConfParams); err != nil { + m.runtime.Logger.Error(fmt.Sprintf("validate parameters of mongodb config file fail, error:%s", err)) + return false, fmt.Errorf("validate parameters of mongodb config file fail, error:%s", err) + } + // 校验port是否合规 + m.runtime.Logger.Info("start to validate port if it is correct") + if m.ConfParams.Port < MongoDBPortMin || m.ConfParams.Port > MongoDBPortMax { + m.runtime.Logger.Error(fmt.Sprintf( + "validate port if it is correct, port is not within defalut range [%d,%d]", + MongoDBPortMin, MongoDBPortMax)) + return false, fmt.Errorf("validate port if it is correct, port is not within defalut range [%d,%d]", + MongoDBPortMin, MongoDBPortMax) + } + + // 校验安装包是否存在,md5值是否一致 + m.runtime.Logger.Info("start to validate install package") + if flag := util.FileExists(m.InstallPackagePath); !flag { + m.runtime.Logger.Error(fmt.Sprintf("validate install package, %s is not existed", + m.InstallPackagePath)) + return false, fmt.Errorf("validate install file, %s is not existed", + m.InstallPackagePath) + } + md5, _ := util.GetFileMd5(m.InstallPackagePath) + if m.ConfParams.MediaPkg.PkgMd5 != md5 { + m.runtime.Logger.Error(fmt.Sprintf("validate install package md5 fail, md5 is incorrect")) + return false, fmt.Errorf("validate install package md5 fail, md5 is incorrect") + } + + // 校验端口是否使用 + m.runtime.Logger.Info("start to validate port if it has been used") + flag, _ := util.CheckPortIsInUse(m.ConfParams.IP, strconv.Itoa(m.ConfParams.Port)) + if flag { + // 校验端口是否是mongod进程 + cmd := fmt.Sprintf("netstat -ntpl |grep %d | awk '{print $7}' |head -1", m.ConfParams.Port) + result, _ := util.RunBashCmd(cmd, "", nil, 10*time.Second) + if strings.Contains(result, "mongod") { + // 检查配置文件是否一致,读取已有配置文件与新生成的配置文件内容对比 + content, _ := ioutil.ReadFile(m.AuthConfFilePath) + if strings.Compare(string(content), string(m.AuthConfFileContent)) == 0 { + // 检查mongod版本 + version, err := common.CheckMongoVersion(m.BinDir, "mongod") + if err != nil { + m.runtime.Logger.Error( + fmt.Sprintf("mongod has been installed, port:%d, check mongod version fail. error:%s", + m.ConfParams.Port, version)) + return false, fmt.Errorf("mongod has been installed, port:%d, check mongod version fail. error:%s", + m.ConfParams.Port, version) + } + if version == m.ConfParams.DbVersion { + m.runtime.Logger.Info(fmt.Sprintf("mongod has been installed, port:%d, version:%s", + m.ConfParams.Port, version)) + return true, nil + } + m.runtime.Logger.Error(fmt.Sprintf("other mongod has been installed, port:%d, version:%s", + m.ConfParams.Port, version)) + return false, fmt.Errorf("other mongod has been installed, port:%d, version:%s", + m.ConfParams.Port, version) + } + + } + m.runtime.Logger.Error( + fmt.Sprintf("validate port if it has been used, port:%d is used by other process", + m.ConfParams.Port)) + return false, fmt.Errorf("validate port if it has been used, port:%d is used by other process", + m.ConfParams.Port) + } + m.runtime.Logger.Info("validate parameters successfully") + return false, nil +} + +// unTarAndCreateSoftLink 解压安装包,创建软链接并给目录授权 +func (m *MongoDBInstall) unTarAndCreateSoftLink() error { + // 解压目录 + unTarPath := filepath.Join(m.BinDir, m.ConfParams.MediaPkg.GePkgBaseName()) + + // soft link目录 + installPath := filepath.Join(m.BinDir, "mongodb") + + // 解压安装包并授权 + // 安装多实例并发执行添加文件锁 + m.runtime.Logger.Info("start to get install file lock") + fileLock := common.NewFileLock(m.LockFilePath) + // 获取锁 + err := fileLock.Lock() + if err != nil { + for { + err = fileLock.Lock() + if err != nil { + time.Sleep(1 * time.Second) + continue + } + m.runtime.Logger.Info("get install file lock successfully") + break + } + } else { + m.runtime.Logger.Info("get install file lock successfully") + } + + if err = common.UnTarAndCreateSoftLinkAndChown(m.runtime, m.BinDir, + m.InstallPackagePath, unTarPath, installPath, m.OsUser, m.OsGroup); err != nil { + return err + } + // 释放锁 + _ = fileLock.UnLock() + m.runtime.Logger.Info("release install file lock successfully") + + // 检查mongod版本 + m.runtime.Logger.Info("start to check mongod version") + version, err := common.CheckMongoVersion(m.BinDir, "mongod") + if err != nil { + m.runtime.Logger.Error(fmt.Sprintf("%s has been existed, check mongodb version, error:%s", + installPath, err)) + return fmt.Errorf("%s has been existed, check mongodb version, error:%s", + installPath, err) + } + if version != m.ConfParams.DbVersion { + m.runtime.Logger.Error( + fmt.Sprintf("%s has been existed, check mongodb version, version:%s is incorrect", + installPath, version)) + return fmt.Errorf("%s has been existed, check mongodb version, version:%s is incorrect", + installPath, version) + } + m.runtime.Logger.Info("check mongod version successfully") + return nil +} + +// mkdir 创建相关目录并给目录授权 +func (m *MongoDBInstall) mkdir() error { + // 创建日志文件目录 + logPathDir, _ := filepath.Split(m.LogPath) + m.runtime.Logger.Info("start to create log directory") + if err := util.MkDirsIfNotExistsWithPerm([]string{logPathDir}, DefaultPerm); err != nil { + m.runtime.Logger.Error(fmt.Sprintf("create log directory fail, error:%s", err)) + return fmt.Errorf("create log directory fail, error:%s", err) + } + m.runtime.Logger.Info("create log directory successfully") + + // 创建数据文件目录 + m.runtime.Logger.Info("start to create data directory") + if err := util.MkDirsIfNotExistsWithPerm([]string{m.DbpathDir}, DefaultPerm); err != nil { + m.runtime.Logger.Error(fmt.Sprintf("create data directory fail, error:%s", err)) + return fmt.Errorf("create data directory fail, error:%s", err) + } + m.runtime.Logger.Info("create data directory successfully") + + // 创建备份文件目录 + m.runtime.Logger.Info("start to create backup directory") + if err := util.MkDirsIfNotExistsWithPerm([]string{m.BackupDir}, DefaultPerm); err != nil { + m.runtime.Logger.Error(fmt.Sprintf("create backup directory fail, error:%s", err)) + return fmt.Errorf("create backup directory fail, error:%s", err) + } + m.runtime.Logger.Info("create backup directory successfully") + + // 修改目录属主 + m.runtime.Logger.Info("start to execute chown command for dbPath, logPath and backupPath") + if _, err := util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", m.OsUser, m.OsGroup, filepath.Join(logPathDir, "../")), + "", nil, + 10*time.Second); err != nil { + m.runtime.Logger.Error(fmt.Sprintf("chown log directory fail, error:%s", err)) + return fmt.Errorf("chown log directory fail, error:%s", err) + } + if _, err := util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", m.OsUser, m.OsGroup, filepath.Join(m.DbpathDir, "../../")), + "", nil, + 10*time.Second); err != nil { + m.runtime.Logger.Error(fmt.Sprintf("chown data directory fail, error:%s", err)) + return fmt.Errorf("chown data directory fail, error:%s", err) + } + if _, err := util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", m.OsUser, m.OsGroup, m.BackupDir), + "", nil, + 10*time.Second); err != nil { + m.runtime.Logger.Error(fmt.Sprintf("chown backup directory fail, error:%s", err)) + return fmt.Errorf("chown backup directory fail, error:%s", err) + } + m.runtime.Logger.Info("execute chown command for dbPath, logPath and backupPath successfully") + return nil +} + +// createFile 创建配置文件以及key文件 +func (m *MongoDBInstall) createFile() error { + if err := common.CreateConfFileAndKeyFileAndDbTypeFileAndChown( + m.runtime, m.AuthConfFilePath, m.AuthConfFileContent, m.OsUser, m.OsGroup, m.NoAuthConfFilePath, + m.NoAuthConfFileContent, m.KeyFilePath, m.ConfParams.KeyFile, m.DbTypeFilePath, + m.ConfParams.InstanceType, DefaultPerm); err != nil { + return err + } + return nil +} + +// startup 启动服务 +func (m *MongoDBInstall) startup() error { + // 声明mongod可执行文件路径,把路径写入/etc/profile + if err := common.AddPathToProfile(m.runtime, m.BinDir); err != nil { + return err + } + + // 启动服务 + m.runtime.Logger.Info("start to startup mongod") + if err := common.StartMongoProcess(m.BinDir, m.ConfParams.Port, + m.OsUser, m.ConfParams.Auth); err != nil { + m.runtime.Logger.Error("startup mongod fail, error:%s", err) + return fmt.Errorf("startup mongod fail, error:%s", err) + } + flag, service, err := common.CheckMongoService(m.ConfParams.Port) + if err != nil { + m.runtime.Logger.Error("check %s fail, error:%s", service, err) + return fmt.Errorf("check %s fail, error:%s", service, err) + } + if flag == false { + m.runtime.Logger.Error("startup %s fail", service) + return fmt.Errorf("startup %s fail", service) + } + m.runtime.Logger.Info("startup %s successfully", service) + + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_replace.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_replace.go new file mode 100644 index 0000000000..65e0e1083b --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongod_replace.go @@ -0,0 +1,379 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// MongoDReplaceConfParams 参数 // 替换mongod +type MongoDReplaceConfParams struct { + IP string `json:"ip" validate:"required"` // 执行节点 + Port int `json:"port" validate:"required"` + SourceIP string `json:"sourceIP"` // 源节点,新加节点时可以为null + SourcePort int `json:"sourcePort"` // 源端口,新加节点时可以为null + SourceDown bool `json:"sourceDown"` // 源端已down机 true:已down false:未down + AdminUsername string `json:"adminUsername" validate:"required"` + AdminPassword string `json:"adminPassword" validate:"required"` + TargetIP string `json:"targetIP"` // 目标节点,移除节点时可以为null + TargetPort int `json:"targetPort"` // 目标端口,移除节点时可以为null + TargetPriority string `json:"targetPriority"` // 可选,默认为null,如果为null,则使用source端的Priority,取值:0-正无穷 + TargetHidden string `json:"targetHidden"` // 可选,默认为null,如果为null,则使用source端的Hidden,取值:null,0,1,0:显现 1:隐藏 +} + +// MongoDReplace 添加分片到集群 +type MongoDReplace struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + Mongo string + OsUser string + DataDir string + DbpathDir string + PrimaryIP string + PrimaryPort int + AddTargetScript string + ConfParams *MongoDReplaceConfParams + TargetIPStatus int + TargetPriority int + TargetHidden bool + StatusCh chan int +} + +// NewMongoDReplace 实例化结构体 +func NewMongoDReplace() jobruntime.JobRunner { + return &MongoDReplace{} +} + +// Name 获取原子任务的名字 +func (r *MongoDReplace) Name() string { + return "mongod_replace" +} + +// Run 运行原子任务 +func (r *MongoDReplace) Run() error { + // 主节点进行切换 + if err := r.primaryStepDown(); err != nil { + return err + } + + // 生成添加新节点脚本 + if err := r.makeAddTargetScript(); err != nil { + return err + } + + // 执行添加新节点脚本 + if err := r.execAddTargetScript(); err != nil { + return err + } + + // 查看新节点状态 + go r.checkTargetStatus() + + // 执行删除老节点脚本 + if err := r.checkTargetStatusAndRemoveSource(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (r *MongoDReplace) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (r *MongoDReplace) Rollback() error { + return nil +} + +// Init 初始化 +func (r *MongoDReplace) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + r.runtime = runtime + r.runtime.Logger.Info("start to init") + r.BinDir = consts.UsrLocal + r.Mongo = filepath.Join(r.BinDir, "mongodb", "bin", "mongo") + r.OsUser = consts.GetProcessUser() + r.DataDir = consts.GetMongoDataDir() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(r.runtime.PayloadDecoded), &r.ConfParams); err != nil { + r.runtime.Logger.Error(fmt.Sprintf( + "get parameters of mongodReplace fail by json.Unmarshal, error:%s", err)) + return fmt.Errorf("get parameters of mongodReplace fail by json.Unmarshal, error:%s", err) + } + + r.DbpathDir = filepath.Join(r.DataDir, "mongodata", strconv.Itoa(r.ConfParams.Port), "db") + + // 获取primary信息 + info, err := common.AuthGetPrimaryInfo(r.Mongo, r.ConfParams.AdminUsername, r.ConfParams.AdminPassword, + r.ConfParams.IP, r.ConfParams.Port) + if err != nil { + r.runtime.Logger.Error(fmt.Sprintf( + "get primary db info of mongodReplace fail, error:%s", err)) + return fmt.Errorf("get primary db info of mongodReplace fail, error:%s", err) + } + // 判断info是否为null + if info == "" { + r.runtime.Logger.Error(fmt.Sprintf( + "get primary db info of mongodReplace fail, error:%s", err)) + return fmt.Errorf("get primary db info of mongodReplace fail, error:%s", err) + } + getInfo := strings.Split(info, ":") + r.PrimaryIP = getInfo[0] + r.PrimaryPort, _ = strconv.Atoi(getInfo[1]) + r.StatusCh = make(chan int, 1) + + // 获取源端的配置信息 + _, _, _, hidden, priority, _, err := common.GetNodeInfo(r.Mongo, r.PrimaryIP, r.PrimaryPort, + r.ConfParams.AdminUsername, r.ConfParams.AdminPassword, r.ConfParams.SourceIP, r.ConfParams.SourcePort) + if err != nil { + return err + } + r.TargetHidden = hidden + if r.ConfParams.TargetHidden == "0" { + r.TargetHidden = false + } else if r.ConfParams.TargetHidden == "1" { + r.TargetHidden = true + } + + r.TargetPriority = priority + if r.ConfParams.TargetPriority != "" { + r.TargetPriority, _ = strconv.Atoi(r.ConfParams.TargetPriority) + } + + r.runtime.Logger.Info("init successfully") + + // 进行校验 + if err = r.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (r *MongoDReplace) checkParams() error { + // 校验重启配置参数 + validate := validator.New() + r.runtime.Logger.Info("start to validate parameters of mongodReplace") + if err := validate.Struct(r.ConfParams); err != nil { + r.runtime.Logger.Error("validate parameters of mongodReplace fail, error:%s", err) + return fmt.Errorf("validate parameters of mongodReplace fail, error:%s", err) + } + r.runtime.Logger.Info("validate parameters of mongodReplace successfully") + return nil +} + +// makeAddTargetScript 创建添加脚本 +func (r *MongoDReplace) makeAddTargetScript() error { + if r.ConfParams.TargetIP == "" { + return nil + } + // 生成脚本内容 + r.runtime.Logger.Info("start to make addTarget script content") + addMember := common.NewReplicasetMemberAdd() + addMember.Host = strings.Join([]string{r.ConfParams.TargetIP, strconv.Itoa(r.ConfParams.TargetPort)}, ":") + addMember.Priority = r.TargetPriority + addMember.Hidden = r.TargetHidden + addMemberJson, err := addMember.GetJson() + if err != nil { + r.runtime.Logger.Error("get addMemberJson info fail, error:%s", err) + return fmt.Errorf("get addMemberJson info fail, error:%s", err) + } + addMemberJson = strings.Replace(addMemberJson, "\"", "\\\"", -1) + addTargetConfScript := strings.Join([]string{"rs.add(", addMemberJson, ")"}, "") + r.AddTargetScript = addTargetConfScript + r.runtime.Logger.Info("make addTarget script content successfully") + return nil +} + +// execAddTargetScript 执行添加脚本 +func (r *MongoDReplace) execAddTargetScript() error { + if r.ConfParams.TargetIP == "" { + return nil + } + // 检查target是否已经存在 + flag, _, _, _, _, _, _ := common.GetNodeInfo(r.Mongo, r.PrimaryIP, r.PrimaryPort, + r.ConfParams.AdminUsername, r.ConfParams.AdminPassword, r.ConfParams.TargetIP, r.ConfParams.TargetPort) + if flag == true { + r.runtime.Logger.Info("target:%s has been existed", strings.Join( + []string{r.ConfParams.TargetIP, strconv.Itoa(r.ConfParams.TargetPort)}, ":")) + return nil + } + + r.runtime.Logger.Info("start to execute addTarget script") + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"%s\"", + r.Mongo, r.ConfParams.AdminUsername, r.ConfParams.AdminPassword, r.PrimaryIP, + r.PrimaryPort, r.AddTargetScript) + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + r.runtime.Logger.Error("execute addTarget script fail, error:%s", err) + return fmt.Errorf("execute addTarget script fail, error:%s", err) + } + r.runtime.Logger.Info("execute addTarget script successfully") + return nil +} + +// checkTargetStatus 检查target状态 +func (r *MongoDReplace) checkTargetStatus() { + if r.ConfParams.TargetIP == "" { + return + } + r.runtime.Logger.Info("start to check Target status") + for { + _, _, status, _, _, _, err := common.GetNodeInfo(r.Mongo, r.PrimaryIP, r.PrimaryPort, + r.ConfParams.AdminUsername, + r.ConfParams.AdminPassword, r.ConfParams.TargetIP, r.ConfParams.TargetPort) + if err != nil { + r.runtime.Logger.Error("get target status fail, error:%s", err) + } + if status != 0 { + r.StatusCh <- status + if status == 2 { + r.runtime.Logger.Info("target status is %d", status) + return + } + } + time.Sleep(5 * time.Second) + } +} + +// primaryStepDown 主库切换 +func (r *MongoDReplace) primaryStepDown() error { + if r.ConfParams.SourceIP == r.PrimaryIP && r.ConfParams.SourcePort == r.PrimaryPort { + r.runtime.Logger.Info("start to convert primary secondary db") + flag, err := common.AuthRsStepDown(r.Mongo, r.PrimaryIP, r.PrimaryPort, r.ConfParams.AdminUsername, + r.ConfParams.AdminPassword) + if err != nil { + r.runtime.Logger.Error(fmt.Sprintf("convert primary secondary db fail, error:%s", err)) + return fmt.Errorf("convert primary secondary db fail, error:%s", err) + } + if flag == true { + info, err := common.AuthGetPrimaryInfo(r.Mongo, r.ConfParams.AdminUsername, r.ConfParams.AdminPassword, + r.ConfParams.IP, r.ConfParams.Port) + if err != nil { + r.runtime.Logger.Error(fmt.Sprintf("get new primary info fail, error:%s", err)) + return fmt.Errorf("get new primary info fail, error:%s", err) + } + if info != fmt.Sprintf("%s:%d", r.ConfParams.IP, r.ConfParams.Port) { + r.runtime.Logger.Info("convert primary secondary db successfully") + infoSlice := strings.Split(info, ":") + r.PrimaryIP = infoSlice[0] + r.PrimaryPort, _ = strconv.Atoi(infoSlice[1]) + return nil + } + } + } + return nil +} + +// shutdownSourceProcess 关闭源端mongod进程 +func (r *MongoDReplace) shutdownSourceProcess() error { + flag, _, _ := common.CheckMongoService(r.ConfParams.Port) + if flag == false { + r.runtime.Logger.Info("source mongod process has been shut") + return nil + } + r.runtime.Logger.Info("start to shutdown source mongod process") + if err := common.ShutdownMongoProcess(r.OsUser, "mongod", r.BinDir, r.DbpathDir, r.ConfParams.Port); err != nil { + source := fmt.Sprintf("%s:%d", r.ConfParams.IP, r.ConfParams.Port) + r.runtime.Logger.Error(fmt.Sprintf("shutdown source:%s fail, error:%s", source, err)) + return fmt.Errorf("shutdown source:%s fail, error:%s", source, err) + } + r.runtime.Logger.Info("shutdown source mongod process successfully") + return nil +} + +// removeSource 复制集中移除source +func (r *MongoDReplace) removeSource() error { + if r.ConfParams.SourceIP == "" { + return nil + } + // 检查source是否存在 + flag, _, _, _, _, _, _ := common.GetNodeInfo(r.Mongo, r.PrimaryIP, r.PrimaryPort, + r.ConfParams.AdminUsername, r.ConfParams.AdminPassword, r.ConfParams.SourceIP, r.ConfParams.SourcePort) + if flag == false { + r.runtime.Logger.Info("source:%s has been remove", strings.Join( + []string{r.ConfParams.SourceIP, strconv.Itoa(r.ConfParams.SourcePort)}, ":")) + return nil + } + r.runtime.Logger.Info("start to make remove source script content") + removeSourceConfScript := strings.Join([]string{ + "rs.remove(", + fmt.Sprintf("\\\"%s:%d\\\"", r.ConfParams.SourceIP, r.ConfParams.SourcePort), + ")"}, "") + r.runtime.Logger.Info("make remove source script content successfully") + r.runtime.Logger.Info("start to execute remove source script") + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"%s\"", + r.Mongo, r.ConfParams.AdminUsername, r.ConfParams.AdminPassword, r.PrimaryIP, + r.PrimaryPort, removeSourceConfScript) + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + r.runtime.Logger.Error(fmt.Sprintf("execute remove source script fail, error:%s", err)) + return fmt.Errorf("execute remove source script fail, error:%s", err) + } + r.runtime.Logger.Info("execute remove source script successfully") + return nil +} + +// checkTargetStatusAndRemoveSource 监控状态并移除 +func (r *MongoDReplace) checkTargetStatusAndRemoveSource() error { + // 下架老节点 + if r.ConfParams.TargetIP == "" && r.ConfParams.SourceDown == false { + if err := r.shutdownSourceProcess(); err != nil { + return err + } + if err := r.removeSource(); err != nil { + return err + } + return nil + } else if r.ConfParams.TargetIP == "" && r.ConfParams.SourceDown == true { + if err := r.removeSource(); err != nil { + return err + } + return nil + } + // 先添加新节点再移除老节点,或者添加新节点 + for { + select { + // 超时时间 + case <-time.After(50 * time.Second): + return fmt.Errorf("check target status timeout") + case status := <-r.StatusCh: + if status == 2 && r.ConfParams.SourceDown == false { + if err := r.shutdownSourceProcess(); err != nil { + return err + } + if err := r.removeSource(); err != nil { + return err + } + return nil + } else if status == 2 && r.ConfParams.SourceDown == true { + if err := r.removeSource(); err != nil { + return err + } + return nil + } else if status == 2 && r.ConfParams.SourceIP == "" { + return nil + } + } + } +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongos_install.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongos_install.go new file mode 100644 index 0000000000..868d754eaf --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/mongos_install.go @@ -0,0 +1,441 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// MongoSConfParams 配置文件参数 +type MongoSConfParams struct { + common.MediaPkg `json:"mediapkg"` + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + InstanceType string `json:"instanceType" validate:"required"` // mongos mongod + App string `json:"app" validate:"required"` + SetId string `json:"setId" validate:"required"` + KeyFile string `json:"keyFile" validate:"required"` // keyFile的内容 app-setId + Auth bool `json:"auth"` // true:以验证方式启动mongos false:以非验证方式启动mongos + ConfigDB []string `json:"configDB" validate:"required"` // ip:port + DbConfig struct { + SlowOpThresholdMs int `json:"slowOpThresholdMs"` + Destination string `json:"destination"` + } `json:"dbConfig" validate:"required"` +} + +// MongoSInstall MongoS安装 +type MongoSInstall struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + DataDir string + OsUser string // MongoDB安装在哪个用户下 + OsGroup string + ConfParams *MongoSConfParams + DbVersion string + AuthConfFilePath string + AuthConfFileContent []byte + NoAuthConfFilePath string + NoAuthConfFileContent []byte + DbTypeFilePath string + LogPath string + PidFilePath string + KeyFilePath string + InstallPackagePath string + LockFilePath string // 锁文件路径 +} + +// NewMongoSInstall 实例化结构体 +func NewMongoSInstall() jobruntime.JobRunner { + return &MongoSInstall{} +} + +// Name 获取原子任务的名字 +func (s *MongoSInstall) Name() string { + return "mongos_install" +} + +// Run 运行原子任务 +func (s *MongoSInstall) Run() error { + // 进行校验 + status, err := s.checkParams() + if err != nil { + return err + } + if status { + return nil + } + + // 解压安装包并修改属主 + if err = s.unTarAndCreateSoftLink(); err != nil { + return err + } + + // 创建目录并修改属主 + if err = s.mkdir(); err != nil { + return err + } + + // 创建配置文件,key文件并修改属主 + if err = s.creatFile(); err != nil { + return err + } + + // 启动服务 + if err = s.startup(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (s *MongoSInstall) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (s *MongoSInstall) Rollback() error { + return nil +} + +// Init 初始化 +func (s *MongoSInstall) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + s.runtime = runtime + s.runtime.Logger.Info("start to init") + s.BinDir = consts.UsrLocal + s.DataDir = consts.GetMongoDataDir() + s.OsUser = consts.GetProcessUser() + s.OsGroup = consts.GetProcessUserGroup() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(s.runtime.PayloadDecoded), &s.ConfParams); err != nil { + s.runtime.Logger.Error(fmt.Sprintf( + "get parameters of mongodb config file fail by json.Unmarshal, error:%s", err)) + return fmt.Errorf("get parameters of mongodb config file fail by json.Unmarshal, error:%s", err) + } + + // 获取信息 + s.InstallPackagePath = s.ConfParams.MediaPkg.GetAbsolutePath() + s.DbVersion = strings.Split(s.ConfParams.MediaPkg.GePkgBaseName(), "-")[3] + + // 设置各种路径 + strPort := strconv.Itoa(s.ConfParams.Port) + s.AuthConfFilePath = filepath.Join(s.DataDir, "mongodata", strPort, "mongo.conf") + s.NoAuthConfFilePath = filepath.Join(s.DataDir, "mongodata", strPort, "noauth.conf") + s.LogPath = filepath.Join(s.DataDir, "mongolog", strPort, "mongo.log") + PidFileName := fmt.Sprintf("pid.%s", strPort) + s.PidFilePath = filepath.Join(s.DataDir, "mongodata", strPort, PidFileName) + s.KeyFilePath = filepath.Join(s.DataDir, "mongodata", strPort, "key_of_mongo") + s.DbTypeFilePath = filepath.Join(s.DataDir, "mongodata", strPort, "dbtype") + s.LockFilePath = filepath.Join(s.DataDir, "mongoinstall.lock") + + // 生成配置文件内容 + s.runtime.Logger.Info("make mongos config file content") + if err := s.makeConfContent(); err != nil { + return err + } + + return nil +} + +// makeConfContent 生成配置文件内容 +func (s *MongoSInstall) makeConfContent() error { + // 只支持mongos 3.0及以上得到配置文件内容 + // 判断mongos版本 + s.runtime.Logger.Info("start to make config file content") + mainVersion, err := strconv.Atoi(strings.Split(s.DbVersion, ".")[0]) + if err != nil { + s.runtime.Logger.Error( + "get %s version fail, error:%s", s.ConfParams.InstanceType, err) + return fmt.Errorf("get %s version fail, error:%s", s.ConfParams.InstanceType, err) + } + clusterId := strings.Join([]string{s.ConfParams.App, s.ConfParams.SetId, "conf"}, "-") + IpConfigDB := strings.Join(s.ConfParams.ConfigDB, ",") + configDB := strings.Join([]string{clusterId, IpConfigDB}, "/") + + // 生成mongos配置文件 + conf := common.NewYamlMongoSConf() + conf.Sharding.ConfigDB = configDB + conf.SystemLog.LogAppend = true + conf.SystemLog.Path = s.LogPath + conf.SystemLog.Destination = s.ConfParams.DbConfig.Destination + conf.ProcessManagement.Fork = true + conf.ProcessManagement.PidFilePath = s.PidFilePath + conf.Net.Port = s.ConfParams.Port + conf.Net.BindIp = strings.Join([]string{"127.0.0.1", s.ConfParams.IP}, ",") + conf.Net.WireObjectCheck = false + // mongos版本小于4获取配置文件内容 + if mainVersion < 4 { + s.NoAuthConfFileContent, err = conf.GetConfContent() + if err != nil { + s.runtime.Logger.Error( + "version:%s make mongos no auth config file content fail, error:%s", s.DbVersion, err) + return fmt.Errorf("version:%s make mongos no auth config file content fail, error:%s", + s.DbVersion, err) + } + conf.Security.KeyFile = s.KeyFilePath + // 获取验证配置文件内容 + s.AuthConfFileContent, err = conf.GetConfContent() + if err != nil { + s.runtime.Logger.Error(fmt.Sprintf( + "version:%s make mongos auth config file content fail, error:%s", + s.DbVersion, err)) + return fmt.Errorf("version:%s make mongos auth config file content fail, error:%s", + s.DbVersion, err) + } + s.runtime.Logger.Info("make config file content successfully") + return nil + } + + // mongos版本4及以上获取配置文件内容 + conf.OperationProfiling.SlowOpThresholdMs = s.ConfParams.DbConfig.SlowOpThresholdMs + conf.OperationProfiling.SlowOpThresholdMs = s.ConfParams.DbConfig.SlowOpThresholdMs + // 获取非验证配置文件内容 + s.NoAuthConfFileContent, err = conf.GetConfContent() + if err != nil { + s.runtime.Logger.Error( + "version:%s make mongos no auth config file content fail, error:%s", s.DbVersion, err) + return fmt.Errorf("version:%s make mongos no auth config file content fail, error:%s", + s.DbVersion, err) + } + conf.Security.KeyFile = s.KeyFilePath + // 获取验证配置文件内容 + s.AuthConfFileContent, err = conf.GetConfContent() + if err != nil { + s.runtime.Logger.Error(fmt.Sprintf( + "version:%s make mongos auth config file content fail, error:%s", + s.DbVersion, err)) + return fmt.Errorf("version:%s make mongos auth config file content fail, error:%s", + s.DbVersion, err) + } + s.runtime.Logger.Info("make config file content successfully") + return nil +} + +// checkParams 校验参数 检查输入的参数 检查端口是否合规 检查安装包 检查端口是否被使用(如果使用,则检查是否是mongodb服务) +func (s *MongoSInstall) checkParams() (bool, error) { + // 校验Mongo配置文件 + s.runtime.Logger.Info("start to validate parameters") + validate := validator.New() + s.runtime.Logger.Info("start to validate parameters of mongos config file") + if err := validate.Struct(s.ConfParams); err != nil { + s.runtime.Logger.Error(fmt.Sprintf("validate parameters of mongos config file fail, error:%s", err)) + return false, fmt.Errorf("validate parameters of mongos config file fail, error:%s", err) + } + s.runtime.Logger.Info("= validate parameters of mongos config file successfully") + + // 校验port是否合规 + s.runtime.Logger.Info("start to validate port if it is correct") + if s.ConfParams.Port < MongoDBPortMin || s.ConfParams.Port > MongoDBPortMax { + s.runtime.Logger.Error(fmt.Sprintf( + "validate port if it is correct, port is not within defalut range [%d,%d]", + MongoDBPortMin, MongoDBPortMax)) + return false, fmt.Errorf("validate port if it is correct, port is not within defalut range [%d,%d]", + MongoDBPortMin, MongoDBPortMax) + } + s.runtime.Logger.Info("validate port if it is correct successfully") + + // 校验安装包是否存在,md5值是否一致 + s.runtime.Logger.Info("start to validate install package") + if flag := util.FileExists(s.InstallPackagePath); !flag { + s.runtime.Logger.Error(fmt.Sprintf("validate install package, %s is not existed", + s.InstallPackagePath)) + return false, fmt.Errorf("validate install file, %s is not existed", + s.InstallPackagePath) + } + md5, _ := util.GetFileMd5(s.InstallPackagePath) + if s.ConfParams.MediaPkg.PkgMd5 != md5 { + s.runtime.Logger.Error(fmt.Sprintf("validate install package md5 fail, md5 is incorrect")) + return false, fmt.Errorf("validate install package md5 fail, md5 is incorrect") + } + s.runtime.Logger.Info("validate install package md5 successfully") + + // 校验端口是否使用 + s.runtime.Logger.Info("start to validate port if it has been used") + flag, _ := util.CheckPortIsInUse(s.ConfParams.IP, strconv.Itoa(s.ConfParams.Port)) + if flag { + // 校验端口是否是mongod进程 + cmd := fmt.Sprintf("netstat -ntpl |grep %d | awk '{print $7}' |head -1", s.ConfParams.Port) + result, _ := util.RunBashCmd(cmd, "", nil, 10*time.Second) + if strings.Contains(result, "mongos") { + // 检查配置文件是否一致,读取已有配置文件与新生成的配置文件内容对比 + content, _ := ioutil.ReadFile(s.AuthConfFilePath) + if strings.Compare(string(content), string(s.AuthConfFileContent)) == 0 { + // 检查mongodb版本 + version, err := common.CheckMongoVersion(s.BinDir, "mongos") + if err != nil { + s.runtime.Logger.Error( + fmt.Sprintf("mongos has been installed, port:%d, check mongos version fail. error:%s", + s.ConfParams.Port, version)) + return false, fmt.Errorf("mongos has been installed, port:%d, check mongos version fail. error:%s", + s.ConfParams.Port, version) + } + if version == s.DbVersion { + s.runtime.Logger.Info(fmt.Sprintf("mongos has been installed, port:%d, version:%s", + s.ConfParams.Port, version)) + return true, nil + } + s.runtime.Logger.Error(fmt.Sprintf("other mongos has been installed, port:%d, version:%s", + s.ConfParams.Port, version)) + return false, fmt.Errorf("other mongos has been installed, port:%d, version:%s", + s.ConfParams.Port, version) + } + + } + s.runtime.Logger.Error( + fmt.Sprintf("validate port if it has been used, port:%d is used by other process", + s.ConfParams.Port)) + return false, fmt.Errorf("validate port if it has been used, port:%d is used by other process", + s.ConfParams.Port) + } + s.runtime.Logger.Info("validate port if it has been used successfully") + s.runtime.Logger.Info("validate parameters successfully") + return false, nil +} + +// unTarAndCreateSoftLink 解压安装包,创建软链接并给目录授权 +func (s *MongoSInstall) unTarAndCreateSoftLink() error { + // 判断解压目录是否存在 + unTarPath := filepath.Join(s.BinDir, s.ConfParams.MediaPkg.GePkgBaseName()) + + // soft link目录 + installPath := filepath.Join(s.BinDir, "mongodb") + + // 解压安装包并授权 + // 安装多实例并发执行添加文件锁 + s.runtime.Logger.Info("start to get install file lock") + fileLock := common.NewFileLock(s.LockFilePath) + // 获取锁 + err := fileLock.Lock() + if err != nil { + for { + err = fileLock.Lock() + if err != nil { + time.Sleep(1 * time.Second) + continue + } + s.runtime.Logger.Info("get install file lock successfully") + break + } + } else { + s.runtime.Logger.Info("get install file lock successfully") + } + if err = common.UnTarAndCreateSoftLinkAndChown(s.runtime, s.BinDir, + s.InstallPackagePath, unTarPath, installPath, s.OsUser, s.OsGroup); err != nil { + return err + } + // 释放锁 + s.runtime.Logger.Info("release install file lock successfully") + _ = fileLock.UnLock() + + // 检查mongos版本 + s.runtime.Logger.Info("start to check mongos version") + version, err := common.CheckMongoVersion(s.BinDir, "mongos") + if err != nil { + s.runtime.Logger.Error(fmt.Sprintf("%s has been existed, check mongodb version, error:%s", + installPath, err)) + return fmt.Errorf("%s has been existed, check mongodb version, error:%s", + installPath, err) + } + if version != s.DbVersion { + s.runtime.Logger.Error( + fmt.Sprintf("%s has been existed, check mongodb version, version:%s is incorrect", + installPath, version)) + return fmt.Errorf("%s has been existed, check mongodb version, version:%s is incorrect", + installPath, version) + } + s.runtime.Logger.Info("check mongos version successfully") + return nil +} + +// mkdir 创建相关目录并给目录授权 +func (s *MongoSInstall) mkdir() error { + // 创建日志文件目录 + logPathDir, _ := filepath.Split(s.LogPath) + s.runtime.Logger.Info("start to create log directory") + if err := util.MkDirsIfNotExistsWithPerm([]string{logPathDir}, DefaultPerm); err != nil { + s.runtime.Logger.Error(fmt.Sprintf("create log directory fail, error:%s", err)) + return fmt.Errorf("create log directory fail, error:%s", err) + } + s.runtime.Logger.Info("create log directory successfully") + + // 创建配置文件目录 + confFilePathDir, _ := filepath.Split(s.AuthConfFilePath) + s.runtime.Logger.Info("start to create data directory") + if err := util.MkDirsIfNotExistsWithPerm([]string{confFilePathDir}, DefaultPerm); err != nil { + s.runtime.Logger.Error(fmt.Sprintf("create data directory fail, error:%s", err)) + return fmt.Errorf("create data directory fail, error:%s", err) + } + s.runtime.Logger.Info("create data directory successfully") + + // 修改目录属主 + s.runtime.Logger.Info("start to execute chown command for dbPath, logPath and backupPath") + if _, err := util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", s.OsUser, s.OsGroup, filepath.Join(logPathDir, "../")), + "", nil, + 10*time.Second); err != nil { + s.runtime.Logger.Error(fmt.Sprintf("chown log directory fail, error:%s", err)) + return fmt.Errorf("chown log directory fail, error:%s", err) + } + if _, err := util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", s.OsUser, s.OsGroup, filepath.Join(confFilePathDir, "../")), + "", nil, + 10*time.Second); err != nil { + s.runtime.Logger.Error(fmt.Sprintf("chown data directory fail, error:%s", err)) + return fmt.Errorf("chown data directory fail, error:%s", err) + } + s.runtime.Logger.Info("execute chown command for dbPath, logPath and backupPath successfully") + return nil +} + +// createFile 创建配置文件以及key文件 +func (s *MongoSInstall) creatFile() error { + // 创建配置文件,key文件,dbType文件并授权 + if err := common.CreateConfFileAndKeyFileAndDbTypeFileAndChown( + s.runtime, s.AuthConfFilePath, s.AuthConfFileContent, s.OsUser, s.OsGroup, s.NoAuthConfFilePath, + s.NoAuthConfFileContent, s.KeyFilePath, s.ConfParams.KeyFile, s.DbTypeFilePath, + s.ConfParams.InstanceType, DefaultPerm); err != nil { + return err + } + return nil +} + +// startup 启动服务 +func (s *MongoSInstall) startup() error { + // 申明mongos可执行文件路径,把路径写入/etc/profile + if err := common.AddPathToProfile(s.runtime, s.BinDir); err != nil { + return err + } + + // 启动服务 + s.runtime.Logger.Info("start to startup mongos") + if err := common.StartMongoProcess(s.BinDir, s.ConfParams.Port, + s.OsUser, s.ConfParams.Auth); err != nil { + s.runtime.Logger.Error(fmt.Sprintf("startup mongos fail, error:%s", err)) + return fmt.Errorf("shutdown mongos fail, error:%s", err) + } + flag, service, err := common.CheckMongoService(s.ConfParams.Port) + if err != nil { + s.runtime.Logger.Error("check %s fail, error:%s", service, err) + return fmt.Errorf("check %s fail, error:%s", service, err) + } + if flag == false { + s.runtime.Logger.Error("startup %s fail", service) + return fmt.Errorf("startup %s fail", service) + } + s.runtime.Logger.Info("startup %s successfully", service) + + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/replicaset_stepdown.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/replicaset_stepdown.go new file mode 100644 index 0000000000..631bbd2f33 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb/replicaset_stepdown.go @@ -0,0 +1,130 @@ +package atommongodb + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strconv" + "strings" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + + "github.com/go-playground/validator/v10" +) + +// StepDownConfParams 参数 +type StepDownConfParams struct { + IP string `json:"ip" validate:"required"` + Port int `json:"port" validate:"required"` + AdminUsername string `json:"adminUsername" validate:"required"` + AdminPassword string `json:"adminPassword" validate:"required"` +} + +// StepDown 添加分片到集群 +type StepDown struct { + runtime *jobruntime.JobGenericRuntime + BinDir string + Mongo string + OsUser string + PrimaryIP string + PrimaryPort int + ConfParams *StepDownConfParams +} + +// NewStepDown 实例化结构体 +func NewStepDown() jobruntime.JobRunner { + return &StepDown{} +} + +// Name 获取原子任务的名字 +func (s *StepDown) Name() string { + return "replicaset_stepdown" +} + +// Run 运行原子任务 +func (s *StepDown) Run() error { + // 执行主备切换 + if err := s.execStepDown(); err != nil { + return err + } + + return nil +} + +// Retry 重试 +func (s *StepDown) Retry() uint { + return 2 +} + +// Rollback 回滚 +func (s *StepDown) Rollback() error { + return nil +} + +// Init 初始化 +func (s *StepDown) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + s.runtime = runtime + s.runtime.Logger.Info("start to init") + s.BinDir = consts.UsrLocal + s.Mongo = filepath.Join(s.BinDir, "mongodb", "bin", "mongo") + s.OsUser = consts.GetProcessUser() + + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(s.runtime.PayloadDecoded), &s.ConfParams); err != nil { + s.runtime.Logger.Error(fmt.Sprintf( + "get parameters of stepDown fail by json.Unmarshal, error:%s", err)) + return fmt.Errorf("get parameters of stepDown fail by json.Unmarshal, error:%s", err) + } + + // 获取primary信息 + info, err := common.AuthGetPrimaryInfo(s.Mongo, s.ConfParams.AdminUsername, s.ConfParams.AdminPassword, + s.ConfParams.IP, s.ConfParams.Port) + if err != nil { + s.runtime.Logger.Error(fmt.Sprintf( + "get primary db info of stepDown fail, error:%s", err)) + return fmt.Errorf("get primary db info of stepDown fail, error:%s", err) + } + getInfo := strings.Split(info, ":") + s.PrimaryIP = getInfo[0] + s.PrimaryPort, _ = strconv.Atoi(getInfo[1]) + + // 进行校验 + s.runtime.Logger.Info("start to validate parameters") + if err = s.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (s *StepDown) checkParams() error { + // 校验配置参数 + validate := validator.New() + s.runtime.Logger.Info("start to validate parameters of deleteUser") + if err := validate.Struct(s.ConfParams); err != nil { + s.runtime.Logger.Error(fmt.Sprintf("validate parameters of deleteUser fail, error:%s", err)) + return fmt.Errorf("validate parameters of deleteUser fail, error:%s", err) + } + return nil +} + +// execStepDown 执行切换 +func (s *StepDown) execStepDown() error { + s.runtime.Logger.Info("start to convert primary secondary db") + flag, err := common.AuthRsStepDown(s.Mongo, s.PrimaryIP, s.PrimaryPort, s.ConfParams.AdminUsername, + s.ConfParams.AdminPassword) + if err != nil { + s.runtime.Logger.Error("convert primary secondary db fail, error:%s", err) + return fmt.Errorf("convert primary secondary db fail, error:%s", err) + } + if flag == true { + s.runtime.Logger.Info("convert primary secondary db successfully") + return nil + } + + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atomsys/atomsys.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atomsys/atomsys.go new file mode 100644 index 0000000000..bb529bb5f4 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atomsys/atomsys.go @@ -0,0 +1,2 @@ +// Package atomsys os系统原子任务 +package atomsys diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atomsys/os_mongo_init.go b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atomsys/os_mongo_init.go new file mode 100644 index 0000000000..0b0935fc3c --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atomsys/os_mongo_init.go @@ -0,0 +1,123 @@ +package atomsys + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/common" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + + "github.com/go-playground/validator/v10" +) + +// OsMongoInitConfParams 系统初始化参数 +type OsMongoInitConfParams struct { + User string `json:"user" validate:"required"` + Password string `json:"password" validate:"required"` +} + +// OsMongoInit 系统初始化原子任务 +type OsMongoInit struct { + runtime *jobruntime.JobGenericRuntime + ConfParams *OsMongoInitConfParams + OsUser string + OsGroup string +} + +// NewOsMongoInit new +func NewOsMongoInit() jobruntime.JobRunner { + return &OsMongoInit{} +} + +// Init 初始化 +func (o *OsMongoInit) Init(runtime *jobruntime.JobGenericRuntime) error { + // 获取安装参数 + o.runtime = runtime + o.runtime.Logger.Info("start to init") + o.OsUser = consts.GetProcessUser() + o.OsGroup = consts.GetProcessUserGroup() + // 获取MongoDB配置文件参数 + if err := json.Unmarshal([]byte(o.runtime.PayloadDecoded), &o.ConfParams); err != nil { + o.runtime.Logger.Error( + "get parameters of mongoOsInit fail by json.Unmarshal, error:%s", err) + return fmt.Errorf("get parameters of mongoOsInit fail by json.Unmarshal, error:%s", err) + } + o.runtime.Logger.Info("init successfully") + + // 进行校验 + if err := o.checkParams(); err != nil { + return err + } + + return nil +} + +// checkParams 校验参数 +func (o *OsMongoInit) checkParams() error { + // 校验配置参数 + o.runtime.Logger.Info("start to validate parameters") + validate := validator.New() + o.runtime.Logger.Info("start to validate parameters of deInstall") + if err := validate.Struct(o.ConfParams); err != nil { + o.runtime.Logger.Error("validate parameters of mongoOsInit fail, error:%s", err) + return fmt.Errorf("validate parameters of mongoOsInit fail, error:%s", err) + } + o.runtime.Logger.Info("validate parameters successfully") + return nil +} + +// Name 名字 +func (o *OsMongoInit) Name() string { + return "os_mongo_init" +} + +// Run 执行函数 +func (o *OsMongoInit) Run() error { + // 获取初始化脚本 + o.runtime.Logger.Info("start to make init script content") + data := common.MongoShellInit + data = strings.Replace(data, "{{user}}", o.OsUser, -1) + data = strings.Replace(data, "{{group}}", o.OsGroup, -1) + o.runtime.Logger.Info("make init script content successfully") + + // 创建脚本文件 + o.runtime.Logger.Info("start to create init script file") + tmpScriptName := "/tmp/sysinit.sh" + if err := ioutil.WriteFile(tmpScriptName, []byte(data), 07555); err != nil { + o.runtime.Logger.Error("write tmp script failed %s", err.Error()) + return err + } + o.runtime.Logger.Info("create init script file successfully") + + // 执行脚本 + o.runtime.Logger.Info("start to execute init script") + _, err := util.RunBashCmd(tmpScriptName, "", nil, 30*time.Second) + if err != nil { + o.runtime.Logger.Error("execute init script fail, error:%s", err) + return fmt.Errorf("execute init script fail, error:%s", err) + } + o.runtime.Logger.Info("execute init script successfully") + // 设置用户名密码 + o.runtime.Logger.Info("start to set user:%s password", o.OsUser) + err = util.SetOSUserPassword(o.ConfParams.User, o.ConfParams.Password) + o.runtime.Logger.Info("set user:%s password successfully", o.OsUser) + if err != nil { + return err + } + return nil +} + +// Retry times +func (o *OsMongoInit) Retry() uint { + return 2 +} + +// Rollback rollback +func (o *OsMongoInit) Rollback() error { + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/backupsys/backupsys.go b/dbm-services/mongo/db-tools/dbactuator/pkg/backupsys/backupsys.go new file mode 100644 index 0000000000..1ef15f9f0a --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/backupsys/backupsys.go @@ -0,0 +1,231 @@ +// Package backupsys 备份系统 +package backupsys + +import ( + "bufio" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/mylog" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" +) + +// UploadTask 操作备份系统 +type UploadTask struct { + Files []string `json:"files"` // 全路径 + TaskIDs []uint64 `json:"taskids"` + Tag string `json:"tag"` +} + +// UploadFiles 上传文件 +func (task *UploadTask) UploadFiles() (err error) { + var taskIDStr string + var taskIDNum uint64 + if len(task.Files) == 0 { + return + } + if task.Tag == "" { + err = fmt.Errorf("BackupSystem uploadFiles tag(%s) cannot be empty", task.Tag) + mylog.Logger.Error(err.Error()) + return + } + for _, file := range task.Files { + if !util.FileExists(file) { + err = fmt.Errorf("BackupSystem uploadFiles %s not exists", file) + mylog.Logger.Error(err.Error()) + return + } + } + for _, bkfile := range task.Files { + bkCmd := fmt.Sprintf("%s -n -f %s --with-md5 -t %s|grep 'taskid'|awk -F: '{print $2}'", + consts.BackupClient, bkfile, task.Tag) + mylog.Logger.Info(bkCmd) + taskIDStr, err = util.RunBashCmd(bkCmd, "", nil, 10*time.Minute) + if err != nil { + return + } + taskIDNum, err = strconv.ParseUint(taskIDStr, 10, 64) + if err != nil { + err = fmt.Errorf("%s ParseUint failed,err:%v", taskIDStr, err) + mylog.Logger.Error(err.Error()) + return + } + task.TaskIDs = append(task.TaskIDs, taskIDNum) + } + return +} + +// CheckTasksStatus 检查tasks状态 +func (task *UploadTask) CheckTasksStatus() (runningTaskIDs, failTaskIDs, succTaskIDs []uint64, + runningFiles, failedFiles, succFiles []string, failMsgs []string, err error) { + var status TaskStatus + for idx, taskID := range task.TaskIDs { + status, err = GetTaskStatus(taskID) + if err != nil { + return + } + if status.Status > 4 { + // err = fmt.Errorf("ToBackupSystem %s failed,err:%s,taskid:%d", + // status.File, status.StatusInfo, taskID) + // mylog.Logger.Error(err.Error()) + failMsgs = append(failMsgs, fmt.Sprintf("taskid:%d,failMsg:%s", taskID, status.StatusInfo)) + failedFiles = append(failedFiles, task.Files[idx]) + failTaskIDs = append(failTaskIDs, task.TaskIDs[idx]) + } else if status.Status == 4 { + succFiles = append(succFiles, task.Files[idx]) + succTaskIDs = append(succTaskIDs, task.TaskIDs[idx]) + } else if status.Status < 4 { + runningFiles = append(runningFiles, task.Files[idx]) + runningTaskIDs = append(runningTaskIDs, task.TaskIDs[idx]) + } + } + return +} + +// WaitForUploadFinish 等待所有files上传成功 +func (task *UploadTask) WaitForUploadFinish() (err error) { + var times int64 + var msg string + var runningFiles, failFiles, succFiles, failMsgs []string + for { + times++ + _, _, _, runningFiles, failFiles, succFiles, failMsgs, err = task.CheckTasksStatus() + if err != nil { + return + } + // 只要有running的task,则继续等待 + if len(runningFiles) > 0 { + if times%6 == 0 { + // 每分钟打印一次日志 + msg = fmt.Sprintf("files[%+v] cnt:%d upload to backupSystem still running", runningFiles, len(runningFiles)) + mylog.Logger.Info(msg) + } + time.Sleep(10 * time.Second) + continue + } + if len(failMsgs) > 0 { + err = fmt.Errorf("failCnt:%d,failFiles:[%+v],err:%s", len(failFiles), failFiles, strings.Join(failFiles, ",")) + mylog.Logger.Error(err.Error()) + return + } + if len(succFiles) == len(task.Files) { + return nil + } + break + } + return +} + +// TaskStatus backup_client -q --taskid=xxxx 命令的结果 +type TaskStatus struct { + File string `json:"file"` + Host string `json:"host"` + SednupDateTime time.Time `json:"sendup_datetime"` + Status int `json:"status"` + StatusInfo string `json:"status_info"` + StartTime time.Time `json:"start_time"` + CompleteTime time.Time `json:"complete_time"` + ExpireTime time.Time `json:"expire_time"` +} + +// String 用于打印 +func (status *TaskStatus) String() string { + statusBytes, _ := json.Marshal(status) + return string(statusBytes) +} + +// GetTaskStatus 执行backup_client -q --taskid=xxxx 命令的结果并解析 +func GetTaskStatus(taskid uint64) (status TaskStatus, err error) { + var cmdRet string + bkCmd := fmt.Sprintf("%s -q --taskid=%d", consts.BackupClient, taskid) + cmdRet, err = util.RunBashCmd(bkCmd, "", nil, 30*time.Second) + if err != nil { + return + } + scanner := bufio.NewScanner(strings.NewReader(cmdRet)) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimSpace(line) + if line == "" { + continue + } + l01 := strings.SplitN(line, ":", 2) + if len(l01) != 2 { + err = fmt.Errorf("len()!=2,cmd:%s,result format not correct:%s", bkCmd, cmdRet) + mylog.Logger.Error(err.Error()) + return + } + first := strings.TrimSpace(l01[0]) + second := strings.TrimSpace(l01[1]) + switch first { + case "file": + status.File = second + case "host": + status.Host = second + case "sendup datetime": + if second == "0000-00-00 00:00:00" { + status.SednupDateTime = time.Time{} // "0000-01-00 00:00:00" + break + } + status.SednupDateTime, err = time.ParseInLocation(consts.UnixtimeLayout, second, time.Local) + if err != nil { + err = fmt.Errorf("time.Parse 'sendup datetime' failed,err:%v,value:%s,cmd:%s", err, second, bkCmd) + mylog.Logger.Error(err.Error()) + return + } + case "status": + status.Status, err = strconv.Atoi(second) + if err != nil { + err = fmt.Errorf("strconv.Atoi failed,err:%v,value:%s,cmd:%s", err, second, bkCmd) + mylog.Logger.Error(err.Error()) + return + } + case "status info": + status.StatusInfo = second + case "start_time": + if second == "0000-00-00 00:00:00" { + status.StartTime = time.Time{} // "0000-01-00 00:00:00" + break + } + status.StartTime, err = time.ParseInLocation(consts.UnixtimeLayout, second, time.Local) + if err != nil { + err = fmt.Errorf("time.Parse start_time failed,err:%v,value:%s,cmd:%s", err, second, bkCmd) + mylog.Logger.Error(err.Error()) + return + } + case "complete_time": + if second == "0000-00-00 00:00:00" { + status.CompleteTime = time.Time{} // "0000-01-00 00:00:00" + break + } + status.CompleteTime, err = time.ParseInLocation(consts.UnixtimeLayout, second, time.Local) + if err != nil { + err = fmt.Errorf("time.Parse complete_time failed,err:%v,value:%s,cmd:%s", err, second, bkCmd) + mylog.Logger.Error(err.Error()) + return + } + case "expire_time": + if second == "0000-00-00 00:00:00" { + status.ExpireTime = time.Time{} // "0000-01-00 00:00:00" + break + } + status.ExpireTime, err = time.ParseInLocation(consts.UnixtimeLayout, second, time.Local) + if err != nil { + err = fmt.Errorf("time.Parse expire_time failed,err:%v,value:%s,cmd:%s", err, second, bkCmd) + mylog.Logger.Error(err.Error()) + return + } + } + } + if err = scanner.Err(); err != nil { + err = fmt.Errorf("scanner.Scan failed,err:%v,cmd:%s", err, cmdRet) + mylog.Logger.Error(err.Error()) + return + } + return +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/common/exporter_conf.go b/dbm-services/mongo/db-tools/dbactuator/pkg/common/exporter_conf.go new file mode 100644 index 0000000000..d275658bdd --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/common/exporter_conf.go @@ -0,0 +1,46 @@ +package common + +import ( + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +func getConfFileName(port int) string { + return filepath.Join(consts.ExporterConfDir, fmt.Sprintf("%d.conf", port)) +} + +// setExporterConfig 写入ExporterConfig文件 +// 目录固定:. consts.ExporterConfDir +// 文件名称:. $port.conf +// 文件已经存在, 覆盖. +// 文件写入失败,报错. + +// WriteExporterConfigFile 写exporter配置文件 +func WriteExporterConfigFile(port int, data interface{}) (err error) { + var fileData []byte + var confFile string + err = util.MkDirsIfNotExists([]string{consts.ExporterConfDir}) + if err != nil { + return err + } + confFile = getConfFileName(port) + fileData, _ = json.Marshal(data) + err = ioutil.WriteFile(confFile, fileData, 0755) + if err != nil { + return err + } + util.LocalDirChownMysql(consts.ExporterConfDir) + return nil +} + +// DeleteExporterConfigFile 删除Exporter配置文件. +func DeleteExporterConfigFile(port int) (err error) { + var confFile string + confFile = getConfFileName(port) + return os.Remove(confFile) +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/common/filelock.go b/dbm-services/mongo/db-tools/dbactuator/pkg/common/filelock.go new file mode 100644 index 0000000000..7c7d52a864 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/common/filelock.go @@ -0,0 +1,34 @@ +package common + +import ( + "os" + "syscall" +) + +// FileLock 结构体 +type FileLock struct { + Path string + FD *os.File +} + +// NewFileLock 生成结构体 +func NewFileLock(path string) *FileLock { + fd, _ := os.Open(path) + return &FileLock{ + Path: path, + FD: fd, + } +} + +// Lock 加锁 +func (f *FileLock) Lock() error { + err := syscall.Flock(int(f.FD.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + return err +} + +// UnLock 解锁 +func (f *FileLock) UnLock() error { + defer f.FD.Close() + err := syscall.Flock(int(f.FD.Fd()), syscall.LOCK_UN) + return err +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/common/initiate_replicaset_conf.go b/dbm-services/mongo/db-tools/dbactuator/pkg/common/initiate_replicaset_conf.go new file mode 100644 index 0000000000..f4b9cd7902 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/common/initiate_replicaset_conf.go @@ -0,0 +1,37 @@ +package common + +import "encoding/json" + +// JsonConfReplicaset 复制集配置 +type JsonConfReplicaset struct { + Id string `json:"_id"` + ConfigSvr bool `json:"configsvr"` + Members []*Member `json:"members"` +} + +// Member 成员 +type Member struct { + Id int `json:"_id"` + Host string `json:"host"` + Priority int `json:"priority"` + Hidden bool `json:"hidden"` +} + +// NewJsonConfReplicaset 获取结构体 +func NewJsonConfReplicaset() *JsonConfReplicaset { + return &JsonConfReplicaset{} +} + +// GetConfContent 获取配置内容 +func (j *JsonConfReplicaset) GetConfContent() ([]byte, error) { + confContent, err := json.Marshal(j) + if err != nil { + return nil, err + } + return confContent, nil +} + +// NewMember 获取结构体 +func NewMember() *Member { + return &Member{} +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/common/media_pkg.go b/dbm-services/mongo/db-tools/dbactuator/pkg/common/media_pkg.go new file mode 100644 index 0000000000..3ae6d84ce1 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/common/media_pkg.go @@ -0,0 +1,130 @@ +package common + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "time" + + "dbm-services/mongo/db-tools/dbactuator/mylog" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" +) + +// MediaPkg 通用介质包处理 +type MediaPkg struct { + Pkg string `json:"pkg" validate:"required"` // 安装包名 + PkgMd5 string `json:"pkg_md5" validate:"required,md5"` // 安装包MD5 +} + +// GetAbsolutePath 返回介质存放的绝对路径 +func (m *MediaPkg) GetAbsolutePath() string { + return filepath.Join(consts.PackageSavePath, m.Pkg) +} + +// GePkgBaseName 例如将 mysql-5.7.20-linux-x86_64-tmysql-3.1.5-gcs.tar.gz +// 解析出 mysql-5.7.20-linux-x86_64-tmysql-3.1.5-gcs +// 用于做软连接使用 +func (m *MediaPkg) GePkgBaseName() string { + pkgFullName := filepath.Base(m.GetAbsolutePath()) + return regexp.MustCompile("(.tar.gz|.tgz)$").ReplaceAllString(pkgFullName, "") +} + +// Check 检查介质包 +func (m *MediaPkg) Check() (err error) { + var fileMd5 string + // 判断安装包是否存在 + pkgAbPath := m.GetAbsolutePath() + if !util.FileExists(pkgAbPath) { + return fmt.Errorf("%s不存在", pkgAbPath) + } + if fileMd5, err = util.GetFileMd5(pkgAbPath); err != nil { + return fmt.Errorf("获取[%s]md5失败, %v", m.Pkg, err.Error()) + } + // 校验md5 + if fileMd5 != m.PkgMd5 { + return fmt.Errorf("安装包的md5不匹配,[%s]文件的md5[%s]不正确", fileMd5, m.PkgMd5) + } + return +} + +// DbToolsMediaPkg db工具包 +type DbToolsMediaPkg struct { + MediaPkg +} + +// Install 安装dbtools +// 1. 确保本地 /data/install/dbtool.tar.gz 存在,且md5校验ok; +// 2. 检查 {REDIS_BACKUP_DIR}/dbbak/dbatool.tar.gz 与 /data/install/dbtool.tar.gz 是否一致; +// - md5一致,则忽略更新; +// - /data/install/dbtool.tar.gz 不存在 or md5不一致 则用最新 /data/install/dbtool.tar.gz 工具覆盖 {REDIS_BACKUP_DIR}/dbbak/dbatool +// 3. 创建 /home/mysql/dbtools -> /data/dbbak/dbtools 软链接 +// 4. cp /data/install/dbtool.tar.gz {REDIS_BACKUP_DIR}/dbbak/dbatool.tar.gz +func (pkg *DbToolsMediaPkg) Install() (err error) { + var fileMd5 string + var overrideLocal bool = true + var newMysqlHomeLink bool = true + var realLink string + err = pkg.Check() + if err != nil { + return + } + toolsName := filepath.Base(consts.DbToolsPath) + backupDir := filepath.Join(consts.GetRedisBackupDir(), "dbbak") // 如 /data/dbbak + bakdirToolsTar := filepath.Join(backupDir, toolsName+".tar.gz") // 如 /data/dbbak/dbtools.tar.gz + installToolTar := pkg.GetAbsolutePath() + if util.FileExists(bakdirToolsTar) { + fileMd5, err = util.GetFileMd5(bakdirToolsTar) + if err != nil { + return + } + if fileMd5 == pkg.PkgMd5 { + overrideLocal = false + } + } + if overrideLocal { + // 最新介质覆盖本地 + untarCmd := fmt.Sprintf("tar -zxf %s -C %s", installToolTar, backupDir) + mylog.Logger.Info(untarCmd) + _, err = util.RunBashCmd(untarCmd, "", nil, 10*time.Minute) + if err != nil { + return + } + } + if !util.FileExists(filepath.Join(backupDir, toolsName)) { // 如 /data/dbbak/dbtools 目录不存在 + err = fmt.Errorf("dir:%s not exists", filepath.Join(backupDir, toolsName)) + mylog.Logger.Error(err.Error()) + return + } + if util.FileExists(consts.DbToolsPath) { + realLink, err = filepath.EvalSymlinks(consts.DbToolsPath) + if err != nil { + err = fmt.Errorf("filepath.EvalSymlinks %s fail,err:%v", consts.DbToolsPath, err) + mylog.Logger.Error(err.Error()) + return err + } + if realLink == filepath.Join(backupDir, toolsName) { // /home/mysql/dbtools 已经是指向 /data/dbbak/dbtools 的软连接 + newMysqlHomeLink = false + } + } + if newMysqlHomeLink { + // 需创建 /home/mysql/dbtools -> /data/dbbak/dbtools 软链接 + err = os.Symlink(filepath.Join(backupDir, toolsName), consts.DbToolsPath) + if err != nil { + err = fmt.Errorf("os.Symlink %s -> %s fail,err:%s", consts.DbToolsPath, filepath.Join(backupDir, toolsName), err) + mylog.Logger.Error(err.Error()) + return + } + mylog.Logger.Info("create softLink success,%s -> %s", consts.DbToolsPath, filepath.Join(backupDir, toolsName)) + } + cpCmd := fmt.Sprintf("cp %s %s", installToolTar, bakdirToolsTar) + mylog.Logger.Info(cpCmd) + _, err = util.RunBashCmd(cpCmd, "", nil, 10*time.Minute) + if err != nil { + return + } + util.LocalDirChownMysql(consts.DbToolsPath) + util.LocalDirChownMysql(backupDir) + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_common.go b/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_common.go new file mode 100644 index 0000000000..ca0db039e2 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_common.go @@ -0,0 +1,575 @@ +package common + +import ( + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" +) + +// UnTarAndCreateSoftLinkAndChown 解压目录,创建软链接并修改属主 +func UnTarAndCreateSoftLinkAndChown(runtime *jobruntime.JobGenericRuntime, binDir string, installPackagePath string, + unTarPath string, + installPath string, user string, group string) error { + // 解压安装包 + if !util.FileExists(unTarPath) { + // 解压到/usr/local目录下 + runtime.Logger.Info("start to unTar install package") + tarCmd := fmt.Sprintf("tar -zxf %s -C %s", installPackagePath, binDir) + if _, err := util.RunBashCmd(tarCmd, "", nil, 10*time.Second); err != nil { + runtime.Logger.Error(fmt.Sprintf("untar install file fail, error:%s", err)) + return fmt.Errorf("untar install file fail, error:%s", err) + } + runtime.Logger.Info("unTar install package successfully") + // 修改属主 + runtime.Logger.Info("start to execute chown command for unTar directory") + if _, err := util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", user, group, unTarPath), + "", nil, + 10*time.Second); err != nil { + runtime.Logger.Error(fmt.Sprintf("chown untar directory fail, error:%s", err)) + return fmt.Errorf("chown untar directory fail, error:%s", err) + } + runtime.Logger.Info("execute chown command for unTar directory successfully") + } + + // 创建软链接 + if !util.FileExists(installPath) { + // 创建软链接 + runtime.Logger.Info("start to create soft link") + softLink := fmt.Sprintf("ln -s %s %s", unTarPath, installPath) + if _, err := util.RunBashCmd(softLink, "", nil, 10*time.Second); err != nil { + runtime.Logger.Error( + fmt.Sprintf("install directory create softLink fail, error:%s", err)) + return fmt.Errorf("install directory create softLink fail, error:%s", err) + } + runtime.Logger.Info("create soft link successfully") + + // 修改属主 + runtime.Logger.Info("start to execute chown command for softLink directory") + if _, err := util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", user, group, installPath), + "", nil, + 10*time.Second); err != nil { + runtime.Logger.Error(fmt.Sprintf("chown softlink directory fail, error:%s", err)) + return fmt.Errorf("chown softlink directory fail, error:%s", err) + } + runtime.Logger.Info("execute chown command for softLink directory successfully") + + } + + return nil +} + +// GetMd5 获取md5值 +func GetMd5(str string) string { + h := md5.New() + h.Write([]byte(str)) + return hex.EncodeToString(h.Sum(nil)) +} + +// CheckMongoVersion 检查mongo版本 +func CheckMongoVersion(binDir string, mongoName string) (string, error) { + cmd := fmt.Sprintf("%s -version |grep -E 'db version|mongos version'| awk -F \" \" '{print $3}' |sed 's/v//g'", + filepath.Join(binDir, "mongodb", "bin", mongoName)) + getVersion, err := util.RunBashCmd(cmd, "", nil, 10*time.Second) + getVersion = strings.Replace(getVersion, "\n", "", -1) + if err != nil { + return "", err + } + return getVersion, nil +} + +// CheckMongoService 检查mongo服务是否存在 +func CheckMongoService(port int) (bool, string, error) { + cmd := fmt.Sprintf("netstat -ntpl |grep %d | awk '{print $7}' |head -1", port) + result, err := util.RunBashCmd(cmd, "", nil, 10*time.Second) + if err != nil { + return false, "", err + } + if strings.Contains(result, "mongos") { + return true, "mongos", nil + } + if strings.Contains(result, "mongod") { + return true, "mongod", nil + } + return false, "", nil +} + +// CreateConfFileAndKeyFileAndDbTypeFileAndChown 创建配置文件,key文件,dbType文件并授权 +func CreateConfFileAndKeyFileAndDbTypeFileAndChown(runtime *jobruntime.JobGenericRuntime, authConfFilePath string, + authConfFileContent []byte, user string, group string, noAuthConfFilePath string, noAuthConfFileContent []byte, + keyFilePath string, keyFileContent string, dbTypeFilePath string, instanceType string, + defaultPerm os.FileMode) error { + // 创建Auth配置文件 + runtime.Logger.Info("start to create auth config file") + authConfFile, err := os.OpenFile(authConfFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultPerm) + defer authConfFile.Close() + if err != nil { + runtime.Logger.Error(fmt.Sprintf("create auth config file fail, error:%s", err)) + return fmt.Errorf("create auth config file fail, error:%s", err) + } + if _, err = authConfFile.WriteString(string(authConfFileContent)); err != nil { + runtime.Logger.Error(fmt.Sprintf("auth config file write content fail, error:%s", err)) + return fmt.Errorf("auth config file write content fail, error:%s", err) + } + runtime.Logger.Info("create auth config file successfully") + + // 修改配置文件属主 + runtime.Logger.Info("start to execute chown command for auth config file") + if _, err = util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", user, group, authConfFilePath), + "", nil, + 10*time.Second); err != nil { + runtime.Logger.Error(fmt.Sprintf("chown auth config file fail, error:%s", err)) + return fmt.Errorf("chown auth config file fail, error:%s", err) + } + runtime.Logger.Info("start to execute chown command for auth config file successfully") + + // 创建NoAuth配置文件 + runtime.Logger.Info("start to create no auth config file") + noAuthConfFile, err := os.OpenFile(noAuthConfFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultPerm) + defer noAuthConfFile.Close() + if err != nil { + runtime.Logger.Error(fmt.Sprintf("create no auth config file fail, error:%s", err)) + return fmt.Errorf("create no auth config file fail, error:%s", err) + } + if _, err = noAuthConfFile.WriteString(string(noAuthConfFileContent)); err != nil { + runtime.Logger.Error(fmt.Sprintf("auth no config file write content fail, error:%s", err)) + return fmt.Errorf("auth no config file write content fail, error:%s", err) + } + runtime.Logger.Info("create no auth config file successfully") + + // 修改配置文件属主 + runtime.Logger.Info("start to execute chown command for no auth config file") + if _, err = util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", user, group, noAuthConfFilePath), + "", nil, + 10*time.Second); err != nil { + runtime.Logger.Error(fmt.Sprintf("chown no auth config file fail, error:%s", err)) + return fmt.Errorf("chown no auth config file fail, error:%s", err) + } + runtime.Logger.Info("execute chown command for no auth config file successfully") + + // 创建key文件 + runtime.Logger.Info("start to create key file") + keyFile, err := os.OpenFile(keyFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) + defer keyFile.Close() + if err != nil { + runtime.Logger.Error(fmt.Sprintf("create key file fail, error:%s", err)) + return fmt.Errorf("create key file fail, error:%s", err) + } + key := GetMd5(keyFileContent) + if _, err = keyFile.WriteString(key); err != nil { + runtime.Logger.Error(fmt.Sprintf("key file write content fail, error:%s", err)) + return fmt.Errorf("key file write content fail, error:%s", err) + } + runtime.Logger.Info("create key file successfully") + + // 修改key文件属主 + runtime.Logger.Info("start to execute chown command for key file") + if _, err = util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", user, group, keyFilePath), + "", nil, + 10*time.Second); err != nil { + runtime.Logger.Error(fmt.Sprintf("chown key file fail, error:%s", err)) + return fmt.Errorf("chown key file fail, error:%s", err) + } + runtime.Logger.Info("execute chown command for key file successfully") + + // 创建dbType文件 + runtime.Logger.Info("start to create dbType file") + dbTypeFile, err := os.OpenFile(dbTypeFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultPerm) + defer dbTypeFile.Close() + if err != nil { + runtime.Logger.Error(fmt.Sprintf("create dbType file fail, error:%s", err)) + return fmt.Errorf("create dbType file fail, error:%s", err) + } + if _, err = dbTypeFile.WriteString(instanceType); err != nil { + runtime.Logger.Error(fmt.Sprintf("dbType file write content fail, error:%s", err)) + return fmt.Errorf("dbType file write content fail, error:%s", err) + } + runtime.Logger.Info("create dbType file successfully") + + // 修改dbType文件属主 + runtime.Logger.Info("start to execute chown command for dbType file") + if _, err = util.RunBashCmd( + fmt.Sprintf("chown -R %s.%s %s", user, group, dbTypeFilePath), + "", nil, + 10*time.Second); err != nil { + runtime.Logger.Error(fmt.Sprintf("chown dbType file fail, error:%s", err)) + return fmt.Errorf("chown dbType file fail, error:%s", err) + } + runtime.Logger.Info("execute chown command for dbType file successfully") + + return nil + +} + +// StartMongoProcess 启动进程 +func StartMongoProcess(binDir string, port int, user string, auth bool) error { + // 启动服务 + var cmd string + cmd = fmt.Sprintf("su %s -c \"%s %d %s\"", user, + filepath.Join(binDir, "mongodb", "bin", "start_mongo.sh"), + port, "noauth") + if auth == true { + cmd = fmt.Sprintf("su %s -c \"%s %d\"", user, + filepath.Join(binDir, "mongodb", "bin", "start_mongo.sh"), + port) + } + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + return err + } + return nil +} + +// ShutdownMongoProcess 关闭进程 +func ShutdownMongoProcess(user string, instanceType string, binDir string, dbpathDir string, port int) error { + var cmd string + cmd = fmt.Sprintf("su %s -c \"%s --shutdown --dbpath %s\"", + user, filepath.Join(binDir, "mongodb", "bin", "mongod"), dbpathDir) + if instanceType == "mongos" { + cmd = fmt.Sprintf("ps -ef|grep mongos |grep -v grep|grep %d|awk '{print $2}' | xargs kill -2", port) + } + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + return err + } + return nil +} + +// AddPathToProfile 把可执行文件路径写入/etc/profile +func AddPathToProfile(runtime *jobruntime.JobGenericRuntime, binDir string) error { + runtime.Logger.Info("start to add binary path in /etc/profile") + etcProfilePath := "/etc/profile" + addEtcProfile := fmt.Sprintf(` +if ! grep -i %s: %s; +then +echo "export PATH=%s:\$PATH" >> %s +fi`, filepath.Join(binDir, "mongodb", "bin"), etcProfilePath, filepath.Join(binDir, "mongodb", "bin"), etcProfilePath) + runtime.Logger.Info(addEtcProfile) + if _, err := util.RunBashCmd(addEtcProfile, "", nil, 10*time.Second); err != nil { + runtime.Logger.Error(fmt.Sprintf("binary path add in /etc/profile, error:%s", err)) + return fmt.Errorf("binary path add in /etc/profile, error:%s", err) + } + runtime.Logger.Info("add binary path in /etc/profile successfully") + return nil +} + +// AuthGetPrimaryInfo 获取primary节点信息 +func AuthGetPrimaryInfo(mongoBin string, username string, password string, ip string, port int) (string, + error) { + // 超时时间 + timeout := time.After(20 * time.Second) + for { + select { + case <-timeout: + return "", fmt.Errorf("get primary info timeout") + default: + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"rs.isMaster().primary\"", + mongoBin, username, password, ip, port) + result, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + return "", err + } + if strings.Replace(result, "\n", "", -1) == "" { + time.Sleep(1 * time.Second) + continue + } + primaryInfo := strings.Replace(result, "\n", "", -1) + return primaryInfo, nil + } + } +} + +// NoAuthGetPrimaryInfo 获取primary节点信息 +func NoAuthGetPrimaryInfo(mongoBin string, ip string, port int) (string, error) { + // 超时时间 + timeout := time.After(20 * time.Second) + for { + select { + case <-timeout: + return "", fmt.Errorf("get primary info timeout") + default: + cmd := fmt.Sprintf( + "%s --host %s --port %d --quiet --eval \"rs.isMaster().primary\"", + mongoBin, ip, port) + result, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + return "", err + } + if strings.Replace(result, "\n", "", -1) == "" { + time.Sleep(1 * time.Second) + continue + } + primaryInfo := strings.Replace(result, "\n", "", -1) + return primaryInfo, nil + } + + } +} + +// InitiateReplicasetGetPrimaryInfo 复制集初始化时判断 +func InitiateReplicasetGetPrimaryInfo(mongoBin string, ip string, port int) (string, error) { + cmd := fmt.Sprintf( + "%s --host %s --port %d --quiet --eval \"rs.isMaster().primary\"", + mongoBin, ip, port) + result, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + return "", err + } + primaryInfo := strings.Replace(result, "\n", "", -1) + return primaryInfo, nil +} + +// RemoveFile 删除文件 +func RemoveFile(filePath string) error { + cmd := fmt.Sprintf("rm -rf %s", filePath) + if _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second); err != nil { + return err + } + return nil +} + +// CreateFile 创建文件 +func CreateFile(path string) error { + installLockFile, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer installLockFile.Close() + return nil +} + +// AuthCheckUser 检查user是否存在 +func AuthCheckUser(mongoBin string, username string, password string, ip string, port int, authDb string, + checkUsername string) (bool, error) { + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"db.getMongo().getDB('%s').getUser('%s')\"", + mongoBin, username, password, ip, port, authDb, checkUsername) + result, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + return false, fmt.Errorf("get user info fail, error:%s", err) + } + if strings.Contains(result, checkUsername) == true { + return true, nil + } + + return false, nil +} + +// GetNodeInfo 获取mongod节点信息 _id int state int hidden bool priority int +func GetNodeInfo(mongoBin string, ip string, port int, username string, password string, + sourceIP string, sourcePort int) (bool, int, int, bool, int, []map[string]string, error) { + source := strings.Join([]string{sourceIP, strconv.Itoa(sourcePort)}, ":") + cmdStatus := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"rs.status().members\"", + mongoBin, username, password, ip, port) + cmdConf := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"rs.conf().members\"", + mongoBin, username, password, ip, port) + + // 获取状态 + result1, err := util.RunBashCmd( + cmdStatus, + "", nil, + 10*time.Second) + if err != nil { + return false, 0, 0, false, 0, nil, fmt.Errorf("get members status info fail, error:%s", err) + } + result1 = strings.Replace(result1, " ", "", -1) + result1 = strings.Replace(result1, "\n", "", -1) + result1 = strings.Replace(result1, "NumberLong(", "", -1) + result1 = strings.Replace(result1, "Timestamp(", "", -1) + result1 = strings.Replace(result1, "ISODate(", "", -1) + result1 = strings.Replace(result1, ",1)", "", -1) + result1 = strings.Replace(result1, ",3)", "", -1) + result1 = strings.Replace(result1, ",2)", "", -1) + result1 = strings.Replace(result1, ",6)", "", -1) + result1 = strings.Replace(result1, ",0)", "", -1) + result1 = strings.Replace(result1, ")", "", -1) + + // 获取配置 + result2, err := util.RunBashCmd( + cmdConf, + "", nil, + 10*time.Second) + if err != nil { + return false, 0, 0, false, 0, nil, fmt.Errorf("get members conf info fail, error:%s", err) + } + result2 = strings.Replace(result2, " ", "", -1) + result2 = strings.Replace(result2, "\n", "", -1) + result2 = strings.Replace(result2, "NumberLong(", "", -1) + result2 = strings.Replace(result2, "Timestamp(", "", -1) + result2 = strings.Replace(result2, "ISODate(", "", -1) + result2 = strings.Replace(result2, ",1)", "", -1) + result2 = strings.Replace(result2, ")", "", -1) + + var statusSlice []map[string]interface{} + var confSlice []map[string]interface{} + if err = json.Unmarshal([]byte(result1), &statusSlice); err != nil { + return false, 0, 0, false, 0, nil, fmt.Errorf("get members status info json.Unmarshal fail, error:%s", err) + } + if err = json.Unmarshal([]byte(result2), &confSlice); err != nil { + return false, 0, 0, false, 0, nil, fmt.Errorf("get members conf info json.Unmarshal fail, error:%s", err) + } + + // 格式化配置信息 + var memberInfo []map[string]string + for _, v := range statusSlice { + member := make(map[string]string) + member["name"] = v["name"].(string) + member["state"] = fmt.Sprintf("%1.0f", v["state"]) + for _, k := range confSlice { + if k["host"].(string) == member["name"] { + member["hidden"] = strconv.FormatBool(k["hidden"].(bool)) + break + } + } + memberInfo = append(memberInfo, member) + } + + var id int + var state int + var hidden bool + var priority int + flag := false + for _, key := range statusSlice { + if key["name"].(string) == source { + id, _ = strconv.Atoi(fmt.Sprintf("%1.0f", key["_id"])) + state, _ = strconv.Atoi(fmt.Sprintf("%1.0f", key["state"])) + flag = true + break + } + } + for _, key := range confSlice { + if key["host"].(string) == source { + hidden = key["hidden"].(bool) + priority, _ = strconv.Atoi(fmt.Sprintf("%1.0f", key["priority"])) + break + } + } + return flag, id, state, hidden, priority, memberInfo, nil + +} + +// AuthRsStepDown 主备切换 +func AuthRsStepDown(mongoBin string, ip string, port int, username string, password string) (bool, error) { + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"rs.stepDown()\"", + mongoBin, username, password, ip, port) + _, _ = util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + time.Sleep(time.Second * 3) + primaryInfo, err := AuthGetPrimaryInfo(mongoBin, username, password, ip, port) + if err != nil { + return false, err + } + if primaryInfo == strings.Join([]string{ip, strconv.Itoa(port)}, ":") { + return false, nil + } + + return true, nil +} + +// NoAuthRsStepDown 主备切换 +func NoAuthRsStepDown(mongoBin string, ip string, port int) (bool, error) { + cmd := fmt.Sprintf( + "%s --host %s --port %d --authenticationDatabase=admin --quiet --eval \"rs.stepDown()\"", + mongoBin, ip, port) + _, _ = util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + time.Sleep(time.Second * 3) + primaryInfo, err := NoAuthGetPrimaryInfo(mongoBin, ip, port) + if err != nil { + return false, err + } + if primaryInfo == strings.Join([]string{ip, strconv.Itoa(port)}, ":") { + return false, nil + } + return true, nil +} + +// CheckBalancer 检查balancer的值 +func CheckBalancer(mongoBin string, ip string, port int, username string, password string) (string, + error) { + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"sh.getBalancerState()\"", + mongoBin, username, password, ip, port) + result, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + return "", err + } + result = strings.Replace(result, "\n", "", -1) + return result, nil +} + +// GetProfilingLevel 获取profile级别 +func GetProfilingLevel(mongoBin string, ip string, port int, username string, password string, + dbName string) (int, error) { + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"db.getMongo().getDB('%s').getProfilingLevel()\"", + mongoBin, username, password, ip, port, dbName) + result, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + return -1, err + } + intResult, _ := strconv.Atoi(result) + return intResult, nil +} + +// SetProfilingLevel 设置profile级别 +func SetProfilingLevel(mongoBin string, ip string, port int, username string, password string, + dbName string, level int) error { + cmd := fmt.Sprintf( + "%s -u %s -p '%s' --host %s --port %d --authenticationDatabase=admin --quiet --eval \"db.getMongo().getDB('%s').setProfilingLevel(%d)\"", + mongoBin, username, password, ip, port, dbName, level) + _, err := util.RunBashCmd( + cmd, + "", nil, + 10*time.Second) + if err != nil { + return err + } + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_init_shell.go b/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_init_shell.go new file mode 100644 index 0000000000..6ed75a98f5 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_init_shell.go @@ -0,0 +1,165 @@ +package common + +// MongoShellInit 初始化os的shell脚本 +var MongoShellInit = `#!/bin/sh +# 新建用户 + +function _exit() { + rm $0 + exit +} +#handler nscd restart 默认使用mysql用户 +#如果存在mysql用户组就groupadd mysql -g 202 +egrep "^{{group}}" /etc/group >& /dev/null +if [ $? -ne 0 ] +then +groupadd {{group}} -g 2000 +fi +#考虑到可能上架已运行的机器,userdel有风险,不采用这种方法 +#如果存在user用户就删掉(因为有可能1)id不为30019,2)不存在home目录) +id {{user}} >& /dev/null +if [ $? -ne 0 ] +then + useradd -m -d /home/{{user}} -g 2000 -G users -u 2000 {{user}} + chage -M 99999 {{user}} + if [ ! -d /home/{{user}} ]; + then + mkdir -p /home/{{user}} + fi + chmod 755 /home/{{user}} + usermod -d /home/{{user}} {{user}} 2>/dev/null +fi +if [[ -z "$MONGO_DATA_DIR" ]] +then + echo "env MONGO_DATA_DIR cannot be empty" >&2 + exit -1 +fi +if [[ -z "$MONGO_BACKUP_DIR" ]] +then + echo "env MONGO_BACKUP_DIR cannot be empty" >&2 + exit -1 +fi + +if [ ! -d $MONGO_DATA_DIR ] +then + mkdir -p $MONGO_DATA_DIR +fi + +if [ ! -d $MONGO_BACKUP_DIR ] +then + mkdir -p $RMONGO_BACKUP_DIR +fi + +#添加mongo安装锁文件 +if [ ! -f $MONGO_DATA_DIR/mongoinstall.lock ] +then + touch $MONGO_DATA_DIR/mongoinstall.lock +fi + +#如果存在mysql用户,上面那一步会报错,也不会创建/home/mysql,所以判断下并创建/home/mysql +if [ ! -d /data ]; +then + ln -s $MONGO_BACKUP_DIR /data +fi +if [ ! -d /data1 ]; +then + ln -s $MONGO_DATA_DIR /data1 +fi +if [[ ! -d /data1/dbha ]] +then + mkdir -p /data1/dbha +fi +chown -R {{user}} /data1/dbha +if [[ ! -d /data/dbha ]] +then + mkdir -p /data/dbha +fi +chown -R {{user}} /data/dbha +if [[ ! -d /data/install ]] +then + mkdir -p /data/install + chown -R {{user}} /data/install +fi +if [[ ! -d $MONGO_BACKUP_DIR/dbbak ]] +then + mkdir -p $MONGO_BACKUP_DIR/dbbak + chown -R {{user}} $MONGO_BACKUP_DIR/dbbak +fi +chown -R {{user}} /home/{{user}} +chmod -R a+rwx /data/install +rm -rf /home/{{user}}/install +ln -s /data/install /home/{{user}}/install +chown -R {{user}} /home/{{user}}/install +#password="$2" +#password=$(echo "$2" | /home/mysql/install/lib/tools/base64 -d) +#echo "mysql:$password" | chpasswd +FOUND=$(grep 'ulimit -n 204800' /etc/profile) +if [ -z "$FOUND" ]; then + echo 'ulimit -n 204800' >> /etc/profile +fi +FOUND=$(grep 'export LC_ALL=en_US' /etc/profile) +if [ -z "$FOUND" ]; then + echo 'export LC_ALL=en_US' >> /etc/profile +fi +#FOUND=$(grep 'export PATH=/usr/local/mongodb/bin/:$PATH' /etc/profile) +#if [ -z "$FOUND" ]; then +# echo 'export PATH=/usr/local/mongodb/bin/:$PATH' >> /etc/profile +#fi +FOUND_umask=$(grep '^umask 022' /etc/profile) +if [ -z "$FOUND_umask" ]; then + echo 'umask 022' >> /etc/profile +fi +FOUND=$(grep 'vm.swappiness = 0' /etc/sysctl.conf) +if [ -z "$FOUND" ];then +echo "vm.swappiness = 0" >> /etc/sysctl.conf +fi +FOUND=$(grep 'kernel.pid_max = 200000' /etc/sysctl.conf) +if [ -z "$FOUND" ];then +echo "kernel.pid_max = 200000" >> /etc/sysctl.conf +fi + +FOUND=$(grep '{{user}} soft nproc 64000' /etc/security/limits.conf) +if [ -z "$FOUND" ];then +echo "{{user}} soft nproc 64000" >> /etc/security/limits.conf +fi +FOUND=$(grep '{{user}} hard nproc 64000' /etc/security/limits.conf) +if [ -z "$FOUND" ];then +echo "{{user}} hard nproc 64000" >> /etc/security/limits.conf +fi +FOUND=$(grep '{{user}} soft fsize unlimited' /etc/security/limits.conf) +if [ -z "$FOUND" ];then +echo "{{user}} soft fsize unlimited" >> /etc/security/limits.conf +fi +FOUND=$(grep '{{user}} hard fsize unlimited' /etc/security/limits.conf) +if [ -z "$FOUND" ];then +echo "{{user}} hard fsize unlimited" >> /etc/security/limits.conf +fi +FOUND=$(grep '{{user}} soft memlock unlimited' /etc/security/limits.conf) +if [ -z "$FOUND" ];then +echo "{{user}} soft memlock unlimited" >> /etc/security/limits.conf +fi +FOUND=$(grep '{{user}} hard memlock unlimited' /etc/security/limits.conf) +if [ -z "$FOUND" ];then +echo "{{user}} hard memlock unlimited" >> /etc/security/limits.conf +fi +FOUND=$(grep '{{user}} soft as unlimited' /etc/security/limits.conf) +if [ -z "$FOUND" ];then +echo "{{user}} soft as unlimited" >> /etc/security/limits.conf +fi +FOUND=$(grep '{{user}} hard as unlimited' /etc/security/limits.conf) +if [ -z "$FOUND" ];then +echo "{{user}} hard as unlimited" >> /etc/security/limits.conf +fi + +FOUND=$(grep 'session required pam_limits.so' /etc/pam.d/login) +if [ -z "$FOUND" ];then +echo "session required pam_limits.so" >> /etc/pam.d/login +fi + +FOUND=$(grep 'session required pam_limits.so' /etc/pam.d/su) +if [ -z "$FOUND" ];then +echo "session required pam_limits.so" >> /etc/pam.d/su +fi + +/sbin/sysctl -p +_exit` diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_user_conf.go b/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_user_conf.go new file mode 100644 index 0000000000..add72f1a02 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongo_user_conf.go @@ -0,0 +1,35 @@ +package common + +import "encoding/json" + +// MongoRole 角色 +type MongoRole struct { + Role string `json:"role"` + Db string `json:"db"` +} + +// MongoUser 用户 +type MongoUser struct { + User string `json:"user"` + Pwd string `json:"pwd"` + Roles []*MongoRole `json:"roles"` +} + +// NewMongoUser 生成结构体 +func NewMongoUser() *MongoUser { + return &MongoUser{} +} + +// GetContent 转成json +func (m *MongoUser) GetContent() (string, error) { + content, err := json.Marshal(m) + if err != nil { + return "", err + } + return string(content), nil +} + +// NewMongoRole 生成结构体 +func NewMongoRole() *MongoRole { + return &MongoRole{} +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongod_conf.go b/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongod_conf.go new file mode 100644 index 0000000000..7cc3629df2 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongod_conf.go @@ -0,0 +1,87 @@ +package common + +import ( + "gopkg.in/yaml.v2" +) + +// YamlMongoDBConf 3.0及以上配置文件 +type YamlMongoDBConf struct { + Storage struct { + DbPath string `yaml:"dbPath"` + Engine string `yaml:"engine"` + WiredTiger struct { + EngineConfig struct { + CacheSizeGB int `yaml:"cacheSizeGB"` + } `yaml:"engineConfig"` + } `yaml:"wiredTiger"` + } `yaml:"storage"` + Replication struct { + OplogSizeMB int `yaml:"oplogSizeMB"` + ReplSetName string `yaml:"replSetName"` + } `yaml:"replication"` + SystemLog struct { + LogAppend bool `yaml:"logAppend"` + Path string `yaml:"path"` + Destination string `yaml:"destination"` + } `yaml:"systemLog"` + ProcessManagement struct { + Fork bool `yaml:"fork"` + PidFilePath string `yaml:"pidFilePath"` + } `yaml:"processManagement"` + Net struct { + Port int `yaml:"port"` + BindIp string `yaml:"bindIp"` + WireObjectCheck bool `yaml:"wireObjectCheck"` + } `yaml:"net"` + OperationProfiling struct { + SlowOpThresholdMs int `yaml:"slowOpThresholdMs"` + } `yaml:"operationProfiling"` + Sharding struct { + ClusterRole string `yaml:"clusterRole,omitempty"` + } `yaml:"sharding,omitempty"` + Security struct { + KeyFile string `yaml:"keyFile,omitempty"` + } `yaml:"security,omitempty"` +} + +// NewYamlMongoDBConf 生成结构体 +func NewYamlMongoDBConf() *YamlMongoDBConf { + return &YamlMongoDBConf{} +} + +// GetConfContent 获取配置文件内容 +func (y *YamlMongoDBConf) GetConfContent() ([]byte, error) { + out, err := yaml.Marshal(y) + if err != nil { + return nil, err + } + return out, nil +} + +// IniNoAuthMongoDBConf 3.0以下配置文件 +var IniNoAuthMongoDBConf = `replSet={{replSet}} +dbpath={{dbpath}} +logpath={{logpath}} +pidfilepath={{pidfilepath}} +logappend=true +port={{port}} +bind_ip={{bind_ip}} +fork=true +nssize=16 +oplogSize={{oplogSize}} +{{instanceRole}} = true` + +// IniAuthMongoDBConf 3.0以下配置文件 +var IniAuthMongoDBConf = `replSet={{replSet}} +dbpath={{dbpath}} +logpath={{logpath}} +pidfilepath={{pidfilepath}} +logappend=true +port={{port}} +bind_ip={{bind_ip}} +keyFile={{keyFile}} +fork=true +nssize=16 +oplogSize={{oplogSize}} +{{instanceRole}} = true +` diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongos_conf.go b/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongos_conf.go new file mode 100644 index 0000000000..66f791a1ca --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/common/mongos_conf.go @@ -0,0 +1,46 @@ +package common + +import ( + "gopkg.in/yaml.v2" +) + +// YamlMongoSConf 4.0及以上配置文件 +type YamlMongoSConf struct { + Sharding struct { + ConfigDB string `yaml:"configDB"` + } `yaml:"sharding"` + SystemLog struct { + LogAppend bool `yaml:"logAppend"` + Path string `yaml:"path"` + Destination string `yaml:"destination"` + } `yaml:"systemLog"` + ProcessManagement struct { + Fork bool `yaml:"fork"` + PidFilePath string `yaml:"pidFilePath"` + } `yaml:"processManagement"` + Net struct { + Port int `yaml:"port"` + BindIp string `yaml:"bindIp"` + WireObjectCheck bool `yaml:"wireObjectCheck"` + } `yaml:"net"` + OperationProfiling struct { + SlowOpThresholdMs int `yaml:"slowOpThresholdMs,omitempty"` + } `yaml:"operationProfiling,omitempty"` + Security struct { + KeyFile string `yaml:"keyFile,omitempty"` + } `yaml:"security,omitempty"` +} + +// NewYamlMongoSConf 生成结构体 +func NewYamlMongoSConf() *YamlMongoSConf { + return &YamlMongoSConf{} +} + +// GetConfContent 获取配置文件内容 +func (y *YamlMongoSConf) GetConfContent() ([]byte, error) { + out, err := yaml.Marshal(y) + if err != nil { + return nil, err + } + return out, nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/common/repliccaset_member_conf.go b/dbm-services/mongo/db-tools/dbactuator/pkg/common/repliccaset_member_conf.go new file mode 100644 index 0000000000..731c652038 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/common/repliccaset_member_conf.go @@ -0,0 +1,24 @@ +package common + +import "encoding/json" + +// ReplicasetMemberAdd 复制集状态 +type ReplicasetMemberAdd struct { + Host string `json:"host"` // ip:port + Hidden bool `json:"hidden"` + Priority int `json:"priority"` +} + +// NewReplicasetMemberAdd 生成结构体 +func NewReplicasetMemberAdd() *ReplicasetMemberAdd { + return &ReplicasetMemberAdd{} +} + +// GetJson 获取json格式 +func (t *ReplicasetMemberAdd) GetJson() (string, error) { + byteInfo, err := json.Marshal(t) + if err != nil { + return "", err + } + return string(byteInfo), nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/consts/consts.go b/dbm-services/mongo/db-tools/dbactuator/pkg/consts/consts.go new file mode 100644 index 0000000000..8f1e4f0b98 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/consts/consts.go @@ -0,0 +1,277 @@ +// Package consts 常量 +package consts + +const ( + // TendisTypePredixyRedisCluster predixy + RedisCluster架构 + TendisTypePredixyRedisCluster = "PredixyRedisCluster" + // TendisTypePredixyTendisplusCluster predixy + TendisplusCluster架构 + TendisTypePredixyTendisplusCluster = "PredixyTendisplusCluster" + // TendisTypeTwemproxyRedisInstance twemproxy + RedisInstance架构 + TendisTypeTwemproxyRedisInstance = "TwemproxyRedisInstance" + // TendisTypeTwemproxyTendisplusInstance twemproxy+ TendisplusInstance架构 + TendisTypeTwemproxyTendisplusInstance = "TwemproxyTendisplusInstance" + // TendisTypeTwemproxyTendisSSDInstance twemproxy+ TendisSSDInstance架构 + TendisTypeTwemproxyTendisSSDInstance = "TwemproxyTendisSSDInstance" + // TendisTypeRedisInstance RedisCache 主从版 + TendisTypeRedisInstance = "RedisInstance" + // TendisTypeTendisplusInsance Tendisplus 主从版 + TendisTypeTendisplusInsance = "TendisplusInstance" + // TendisTypeTendisSSDInsance TendisSSD 主从版 + TendisTypeTendisSSDInsance = "TendisSSDInstance" + // TendisTypeRedisCluster 原生RedisCluster 架构 + TendisTypeRedisCluster = "RedisCluster" + // TendisTypeTendisplusCluster TendisplusCluster架构 + TendisTypeTendisplusCluster = "TendisplusCluster" +) + +// kibis of bits +const ( + Byte = 1 << (iota * 10) + KiByte + MiByte + GiByte + TiByte + EiByte +) + +const ( + // RedisMasterRole redis role master + RedisMasterRole = "master" + // RedisSlaveRole redis role slave + RedisSlaveRole = "slave" + + // RedisNoneRole none role + RedisNoneRole = "none" + + // MasterLinkStatusUP up status + MasterLinkStatusUP = "up" + // MasterLinkStatusDown down status + MasterLinkStatusDown = "down" + + // TendisSSDIncrSyncState IncrSync state + TendisSSDIncrSyncState = "IncrSync" + // TendisSSDReplFollowtate REPL_FOLLOW state + TendisSSDReplFollowtate = "REPL_FOLLOW" +) + +const ( + // RedisLinkStateConnected redis connection status connected + RedisLinkStateConnected = "connected" + // RedisLinkStateDisconnected redis connection status disconnected + RedisLinkStateDisconnected = "disconnected" +) + +const ( + // NodeStatusPFail Node is in PFAIL state. Not reachable for the node you are contacting, but still logically reachable + NodeStatusPFail = "fail?" + // NodeStatusFail Node is in FAIL state. It was not reachable for multiple nodes that promoted the PFAIL state to FAIL + NodeStatusFail = "fail" + // NodeStatusHandshake Untrusted node, we are handshaking. + NodeStatusHandshake = "handshake" + // NodeStatusNoAddr No address known for this node + NodeStatusNoAddr = "noaddr" + // NodeStatusNoFlags no flags at all + NodeStatusNoFlags = "noflags" +) + +const ( + // ClusterStateOK command 'cluster info',cluster_state + ClusterStateOK = "ok" + // ClusterStateFail command 'cluster info',cluster_state + ClusterStateFail = "fail" +) +const ( + // DefaultMinSlots 0 + DefaultMinSlots = 0 + // DefaultMaxSlots 16383 + DefaultMaxSlots = 16383 + + // TwemproxyMaxSegment twemproxy max segment + TwemproxyMaxSegment = 419999 + // TotalSlots 集群总槽数 + TotalSlots = 16384 +) + +// time layout +const ( + UnixtimeLayout = "2006-01-02 15:04:05" + FilenameTimeLayout = "20060102-150405" + FilenameDayLayout = "20060102" +) + +// account +const ( + MysqlAaccount = "mysql" + MysqlGroup = "mysql" + OSAccount = "mysql" + OSGroup = "mysql" +) + +// path dirs +const ( + UsrLocal = "/usr/local" + PackageSavePath = "/data/install" + Data1Path = "/data1" + DataPath = "/data" + DbaReportSaveDir = "/home/mysql/dbareport/" + RedisReportSaveDir = "/home/mysql/dbareport/redis/" + ExporterConfDir = "/home/mysql/.exporter" + RedisReportLeftDay = 15 +) + +// tool path +const ( + DbToolsPath = "/home/mysql/dbtools" + RedisShakeBin = "/home/mysql/dbtools/redis-shake" + RedisSafeDeleteToolBin = "/home/mysql/dbtools/redisSafeDeleteTool" + LdbTendisplusBin = "/home/mysql/dbtools/ldb_tendisplus" + TredisverifyBin = "/home/mysql/dbtools/tredisverify" + TredisBinlogBin = "/home/mysql/dbtools/tredisbinlog" + TredisDumpBin = "/home/mysql/dbtools/tredisdump" + NetCatBin = "/home/mysql/dbtools/netcat" + TendisKeyLifecycleBin = "/home/mysql/dbtools/tendis-key-lifecycle" + ZkWatchBin = "/home/mysql/dbtools/zkwatch" + ZstdBin = "/home/mysql/dbtools/zstd" + LzopBin = "/home/mysql/dbtools/lzop" + LdbWithV38Bin = "/home/mysql/dbtools/ldb_with_len.3.8" + LdbWithV513Bin = "/home/mysql/dbtools/ldb_with_len.5.13" + MyRedisCaptureBin = "/home/mysql/dbtools/myRedisCapture" + BinlogToolTendisplusBin = "/home/mysql/dbtools/binlogtool_tendisplus" + RedisCliBin = "/home/mysql/dbtools/redis-cli" + TendisDataCheckBin = "/home/mysql/dbtools/tendisDataCheck" + RedisDiffKeysRepairerBin = "/home/mysql/dbtools/redisDiffKeysRepairer" +) + +// bk-dbmon path +const ( + BkDbmonPath = "/home/mysql/bk-dbmon" + BkDbmonBin = "/home/mysql/bk-dbmon/bk-dbmon" + BkDbmonConfFile = "/home/mysql/bk-dbmon/dbmon-config.yaml" + BkDbmonPort = 6677 + BkDbmonHTTPAddress = "127.0.0.1:6677" +) + +// backup +const ( + NormalBackupType = "normal_backup" + ForeverBackupType = "forever_backup" + BackupClient = "/usr/local/bin/backup_client" + BackupTarSplitSize = "8G" + RedisFullBackupTAG = "REDIS_FULL" + RedisBinlogTAG = "REDIS_BINLOG" + RedisForeverBackupTAG = "DBFILE" + RedisFullBackupReportType = "redis_fullbackup" + RedisBinlogBackupReportType = "redis_binlogbackup" + DoingRedisFullBackFileList = "redis_backup_file_list_%d_doing" + DoneRedisFullBackFileList = "redis_backup_file_list_%d_done" + DoingRedisBinlogFileList = "redis_binlog_file_list_%d_doing" + DoneRedisBinlogFileList = "redis_binlog_file_list_%d_done" + RedisFullbackupRepoter = "redis_fullbackup_%s.log" + RedisBinlogRepoter = "redis_binlog_%s.log" + BackupStatusStart = "start" + BackupStatusRunning = "running" + BackupStatusToBakSystemStart = "to_backup_system_start" + BackupStatusToBakSystemFailed = "to_backup_system_failed" + BackupStatusToBakSysSuccess = "to_backup_system_success" + BackupStatusFailed = "failed" + BackupStatusLocalSuccess = "local_success" +) + +// meta role +const ( + MetaRoleRedisMaster = "redis_master" + MetaRoleRedisSlave = "redis_slave" +) + +// proxy operations +const ( + ProxyStart = "proxy_open" + ProxyStop = "proxy_close" + ProxyRestart = "proxy_restart" + ProxyShutdown = "proxy_shutdown" +) + +const ( + // FlushDBRename .. + FlushDBRename = "cleandb" + // CacheFlushAllRename .. + CacheFlushAllRename = "cleanall" + // SSDFlushAllRename .. + SSDFlushAllRename = "flushalldisk" + // KeysRename .. + KeysRename = "mykeys" + // ConfigRename .. + ConfigRename = "confxx" +) + +// IsClusterDbType 存储端是否是cluster类型 +func IsClusterDbType(dbType string) bool { + if dbType == TendisTypePredixyRedisCluster || + dbType == TendisTypePredixyTendisplusCluster || + dbType == TendisTypeRedisCluster || + dbType == TendisTypeTendisplusCluster { + return true + } + return false +} + +// IsRedisInstanceDbType 存储端是否是cache类型 +func IsRedisInstanceDbType(dbType string) bool { + if dbType == TendisTypePredixyRedisCluster || + dbType == TendisTypeTwemproxyRedisInstance || + dbType == TendisTypeRedisInstance || + dbType == TendisTypeRedisCluster { + return true + } + return false +} + +// IsTwemproxyClusterType 检查proxy是否为Twemproxy +func IsTwemproxyClusterType(dbType string) bool { + if dbType == TendisTypeTwemproxyRedisInstance || + dbType == TendisTypeTwemproxyTendisSSDInstance || + dbType == TendisTypeTwemproxyTendisplusInstance { + return true + } + return false +} + +// IsTendisplusInstanceDbType 存储端是否是tendisplus类型 +func IsTendisplusInstanceDbType(dbType string) bool { + if dbType == TendisTypePredixyTendisplusCluster || + dbType == TendisTypeTwemproxyTendisplusInstance || + dbType == TendisTypeTendisplusInsance || + dbType == TendisTypeTendisplusCluster { + return true + } + return false +} + +// IsTendisSSDInstanceDbType 存储端是否是tendisSSD类型 +func IsTendisSSDInstanceDbType(dbType string) bool { + if dbType == TendisTypeTwemproxyTendisSSDInstance || + dbType == TendisTypeTendisSSDInsance { + return true + } + return false +} + +// IsAllowFlushMoreDB 是否支持flush 多DB +func IsAllowFlushMoreDB(dbType string) bool { + if dbType == TendisTypeRedisInstance || + dbType == TendisTypeTendisplusInsance { + return true + } + return false +} + +// IsAllowRandomkey 是否支持randomkey命令 +func IsAllowRandomkey(dbType string) bool { + if dbType == TendisTypePredixyTendisplusCluster || + dbType == TendisTypeTwemproxyTendisplusInstance || + dbType == TendisTypeTendisplusInsance || + dbType == TendisTypeTendisplusCluster { + return false + } + return true +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/consts/data_dir.go b/dbm-services/mongo/db-tools/dbactuator/pkg/consts/data_dir.go new file mode 100644 index 0000000000..c8c7ec6c0f --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/consts/data_dir.go @@ -0,0 +1,325 @@ +package consts + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" +) + +// fileExists 检查目录是否已经存在 +func fileExists(path string) bool { + _, err := os.Stat(path) + if err != nil { + return os.IsExist(err) + } + return true +} + +// IsMountPoint2 Determine if a directory is a mountpoint, by comparing the device for the directory +// with the device for it's parent. If they are the same, it's not a mountpoint, if they're +// different, it is. +// reference: https://github.com/cnaize/kubernetes/blob/master/pkg/util/mount/mountpoint_unix.go#L29 +// 该函数与util/util.go 中 IsMountPoint()相同,但package consts 不建议依赖其他模块故拷贝了实现 +func IsMountPoint2(file string) bool { + stat, err := os.Stat(file) + if err != nil { + return false + } + rootStat, err := os.Lstat(file + "/..") + if err != nil { + return false + } + // If the directory has the same device as parent, then it's not a mountpoint. + return stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev +} + +// SetRedisDataDir 设置环境变量 REDIS_DATA_DIR,并持久化到/etc/profile中 +// 如果函数参数 dataDir 不为空,则 REDIS_DATA_DIR = {dataDir} +// 否则,如果环境变量 REDIS_DATA_DIR 不为空,则直接读取; +// 否则,如果 /data1/redis 存在, 则 REDIS_DATA_DIR=/data1 +// 否则,如果 /data/redis, 则 REDIS_DATA_DIR=/data +// 否则,如果 /data1 是挂载点, 则 REDIS_DATA_DIR=/data1 +// 否则,如果 /data 是挂载点, 则 REDIS_DATA_DIR=/data +// 否则,REDIS_DATA_DIR=/data1 +func SetRedisDataDir(dataDir string) (err error) { + if dataDir == "" { + envDir := os.Getenv("REDIS_DATA_DIR") + if envDir != "" { // 环境变量 REDIS_DATA_DIR 不为空 + dataDir = envDir + } else { + if fileExists(filepath.Join(Data1Path, "redis")) { + // /data1/redis 存在 + dataDir = Data1Path + } else if fileExists(filepath.Join(DataPath, "redis")) { + // /data/redis 存在 + dataDir = DataPath + } else if IsMountPoint2(Data1Path) { + // /data1是挂载点 + dataDir = Data1Path + } else if IsMountPoint2(DataPath) { + // /data是挂载点 + dataDir = DataPath + } else { + // 函数参数 dataDir为空, 环境变量 REDIS_DATA_DIR 为空 + // /data1 和 /data 均不是挂载点 + // 强制指定 REDIS_DATA_DIR=/data1 + dataDir = Data1Path + } + } + } + dataDir = strings.TrimSpace(dataDir) + var ret []byte + shCmd := fmt.Sprintf(` +ret=$(grep '^export REDIS_DATA_DIR=' /etc/profile) +if [[ -z $ret ]] +then +echo "export REDIS_DATA_DIR=%s">>/etc/profile +fi + `, dataDir) + ret, err = exec.Command("bash", "-c", shCmd).Output() + if err != nil { + err = fmt.Errorf("SetRedisDataDir failed,err:%v,ret:%s,shCmd:%s", err, string(ret), shCmd) + return + } + os.Setenv("REDIS_DATA_DIR", dataDir) + return nil +} + +// GetRedisDataDir 获取环境变量 REDIS_DATA_DIR,不为空直接返回, +// 否则,如果目录 /data1/redis存在,返回 /data1; +// 否则,如果目录 /data/redis存在,返回 /data; +// 否则,返回 /data1 +func GetRedisDataDir() string { + dataDir := os.Getenv("REDIS_DATA_DIR") + if dataDir == "" { + if fileExists(filepath.Join(Data1Path, "redis")) { + // /data1/redis 存在 + dataDir = Data1Path + } else if fileExists(filepath.Join(DataPath, "redis")) { + // /data/redis 存在 + dataDir = DataPath + } else { + dataDir = Data1Path + } + } + return dataDir +} + +// SetRedisBakcupDir 设置环境变量 REDIS_BACKUP_DIR ,并持久化到/etc/profile中 +// 如果函数参数 backupDir 不为空,则 REDIS_BACKUP_DIR = {backupDir} +// 否则,如果环境变量 REDIS_BACKUP_DIR 不为空,则直接读取; +// 否则,如果 /data/dbbak 存在, 则 REDIS_BACKUP_DIR=/data +// 否则,如果 /data1/dbbak 存在, 则 REDIS_BACKUP_DIR=/data1 +// 否则,如果 /data 是挂载点, 则 REDIS_BACKUP_DIR=/data +// 否则,如果 /data1 是挂载点, 则 REDIS_BACKUP_DIR=/data1 +// 否则,REDIS_BACKUP_DIR=/data +func SetRedisBakcupDir(backupDir string) (err error) { + if backupDir == "" { + envDir := os.Getenv("REDIS_BACKUP_DIR") + if envDir != "" { + backupDir = envDir + } else { + if fileExists(filepath.Join(DataPath, "dbbak")) { + // /data/dbbak 存在 + backupDir = DataPath + } else if fileExists(filepath.Join(Data1Path, "dbbak")) { + // /data1/dbbak 存在 + backupDir = Data1Path + } else if IsMountPoint2(DataPath) { + // /data是挂载点 + backupDir = DataPath + } else if IsMountPoint2(Data1Path) { + // /data1是挂载点 + backupDir = Data1Path + } else { + // 函数参数 backupDir 为空, 环境变量 REDIS_BACKUP_DIR 为空 + // /data1 和 /data 均不是挂载点 + // 强制指定 REDIS_BACKUP_DIR=/data + backupDir = DataPath + } + } + } + backupDir = strings.TrimSpace(backupDir) + var ret []byte + shCmd := fmt.Sprintf(` +ret=$(grep '^export REDIS_BACKUP_DIR=' /etc/profile) +if [[ -z $ret ]] +then +echo "export REDIS_BACKUP_DIR=%s">>/etc/profile +fi + `, backupDir) + ret, err = exec.Command("bash", "-c", shCmd).Output() + if err != nil { + err = fmt.Errorf("SetRedisBakcupDir failed,err:%v,ret:%s", err, string(ret)) + return + } + os.Setenv("REDIS_BACKUP_DIR", backupDir) + return nil +} + +// GetRedisBackupDir 获取环境变量 REDIS_BACKUP_DIR,默认值 /data +// 否则,如果目录 /data/dbbak 存在,返回 /data; +// 否则,如果目录 /data1/dbbak 存在,返回 /data1; +// 否则,返回 /data +func GetRedisBackupDir() string { + dataDir := os.Getenv("REDIS_BACKUP_DIR") + if dataDir == "" { + if fileExists(filepath.Join(DataPath, "dbbak")) { + // /data/dbbak 存在 + dataDir = DataPath + } else if fileExists(filepath.Join(Data1Path, "dbbak")) { + // /data1/dbbak 存在 + dataDir = Data1Path + } else { + dataDir = DataPath + } + } + return dataDir +} + +// SetMongoDataDir 设置环境变量 MONGO_DATA_DIR,并持久化到/etc/profile中 +// 如果函数参数 dataDir 不为空,则 MONGO_DATA_DIR = {dataDir} +// 否则,如果环境变量 MONGO_DATA_DIR 不为空,则直接读取; +// 否则,如果 /data1/redis 存在, 则 MONGO_DATA_DIR=/data1 +// 否则,如果 /data/redis, 则 MONGO_DATA_DIR=/data +// 否则,如果 /data1 是挂载点, 则 MONGO_DATA_DIR=/data1 +// 否则,如果 /data 是挂载点, 则 MONGO_DATA_DIR=/data +// 否则,MONGO_DATA_DIR=/data1 +func SetMongoDataDir(dataDir string) (err error) { + if dataDir == "" { + envDir := os.Getenv("MONGO_DATA_DIR") + if envDir != "" { // 环境变量 MONGO_DATA_DIR 不为空 + dataDir = envDir + } else { + if fileExists(filepath.Join(Data1Path, "mongodata")) { + // /data1/mongodata 存在 + dataDir = Data1Path + } else if fileExists(filepath.Join(DataPath, "mongodata")) { + // /data/mongodata 存在 + dataDir = DataPath + } else if IsMountPoint2(Data1Path) { + // /data1是挂载点 + dataDir = Data1Path + } else if IsMountPoint2(DataPath) { + // /data是挂载点 + dataDir = DataPath + } else { + // 函数参数 dataDir为空, 环境变量 MONGO_DATA_DIR 为空 + // /data1 和 /data 均不是挂载点 + // 强制指定 MONGO_DATA_DIR=/data1 + dataDir = Data1Path + } + } + } + dataDir = strings.TrimSpace(dataDir) + var ret []byte + shCmd := fmt.Sprintf(` +ret=$(grep '^export MONGO_DATA_DIR=' /etc/profile) +if [[ -z $ret ]] +then +echo "export MONGO_DATA_DIR=%s">>/etc/profile +fi + `, dataDir) + ret, err = exec.Command("bash", "-c", shCmd).Output() + if err != nil { + err = fmt.Errorf("SetMongoDataDir failed,err:%v,ret:%s,shCmd:%s", err, string(ret), shCmd) + return + } + os.Setenv("MONGO_DATA_DIR", dataDir) + return nil +} + +// GetMongoDataDir 获取环境变量 MONGO_DATA_DIR,不为空直接返回, +// 否则,如果目录 /data1/mongodata存在,返回 /data1; +// 否则,如果目录 /data/mongodata存在,返回 /data; +// 否则,返回 /data1 +func GetMongoDataDir() string { + dataDir := os.Getenv("MONGO_DATA_DIR") + if dataDir == "" { + if fileExists(filepath.Join(Data1Path, "mongodata")) { + // /data1/mongodata 存在 + dataDir = Data1Path + } else if fileExists(filepath.Join(DataPath, "mongodata")) { + // /data/mongodata 存在 + dataDir = DataPath + } else { + dataDir = Data1Path + } + } + return dataDir +} + +// SetMongoBackupDir 设置环境变量 MONGO_BACKUP_DIR ,并持久化到/etc/profile中 +// 如果函数参数 backupDir 不为空,则 MONGO_BACKUP_DIR = {backupDir} +// 否则,如果环境变量 MONGO_BACKUP_DIR 不为空,则直接读取; +// 否则,如果 /data/dbbak 存在, 则 MONGO_BACKUP_DIR=/data +// 否则,如果 /data1/dbbak 存在, 则 MONGO_BACKUP_DIR=/data1 +// 否则,如果 /data 是挂载点, 则 MONGO_BACKUP_DIR=/data +// 否则,如果 /data1 是挂载点, 则 MONGO_BACKUP_DIR=/data1 +// 否则,MONGO_BACKUP_DIR=/data +func SetMongoBackupDir(backupDir string) (err error) { + if backupDir == "" { + envDir := os.Getenv("MONGO_BACKUP_DIR") + if envDir != "" { + backupDir = envDir + } else { + if fileExists(filepath.Join(DataPath, "dbbak")) { + // /data/dbbak 存在 + backupDir = DataPath + } else if fileExists(filepath.Join(Data1Path, "dbbak")) { + // /data1/dbbak 存在 + backupDir = Data1Path + } else if IsMountPoint2(DataPath) { + // /data是挂载点 + backupDir = DataPath + } else if IsMountPoint2(Data1Path) { + // /data1是挂载点 + backupDir = Data1Path + } else { + // 函数参数 backupDir 为空, 环境变量 MONGO_BACKUP_DIR 为空 + // /data1 和 /data 均不是挂载点 + // 强制指定 MONGO_BACKUP_DIR=/data + backupDir = DataPath + } + } + } + backupDir = strings.TrimSpace(backupDir) + var ret []byte + shCmd := fmt.Sprintf(` +ret=$(grep '^export MONGO_BACKUP_DIR=' /etc/profile) +if [[ -z $ret ]] +then +echo "export MONGO_BACKUP_DIR=%s">>/etc/profile +fi + `, backupDir) + ret, err = exec.Command("bash", "-c", shCmd).Output() + if err != nil { + err = fmt.Errorf("SetMongoBakcupDir failed,err:%v,ret:%s", err, string(ret)) + return + } + os.Setenv("MONGO_BACKUP_DIR", backupDir) + return nil +} + +// GetMongoBackupDir 获取环境变量 MONGO_BACKUP_DIR,默认值 /data +// 否则,如果目录 /data/dbbak 存在,返回 /data; +// 否则,如果目录 /data1/dbbak 存在,返回 /data1; +// 否则,返回 /data +func GetMongoBackupDir() string { + dataDir := os.Getenv("MONGO_BACKUP_DIR") + if dataDir == "" { + if fileExists(filepath.Join(DataPath, "dbbak")) { + // /data/dbbak 存在 + dataDir = DataPath + } else if fileExists(filepath.Join(Data1Path, "dbbak")) { + // /data1/dbbak 存在 + dataDir = Data1Path + } else { + dataDir = DataPath + } + } + return dataDir +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/consts/dts.go b/dbm-services/mongo/db-tools/dbactuator/pkg/consts/dts.go new file mode 100644 index 0000000000..342f035d3d --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/consts/dts.go @@ -0,0 +1,25 @@ +package consts + +// dts type +const ( + DtsTypeOneAppDiffCluster = "one_app_diff_cluster" // 一个业务下的不同集群 + DtsTypeDiffAppDiffCluster = "diff_app_diff_cluster" // 不同业务下的不同集群 + DtsTypeSyncToOtherSystem = "sync_to_other_system" // 同步到其他系统,如迁移到腾讯云 + DtsTypeUserBuiltToDbm = "user_built_to_dbm" // 用户自建redis到dbm系统 +) + +// IsDtsTypeSrcClusterBelongDbm (该dst类型中)源集群是否属于dbm系统 +func IsDtsTypeSrcClusterBelongDbm(dtsType string) bool { + if dtsType == DtsTypeOneAppDiffCluster || + dtsType == DtsTypeDiffAppDiffCluster || + dtsType == DtsTypeSyncToOtherSystem { + return true + } + return false +} + +// dts datacheck mode +const ( + DtsDataCheckByKeysFileMode = "bykeysfile" // 基于key提取结果,做数据校验 + DtsDataCheckByScanMode = "byscan" // 通过scan命令获取key名,做数据校验 +) diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/consts/test.go b/dbm-services/mongo/db-tools/dbactuator/pkg/consts/test.go new file mode 100644 index 0000000000..8dd5594c3e --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/consts/test.go @@ -0,0 +1,94 @@ +package consts + +import "fmt" + +// test consts +const ( + // ----- tendisplus master指定的端口范围 [11000,11999] ------ + // ----- tendisplus slave指定的端口范围 [12000,12999] ------ + // TestTendisPlusMasterStartPort master start port + TestTendisPlusMasterStartPort = 11000 + // TestTendisPlusSlaveStartPort slave start port + TestTendisPlusSlaveStartPort = 12000 + + // ExpansionTestTendisPlusMasterStartPort master start port + ExpansionTestTendisPlusMasterStartPort = 11100 + // ExpansionTestTendisPlusSlaveStartPort slave start port + ExpansionTestTendisPlusSlaveStartPort = 12100 + + // SlotTestTendisPlusMasterPort master start port + SlotTestTendisPlusMasterPort = 11200 + // SLotTestTendisPlusSlaveStart slave start port + SLotTestTendisPlusSlaveStart = 12200 + // SlotsMigrateTest 指定迁移slot + SlotsMigrateTest = "0-100" + + // TestSyncTendisPlusMasterStartPort make sync /redo slave + TestSyncTendisPlusMasterStartPort = 11300 + // TestSyncTendisPlusSlaveStartPort make sync / + TestSyncTendisPlusSlaveStartPort = 12300 + + // ----- cache redis master指定的端口范围 [13000,13999] ------ + // ----- cache redis slave指定的端口范围 [14000,14999] ------ + + // TestRedisMasterStartPort master start port + TestRedisMasterStartPort = 13000 + // TestRedisSlaveStartPort slave start port + TestRedisSlaveStartPort = 14000 + + // TestSyncRedisMasterStartPort make sync /redo slave + TestSyncRedisMasterStartPort = 13300 + // TestSyncRedisSlaveStartPort make sync / + TestSyncRedisSlaveStartPort = 14300 + + // ----- tendisssd master指定的端口范围 [14000,14999] ------ + // ----- tendisssd slave指定的端口范围 [15000,15999] ------ + + // TestTendisSSDMasterStartPort master start port + TestTendisSSDMasterStartPort = 15000 + // TestTendisSSDSlaveStartPort slave start port + TestTendisSSDSlaveStartPort = 16000 + + // TestTwemproxyPort twemproxy port + TestTwemproxyPort = 50100 + // TestPredixyPort predixy port + TestPredixyPort = 50200 + // TestSSDClusterTwemproxyPort twemproxy port + TestSSDClusterTwemproxyPort = 50300 + + // TestRedisInstanceNum instance number + TestRedisInstanceNum = 4 + + // ExpansionTestRedisInstanceNum instance number + ExpansionTestRedisInstanceNum = 2 + // SLotTestRedisInstanceNum instance number + SLotTestRedisInstanceNum = 1 +) +const ( + // RedisTestPasswd redis test password + RedisTestPasswd = "redisPassTest" + // ProxyTestPasswd proxy test password + ProxyTestPasswd = "proxyPassTest" +) + +// test uid/rootid/nodeid +const ( + TestUID = 1111 + TestRootID = 2222 + TestNodeID = 3333 +) + +var ( + // ActuatorTestCmd actuator测试命令 + ActuatorTestCmd = fmt.Sprintf( + // NOCC:tosa/linelength(设计如此) + "cd %s && ./dbactuator_redis --uid=%d --root_id=%d --node_id=%d --version_id=v1 --atom-job-list=%%q --payload=%%q --payload-format=raw", + PackageSavePath, TestUID, TestRootID, TestNodeID) +) + +const ( + // PayloadFormatRaw raw + PayloadFormatRaw = "raw" + // PayloadFormatBase64 base64 + PayloadFormatBase64 = "base64" +) diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/consts/user.go b/dbm-services/mongo/db-tools/dbactuator/pkg/consts/user.go new file mode 100644 index 0000000000..f19fa9bb06 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/consts/user.go @@ -0,0 +1,80 @@ +package consts + +import ( + "fmt" + "os" + "os/exec" +) + +// SetProcessUser 设置os用户 +func SetProcessUser(user string) error { + // 如果有user参数,设置环境变量 + if user != "" { + envUser := os.Getenv("PROCESS_EXEC_USER") + if envUser == user { + return nil + } + envUser = user + var ret []byte + shCmd := fmt.Sprintf(` +ret=$(grep '^export PROCESS_EXEC_USER=' /etc/profile) +if [[ -z $ret ]] +then +echo "export PROCESS_EXEC_USER=%s">>/etc/profile +fi + `, envUser) + ret, err := exec.Command("bash", "-c", shCmd).Output() + if err != nil { + err = fmt.Errorf("SetProcessUser failed,err:%v,ret:%s", err, string(ret)) + return err + } + os.Setenv("PROCESS_EXEC_USER", envUser) + } + return nil +} + +// GetProcessUser 获取os用户 +func GetProcessUser() string { + envUser := os.Getenv("PROCESS_EXEC_USER") + if envUser == "" { + return OSAccount + } + return envUser +} + +// SetProcessUserGroup 设置os用户Group +func SetProcessUserGroup(group string) error { + // 如果有user参数,设置环境变量 + if group != "" { + envGroup := os.Getenv("PROCESS_EXEC_USER_GROUP") + if envGroup == group { + return nil + } + envGroup = group + var ret []byte + shCmd := fmt.Sprintf(` +ret=$(grep '^export PROCESS_EXEC_USER_GROUP=' /etc/profile) +if [[ -z $ret ]] +then +echo "export PROCESS_EXEC_USER_GROUP=%s">>/etc/profile +fi + `, envGroup) + ret, err := exec.Command("bash", "-c", shCmd).Output() + if err != nil { + err = fmt.Errorf("SetProcessUserGroup failed,err:%v,ret:%s", err, string(ret)) + return err + } + os.Setenv("PROCESS_EXEC_USER_GROUP", envGroup) + + } + return nil +} + +// GetProcessUserGroup 获取os用户group +func GetProcessUserGroup() string { + envGroup := os.Getenv("PROCESS_EXEC_USER_GROUP") + if envGroup == "" { + return OSGroup + } + return envGroup +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/customtime/customtime.go b/dbm-services/mongo/db-tools/dbactuator/pkg/customtime/customtime.go new file mode 100644 index 0000000000..3e9c9f8150 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/customtime/customtime.go @@ -0,0 +1,76 @@ +// Package customtime 自定义time +package customtime + +import ( + "database/sql/driver" + "fmt" + "strings" + "time" +) + +// CustomTime 自定义时间类型 +type CustomTime struct { + time.Time +} + +const ctLayout = "2006-01-02 15:04:05" + +var nilTime = (time.Time{}).UnixNano() + +// UnmarshalJSON .. +func (ct *CustomTime) UnmarshalJSON(b []byte) (err error) { + s := strings.Trim(string(b), "\"") + if s == "null" || s == "" { + ct.Time = time.Time{} + return + } + ct.Time, err = time.ParseInLocation(ctLayout, s, time.Local) + return +} + +// MarshalJSON .. +func (ct CustomTime) MarshalJSON() ([]byte, error) { + if ct.Time.UnixNano() == nilTime { + return []byte("null"), nil + } + return []byte(fmt.Sprintf("\"%s\"", ct.Time.Format(ctLayout))), nil +} + +// Scan scan +func (ct *CustomTime) Scan(value interface{}) error { + switch v := value.(type) { + case []byte: + return ct.UnmarshalText(string(v)) + case string: + return ct.UnmarshalText(v) + case time.Time: + ct.Time = v + case nil: + ct.Time = time.Time{} + default: + return fmt.Errorf("cannot sql.Scan() CustomTime from: %#v", v) + } + return nil +} + +// UnmarshalText unmarshal ... +func (ct *CustomTime) UnmarshalText(value string) error { + dd, err := time.ParseInLocation(ctLayout, value, time.Local) + if err != nil { + return err + } + ct.Time = dd + return nil +} + +// Value .. +// 注意这里ct不能是指针 +// 参考文章:https://www.codenong.com/44638610/ +func (ct CustomTime) Value() (driver.Value, error) { + return driver.Value(ct.Local().Format(ctLayout)), nil +} + +// IsSet .. +func (ct *CustomTime) IsSet() bool { + return ct.UnixNano() != nilTime +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/jobmanager/jobmanager.go b/dbm-services/mongo/db-tools/dbactuator/pkg/jobmanager/jobmanager.go new file mode 100644 index 0000000000..1e35d3ee31 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/jobmanager/jobmanager.go @@ -0,0 +1,153 @@ +// Package jobmanager 原子任务工厂类 与 管理类 +package jobmanager + +import ( + "fmt" + "log" + "runtime/debug" + "strings" + "sync" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atommongodb" + "dbm-services/mongo/db-tools/dbactuator/pkg/atomjobs/atomsys" + "dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" +) + +// AtomJobCreatorFunc 原子任务创建接口 +type AtomJobCreatorFunc func() jobruntime.JobRunner + +// JobGenericManager 原子任务管理者 +type JobGenericManager struct { + Runners []jobruntime.JobRunner `json:"runners"` + atomJobMapper map[string]AtomJobCreatorFunc + once sync.Once + runtime *jobruntime.JobGenericRuntime +} + +// NewJobGenericManager new +func NewJobGenericManager(uid, rootID, nodeID, versionID, payload, payloadFormat, atomJobs, baseDir string) ( + ret *JobGenericManager, err error) { + runtime, err := jobruntime.NewJobGenericRuntime(uid, rootID, nodeID, versionID, + payload, payloadFormat, atomJobs, baseDir) + if err != nil { + log.Panicf(err.Error()) + } + ret = &JobGenericManager{ + runtime: runtime, + } + return +} + +// LoadAtomJobs 加载子任务 +func (m *JobGenericManager) LoadAtomJobs() (err error) { + defer func() { + // err最后输出到标准错误 + if err != nil { + m.runtime.PrintToStderr(err.Error()) + } + }() + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%s", (debug.Stack())) + } + }() + m.runtime.AtomJobList = strings.TrimSpace(m.runtime.AtomJobList) + if m.runtime.AtomJobList == "" { + err = fmt.Errorf("atomJobList(%s) cannot be empty", m.runtime.AtomJobList) + m.runtime.Logger.Error(err.Error()) + return + } + jobList := strings.Split(m.runtime.AtomJobList, ",") + for _, atomName := range jobList { + atomName = strings.TrimSpace(atomName) + if atomName == "" { + continue + } + atom := m.GetAtomJobInstance(atomName) + if atom == nil { + err = fmt.Errorf("atomJob(%s) not found", atomName) + m.runtime.Logger.Error(err.Error()) + return + } + m.Runners = append(m.Runners, atom) + m.runtime.Logger.Info(fmt.Sprintf("atomJob:%s instance load success", atomName)) + } + return +} + +// RunAtomJobs 顺序执行原子任务 +func (m *JobGenericManager) RunAtomJobs() (err error) { + defer func() { + // err最后输出到标准错误 + if err != nil { + m.runtime.PrintToStderr(err.Error() + "\n") + } + }() + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%s", string(debug.Stack())) + } + }() + + m.runtime.StartHeartbeat(10 * time.Second) + + defer m.runtime.StopHeartbeat() + + for _, runner := range m.Runners { + name := util.GetTypeName(runner) + m.runtime.Logger.Info(fmt.Sprintf("begin to run %s init", name)) + if err = runner.Init(m.runtime); err != nil { + return + } + m.runtime.Logger.Info(fmt.Sprintf("begin to run %s", name)) + err = runner.Run() + if err != nil { + m.runtime.Logger.Info(fmt.Sprintf("runner %s run failed,err:%s", name, err)) + // err = runner.Rollback() + // if err != nil { + // err = fmt.Errorf("runner %s rollback failed,err:%+v", name, err) + // m.runtime.Logger.Error(err.Error()) + // return + // } + // m.runtime.Logger.Info(fmt.Sprintf("runner %s rollback success!!!", name)) + return + } + m.runtime.Logger.Info(fmt.Sprintf("finished run %s", name)) + } + m.runtime.Logger.Info(fmt.Sprintf("run all atomJobList:%s success", m.runtime.AtomJobList)) + + m.runtime.OutputPipeContextData() + return +} + +// GetAtomJobInstance 根据atomJobName,从m.atomJobMapper中获取其creator函数,执行creator函数 +func (m *JobGenericManager) GetAtomJobInstance(atomJob string) jobruntime.JobRunner { + m.once.Do(func() { + m.atomJobMapper = make(map[string]AtomJobCreatorFunc) + + // os初始化 + m.atomJobMapper[atomsys.NewOsMongoInit().Name()] = atomsys.NewOsMongoInit + // mongo atom jobs + m.atomJobMapper[atommongodb.NewMongoDBInstall().Name()] = atommongodb.NewMongoDBInstall + m.atomJobMapper[atommongodb.NewMongoSInstall().Name()] = atommongodb.NewMongoSInstall + m.atomJobMapper[atommongodb.NewInitiateReplicaset().Name()] = atommongodb.NewInitiateReplicaset + m.atomJobMapper[atommongodb.NewAddShardToCluster().Name()] = atommongodb.NewAddShardToCluster + m.atomJobMapper[atommongodb.NewAddUser().Name()] = atommongodb.NewAddUser + m.atomJobMapper[atommongodb.NewDelUser().Name()] = atommongodb.NewDelUser + m.atomJobMapper[atommongodb.NewMongoDReplace().Name()] = atommongodb.NewMongoDReplace + m.atomJobMapper[atommongodb.NewMongoRestart().Name()] = atommongodb.NewMongoRestart + m.atomJobMapper[atommongodb.NewStepDown().Name()] = atommongodb.NewStepDown + m.atomJobMapper[atommongodb.NewBalancer().Name()] = atommongodb.NewBalancer + m.atomJobMapper[atommongodb.NewDeInstall().Name()] = atommongodb.NewDeInstall + m.atomJobMapper[atommongodb.NewExecScript().Name()] = atommongodb.NewExecScript + m.atomJobMapper[atommongodb.NewSetProfiler().Name()] = atommongodb.NewSetProfiler + m.atomJobMapper[atommongodb.NewMongoDChangeOplogSize().Name()] = atommongodb.NewMongoDChangeOplogSize + }) + creator, ok := m.atomJobMapper[strings.ToLower(atomJob)] + if ok { + return creator() + } + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime/jobrunner.go b/dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime/jobrunner.go new file mode 100644 index 0000000000..26c2eb74cc --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime/jobrunner.go @@ -0,0 +1,19 @@ +package jobruntime + +// JobRunner defines a behavior of a job +type JobRunner interface { + // Init doing some operation before run a job + // such as reading parametes + Init(*JobGenericRuntime) error + + // Name return the name of the job + Name() string + + // Run run a job + Run() error + + Retry() uint + + // Rollback you can define some rollback logic here when job fails + Rollback() error +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime/jobruntime.go b/dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime/jobruntime.go new file mode 100644 index 0000000000..e4d885b4e3 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/jobruntime/jobruntime.go @@ -0,0 +1,159 @@ +// Package jobruntime 全局操作、全局变量 +package jobruntime + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "time" + + "dbm-services/common/go-pubpkg/logger" + "dbm-services/mongo/db-tools/dbactuator/mylog" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" +) + +const ( + logDir = "logs/" +) + +// JobGenericRuntime job manager +type JobGenericRuntime struct { + UID string `json:"uid"` // 单据ID + RootID string `json:"rootId"` // 流程ID + NodeID string `json:"nodeId"` // 节点ID + VersionID string `json:"versionId"` // 运行版本ID + PayloadEncoded string `json:"payloadEncoded"` // 参数encoded + PayloadDecoded string `json:"payloadDecoded"` // 参数decoded + PayLoadFormat string `json:"payloadFormat"` // payload的内容格式,raw/base64 + AtomJobList string `json:"atomJobList"` // 原子任务列表,逗号分割 + BaseDir string `json:"baseDir"` + // ShareData保存多个atomJob间的中间结果,前后atomJob可通过ShareData通信 + ShareData interface{} `json:"shareData"` + // PipeContextData保存流程调用,上下文结果 + // PipeContextData=>json.Marshal=>Base64=>标准输出打印{Result} + PipeContextData interface{} `json:"pipeContextData"` + Logger *logger.Logger `json:"-"` // 线程安全日志输出 + ctx context.Context `json:"-"` + cancelFunc context.CancelFunc `json:"-"` + Err error +} + +// NewJobGenericRuntime new +func NewJobGenericRuntime(uid, rootID string, + nodeID, versionID, payload, payloadFormat, atomJobs, baseDir string) (ret *JobGenericRuntime, err error) { + ret = &JobGenericRuntime{ + UID: uid, + RootID: rootID, + NodeID: nodeID, + VersionID: versionID, + PayloadEncoded: payload, + PayLoadFormat: payloadFormat, + AtomJobList: atomJobs, + BaseDir: baseDir, + ShareData: nil, + } + + if ret.PayLoadFormat == consts.PayloadFormatRaw { + ret.PayloadDecoded = ret.PayloadEncoded + } else { + var decodedStr []byte + decodedStr, err = base64.StdEncoding.DecodeString(ret.PayloadEncoded) + if err != nil { + log.Printf("Base64.DecodeString failed,err:%v,encodedString:%s", err, ret.PayloadEncoded) + os.Exit(0) + } + ret.PayloadDecoded = string(decodedStr) + // log.Printf("===========PayloadDecoded========") + // log.Printf(ret.PayloadDecoded) + } + ret.ctx, ret.cancelFunc = context.WithCancel(context.TODO()) + ret.SetLogger() + return +} + +// SetLogger set logger +func (r *JobGenericRuntime) SetLogger() { + var err error + logFile := fmt.Sprintf("redis_actuator_%s_%s.log", r.UID, r.NodeID) + err = util.MkDirsIfNotExists([]string{logDir}) + if err != nil { + panic(err) + } + + logFilePath := filepath.Join(logDir, logFile) + file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm) + if err != nil { + panic(err) + } + extMap := map[string]string{ + "uid": r.UID, + "node_id": r.NodeID, + "root_id": r.RootID, + "version_id": r.VersionID, + } + r.Logger = logger.New(file, true, logger.InfoLevel, extMap) + r.Logger.Sync() + mylog.SetDefaultLogger(r.Logger) + + // 修改日志目录owner + chownCmd := fmt.Sprintf("chown -R %s.%s %s", consts.MysqlAaccount, consts.MysqlGroup, logDir) + cmd := exec.Command("bash", "-c", chownCmd) + cmd.Run() +} + +// PrintToStdout 打印到标准输出 +func (r *JobGenericRuntime) PrintToStdout(format string, args ...interface{}) { + fmt.Fprintf(os.Stdout, format, args...) +} + +// PrintToStderr 打印到标准错误 +func (r *JobGenericRuntime) PrintToStderr(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format, args...) +} + +// OutputPipeContextData PipeContextData=>json.Marshal=>Base64=>标准输出打印{Result} +func (r *JobGenericRuntime) OutputPipeContextData() { + if r.PipeContextData == nil { + r.Logger.Info("no PipeContextData to output") + return + } + tmpBytes, err := json.Marshal(r.PipeContextData) + if err != nil { + r.Err = fmt.Errorf("json.Marshal PipeContextData failed,err:%v", err) + r.Logger.Error(r.Err.Error()) + return + } + // decode函数: base64.StdEncoding.DecodeString + base64Ret := base64.StdEncoding.EncodeToString(tmpBytes) + r.PrintToStdout("" + base64Ret + "") +} + +// StartHeartbeat 开始心跳 +func (r *JobGenericRuntime) StartHeartbeat(period time.Duration) { + go func() { + ticker := time.NewTicker(period) + defer ticker.Stop() + var heartbeatTime string + for { + select { + case <-ticker.C: + heartbeatTime = time.Now().Local().Format(consts.UnixtimeLayout) + r.PrintToStdout("[" + heartbeatTime + "]heartbeat\n") + case <-r.ctx.Done(): + r.Logger.Info("stop heartbeat") + return + } + } + }() +} + +// StopHeartbeat 结束心跳 +func (r *JobGenericRuntime) StopHeartbeat() { + r.cancelFunc() +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/report/filereport.go b/dbm-services/mongo/db-tools/dbactuator/pkg/report/filereport.go new file mode 100644 index 0000000000..dbc50ce87c --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/report/filereport.go @@ -0,0 +1,101 @@ +// Package report (备份等)记录上报 +package report + +import ( + "bufio" + "fmt" + "os" + "sync" + + "dbm-services/mongo/db-tools/dbactuator/mylog" +) + +var _ Reporter = (*FileReport)(nil) + +// FileReport 文件上报 +type FileReport struct { + saveFile string + fileP *os.File + bufWriter *bufio.Writer + mux sync.Mutex // 并发安全写入 +} + +// NewFileReport new +func NewFileReport(savefile string) (ret *FileReport, err error) { + ret = &FileReport{} + err = ret.SetSaveFile(savefile) + return ret, err +} + +// AddRecord 新增记录 +func (f *FileReport) AddRecord(item string, flush bool) (err error) { + if f.saveFile == "" { + err = fmt.Errorf("saveFile(%s) can't be empty", f.saveFile) + mylog.Logger.Error(err.Error()) + return + } + _, err = f.bufWriter.WriteString(item) + if err != nil { + err = fmt.Errorf("bufio.Writer WriteString fail,err:%v,saveFile:%s", err, f.saveFile) + mylog.Logger.Error(err.Error()) + return + } + if flush == true { + f.bufWriter.Flush() + } + return nil +} + +// SaveFile .. +func (f *FileReport) SaveFile() string { + return f.saveFile +} + +// SetSaveFile set方法 +func (f *FileReport) SetSaveFile(savefile string) error { + var err error + err = f.Close() + if err != nil { + return err + } + if savefile == "" { + err = fmt.Errorf("saveFile(%s) cannot be empty", savefile) + mylog.Logger.Error(err.Error()) + return err + } + f.saveFile = savefile + f.fileP, err = os.OpenFile(savefile, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + err = fmt.Errorf("open file:%s fail,err:%v", savefile, err) + mylog.Logger.Error(err.Error()) + return err + } + f.bufWriter = bufio.NewWriter(f.fileP) + return nil +} + +// Close file +func (f *FileReport) Close() error { + f.mux.Lock() + defer f.mux.Unlock() + + var err error + if f.saveFile == "" { + return nil + } + f.saveFile = "" + + err = f.bufWriter.Flush() + if err != nil { + err = fmt.Errorf("bufio flush fail.err:%v,file:%s", err, f.saveFile) + mylog.Logger.Error(err.Error()) + return nil + } + err = f.fileP.Close() + if err != nil { + err = fmt.Errorf("file close fail.err:%v,file:%s", err, f.saveFile) + mylog.Logger.Error(err.Error()) + return nil + } + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/report/reporter.go b/dbm-services/mongo/db-tools/dbactuator/pkg/report/reporter.go new file mode 100644 index 0000000000..ce13a8f5b4 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/report/reporter.go @@ -0,0 +1,58 @@ +package report + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "dbm-services/mongo/db-tools/dbactuator/mylog" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + "dbm-services/mongo/db-tools/dbactuator/pkg/util" +) + +// Reporter 上报接口 +type Reporter interface { + AddRecord(item string, flush bool) error + Close() error +} + +// CreateReportDir 创建上报目录 /home/mysql/dbareport -> {REDIS_BACKUP_DIR}/dbbak/dbareport +func CreateReportDir() (err error) { + mylog.Logger.Info("begin to create reportDir(%s)", consts.DbaReportSaveDir) + var realLink string + realReportDir := filepath.Join(consts.GetRedisBackupDir(), "dbbak", "dbareport") // 如 /data/dbbak/dbareport + if !util.FileExists(realReportDir) { + err = util.MkDirsIfNotExists([]string{realReportDir}) + if err != nil { + mylog.Logger.Error(err.Error()) + return + } + } + util.LocalDirChownMysql(realReportDir) + if util.FileExists(consts.DbaReportSaveDir) { + realLink, err = filepath.EvalSymlinks(consts.DbaReportSaveDir) + if err != nil { + err = fmt.Errorf("filepath.EvalSymlinks %s fail,err:%v", consts.DbaReportSaveDir, err) + mylog.Logger.Error(err.Error()) + return err + } + // /home/mysql/dbareport -> /data/dbbak/dbareport ok,直接返回 + if realLink == realReportDir { + return nil + } + // 如果 /home/mysql/dbareport 不是指向 /data/dbbak/dbareport,先删除 + rmCmd := "rm -rf " + consts.DbaReportSaveDir + util.RunBashCmd(rmCmd, "", nil, 1*time.Minute) + } + err = os.Symlink(realReportDir, filepath.Dir(consts.DbaReportSaveDir)) + if err != nil { + err = fmt.Errorf("os.Symlink %s -> %s fail,err:%s", consts.DbaReportSaveDir, realReportDir, err) + mylog.Logger.Error(err.Error()) + return + } + mylog.Logger.Info("create softLink success,%s -> %s", consts.DbaReportSaveDir, realReportDir) + util.MkDirsIfNotExists([]string{consts.RedisReportSaveDir}) + util.LocalDirChownMysql(consts.DbaReportSaveDir) + return +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/util/bkrepo.go b/dbm-services/mongo/db-tools/dbactuator/pkg/util/bkrepo.go new file mode 100644 index 0000000000..f2695eeac9 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/util/bkrepo.go @@ -0,0 +1,102 @@ +package util + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + + "dbm-services/mongo/db-tools/dbactuator/mylog" +) + +// FileServerInfo 文件服务器 +type FileServerInfo struct { + URL string `json:"url"` // 制品库地址 + Bucket string `json:"bucket"` // 目标bucket + Password string `json:"password"` // 制品库 password + Username string `json:"username"` // 制品库username + Project string `json:"project"` // 制品库project +} + +// UploadFile 上传文件到蓝盾制品库 +// filepath: 本地需要上传文件的路径 +// targetURL: 仓库文件完整路径 +func UploadFile(filepath string, targetURL string, username string, password string) (*http.Response, error) { + + userMsg := fmt.Sprintf(username + ":" + password) + token := base64.StdEncoding.EncodeToString([]byte(userMsg)) + msg := fmt.Sprintf("start upload files from %s to %s", filepath, targetURL) + mylog.Logger.Info(msg) + bodyBuf := bytes.NewBufferString("") + bodyWriter := multipart.NewWriter(bodyBuf) + + fh, err := os.Open(filepath) + if err != nil { + mylog.Logger.Info("error opening file") + return nil, err + } + boundary := bodyWriter.Boundary() + closeBuf := bytes.NewBufferString("") + + requestReader := io.MultiReader(bodyBuf, fh, closeBuf) + fi, err := fh.Stat() + if err != nil { + fmt.Printf("Error Stating file: %s", filepath) + return nil, err + } + req, err := http.NewRequest("PUT", targetURL, requestReader) + if err != nil { + return nil, err + } + + // Set headers for multipart, and Content Length + req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary) + // 文件是否可以被覆盖,默认false + req.Header.Set("X-BKREPO-OVERWRITE", "True") + // 文件默认保留半年 + req.Header.Set("X-BKREPO-EXPIRES", "183") + req.Header.Set("Authorization", "Basic "+token) + req.ContentLength = fi.Size() + int64(bodyBuf.Len()) + int64(closeBuf.Len()) + return http.DefaultClient.Do(req) + // return response, err +} + +// DownloadFile 从蓝盾制品库下载文件 +// filepath: 本地保存文件压缩包名 +// targetURL: 仓库文件完整路径 +func DownloadFile(filepath string, targetURL string, username string, password string) (err error) { + msg := fmt.Sprintf("start download files from %s to %s", targetURL, filepath) + mylog.Logger.Info(msg) + userMsg := fmt.Sprintf(username + ":" + password) + token := base64.StdEncoding.EncodeToString([]byte(userMsg)) + outFile, err := os.Create(filepath) + if err != nil { + return err + } + defer outFile.Close() + + resp, err := http.Get(targetURL) + if err != nil { + return err + } + resp.Header.Set("Authorization", "Basic "+token) + defer resp.Body.Close() + + // Check server response + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + // Writer the body to file + _, err = io.Copy(outFile, resp.Body) + if err != nil { + return err + } + mylog.Logger.Info("finish download files") + + return nil + +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/util/compress.go b/dbm-services/mongo/db-tools/dbactuator/pkg/util/compress.go new file mode 100644 index 0000000000..4d54e3b734 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/util/compress.go @@ -0,0 +1,206 @@ +package util + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/mylog" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + + "github.com/dustin/go-humanize" +) + +// IsZstdExecutable 通过'zstd -V'命令确定本地zstd工具是能正常运行的 +func IsZstdExecutable() (ok bool) { + var err error + if !FileExists(consts.ZstdBin) { + return false + } + cmd := exec.Command(consts.ZstdBin, "-V") + if err = cmd.Start(); err != nil { + // err = fmt.Errorf("'%s -V' cmd.Start fail,err:%v", zstdBin, err) + return false + } + if err = cmd.Wait(); err != nil { + // err = fmt.Errorf("'%s -V' cmd.Wait fail,err:%v", zstdBin, err) + return false + } + return true +} + +// CompressFile 压缩文件 +// 优先使用zstd 做压缩,zstd无法使用则使用gzip +func CompressFile(file, targetDir string, rmOrigin bool) (retFile string, err error) { + var compressCmd string + fileDir := filepath.Dir(file) + filename := filepath.Base(file) + if targetDir == "" { + targetDir = fileDir + } + if IsZstdExecutable() { + retFile = filepath.Join(targetDir, filename+".zst") + if rmOrigin { + compressCmd = fmt.Sprintf(`cd %s && %s --rm -T4 %s -o %s`, fileDir, consts.ZstdBin, filename, retFile) + } else { + compressCmd = fmt.Sprintf(`cd %s && %s -T4 %s -o %s`, fileDir, consts.ZstdBin, filename, retFile) + } + _, err = RunBashCmd(compressCmd, "", nil, 6*time.Hour) + if err != nil { + return + } + } else { + retFile = filepath.Join(fileDir, filename+".gz") + if rmOrigin { + compressCmd = fmt.Sprintf(`gzip < %s >%s && rm -f %s`, file, retFile, file) + } else { + compressCmd = fmt.Sprintf(`gzip < %s >%s`, file, retFile) + } + _, err = RunBashCmd(compressCmd, "", nil, 6*time.Hour) + if err != nil { + return + } + } + return +} + +// SplitLargeFile 切割大文件为小文件,并返回切割后的结果 +// 参数file须是全路径; +// 如果file大小 小于 splitTargetSize,则返回值splitTargetSize只包含 file 一个元素 +func SplitLargeFile(file, splitTargetSize string, rmOrigin bool) (splitedFiles []string, err error) { + var fileSize int64 + var splitLimit uint64 + var cmdRet string + if file == "" { + return + } + fileSize, err = GetFileSize(file) + if err != nil { + return + } + splitLimit, err = humanize.ParseBytes(splitTargetSize) + if err != nil { + err = fmt.Errorf("humanize.ParseBytes fail,err:%v,splitTargetSize:%s", err, splitTargetSize) + return + } + if fileSize < int64(splitLimit) { + splitedFiles = append(splitedFiles, file) + return + } + fileDir := filepath.Dir(file) + fileBase := filepath.Base(file) + fileBase = strings.TrimSuffix(fileBase, ".tar") + fileBase = strings.TrimSuffix(fileBase, ".tar.gz") + fileBase = fileBase + ".split." + splitCmd := fmt.Sprintf(`cd %s && split --verbose -a 3 -b %s -d %s %s|grep -i --only-match -E "%s[0-9]+"`, + fileDir, splitTargetSize, file, fileBase, fileBase) + mylog.Logger.Info(splitCmd) + cmdRet, err = RunBashCmd(splitCmd, "", nil, 6*time.Hour) + if err != nil { + return + } + l01 := strings.Split(cmdRet, "\n") + for _, item := range l01 { + item = strings.TrimSpace(item) + if item == "" { + continue + } + splitedFiles = append(splitedFiles, filepath.Join(fileDir, item)) + } + if rmOrigin { + err = os.Remove(file) + mylog.Logger.Info(fmt.Sprintf("rm %s", file)) + if err != nil { + err = fmt.Errorf("os.Remove fail,err:%v,file:%s", err, file) + return + } + } + return +} + +// TarADir 对一个目录进行tar打包, +// 如打包 /data/dbbak/REDIS-FULL-rocksdb-1.1.1.1-30000 为 /tmp/REDIS-FULL-rocksdb-1.1.1.1-30000.tar +// 参数: originDir 为 /data/dbbak/REDIS-FULL-rocksdb-1.1.1.1-30000 +// 参数: tarSaveDir 为 /tmp/ +// 返回值: tarFile 为 /tmp/REDIS-FULL-rocksdb-1.1.1.1-30000.tar +func TarADir(originDir, tarSaveDir string, rmOrigin bool) (tarFile string, err error) { + var tarCmd string + basename := filepath.Base(originDir) + baseDir := filepath.Dir(originDir) + if tarSaveDir == "" { + tarSaveDir = filepath.Dir(originDir) + } + tarFile = filepath.Join(tarSaveDir, basename+".tar") + + if rmOrigin { + tarCmd = fmt.Sprintf(`tar --remove-files -cf %s -C %s %s`, tarFile, baseDir, basename) + } else { + tarCmd = fmt.Sprintf(`tar -cf %s -C %s %s`, tarFile, baseDir, basename) + } + mylog.Logger.Info(tarCmd) + _, err = RunBashCmd(tarCmd, "", nil, 6*time.Hour) + if err != nil { + return + } + return +} + +// TarAndSplitADir 对目录tar打包并执行split +func TarAndSplitADir(originDir, targetSaveDir, splitTargetSize string, rmOrigin bool) ( + splitedFiles []string, err error) { + var tarFile string + tarFile, err = TarADir(originDir, targetSaveDir, rmOrigin) + if err != nil { + return + } + splitedFiles, err = SplitLargeFile(tarFile, splitTargetSize, rmOrigin) + if err != nil { + return + } + return +} + +// UnionSplitFiles 合并多个split文件为一个tar文件 +func UnionSplitFiles(dir string, splitFiles []string) (tarfile string, err error) { + if len(splitFiles) == 0 { + err = fmt.Errorf("splitFiles:%+v empty list", splitFiles) + return + } + if len(splitFiles) == 1 && strings.HasSuffix(splitFiles[0], ".tar") { + return splitFiles[0], nil + } + var name string + var fullpath string + var cmd01 string + reg01 := regexp.MustCompile(`.split.\d+$`) + baseNames := make([]string, 0, len(splitFiles)) + for _, file01 := range splitFiles { + name = filepath.Base(file01) + baseNames = append(baseNames, name) + if !reg01.MatchString(file01) { + err = fmt.Errorf("%+v not split files?", splitFiles) + return + } + fullpath = filepath.Join(dir, name) + if !FileExists(fullpath) { + err = fmt.Errorf("%s not exists", fullpath) + return + } + } + + prefix := reg01.ReplaceAllString(baseNames[0], "") + tarfile = prefix + ".tar" + if len(baseNames) == 1 { + cmd01 = fmt.Sprintf("cd %s && mv %s %s", dir, baseNames[0], tarfile) + } else { + cmd01 = fmt.Sprintf("cd %s && cat %s.split* > %s", dir, prefix, tarfile) + } + mylog.Logger.Info(cmd01) + _, err = RunBashCmd(cmd01, "", nil, 2*time.Hour) + tarfile = filepath.Join(dir, tarfile) + return +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/util/file.go b/dbm-services/mongo/db-tools/dbactuator/pkg/util/file.go new file mode 100644 index 0000000000..3e708ee613 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/util/file.go @@ -0,0 +1,66 @@ +package util + +import ( + "bufio" + "bytes" + "crypto/md5" + "fmt" + "io" + "os" +) + +// FileExists 检查目录是否已经存在 +func FileExists(path string) bool { + _, err := os.Stat(path) + if err != nil { + return os.IsExist(err) + } + return true +} + +// GetFileMd5 求文件md5sum值 +func GetFileMd5(fileAbPath string) (md5sum string, err error) { + rFile, err := os.Open(fileAbPath) + if err != nil { + return "", fmt.Errorf("GetFileMd5 fail,err:%v,file:%s", err, fileAbPath) + } + defer func(rFile *os.File) { + _ = rFile.Close() + }(rFile) + h := md5.New() + if _, err := io.Copy(h, rFile); err != nil { + return "", fmt.Errorf("GetFileMd5 io.Copy fail,err:%v,file:%s", err, fileAbPath) + } + return fmt.Sprintf("%x", h.Sum(nil)), nil +} + +// FileLineCounter 计算文件行数 +// 参考: https://stackoverflow.com/questions/24562942/golang-how-do-i-determine-the-number-of-lines-in-a-file-efficiently +func FileLineCounter(filename string) (lineCnt uint64, err error) { + _, err = os.Stat(filename) + if err != nil && os.IsNotExist(err) == true { + return 0, fmt.Errorf("file:%s not exists", filename) + } + file, err := os.Open(filename) + if err != nil { + return 0, fmt.Errorf("file:%s open fail,err:%v", filename, err) + } + defer file.Close() + reader01 := bufio.NewReader(file) + buf := make([]byte, 32*1024) + lineCnt = 0 + lineSep := []byte{'\n'} + + for { + c, err := reader01.Read(buf) + lineCnt += uint64(bytes.Count(buf[:c], lineSep)) + + switch { + case err == io.EOF: + return lineCnt, nil + + case err != nil: + return lineCnt, fmt.Errorf("file:%s read fail,err:%v", filename, err) + } + } +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/util/net.go b/dbm-services/mongo/db-tools/dbactuator/pkg/util/net.go new file mode 100644 index 0000000000..413be8aceb --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/util/net.go @@ -0,0 +1,67 @@ +package util + +import ( + "fmt" + "net" +) + +// GetIpv4InterfaceName 根据ipv4地址获取网络接口名 +// https://stackoverflow.com/questions/23529663/how-to-get-all-addresses-and-masks-from-local-interfaces-in-go +func GetIpv4InterfaceName(ipv4 string) (interName string, err error) { + var ifaces []net.Interface + var addrs []net.Addr + + ifaces, err = net.Interfaces() + if err != nil { + err = fmt.Errorf("net.Interfaces fail,err:%v", err) + return + } + for _, i := range ifaces { + addrs, err = i.Addrs() + if err != nil { + // err = fmt.Errorf("%s get addrs fail,err:%v", i.Name, err) + continue + } + for _, a := range addrs { + switch v := a.(type) { + case *net.IPAddr: + if v.IP.String() == ipv4 { + return i.Name, nil + } + + case *net.IPNet: + if v.IP.String() == ipv4 { + return i.Name, nil + } + } + } + } + err = fmt.Errorf("ipv4:%s not found interfacename", ipv4) + return +} + +// GetInterfaceIpv4Addr 获取网络接口对应的 ipv4地址 +// https://gist.github.com/schwarzeni/f25031a3123f895ff3785970921e962c +func GetInterfaceIpv4Addr(interfaceName string) (addr string, err error) { + var ( + ief *net.Interface + addrs []net.Addr + ipv4Addr net.IP + ) + if ief, err = net.InterfaceByName(interfaceName); err != nil { // get interface + err = fmt.Errorf("net.InterfaceByName %s fail,err:%v", interfaceName, err) + return + } + if addrs, err = ief.Addrs(); err != nil { // get addresses + return + } + for _, addr := range addrs { // get ipv4 address + if ipv4Addr = addr.(*net.IPNet).IP.To4(); ipv4Addr != nil { + break + } + } + if ipv4Addr == nil { + return "", fmt.Errorf("interface %s don't have an ipv4 address\n", interfaceName) + } + return ipv4Addr.String(), nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/util/osCmd.go b/dbm-services/mongo/db-tools/dbactuator/pkg/util/osCmd.go new file mode 100644 index 0000000000..62750bf58b --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/util/osCmd.go @@ -0,0 +1,107 @@ +package util + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/mylog" +) + +// DealLocalCmdPid 处理本地命令得到pid +type DealLocalCmdPid interface { + DealProcessPid(pid int) error +} + +// RunBashCmd bash -c "$cmd" 执行命令并得到命令结果 +func RunBashCmd(cmd, outFile string, dealPidMethod DealLocalCmdPid, + timeout time.Duration) (retStr string, err error) { + opts := []string{"-c", cmd} + return RunLocalCmd("bash", opts, outFile, dealPidMethod, timeout) +} + +// RunLocalCmd 运行本地命令并得到命令结果 +/* + *参数: + * outFile: 不为空,则将标准输出结果打印到outFile中; + * dealPidMethod: 不为空,则将命令pid传给dealPidMethod.DealProcessPid()函数; + * logger: 用于打印日志; + */ +func RunLocalCmd( + cmd string, opts []string, outFile string, + dealPidMethod DealLocalCmdPid, timeout time.Duration) (retStr string, err error) { + ctx, cancel := context.WithTimeout(context.TODO(), timeout) + defer cancel() + + cmdCtx := exec.CommandContext(ctx, cmd, opts...) + var retBuffer bytes.Buffer + var errBuffer bytes.Buffer + var outFileHandler *os.File + if len(strings.TrimSpace(outFile)) == 0 { + cmdCtx.Stdout = &retBuffer + } else { + outFileHandler, err = os.Create(outFile) + if err != nil { + mylog.Logger.Error("RunLocalCmd create outfile fail,err:%v,outFile:%s", err, outFile) + return "", fmt.Errorf("RunLocalCmd create outfile fail,err:%v,outFile:%s", err, outFile) + } + defer outFileHandler.Close() + mylog.Logger.Info("RunLocalCmd create outfile(%s) success ...", outFile) + cmdCtx.Stdout = outFileHandler + } + cmdCtx.Stderr = &errBuffer + mylog.Logger.Debug("Running a new local cmd:%s,opts:%+v", cmd, opts) + + if err = cmdCtx.Start(); err != nil { + mylog.Logger.Error("RunLocalCmd cmd Start fail,err:%v,cmd:%s,opts:%+v", err, cmd, opts) + return "", fmt.Errorf("RunLocalCmd cmd Start fail,err:%v", err) + } + if dealPidMethod != nil { + dealPidMethod.DealProcessPid(cmdCtx.Process.Pid) + } + if err = cmdCtx.Wait(); err != nil { + mylog.Logger.Error("RunLocalCmd cmd wait fail,err:%v,errBuffer:%s,retBuffer:%s,cmd:%s,opts:%+v", err, + errBuffer.String(), retBuffer.String(), cmd, opts) + return "", fmt.Errorf("RunLocalCmd cmd wait fail,err:%v,detail:%s", err, errBuffer.String()) + } + retStr = retBuffer.String() + + if strings.TrimSpace(errBuffer.String()) != "" { + mylog.Logger.Error("RunLocalCmd fail,err:%v,cmd:%s,opts:%+v", errBuffer.String(), cmd, opts) + err = fmt.Errorf("RunLocalCmd fail,err:%s", retBuffer.String()+"\n"+errBuffer.String()) + } else { + err = nil + } + retStr = strings.TrimSpace(retStr) + return +} + +// SetOSUserPassword run set user password by chpasswd +func SetOSUserPassword(user, password string) error { + exec.Command("/bin/bash", "-c", "") + cmd := exec.Command("chpasswd") + stdin, err := cmd.StdinPipe() + if err != nil { + return fmt.Errorf("new pipe failed, err:%w", err) + } + go func() { + _, err := io.WriteString(stdin, fmt.Sprintf("%s:%s", user, password)) + if err != nil { + mylog.Logger.Warn("write into pipe failed, err:%s", err.Error()) + } + if err := stdin.Close(); err != nil { + mylog.Logger.Warn("colse stdin failed, err:%s", err.Error()) + } + }() + if output, err := cmd.CombinedOutput(); err != nil { + err = fmt.Errorf("run chpasswd failed, output:%s, err:%w", string(output), err) + mylog.Logger.Error(err.Error()) + return err + } + return nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/util/proxy_tools.go b/dbm-services/mongo/db-tools/dbactuator/pkg/util/proxy_tools.go new file mode 100644 index 0000000000..8f4d60b17a --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/util/proxy_tools.go @@ -0,0 +1,103 @@ +// Package util here mybe something +package util + +import ( + "bufio" + "crypto/md5" + "encoding/json" + "fmt" + "io" + "math/rand" + "net" + "sort" + "strconv" + "strings" + "time" +) + +// NCInstance NCInstance +var NCInstance *NetCat + +// NetCat use tcp for nc +type NetCat struct { + AdminAddr string + ReadTimeOut time.Duration + Nc net.Conn +} + +func init() { + NCInstance = &NetCat{} + rand.Seed(time.Now().UnixNano()) +} + +// GetTwemProxyBackendsMd5Sum 获取MD5 sum +func GetTwemProxyBackendsMd5Sum(addr string) (string, error) { + pinfo := strings.Split(addr, ":") + port, _ := strconv.Atoi(pinfo[1]) + segsMap, err := GetTwemproxyBackends(pinfo[0], port) + if err != nil { + return "errFailed", err + } + segList := []string{} + for addr, seg := range segsMap { + segList = append(segList, fmt.Sprintf("%s|%s", addr, seg)) + } + sort.Slice(segList, func(i, j int) bool { + return segList[i] > segList[j] + }) + + x, _ := json.Marshal(segList) + return fmt.Sprintf("%x", md5.Sum(x)), nil +} + +// DoSwitchTwemproxyBackends "change nosqlproxy $mt:$mp $st:$sp" +func DoSwitchTwemproxyBackends(ip string, port int, from, to string) (rst string, err error) { + addr := fmt.Sprintf("%s:%d", ip, port+1000) + nc, err := net.DialTimeout("tcp", addr, time.Second) + if err != nil { + return "nil", err + } + _, err = nc.Write([]byte(fmt.Sprintf("change nosqlproxy %s %s", from, to))) + if err != nil { + return "nil", err + } + return bufio.NewReader(nc).ReadString('\n') +} + +// GetTwemproxyBackends get nosqlproxy servers +func GetTwemproxyBackends(ip string, port int) (segs map[string]string, err error) { + addr := fmt.Sprintf("%s:%d", ip, port+1000) + nc, err := net.DialTimeout("tcp", addr, time.Second) + if err != nil { + return nil, err + } + if segs, err = GetSegDetails(nc); err != nil { + return nil, err + } + return segs, nil +} + +// GetSegDetails echo stats |nc twempip port +func GetSegDetails(nc net.Conn) (map[string]string, error) { + _, err := nc.Write([]byte("stats")) + if err != nil { + return nil, err + } + reader := bufio.NewReader(nc) + segs := make(map[string]string) + for { + // rep, _, err := reader.ReadLine() + line, _, err := reader.ReadLine() + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + strws := strings.Split(string(line), " ") + if len(strws) == 4 { + segs[strws[2]] = strws[0] + } + } + return segs, nil +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/util/redisutil.go b/dbm-services/mongo/db-tools/dbactuator/pkg/util/redisutil.go new file mode 100644 index 0000000000..f9c4e95fd5 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/util/redisutil.go @@ -0,0 +1,102 @@ +package util + +import ( + "fmt" + "path/filepath" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/mylog" + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + + "github.com/shirou/gopsutil/v3/mem" +) + +// GetTendisplusBlockcache 返回单位Mbyte +// 如果系统内存小于4GB,则 instBlockcache = 系统总内存 * 0.3 / 实例数 +// 否则 instBlockcache = 系统总内存 * 0.5 / 实例数 +func GetTendisplusBlockcache(instCount uint64) (instBlockcache uint64, err error) { + if instCount <= 0 { + err = fmt.Errorf("instCount==%d <=0", instCount) + return + } + var vMem *mem.VirtualMemoryStat + vMem, err = mem.VirtualMemory() + if err != nil { + err = fmt.Errorf("mem.VirtualMemory fail,err:%v", err) + return + } + if vMem.Total < 4*consts.GiByte { + instBlockcache = vMem.Total * 3 / (10 * instCount) + } else { + instBlockcache = vMem.Total * 5 / (10 * instCount) + } + if instBlockcache < 128*consts.MiByte { + instBlockcache = 128 * consts.MiByte + } + instBlockcache = instBlockcache / consts.MiByte + return +} + +// GetTendisplusWriteBufferSize 返回单位是Byte +// 如果系统内存小于8GB,则 writeBufferSize = 8MB,否则 writeBufferSize = 32MB +func GetTendisplusWriteBufferSize(instCount uint64) (writeBufferSize uint64, err error) { + if instCount <= 0 { + err = fmt.Errorf("instCount==%d <=0", instCount) + return + } + var vMem *mem.VirtualMemoryStat + vMem, err = mem.VirtualMemory() + if err != nil { + err = fmt.Errorf("mem.VirtualMemory fail,err:%v", err) + return + } + if vMem.Total <= 8*consts.GiByte { + writeBufferSize = 8 * consts.MiByte + } else { + writeBufferSize = 32 * consts.MiByte + } + return +} + +// StopBkDbmon 停止bk-dbmon +func StopBkDbmon() (err error) { + if FileExists(consts.BkDbmonBin) { + stopScript := filepath.Join(consts.BkDbmonPath, "stop.sh") + stopCmd := fmt.Sprintf("su %s -c '%s'", consts.MysqlAaccount, "sh "+stopScript) + mylog.Logger.Info(stopCmd) + _, err = RunLocalCmd("su", []string{consts.MysqlAaccount, "-c", "sh " + stopScript}, + "", nil, 1*time.Minute) + return + } + killCmd := ` +pid=$(ps aux|grep 'bk-dbmon --config'|grep -v dbactuator|grep -v grep|awk '{print $2}') +if [[ -n $pid ]] +then +kill $pid +fi +` + mylog.Logger.Info(killCmd) + _, err = RunBashCmd(killCmd, "", nil, 1*time.Minute) + return +} + +// StartBkDbmon start local bk-dbmon +func StartBkDbmon() (err error) { + startScript := filepath.Join(consts.BkDbmonPath, "start.sh") + if !FileExists(startScript) { + err = fmt.Errorf("%s not exists", startScript) + mylog.Logger.Error(err.Error()) + return + } + startCmd := fmt.Sprintf("su %s -c 'nohup %s &'", consts.MysqlAaccount, "sh "+startScript) + mylog.Logger.Info(startCmd) + _, err = RunLocalCmd("su", []string{consts.MysqlAaccount, "-c", "nohup sh " + startScript + " &"}, + "", nil, 1*time.Minute) + + if err != nil && strings.Contains(err.Error(), "no crontab for") { + return nil + } + + return +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/util/reflect.go b/dbm-services/mongo/db-tools/dbactuator/pkg/util/reflect.go new file mode 100644 index 0000000000..0698796aa2 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/util/reflect.go @@ -0,0 +1,20 @@ +package util + +import ( + "reflect" + "runtime" +) + +// GetTypeName 获取接口类型名 +func GetTypeName(object interface{}) string { + t := reflect.TypeOf(object) + if t.Kind() == reflect.Ptr { + return "*" + t.Elem().Name() + } + return t.Name() +} + +// GetFunctionName 获取函数名 +func GetFunctionName(i interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/util/util.go b/dbm-services/mongo/db-tools/dbactuator/pkg/util/util.go new file mode 100644 index 0000000000..836e907223 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/util/util.go @@ -0,0 +1,254 @@ +// Package util 公共函数 +package util + +import ( + "errors" + "fmt" + "net" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "syscall" + "time" + + "dbm-services/mongo/db-tools/dbactuator/pkg/consts" + + "golang.org/x/sys/unix" +) + +// NotFound error +const NotFound = "not found" + +// NewNotFound .. +func NewNotFound() error { + return errors.New(NotFound) +} + +// IsNotFoundErr .. +func IsNotFoundErr(err error) bool { + if err.Error() == NotFound { + return true + } + return false +} + +// GetCurrentDirectory 获取当前二进制程序所在执行路径 +func GetCurrentDirectory() (string, error) { + dir, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err != nil { + return dir, fmt.Errorf("convert absolute path failed, err: %+v", err) + } + dir = strings.Replace(dir, "\\", "/", -1) + return dir, nil +} + +// GetLocalIP 获得本地ip +func GetLocalIP() (string, error) { + var localIP string + var err error + addrs, err := net.InterfaceAddrs() + if err != nil { + return localIP, fmt.Errorf("GetLocalIP net.InterfaceAddrs fail,err:%v", err) + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + localIP = ipnet.IP.String() + return localIP, nil + } + } + } + return localIP, fmt.Errorf("can't find local ip") +} + +// IsMountPoint Determine if a directory is a mountpoint, by comparing the device for the directory +// with the device for it's parent. If they are the same, it's not a mountpoint, if they're +// different, it is. +// reference: https://github.com/cnaize/kubernetes/blob/master/pkg/util/mount/mountpoint_unix.go#L29 +func IsMountPoint(file string) (bool, error) { + stat, err := os.Stat(file) + if err != nil { + return false, err + } + rootStat, err := os.Lstat(file + "/..") + if err != nil { + return false, err + } + // If the directory has the same device as parent, then it's not a mountpoint. + return stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev, nil +} + +// FindFirstMountPoint find first mountpoint in prefer order +func FindFirstMountPoint(paths ...string) (string, error) { + for _, path := range paths { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + continue + } + } + isMountPoint, err := IsMountPoint(path) + if err != nil { + return "", fmt.Errorf("check whether mountpoint failed, path: %s, err: %v", path, err) + } + if isMountPoint { + return path, nil + } + } + return "", fmt.Errorf("no available mountpoint found, choices: %#v", paths) +} + +// CheckPortIsInUse 检查端口是否被占用 +func CheckPortIsInUse(ip, port string) (inUse bool, err error) { + timeout := time.Second + conn, err := net.DialTimeout("tcp", net.JoinHostPort(ip, port), timeout) + if err != nil && strings.Contains(err.Error(), "connection refused") { + return false, nil + } else if err != nil { + return false, fmt.Errorf("net.DialTimeout fail,err:%v", err) + } + if conn != nil { + defer func(conn net.Conn) { + _ = conn.Close() + }(conn) + return true, nil + } + return false, nil +} + +// IsValidIP 判断字符串是否是一个有效IP +func IsValidIP(ipStr string) bool { + if net.ParseIP(ipStr) == nil { + return false + } + return true +} + +// MkDirsIfNotExists 如果目录不存在则创建 +func MkDirsIfNotExists(dirs []string) error { + return MkDirsIfNotExistsWithPerm(dirs, 0755) +} + +// MkDirsIfNotExistsWithPerm 如果目录不存在则创建,并指定文件Perm +func MkDirsIfNotExistsWithPerm(dirs []string, perm os.FileMode) error { + for _, dir := range dirs { + _, err := os.Stat(dir) + if err == nil { + continue + } + if os.IsNotExist(err) == true { + err = os.MkdirAll(dir, perm) + if err != nil { + return fmt.Errorf("MkdirAll fail,err:%v,dir:%s", err, dirs) + } + } + } + return nil +} + +// IsExecOwner owner是否可执行 +func IsExecOwner(mode os.FileMode) bool { + return mode&0100 != 0 +} + +// IsExecGroup grouper是否可执行 +func IsExecGroup(mode os.FileMode) bool { + return mode&0010 != 0 +} + +// IsExecOther other是否可执行 +func IsExecOther(mode os.FileMode) bool { + return mode&0001 != 0 +} + +// IsExecAny owner/grouper/other 任意一个可执行 +func IsExecAny(mode os.FileMode) bool { + return mode&0111 != 0 +} + +// IsExecAll owner/grouper/other 全部可执行 +func IsExecAll(mode os.FileMode) bool { + return mode&0111 == 0111 +} + +// LocalDirChownMysql 改变localDir的属主为mysql +func LocalDirChownMysql(localDir string) (err error) { + cmd := fmt.Sprintf("chown -R %s.%s %s", consts.MysqlAaccount, consts.MysqlGroup, localDir) + _, err = RunBashCmd(cmd, "", nil, 1*time.Hour) + return +} + +// HostDiskUsage 本地路径所在磁盘使用情况 +type HostDiskUsage struct { + TotalSize uint64 `json:"ToTalSize"` // bytes + UsedSize uint64 `json:"UsedSize"` // bytes + AvailSize uint64 `json:"AvailSize"` // bytes + UsageRatio int `json:"UsageRatio"` +} + +// String 用于打印 +func (disk *HostDiskUsage) String() string { + ret := fmt.Sprintf("total_size=%dMB,used_size=%d,avail_size=%d,Use=%d%%", + disk.TotalSize/1024/1024, + disk.UsedSize/1024/1024, + disk.AvailSize/1024/1024, + disk.UsageRatio) + return ret +} + +// GetLocalDirDiskUsg 获取本地路径所在磁盘使用情况 +// 参考: +// https://stackoverflow.com/questions/20108520/get-amount-of-free-disk-space-using-go +// http://evertrain.blogspot.com/2018/05/golang-disk-free.html +func GetLocalDirDiskUsg(localDir string) (diskUsg HostDiskUsage, err error) { + var stat unix.Statfs_t + if err = unix.Statfs(localDir, &stat); err != nil { + err = fmt.Errorf("unix.Statfs fail,err:%v,localDir:%s", err, localDir) + return + } + diskUsg.TotalSize = stat.Blocks * uint64(stat.Bsize) + diskUsg.AvailSize = stat.Bavail * uint64(stat.Bsize) + diskUsg.UsedSize = (stat.Blocks - stat.Bfree) * uint64(stat.Bsize) + diskUsg.UsageRatio = int(diskUsg.UsedSize * 100 / diskUsg.TotalSize) + return +} + +// GetFileSize 获取文件大小(单位byte) +func GetFileSize(filename string) (size int64, err error) { + fileInfo, err := os.Stat(filename) + if err != nil { + err = fmt.Errorf("file:%s os.Stat fail,err:%v", filename, err) + return + } + return fileInfo.Size(), nil +} + +// IntSliceInter 两个[]int 求交集 +func IntSliceInter(list01, list02 []int) []int { + m01 := make(map[int]bool) + m02 := make(map[int]bool) + for _, item01 := range list01 { + m01[item01] = true + } + for _, item02 := range list02 { + m02[item02] = true + } + ret := []int{} + for item01 := range m01 { + if _, ok := m02[item01]; ok == true { + ret = append(ret, item01) + } + } + sort.Ints(ret) + return ret +} + +// IntSliceToString 将[]int join,返回一个字符串 +func IntSliceToString(src []int, seq string) string { + strList := []string{} + for _, item := range src { + strList = append(strList, strconv.Itoa(item)) + } + return strings.Join(strList, seq) +} diff --git a/dbm-services/mongo/db-tools/dbactuator/pkg/util/version.go b/dbm-services/mongo/db-tools/dbactuator/pkg/util/version.go new file mode 100644 index 0000000000..8e50739841 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/pkg/util/version.go @@ -0,0 +1,119 @@ +package util + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "dbm-services/mongo/db-tools/dbactuator/mylog" +) + +func convertVersionToUint(version string) (total uint64, err error) { + version = strings.TrimSpace(version) + if version == "" { + return 0, nil + } + list01 := strings.Split(version, ".") + var billion string + var thousand string + var single string + if len(list01) == 0 { + err = fmt.Errorf("version:%s format not correct", version) + mylog.Logger.Error(err.Error()) + return 0, err + } + billion = list01[0] + if len(list01) >= 2 { + thousand = list01[1] + } + if len(list01) >= 3 { + single = list01[2] + } + + if billion != "" { + b, err := strconv.ParseUint(billion, 10, 64) + if err != nil { + err = fmt.Errorf("convertVersionToUint strconv.ParseUint fail,err:%v,billion:%s,version:%s", err, billion, version) + mylog.Logger.Error(err.Error()) + return 0, err + } + total += b * 1000000 + } + if thousand != "" { + t, err := strconv.ParseUint(thousand, 10, 64) + if err != nil { + err = fmt.Errorf("convertVersionToUint strconv.ParseUint fail,err:%v,thousand:%s,version:%s", err, thousand, version) + mylog.Logger.Error(err.Error()) + return 0, err + } + total += t * 1000 + } + if single != "" { + s, err := strconv.ParseUint(single, 10, 64) + if err != nil { + err = fmt.Errorf("convertVersionToUint strconv.ParseUint fail,err:%v,single:%s,version:%s", err, single, version) + mylog.Logger.Error(err.Error()) + return 0, err + } + total += s + } + return total, nil +} + +// VersionParse tendis版本解析 +/* + * VersionParse + * 2.8.17-TRedis-v1.2.20, baseVersion: 2008017,subVersion:1002020 + * 6.2.7,baseVersion: 6002007 + */ +func VersionParse(version string) (baseVersion, subVersion uint64, err error) { + reg01 := regexp.MustCompile(`[\d+.]+`) + rets := reg01.FindAllString(version, -1) + if len(rets) == 0 { + err = fmt.Errorf("TendisVersionParse version:%s format not correct", version) + mylog.Logger.Error(err.Error()) + return 0, 0, err + } + if len(rets) >= 1 { + baseVersion, err = convertVersionToUint(rets[0]) + if err != nil { + return 0, 0, err + } + } + if len(rets) >= 2 { + subVersion, err = convertVersionToUint(rets[1]) + if err != nil { + return 0, 0, err + } + } + + return baseVersion, subVersion, nil +} + +// RedisCliVersion redis-cli 的版本解析 +func RedisCliVersion(cliBin string) (baseVersion, subVersion uint64, err error) { + cmd := cliBin + " -v" + verRet, err := RunBashCmd(cmd, "", nil, 20*time.Second) + if err != nil { + return + } + baseVersion, subVersion, err = VersionParse(verRet) + if err != nil { + return + } + return +} + +// IsCliSupportedNoAuthWarning redis-cli 是否支持 --no-auth-warning参数 +func IsCliSupportedNoAuthWarning(cliBin string) bool { + bVer, _, err := RedisCliVersion(cliBin) + if err != nil { + return false + } + if bVer > 6000000 { + return true + } + return false +} diff --git a/dbm-services/mongo/db-tools/dbactuator/scripts/upload.sh b/dbm-services/mongo/db-tools/dbactuator/scripts/upload.sh new file mode 100644 index 0000000000..ff9e93e222 --- /dev/null +++ b/dbm-services/mongo/db-tools/dbactuator/scripts/upload.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash + +# 安全模式 +set -euo pipefail + +# 重置PATH +PATH=/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin +export PATH + +# 通用脚本框架变量 +PROGRAM=$(basename "$0") +EXITCODE=0 + +BKREPO_USER= +BKREPO_PASSWORD= +BKREPO_API=http://127.0.0.1:8080 #介质库https地址 +BKREPO_PROJECT=generic # 项目代号 +BKREPO_NAME=bk-dbm # 仓库名字,默认自定义仓库 +DOWNLOAD_DIR=/tmp # 下载文件的默认路径:/tmp +BKREPO_METHOD=GET # 默认为下载 +BKREPO_PUT_OVERWRITE=true # 上传时是否覆盖仓库 +REMOTE_PATH= +declare -a REMOTE_FILE=() # 下载的文件列表 +declare -a UPLOAD_FILE=() # 上传的文件列表 + +trap 'rm -f /tmp/bkrepo_tool.*.log' EXIT +usage () { + cat < -p [ -d ] -r /devops/path1 -r /devops/path2 ... + $PROGRAM -u -p -X PUT -T local_file_path1 -T local_file_path2 -R remote_path + [ -u, --user [必填] "指定访问bkrepo的api用户名" ] + [ -p, --password [必填] "指定访问bkrepo的api密码" ] + [ -i, --url [必填] "指定访问bkrepo的url,默认是$BKREPO_API" ] + [ -r, --remote-file [必填] "指定下载的远程文件路径路径" ] + [ -n, --repo [选填] "指定项目的仓库名字,默认为$BKREPO_NAME" ] + [ -P, --project [选填] "指定项目名字,默认为blueking" ] + [ -d, --dir [选填] "指定下载制品库文件的存放文件夹,若不指定,则为/tmp" ] + [ -X, --method [选填] "默认为下载(GET),可选PUT,为上传" ] + + -X PUT时,以下参数生效: + [ -T, --upload-file [必填] "指定需要上传的本机文件路径" ] + [ -R, --remote-path [必填] "指定上传到的仓库目录的路径" ] + [ -O, --override [选填] "指定上传同名文件是否覆盖" ] + [ -h --help -? 查看帮助 ] +EOF +} + +usage_and_exit () { + usage + exit "$1" +} + +log () { + echo "$@" +} + +error () { + echo "$@" 1>&2 + usage_and_exit 1 +} + +warning () { + echo "$@" 1>&2 + EXITCODE=$((EXITCODE + 1)) +} + +# 解析命令行参数,长短混合模式 +(( $# == 0 )) && usage_and_exit 1 +while (( $# > 0 )); do + case "$1" in + -u | --user ) + shift + BKREPO_USER=$1 + ;; + -p | --password) + shift + BKREPO_PASSWORD=$1 + ;; + -i | --url) + shift + BKREPO_API=$1 + ;; + -d | --dir ) + shift + DOWNLOAD_DIR=$1 + ;; + -n | --name ) + shift + BKREPO_NAME=$1 + ;; + -P | --project ) + shift + BKREPO_PROJECT=$1 + ;; + -r | --remote-file ) + shift + REMOTE_FILE+=("$1") + ;; + -T | --upload-file ) + shift + UPLOAD_FILE+=("$1") + ;; + -O | --override) + BKREPO_PUT_OVERWRITE=true + ;; + -R | --remote-path ) + shift + REMOTE_PATH=$1 + ;; + -X | --method ) + shift + BKREPO_METHOD=$1 + ;; + --help | -h | '-?' ) + usage_and_exit 0 + ;; + -*) + error "不可识别的参数: $1" + ;; + *) + break + ;; + esac + shift +done + +if [[ -z "$BKREPO_USER" || -z "$BKREPO_PASSWORD" ]]; then + warning "-u, -p must not be empty" +fi + +if (( EXITCODE > 0 )); then + usage_and_exit "$EXITCODE" +fi + +case $BKREPO_METHOD in + GET ) + if ! [[ -d "$DOWNLOAD_DIR" ]]; then + mkdir -p "$DOWNLOAD_DIR" + fi + + cd "$DOWNLOAD_DIR" || { echo "can't change into $DOWNLOAD_DIR"; exit 1; } + + for remote_file in "${REMOTE_FILE[@]}"; do + echo "start downloading $remote_file ..." + curl -X "$BKREPO_METHOD" -sLO -u "$BKREPO_USER:$BKREPO_PASSWORD" "${BKREPO_API}/${BKREPO_PROJECT}/$BKREPO_NAME/$remote_file" + rt=$? + if [[ $rt -eq 0 ]]; then + echo "download $remote_file finished in $DOWNLOAD_DIR/${remote_file##*/}" + else + echo "download $remote_file with error code: <$rt>" + fi + done + ;; + PUT ) + for local_file in "${UPLOAD_FILE[@]}"; do + if [[ -r "$local_file" ]]; then + local_file_md5=$(md5sum "$local_file" | awk '{print $1}') + local_file_name=$(basename "$local_file") + http_code=$(curl -s -o /tmp/bkrepo_tool.$$.log -w "%{http_code}" \ + -u "$BKREPO_USER:$BKREPO_PASSWORD" "${BKREPO_API}/${BKREPO_PROJECT}/${BKREPO_NAME}/$REMOTE_PATH/$local_file_name" \ + -T "$local_file" \ + -H "X-BKREPO-OVERWRITE: $BKREPO_PUT_OVERWRITE" \ + -H "X-BKREPO-MD5: $local_file_md5" + ) + if [[ $http_code -eq 200 ]]; then + echo "upload $local_file to $REMOTE_PATH succeed" + else + echo "upload $local_file to $REMOTE_PATH failed" + echo "http response is: $( bool: name=kwargs["name"], immute_domain=kwargs["immute_domain"], alias=kwargs["alias"], + db_module_id=kwargs["db_module_id"], major_version=kwargs["major_version"], proxies=kwargs["proxies"], configs=kwargs["configs"], diff --git a/dbm-ui/backend/flow/plugins/components/collections/mongodb/exec_actuator_job.py b/dbm-ui/backend/flow/plugins/components/collections/mongodb/exec_actuator_job.py index 3912c8fe68..4b0e40a683 100644 --- a/dbm-ui/backend/flow/plugins/components/collections/mongodb/exec_actuator_job.py +++ b/dbm-ui/backend/flow/plugins/components/collections/mongodb/exec_actuator_job.py @@ -120,6 +120,12 @@ def _execute(self, data, parent_data) -> bool: kwargs["db_act_template"]["payload"]["adminUsername"] ] + # cluster添加shards,从上游流程节点获取密码 + if kwargs.get("add_shard_to_cluster", False): + kwargs["db_act_template"]["payload"]["adminPassword"] = trans_data[ + kwargs["db_act_template"]["payload"]["adminUsername"] + ] + # 拼接节点执行ip所需要的信息,ip信息统一用list处理拼接 if kwargs["get_trans_data_ip_var"]: exec_ips = self.splice_exec_ips_list( diff --git a/dbm-ui/backend/flow/utils/mongodb/calculate_cluster.py b/dbm-ui/backend/flow/utils/mongodb/calculate_cluster.py index 4c69a808f8..7251cbd33f 100644 --- a/dbm-ui/backend/flow/utils/mongodb/calculate_cluster.py +++ b/dbm-ui/backend/flow/utils/mongodb/calculate_cluster.py @@ -12,7 +12,206 @@ from backend.configuration.constants import AffinityEnum from backend.db_meta.enums.cluster_type import ClusterType -from backend.flow.consts import MongoDBDomainPrefix, MongoDBTotalCache +from backend.flow.consts import MongoDBClusterDefaultPort, MongoDBDomainPrefix, MongoDBTotalCache + + +def machine_order_by_tolerance(disaster_tolerance_level: str, machine_set: list) -> list: + """通过容灾级别获取机器顺序""" + + machines = [] + # 主从节点分布在不同的机房 + if disaster_tolerance_level == AffinityEnum.CROS_SUBZONE: + mongo_machine_set = deepcopy(machine_set) + machines.append(mongo_machine_set[0]) + mongo_machine_set.remove(mongo_machine_set[0]) + for machine in mongo_machine_set: + if machine["sub_zone_id"] != machines[0]["sub_zone_id"]: + machines.append(machine) + break + mongo_machine_set.remove(machines[1]) + machines.extend(mongo_machine_set) + # 主从节点分布在相同的机房 + elif disaster_tolerance_level == AffinityEnum.SAME_SUBZONE: + machines = machine_set + return machines + + +def replicase_calc(payload: dict, payload_clusters: dict, app: str, domain_prefix: list) -> dict: + """replicase进行计算""" + + payload_clusters["spec_id"] = payload["spec_id"] + payload_clusters["spec_config"] = payload["infos"][0]["resource_spec"]["spec_config"] + # 获取全部主机 + hosts = [] + for info in payload["infos"]: + for machine in info["mongo_machine_set"]: + hosts.append({"ip": machine["ip"], "bk_cloud_id": machine["bk_cloud_id"]}) + payload_clusters["hosts"] = hosts + # 获取复制集实例 + sets = [] + node_replica_count = payload["node_replica_count"] + port = payload["start_port"] + oplog_percent = payload["oplog_percent"] / 100 + data_disk = "/data1" + # 计算cacheSizeGB和oplogSizeMB bk_mem:MB ["/data1"]["size"]:GB + avg_mem_size_gb = int( + payload["infos"][0]["mongo_machine_set"][0]["bk_mem"] + * MongoDBTotalCache.Cache_Percent + / node_replica_count + / 1024 + ) + if payload["infos"][0]["mongo_machine_set"][0]["storage"].get("/data1"): + data_disk = "/data1" + elif payload["infos"][0]["mongo_machine_set"][0]["storage"].get("/data"): + data_disk = "/data" + oplog_size_mb = int( + payload["infos"][0]["mongo_machine_set"][0]["storage"].get(data_disk)["size"] + * 1024 + * oplog_percent + / node_replica_count + ) + # 分配机器 + for index, info in enumerate(payload["infos"]): + # 通过容灾获取机器顺序 + machines = machine_order_by_tolerance(payload["disaster_tolerance_level"], info["mongo_machine_set"]) + # 获取机器对应的多个复制集 + replica_sets = payload["replica_sets"][index * node_replica_count : node_replica_count * (index + 1)] + + for replica_set_index, replica_set in enumerate(replica_sets): + skip_machine = True + if replica_set_index == 0: + skip_machine = False + nodes = [] + for machine_index, machine in enumerate(machines): + if machine_index == len(machines) - 1: + domain = "{}.{}.{}.db".format(domain_prefix[-1], replica_set["set_id"], app) + else: + domain = "{}.{}.{}.db".format(domain_prefix[machine_index], replica_set["set_id"], app) + nodes.append({"ip": machine["ip"], "bk_cloud_id": machine["bk_cloud_id"], "domain": domain}) + sets.append( + { + "set_id": replica_set["set_id"], + "alias": replica_set["name"], + "port": port, + "key_file": "{}-{}".format(app, replica_set["set_id"]), + "cacheSizeGB": avg_mem_size_gb, + "oplogSizeMB": oplog_size_mb, + "skip_machine": skip_machine, + "nodes": nodes, + } + ) + port += 1 + payload_clusters["sets"] = sets + return payload_clusters + + +def cluster_calc(payload: dict, payload_clusters: dict, app: str) -> dict: + """cluster进行计算""" + + payload_clusters["alias"] = payload["cluster_alias"] + payload_clusters["cluster_id"] = payload["cluster_name"] + payload_clusters["machine_specs"] = payload["machine_specs"] + oplog_percent = payload["oplog_percent"] / 100 + disaster_tolerance_level = payload["disaster_tolerance_level"] + node_replica_count = int(payload["shard_num"] / payload["shard_machine_group"]) + payload_clusters["key_file"] = "{}-{}".format(app, payload["cluster_name"]) + config_port = MongoDBClusterDefaultPort.CONFIG_PORT.value # 设置常量 + shard_port = MongoDBClusterDefaultPort.SHARD_START_PORT.value # 以这个27001开始 + shard_port_not_use = [payload["proxy_port"], config_port] + + # 计算configCacheSizeGB,shardCacheSizeGB,oplogSizeMB + shard_avg_mem_size_gb = int( + payload["nodes"]["mongodb"][0][0]["bk_mem"] * MongoDBTotalCache.Cache_Percent / node_replica_count / 1024 + ) + config_mem_size_gb = int( + payload["nodes"]["mongo_config"][0]["bk_mem"] * MongoDBTotalCache.Cache_Percent / node_replica_count / 1024 + ) + # shard oplogSizeMB + if payload["nodes"]["mongodb"][0][0]["storage"].get("/data1"): + data_disk = "/data1" + elif payload["nodes"]["mongodb"][0][0]["storage"].get("/data"): + data_disk = "/data" + shard_oplog_size_mb = int( + payload["nodes"]["mongodb"][0][0]["storage"].get(data_disk)["size"] * 1024 * oplog_percent / node_replica_count + ) + # config oplogSizeMB + if payload["nodes"]["mongo_config"][0]["storage"].get("/data1"): + data_disk = "/data1" + elif payload["nodes"]["mongo_config"][0]["storage"].get("/data"): + data_disk = "/data" + config_oplog_size_mb = int( + payload["nodes"]["mongo_config"][0]["storage"].get(data_disk)["size"] * 1024 * oplog_percent + ) + + # 获取全部主机 + hosts = [] + # mongo_config + for machine in payload["nodes"]["mongo_config"]: + hosts.append({"ip": machine["ip"], "bk_cloud_id": machine["bk_cloud_id"]}) + # mongodb + for machines in payload["nodes"]["mongodb"]: + for machine in machines: + hosts.append({"ip": machine["ip"], "bk_cloud_id": machine["bk_cloud_id"]}) + # mongos + for machine in payload["nodes"]["mongos"]: + hosts.append({"ip": machine["ip"], "bk_cloud_id": machine["bk_cloud_id"]}) + payload_clusters["hosts"] = hosts + + # 分配机器 + # mongo_config + config = {} + config["set_id"] = "{}-{}".format(payload["cluster_name"], "conf") # 设置常量 + config["port"] = config_port # 设置常量 + config["cacheSizeGB"] = config_mem_size_gb + config["oplogSizeMB"] = config_oplog_size_mb + machines = machine_order_by_tolerance(disaster_tolerance_level, payload["nodes"]["mongo_config"]) + config["nodes"] = [] + for machine in machines: + config["nodes"].append({"ip": machine["ip"], "bk_cloud_id": machine["bk_cloud_id"]}) + payload_clusters["config"] = config + # shards + # 获取shard的id,port + shard_info = [] + add_shards = {} + for i in range(payload["shard_num"]): + if shard_port in shard_port_not_use: + shard_port += 1 + shard_info.append( + { + "set_id": "{}-s{}".format(payload["cluster_name"], str(i + 1)), + "port": shard_port, + "cacheSizeGB": shard_avg_mem_size_gb, + "oplogSizeMB": shard_oplog_size_mb, + } + ) + shard_port += 1 + shards = [] + for index, machine_set in enumerate(payload["nodes"]["mongodb"]): + # 通过容灾获取机器顺序 + machines = machine_order_by_tolerance(payload["disaster_tolerance_level"], machine_set) + # 获取机器对应的多个复制集 + replica_sets = shard_info[index * node_replica_count : node_replica_count * (index + 1)] + for replica_set in replica_sets: + nodes = [{"ip": machine["ip"], "bk_cloud_id": machine["bk_cloud_id"]} for machine in machines] + replica_set["nodes"] = nodes + shards.append(replica_set) + add_shards["{}-{}".format(app, replica_set["set_id"])] = ",".join( + ["{}:{}".format(node["ip"], str(replica_set["port"])) for node in nodes[0:-1]] + ) + + payload_clusters["shards"] = shards + payload_clusters["add_shards"] = add_shards + + # mongos + mongos = {} + mongos["port"] = payload["proxy_port"] # 默认27021 + mongos["set_id"] = payload["cluster_name"] + mongos["domain"] = "mongos.{}.{}.db".format(payload["cluster_name"], app) + nodes = [{"ip": machine["ip"], "bk_cloud_id": machine["bk_cloud_id"]} for machine in payload["nodes"]["mongos"]] + mongos["nodes"] = nodes + payload_clusters["mongos"] = mongos + + return payload_clusters def calculate_cluster(payload: dict) -> dict: @@ -28,6 +227,8 @@ def calculate_cluster(payload: dict) -> dict: payload_clusters["app"] = payload["bk_app_abbr"] app = payload["bk_app_abbr"] payload_clusters["db_version"] = payload["db_version"] + cluster_type = payload["cluster_type"] + # 目前只支持11个节点 domain_prefix = [ MongoDBDomainPrefix.M1, @@ -42,81 +243,9 @@ def calculate_cluster(payload: dict) -> dict: MongoDBDomainPrefix.M10, MongoDBDomainPrefix.BACKUP, ] - - if payload["cluster_type"] == ClusterType.MongoReplicaSet.value: - payload_clusters["spec_id"] = payload["spec_id"] - payload_clusters["spec_config"] = payload["infos"][0]["resource_spec"]["spec_config"] - # 获取全部主机 - hosts = [] - for info in payload["infos"]: - for machine in info["mongo_machine_set"]: - hosts.append({"ip": machine["ip"], "bk_cloud_id": machine["bk_cloud_id"]}) - payload_clusters["hosts"] = hosts - # 获取复制集实例 - sets = [] - node_replica_count = payload["node_replica_count"] - print("node_replica_count") - print(node_replica_count) - port = payload["start_port"] - oplog_percent = payload["oplog_percent"] / 100 - data_disk = "/data1" - # 计算cacheSizeGB和oplogSizeMB bk_mem:MB ["/data1"]["size"]:GB - avg_mem_size_gb = int( - payload["infos"][0]["mongo_machine_set"][0]["bk_mem"] - * MongoDBTotalCache.Cache_Percent - / node_replica_count - / 1024 - ) - if payload["infos"][0]["mongo_machine_set"][0]["storage"].get("/data1"): - data_disk = "/data1" - elif payload["infos"][0]["mongo_machine_set"][0]["storage"].get("/data"): - data_disk = "/data" - oplog_size_mb = int( - payload["infos"][0]["mongo_machine_set"][0]["storage"].get(data_disk)["size"] * 1024 * oplog_percent - ) - # 分配机器 - for index, info in enumerate(payload["infos"]): - machines = [] - # 主从节点分布在不同的机房 - if payload["disaster_tolerance_level"] == AffinityEnum.CROS_SUBZONE: - mongo_machine_set = deepcopy(info["mongo_machine_set"]) - if machines: - machines.clear() - machines.append(mongo_machine_set[0]) - mongo_machine_set.remove(mongo_machine_set[0]) - for machine in mongo_machine_set: - if machine["sub_zone_id"] != machines[0]["sub_zone_id"]: - machines.append(machine) - break - mongo_machine_set.remove(machines[1]) - machines.extend(mongo_machine_set) - elif payload["disaster_tolerance_level"] == AffinityEnum.SAME_SUBZONE: - machines = info["mongo_machine_set"] - replica_sets = payload["replica_sets"][index * node_replica_count : node_replica_count * (index + 1)] - for replica_set_index, replica_set in enumerate(replica_sets): - skip_machine = True - if replica_set_index == 0: - skip_machine = False - nodes = [] - for machine_index, machine in enumerate(machines): - if machine_index == len(machines) - 1: - domain = "{}.{}.{}.db".format(domain_prefix[-1], replica_set["set_id"], app) - else: - domain = "{}.{}.{}.db".format(domain_prefix[machine_index], replica_set["set_id"], app) - nodes.append({"ip": machine["ip"], "bk_cloud_id": machine["bk_cloud_id"], "domain": domain}) - sets.append( - { - "set_id": replica_set["set_id"], - "alias": replica_set["name"], - "port": port, - "cacheSizeGB": avg_mem_size_gb, - "oplogSizeMB": oplog_size_mb, - "skip_machine": skip_machine, - "nodes": nodes, - } - ) - port += 1 - payload_clusters["sets"] = sets - return payload_clusters - elif "cluster_type" == ClusterType.MongoShardedCluster.value: - pass + result = {} + if cluster_type == ClusterType.MongoReplicaSet.value: + result = replicase_calc(payload, payload_clusters, app, domain_prefix) + elif cluster_type == ClusterType.MongoShardedCluster.value: + result = cluster_calc(payload, payload_clusters, app) + return result diff --git a/dbm-ui/backend/flow/utils/mongodb/mongodb_dataclass.py b/dbm-ui/backend/flow/utils/mongodb/mongodb_dataclass.py index e9ef52b067..1cb9967c15 100644 --- a/dbm-ui/backend/flow/utils/mongodb/mongodb_dataclass.py +++ b/dbm-ui/backend/flow/utils/mongodb/mongodb_dataclass.py @@ -60,9 +60,9 @@ def __get_define_config(self, namespace: str, conf_file: str, conf_type: str) -> data = DBConfigApi.query_conf_item( params={ - "bk_biz_id": self.payload["bk_biz_id"], + "bk_biz_id": str(self.payload["bk_biz_id"]), "level_name": LevelName.APP, - "level_value": self.payload["bk_biz_id"], + "level_value": str(self.payload["bk_biz_id"]), "conf_file": conf_file, "conf_type": conf_type, "namespace": namespace, @@ -185,6 +185,7 @@ def get_install_mongod_kwargs(self, node: dict, cluster_role: str) -> dict: "instanceType": MediumEnum.MongoD, "app": self.payload["app"], "setId": self.replicaset_info["set_id"], + "keyFile": self.payload["key_file"], "auth": True, "clusterRole": cluster_role, "dbConfig": db_config, @@ -227,6 +228,7 @@ def get_install_mongos_kwargs(self, node: dict) -> dict: "instanceType": MediumEnum.MongoS, "app": self.payload["app"], "setId": self.mongos_info["set_id"], + "keyFile": self.payload["key_file"], "auth": True, "configDB": config_db, "dbConfig": db_config, @@ -275,10 +277,11 @@ def get_add_relationship_to_meta_kwargs(self, replicaset_info: dict) -> dict: """添加replicaset关系到meta的kwargs""" info = { - "bk_biz_id": int(self.payload["bk_biz_id"]), + "bk_biz_id": self.payload["bk_biz_id"], "major_version": self.payload["db_version"], "creator": self.payload["created_by"], "region": self.payload["city"], + "db_module_id": 0, } instance_role = [ InstanceRole.MONGO_M1, @@ -297,12 +300,12 @@ def get_add_relationship_to_meta_kwargs(self, replicaset_info: dict) -> dict: info["cluster_type"] = ClusterType.MongoReplicaSet.value info["skip_machine"] = replicaset_info["skip_machine"] info["immute_domain"] = replicaset_info["nodes"][0]["domain"] - info["name"] = "{}-{}".format(self.payload["app"], replicaset_info["set_id"]) + info["name"] = replicaset_info["set_id"] info["alias"] = replicaset_info["alias"] info["spec_id"] = self.payload["spec_id"] info["spec_config"] = self.payload["spec_config"] info["bk_cloud_id"] = replicaset_info["nodes"][0]["bk_cloud_id"] - info["db_module_id"] = 0 + # 复制集节点 info["storages"] = [] if len(replicaset_info["nodes"]) <= 11: @@ -327,7 +330,7 @@ def get_add_relationship_to_meta_kwargs(self, replicaset_info: dict) -> dict: ) elif self.payload["cluster_type"] == ClusterType.MongoShardedCluster.value: info["cluster_type"] = ClusterType.MongoShardedCluster.value - info["name"] = "{}-{}".format(self.payload["app"], self.payload["config"]["set_id"]) + info["name"] = self.payload["cluster_id"] info["alias"] = self.payload["alias"] info["bk_cloud_id"] = self.payload["config"]["nodes"][0]["bk_cloud_id"] info["machine_specs"] = self.payload["machine_specs"] @@ -338,22 +341,26 @@ def get_add_relationship_to_meta_kwargs(self, replicaset_info: dict) -> dict: ] # config info["configs"] = [] - # TODO config name - for index, node in enumerate(self.payload["config"]["nodes"]): - if index == len(self.payload["config"]["nodes"]) - 1: - info["configs"].append( - {"ip": node["ip"], "port": self.payload["config"]["port"], "role": instance_role[-1]} - ) - else: - info["configs"].append( - {"ip": node["ip"], "port": self.payload["config"]["port"], "role": instance_role[index]} - ) - + config = { + "shard": self.payload["config"]["set_id"], + "nodes": [], + } + if len(self.payload["config"]["nodes"]) <= 11: + for index, node in enumerate(self.payload["config"]["nodes"]): + if index == len(self.payload["config"]["nodes"]) - 1: + config["nodes"].append( + {"ip": node["ip"], "port": self.payload["config"]["port"], "role": instance_role[-1]} + ) + else: + config["nodes"].append( + {"ip": node["ip"], "port": self.payload["config"]["port"], "role": instance_role[index]} + ) + info["configs"].append(config) # shard info["storages"] = [] for shard in self.payload["shards"]: storage = { - "shard": "{}-{}".format(self.payload["app"], shard["set_id"]), + "shard": shard["set_id"], "nodes": [], } if len(shard["nodes"]) <= 11: @@ -371,6 +378,7 @@ def get_add_relationship_to_meta_kwargs(self, replicaset_info: dict) -> dict: def get_add_domain_to_dns_kwargs(self, cluster: bool) -> dict: """添加域名到dns的kwargs""" + if not cluster: domains = [ { @@ -398,6 +406,27 @@ def get_add_domain_to_dns_kwargs(self, cluster: bool) -> dict: "domains": domains, } + def get_add_shard_to_cluster_kwargs(self) -> dict: + """把shard添加到cluster的kwargs""" + + return { + "set_trans_data_dataclass": CommonContext.__name__, + "get_trans_data_ip_var": None, + "add_shard_to_cluster": True, + "bk_cloud_id": self.payload["mongos"]["nodes"][0]["bk_cloud_id"], + "exec_ip": self.payload["mongos"]["nodes"][0]["ip"], + "db_act_template": { + "action": MongoDBActuatorActionEnum.AddShardToCluster, + "file_path": self.file_path, + "payload": { + "ip": self.payload["mongos"]["nodes"][0]["ip"], + "port": self.payload["mongos"]["port"], + "adminUsername": MediumEnum.DbaUser, + "shards": self.payload["add_shards"], + }, + }, + } + def get_init_exec_script_kwargs(self, script_type: str) -> dict: """通过执行脚本"""