From d7db7c6eedaa2e7c91a029d0598d98ad6b65478a Mon Sep 17 00:00:00 2001
From: Cyber-SiKu <Cyber-SiKu@outlook.com>
Date: Tue, 7 Mar 2023 16:44:13 +0800
Subject: [PATCH] [feat]curvefs/curvebs: auto restart&mount

Signed-off-by: Cyber-SiKu <Cyber-SiKu@outlook.com>

Set the restart policy of the client container to unless-stopped,
so that it will automatically remount when the machine restarts

Signed-off-by: Cyber-SiKu <Cyber-SiKu@outlook.com>
---
 cli/command/target/list.go                    |   5 +-
 internal/common/common.go                     |   7 +
 internal/errno/errno.go                       |   1 +
 internal/task/step/container.go               |  17 ++
 internal/task/step/daemon.go                  | 161 ++++++++++++++++++
 internal/task/task/bs/add_target.go           |  27 ++-
 internal/task/task/bs/create_volume.go        |   1 +
 internal/task/task/bs/delete_target.go        |  18 ++
 internal/task/task/bs/list_targets.go         |  18 +-
 internal/task/task/bs/map.go                  |  14 ++
 internal/task/task/bs/start_nebd.go           |   1 +
 internal/task/task/bs/start_tgtd.go           |  18 +-
 internal/task/task/common/create_container.go |  13 +-
 internal/task/task/fs/mount.go                | 106 +++++++++++-
 internal/task/task/fs/umount.go               |   6 +
 internal/tui/targets.go                       |   6 +-
 internal/utils/common.go                      |   6 +
 pkg/module/docker_cli.go                      |   7 +
 playbook/automount/hosts.yaml                 |  20 +++
 playbook/automount/scripts/add_disk.sh        | 137 +++++++++++++++
 playbook/automount/scripts/del_disk.sh        |  73 ++++++++
 21 files changed, 627 insertions(+), 35 deletions(-)
 create mode 100644 internal/task/step/daemon.go
 create mode 100644 playbook/automount/hosts.yaml
 create mode 100644 playbook/automount/scripts/add_disk.sh
 create mode 100644 playbook/automount/scripts/del_disk.sh

diff --git a/cli/command/target/list.go b/cli/command/target/list.go
index 6214891be..bfdb03a96 100644
--- a/cli/command/target/list.go
+++ b/cli/command/target/list.go
@@ -26,6 +26,7 @@ import (
 	"github.com/opencurve/curveadm/cli/cli"
 	comm "github.com/opencurve/curveadm/internal/common"
 	"github.com/opencurve/curveadm/internal/playbook"
+	"github.com/opencurve/curveadm/internal/task/step"
 	"github.com/opencurve/curveadm/internal/task/task/bs"
 	"github.com/opencurve/curveadm/internal/tui"
 	cliutil "github.com/opencurve/curveadm/internal/utils"
@@ -80,10 +81,10 @@ func genListPlaybook(curveadm *cli.CurveAdm, options listOptions) (*playbook.Pla
 }
 
 func displayTargets(curveadm *cli.CurveAdm) {
-	targets := []bs.Target{}
+	targets := []step.Target{}
 	value := curveadm.MemStorage().Get(comm.KEY_ALL_TARGETS)
 	if value != nil {
-		m := value.(map[string]*bs.Target)
+		m := value.(map[string]*step.Target)
 		for _, target := range m {
 			targets = append(targets, *target)
 		}
diff --git a/internal/common/common.go b/internal/common/common.go
index 7887ef584..bd81cb5c0 100644
--- a/internal/common/common.go
+++ b/internal/common/common.go
@@ -130,3 +130,10 @@ const (
 	AUDIT_STATUS_FAIL
 	AUDIT_STATUS_CANCEL
 )
+
+// container restart policy
+const (
+	POLICY_ALWAYS_RESTART = "always"
+	POLICY_NEVER_RESTART  = "no"
+	POLICY_UNLESS_STOPPED = "unless-stopped"
+)
diff --git a/internal/errno/errno.go b/internal/errno/errno.go
index 4ac0f7204..bfa060355 100644
--- a/internal/errno/errno.go
+++ b/internal/errno/errno.go
@@ -539,6 +539,7 @@ var (
 	ERR_COPY_INTO_CONTAINER_FAILED      = EC(630011, "copy file into container failed (docker cp SRC_PATH CONTAINER:DEST_PATH)")
 	ERR_INSPECT_CONTAINER_FAILED        = EC(630012, "get container low-level information failed (docker inspect ID)")
 	ERR_GET_CONTAINER_LOGS_FAILED       = EC(630013, "get container logs failed (docker logs ID)")
+	ERR_UPDATE_CONTAINER_FAILED         = EC(630014, "update container failed (docker update ID)")
 
 	// 690: execuetr task (others)
 	ERR_START_CRONTAB_IN_CONTAINER_FAILED = EC(690000, "start crontab in container failed")
diff --git a/internal/task/step/container.go b/internal/task/step/container.go
index 52da2b6e0..54ec1dc92 100644
--- a/internal/task/step/container.go
+++ b/internal/task/step/container.go
@@ -152,6 +152,14 @@ type (
 		Success     *bool
 		module.ExecOptions
 	}
+
+	UpdateContainer struct {
+		ContainerId *string
+		Restart     string
+		Out         *string
+		Success     *bool
+		module.ExecOptions
+	}
 )
 
 func (s *DockerInfo) Execute(ctx *context.Context) error {
@@ -312,3 +320,12 @@ func (s *ContainerLogs) Execute(ctx *context.Context) error {
 	out, err := cli.Execute(s.ExecOptions)
 	return PostHandle(s.Success, s.Out, out, err, errno.ERR_GET_CONTAINER_LOGS_FAILED)
 }
+
+func (s *UpdateContainer) Execute(ctx *context.Context) error {
+	cli := ctx.Module().DockerCli().UpdateContainer(*s.ContainerId)
+	if len(s.Restart) > 0 {
+		cli.AddOption("--restart %s", s.Restart)
+	}
+	out, err := cli.Execute(s.ExecOptions)
+	return PostHandle(s.Success, s.Out, out, err, errno.ERR_UPDATE_CONTAINER_FAILED)
+}
diff --git a/internal/task/step/daemon.go b/internal/task/step/daemon.go
new file mode 100644
index 000000000..71a0cd4e1
--- /dev/null
+++ b/internal/task/step/daemon.go
@@ -0,0 +1,161 @@
+/*
+ *  Copyright (c) 2023 NetEase Inc.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+/*
+ * Project: CurveAdm
+ * Created Date: 2023-03-16
+ * Author: Cyber-SiKu
+ */
+
+package step
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"strings"
+
+	comm "github.com/opencurve/curveadm/internal/common"
+	"github.com/opencurve/curveadm/internal/task/context"
+	"github.com/opencurve/curveadm/internal/utils"
+	"github.com/opencurve/curveadm/pkg/module"
+)
+
+const (
+	AFTER_TASK_DIR = "/curve/init.d/"
+)
+
+var ALLOC_ID int = 0
+
+type afterRunTask struct {
+	ID         int      `json:"ID"`
+	Path       string   `json:"Path"`
+	Args       []string `json:"Args"`
+	Env        []string `json:"Env"`
+	Dir        string   `json:"Dir"`
+	OutputPath string   `json:"OutputPath"`
+	InputPath  string   `json:"InputPath"`
+}
+
+func (task afterRunTask) ToString() string {
+	b, err := json.Marshal(task)
+	if err != nil {
+		return ""
+	}
+	return string(b)
+}
+
+func newDaemonTask(path string, args ...string) *afterRunTask {
+	task := afterRunTask{
+		ID:   ALLOC_ID,
+		Path: path,
+		Args: args,
+	}
+	return &task
+}
+
+type AddDaemonTask struct {
+	ContainerId *string
+	Cmd         string
+	Args        []string
+	TaskName    string
+	module.ExecOptions
+}
+
+type DelDaemonTask struct {
+	ContainerId *string
+	Tid         string
+	MemStorage  *utils.SafeMap
+	module.ExecOptions
+}
+
+func (s *AddDaemonTask) getAllocId(ctx *context.Context) {
+	if ALLOC_ID != 0 {
+		ALLOC_ID++
+		return
+	} else {
+		// first add daemon task
+		// create dir AFTER_TASK_DIR
+		step := ContainerExec{
+			ContainerId: s.ContainerId,
+			Command:     fmt.Sprintf("mkdir -p %s", AFTER_TASK_DIR),
+			ExecOptions: s.ExecOptions,
+		}
+		err := step.Execute(ctx)
+		if err != nil {
+			return
+		}
+	}
+	var count string
+	// get max id
+	// The contents of the file are as follows:
+	// {"ID":1,"Path":"tgtd","Args":null,"Env":null,"Dir":"","OutputPath":"","InputPath":""}
+	step := ContainerExec{
+		ContainerId: s.ContainerId,
+		Command:     fmt.Sprintf("grep -r '\"ID\":[0-9]*,' %s | awk -F \":\" '{print $3}' | awk -F \",\" '{print $1}' | sort -n | tail -1", AFTER_TASK_DIR),
+		Out:         &count,
+		ExecOptions: s.ExecOptions,
+	}
+	err := step.Execute(ctx)
+	if err != nil {
+		ALLOC_ID = 1
+	}
+	id, err := strconv.Atoi(count)
+	if err != nil {
+		ALLOC_ID = 1
+	}
+	ALLOC_ID = id + 1
+}
+
+func (s *AddDaemonTask) Execute(ctx *context.Context) error {
+	s.getAllocId(ctx)
+	content := newDaemonTask(s.Cmd, s.Args...).ToString()
+	step := InstallFile{
+		Content:           &content,
+		ContainerId:       s.ContainerId,
+		ContainerDestPath: AFTER_TASK_DIR + s.TaskName + ".task",
+		ExecOptions:       s.ExecOptions,
+	}
+	return step.Execute(ctx)
+}
+
+type Target struct {
+	Host   string
+	Tid    string
+	Name   string
+	Store  string
+	Portal string
+}
+
+func (s *DelDaemonTask) Execute(ctx *context.Context) error {
+	v := s.MemStorage.Get(comm.KEY_ALL_TARGETS)
+	target := v.(map[string]*Target)[s.Tid]
+	if target == nil {
+		return nil
+	}
+	stores := strings.Split(target.Store, "//")
+	if len(stores) < 2 {
+		// unable to recognize cbd:pool
+		return nil
+	}
+	path := AFTER_TASK_DIR + "addTarget_" + stores[1] + ".task"
+	step := ContainerExec{
+		ContainerId: s.ContainerId,
+		Command:     "rm -f " + path,
+		ExecOptions: s.ExecOptions,
+	}
+	return step.Execute(ctx)
+}
diff --git a/internal/task/task/bs/add_target.go b/internal/task/task/bs/add_target.go
index a5f3beee5..4c59ee6bb 100644
--- a/internal/task/task/bs/add_target.go
+++ b/internal/task/task/bs/add_target.go
@@ -24,6 +24,8 @@ package bs
 
 import (
 	"fmt"
+	"regexp"
+	"strconv"
 
 	"github.com/opencurve/curveadm/cli/cli"
 	comm "github.com/opencurve/curveadm/internal/common"
@@ -34,12 +36,12 @@ import (
 )
 
 type TargetOption struct {
-	Host   string
-	User   string
-	Volume string
-	Create bool
-	Size   int
-	Tid    string
+	Host      string
+	User      string
+	Volume    string
+	Create    bool
+	Size      int
+	Tid       string
 	Blocksize uint64
 }
 
@@ -91,5 +93,18 @@ func NewAddTargetTask(curveadm *cli.CurveAdm, cc *configure.ClientConfig) (*task
 		ExecOptions: curveadm.ExecOptions(),
 	})
 
+	t.AddStep(&step.AddDaemonTask{ // install addTarget.task
+		ContainerId: &containerId,
+		Cmd:         "/bin/bash",
+		Args:        []string{targetScriptPath, user, volume, strconv.FormatBool(options.Create), strconv.Itoa(options.Size), strconv.FormatUint(options.Blocksize, 10)},
+		TaskName:    "addTarget"+TranslateVolumeName(volume, user),
+		ExecOptions: curveadm.ExecOptions(),
+	})
+
 	return t, nil
 }
+
+func TranslateVolumeName(volume, user string) string {
+	reg, _ := regexp.Compile("[^a-zA-Z0-9]+")
+	return reg.ReplaceAllString(volume, "_") + "_" + user + "_"
+}
diff --git a/internal/task/task/bs/create_volume.go b/internal/task/task/bs/create_volume.go
index 41607ba96..02e585d45 100644
--- a/internal/task/task/bs/create_volume.go
+++ b/internal/task/task/bs/create_volume.go
@@ -84,6 +84,7 @@ func NewCreateVolumeTask(curveadm *cli.CurveAdm, cc *configure.ClientConfig) (*t
 	script := scripts.CREATE_VOLUME
 	scriptPath := "/curvebs/nebd/sbin/create.sh"
 	command := fmt.Sprintf("/bin/bash %s %s %s %d", scriptPath, options.User, options.Volume, options.Size)
+
 	t.AddStep(&step.ListContainers{
 		ShowAll:     true,
 		Format:      "'{{.Status}}'",
diff --git a/internal/task/task/bs/delete_target.go b/internal/task/task/bs/delete_target.go
index e5e58cb61..7f5a6135e 100644
--- a/internal/task/task/bs/delete_target.go
+++ b/internal/task/task/bs/delete_target.go
@@ -75,6 +75,24 @@ func NewDeleteTargetTask(curveadm *cli.CurveAdm, cc *client.ClientConfig) (*task
 	t.AddStep(&step2CheckTgtdStatus{
 		output: &output,
 	})
+	t.AddStep(&step.ContainerExec{
+		ContainerId: &containerId,
+		Command:     fmt.Sprintf("tgtadm --lld iscsi --mode target --op show"),
+		Out:         &output,
+		ExecOptions: curveadm.ExecOptions(),
+	})
+	t.AddStep(&step2FormatTarget{
+		host:       options.Host,
+		hostname:   hc.GetHostname(),
+		output:     &output,
+		memStorage: curveadm.MemStorage(),
+	})
+	t.AddStep(&step.DelDaemonTask{
+		ContainerId: &containerId,
+		Tid:         tid,
+		MemStorage:  curveadm.MemStorage(),
+		ExecOptions: curveadm.ExecOptions(),
+	})
 	t.AddStep(&step.ContainerExec{
 		ContainerId: &containerId,
 		Command:     fmt.Sprintf("tgtadm --lld iscsi --mode target --op delete --tid %s", tid),
diff --git a/internal/task/task/bs/list_targets.go b/internal/task/task/bs/list_targets.go
index ccdd618ad..5e897bf75 100644
--- a/internal/task/task/bs/list_targets.go
+++ b/internal/task/task/bs/list_targets.go
@@ -46,22 +46,14 @@ type (
 		output     *string
 		memStorage *utils.SafeMap
 	}
-
-	Target struct {
-		Host   string
-		Tid    string
-		Name   string
-		Store  string
-		Portal string
-	}
 )
 
-func addTarget(memStorage *utils.SafeMap, id string, target *Target) {
+func addTarget(memStorage *utils.SafeMap, id string, target *step.Target) {
 	memStorage.TX(func(kv *utils.SafeMap) error {
-		m := map[string]*Target{}
+		m := map[string]*step.Target{}
 		v := kv.Get(comm.KEY_ALL_TARGETS)
 		if v != nil {
-			m = v.(map[string]*Target)
+			m = v.(map[string]*step.Target)
 		}
 		m[id] = target
 		kv.Set(comm.KEY_ALL_TARGETS, m)
@@ -84,13 +76,13 @@ func (s *step2FormatTarget) Execute(ctx *context.Context) error {
 	output := *s.output
 	lines := strings.Split(output, "\n")
 
-	var target *Target
+	var target *step.Target
 	titlePattern := regexp.MustCompile("^Target ([0-9]+): (.+)$")
 	storePattern := regexp.MustCompile("Backing store path: (cbd:pool//.+)$")
 	for _, line := range lines {
 		mu := titlePattern.FindStringSubmatch(line)
 		if len(mu) > 0 {
-			target = &Target{
+			target = &step.Target{
 				Host: s.host,
 				Tid:    mu[1],
 				Name:   mu[2],
diff --git a/internal/task/task/bs/map.go b/internal/task/task/bs/map.go
index 26f9a6a13..dcfdfcf62 100644
--- a/internal/task/task/bs/map.go
+++ b/internal/task/task/bs/map.go
@@ -106,6 +106,13 @@ func NewMapTask(curveadm *cli.CurveAdm, cc *configure.ClientConfig) (*task.Task,
 		Args:        []string{"nbds_max=64"},
 		ExecOptions: curveadm.ExecOptions(),
 	})
+	t.AddStep(&step.AddDaemonTask{ // install modprobe.task
+		ContainerId: &containerId,
+		Cmd:         "modprobe",
+		Args:        []string{comm.KERNERL_MODULE_NBD, "nbds_max=64"},
+		TaskName:    "modProbe",
+		ExecOptions: curveadm.ExecOptions(),
+	})
 	t.AddStep(&step.SyncFile{ // sync nebd-client config
 		ContainerSrcId:    &containerId,
 		ContainerSrcPath:  "/curvebs/conf/nebd-client.conf",
@@ -130,6 +137,13 @@ func NewMapTask(curveadm *cli.CurveAdm, cc *configure.ClientConfig) (*task.Task,
 		Mutate:            newToolsV2Mutate(cc, TOOLSV2_CONFIG_DELIMITER),
 		ExecOptions:       curveadm.ExecOptions(),
 	})
+	t.AddStep(&step.AddDaemonTask{ // install map.task
+		ContainerId: &containerId,
+		Cmd:         "/bin/bash",
+		Args:        []string{scriptPath, options.User, options.Volume, mapOptions},
+		TaskName:    "map",
+		ExecOptions: curveadm.ExecOptions(),
+	})
 	t.AddStep(&step.ContainerExec{
 		ContainerId: &containerId,
 		Command:     command,
diff --git a/internal/task/task/bs/start_nebd.go b/internal/task/task/bs/start_nebd.go
index aaee80172..d71b34d7e 100644
--- a/internal/task/task/bs/start_nebd.go
+++ b/internal/task/task/bs/start_nebd.go
@@ -194,6 +194,7 @@ func NewStartNEBDServiceTask(curveadm *cli.CurveAdm, cc *configure.ClientConfig)
 		Privileged:  true,
 		Volumes:     getVolumes(cc),
 		Out:         &containerId,
+		Restart:     comm.POLICY_UNLESS_STOPPED,
 		ExecOptions: curveadm.ExecOptions(),
 	})
 	t.AddStep(&step2InsertClient{
diff --git a/internal/task/task/bs/start_tgtd.go b/internal/task/task/bs/start_tgtd.go
index 0aa917149..7c5142b63 100644
--- a/internal/task/task/bs/start_tgtd.go
+++ b/internal/task/task/bs/start_tgtd.go
@@ -100,6 +100,7 @@ func NewStartTargetDaemonTask(curveadm *cli.CurveAdm, cc *configure.ClientConfig
 		Privileged:  true,
 		Volumes:     getVolumes(cc),
 		Out:         &containerId,
+		Restart:     comm.POLICY_UNLESS_STOPPED,
 		ExecOptions: curveadm.ExecOptions(),
 	})
 	for _, filename := range []string{"client.conf", "nebd-server.conf"} {
@@ -113,6 +114,15 @@ func NewStartTargetDaemonTask(curveadm *cli.CurveAdm, cc *configure.ClientConfig
 			ExecOptions:       curveadm.ExecOptions(),
 		})
 	}
+	t.AddStep(&step.SyncFile{ // sync client configuration for tgtd
+		ContainerSrcId:    &containerId,
+		ContainerSrcPath:  "/curvebs/conf/client.conf",
+		ContainerDestId:   &containerId,
+		ContainerDestPath: "/etc/curve/client.conf",
+		KVFieldSplit:      CLIENT_CONFIG_DELIMITER,
+		Mutate:            newMutate(cc, CLIENT_CONFIG_DELIMITER),
+		ExecOptions:       curveadm.ExecOptions(),
+	})
 	t.AddStep(&step.SyncFile{ // sync nebd-client config
 		ContainerSrcId:    &containerId,
 		ContainerSrcPath:  "/curvebs/conf/nebd-client.conf",
@@ -128,8 +138,14 @@ func NewStartTargetDaemonTask(curveadm *cli.CurveAdm, cc *configure.ClientConfig
 		ExecOptions: curveadm.ExecOptions(),
 	})
 	t.AddStep(&step.ContainerExec{
-		Command:     "tgtd -f &",
+		Command:     "tgtd",
+		ContainerId: &containerId,
+		ExecOptions: curveadm.ExecOptions(),
+	})
+	t.AddStep(&step.AddDaemonTask{ // install tgtd.task
 		ContainerId: &containerId,
+		Cmd:         "tgtd",
+		TaskName:    "tgtd",
 		ExecOptions: curveadm.ExecOptions(),
 	})
 
diff --git a/internal/task/task/common/create_container.go b/internal/task/task/common/create_container.go
index d2cc87892..7de49259c 100644
--- a/internal/task/task/common/create_container.go
+++ b/internal/task/task/common/create_container.go
@@ -40,11 +40,6 @@ import (
 	log "github.com/opencurve/curveadm/pkg/log/glg"
 )
 
-const (
-	POLICY_ALWAYS_RESTART = "always"
-	POLICY_NEVER_RESTART  = "no"
-)
-
 type step2GetService struct {
 	serviceId   string
 	containerId *string
@@ -193,15 +188,15 @@ func getMountVolumes(dc *topology.DeployConfig, serviceMountDevice bool) []step.
 func getRestartPolicy(dc *topology.DeployConfig, serviceMountDevice bool) string {
 	switch dc.GetRole() {
 	case topology.ROLE_ETCD:
-		return POLICY_ALWAYS_RESTART
+		return comm.POLICY_ALWAYS_RESTART
 	case topology.ROLE_MDS:
-		return POLICY_ALWAYS_RESTART
+		return comm.POLICY_ALWAYS_RESTART
 	case topology.ROLE_CHUNKSERVER:
 		if serviceMountDevice {
-			return POLICY_ALWAYS_RESTART
+			return comm.POLICY_ALWAYS_RESTART
 		}
 	}
-	return POLICY_NEVER_RESTART
+	return comm.POLICY_NEVER_RESTART
 }
 
 func trimContainerId(containerId *string) step.LambdaType {
diff --git a/internal/task/task/fs/mount.go b/internal/task/task/fs/mount.go
index 95c02e0fb..a9a48161c 100644
--- a/internal/task/task/fs/mount.go
+++ b/internal/task/task/fs/mount.go
@@ -27,8 +27,10 @@ import (
 	"errors"
 	"fmt"
 	"strings"
+	"time"
 
 	"github.com/opencurve/curveadm/cli/cli"
+	"github.com/opencurve/curveadm/internal/common"
 	comm "github.com/opencurve/curveadm/internal/common"
 	"github.com/opencurve/curveadm/internal/configure"
 	"github.com/opencurve/curveadm/internal/configure/topology"
@@ -39,6 +41,7 @@ import (
 	"github.com/opencurve/curveadm/internal/task/task"
 	"github.com/opencurve/curveadm/internal/task/task/checker"
 	"github.com/opencurve/curveadm/internal/utils"
+	"github.com/opencurve/curveadm/pkg/module"
 )
 
 const (
@@ -50,6 +53,10 @@ const (
 	KEY_CURVEBS_CLUSTER = "curvebs.cluster"
 
 	CURVEBS_CONF_PATH = "/etc/curve/client.conf"
+
+	CURVEFS_LIST_FS = "curvefs_tool list-fs"
+
+	CHECK_MOUTPOINT_TIMES = 3
 )
 
 type (
@@ -72,6 +79,12 @@ type (
 		MountPoint string `json:"mount_point,"`
 		Config     string `json:"config,omitempty"` // TODO(P1)
 	}
+
+	CheckMountDone struct {
+		ContainerId *string
+		MountPoint  string
+		module.ExecOptions
+	}
 )
 
 var (
@@ -358,6 +371,7 @@ func NewMountFSTask(curveadm *cli.CurveAdm, cc *configure.ClientConfig) (*task.T
 		Privileged:        true,
 		Out:               &containerId,
 		ExecOptions:       curveadm.ExecOptions(),
+		Restart:           common.POLICY_ALWAYS_RESTART,
 	})
 	t.AddStep(&step2InsertClient{
 		curveadm:    curveadm,
@@ -420,8 +434,98 @@ func NewMountFSTask(curveadm *cli.CurveAdm, cc *configure.ClientConfig) (*task.T
 	t.AddStep(&step.Lambda{
 		Lambda: checkStartContainerStatus(&success, &out),
 	})
-	// TODO(P0): wait mount done
+	t.AddStep(&CheckMountDone{
+		ContainerId: &containerId,
+		MountPoint:  mountPoint,
+		ExecOptions: curveadm.ExecOptions(),
+	})
+	t.AddStep(&step.UpdateContainer{
+		ContainerId: &containerId,
+		Restart:     comm.POLICY_UNLESS_STOPPED,
+		ExecOptions: curveadm.ExecOptions(),
+	})
 
 	return t, nil
 
 }
+
+func (s *CheckMountDone) Execute(ctx *context.Context) error {
+	steps := []task.Step{}
+	var success bool
+	var out, hostname string
+	// get hostname
+	steps = append(steps, &step.Hostname{
+		Success:     &success,
+		Out:         &hostname,
+		ExecOptions: s.ExecOptions,
+	})
+	for i := 0; i < CHECK_MOUTPOINT_TIMES; i++ {
+		steps = append(steps, &step.ContainerExec{
+			ContainerId: s.ContainerId,
+			Command:     CURVEFS_LIST_FS,
+			Success:     &success,
+			Out:         &out,
+			ExecOptions: s.ExecOptions,
+		})
+	}
+	// list fs 3 times to check mountpoint
+	// if no this mountpoint stop container
+	steps = append(steps, &step.StopContainer{
+		ContainerId: *s.ContainerId,
+		ExecOptions: s.ExecOptions,
+	})
+	mountPoint := configure.GetFSClientMountPath(s.MountPoint)
+	for _, step := range steps {
+		time.Sleep(time.Duration(1) * time.Second)
+		err := step.Execute(ctx)
+		if err == nil && success && out != "" {
+			if checkMountpointExist(out, mountPoint, hostname) {
+				return nil
+			}
+		}
+	}
+	return errno.ERR_MOUNT_FILESYSTEM_FAILED
+}
+
+const (
+	JSON_FS_INFO     = "fsInfo"
+	JSON_MOUNT_NUM   = "mountNum"
+	JSON_MOUNTPOINTS = "mountpoints"
+	JSON_HOSTNAME    = "hostname"
+	JSON_PATH        = "path"
+)
+
+func checkMountpointExist(out, path, hostname string) bool {
+	var jsonMap map[string]interface{}
+	err := json.Unmarshal([]byte(out), &jsonMap)
+	if err != nil || jsonMap[JSON_FS_INFO] == nil {
+		return false
+	}
+
+	if !utils.IsAnySlice(jsonMap[JSON_FS_INFO]) {
+		return false
+	}
+
+	for _, fsinfo := range jsonMap[JSON_FS_INFO].([]interface{}) {
+		if !utils.IsStringAnyMap(fsinfo) {
+			continue
+		}
+		info := fsinfo.(map[string]interface{})
+		if !utils.IsFloat64(info[JSON_MOUNT_NUM]) ||
+			int(info[JSON_MOUNT_NUM].(float64)) <= 0 ||
+			!utils.IsAnySlice(info[JSON_MOUNTPOINTS].([]interface{})) {
+			continue
+		}
+		for _, mountpoint := range info[JSON_MOUNTPOINTS].([]interface{}) {
+			if !utils.IsStringAnyMap(mountpoint) {
+				continue
+			}
+			point := mountpoint.(map[string]interface{})
+			if point[JSON_HOSTNAME] == hostname && point[JSON_PATH] == path {
+				return true
+			}
+		}
+	}
+
+	return false
+}
diff --git a/internal/task/task/fs/umount.go b/internal/task/task/fs/umount.go
index 6c14c93af..8d4b35d05 100644
--- a/internal/task/task/fs/umount.go
+++ b/internal/task/task/fs/umount.go
@@ -147,6 +147,12 @@ func NewUmountFSTask(curveadm *cli.CurveAdm, v interface{}) (*task.Task, error)
 		mountPoint:  options.MountPoint,
 		curveadm:    curveadm,
 	})
+	if containerId != "" {
+		t.AddStep(&step.StopContainer{
+			ContainerId: containerId,
+			ExecOptions: curveadm.ExecOptions(),
+		})
+	}
 	t.AddStep(&step2RemoveContainer{
 		status:      &status,
 		containerId: containerId,
diff --git a/internal/tui/targets.go b/internal/tui/targets.go
index 262ca09ba..2bda6a389 100644
--- a/internal/tui/targets.go
+++ b/internal/tui/targets.go
@@ -25,19 +25,19 @@ package tui
 import (
 	"sort"
 
-	task "github.com/opencurve/curveadm/internal/task/task/bs"
+	"github.com/opencurve/curveadm/internal/task/step"
 	"github.com/opencurve/curveadm/internal/tui/common"
 	tuicommon "github.com/opencurve/curveadm/internal/tui/common"
 )
 
-func sortTargets(targets []task.Target) {
+func sortTargets(targets []step.Target) {
 	sort.Slice(targets, func(i, j int) bool {
 		t1, t2 := targets[i], targets[j]
 		return t1.Tid < t2.Tid
 	})
 }
 
-func FormatTargets(targets []task.Target) string {
+func FormatTargets(targets []step.Target) string {
 	lines := [][]interface{}{}
 	title := []string{"Tid", "Host", "Target Name", "Store", "Portal"}
 	first, second := tuicommon.FormatTitle(title)
diff --git a/internal/utils/common.go b/internal/utils/common.go
index ec310a615..be9aae86a 100644
--- a/internal/utils/common.go
+++ b/internal/utils/common.go
@@ -74,6 +74,8 @@ func Type(v interface{}) string {
 		return "string_interface_map"
 	case []interface{}:
 		return "any_slice"
+	case float64:
+		return "float64"
 	default:
 		return "unknown"
 	}
@@ -107,6 +109,10 @@ func IsFunc(v interface{}) bool {
 	return reflect.TypeOf(v).Kind() == reflect.Func
 }
 
+func IsFloat64(v interface{}) bool {
+	return Type(v) == "float64"
+}
+
 func All2Str(v interface{}) (value string, ok bool) {
 	ok = true
 	if IsString(v) {
diff --git a/pkg/module/docker_cli.go b/pkg/module/docker_cli.go
index 83fe1ccfd..40b588b97 100644
--- a/pkg/module/docker_cli.go
+++ b/pkg/module/docker_cli.go
@@ -45,6 +45,7 @@ const (
 	TEMPLATE_COPY_INTO_CONTAINER = "docker cp {{.options}}  {{.srcPath}} {{.container}}:{{.destPath}}"
 	TEMPLATE_INSPECT_CONTAINER   = "docker inspect {{.options}} {{.container}}"
 	TEMPLATE_CONTAINER_LOGS      = "docker logs {{.options}} {{.container}}"
+	TEMPLATE_UPDATE_CONTAINER = "docker update {{.options}} {{.container}}"
 )
 
 type DockerCli struct {
@@ -160,3 +161,9 @@ func (cli *DockerCli) ContainerLogs(containerId string) *DockerCli {
 	cli.data["container"] = containerId
 	return cli
 }
+
+func (cli *DockerCli) UpdateContainer(containerId string) *DockerCli {
+	cli.tmpl = template.Must(template.New("UpdateContainer").Parse(TEMPLATE_UPDATE_CONTAINER))
+	cli.data["container"] = containerId
+	return cli
+}
diff --git a/playbook/automount/hosts.yaml b/playbook/automount/hosts.yaml
new file mode 100644
index 000000000..0e78c6b3b
--- /dev/null
+++ b/playbook/automount/hosts.yaml
@@ -0,0 +1,20 @@
+global:
+  user: curve
+  ssh_port: 22
+  private_key_file: /home/curve/.ssh/id_rsa
+
+hosts:
+  - host: server-host1
+    hostname: 10.0.1.1
+    labels:
+      - automount
+    envs:
+      - SUDO_ALIAS=sudo
+      - OPTIONS=""
+  - host: server-host2
+    hostname: 10.0.1.2
+    labels:
+      - automount
+    envs:
+      - SUDO_ALIAS=sudo
+      - OPTIONS=""
\ No newline at end of file
diff --git a/playbook/automount/scripts/add_disk.sh b/playbook/automount/scripts/add_disk.sh
new file mode 100644
index 000000000..26a761ce9
--- /dev/null
+++ b/playbook/automount/scripts/add_disk.sh
@@ -0,0 +1,137 @@
+#!/usr/bin/env bash
+
+#
+#  Copyright (c) 2023 NetEase Inc.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+
+# check parameters
+if [ $# -ne 2 ]; then
+    echo "Usage: $0 <device> <mountpoint>"
+    exit 1
+fi
+
+g_device=$1
+g_mountpoint=$2
+g_systemd_name=""
+g_disk_uuid=""
+g_filesystem_type=""
+g_options="defaults"
+
+g_blkid_cmd="${SUDO_ALIAS} blkid -o value"
+g_lsblk_cmd="${SUDO_ALIAS} lsblk"
+g_mkdir_cmd="${SUDO_ALIAS} mkdir -p"
+g_mount_cmd="${SUDO_ALIAS} mount"
+g_umount_cmd="${SUDO_ALIAS} umount"
+g_systemctl_cmd="${SUDO_ALIAS} systemctl"
+g_cat_cmd="${SUDO_ALIAS} cat"
+g_tee_cmd="${SUDO_ALIAS} tee"
+
+function msg() {
+    printf '%b' "$1" >&2
+}
+
+function success() {
+    msg "\33[32m[✔]\33[0m ${1}${2}"
+}
+
+function die() {
+    msg "\33[31m[✘]\33[0m ${1}${2}"
+    exit 1
+}
+
+precheck() {
+    # check block devices
+    ${g_lsblk_cmd} ${g_device} >& /dev/null
+    if [ $? -ne 0 ];then
+        die "${g_device} is not a block devices!\n"
+        exit 1
+    fi
+    # mkdir mountpoint
+    if [ ! -d ${g_mountpoint} ];then
+        die "${g_mountpoint} is not a directory!\n"
+    fi
+    
+    # check uuid
+    g_disk_uuid=`${g_blkid_cmd} -s UUID ${g_device}`
+    if [ ! ${g_disk_uuid} ];then
+        die "${g_device} has no uuid!\n"
+    fi
+    
+    # check options by mount
+    ${g_umount_cmd} ${g_mountpoint} >& /dev/null
+    if [ "${OPTIONS}" != "" ];then
+        g_options=${OPTIONS}
+    fi
+    out=`${g_mount_cmd} --options ${g_options} ${g_device} ${g_mountpoint} 2>&1`
+    if [ $? -ne 0 ];then
+        die "${out}!\n"
+        exit 1
+    fi
+    ${g_umount_cmd} ${g_mountpoint} >& /dev/null
+}
+
+init() {
+    src=${g_mountpoint//\//-}
+    g_systemd_name=${src#-}
+    g_filesystem_type=`${g_blkid_cmd} -s TYPE ${g_device}`
+}
+
+create_systemd() {
+    # add mount
+    echo "[Unit]
+Description=Mount ${g_device} at ${g_mountpoint}
+
+[Mount]
+What=/dev/disk/by-uuid/${g_disk_uuid}
+Where=${g_mountpoint}
+Type=${g_filesystem_type}
+Options=${g_options}
+
+[Install]
+WantedBy=multi-user.target
+    " | ${g_tee_cmd} /etc/systemd/system/${g_systemd_name}.mount >& /dev/null
+    
+    # add automount
+    echo "[Unit]
+Description=Automount ${g_device} at ${g_mountpoint}
+
+[Automount]
+Where=${g_mountpoint}
+
+[Install]
+WantedBy=multi-user.target
+    " | ${g_tee_cmd} /etc/systemd/system/${g_systemd_name}.automount >& /dev/null
+    
+    ${g_systemctl_cmd} daemon-reload >& /dev/null
+    ${g_systemctl_cmd} enable ${g_systemd_name}.mount >& /dev/null
+    ${g_systemctl_cmd} enable ${g_systemd_name}.automount >& /dev/null
+    ${g_systemctl_cmd} start ${g_systemd_name}.automount >& /dev/null
+    ${g_systemctl_cmd} start ${g_systemd_name}.mount >& /dev/null
+    
+    sleep 3
+    
+    status=`${g_systemctl_cmd} is-active ${g_systemd_name}.mount`
+    auto_status=`${g_systemctl_cmd} is-active ${g_systemd_name}.automount`
+    if [ "${status}" == "active" ] && [ "${auto_status}" == "active" ];then
+        success "add automount ${g_device} to ${g_mountpoint} successfully!\n"
+    else
+        die "add automount ${g_device} to ${g_mountpoint} failed please check!\n"
+        exit 1
+    fi
+}
+
+precheck
+init
+create_systemd
diff --git a/playbook/automount/scripts/del_disk.sh b/playbook/automount/scripts/del_disk.sh
new file mode 100644
index 000000000..8718a16b0
--- /dev/null
+++ b/playbook/automount/scripts/del_disk.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+
+#
+#  Copyright (c) 2023 NetEase Inc.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+
+# check parameters
+if [ $# -ne 1 ]; then
+    echo "Usage: $0 <mountpoint>"
+    exit 1
+fi
+
+g_mountpoint=$1
+g_systemd_name=""
+g_rm_cmd="${SUDO_ALIAS} rm"
+g_umount_cmd="${SUDO_ALIAS} umount"
+
+function msg() {
+    printf '%b' "$1" >&2
+}
+
+function success() {
+    msg "\33[32m[✔]\33[0m ${1}${2}"
+}
+
+function die() {
+    msg "\33[31m[✘]\33[0m ${1}${2}"
+    exit 1
+}
+
+precheck() {
+    # check systemd file is exist
+    src=${g_mountpoint//\//-}
+    g_systemd_name=${src#-}
+    if [ ! -e /etc/systemd/system/${g_systemd_name}.automount ];then
+        die "no /etc/systemd/system/${g_systemd_name}.automount file!\n"
+        exit 1
+    fi
+
+    if [ ! -e /etc/systemd/system/${g_systemd_name}.mount ];then
+        die "no /etc/systemd/system/${g_systemd_name}.mount file!\n"
+        exit 1
+    fi
+}
+
+del_systemd() {
+    ${g_rm_cmd} /etc/systemd/system/${g_systemd_name}.automount
+    ${g_rm_cmd} /etc/systemd/system/${g_systemd_name}.mount
+    ${g_rm_cmd} /etc/systemd/system/multi-user.target.wants/${g_systemd_name}.automount
+    ${g_rm_cmd} /etc/systemd/system/multi-user.target.wants/${g_systemd_name}.mount
+
+    ${g_systemctl_cmd} daemon-reload >& /dev/null
+
+    ${g_umount_cmd} ${g_mountpoint}
+
+    success "rm automount ${g_mountpoint} successfully" 
+}
+
+precheck
+init
+del_systemd