Flexvolume学习笔记
容器在磁盘上写的文件是临时性的,没有持久化保证。如果容器崩溃,Kubelet会重启它,临时文件就都丢失了。此外,在Pod中运行的多个容器,可能有共享存储的需求。这两点正是K8S卷(Volume)来解决的。
Docker也有卷的概念,但是它的卷仅仅是宿主机上的一个目录,或者是另外一个容器中的目录,没有被有效的管理起来。
K8S卷具有明确的生命周期 —— 和使用它的Pod相同,Pod中所有容器都可以挂载卷。Pod存在则卷一直存在,容器重启不会导致卷数据丢失。
卷的核心,就是一个目录,但是这个目录从何而来,由什么设备后备,取决于卷类型。
容器中进程看到的文件系统视图,由Docker镜像和卷组成,Docker镜像构成根文件系统,卷则挂载到某个特定的位置。卷不能挂载到其它卷(的挂载点下面),也不能具有指向其它卷的硬链接。
K8S支持大量类型的卷:
- Configmap、Secret等是由K8S资源支持(Backed by)的
- rbd、cephfs、nfs等是利用内核既有特性实现外部文件系统、块设备的挂载
- AWS、Azure等头部云厂商的卷对接
- out-of-tree卷插件机制:
- CSI(Container Storage Interface )是新的,标准化的扩展方式,用于将任何存储系统暴露给容器工作负载
- FlexVolume,本文的主题
FlexVolume是最初的卷扩展机制,从K8S 1.2版本开始出现,通过in-tree插件flexVolume进行对接。
使用FlexVolume,你可以为自己厂商的卷编写驱动,以支持K8S。驱动必须安装到每个K8S节点的卷插件目录,如果需要支持Attach操作,驱动也需要安装到Master节点。
你需要将FlexVolume驱动存放到所有节点的 <plugindir>/<vendor~driver>/<driver>,plugindir默认位置为 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/。
如果启用了Attach/Detach功能,也就是Kubelet选项 --enable-controller-attach-detach=true(默认),则也需要存放驱动到Master节点。
插件存放目录:
- Kubelet选项: --volume-plugin-dir
- controller manager选项: --flex-volume-plugin-dir
例如,对于名为cifs的插件,你需要存放到:/usr/libexec/kubernetes/kubelet-plugins/volume/exec/gmem.cc~cifs/cifs
vendor~driver必须和卷的flexVolume.driver字段匹配,把~换成/即可:
1 2 3 |
volumes: - flexVolume: driver: gmem.cc/cifs |
从1.8开始, Flexvolume支持动态按需发现插件,不需要插件预先放到特定目录。安装好插件后,也不需要重启Kubelet或Controller manager。
一个可能的安装和升级Flexvolume插件的方式是通过DaemonSet:
- 将宿主机的插件目录挂载给容器
- 将容器镜像里带的最新Flexvolume插件拷贝到上述共享的目录中
flexVolume是通过命令行的方式调用插件的,就像CNI那样。
通过这些接口,你能够实现:
- 划分本地磁盘,并将划分后的部分挂载给容器,可以控制容器能够使用的量
- 将分布式存储Attach到本机,并挂载给容器使用
如果要挂载设备,你需要实现attach detach waitforattach isattached mountdevicve unmountdevice。
如果只是要挂载一个NFS目录,只需要实现mount umount。
初始化驱动,在Kubelet、Controller manager初始化时调用。格式:
1 |
<driver executable> init |
如果调用成功,返回capabilities map,说明插件支持的capabilities。当前可用的capabilities包括:
- attach,bool类型,提示插件是否需要attach/detach操作。默认true
由Controller manager调用。将具有指定选项的卷Attach到指定的节点上。如果操作成功,返回在节点上的设备文件路径。格式:
1 |
<driver executable> attach <json options> <node name> |
由Controller manager调用。解除指定选项的卷和节点的关联。格式:
1 |
<driver executable> detach <mount device> <node name> |
由Controller manager调用。等待Attach成功,超时默认10m。格式:
1 |
<driver executable> waitforattach <mount device> <json options> |
由Controller manager调用。检查卷是否被Attach到节点。格式:
1 |
<driver executable> isattached <json options> <node name> |
Attach往往是将分布式存储卷映射为本机设备,要使用它,还需要进行挂载。
由Kubelet调用,将设备挂载到一个全局的路径,以便由Pod进行绑定挂载。格式:
1 |
<driver executable> mountdevice <mount dir> <mount device> <json options> |
由Kubelet调用,解除设备的全局挂载。一次调用就会导致所有bind挂载被umount。格式:
1 |
<driver executable> unmountdevice <mount device> |
除了用户指定选项、默认JSON选项之外,以下选项自动捕获,并传递给上述命令:
1 2 3 4 |
kubernetes.io/pod.name kubernetes.io/pod.namespace kubernetes.io/pod.uid kubernetes.io/serviceAccount.name |
由Kubelet调用,对于是实现了mountdevice+attach的驱动,如果没有实现此接口,默认行为是进行bind挂载。格式:
1 |
<driver executable> mount <mount dir> <json options> |
由Kubelet调用, 对于是实现了mountdevice+attach的驱动,如果没有实现此接口,默认行为是解除bind挂载。格式:
1 |
<driver executable> unmount <mount dir> |
除了用户通过卷Spec传递的选项之外,以下选项自动传递:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
"kubernetes.io/fsType":"<FS type>", "kubernetes.io/readwrite":"<rw>", "kubernetes.io/fsGroup":"<FS group>", "kubernetes.io/mountsDir":"<string>", // 卷或者PV的名称 "kubernetes.io/pvOrVolumeName":"<Volume name if the volume is in-line in the pod spec; PV name if the volume is a PV>" "kubernetes.io/pod.name":"<string>", "kubernetes.io/pod.namespace":"<string>", "kubernetes.io/pod.uid":"<string>", "kubernetes.io/serviceAccount.name":"<string>", "kubernetes.io/secret/key1":"<secret1>" ... "kubernetes.io/secret/keyN":"<secretN>" |
其中Secrets仅仅传递给mount/umount接口。
调用上述接口后,插件应该给出如下形式的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ // 调用结果 "status": "<Success/Failure/Not supported>", // 调用结果的原因 "message": "<Reason for success/failure>", // Attach的设备文件的路径。仅attach/waitforattach "device": "<Path to the device attached>" // 集群级别的唯一卷名称,仅getvolumename "volumeName": "<Cluster wide unique name of the volume.>" // 卷是否Attach到节点,仅isattached "attached": <True/False> // 返回插件支持capabilities,的仅Init "capabilities": { "attach": <True/False> } } |
这里的flexvolume是指in-tree的卷插件,对外部Flexvolume驱动的调用,都是由它执行的。
任何一种卷插件,包括flexvolume,都需要实现VolumePlugin接口,本节阅读一下相关源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// 由K8S节点调用,来示例化和管理卷 type VolumePlugin interface { // 初始化插件,仅仅调用一次 Init(host VolumeHost) error // 插件的名称,必须使用命名空间化的名称,例如 example.com/volume ,只能包含一个/ // 命名空间kubernetes.io保留供K8S内部使用 GetPluginName() string // 获得唯一识别后备设备、目录或路径的名字/ID // 对于Attachable的卷,此ID必须被传递给Detach方法 // 如果指定的spec不被支持,返回错误 GetVolumeName(spec *Spec) (string, error) // 判断指定的spec能否被支持 CanSupport(spec *Spec) bool // 如果插件要求重新执行mount调用,返回true // 类似于Downward API这样自动更新的卷,依赖于此方法判断是否需要更新卷的内容 RequiresRemount() bool // 创建一个volume.Mounter NewMounter(spec *Spec, podRef *v1.Pod, opts VolumeOptions) (Mounter, error) // 创建一个volume.Unmounter NewUnmounter(name string, podUID types.UID) (Unmounter, error) // 根据卷名称、路径构建出spec,生成的spec可能是不完整的。该方法被卷管理器调用 // 通过读取磁盘上的卷目录来重新生成spec ConstructVolumeSpec(volumeName, volumePath string) (*Spec, error) // 该插件是否支持挂载选项 SupportsMountOption() bool // 是否支持针对所有节点的批量polling。用于加速Attached卷的校验 SupportsBulkVolumeVerification() bool } |
扩展VolumePlugin,建模那些在Bind到Pod之前,需要先在节点上进行Mount的卷:
1 2 3 4 5 6 7 8 |
type DeviceMountableVolumePlugin interface { VolumePlugin NewDeviceMounter() (DeviceMounter, error) NewDeviceUnmounter() (DeviceUnmounter, error) GetDeviceMountRefs(deviceMountPath string) ([]string, error) // CanDeviceMount determines if device in volume.Spec is mountable CanDeviceMount(spec *Spec) (bool, error) } |
扩展DeviceMountableVolumePlugin,建模那些在挂载之前,必须先Attach(映射为块设备)的卷:
1 2 3 4 5 6 7 |
type AttachableVolumePlugin interface { DeviceMountableVolumePlugin NewAttacher() (Attacher, error) NewDetacher() (Detacher, error) // CanAttach tests if provided volume spec is attachable CanAttach(spec *Spec) (bool, error) } |
在Init阶段,卷插件通过此接口访问Kubelet,获得环境信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
type VolumeHost interface { // 返回一个插件可以存放数据的目录,可能尚不存在 GetPluginDir(pluginName string) string // 返回一个插件可以存放数据的目录 // plugins/kubernetes.io/{PluginName}/{DefaultKubeletVolumeDevicesDirName}/{volumePluginDependentPath}/ GetVolumeDevicePluginDir(pluginName string) string // 返回所有Pod信息所在目录 GetPodsDir() string // 返回Pod专属的,某个卷插件的,某个具体卷的目录 GetPodVolumeDir(podUID types.UID, pluginName string, volumeName string) string // 返回插件存储针对某个Pod数据的目录 GetPodPluginDir(podUID types.UID, pluginName string) string GetPodVolumeDeviceDir(podUID types.UID, pluginName string) string // K8S客户端 GetKubeClient() clientset.Interface // 返回能处理指定Spec的插件 // 用于实现warap其它插件的插件。例如secret卷插件就是wrapemptyDir的插件 NewWrapperMounter(volName string, spec Spec, pod *v1.Pod, opts VolumeOptions) (Mounter, error) // 返回能处理指定Spec的插件 NewWrapperUnmounter(volName string, spec Spec, podUID types.UID) (Unmounter, error) // 从Kubelet得到云提供商信息 GetCloudProvider() cloudprovider.Interface // 得到Mounter接口,封装系统底层Mount操作 GetMounter(pluginName string) mount.Interface // kubelet所在主机名 GetHostName() string // kubelet所在主机IP GetHostIP() (net.IP, error) // 获取主机资源信息 GetNodeAllocatable() (v1.ResourceList, error) // 得到用于获取Secret内容的函数 GetSecretFunc() func(namespace, name string) (*v1.Secret, error) // 得到用于获取Configmap内容的函数 GetConfigMapFunc() func(namespace, name string) (*v1.ConfigMap, error) // 得到用于获取SA Token内容的函数 GetServiceAccountTokenFunc() func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) DeleteServiceAccountTokenFunc() func(podUID types.UID) // 用于执行宿主机命令 GetExec(pluginName string) exec.Interface // 获得节点标签 GetNodeLabels() (map[string]string, error) // 获取节点名 GetNodeName() types.NodeName // 获取事件记录器 GetEventRecorder() record.EventRecorder // 用于执行Subpath操作的接口 GetSubpather() subpath.Interface } |
卷本质上是宿主机上的一个目录,该目录会被Pod(或宿主机)使用:
1 2 3 4 5 6 7 |
type Volume interface { // 宿主机的路径,卷的挂载点 GetPath() string // 用于暴露指标,例如可用空间 MetricsProvider } |
该接口负责安装/挂载一个卷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
type Mounter interface { // 提供需要为Pod挂载到的卷的路径 Volume // 在Setup(挂载)之前调用,检查挂载所需要的组件(例如二进制文件)在节点上可用 // 如果返回错误,则挂载过程中止,产生一个事件,说明具体原因 // 使用 --experimental-check-mount-binaries标记可以禁用此检查 CanMount() error // 准备并挂载(解包)卷到一个自己决定的目录下。挂载点、及其内容的所有者必须是 // `fsUser` 或 'fsGroup' ,以确保能被Pod访问 // 该方法可能被多次调用,因此必须实现幂等性 // 可以返回以下类型的错误: // - TransientOperationFailure // - UncertainProgressError // - 其它类型的错误,被视为致命(final)错误 SetUp(mounterArgs MounterArgs) error // 类似上面,挂载到指定目录 SetUpAt(dir string, mounterArgs MounterArgs) error // 在Setup/SetupAt之后调用,获得Mounter的属性 GetAttributes() Attributes } |
用于清理、卸载卷
1 2 3 4 5 6 7 |
type Unmounter interface { Volume // 从自己决定得路径进行卸载,并完成清理 TearDown() error // 从指定路径进行卸载,并完成清理 TearDownAt(dir string) error } |
挂载相关参数:
1 2 3 4 5 6 7 8 |
type MounterArgs struct { // 如果设置了FsUser,则卷的所有权会被改为FsUser,并确保FsUser可以写入 // 当前仅仅被映射到Pod目录中的service account tokens支持 FsUser *int64 FsGroup *int64 FSGroupChangePolicy *v1.PodFSGroupChangePolicy DesiredSize *resource.Quantity } |
flexvolume的代码位于pkg/volume/flexvolume目录下。
下面工厂函数实例化flexvolume:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
func (pluginFactory) NewFlexVolumePlugin(pluginDir, name string, runner exec.Interface) (volume.VolumePlugin, error) { // 查找二进制文件 execPath := filepath.Join(pluginDir, name) driverName := utilstrings.UnescapeQualifiedName(name) flexPlugin := &flexVolumePlugin{ driverName: driverName, execPath: execPath, runner: runner, unsupportedCommands: []string{}, } // 初始化驱动,获取特性列表 call := flexPlugin.NewDriverCall(initCmd) ds, err := call.Run() if err != nil { return nil, err } flexPlugin.capabilities = *ds.Capabilities if flexPlugin.capabilities.Attach { // 如果支持Attach,则返回flexVolumeAttachablePlugin return &flexVolumeAttachablePlugin{flexVolumePlugin: flexPlugin}, nil } // 否则,返回flexVolumePlugin return flexPlugin, nil } |
可以看到,驱动是否支持Attach,决定了使用何种实现。如前文所述,支持Attach的(面向设备的驱动)Flexvolume行为比较复杂,因此单独实现。
只需要mount/umount的驱动,逻辑由此实现负责。它由如下结构表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type flexVolumePlugin struct { // 使用的FlexVoulme驱动名称,这决定从何处寻找二进制文件 driverName string // 二进制文件路径 execPath string // 宿主机环境接口 host volume.VolumeHost // 执行命令行的接口 runner exec.Interface sync.Mutex // 驱动不支持的命令列表 unsupportedCommands []string capabilities DriverCapabilities } |
公共接口的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
func (plugin *flexVolumePlugin) NewMounter(spec *volume.Spec, pod *api.Pod, _ volume.VolumeOptions) (volume.Mounter, error) { return plugin.newMounterInternal(spec, pod, plugin.host.GetMounter(plugin.GetPluginName()), plugin.runner) } func (plugin *flexVolumePlugin) newMounterInternal(spec *volume.Spec, pod *api.Pod, mounter mount.Interface, runner exec.Interface) (volume.Mounter, error) { sourceDriver, err := getDriver(spec) if err != nil { return nil, err } readOnly, err := getReadOnly(spec) if err != nil { return nil, err } var metricsProvider volume.MetricsProvider if plugin.capabilities.SupportsMetrics { metricsProvider = volume.NewMetricsStatFS(plugin.host.GetPodVolumeDir( pod.UID, utilstrings.EscapeQualifiedName(sourceDriver), spec.Name())) } else { metricsProvider = &volume.MetricsNil{} } return &flexVolumeMounter{ flexVolume: &flexVolume{ driverName: sourceDriver, execPath: plugin.getExecutable(), mounter: mounter, plugin: plugin, podName: pod.Name, podUID: pod.UID, podNamespace: pod.Namespace, podServiceAccountName: pod.Spec.ServiceAccountName, volName: spec.Name(), MetricsProvider: metricsProvider, }, runner: runner, spec: spec, readOnly: readOnly, }, nil } |
可以看到,使用的Mounter是flexVolumeMounter,它的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
type flexVolumeMounter struct { *flexVolume runner exec.Interface spec *volume.Spec readOnly bool } func (f *flexVolumeMounter) SetUp(mounterArgs volume.MounterArgs) error { return f.SetUpAt(f.GetPath(), mounterArgs) } // 默认挂载路径 func (f *flexVolume) GetPath() string { name := f.driverName return f.plugin.host.GetPodVolumeDir(f.podUID, utilstrings.EscapeQualifiedName(name), f.volName) } // 挂载逻辑 func (f *flexVolumeMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error { // 准备挂载点 alreadyMounted, err := prepareForMount(f.mounter, dir) if err != nil { return err } // 仅仅挂载一次 if alreadyMounted { return nil } // 调用驱动的mount命令 call := f.plugin.NewDriverCall(mountCmd) // 添加命令行参数 call.Append(dir) // 添加JSON选项 extraOptions := make(map[string]string) // Pod元数据 extraOptions[optionKeyPodName] = f.podName extraOptions[optionKeyPodNamespace] = f.podNamespace extraOptions[optionKeyPodUID] = string(f.podUID) // SA元数据 extraOptions[optionKeyServiceAccountName] = f.podServiceAccountName // 抽取Secret并传递 if err := addSecretsToOptions(extraOptions, f.spec, f.podNamespace, f.driverName, f.plugin.host); err != nil { os.Remove(dir) return err } // 隐含选项 if mounterArgs.FsGroup != nil { extraOptions[optionFSGroup] = strconv.FormatInt(int64(*mounterArgs.FsGroup), 10) } // 将VolumeSpec传递过去 call.AppendSpec(f.spec, f.plugin.host, extraOptions) // 调用命令,完成真实的挂载行为 _, err = call.Run() if isCmdNotSupportedErr(err) { err = (*mounterDefaults)(f).SetUpAt(dir, mounterArgs) } if err != nil { // 出错就删除挂载点 os.Remove(dir) return err } if !f.readOnly { // 设置卷的权限 if f.plugin.capabilities.FSGroup { volume.SetVolumeOwnership(f, mounterArgs.FsGroup, mounterArgs.FSGroupChangePolicy) } } return nil } // GetAttributes get the flex volume attributes. The attributes will be queried // using plugin callout after we finalize the callout syntax. func (f *flexVolumeMounter) GetAttributes() volume.Attributes { return (*mounterDefaults)(f).GetAttributes() } func (f *flexVolumeMounter) CanMount() error { return nil } |
它内嵌了一个flexVolumePlugin,在其基础上增加Attach等能力:
1 2 3 |
type flexVolumeAttachablePlugin struct { *flexVolumePlugin } |
1 2 3 |
func (plugin *flexVolumeAttachablePlugin) NewAttacher() (volume.Attacher, error) { return &flexVolumeAttacher{plugin}, nil } |
flexVolumeAttacher由下面的结构表示:
1 2 3 |
type flexVolumeAttacher struct { plugin *flexVolumeAttachablePlugin } |
Attach的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func (a *flexVolumeAttacher) Attach(spec *volume.Spec, hostName types.NodeName) (string, error) { // 调用驱动的attach命令 call := a.plugin.NewDriverCall(attachCmd) // 传入volume spec 没用 call.AppendSpec(spec, a.plugin.host, nil) // 传入主机名 call.Append(string(hostName)) status, err := call.Run() if isCmdNotSupportedErr(err) { return (*attacherDefaults)(a).Attach(spec, hostName) } else if err != nil { return "", err } // 返回设备文件路径 return status.DevicePath, err } |
由于FlexVolume使用基于命令行的接口,因此是语言无关的,你甚至可以用Shell脚本实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
usage() { err "Invalid usage. Usage: " err "\t$0 init" err "\t$0 attach <json params> <nodename>" err "\t$0 detach <mount device> <nodename>" err "\t$0 waitforattach <mount device> <json params>" err "\t$0 mountdevice <mount dir> <mount device> <json params>" err "\t$0 unmountdevice <mount dir>" err "\t$0 isattached <json params> <nodename>" exit 1 } err() { echo -ne $* 1>&2 } log() { echo -ne $* >&1 } ismounted() { MOUNT=`findmnt -n ${MNTPATH} 2>/dev/null | cut -d' ' -f1` if [ "${MOUNT}" == "${MNTPATH}" ]; then echo "1" else echo "0" fi } getdevice() { VOLUMEID=$(echo ${JSON_PARAMS} | jq -r '.volumeID') VG=$(echo ${JSON_PARAMS}|jq -r '.volumegroup') # LVM substitutes - with -- VOLUMEID=`echo $VOLUMEID|sed s/-/--/g` VG=`echo $VG|sed s/-/--/g` DMDEV="/dev/mapper/${VG}-${VOLUMEID}" echo ${DMDEV} } attach() { # JSON选项 JSON_PARAMS=$1 SIZE=$(echo $1 | jq -r '.size') # 卷标识,使用POD的UUID VOLUMEID=$(echo ${JSON_PARAMS} | jq -r '.["kubernetes.io/pod.uid"]' # 所属卷组(卷组需要提前准备好) VG=$(echo ${JSON_PARAMS}|jq -r '.volumegroup') # 如果逻辑卷已经存在,直接返回 DMDEV="/dev/mapper/${VG}-${VOLUMEID}" if [ -b "${DMDEV}" ]; then log "{\"status\": \"Success\", \"device\":\"${DMDEV}\"}" exit 0 fi # 开始创建逻辑卷 lvcreate -L ${SIZE} -n ${VOLUMEID} ${VG} &> /tmp/${VOLUMEID} if [ $? -ne 0 ]; then RST=`cat /tmp/${VOLUMEID}` RST=`echo ${RST//\"/}` err "{ \"status\": \"Failure\", \"message\": \"Failed to create volume ${VOLUMEID} at ${VG}, Reason:${RST}\"}" exit 1 fi /bin/rm -rf /tmp/${VOLUMEID} # 确认逻辑卷存在 if [ ! -b "${DMDEV}" ]; then err "{\"status\": \"Failure\", \"message\": \"Volume ${VOLUMEID} does not exist\"}" exit 1 fi log "{\"status\": \"Success\", \"device\":\"${DMDEV}\"}" exit 0 } detach() { log "{\"status\": \"Success\"}" exit 0 } waitforattach() { shift attach $* } domountdevice() { # 挂载路径 MNTPATH=$1 # 挂载的设备,由attach阶段创建的LV DMDEV=$2 FSTYPE=$(echo $3|jq -r '.["kubernetes.io/fsType"]') if [ ! -b "${DMDEV}" ]; then err "{\"status\": \"Failure\", \"message\": \"${DMDEV} does not exist\"}" exit 1 fi # 如果已经挂载,不做操作(幂等) if [ $(ismounted) -eq 1 ] ; then log "{\"status\": \"Success\"}" exit 0 fi # 创建文件系统 VOLFSTYPE=`blkid -o udev ${DMDEV} 2>/dev/null|grep "ID_FS_TYPE"|cut -d"=" -f2` if [ "${VOLFSTYPE}" == "" ]; then mkfs -t ${FSTYPE} ${DMDEV} >/dev/null 2>&1 if [ $? -ne 0 ]; then err "{ \"status\": \"Failure\", \"message\": \"Failed to create fs ${FSTYPE} on device ${DMDEV}\"}" exit 1 fi fi # 创建挂载点 mkdir -p ${MNTPATH} &> /dev/null # 执行挂载 mount ${DMDEV} ${MNTPATH} &> /dev/null if [ $? -ne 0 ]; then err "{ \"status\": \"Failure\", \"message\": \"Failed to mount device ${DMDEV} at ${MNTPATH}\"}" exit 1 fi log "{\"status\": \"Success\"}" exit 0 } unmountdevice() { MNTPATH=$1 if [ ! -d ${MNTPATH} ]; then log "{\"status\": \"Success\"}" exit 0 fi if [ $(ismounted) -eq 0 ] ; then log "{\"status\": \"Success\"}" exit 0 fi umount ${MNTPATH} &> /dev/null if [ $? -ne 0 ]; then err "{ \"status\": \"Failed\", \"message\": \"Failed to unmount volume at ${MNTPATH}\"}" exit 1 fi log "{\"status\": \"Success\"}" exit 0 } isattached() { log "{\"status\": \"Success\", \"attached\":true}" exit 0 } op=$1 if [ "$op" = "init" ]; then log "{\"status\": \"Success\"}" exit 0 fi if [ $# -lt 2 ]; then usage fi shift case "$op" in attach) attach $* ;; detach) detach $* ;; waitforattach) waitforattach $* ;; mountdevice) domountdevice $* ;; unmountdevice) unmountdevice $* ;; isattached) isattached $* ;; *) log "{ \"status\": \"Not supported\" }" exit 0 esac exit 1 |
本节的例子来自:https://blog.spider.im/post/control-disk-size-in-docker。
Leave a Reply