Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Flexvolume学习笔记

2
Feb
2020

Flexvolume学习笔记

By Alex
/ in PaaS
/ tags K8S
0 Comments
简介
卷

容器在磁盘上写的文件是临时性的,没有持久化保证。如果容器崩溃,Kubelet会重启它,临时文件就都丢失了。此外,在Pod中运行的多个容器,可能有共享存储的需求。这两点正是K8S卷(Volume)来解决的。

Docker也有卷的概念,但是它的卷仅仅是宿主机上的一个目录,或者是另外一个容器中的目录,没有被有效的管理起来。

K8S卷具有明确的生命周期 —— 和使用它的Pod相同,Pod中所有容器都可以挂载卷。Pod存在则卷一直存在,容器重启不会导致卷数据丢失。

卷的核心,就是一个目录,但是这个目录从何而来,由什么设备后备,取决于卷类型。

容器中进程看到的文件系统视图,由Docker镜像和卷组成,Docker镜像构成根文件系统,卷则挂载到某个特定的位置。卷不能挂载到其它卷(的挂载点下面),也不能具有指向其它卷的硬链接。

K8S支持大量类型的卷:

  1. Configmap、Secret等是由K8S资源支持(Backed by)的
  2. rbd、cephfs、nfs等是利用内核既有特性实现外部文件系统、块设备的挂载
  3. AWS、Azure等头部云厂商的卷对接
  4. out-of-tree卷插件机制:
    1. CSI(Container Storage Interface )是新的,标准化的扩展方式,用于将任何存储系统暴露给容器工作负载
    2. FlexVolume,本文的主题
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节点。

插件存放目录:

  1. Kubelet选项: --volume-plugin-dir
  2. controller manager选项: --flex-volume-plugin-dir

例如,对于名为cifs的插件,你需要存放到:/usr/libexec/kubernetes/kubelet-plugins/volume/exec/gmem.cc~cifs/cifs

vendor~driver必须和卷的flexVolume.driver字段匹配,把~换成/即可:

YAML
1
2
3
volumes:
- flexVolume:
    driver: gmem.cc/cifs
动态发现

从1.8开始, Flexvolume支持动态按需发现插件,不需要插件预先放到特定目录。安装好插件后,也不需要重启Kubelet或Controller manager。

自动安装和升级

一个可能的安装和升级Flexvolume插件的方式是通过DaemonSet:

  1. 将宿主机的插件目录挂载给容器
  2. 将容器镜像里带的最新Flexvolume插件拷贝到上述共享的目录中
接口

flexVolume是通过命令行的方式调用插件的,就像CNI那样。

通过这些接口,你能够实现:

  1. 划分本地磁盘,并将划分后的部分挂载给容器,可以控制容器能够使用的量
  2. 将分布式存储Attach到本机,并挂载给容器使用

如果要挂载设备,你需要实现attach detach waitforattach isattached mountdevicve unmountdevice。

如果只是要挂载一个NFS目录,只需要实现mount umount。

init

初始化驱动,在Kubelet、Controller manager初始化时调用。格式:

Shell
1
<driver executable> init 

如果调用成功,返回capabilities map,说明插件支持的capabilities。当前可用的capabilities包括:

  1. attach,bool类型,提示插件是否需要attach/detach操作。默认true
attach

由Controller manager调用。将具有指定选项的卷Attach到指定的节点上。如果操作成功,返回在节点上的设备文件路径。格式:

Shell
1
<driver executable> attach <json options> <node name>
detach

由Controller manager调用。解除指定选项的卷和节点的关联。格式:

Shell
1
<driver executable> detach <mount device> <node name>
waitforattach

由Controller manager调用。等待Attach成功,超时默认10m。格式:

Shell
1
<driver executable> waitforattach <mount device> <json options>
isattached

由Controller manager调用。检查卷是否被Attach到节点。格式: 

Shell
1
<driver executable> isattached <json options> <node name>
mountdevice 

Attach往往是将分布式存储卷映射为本机设备,要使用它,还需要进行挂载。

由Kubelet调用,将设备挂载到一个全局的路径,以便由Pod进行绑定挂载。格式:

Shell
1
<driver executable> mountdevice <mount dir> <mount device> <json options>
unmountdevice

由Kubelet调用,解除设备的全局挂载。一次调用就会导致所有bind挂载被umount。格式:

Shell
1
<driver executable> unmountdevice <mount device>

除了用户指定选项、默认JSON选项之外,以下选项自动捕获,并传递给上述命令:

JSON
1
2
3
4
kubernetes.io/pod.name
kubernetes.io/pod.namespace
kubernetes.io/pod.uid
kubernetes.io/serviceAccount.name
mount

由Kubelet调用,对于是实现了mountdevice+attach的驱动,如果没有实现此接口,默认行为是进行bind挂载。格式:

Shell
1
<driver executable> mount <mount dir> <json options>
unmount 

由Kubelet调用, 对于是实现了mountdevice+attach的驱动,如果没有实现此接口,默认行为是解除bind挂载。格式:

Shell
1
<driver executable> unmount <mount dir>
JSON选项

除了用户通过卷Spec传递的选项之外,以下选项自动传递:

JSON
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接口。

接口输出

调用上述接口后,插件应该给出如下形式的输出: 

JSON
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源码分析

这里的flexvolume是指in-tree的卷插件,对外部Flexvolume驱动的调用,都是由它执行的。

公共接口

任何一种卷插件,包括flexvolume,都需要实现VolumePlugin接口,本节阅读一下相关源码。

VolumePlugin
Go
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
}
DeviceMountableVolumePlugin

扩展VolumePlugin,建模那些在Bind到Pod之前,需要先在节点上进行Mount的卷:

Go
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)
} 
AttachableVolumePlugin

扩展DeviceMountableVolumePlugin,建模那些在挂载之前,必须先Attach(映射为块设备)的卷:

Go
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)
}
VolumeHost

在Init阶段,卷插件通过此接口访问Kubelet,获得环境信息:

Go
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
} 
Volume

卷本质上是宿主机上的一个目录,该目录会被Pod(或宿主机)使用:

Go
1
2
3
4
5
6
7
type Volume interface {
    // 宿主机的路径,卷的挂载点
    GetPath() string
 
    // 用于暴露指标,例如可用空间
    MetricsProvider
} 
Mounter

该接口负责安装/挂载一个卷:

Go
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
}
Unmounter 

用于清理、卸载卷

Go
1
2
3
4
5
6
7
type Unmounter interface {
    Volume
    // 从自己决定得路径进行卸载,并完成清理
    TearDown() error
    // 从指定路径进行卸载,并完成清理
    TearDownAt(dir string) error
}
MounterArgs

挂载相关参数:

Go
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实现

flexvolume的代码位于pkg/volume/flexvolume目录下。

实例化

下面工厂函数实例化flexvolume:

Go
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行为比较复杂,因此单独实现。

flexVolumePlugin

只需要mount/umount的驱动,逻辑由此实现负责。它由如下结构表示:

Go
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
}
挂载行为

公共接口的实现如下:

Go
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,它的实现:

Go
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
}
flexVolumeAttachablePlugin

它内嵌了一个flexVolumePlugin,在其基础上增加Attach等能力: 

Go
1
2
3
type flexVolumeAttachablePlugin struct {
    *flexVolumePlugin
}
Attach行为
Go
1
2
3
func (plugin *flexVolumeAttachablePlugin) NewAttacher() (volume.Attacher, error) {
    return &flexVolumeAttacher{plugin}, nil
}

flexVolumeAttacher由下面的结构表示:

Go
1
2
3
type flexVolumeAttacher struct {
    plugin *flexVolumeAttachablePlugin
}

Attach的实现:

Go
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
} 
示例
lvm

由于FlexVolume使用基于命令行的接口,因此是语言无关的,你甚至可以用Shell脚本实现。

Go
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。

 

← CSI学习笔记
限制Pod磁盘空间用量 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • Kata Containers学习笔记
  • Kustomize学习笔记
  • Cilium学习笔记
  • 基于Helm的Kubernetes资源管理
  • 限制Pod磁盘空间用量

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2