diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..ec1f8c2
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+# CHANGELOG
+
+---
+
+### v0.1.0
+
+### node-resource-manager
+
+- Add LVM resource into node resource manager
+- Add QuotaPath resource into node resource manager
+- Add KMEM resource into node resource manager
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..31941b4
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,5 @@
+# OpenYurt Community Code of Conduct
+
+Node-resource-manager follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
+
+In cases of abusive, harassing, or any unacceptable behaviors, please don't hesitate to contact the project team at openyurt@gmail.com.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..fd50c12
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+# Copyright 2021 The OpenYurt Authors.
+#
+# 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.
+
+.PHONY: build
+build:
+# options:
+# ARCH=amd64
+# VERSION=v1.0
+ bash ./build/build.sh $(OPTS)
diff --git a/PROJECT b/PROJECT
new file mode 100644
index 0000000..25830f5
--- /dev/null
+++ b/PROJECT
@@ -0,0 +1,3 @@
+version: "1"
+domain: openyurt.io
+repo: github.com/openyurtio/node-resource-manager
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1fcf9e3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+# openyurtio/node-resource-manager
+
+English | [简体中文](./README.zh.md)
+
+Node-resource-manager manages local node resources of OpenYurt cluster in a unified manner.
+
+It currently manages:
+- LVM built on top of block device or pmem device.
+- QuotaPath built on top of block device or pmem device.
+- Memory built on top of pmem device.
+
+The majority function consists of:
+- Initialize local resources on edge node.
+- Update local resources on edge node.
+
+You can define the spec of local resources by simply modifying the pre-defined ConfigMap.
+
+Node-resource-manager has the following advantages in terms of compatibility and usability.
+- **Easily to use**. Initialization and modification of local resources are easily done by editing the ConfigMap.
+- **Easily to integrate**. Node-resource-manager can work together with csi driver, to perform local storage lifecycle management.
+- **Platform free**. Node-resource-manager can be running in any kubernetes clusters.
+
+## Architecture
+The component consists of two parts, the first part is the ConfigMap named node-resource-topo in kube-system namespace,
+and the second is the node-resource-manager DaemonSet deployed in kube-system namespace.
+Node-resource-manager on each node mounts and reads the node-resource-topo ConfigMap to managed local resources.
+
+
+
+
+## Getting started
+
+1. Create node-resource-topo ConfigMap in kube-system namespace. ConfigMap example is in [configmap.md](./docs/configmap.md).
+```
+kubectl apply -f deploy/configmap.yaml
+```
+
+2. Deploy node-resource-manager DaemonSet.
+```
+kubectl apply -f deploy/nrm.yaml
+```
+
+## Developer guide
+
+Please refer to [developer-guide.md](./docs/developer-guide.md) for developing and building the project.
+
+## Roadmap
+
+[2021 Roadmap](docs/roadmap.md)
diff --git a/README.zh.md b/README.zh.md
new file mode 100644
index 0000000..563ea2a
--- /dev/null
+++ b/README.zh.md
@@ -0,0 +1,53 @@
+# openyurtio/node-resource-manager
+
+[English](./README.md) | 简体中文
+
+node-resource-manager 是用于管理 OpenYurt 集群本地资源的组件,用户可以通过修改集群内 ConfigMap 的定义来动态配置集群内宿主机上的本地资源。
+
+管理的本地资源包括:
+- 基于块设备或者是持久化内存设备创建的 LVM
+- 基于块设备或者是持久化内存设备创建的 QuotaPath
+
+主要功能包括:
+- 初始化节点上本地资源
+- 更新节点上的本地资源
+
+主要优点:
+- **简单易用**。node-resource-manager 可以仅通过定义 ConfigMap 就完成对集群中的本地资源的初始化和更新
+- **易于集成**。 node-resource-manager 可以与 csi 插件集成来完成 kubernetes 集群中的相关本地资源的生命周期管理
+- **与云平台无关**。 node-resource-manager 可以轻松部署在任何公共云 Kubernetes 服务中。
+
+## 架构
+
+该组件主要包含两个部分, 一个是定义在集群中 kube-system namespace 的 node-resource-topo ConfigMap,
+一个是部署在集群中 kube-system namespace 下面的 node-resource-manager Daemonset,
+每个 Node 节点上的 node-resource-manager 通过挂载 node-resource-topo ConfigMap 的方式生产并管理用户定义的本地资源。
+
+
+
+
+## 开始使用
+
+1. 在 Kubernetes kube-system namespace 下定义 node-resource-topo ConfigMap, 该 ConfigMap 用于定义集群中需要自动生成并管理的节点本地资源. 关于如何创建一个ConfigMap,请参见 ConfigMap [定义](./docs/configmap.zh.md)
+
+```
+kubectl apply -f deploy/configmap.yaml
+```
+
+2. 在 Kubernetes 集群中创建 node-resource-manager Daemonset。
+```
+kubectl apply -f deploy/nrm.yaml
+```
+
+3. 检查在 ConfigMap 上定义的资源是否都已经在对应的节点上被正确的创建。
+
+
+4. 配合 [alibaba-local-csi-plugin](https://help.aliyun.com/document_detail/178472.html?spm=a2c4g.11186623.6.844.13a019caYIiivY) 插件在集群中动态创建本地资源 pvc/pv, 供 pod 进行挂载使用。
+
+## 开发指南
+
+请参考 [developer-guide.md](./docs/developer-guide.md) 进行本项目的开发和构建。
+
+## 发展规划
+
+[2021年 发展规划](docs/roadmap.md)
\ No newline at end of file
diff --git a/build/build.sh b/build/build.sh
new file mode 100755
index 0000000..cd403b1
--- /dev/null
+++ b/build/build.sh
@@ -0,0 +1,118 @@
+# Copyright 2021 The OpenYurt Authors.
+#
+# 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.
+
+#!/usr/bin/env bash
+set -x
+
+# usage:
+# ./build.sh ARCH=amd64 VERSION=v1.0
+
+NRM_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
+NRM_BUILD_DIR=${NRM_ROOT}/build
+NRM_OUTPUT_DIR=${NRM_BUILD_DIR}/_output
+NRM_BUILD_IMAGE="golang:1.13.3-alpine"
+
+GIT_VERSION="v1.0"
+GIT_VERSION=(${VERSION:-${GIT_VERSION}})
+GIT_SHA=`git rev-parse --short HEAD || echo "HEAD"`
+GIT_BRANCH=`git rev-parse --abbrev-ref HEAD 2>/dev/null`
+BUILD_TIME=`date "+%Y-%m-%d-%H:%M:%S"`
+
+IMG_REPO="openyurt/node-resource-manager"
+IMG_VERSION=${GIT_VERSION}-${GIT_SHA}
+
+readonly -a SUPPORTED_ARCH=(
+ amd64
+ arm
+ arm64
+)
+
+readonly -a target_arch=(${ARCH:-${SUPPORTED_ARCH[@]}})
+
+function build_multi_arch_binaries() {
+ local docker_run_opts=(
+ "-i"
+ "--rm"
+ "--network host"
+ "-v ${NRM_ROOT}:/opt/src"
+ "--env CGO_ENABLED=0"
+ "--env GOOS=linux"
+ "--env GIT_VERSION=${GIT_VERSION}"
+ "--env GIT_SHA=${GIT_SHA}"
+ "--env GIT_BRANCH=${GIT_BRANCH}"
+ "--env BUILD_TIME=${BUILD_TIME}"
+ )
+ # use goproxy if build from inside mainland China
+ [[ $region == "cn" ]] && docker_run_opts+=("--env GOPROXY=https://goproxy.cn")
+
+ local docker_run_cmd=(
+ "/bin/sh"
+ "-xe"
+ "-c"
+ )
+
+ local sub_commands="sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories; \
+ apk --no-cache add bash; \
+ cd /opt/src; "
+ for arch in ${target_arch[@]}; do
+ sub_commands+="CGO_ENABLED=0 GOOS=linux GOARCH='${arch}' go build \
+ -ldflags '-X main._BRANCH_=${GIT_BRANCH} -X main._VERSION_=${IMG_VERSION} -X main._BUILDTIME_=${BUILD_TIME}' -o build/_output/nrm.${arch} main.go; "
+ done
+
+ docker run ${docker_run_opts[@]} ${NRM_BUILD_IMAGE} ${docker_run_cmd[@]} "${sub_commands}"
+}
+
+function build_images() {
+ for arch in ${target_arch[@]}; do
+ local docker_file_path=${NRM_BUILD_DIR}/Dockerfile.$arch
+ local docker_build_path=${NRM_BUILD_DIR}
+ local nrm_image=$IMG_REPO:${IMG_VERSION}.${arch}
+ local base_image
+ case $arch in
+ amd64)
+ base_image="amd64/alpine:3.10"
+ ;;
+ arm64)
+ base_image="arm64v8/alpine:3.10"
+ ;;
+ arm)
+ base_image="arm32v7/alpine:3.10"
+ ;;
+ *)
+ echo unknown arch $arch
+ exit 1
+ esac
+ cat << EOF > $docker_file_path
+FROM ${base_image}
+LABEL maintainers="OpenYurt Authors"
+LABEL description="OpenYurt Node Resource Manager"
+
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
+RUN apk update && apk add --no-cache ca-certificates file util-linux lvm2 xfsprogs e2fsprogs blkid
+COPY entrypoint.sh /entrypoint.sh
+COPY _output/nrm.${arch} /bin/nrm
+RUN chmod +x /bin/nrm && chmod +x /entrypoint.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
+EOF
+ docker build --no-cache -t $nrm_image -f $docker_file_path $docker_build_path
+ docker save $nrm_image > ${NRM_OUTPUT_DIR}/node-resource-manager-${arch}.tar
+ done
+}
+
+rm -rf ${NRM_OUTPUT_DIR}
+mkdir -p ${NRM_OUTPUT_DIR}
+umask 0022
+build_multi_arch_binaries
+build_images
diff --git a/build/entrypoint.sh b/build/entrypoint.sh
new file mode 100644
index 0000000..1ae02eb
--- /dev/null
+++ b/build/entrypoint.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+if uname -a | grep -i -q ubuntu; then
+ lvmLine=`/usr/bin/nsenter --mount=/proc/1/ns/mnt dpkg --get-selections lvm2 | grep install -w -i | wc -l`
+ if [ "$lvmLine" = "0" ]; then
+ /usr/bin/nsenter --mount=/proc/1/ns/mnt apt install lvm2 -y
+ fi
+else
+ lvmLine=`/usr/bin/nsenter --mount=/proc/1/ns/mnt rpm -qa lvm2 | wc -l`
+ if [ "$lvmLine" = "0" ]; then
+ /usr/bin/nsenter --mount=/proc/1/ns/mnt yum install lvm2 -y
+ fi
+fi
+
+if [ "$lvmLine" = "0" ]; then
+ /usr/bin/nsenter --mount=/proc/1/ns/mnt sed -i 's/udev_sync\ =\ 0/udev_sync\ =\ 1/g' /etc/lvm/lvm.conf
+ /usr/bin/nsenter --mount=/proc/1/ns/mnt sed -i 's/udev_rules\ =\ 0/udev_rules\ =\ 1/g' /etc/lvm/lvm.conf
+ /usr/bin/nsenter --mount=/proc/1/ns/mnt systemctl restart lvm2-lvmetad.service
+ echo "install lvm and starting..."
+else
+ udevLine=`/usr/bin/nsenter --mount=/proc/1/ns/mnt cat /etc/lvm/lvm.conf | grep "udev_sync = 0" | wc -l`
+ if [ "$udevLine" != "0" ]; then
+ /usr/bin/nsenter --mount=/proc/1/ns/mnt sed -i 's/udev_sync\ =\ 0/udev_sync\ =\ 1/g' /etc/lvm/lvm.conf
+ /usr/bin/nsenter --mount=/proc/1/ns/mnt sed -i 's/udev_rules\ =\ 0/udev_rules\ =\ 1/g' /etc/lvm/lvm.conf
+ /usr/bin/nsenter --mount=/proc/1/ns/mnt systemctl restart lvm2-lvmetad.service
+ echo "update lvm.conf file: udev_sync from 0 to 1, udev_rules from 0 to 1"
+ fi
+fi
+
+/bin/nrm $@
diff --git a/deploy/configmap.yaml b/deploy/configmap.yaml
new file mode 100644
index 0000000..bbd66c0
--- /dev/null
+++ b/deploy/configmap.yaml
@@ -0,0 +1,61 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: node-resource-topo
+ namespace: kube-system
+data:
+ volumegroup: |-
+ volumegroup:
+ - name: volumegroup1
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-zhangjiakou.192.168.3.114
+ topology:
+ type: device
+ devices:
+ - /dev/vdb
+ - /dev/vdc
+
+ - name: volumegroup1
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-beijing.192.168.3.35
+ topology:
+ type: pmem
+ regions:
+ - region0
+
+ quotapath: |-
+ quotapath:
+ - name: /mnt/path1
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-beijing.192.168.3.35
+ topology:
+ type: device
+ options: prjquota
+ fstype: ext4
+ devices:
+ - /dev/vdb
+
+ - name: /mnt/path2
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-beijing.192.168.3.36
+ topology:
+ type: pmem
+ options: prjquota,shared
+ fstype: ext4
+ regions:
+ - region0
+
+ memory: |-
+ memory:
+ - name: test1
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-beijing.192.168.3.37
+ topology:
+ type: pmem
+ regions:
+ - region0
diff --git a/deploy/nrm.yaml b/deploy/nrm.yaml
new file mode 100644
index 0000000..ea72110
--- /dev/null
+++ b/deploy/nrm.yaml
@@ -0,0 +1,92 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: node-resource-manager
+ namespace: kube-system
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: node-resource-manager
+rules:
+ - apiGroups: [""]
+ resources: ["configmaps"]
+ verbs: ["get", "watch", "list", "delete", "update", "create"]
+ - apiGroups: [""]
+ resources: ["nodes"]
+ verbs: ["get", "list", "watch"]
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: node-resource-manager-binding
+subjects:
+ - kind: ServiceAccount
+ name: node-resource-manager
+ namespace: kube-system
+roleRef:
+ kind: ClusterRole
+ name: node-resource-manager
+ apiGroup: rbac.authorization.k8s.io
+---
+kind: DaemonSet
+apiVersion: apps/v1
+metadata:
+ name: node-resource-manager
+ namespace: kube-system
+spec:
+ selector:
+ matchLabels:
+ app: node-resource-manager
+ template:
+ metadata:
+ labels:
+ app: node-resource-manager
+ spec:
+ tolerations:
+ - operator: "Exists"
+ priorityClassName: system-node-critical
+ serviceAccountName: node-resource-manager
+ hostNetwork: true
+ hostPID: true
+ containers:
+ - name: node-resource-manager
+ securityContext:
+ privileged: true
+ capabilities:
+ add: ["SYS_ADMIN"]
+ allowPrivilegeEscalation: true
+ image: openyurt/node-resource-manager:v1.0
+ imagePullPolicy: "Always"
+ args:
+ - "--nodeid=$(KUBE_NODE_NAME)"
+ env:
+ - name: KUBE_NODE_NAME
+ valueFrom:
+ fieldRef:
+ apiVersion: v1
+ fieldPath: spec.nodeName
+ volumeMounts:
+ - mountPath: /dev
+ mountPropagation: "HostToContainer"
+ name: host-dev
+ - mountPath: /var/log/
+ name: host-log
+ - name: etc
+ mountPath: /host/etc
+ - name: config
+ mountPath: /etc/unified-config
+ volumes:
+ - name: host-dev
+ hostPath:
+ path: /dev
+ - name: host-log
+ hostPath:
+ path: /var/log/
+ - name: etc
+ hostPath:
+ path: /etc
+ - name: config
+ configMap:
+ name: node-resource-topo
diff --git a/docs/configmap.md b/docs/configmap.md
new file mode 100644
index 0000000..d69df16
--- /dev/null
+++ b/docs/configmap.md
@@ -0,0 +1,148 @@
+# ConfigMap Intro
+
+"node-resource-topo" ConfigMap defines the resource spec in the whole cluster.
+Node-resource-manager on each node will perform create or update action according the resource definition in ConfigMap.
+
+## Supported Node Resources
+
+### LVM
+- LVM VolumeGroup creation, according to the VG defined in ConfigMap;
+- Create PV from local disk and add into VG;
+- Don't support deletion or shrink of VG, to avoid data lost risk;
+
+### QuotaPath
+- QuotaPath creation and mount, according to the definition in ConfigMap;
+- Don't support deletion or shrink of QuotaPath, to avoid data lost risk;
+
+### PMEM
+- Format pmem device as local memory, it can be later used by pod;
+
+## How to define target node
+We use the kubernetes label selector to choose target node:
+```yaml
+key: kubernetes.io/hostname
+operator: In
+value: xxxxx
+```
+- key: match the key in Node labels;
+- operator: Labels selector operator,
+ - In: matched only if current value equals to the value of the key in Node Labels;
+ - NotIn: matched only if current value not equals to the value of key in the Node Labels;
+ - Exists: matched if current key exists in Node Labels;
+ - DoesNotExist: matched if current key not exists in Node Labels;
+- value: match the corresponding value of the key in Node labels;
+
+## Example
+
+### LVM
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: node-resource-topo
+ namespace: kube-system
+data:
+ volumegroup: |-
+ volumegroup:
+ - name: volumegroup1
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-zhangjiakou.192.168.3.114
+ topology:
+ type: device
+ devices:
+ - /dev/vdb
+ - /dev/vdc
+
+ - name: volumegroup2
+ key: kubernetes.io/nodetype
+ operator: NotIn
+ value: localdisk
+ topology:
+ type: alibabacloud-local-disk
+
+ - name: volumegroup1
+ key: kubernetes.io/hostname
+ operator: Exists
+ value: cn-beijing.192.168.3.35
+ topology:
+ type: pmem
+ regions:
+ - region0
+```
+The above ConfigMap defines:
+- select nodes with label `kubernetes.io/hostname: cn-zhangjiakou.192.168.3.114`, and create LVM VolumeGroup named `volumegroup1` from device `/dev/vdb` and `/dev/vdc`;
+- select nodes without label `kubernetes.io/nodetype: localdisk`, and create LVM VolumeGroup named `volumegroup2` from all ecs cloud disks;
+- select nodes with label `kubernetes.io/hostname: cn-beijing.192.168.3.35`, and create LVM VolumeGroup named `volumegroup1` from pmem in `region0`;
+
+LVM currently supports three types of devices:
+- `type: device` define lvm on top of local block devices, the volumegroup's name is specified in `name` field;
+- `type: alibabacloud-local-disk` define lvm on top of attached cloud disks for alicloud ecs, the volumegroup's name is specified in `name` field;
+- `type: pmem` define lvm on top of local pmem resources, the volumegroup's name is specified in `name` field, the pmem regions is specified in `regions` field;
+
+### QuotaPath
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: node-resource-topo
+ namespace: kube-system
+data:
+ quotapath: |-
+ quotapath:
+ - name: /mnt/path1
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-beijing.192.168.3.35
+ topology:
+ type: device
+ options: prjquota
+ fstype: ext4
+ devices:
+ - /dev/vdb
+
+ - name: /mnt/path2
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-beijing.192.168.3.36
+ topology:
+ type: pmem
+ options: prjquota,shared
+ fstype: ext4
+ regions:
+ - region0
+```
+The above ConfigMap defines:
+- select nodes with label `kubernetes.io/hostname: cn-beijing.192.168.3.35`, and create quota path `/mnt/path1` mounted from device `/dev/vdb`, and format to ext4 filesystem with prjquota option;
+- select nodes with label `kubernetes.io/hostname: cn-beijing.192.168.3.36`, and create quota path `/mnt/path2` mounted from pmem in `region0`, and format to ext4 filesystem with prjquota, shared option;
+
+QuotaPath currently supports two types of devices:
+- `type: device` define quota path on top of local block device, the quota path is specified in `name` field:
+ - options: mount options, `prjquota` is mandatory;
+ - fstype: filesystem type, ext4 is used by default;
+ - devices: block device to be mounted, you should specify only one device;
+- `type: pmem` define quota path on top of local pmem resources, the quota path is specified in `name` field, you can speficy pmem regions in `regions` field;
+
+### PMEM
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: node-resource-topo
+ namespace: kube-system
+data:
+ memory: |-
+ memory:
+ - name: test1
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-beijing.192.168.3.37
+ topology:
+ type: pmem
+ regions:
+ - region0
+```
+The above ConfigMap defines:
+- select nodes with label `kubernetes.io/hostname: cn-beijing.192.168.3.37`, and create memory from pmem in `region0`;
+
+PMEM only support `type: pmem`, you can speficy pmem regions in `regions` field, name field is only a symbol and has no actual usage.
diff --git a/docs/configmap.zh.md b/docs/configmap.zh.md
new file mode 100644
index 0000000..444f4e8
--- /dev/null
+++ b/docs/configmap.zh.md
@@ -0,0 +1,152 @@
+# ConfigMap 介绍
+
+"node-resource-topo" ConfigMap 定义整个集群的设备拓扑,每个节点的 node-resource-manager 会根据 ConfigMap 中的资源定义决定是否创建或者更新资源;
+
+## 节点资源支持
+
+### LVM
+- 根据 ConfigMap 中定义,创建 LVM VolumeGroup;
+- 在 VG 中添加、扩展 PV;根据 ConfigMap 中定义的 VG 信息,判断是否执行 VG 扩容命令;
+- 考虑数据安全性,不支持 VG 的删除、缩容操作;
+
+### QuotaPath
+- QuotaPath 的创建, 根据 ConfigMap 中的定义来初始化相关本地资源设备以 QuotaPath 的形式挂载到指定路径上;
+- 不支持相关 QuotaPath 的变更, 删除等操作;
+
+### PMEM
+- 支持将持久化内存设备初始化为内存格式。后续可以直接被挂载到pod内部目录中使用;
+
+## 如何定义节点
+
+我们通过如下三个 key/value 来共同定义资源所在的节点:
+
+```yaml
+key: kubernetes.io/hostname
+operator: In
+value: xxxxx
+```
+- key: 匹配 Kubernetes Node Labels 中的 key 的值;
+- operator: Kubernetes 定义的 Labels selector operator,主要包含如下四种操作符;
+ - In: 只有 value 的值与 Node 上 Labels key 对应的值相同的时候才会匹配;
+ - NotIn: 只有 value 的值与 Node 上 Labels key 对应的 value 的值 ***不*** 相同的时候才会匹配;
+ - Exists: 只要 Node 的 Labels 上存在 Key 就会匹配;
+ - DoesNotExist: 只要 Node 的 Labels 上 ***不*** 存在 Key 就会匹配;
+- value: 匹配 Kubernetes Node Labels 的 key 对应的 value 的值;
+
+## 常用模板用例及说明
+
+### LVM
+
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: node-resource-topo
+ namespace: kube-system
+data:
+ volumegroup: |-
+ volumegroup:
+ - name: volumegroup1
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-zhangjiakou.192.168.3.114
+ topology:
+ type: device
+ devices:
+ - /dev/vdb
+ - /dev/vdc
+
+ - name: volumegroup2
+ key: kubernetes.io/nodetype
+ operator: NotIn
+ value: localdisk
+ topology:
+ type: alibabacloud-local-disk
+
+ - name: volumegroup1
+ key: kubernetes.io/hostname
+ operator: Exists
+ value: cn-beijing.192.168.3.35
+ topology:
+ type: pmem
+ regions:
+ - region0
+```
+上面的 ConfigMap 定义了
+1. 在拥有 Label key 等于 kubernetes.io/hostname 并且 Label key value 等于 cn-zhangjiakou.192.168.3.114 的 Node 上创建一个名字为 volumegroup1 的 LVM VolumeGroup, 这个 VolumeGroup 由宿主机上的 ```/dev/vdb, /dev/vdc``` 两个块设备组成
+2. 在拥有 Label key 等于 kubernetes.io/hostname 并且 Label key value 不等于 localdisk 的 Node 上创建一个名字为 volumegroup2 的 LVM VolumeGroup,这个 LVM VolumeGroup 由宿主机上的所有本地盘组成
+3. 在拥有 Label key 等于 kubernetes.io/hostname 的 Node 上创建一个名字为 volumegroup1 的 LVM VolumeGroup, 这个 VolumeGroup 由宿主机上的 Pmem 设备 region0 组成
+
+lvm目前仅支持三种定义资源拓扑的方式
+- 当定义 ```type: device``` 的时候是通过 nrm 所在宿主机上存在的块设备 devices 进行 lvm 的声明,声明的块设备会组成一个 volumegroup, volumegroup 的名字由 name 字段指定,供后续应用启动时分配 logical volume。```type: device``` 类型与下面的 ```devices``` 字段绑定;
+- 当定义 ```type: alibabacloud-local-disk``` 的时候指的是使用 urm 所在宿主机上所有的本地盘 (选择 ecs 类型带有本地盘的 instance , 例如 本地 SSD 型 i2, 手动挂载到 ecs 上的云盘不是本地盘) 共同创建一个 名称为 name 值的 volumegroup;
+- 当定义 ```type: pmem``` 的时候是使用 urm 所在宿主机上的 pmem 资源创建一个名称为 name 值的 volumegroup, 其中 regions 可以指定当前机器上多个 pmem region 资源。```type: pmem``` 类型与下面的 ```regions``` 字段绑定。
+
+### QuotaPath
+
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: node-resource-topo
+ namespace: kube-system
+data:
+ quotapath: |-
+ quotapath:
+ - name: /mnt/path1
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-beijing.192.168.3.35
+ topology:
+ type: device
+ options: prjquota
+ fstype: ext4
+ devices:
+ - /dev/vdb
+
+ - name: /mnt/path2
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-beijing.192.168.3.36
+ topology:
+ type: pmem
+ options: prjquota,shared
+ fstype: ext4
+ regions:
+ - region0
+```
+上面的 ConfigMap 定义了
+1. 在拥有 Label key 等于 kubernetes.io/hostname 并且 Label key value 等于 cn-beijing.192.168.3.35 的 Node 上的 ```/mnt/path1``` 上以 prjquota 类型挂载 ```/dev/vdb``` 的块设备, 并且格式化成 project quota ext4 格式。
+2. 在拥有 Label key 等于 kubernetes.io/hostname 并且 Label key value 等于 cn-beijing.192.168.3.36 的 Node 上的 ```/mnt/path2``` 上以prjquota 类型挂载 ```region0``` 的 Pmem 设备, 并且格式化成 project quota ext4 格式。
+
+QuotaPath 类型的本地资源仅支持两种定义资源拓扑的方式:
+- 当定义 ```type: device``` 的时候,使用的是 nrm 所在宿主机的块设备进行 QuotaPath 的初始化,初始化路径是 name 字段定义的值, 下面解释下其他几个字段的定义:
+ - options: 块设备在被挂载的时候使用的参数。无特殊需求使用例子中提供的参数即可;
+ - fstype: 格式化块设备使用的文件系统,默认使用 ext4;
+ - devices:挂载使用的块设备,只可以声明一个;
+
+### pmem
+
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: node-resource-topo
+ namespace: kube-system
+data:
+ memory: |-
+ memory:
+ - name: test1
+ key: kubernetes.io/hostname
+ operator: In
+ value: cn-beijing.192.168.3.37
+ topology:
+ type: pmem
+ regions:
+ - region0
+```
+
+上面的 ConfigMap 定义了
+1. 在拥有 Label key 等于 kubernetes.io/hostname 并且 Label key value 等于 cn-beijing.192.168.3.37 的 Node 上用 Pmem 的 Region0 设备初始化一个内存格式的 Pmem 设备。
+
+持久化内存类型本地资源只支持 pmem type 的挂载,其中 regions 代表需要被格式化成 memory 的 pmem 设备,name 字段在这里只起到标识作用,无实际意义。
diff --git a/docs/developer-guide.md b/docs/developer-guide.md
new file mode 100644
index 0000000..602ae96
--- /dev/null
+++ b/docs/developer-guide.md
@@ -0,0 +1,15 @@
+# Developer Guide
+
+There's a `Makefile` in the root folder.
+
+
+
+- Build docker images for specific architecture, supported architectures are `amd64`,`arm`,`arm64`:
+```bash
+ARCH=amd64 make build
+```
+
+- Build all docker images for all supported architectures.
+```bash
+make build
+```
diff --git a/docs/images/node-resource-manager.png b/docs/images/node-resource-manager.png
new file mode 100644
index 0000000..d8c5a1e
Binary files /dev/null and b/docs/images/node-resource-manager.png differ
diff --git a/docs/roadmap.md b/docs/roadmap.md
new file mode 100644
index 0000000..bf845cb
--- /dev/null
+++ b/docs/roadmap.md
@@ -0,0 +1,3 @@
+# Roadmap
+
+This document outlines the development roadmap for the node-resource-manager project.
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..270ae8e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,23 @@
+module github.com/openyurtio/node-resource-manager
+
+go 1.15
+
+require (
+ github.com/aliyun/alibaba-cloud-sdk-go v1.61.895
+ github.com/golang/mock v1.3.1
+ github.com/imdario/mergo v0.3.11 // indirect
+ github.com/sirupsen/logrus v1.7.0
+ github.com/stretchr/testify v1.6.1
+ golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
+ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
+ gopkg.in/yaml.v2 v2.4.0
+ k8s.io/api v0.20.2
+ k8s.io/apimachinery v0.20.2
+ k8s.io/client-go v11.0.0+incompatible
+ k8s.io/utils v0.0.0-20210111153108-fddb29f9d009
+)
+
+replace (
+ k8s.io/api => k8s.io/api v0.19.2
+ k8s.io/client-go => k8s.io/client-go v0.19.2
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..74780d2
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,388 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
+github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
+github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
+github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
+github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
+github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
+github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
+github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/aliyun/alibaba-cloud-sdk-go v1.61.895 h1:8OvEa8rSbkUzjBNTj3vV9mJx+i8Tw6YS1J26FyBURDw=
+github.com/aliyun/alibaba-cloud-sdk-go v1.61.895/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
+github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
+github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
+github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
+github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+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.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
+golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
+gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+k8s.io/api v0.19.2 h1:q+/krnHWKsL7OBZg/rxnycsl9569Pud76UJ77MvKXms=
+k8s.io/api v0.19.2/go.mod h1:IQpK0zFQ1xc5iNIQPqzgoOwuFugaYHK4iCknlAQP9nI=
+k8s.io/apimachinery v0.19.2/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
+k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg=
+k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/client-go v0.19.2 h1:gMJuU3xJZs86L1oQ99R4EViAADUPMHHtS9jFshasHSc=
+k8s.io/client-go v0.19.2/go.mod h1:S5wPhCqyDNAlzM9CnEdgTGV4OqhsW3jGO1UM1epwfJA=
+k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
+k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
+k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
+k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+k8s.io/utils v0.0.0-20210111153108-fddb29f9d009 h1:0T5IaWHO3sJTEmCP6mUlBvMukxPKUQWqiI/YuiBNMiQ=
+k8s.io/utils v0.0.0-20210111153108-fddb29f9d009/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..052179a
--- /dev/null
+++ b/main.go
@@ -0,0 +1,123 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package main
+
+import (
+ "flag"
+ "io"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/openyurtio/node-resource-manager/pkg/manager"
+ "github.com/openyurtio/node-resource-manager/pkg/signals"
+ log "github.com/sirupsen/logrus"
+)
+
+const (
+ // LogfilePrefix prefix of log file
+ LogfilePrefix = "/var/log/openyurt/"
+ // MBSIZE MB size
+ MBSIZE = 1024 * 1024
+)
+
+// BRANCH is NRM Branch
+var _BRANCH_ = ""
+
+// VERSION is NRM Version
+var _VERSION_ = ""
+
+// BUILDTIME is NRM Buildtime
+var _BUILDTIME_ = ""
+
+var (
+ nodeID = flag.String("nodeid", "", "node id")
+ cmName = flag.String("cm-name", "node-resource-manager", "used configmap name")
+ cmNameSpace = flag.String("cm-namespace", "kube-system", "used configmap namespace")
+ logLevel = flag.String("log-level", "Info", "Set Log Level")
+ updateInterval = flag.Int("update-interval", 30, "Node Storage update internal time(s)")
+ masterURL = flag.String("master", "", "The address of the Kubernetes API server (https://hostname:port, overrides any value in kubeconfig)")
+ kubeconfig = flag.String("kubeconfig", "", "Path to kubeconfig file with authorization and master location information")
+)
+
+// main
+func main() {
+ flag.Parse()
+
+ // set log config
+ setLogAttribute("unified-resource-manager")
+ log.Infof("Unified Resource Manager, Version: %s, Build Time: %s", _VERSION_, _BUILDTIME_)
+
+ // new signal handler
+ stopCh := signals.SetupSignalHandler()
+
+ // New Controller Manager
+ manager := manager.NewManager(*nodeID, *cmName, *cmNameSpace, *updateInterval, *masterURL, *kubeconfig)
+ manager.Run(stopCh)
+
+ os.Exit(0)
+}
+
+func init() {
+ flag.Set("logtostderr", "true")
+}
+
+// rotate log file by 2M bytes
+// default print log to stdout and file both.
+func setLogAttribute(driver string) {
+ logType := os.Getenv("LOG_TYPE")
+ logType = strings.ToLower(logType)
+ if logType != "stdout" && logType != "host" {
+ logType = "both"
+ }
+ if logType == "stdout" {
+ return
+ }
+
+ os.MkdirAll(LogfilePrefix, os.FileMode(0755))
+ logFile := LogfilePrefix + driver + ".log"
+ f, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+ if err != nil {
+ os.Exit(1)
+ }
+
+ // rotate the log file if too large
+ if fi, err := f.Stat(); err == nil && fi.Size() > 2*MBSIZE {
+ f.Close()
+ timeStr := time.Now().Format("-2006-01-02-15:04:05")
+ timedLogfile := LogfilePrefix + driver + timeStr + ".log"
+ os.Rename(logFile, timedLogfile)
+ f, err = os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+ if err != nil {
+ os.Exit(1)
+ }
+ }
+ if logType == "both" {
+ mw := io.MultiWriter(os.Stdout, f)
+ log.SetOutput(mw)
+ } else {
+ log.SetOutput(f)
+ }
+
+ logLevelLow := strings.ToLower(*logLevel)
+ if logLevelLow == "debug" {
+ log.SetLevel(log.DebugLevel)
+ } else if logLevelLow == "warning" {
+ log.SetLevel(log.WarnLevel)
+ }
+ log.Infof("Set Log level to %s...", logLevelLow)
+}
diff --git a/pkg/config/cloud.go b/pkg/config/cloud.go
new file mode 100644
index 0000000..54df3d3
--- /dev/null
+++ b/pkg/config/cloud.go
@@ -0,0 +1,104 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package config
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs"
+ "github.com/openyurtio/node-resource-manager/pkg/utils"
+ log "github.com/sirupsen/logrus"
+)
+
+func newEcsClient(accessKeyID, accessKeySecret, accessToken string) (ecsClient *ecs.Client) {
+ var err error
+ regionID, _ := utils.GetMetaData(RegionIDTag)
+ if accessToken == "" {
+ ecsClient, err = ecs.NewClientWithAccessKey(regionID, accessKeyID, accessKeySecret)
+ if err != nil {
+ return nil
+ }
+ } else {
+ ecsClient, err = ecs.NewClientWithStsToken(regionID, accessKeyID, accessKeySecret, accessToken)
+ if err != nil {
+ return nil
+ }
+ }
+ return
+}
+
+// GetDefaultAK read default ak from local file or from STS
+func GetDefaultAK() (string, string, string) {
+ accessKeyID, accessSecret := GetLocalAK()
+
+ accessToken := ""
+ if accessKeyID == "" || accessSecret == "" {
+ accessKeyID, accessSecret, accessToken = GetSTSAK()
+ }
+
+ return accessKeyID, accessSecret, accessToken
+}
+
+func GetLocalAK() (string, string) {
+ var accessKeyID, accessSecret string
+ // first check if the environment setting
+ accessKeyID = os.Getenv("ACCESS_KEY_ID")
+ accessSecret = os.Getenv("ACCESS_KEY_SECRET")
+ if accessKeyID != "" && accessSecret != "" {
+ return accessKeyID, accessSecret
+ }
+
+ return accessKeyID, accessSecret
+}
+
+// RoleAuth define STS Token Response
+type RoleAuth struct {
+ AccessKeyID string
+ AccessKeySecret string
+ Expiration time.Time
+ SecurityToken string
+ LastUpdated time.Time
+ Code string
+}
+
+// GetSTSAK get STS AK and token from ecs meta server
+func GetSTSAK() (string, string, string) {
+ roleAuth := RoleAuth{}
+ subpath := "ram/security-credentials/"
+ roleName, err := utils.GetMetaData(subpath)
+ if err != nil {
+ log.Errorf("GetSTSToken: request roleName with error: %s", err.Error())
+ return "", "", ""
+ }
+
+ fullPath := filepath.Join(subpath, roleName)
+ roleInfo, err := utils.GetMetaData(fullPath)
+ if err != nil {
+ log.Errorf("GetSTSToken: request roleInfo with error: %s", err.Error())
+ return "", "", ""
+ }
+
+ err = json.Unmarshal([]byte(roleInfo), &roleAuth)
+ if err != nil {
+ log.Errorf("GetSTSToken: unmarshal roleInfo: %s, with error: %s", roleInfo, err.Error())
+ return "", "", ""
+ }
+ return roleAuth.AccessKeyID, roleAuth.AccessKeySecret, roleAuth.SecurityToken
+}
diff --git a/pkg/config/global.go b/pkg/config/global.go
new file mode 100644
index 0000000..a9a76ca
--- /dev/null
+++ b/pkg/config/global.go
@@ -0,0 +1,71 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package config
+
+import (
+ "context"
+
+ log "github.com/sirupsen/logrus"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/tools/clientcmd"
+)
+
+// GlobalConfig save global values for plugin
+type GlobalConfig struct {
+ //EcsClient *ecs.Client
+ //Region string
+ NodeInfo *v1.Node
+ KubeClient *kubernetes.Clientset
+}
+
+var (
+ GlobalConfigVar GlobalConfig
+)
+
+const (
+ // RegionIDTag is region id
+ RegionIDTag = "region-id"
+ // InstanceIDTag is instance id
+ InstanceIDTag = "instance-id"
+)
+
+// GlobalConfigSet set Global Config
+func GlobalConfigSet(nodeID, masterURL, kubeconfig string) {
+
+ // Global Configs Set
+ cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
+ if err != nil {
+ log.Fatalf("Error building kubeconfig: %s", err.Error())
+ }
+ kubeClient, err := kubernetes.NewForConfig(cfg)
+ if err != nil {
+ log.Fatalf("Error building kubernetes clientset: %s", err.Error())
+ }
+
+ node, err := kubeClient.CoreV1().Nodes().Get(context.Background(), nodeID, metav1.GetOptions{})
+ if err != nil {
+ log.Fatal("Error get current node info: %s", err.Error())
+ }
+
+ // Global Config Set
+ GlobalConfigVar = GlobalConfig{
+ KubeClient: kubeClient,
+ NodeInfo: node,
+ }
+}
diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go
new file mode 100644
index 0000000..df583ec
--- /dev/null
+++ b/pkg/manager/manager.go
@@ -0,0 +1,113 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package manager
+
+import (
+ "context"
+ "time"
+
+ "github.com/openyurtio/node-resource-manager/pkg/config"
+ "github.com/openyurtio/node-resource-manager/pkg/manager/memory"
+ "github.com/openyurtio/node-resource-manager/pkg/manager/quotapath"
+ "github.com/openyurtio/node-resource-manager/pkg/manager/volumegroup"
+ log "github.com/sirupsen/logrus"
+ "k8s.io/client-go/kubernetes"
+)
+
+// Manager interface define local resource manager's action
+type Manager interface {
+ AnalyseConfigMap() error
+ ApplyResourceDiff() error
+}
+
+// UnifiedResourceManager is global resource manager struct
+type UnifiedResourceManager struct {
+ KubeClientSet *kubernetes.Clientset
+ UpdateInterval int
+ NodeID string
+}
+
+// NewDriver create a cpfs driver object
+func NewManager(nodeID, cmName, cmNameSpace string, updateInterval int, masterURL, kubeconfig string) *UnifiedResourceManager {
+ manager := &UnifiedResourceManager{
+ NodeID: nodeID,
+ UpdateInterval: updateInterval,
+ }
+ // Config GlobalVar
+ config.GlobalConfigSet(nodeID, masterURL, kubeconfig)
+
+ return manager
+}
+
+// Run Start the UnifiedResourceManager
+// First will check UnifiedResource exist or not;
+// Update UnifiedResource every internal seconds, the total/free capacity, storage status;
+// Maintain Unified Resource, Like: local disk in alibaba cloud cluster.
+func (urm *UnifiedResourceManager) Run(stopCh <-chan struct{}) {
+ ctx := context.Background()
+
+ // Create UnifiedResource CR if not exist
+ urm.CreateUnifiedResourceCRD(ctx)
+
+ // Maintain VolumeGroup if set in configMap
+ go urm.BuildUnifiedResource()
+
+ // UpdateUnifiedStorage
+ // go wait.Until(urm.RecordUnifiedResources, time.Duration(urm.UpdateInterval)*time.Second, stopCh)
+
+ log.Infof("Starting to update node storage on %s...", urm.NodeID)
+ <-stopCh
+ log.Infof("Stop to update node storage...")
+}
+
+// CreateUnifiedResourceCRD ...
+func (urm *UnifiedResourceManager) CreateUnifiedResourceCRD(ctx context.Context) error {
+ return nil
+}
+
+// BuildUnifiedResource ...
+func (urm *UnifiedResourceManager) BuildUnifiedResource() {
+ log.Infof("BuildUnifiedResource:: Starting to maintain unified storage...")
+ rms := []Manager{volumegroup.NewResourceManager(), quotapath.NewResourceManager(), memory.NewResourceManager()}
+
+ for {
+ for _, rm := range rms {
+ err := BuildResource(rm)
+ if err != nil {
+ continue
+ }
+ }
+ time.Sleep(time.Duration(20) * time.Second)
+ }
+}
+
+// BuildResource ...
+func BuildResource(m Manager) error {
+
+ // Get Desired VolumeGroup from ConfigMap
+ err := m.AnalyseConfigMap()
+ if err != nil {
+ return err
+ }
+ return m.ApplyResourceDiff()
+
+}
+
+// Update Unified Storage CRD every internal seconds
+func (urm *UnifiedResourceManager) RecordUnifiedResources() {
+ // Get Unified Storage Object
+}
diff --git a/pkg/manager/memory/memory.go b/pkg/manager/memory/memory.go
new file mode 100644
index 0000000..721c058
--- /dev/null
+++ b/pkg/manager/memory/memory.go
@@ -0,0 +1,114 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package memory
+
+import (
+ "io/ioutil"
+ "os"
+
+ "github.com/openyurtio/node-resource-manager/pkg/config"
+ "github.com/openyurtio/node-resource-manager/pkg/utils"
+ log "github.com/sirupsen/logrus"
+ "gopkg.in/yaml.v2"
+)
+
+// ResourceManager ...
+type ResourceManager struct {
+ Memory []*MConfig
+ pmem utils.Pmemer
+ configPath string
+}
+
+// NewResourceManager ...
+func NewResourceManager() *ResourceManager {
+ return &ResourceManager{
+ Memory: []*MConfig{},
+ pmem: utils.NewNodePmemer(),
+ configPath: "/etc/unified-config/memory",
+ }
+}
+
+// AnalyseConfigMap analyse memory resource config
+func (mrm *ResourceManager) AnalyseConfigMap() error {
+ memoryConfig := []*MConfig{}
+ memoryList := &MList{}
+
+ yamlFile, err := ioutil.ReadFile(mrm.configPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ log.Debugf("memory config file %s not exist", mrm.configPath)
+ return nil
+ }
+ log.Errorf("AnalyseConfigMap:: yamlFile.Get memory error %v", err)
+ return err
+ }
+ err = yaml.Unmarshal(yamlFile, memoryList)
+ if err != nil {
+ log.Errorf("AnalyseConfigMap:: parse yaml file error: %v", err)
+ return err
+ }
+
+ nodeInfo := config.GlobalConfigVar.NodeInfo
+ for _, memConfig := range memoryList.Memories {
+ isMatched := utils.NodeFilter(memConfig.Operator, memConfig.Key, memConfig.Value, nodeInfo)
+ if isMatched {
+ conf := &MConfig{}
+ if len(memConfig.Topology.Regions) != 1 {
+ log.Errorf("AnalyseConfigMap:: regions has multi config %s", memConfig.Topology.Regions)
+ continue
+ }
+ conf.Region = memConfig.Topology.Regions[0]
+ conf.Type = memConfig.Topology.Type
+ memoryConfig = append(memoryConfig, conf)
+ }
+ }
+ mrm.Memory = memoryConfig
+ return nil
+}
+
+// ApplyResourceDiff apply memory resource to current node
+func (mrm *ResourceManager) ApplyResourceDiff() error {
+ log.Infof("ApplyResourceDiff: matched node resources mrm.Memory: %v", mrm.Memory)
+ for _, memConfig := range mrm.Memory {
+ devicePath, _, err := mrm.pmem.GetPmemNamespaceDeivcePath(memConfig.Region, "devdax")
+ if err != nil {
+ err := mrm.pmem.CreateNamespace(memConfig.Region, "dax")
+ if err != nil {
+ log.Errorf("applyResourceDiff:: create kmem namespace for region [%s], error: %v", memConfig.Region, err)
+ continue
+ }
+ devicePath, _, err = mrm.pmem.GetPmemNamespaceDeivcePath(memConfig.Region, "devdax")
+ if err != nil {
+ log.Errorf("applyResourceDiff:: list kmem namespace for region [%s], error: %v", memConfig.Region, err)
+ continue
+ }
+ }
+ isCreated, err := mrm.pmem.CheckKMEMCreated(devicePath[5:])
+ if err != nil {
+ log.Errorf("applyResourceDiff:: check kmem create error: %v", err)
+ continue
+ }
+ if !isCreated {
+ err := mrm.pmem.MakeNamespaceMemory(devicePath[5:])
+ if err != nil {
+ log.Errorf("applyRegionQuotaPath:: make kmem memory failed %v", err)
+ continue
+ }
+ }
+ }
+ return nil
+}
diff --git a/pkg/manager/memory/memory_test.go b/pkg/manager/memory/memory_test.go
new file mode 100644
index 0000000..7149e38
--- /dev/null
+++ b/pkg/manager/memory/memory_test.go
@@ -0,0 +1,127 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package memory
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/openyurtio/node-resource-manager/pkg/config"
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+ "github.com/openyurtio/node-resource-manager/pkg/utils"
+ "github.com/stretchr/testify/assert"
+ "gopkg.in/yaml.v2"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func makeValidResourceYaml() *model.ResourceYaml {
+ return &model.ResourceYaml{
+ Name: "foo",
+ }
+}
+
+func makeResourceYamlCustom(tweaks ...func(*model.ResourceYaml)) *model.ResourceYaml {
+ resourceYaml := makeValidResourceYaml()
+ for _, fn := range tweaks {
+ fn(resourceYaml)
+ }
+ return resourceYaml
+}
+
+// EnsureVolumeGroupEnv ...
+func EnsureVolumeGroupEnv() (string, error, *ResourceManager) {
+ config.GlobalConfigVar.NodeInfo = &v1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "foo",
+ Labels: map[string]string{"bar": "foo"},
+ },
+ }
+ configPath := "/tmp/memory"
+ mdkirCmd := "mkdir"
+ _, err := exec.LookPath(mdkirCmd)
+ if err != nil {
+ if err == exec.ErrNotFound {
+ return "", fmt.Errorf("EnsureFolder:: %q executable not found in $PATH", mdkirCmd), nil
+ }
+ return "", err, nil
+ }
+
+ mkdirArgs := []string{"-p", filepath.Dir(configPath)}
+ //log.Infof("mkdir for folder, the command is %s %v", mdkirCmd, mkdirArgs)
+ _, err = exec.Command(mdkirCmd, mkdirArgs...).CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("EnsureFolder:: mkdir for folder error: %v", err), nil
+ }
+
+ newMockVolumegroupResourceManager := func() *ResourceManager {
+ return &ResourceManager{
+ configPath: configPath,
+ }
+ }
+ return configPath, nil, newMockVolumegroupResourceManager()
+}
+
+func TestAnalyseConfigMap(t *testing.T) {
+ mockCtl := gomock.NewController(t)
+ defer mockCtl.Finish()
+ configPath, err, resourceManager := EnsureVolumeGroupEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(configPath)
+ mockPmemer := utils.NewMockPmemer(mockCtl)
+ resourceManager.pmem = mockPmemer
+ setOpInOperatorElement := func(m *model.ResourceYaml) {
+ m.Key = "bar"
+ m.Operator = metav1.LabelSelectorOpIn
+ m.Value = "foo"
+ }
+
+ setPmemTopology := func(m *model.ResourceYaml) {
+ m.Topology = model.Topology{
+ Type: "pmem",
+ Regions: []string{"region0"},
+ }
+ }
+ testYamls := MList{Memories: []model.ResourceYaml{
+ *makeResourceYamlCustom(setOpInOperatorElement, setPmemTopology),
+ }}
+
+ d, err := yaml.Marshal(&testYamls)
+ if err != nil {
+ t.Error()
+ }
+ err = ioutil.WriteFile(configPath, d, 0777)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Nil(t, resourceManager.AnalyseConfigMap())
+ assert.Equal(t, 1, len(resourceManager.Memory))
+ gomock.InOrder(
+ mockPmemer.EXPECT().GetPmemNamespaceDeivcePath(gomock.Eq("region0"), gomock.Eq("devdax")).Return("/dev/dax0.0", "", nil),
+ mockPmemer.EXPECT().CheckKMEMCreated(gomock.Eq("dax0.0")).Return(true, nil),
+ )
+ assert.Nil(t, resourceManager.ApplyResourceDiff())
+
+}
diff --git a/pkg/manager/memory/type.go b/pkg/manager/memory/type.go
new file mode 100644
index 0000000..a1908af
--- /dev/null
+++ b/pkg/manager/memory/type.go
@@ -0,0 +1,32 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package memory
+
+import (
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+)
+
+// MConfig ...
+type MConfig struct {
+ Type string
+ Region string
+}
+
+// MList ...
+type MList struct {
+ Memories []model.ResourceYaml `yaml:"memory,omitempty"`
+}
diff --git a/pkg/manager/quotapath/quotapath.go b/pkg/manager/quotapath/quotapath.go
new file mode 100644
index 0000000..4f21c86
--- /dev/null
+++ b/pkg/manager/quotapath/quotapath.go
@@ -0,0 +1,175 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package quotapath
+
+import (
+ "io/ioutil"
+ "os"
+ "strings"
+
+ "github.com/openyurtio/node-resource-manager/pkg/config"
+ "github.com/openyurtio/node-resource-manager/pkg/utils"
+ log "github.com/sirupsen/logrus"
+ "gopkg.in/yaml.v2"
+)
+
+// ResourceManager ...
+type ResourceManager struct {
+ DeviceQuotaPath map[string]*QpConfig
+ RegionQuotaPath map[string]*QpConfig
+ mounter utils.Mounter
+ mkfsOption []string
+ pmemer utils.Pmemer
+ configPath string
+}
+
+// NewResourceManager ...
+func NewResourceManager() *ResourceManager {
+ return &ResourceManager{
+ DeviceQuotaPath: make(map[string]*QpConfig),
+ RegionQuotaPath: make(map[string]*QpConfig),
+ mounter: utils.NewMounter(),
+ pmemer: utils.NewNodePmemer(),
+ configPath: "/etc/unified-config/quotapath",
+ }
+}
+
+// AnalyseConfigMap analyse quotapath resource config
+func (qrm *ResourceManager) AnalyseConfigMap() error {
+ deviceQuotaConfig := map[string]*QpConfig{}
+ regionQuotaConfig := map[string]*QpConfig{}
+ quotaPathList := &QPList{}
+ yamlFile, err := ioutil.ReadFile(qrm.configPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ log.Debugf("quota config file %s not exist", qrm.configPath)
+ return nil
+ }
+ log.Errorf("AnalyseConfigMap:: yamlFile.Get error %v", err)
+ return err
+ }
+ err = yaml.Unmarshal(yamlFile, quotaPathList)
+ if err != nil {
+ log.Errorf("AnalyseConfigMap:: parse yaml file error: %v", err)
+ return err
+ }
+ mountPathMap := map[string]string{}
+ nodeInfo := config.GlobalConfigVar.NodeInfo
+ for _, quotaConfig := range quotaPathList.QuotaPaths {
+ isMatched := utils.NodeFilter(quotaConfig.Operator, quotaConfig.Key, quotaConfig.Value, nodeInfo)
+ if isMatched {
+ if _, ok := mountPathMap[quotaConfig.Name]; ok {
+ log.Errorf("AnalyseConfigMap:: quotapath config has multi mount path config [%s] on same node", quotaConfig.Name)
+ continue
+ }
+ switch quotaConfig.Topology.Type {
+ case "device":
+ conf := &QpConfig{}
+ if len(quotaConfig.Topology.Devices) != 1 {
+ log.Errorf("AnalyseConfigMap:: quotapath regions [%v] config only support one device", quotaConfig.Topology.Regions)
+ continue
+ }
+ conf.Device = quotaConfig.Topology.Devices[0]
+ conf.Fstype = quotaConfig.Topology.Fstype
+ conf.Options = quotaConfig.Topology.Options
+ conf.Type = quotaConfig.Topology.Type
+ deviceQuotaConfig[quotaConfig.Name] = conf
+ case "pmem":
+ conf := &QpConfig{}
+ if len(quotaConfig.Topology.Regions) != 1 {
+ log.Errorf("AnalyseConfigMap:: quotapath regions [%s] config only support one device", quotaConfig.Topology.Regions)
+ continue
+ }
+ conf.Region = quotaConfig.Topology.Regions[0]
+ conf.Fstype = quotaConfig.Topology.Fstype
+ conf.Options = quotaConfig.Topology.Options
+ conf.Type = quotaConfig.Topology.Type
+ regionQuotaConfig[quotaConfig.Name] = conf
+ default:
+ log.Errorf("AnalyseConfigMap:: not support quotapath config type: [%v]", quotaConfig.Topology.Type)
+ continue
+ }
+ mountPathMap[quotaConfig.Name] = ""
+ }
+ }
+
+ qrm.DeviceQuotaPath = deviceQuotaConfig
+ qrm.RegionQuotaPath = regionQuotaConfig
+ return nil
+}
+
+// ApplyResourceDiff apply quotapath resource to current node
+func (qrm *ResourceManager) ApplyResourceDiff() error {
+ log.Infof("ApplyResourceDiff: matched node resources qrm.DeviceQuotaPath: %v, qrm.RegionQuotaPath: %v", qrm.DeviceQuotaPath, qrm.RegionQuotaPath)
+ qrm.mkfsOption = strings.Split("-O project,quota", " ")
+ err := qrm.applyDeivceQuotaPath()
+ if err != nil {
+ log.Errorf("ApplyResourceDiff:: apply deivce quotapath error: %v", err)
+ }
+ err = qrm.applyRegionQuotaPath()
+ if err != nil {
+ log.Errorf("ApplyResourceDiff:: apply region quotapath error: %v", err)
+ }
+ return err
+}
+
+func (qrm *ResourceManager) applyDeivceQuotaPath() error {
+
+ for mountPath, deivceQuotaPathConfig := range qrm.DeviceQuotaPath {
+ err := qrm.mounter.EnsureFolder(mountPath)
+ if err != nil {
+ log.Errorf("applyDeivceQuotaPath:: ensure quotapath error: %v", err)
+ continue
+ }
+ err = qrm.mounter.FormatAndMount(deivceQuotaPathConfig.Device, mountPath, deivceQuotaPathConfig.Fstype, qrm.mkfsOption, deivceQuotaPathConfig.Options)
+ if err != nil {
+ log.Errorf("applyDeivceQuotaPath:: mounter FormatAndMount error: %v", err)
+ continue
+ }
+ }
+ return nil
+}
+
+func (qrm *ResourceManager) applyRegionQuotaPath() error {
+ for mountPath, regionQuotaPathConfig := range qrm.RegionQuotaPath {
+ devicePath, _, err := qrm.pmemer.GetPmemNamespaceDeivcePath(regionQuotaPathConfig.Region, "fsdax")
+ if err != nil {
+ if strings.Contains(err.Error(), "list Namespace for region get 0 or multi namespaces") {
+ err := qrm.pmemer.CreateNamespace(regionQuotaPathConfig.Region, "lvm")
+ if err != nil {
+ log.Errorf("applyRegionQuotaPath:: create namespace for region [%s], error: %v", regionQuotaPathConfig.Region, err)
+ continue
+ }
+ devicePath, _, err = qrm.pmemer.GetPmemNamespaceDeivcePath(regionQuotaPathConfig.Region, "fsdax")
+ } else {
+ log.Errorf("applyRegionQuotaPath:: get region [%s] namespace device path error: %v", regionQuotaPathConfig.Region, err)
+ continue
+ }
+ }
+ err = qrm.mounter.EnsureFolder(mountPath)
+ if err != nil {
+ log.Errorf("applyRegionQuotaPath:: ensure quotapath error: %v", err)
+ continue
+ }
+ err = qrm.mounter.FormatAndMount(devicePath, mountPath, regionQuotaPathConfig.Fstype, qrm.mkfsOption, regionQuotaPathConfig.Options)
+ if err != nil {
+ log.Errorf("applyRegionQuotaPath:: mounter FormatAndMount error: %v", err)
+ continue
+ }
+ }
+ return nil
+}
diff --git a/pkg/manager/quotapath/quotapath_test.go b/pkg/manager/quotapath/quotapath_test.go
new file mode 100644
index 0000000..03bac62
--- /dev/null
+++ b/pkg/manager/quotapath/quotapath_test.go
@@ -0,0 +1,151 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package quotapath
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/openyurtio/node-resource-manager/pkg/config"
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+ "github.com/openyurtio/node-resource-manager/pkg/utils"
+ "github.com/stretchr/testify/assert"
+ "gopkg.in/yaml.v2"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func makeValidResourceYaml() *model.ResourceYaml {
+ return &model.ResourceYaml{
+ Name: "/tmp/foo",
+ }
+}
+
+func makeResourceYamlCustom(tweaks ...func(*model.ResourceYaml)) *model.ResourceYaml {
+ resourceYaml := makeValidResourceYaml()
+ for _, fn := range tweaks {
+ fn(resourceYaml)
+ }
+ return resourceYaml
+}
+
+// EnsureVolumeGroupEnv ...
+func EnsureVolumeGroupEnv() (string, error, *ResourceManager) {
+ config.GlobalConfigVar.NodeInfo = &v1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "foo",
+ Labels: map[string]string{"bar": "foo"},
+ },
+ }
+ configPath := "/tmp/quotapath"
+ mdkirCmd := "mkdir"
+ _, err := exec.LookPath(mdkirCmd)
+ if err != nil {
+ if err == exec.ErrNotFound {
+ return "", fmt.Errorf("EnsureFolder:: %q executable not found in $PATH", mdkirCmd), nil
+ }
+ return "", err, nil
+ }
+
+ mkdirArgs := []string{"-p", filepath.Dir(configPath)}
+ //log.Infof("mkdir for folder, the command is %s %v", mdkirCmd, mkdirArgs)
+ _, err = exec.Command(mdkirCmd, mkdirArgs...).CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("EnsureFolder:: mkdir for folder error: %v", err), nil
+ }
+
+ newMockVolumegroupResourceManager := func() *ResourceManager {
+ return &ResourceManager{
+ configPath: configPath,
+ }
+ }
+ return configPath, nil, newMockVolumegroupResourceManager()
+}
+
+func TestAnalyseDiff(t *testing.T) {
+ mockCtl := gomock.NewController(t)
+ defer mockCtl.Finish()
+ configPath, err, resourceManager := EnsureVolumeGroupEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(configPath)
+ mockPmemer := utils.NewMockPmemer(mockCtl)
+ mockMounter := utils.NewMockMounter(mockCtl)
+ resourceManager.mounter = mockMounter
+ resourceManager.pmemer = mockPmemer
+ setOpInOperatorElement := func(m *model.ResourceYaml) {
+ m.Key = "bar"
+ m.Operator = metav1.LabelSelectorOpIn
+ m.Value = "foo"
+ }
+
+ setPmemTopology := func(m *model.ResourceYaml) {
+ m.Topology = model.Topology{
+ Type: "pmem",
+ Options: "prjquota,shared",
+ Fstype: "ext4",
+ Regions: []string{"region0"},
+ }
+ }
+ setDeviceTopology := func(m *model.ResourceYaml) {
+ m.Topology = model.Topology{
+ Type: "device",
+ Options: "prjquota",
+ Fstype: "ext4",
+ Devices: []string{"/dev/vdc"},
+ }
+ }
+ setDeviceNameElement := func(m *model.ResourceYaml) {
+ m.Name = "/tmp/foo1"
+ }
+ testYamls := QPList{QuotaPaths: []model.ResourceYaml{
+ *makeResourceYamlCustom(setOpInOperatorElement, setPmemTopology),
+ *makeResourceYamlCustom(setDeviceNameElement, setOpInOperatorElement, setDeviceTopology),
+ }}
+
+ d, err := yaml.Marshal(&testYamls)
+ if err != nil {
+ t.Error()
+ }
+ err = ioutil.WriteFile(configPath, d, 0777)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Nil(t, resourceManager.AnalyseConfigMap())
+ assert.Equal(t, 1, len(resourceManager.RegionQuotaPath))
+ assert.Equal(t, 1, len(resourceManager.DeviceQuotaPath))
+ gomock.InOrder(
+ mockMounter.EXPECT().EnsureFolder(
+ gomock.Eq("/tmp/foo1")).Return(nil),
+ mockMounter.EXPECT().FormatAndMount(
+ gomock.Eq("/dev/vdc"), gomock.Eq("/tmp/foo1"), gomock.Eq("ext4"), gomock.Eq([]string{"-O", "project,quota"}), gomock.Eq("prjquota")).Return(nil),
+ mockPmemer.EXPECT().GetPmemNamespaceDeivcePath(
+ gomock.Eq("region0"), gomock.Eq("fsdax")).Return("/dev/pmem0", "", nil),
+ mockMounter.EXPECT().EnsureFolder(
+ gomock.Eq("/tmp/foo")).Return(nil),
+ mockMounter.EXPECT().FormatAndMount(
+ gomock.Eq("/dev/pmem0"), gomock.Eq("/tmp/foo"), gomock.Eq("ext4"), gomock.Eq([]string{"-O", "project,quota"}), gomock.Eq("prjquota,shared")).Return(nil),
+ )
+ assert.Nil(t, resourceManager.ApplyResourceDiff())
+}
diff --git a/pkg/manager/quotapath/type.go b/pkg/manager/quotapath/type.go
new file mode 100644
index 0000000..72c791a
--- /dev/null
+++ b/pkg/manager/quotapath/type.go
@@ -0,0 +1,35 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package quotapath
+
+import (
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+)
+
+// QpConfig ...
+type QpConfig struct {
+ Type string
+ Options string
+ Fstype string
+ Region string
+ Device string
+}
+
+// QPList ...
+type QPList struct {
+ QuotaPaths []model.ResourceYaml `yaml:"quotapath,omitempty"`
+}
diff --git a/pkg/manager/volumegroup/recorder.go b/pkg/manager/volumegroup/recorder.go
new file mode 100644
index 0000000..b2bd271
--- /dev/null
+++ b/pkg/manager/volumegroup/recorder.go
@@ -0,0 +1,17 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package volumegroup
diff --git a/pkg/manager/volumegroup/type.go b/pkg/manager/volumegroup/type.go
new file mode 100644
index 0000000..a3d5dce
--- /dev/null
+++ b/pkg/manager/volumegroup/type.go
@@ -0,0 +1,32 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package volumegroup
+
+import (
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+)
+
+// VgDeviceConfig ...
+type VgDeviceConfig struct {
+ Name string `json:"name,omitempty"`
+ PhysicalVolumes []string `json:"physicalVolumes,omitempty"`
+}
+
+// VgList ...
+type VgList struct {
+ VolumeGroups []model.ResourceYaml `yaml:"volumegroup,omitempty"`
+}
diff --git a/pkg/manager/volumegroup/utils.go b/pkg/manager/volumegroup/utils.go
new file mode 100644
index 0000000..0b212f2
--- /dev/null
+++ b/pkg/manager/volumegroup/utils.go
@@ -0,0 +1,270 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package volumegroup
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs"
+ "github.com/openyurtio/node-resource-manager/pkg/utils"
+ log "github.com/sirupsen/logrus"
+)
+
+const (
+ // MetadataURL is metadata server url
+ MetadataURL = "http://100.100.100.200/latest/meta-data/"
+ // InstanceID is the instance id tag
+ InstanceID = "instance-id"
+ // RegionIDTag is the region id tag
+ RegionIDTag = "region-id"
+ // NsenterCmd is the nsenter command
+ NsenterCmd = "/usr/bin/nsenter --mount=/proc/1/ns/mnt "
+)
+
+const (
+ // ConfigPath the secret mount file
+ ConfigPath = "/var/addon/token-config"
+)
+
+// AKInfo access key info
+type AKInfo struct {
+ // AccessKeyId access key id
+ AccessKeyID string `json:"access.key.id"`
+ // AccessKeySecret access key secret
+ AccessKeySecret string `json:"access.key.secret"`
+ // SecurityToken security token
+ SecurityToken string `json:"security.token"`
+ // Expiration expiration duration
+ Expiration string `json:"expiration"`
+ // Keyring key ring
+ Keyring string `json:"keyring"`
+}
+
+// RoleAuth define STS Token Response
+type RoleAuth struct {
+ AccessKeyID string
+ AccessKeySecret string
+ Expiration time.Time
+ SecurityToken string
+ LastUpdated time.Time
+ Code string
+}
+
+func ListDevice(vgName string) []string {
+ cmd := fmt.Sprintf("%s pvscan | grep \"VG %s\" | awk '{print $2}'", NsenterCmd, vgName)
+ out, err := utils.Run(cmd)
+ if err != nil {
+ devs := make([]string, 0)
+ return devs
+ }
+
+ outLines := strings.Split(out, "\n")
+ deviceList := []string{}
+ for _, line := range outLines {
+ line = strings.TrimSpace(line)
+ if line != "" && !strings.HasPrefix(line, "WARNING") {
+ deviceList = append(deviceList, line)
+ }
+ }
+ return deviceList
+}
+
+func diffDevice(devList1, devList2 []string) bool {
+ if len(devList1) != len(devList2) {
+ return true
+ }
+ for _, dev1 := range devList1 {
+ isSearched := false
+ for _, dev2 := range devList2 {
+ if dev1 == dev2 {
+ isSearched = true
+ break
+ }
+ }
+ if !isSearched {
+ return true
+ }
+ }
+
+ return false
+}
+
+// NewEcsClient create a ecsClient object
+func NewEcsClient(accessKeyID, accessKeySecret, accessToken string) (ecsClient *ecs.Client) {
+ var err error
+ if accessToken == "" {
+ ecsClient, err = ecs.NewClientWithAccessKey("cn-hangzhou", accessKeyID, accessKeySecret)
+ if err != nil {
+ return nil
+ }
+ } else {
+ ecsClient, err = ecs.NewClientWithStsToken("cn-hangzhou", accessKeyID, accessKeySecret, accessToken)
+ if err != nil {
+ return nil
+ }
+ }
+ return
+}
+
+// GetMetaData get host regionid, zoneid
+func GetMetaData(resource string) string {
+ resp, err := http.Get(MetadataURL + resource)
+ if err != nil {
+ return ""
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return ""
+ }
+ return string(body)
+}
+
+// GetDefaultAK read default ak from local file or from STS
+func GetDefaultAK() (string, string, string) {
+ accessKeyID, accessSecret := GetLocalAK()
+
+ accessToken := ""
+ if accessKeyID == "" || accessSecret == "" {
+ accessKeyID, accessSecret, accessToken = GetManagedToken()
+ if accessKeyID != "" {
+ log.Infof("Get AK: use Managed AK")
+ }
+ } else {
+ log.Infof("Get AK: use Local AK")
+ }
+
+ if accessKeyID == "" || accessSecret == "" {
+ accessKeyID, accessSecret, accessToken = GetSTSAK()
+ }
+
+ return accessKeyID, accessSecret, accessToken
+}
+
+// GetLocalAK read ossfs ak from local or from secret file
+func GetLocalAK() (string, string) {
+ accessKeyID, accessSecret := "", ""
+ accessKeyID = os.Getenv("ACCESS_KEY_ID")
+ accessSecret = os.Getenv("ACCESS_KEY_SECRET")
+
+ return strings.TrimSpace(accessKeyID), strings.TrimSpace(accessSecret)
+}
+
+// GetSTSAK get STS AK and token from ecs meta server
+func GetSTSAK() (string, string, string) {
+ roleAuth := RoleAuth{}
+ subpath := "ram/security-credentials/"
+ roleName := GetMetaData(subpath)
+ fullPath := filepath.Join(subpath, roleName)
+ roleInfo := GetMetaData(fullPath)
+
+ err := json.Unmarshal([]byte(roleInfo), &roleAuth)
+ if err != nil {
+ log.Errorf("GetSTSToken: unmarshal roleInfo: %s, with error: %s", roleInfo, err.Error())
+ return "", "", ""
+ }
+ return roleAuth.AccessKeyID, roleAuth.AccessKeySecret, roleAuth.SecurityToken
+}
+
+// GetManagedToken get ak from csi secret
+func GetManagedToken() (string, string, string) {
+ var akInfo AKInfo
+ AccessKeyID, AccessKeySecret, SecurityToken := "", "", ""
+ if _, err := os.Stat(ConfigPath); err == nil {
+ encodeTokenCfg, err := ioutil.ReadFile(ConfigPath)
+ if err != nil {
+ log.Errorf("failed to read token config, err: %v", err)
+ return "", "", ""
+ }
+ err = json.Unmarshal(encodeTokenCfg, &akInfo)
+ if err != nil {
+ log.Errorf("error unmarshal token config: %v", err)
+ return "", "", ""
+ }
+ keyring := akInfo.Keyring
+ ak, err := Decrypt(akInfo.AccessKeyID, []byte(keyring))
+ if err != nil {
+ log.Errorf("failed to decode ak, err: %v", err)
+ return "", "", ""
+ }
+
+ sk, err := Decrypt(akInfo.AccessKeySecret, []byte(keyring))
+ if err != nil {
+ log.Errorf("failed to decode sk, err: %v", err)
+ return "", "", ""
+ }
+
+ token, err := Decrypt(akInfo.SecurityToken, []byte(keyring))
+ if err != nil {
+ log.Errorf("failed to decode token, err: %v", err)
+ return "", "", ""
+ }
+ layout := "2006-01-02T15:04:05Z"
+ t, err := time.Parse(layout, akInfo.Expiration)
+ if err != nil {
+ log.Errorf("Parse expiration error: %s", err.Error())
+ }
+ if t.Before(time.Now()) {
+ log.Errorf("invalid token which is expired, expiration as: %s", akInfo.Expiration)
+ }
+ AccessKeyID = string(ak)
+ AccessKeySecret = string(sk)
+ SecurityToken = string(token)
+ }
+ return AccessKeyID, AccessKeySecret, SecurityToken
+}
+
+// PKCS5UnPadding get pkc
+func PKCS5UnPadding(origData []byte) []byte {
+ length := len(origData)
+ unpadding := int(origData[length-1])
+ return origData[:(length - unpadding)]
+}
+
+// Decrypt secret Decrypt
+func Decrypt(s string, keyring []byte) ([]byte, error) {
+ cdata, err := base64.StdEncoding.DecodeString(s)
+ if err != nil {
+ log.Errorf("failed to decode base64 string, err: %v", err)
+ return nil, err
+ }
+ block, err := aes.NewCipher(keyring)
+ if err != nil {
+ log.Errorf("failed to new cipher, err: %v", err)
+ return nil, err
+ }
+ blockSize := block.BlockSize()
+
+ iv := cdata[:blockSize]
+ blockMode := cipher.NewCBCDecrypter(block, iv)
+ origData := make([]byte, len(cdata)-blockSize)
+
+ blockMode.CryptBlocks(origData, cdata[blockSize:])
+
+ origData = PKCS5UnPadding(origData)
+ return origData, nil
+}
diff --git a/pkg/manager/volumegroup/volumegroup.go b/pkg/manager/volumegroup/volumegroup.go
new file mode 100644
index 0000000..f1d1c6b
--- /dev/null
+++ b/pkg/manager/volumegroup/volumegroup.go
@@ -0,0 +1,432 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package volumegroup
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs"
+ "github.com/openyurtio/node-resource-manager/pkg/config"
+ "github.com/openyurtio/node-resource-manager/pkg/utils"
+ log "github.com/sirupsen/logrus"
+ yaml "gopkg.in/yaml.v2"
+)
+
+const (
+ StorageCmNameSpace = "kube-system"
+ StorageCmName = "cm-node-resource"
+ VGConfigKey = "volumegroup.json"
+ AliyunLocalDisk = "aliyun-local-disk"
+
+ VgConfigFile = "/etc/unified-config/volumegroup"
+ VgTypeDevice = "device"
+ VgTypePvc = "pvc"
+ VgTypeLocal = "alibabacloud-local-disk"
+ VgTypePmem = "pmem"
+)
+
+// ResourceManager ...
+type ResourceManager struct {
+ volumeGroupDeviceMap map[string]*VgDeviceConfig
+ volumeGroupRegionMap map[string][]string
+ pmemer utils.Pmemer
+ lvmer utils.LVM
+ configPath string
+}
+
+// NewResourceManager ...
+func NewResourceManager() *ResourceManager {
+ return &ResourceManager{
+ volumeGroupDeviceMap: make(map[string]*VgDeviceConfig),
+ volumeGroupRegionMap: make(map[string][]string),
+ pmemer: utils.NewNodePmemer(),
+ lvmer: utils.NewNodeLVM(),
+ configPath: "/etc/unified-config/volumegroup",
+ }
+}
+
+// DeviceChars ...
+var DeviceChars = []string{"b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
+
+// Create VolumeGroup
+func (vrm *ResourceManager) createVg(vgName string, desirePvList []string) error {
+ pvListStr := strings.Join(desirePvList, " ")
+ tagList := []string{}
+ out, err := vrm.lvmer.CreateVG(vgName, pvListStr, tagList)
+ if err != nil {
+ log.Errorf("createVg:: Create Vg(%s) error with: %s", vgName, err.Error())
+ return err
+ }
+ log.Infof("createVg:: Successful Create Vg(%s) with out: %s", vgName, out)
+ return nil
+}
+
+// Upgrade VolumeGroup, only support extend pv;
+func (vrm *ResourceManager) updateVg(vgName string, expectPvList, realPvList []string) error {
+ if len(expectPvList) < len(realPvList) {
+ msg := fmt.Sprintf("updateVg:: VolumeGroup: %s, expected pv list should be more than current pv list when update volume group: %v, %v", vgName, expectPvList, realPvList)
+ log.Errorf(msg)
+ return errors.New(msg)
+ }
+
+ // removed pv: pv exist in current node, but not in expect
+ removePv := difference(realPvList, expectPvList)
+ if len(removePv) > 0 {
+ msg := fmt.Sprintf("updateVg:: VolumeGroup: %s, expected pv list should be more than current pv list when update volume group: expect %v, current %v, and not support remove pv now", vgName, expectPvList, realPvList)
+ log.Errorf(msg)
+ return errors.New(msg)
+ }
+
+ // added pv: pv exist in expect, but not in current node.
+ addedPv := difference(expectPvList, realPvList)
+ if len(addedPv) == 0 {
+ msg := fmt.Sprintf("updateVg:: VolumeGroup: %s, expected pv list same with current pv list, expect %v, current %v", vgName, expectPvList, realPvList)
+ log.Errorf(msg)
+ return errors.New(msg)
+ }
+
+ pvListStr := strings.Join(addedPv, " ")
+ _, err := vrm.lvmer.ExtendVG(vgName, pvListStr)
+ if err != nil {
+ msg := fmt.Sprintf("updateVg:: Extend vg(%s) error: %v", vgName, err)
+ log.Errorf(msg)
+ return errors.New(msg)
+ }
+ log.Infof("updateVg:: Successful Add pvs %s to VolumeGroup %s", addedPv, vgName)
+ return nil
+}
+
+// difference returns the elements in `a` that aren't in `b`.
+func difference(a, b []string) []string {
+ mb := make(map[string]struct{}, len(b))
+ for _, x := range b {
+ mb[x] = struct{}{}
+ }
+ var diff []string
+ for _, x := range a {
+ if _, found := mb[x]; !found {
+ diff = append(diff, x)
+ }
+ }
+ return diff
+}
+
+func getPvListForLocalDisk() []string {
+ // Step 2: Get LocalDisk Number
+ localDeviceList := []string{}
+ localDeviceNum, err := getLocalDeviceNum()
+ if err != nil {
+ log.Errorf("getPvListForLocalDisk:: LocalDiskMount:: Get Local Disk Number Error, Error: %s", err.Error())
+ return localDeviceList
+ }
+ if localDeviceNum < 1 {
+ log.Errorf("getPvListForLocalDisk:: VG not exist and also not local disk exist, localDeivceNum: %v", localDeviceNum)
+ return localDeviceList
+ }
+
+ // Step 3: Get LocalDisk device
+ deviceStartWith := "vdb"
+ deviceNamePrefix := "vd"
+ deviceStartIndex := 0
+ deviceNameLen := len(deviceStartWith)
+ if deviceNameLen > 1 {
+ deviceStartChar := deviceStartWith[deviceNameLen-1 : deviceNameLen]
+ for index := 0; index < 15; index++ {
+ if deviceStartChar == DeviceChars[index] {
+ deviceStartIndex = index
+ }
+ }
+ deviceNamePrefix = deviceStartWith[0 : deviceNameLen-1]
+ }
+ for i := deviceStartIndex; i < localDeviceNum; i++ {
+ deviceName := deviceNamePrefix + DeviceChars[i]
+ devicePath := filepath.Join("/dev", deviceName)
+ localDeviceList = append(localDeviceList, devicePath)
+ }
+ //log.Infof("doLocalVolumeMounts, Starting LocalDisk Mount: LocalDisk Number: %d, LocalDisk: %v", localDeviceNum, localDeviceList)
+ return localDeviceList
+}
+
+// Get Local Disk Number from ecs API
+// Requirements: The instance must have role which contains ecs::DescribeInstances, ecs::DescribeInstancesType.
+func getLocalDeviceNum() (int, error) {
+ instanceID := GetMetaData(InstanceID)
+ regionID := GetMetaData(RegionIDTag)
+ localDeviceNum := 0
+ akID, akSecret, token := GetDefaultAK()
+ client := NewEcsClient(akID, akSecret, token)
+
+ // Get Instance Type
+ request := ecs.CreateDescribeInstancesRequest()
+ request.RegionId = regionID
+ request.InstanceIds = "[\"" + instanceID + "\"]"
+ instanceResponse, err := client.DescribeInstances(request)
+ if err != nil {
+ log.Errorf("getLocalDeviceNum:: Describe Instance: %s Error: %s", instanceID, err.Error())
+ return -1, err
+ }
+ if instanceResponse == nil || len(instanceResponse.Instances.Instance) == 0 {
+ log.Infof("getLocalDeviceNum:: Describe Instance Error, with empty response: %s", instanceID)
+ return -1, err
+ }
+
+ // Get Instance LocalDisk Number
+ instanceTypeID := instanceResponse.Instances.Instance[0].InstanceType
+ instanceTypeFamily := instanceResponse.Instances.Instance[0].InstanceTypeFamily
+ instanceTypeRequest := ecs.CreateDescribeInstanceTypesRequest()
+ instanceTypeRequest.InstanceTypeFamily = instanceTypeFamily
+ response, err := client.DescribeInstanceTypes(instanceTypeRequest)
+ if err != nil {
+ log.Errorf("getLocalDeviceNum:: Describe Instance: %s, Type: %s, Family: %s Error: %s", instanceID, instanceTypeID, instanceTypeFamily, err.Error())
+ return -1, err
+ }
+ for _, instance := range response.InstanceTypes.InstanceType {
+ if instance.InstanceTypeId == instanceTypeID {
+ localDeviceNum = instance.LocalStorageAmount
+ log.Infof("getLocalDeviceNum:: Instance: %s, InstanceType: %s, InstanceLocalDiskNum: %d", instanceID, instanceTypeID, localDeviceNum)
+ break
+ }
+ }
+ return localDeviceNum, nil
+}
+
+// Get current VolumeGroup in node
+// echo vgOjbect contains: vgName, pvList;
+func (vrm *ResourceManager) getRealVgList() ([]*VgDeviceConfig, error) {
+ physicalVolumeList, err := vrm.lvmer.ListPhysicalVolume()
+ if err != nil {
+ log.Errorf("List PhysicalVolume get error %v", err)
+ return nil, err
+ }
+ log.Debugf("Real VolumeGroup List: %+v", physicalVolumeList)
+ nodeVgList := []*VgDeviceConfig{}
+ for _, physicalVolume := range physicalVolumeList {
+ isAlreadyAdded := false
+ for _, nodeVg := range nodeVgList {
+ if nodeVg.Name == physicalVolume.VgName {
+ nodeVg.PhysicalVolumes = append(nodeVg.PhysicalVolumes, physicalVolume.Name)
+ isAlreadyAdded = true
+ log.Debugf("getRealVgList:: physicalVolumes: %v, physicalVolumeName: %s, volumeGroupName: %v", nodeVg.PhysicalVolumes, physicalVolume.Name, physicalVolume.VgName)
+ }
+ }
+ if isAlreadyAdded == false {
+ vgPvConfig := &VgDeviceConfig{}
+ vgPvConfig.Name = physicalVolume.VgName
+ vgPvConfig.PhysicalVolumes = append(vgPvConfig.PhysicalVolumes, physicalVolume.Name)
+ nodeVgList = append(nodeVgList, vgPvConfig)
+ log.Debugf("Add New VolumeGroupPvConfig: %s, %s, %v", physicalVolume.Name, physicalVolume.VgName, vgPvConfig)
+ }
+ }
+ return nodeVgList, nil
+}
+
+// AnalyseConfigMap analyse pmem resource config
+func (vrm *ResourceManager) AnalyseConfigMap() error {
+
+ vgDeviceMap := map[string]*VgDeviceConfig{}
+ vgRegionMap := map[string][]string{}
+
+ volumeGroupList := &VgList{}
+ yamlFile, err := ioutil.ReadFile(vrm.configPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ log.Debugf("volume config file %s not exist", vrm.configPath)
+ return nil
+ }
+ log.Errorf("AnalyseConfigMap:: ReadFile: yamlFile.Get error #%v ", err)
+ return err
+ }
+
+ err = yaml.Unmarshal(yamlFile, volumeGroupList)
+ if err != nil {
+ log.Errorf("AnalyseConfigMap:: Unmarshal: parse yaml file error: %v", err)
+ return err
+ }
+
+ nodeInfo := config.GlobalConfigVar.NodeInfo
+ for _, devConfig := range volumeGroupList.VolumeGroups {
+ vgDeviceConfig := &VgDeviceConfig{}
+
+ isMatched := utils.NodeFilter(devConfig.Operator, devConfig.Key, devConfig.Value, nodeInfo)
+ log.Infof("AnalyseConfigMap:: isMatched: %v, devConfig: %+v", isMatched, devConfig)
+
+ if isMatched {
+ switch devConfig.Topology.Type {
+ case VgTypeDevice:
+ vgDeviceConfig.PhysicalVolumes = devConfig.Topology.Devices
+ vgDeviceMap[devConfig.Name] = vgDeviceConfig
+ case VgTypeLocal:
+ tmpConfig := &VgDeviceConfig{}
+ tmpConfig.PhysicalVolumes = getPvListForLocalDisk()
+ vgDeviceMap[devConfig.Name] = tmpConfig
+ case VgTypePvc:
+ // not support yet
+ continue
+ case VgTypePmem:
+ vgRegionMap[devConfig.Name] = devConfig.Topology.Regions
+ default:
+ log.Errorf("AnalyseConfigMap:: Get unsupported volumegroup type: %s", devConfig.Topology.Type)
+ continue
+ }
+ }
+ }
+ vrm.volumeGroupDeviceMap = vgDeviceMap
+ vrm.volumeGroupRegionMap = vgRegionMap
+ return nil
+}
+
+// ApplyResourceDiff apply volume group resource to current node
+func (vrm *ResourceManager) ApplyResourceDiff() error {
+
+ // Get Actual VolumeGroup on node.
+ actualVgConfig, err := vrm.getRealVgList()
+ if err != nil {
+ log.Errorf("ApplyResourceDiff:: Get Node Actual VolumeGroup Error: %s", err.Error())
+ return err
+ }
+ if len(vrm.volumeGroupDeviceMap) > 0 {
+ vrm.applyDeivce(actualVgConfig)
+ }
+ if len(vrm.volumeGroupRegionMap) > 0 {
+ vrm.applyRegion(actualVgConfig)
+ }
+
+ log.Infof("ApplyResourceDiff:: Finish volumegroup loop...")
+ return nil
+}
+
+func (vrm *ResourceManager) applyDeivce(actualVgConfig []*VgDeviceConfig) error {
+ // process each expect volume group
+ for expectVgName, expectVg := range vrm.volumeGroupDeviceMap {
+ log.Infof("applyDevice:: expectName: %s, expectVgDevices: %v", expectVgName, expectVg.PhysicalVolumes)
+ isVgExist := false
+ isVgNeedUpdate := false
+ realPhysicalVolumeList := []string{}
+
+ for _, realVg := range actualVgConfig {
+ if expectVgName == realVg.Name {
+ isVgExist = true
+ diffs := difference(expectVg.PhysicalVolumes, realVg.PhysicalVolumes)
+ if len(diffs) != 0 {
+ realPhysicalVolumeList = realVg.PhysicalVolumes
+ isVgNeedUpdate = true
+ }
+ break
+ }
+ }
+ if !isVgExist {
+ log.Infof("Create VolumeGroup:: %+v, %+v", expectVgName, expectVg.PhysicalVolumes)
+ return vrm.createVg(expectVgName, expectVg.PhysicalVolumes)
+ } else if isVgNeedUpdate {
+ log.Infof("Update VolumeGroup:: %+v, %+v", expectVgName, expectVg.PhysicalVolumes)
+ return vrm.updateVg(expectVgName, expectVg.PhysicalVolumes, realPhysicalVolumeList)
+ }
+ }
+ return nil
+}
+
+func (vrm *ResourceManager) applyRegion(actualVgConfig []*VgDeviceConfig) error {
+ regions, err := vrm.pmemer.GetRegions()
+ if err != nil {
+ log.Errorf("applyRegion: get pmem regions error: %v", err)
+ return err
+ }
+
+ for _, expectRegions := range vrm.volumeGroupRegionMap {
+ expectNamespaces := []string{}
+ for _, expectRegion := range expectRegions {
+ expectRegionExists := false
+ for _, region := range regions.Regions {
+ if expectRegion == region.Dev {
+ expectRegionExists = true
+ if len(region.Namespaces) == 0 {
+ vrm.pmemer.CreateNamespace(region.Dev, "lvm")
+ }
+ }
+ }
+ if !expectRegionExists {
+ err := fmt.Errorf("applyRegion:: expect region %s not exists", expectRegion)
+ return err
+ }
+ expectNamespaces = append(expectNamespaces, utils.ConvertRegion2Namespace(expectRegion))
+ }
+ }
+ updatedRegions, err := vrm.pmemer.GetRegions()
+ if err != nil {
+ log.Errorf("applyRegion: get pmem regions error: %v", err)
+ return err
+ }
+ for expectVgName, expectRegions := range vrm.volumeGroupRegionMap {
+ log.Infof("applyDevice:: expectVgName: %v, expectRegions: %v", expectVgName, expectRegions)
+ expectLvmInUseDevices := []string{}
+ expectLvmNotInUseDevices := []string{}
+ for _, expectRegion := range expectRegions {
+ devicePath := utils.ConvertNamespace2LVMDevicePath(utils.ConvertRegion2Namespace(expectRegion), updatedRegions)
+ if devicePath == "" {
+ log.Errorf("applyRegion:: did not get namespace.Blockdev from expectRegion: %s, regions: %v", expectRegion, updatedRegions)
+ }
+ if vrm.pmemer.CheckNamespaceUsed(devicePath) {
+ log.Warnf("NameSpace heen used region: %v, devicePath: %s", expectRegion, devicePath)
+ expectLvmInUseDevices = append(expectLvmInUseDevices, devicePath)
+ }
+ expectLvmNotInUseDevices = append(expectLvmNotInUseDevices, devicePath)
+ }
+
+ isVgNeedCreate := true
+ for _, actualVg := range actualVgConfig {
+ if expectVgName == actualVg.Name {
+ isVgNeedCreate = false
+ if otherUsage := difference(expectLvmInUseDevices, actualVg.PhysicalVolumes); len(otherUsage) != 0 {
+ log.Errorf("applyRegion:: device [%s] is used in other usage", otherUsage)
+ break
+ }
+ updatePvs := difference(expectLvmNotInUseDevices, actualVg.PhysicalVolumes)
+ if len(updatePvs) == 0 {
+ break
+ }
+ vrm.updatePmemVg(expectVgName, expectLvmNotInUseDevices)
+ }
+ }
+ if isVgNeedCreate {
+ if len(expectLvmInUseDevices) != 0 {
+ log.Errorf("applyRegion:: attempt to use inused devices [%s] to create volumegroup", expectLvmInUseDevices)
+ continue
+ }
+ vrm.createVg(expectVgName, expectLvmNotInUseDevices)
+ }
+ }
+
+ return nil
+}
+
+func (vrm *ResourceManager) updatePmemVg(vgName string, addedPv []string) error {
+
+ pvListStr := strings.Join(addedPv, " ")
+ _, err := vrm.lvmer.ExtendVG(vgName, pvListStr)
+ if err != nil {
+ msg := fmt.Sprintf("updatePmemVg:: Extend vg(%s) error: %v", vgName, err)
+ log.Errorf(msg)
+ return errors.New(msg)
+ }
+ log.Infof("updatePmemVg:: Successful Add pvs %s to VolumeGroup %s", addedPv, vgName)
+ return nil
+}
diff --git a/pkg/manager/volumegroup/volumegroup_test.go b/pkg/manager/volumegroup/volumegroup_test.go
new file mode 100644
index 0000000..737ab54
--- /dev/null
+++ b/pkg/manager/volumegroup/volumegroup_test.go
@@ -0,0 +1,200 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package volumegroup
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/openyurtio/node-resource-manager/pkg/config"
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+ "github.com/openyurtio/node-resource-manager/pkg/utils"
+ "github.com/stretchr/testify/assert"
+ yaml "gopkg.in/yaml.v2"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func makeValidResourceYaml() *model.ResourceYaml {
+ return &model.ResourceYaml{
+ Name: "foo",
+ }
+}
+
+func makeResourceYamlCustom(tweaks ...func(*model.ResourceYaml)) *model.ResourceYaml {
+ resourceYaml := makeValidResourceYaml()
+ for _, fn := range tweaks {
+ fn(resourceYaml)
+ }
+ return resourceYaml
+}
+
+// EnsureVolumeGroupEnv ...
+func EnsureVolumeGroupEnv() (string, error, *ResourceManager) {
+ config.GlobalConfigVar.NodeInfo = &v1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "foo",
+ Labels: map[string]string{"bar": "foo"},
+ },
+ }
+ configPath := "/tmp/volumegroup"
+ err := EnsureFolder(filepath.Dir(configPath))
+ if err != nil {
+ return "", err, nil
+ }
+
+ newMockVolumegroupResourceManager := func() *ResourceManager {
+ return &ResourceManager{
+ configPath: configPath,
+ }
+ }
+ return configPath, nil, newMockVolumegroupResourceManager()
+}
+
+func TestAnalyseConfigMap(t *testing.T) {
+ // set test node info
+ configPath, err, resourceManager := EnsureVolumeGroupEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(configPath)
+ setOpInOperatorElement := func(m *model.ResourceYaml) {
+ m.Key = "bar"
+ m.Operator = metav1.LabelSelectorOpIn
+ m.Value = "foo"
+ }
+ setOpNotInOperatorElement := func(m *model.ResourceYaml) {
+ m.Key = "bar"
+ m.Operator = metav1.LabelSelectorOpNotIn
+ m.Value = "foo"
+ }
+ setOpExistsOperatorElement := func(m *model.ResourceYaml) {
+ m.Key = "bar"
+ m.Operator = metav1.LabelSelectorOpExists
+ m.Value = "foo"
+ }
+ setOpNotExistsOperatorElement := func(m *model.ResourceYaml) {
+ m.Key = "bar"
+ m.Operator = metav1.LabelSelectorOpExists
+ m.Value = "foo"
+ }
+ setRyNameBar1Element := func(m *model.ResourceYaml) {
+ m.Name = "bar1"
+ }
+ setDeviceTopology := func(m *model.ResourceYaml) {
+ m.Topology = model.Topology{
+ Type: "device",
+ Devices: []string{"/dev/vdb", "/dev/vdc"},
+ }
+ }
+ setPmemTopology := func(m *model.ResourceYaml) {
+ m.Topology = model.Topology{
+ Type: "pmem",
+ Regions: []string{"/dev/vdb", "/dev/vdc"},
+ }
+ }
+
+ testYamls := VgList{VolumeGroups: []model.ResourceYaml{
+ *makeResourceYamlCustom(setOpInOperatorElement, setDeviceTopology),
+ *makeResourceYamlCustom(setOpNotInOperatorElement),
+ *makeResourceYamlCustom(setOpNotExistsOperatorElement),
+ *makeResourceYamlCustom(setOpExistsOperatorElement, setRyNameBar1Element, setPmemTopology),
+ }}
+ d, err := yaml.Marshal(&testYamls)
+ defer os.Remove(configPath)
+ if err != nil {
+ t.Error()
+ }
+ ioutil.WriteFile(configPath, d, 0777)
+
+ assert.Nil(t, resourceManager.AnalyseConfigMap())
+ assert.Equal(t, 1, len(resourceManager.volumeGroupDeviceMap))
+ assert.Equal(t, 1, len(resourceManager.volumeGroupRegionMap))
+}
+
+// EnsureFolder ...
+func EnsureFolder(target string) error {
+ mdkirCmd := "mkdir"
+ _, err := exec.LookPath(mdkirCmd)
+ if err != nil {
+ if err == exec.ErrNotFound {
+ return fmt.Errorf("EnsureFolder:: %q executable not found in $PATH", mdkirCmd)
+ }
+ return err
+ }
+
+ mkdirArgs := []string{"-p", target}
+ //log.Infof("mkdir for folder, the command is %s %v", mdkirCmd, mkdirArgs)
+ _, err = exec.Command(mdkirCmd, mkdirArgs...).CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("EnsureFolder:: mkdir for folder error: %v", err)
+ }
+ return nil
+}
+
+func TestAnalyseDiff(t *testing.T) {
+ mockCtl := gomock.NewController(t)
+ defer mockCtl.Finish()
+ configPath, err, resourceManager := EnsureVolumeGroupEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(configPath)
+ mockPmemer := utils.NewMockPmemer(mockCtl)
+ resourceManager.pmemer = mockPmemer
+ mockLVM := utils.NewMockLVM(mockCtl)
+ resourceManager.lvmer = mockLVM
+ setOpInOperatorElement := func(m *model.ResourceYaml) {
+ m.Key = "bar"
+ m.Operator = metav1.LabelSelectorOpIn
+ m.Value = "foo"
+ }
+ setVgDeviceTopology := func(m *model.ResourceYaml) {
+ m.Topology = model.Topology{
+ Type: "device",
+ Devices: []string{"/dev/vdb", "/dev/vdc"},
+ }
+ }
+ setVgDeviceName := func(m *model.ResourceYaml) {
+ m.Name = "volumegroup1"
+ }
+
+ testYamls := VgList{VolumeGroups: []model.ResourceYaml{
+ *makeResourceYamlCustom(setOpInOperatorElement, setVgDeviceTopology, setVgDeviceName),
+ }}
+ d, err := yaml.Marshal(&testYamls)
+ if err != nil {
+ t.Error()
+ }
+ err = ioutil.WriteFile(configPath, d, 0777)
+ if err != nil {
+ t.Fatal(err)
+ }
+ prListStr := strings.Join([]string{"/dev/vdb", "/dev/vdc"}, " ")
+ assert.Nil(t, resourceManager.AnalyseConfigMap())
+ gomock.InOrder(
+ mockLVM.EXPECT().ListPhysicalVolume().Return([]*model.PV{}, nil),
+ mockLVM.EXPECT().CreateVG(gomock.Eq("volumegroup1"), gomock.Eq(prListStr), gomock.Eq([]string{})).Return("", nil),
+ )
+ assert.Nil(t, resourceManager.ApplyResourceDiff())
+}
diff --git a/pkg/model/type.go b/pkg/model/type.go
new file mode 100644
index 0000000..ac38f86
--- /dev/null
+++ b/pkg/model/type.go
@@ -0,0 +1,417 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package model
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// VolumeType is volume type
+type VolumeType byte
+
+const (
+ // separator for vg info
+ separator = "<:SEP:>"
+)
+
+var volumeTypeKeys = []byte("mMoOrRsSpviIlcVtTe")
+
+// types
+const (
+ VolumeTypeMirrored VolumeType = 'm'
+ VolumeTypeMirroredWithoutSync VolumeType = 'M'
+ VolumeTypeOrigin VolumeType = 'o'
+ VolumeTypeOriginWithMergingSnapshot VolumeType = 'O'
+ VolumeTypeRAID VolumeType = 'r'
+ VolumeTypeRAIDWithoutSync VolumeType = 'R'
+ VolumeTypeSnapshot VolumeType = 's'
+ VolumeTypeMergingSnapshot VolumeType = 'S'
+ VolumeTypePVMove VolumeType = 'p'
+ VolumeTypeVirtualMirror VolumeType = 'v'
+ VolumeTypeVirtualRaidImage VolumeType = 'i'
+ VolumeTypeRaidImageOutOfSync VolumeType = 'I'
+ VolumeTypeMirrorLog VolumeType = 'l'
+ VolumeTypeUnderConversion VolumeType = 'c'
+ VolumeTypeThin VolumeType = 'V'
+ VolumeTypeThinPool VolumeType = 't'
+ VolumeTypeThinPoolData VolumeType = 'T'
+ VolumeTypeRaidOrThinPoolMetadata VolumeType = 'e'
+)
+
+// VolumePermissions is volume permissions
+type VolumePermissions rune
+
+var volumePermissonsKeys = []byte("wrR")
+
+// permissions
+const (
+ VolumePermissionsWriteable VolumePermissions = 'w'
+ VolumePermissionsReadOnly VolumePermissions = 'r'
+ VolumePermissionsReadOnlyActivation VolumePermissions = 'R'
+)
+
+// VolumeAllocation is volume allocation policy
+type VolumeAllocation rune
+
+var volumeAllocationKeys = []byte("acilnACILN")
+
+// allocations
+const (
+ VolumeAllocationAnywhere VolumeAllocation = 'a'
+ VolumeAllocationContiguous VolumeAllocation = 'c'
+ VolumeAllocationInherited VolumeAllocation = 'i'
+ VolumeAllocationCling VolumeAllocation = 'l'
+ VolumeAllocationNormal VolumeAllocation = 'n'
+ VolumeAllocationAnywhereLocked VolumeAllocation = 'A'
+ VolumeAllocationContiguousLocked VolumeAllocation = 'C'
+ VolumeAllocationInheritedLocked VolumeAllocation = 'I'
+ VolumeAllocationClingLocked VolumeAllocation = 'L'
+ VolumeAllocationNormalLocked VolumeAllocation = 'N'
+)
+
+// VolumeFixedMinor is volume fixed minor
+type VolumeFixedMinor rune
+
+// fixed minor
+const (
+ VolumeFixedMinorEnabled VolumeFixedMinor = 'm'
+ VolumeFixedMinorDisabled VolumeFixedMinor = '-'
+)
+
+func (t VolumeFixedMinor) toProto() bool {
+ return t == VolumeFixedMinorEnabled
+}
+
+// VolumeState is volume state
+type VolumeState rune
+
+var volumeStateKeys = []byte("asISmMdi")
+
+// states
+const (
+ VolumeStateActive VolumeState = 'a'
+ VolumeStateSuspended VolumeState = 's'
+ VolumeStateInvalidSnapshot VolumeState = 'I'
+ VolumeStateInvalidSuspendedSnapshot VolumeState = 'S'
+ VolumeStateSnapshotMergeFailed VolumeState = 'm'
+ VolumeStateSuspendedSnapshotMergeFailed VolumeState = 'M'
+ VolumeStateMappedDevicePresentWithoutTables VolumeState = 'd'
+ VolumeStateMappedDevicePresentWithInactiveTable VolumeState = 'i'
+)
+
+// VolumeOpen is volume open
+type VolumeOpen rune
+
+// open
+const (
+ VolumeOpenIsOpen VolumeOpen = 'o'
+ VolumeOpenIsNotOpen VolumeOpen = '-'
+)
+
+// VolumeTargetType is volume taget type
+type VolumeTargetType rune
+
+var volumeTargetTypeKeys = []byte("mrstuv")
+
+// target type
+const (
+ VolumeTargetTypeMirror VolumeTargetType = 'm'
+ VolumeTargetTypeRAID VolumeTargetType = 'r'
+ VolumeTargetTypeSnapshot VolumeTargetType = 's'
+ VolumeTargetTypeThin VolumeTargetType = 't'
+ VolumeTargetTypeUnknown VolumeTargetType = 'u'
+ VolumeTargetTypeVirtual VolumeTargetType = 'v'
+)
+
+// VolumeZeroing is volume zeroing
+type VolumeZeroing rune
+
+// zeroing
+const (
+ VolumeZeroingIsZeroing VolumeZeroing = 'z'
+ VolumeZeroingIsNonZeroing VolumeZeroing = '-'
+)
+
+// VolumeHealth is volume health
+type VolumeHealth rune
+
+// health
+const (
+ VolumeHealthOK VolumeHealth = '-'
+ VolumeHealthPartial VolumeHealth = 'p'
+ VolumeHealthRefreshNeeded VolumeHealth = 'r'
+ VolumeHealthMismatchesExist VolumeHealth = 'm'
+ VolumeHealthWritemostly VolumeHealth = 'w'
+)
+
+// VolumeActivationSkipped is volume activation
+type VolumeActivationSkipped rune
+
+// activation
+const (
+ VolumeActivationSkippedIsSkipped VolumeActivationSkipped = 's'
+ VolumeActivationSkippedIsNotSkipped VolumeActivationSkipped = '-'
+)
+
+func (t VolumeActivationSkipped) toProto() bool {
+ return t == VolumeActivationSkippedIsSkipped
+}
+
+// ResourceYaml ...
+type ResourceYaml struct {
+ Name string `yaml:"name,omitempty"`
+ Key string `yaml:"key,omitempty"`
+ Operator metav1.LabelSelectorOperator `yaml:"operator,omitempty"`
+ Value string `yaml:"value,omitempty"`
+ Topology Topology `yaml:"topology,omitempty"`
+}
+
+// Topology ...
+type Topology struct {
+ Type string `yaml:"type,omitempty"`
+ Options string `yaml:"options,omitempty"`
+ Fstype string `yaml:"fstype,omitempty"`
+
+ Devices []string `yaml:"devices,omitempty"`
+ Volumes []map[string]string `yaml:"volumes,omitempty"`
+ Regions []string `yaml:"regions,omitempty"`
+}
+
+// PmemRegions list all regions
+type PmemRegions struct {
+ Regions []PmemRegion `json:"regions"`
+}
+
+// PmemRegion define on pmem region
+type PmemRegion struct {
+ Dev string `json:"dev"`
+ Size int64 `json:"size,omitempty"`
+ AvailableSize int64 `json:"available_size,omitempty"`
+ MaxAvailableExent int64 `json:"max_available_extent,omitempty"`
+ RegionType string `json:"type,omitempty"`
+ IsetID int64 `json:"iset_id,omitempty"`
+ PersistenceDomain string `json:"persistence_domain,omitempty"`
+ Namespaces []PmemNameSpace `json:"namespaces,omitempty"`
+}
+
+// PmemNameSpace define one pmem namespaces
+type PmemNameSpace struct {
+ Dev string `json:"dev,omitempty"`
+ Mode string `json:"mode,omitempty"`
+ MapType string `json:"map,omitempty"`
+ Size int64 `json:"size,omitempty"`
+ UUID string `json:"uuid,omitempty"`
+ SectorSize int64 `json:"sectorsize,omitempty"`
+ Align int64 `json:"align,omitempty"`
+ BlockDev string `json:"blockdev,omitempty"`
+ CharDev string `json:"chardev,omitempty"`
+ Name string `json:"name,omitempty"`
+}
+
+// DaxctrlMem list all mems
+type DaxctrlMem struct {
+ Chardev string `json:"chardev"`
+ Size int64 `json:"size"`
+ TargetNode int `json:"target_node"`
+ Mode string `json:"mode"`
+ Movable bool `json:"movable"`
+}
+
+// LV is a logical volume
+type LV struct {
+ Name string
+ Size uint64
+ UUID string
+ Attributes LVAttributes
+ CopyPercent string
+ ActualDevMajNumber uint32
+ ActualDevMinNumber uint32
+ Tags []string
+}
+
+// VG is volume group
+type VG struct {
+ Name string
+ Size uint64
+ FreeSize uint64
+ UUID string
+ Tags []string
+}
+
+// PV is Physica lVolume
+type PV struct {
+ Name string
+ VgName string
+ Size uint64
+ UUID string
+}
+
+// LVAttributes is attributes
+type LVAttributes struct {
+ Type VolumeType
+ Permissions VolumePermissions
+ Allocation VolumeAllocation
+ FixedMinor VolumeFixedMinor
+ State VolumeState
+ Open VolumeOpen
+ TargetType VolumeTargetType
+ Zeroing VolumeZeroing
+ Health VolumeHealth
+ ActivationSkipped VolumeActivationSkipped
+}
+
+// ParseLV ...
+func ParseLV(line string) (*LV, error) {
+ // lvs --units=b --separator="<:SEP:>" --nosuffix --noheadings -o lv_name,lv_size,lv_uuid,lv_attr,copy_percent,lv_kernel_major,lv_kernel_minor,lv_tags --nameprefixes -a
+ // todo: devices, lv_ancestors, lv_descendants, lv_major, lv_minor, mirror_log, modules, move_pv, origin, region_size
+ // seg_count, seg_size, seg_start, seg_tags, segtype, snap_percent, stripes, stripe_size
+ fields, err := parse(line, 8)
+ if err != nil {
+ return nil, err
+ }
+
+ size, err := strconv.ParseUint(fields["LVM2_LV_SIZE"], 10, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ kernelMajNumber, err := strconv.ParseUint(fields["LVM2_LV_KERNEL_MAJOR"], 10, 32)
+ if err != nil {
+ return nil, err
+ }
+
+ kernelMinNumber, err := strconv.ParseUint(fields["LVM2_LV_KERNEL_MINOR"], 10, 32)
+ if err != nil {
+ return nil, err
+ }
+
+ attrs, err := parseAttrs(fields["LVM2_LV_ATTR"])
+ if err != nil {
+ return nil, err
+ }
+
+ return &LV{
+ Name: fields["LVM2_LV_NAME"],
+ Size: size,
+ UUID: fields["LVM2_LV_UUID"],
+ Attributes: *attrs,
+ CopyPercent: fields["LVM2_COPY_PERCENT"],
+ ActualDevMajNumber: uint32(kernelMajNumber),
+ ActualDevMinNumber: uint32(kernelMinNumber),
+ Tags: strings.Split(fields["LVM2_LV_TAGS"], ","),
+ }, nil
+
+}
+
+// ParseVG parse volume group
+func ParseVG(line string) (*VG, error) {
+ // vgs --units=b --separator="<:SEP:>" --nosuffix --noheadings -o vg_name,vg_size,vg_free,vg_uuid,vg_tags --nameprefixes -a
+ fields, err := parse(line, 5)
+ if err != nil {
+ return nil, err
+ }
+
+ size, err := strconv.ParseUint(fields["LVM2_VG_SIZE"], 10, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ freeSize, err := strconv.ParseUint(fields["LVM2_VG_FREE"], 10, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ return &VG{
+ Name: fields["LVM2_VG_NAME"],
+ Size: size,
+ FreeSize: freeSize,
+ UUID: fields["LVM2_VG_UUID"],
+ Tags: strings.Split(fields["LVM2_VG_TAGS"], ","),
+ }, nil
+}
+
+// ParsePV parse volume group
+func ParsePV(line string) (*PV, error) {
+ // vgs --units=b --separator="<:SEP:>" --nosuffix --noheadings -o vg_name,vg_size,vg_free,vg_uuid,vg_tags --nameprefixes -a
+ fields, err := parse(line, 4)
+ if err != nil {
+ return nil, err
+ }
+
+ size, err := strconv.ParseUint(fields["LVM2_PV_SIZE"], 10, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ return &PV{
+ Name: fields["LVM2_PV_NAME"],
+ VgName: fields["LVM2_VG_NAME"],
+ Size: size,
+ UUID: fields["LVM2_PV_UUID"],
+ }, nil
+}
+
+func parse(line string, numComponents int) (map[string]string, error) {
+ components := strings.Split(line, separator)
+ if len(components) != numComponents {
+ return nil, fmt.Errorf("expected %d components, got %d", numComponents, len(components))
+ }
+
+ fields := map[string]string{}
+ for _, c := range components {
+ idx := strings.Index(c, "=")
+ if idx == -1 {
+ return nil, fmt.Errorf("failed to parse component '%s'", c)
+ }
+ key := c[0:idx]
+ value := c[idx+1:]
+ if len(value) < 2 {
+ return nil, fmt.Errorf("failed to parse component '%s'", c)
+ }
+ if value[0] != '\'' || value[len(value)-1] != '\'' {
+ return nil, fmt.Errorf("failed to parse component '%s'", c)
+ }
+ value = value[1 : len(value)-1]
+ fields[key] = value
+ }
+
+ return fields, nil
+}
+
+func parseAttrs(attrs string) (*LVAttributes, error) {
+ if len(attrs) != 10 {
+ return nil, fmt.Errorf("incorrect attrs block size, expected 10, got %d in %s", len(attrs), attrs)
+ }
+
+ ret := &LVAttributes{}
+ ret.Type = VolumeType(attrs[0])
+ ret.Permissions = VolumePermissions(attrs[1])
+ ret.Allocation = VolumeAllocation(attrs[2])
+ ret.FixedMinor = VolumeFixedMinor(attrs[3])
+ ret.State = VolumeState(attrs[4])
+ ret.Open = VolumeOpen(attrs[5])
+ ret.TargetType = VolumeTargetType(attrs[6])
+ ret.Zeroing = VolumeZeroing(attrs[7])
+ ret.Health = VolumeHealth(attrs[8])
+ ret.ActivationSkipped = VolumeActivationSkipped(attrs[9])
+
+ return ret, nil
+}
diff --git a/pkg/signals/signal_posix.go b/pkg/signals/signal_posix.go
new file mode 100644
index 0000000..1bafca8
--- /dev/null
+++ b/pkg/signals/signal_posix.go
@@ -0,0 +1,25 @@
+// +build !windows
+
+/*
+Copyright 2021 The OpenYurt Authors.
+Copyright 2018 The Knative Authors.
+
+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.
+*/
+
+package signals
+
+import (
+ "os"
+ "syscall"
+)
+
+var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM}
diff --git a/pkg/signals/signals.go b/pkg/signals/signals.go
new file mode 100644
index 0000000..3e52837
--- /dev/null
+++ b/pkg/signals/signals.go
@@ -0,0 +1,82 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+Copyright 2018 The Knative Authors.
+
+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.
+*/
+
+package signals
+
+import (
+ "context"
+ "errors"
+ "os"
+ "os/signal"
+ "time"
+)
+
+var onlyOneSignalHandler = make(chan struct{})
+
+// SetupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned
+// which is closed on one of these signals. If a second signal is caught, the program
+// is terminated with exit code 1.
+func SetupSignalHandler() (stopCh <-chan struct{}) {
+ close(onlyOneSignalHandler) // panics when called twice
+
+ stop := make(chan struct{})
+ c := make(chan os.Signal, 2)
+ signal.Notify(c, shutdownSignals...)
+ go func() {
+ <-c
+ close(stop)
+ <-c
+ os.Exit(1) // second signal. Exit directly.
+ }()
+
+ return stop
+}
+
+// NewContext creates a new context with SetupSignalHandler()
+// as our Done() channel.
+func NewContext() context.Context {
+ return &signalContext{stopCh: SetupSignalHandler()}
+}
+
+type signalContext struct {
+ stopCh <-chan struct{}
+}
+
+// Deadline implements context.Context
+func (scc *signalContext) Deadline() (deadline time.Time, ok bool) {
+ return
+}
+
+// Done implements context.Context
+func (scc *signalContext) Done() <-chan struct{} {
+ return scc.stopCh
+}
+
+// Err implements context.Context
+func (scc *signalContext) Err() error {
+ select {
+ case _, ok := <-scc.Done():
+ if !ok {
+ return errors.New("received a termination signal")
+ }
+ default:
+ }
+ return nil
+}
+
+// Value implements context.Context
+func (scc *signalContext) Value(key interface{}) interface{} {
+ return nil
+}
diff --git a/pkg/utils/lvm.go b/pkg/utils/lvm.go
new file mode 100644
index 0000000..72aa83c
--- /dev/null
+++ b/pkg/utils/lvm.go
@@ -0,0 +1,297 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package utils
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+)
+
+const (
+ ProtectedTagName = "protected"
+)
+
+// LVM ...
+type LVM interface {
+ ListLV(listSpec string) ([]*model.LV, error)
+ CreateLV(vg, name string, size uint64, mirrors uint32, tags []string) (string, error)
+ RemoveLV(vg, name string) (string, error)
+ CloneLV(src, dest string) (string, error)
+ ListVG() ([]*model.VG, error)
+ ListPhysicalVolume() ([]*model.PV, error)
+ CreateVG(name, physicalVolume string, tags []string) (string, error)
+ ExtendVG(name, physicalVolume string) (string, error)
+ RemoveVG(name string) (string, error)
+ AddTagLV(vg, name string, tags []string) (string, error)
+ RemoveTagLV(vg, name string, tags []string) (string, error)
+}
+
+// NodeLVM ...
+type NodeLVM struct {
+}
+
+// NewNodeLVM ...
+func NewNodeLVM() *NodeLVM {
+ return &NodeLVM{}
+}
+
+// ListLV ...
+func (nl *NodeLVM) ListLV(listSpec string) ([]*model.LV, error) {
+ cmdList := []string{NsenterCmd, "lvs", "--units=b", "--separator=\"<:SEP:>\"", "--nosuffix", "--noheadings",
+ "-o", "lv_name,lv_size,lv_uuid,lv_attr,copy_percent,lv_kernel_major,lv_kernel_minor,lv_tags", "--nameprefixes", "-a", listSpec}
+ cmd := strings.Join(cmdList, " ")
+ out, err := Run(cmd)
+ if err != nil {
+ return nil, err
+ }
+ outStr := strings.TrimSpace(string(out))
+ if outStr == "" {
+ lvs := make([]*model.LV, 0)
+ return lvs, nil
+ }
+ outLines := strings.Split(outStr, "\n")
+ lvs := []*model.LV{}
+ for _, line := range outLines {
+ line = strings.TrimSpace(line)
+ if !strings.Contains(line, "LVM2_LV_NAME") {
+ continue
+ }
+ lv, err := model.ParseLV(line)
+ if err != nil {
+ return nil, errors.New("Parse LVM: " + line + ", with error: " + err.Error())
+ }
+ lvs = append(lvs, lv)
+ }
+ return lvs, nil
+}
+
+// CreateLV ...
+func (nl *NodeLVM) CreateLV(vg, name string, size uint64, mirrors uint32, tags []string) (string, error) {
+ if size == 0 {
+ return "", errors.New("size must be greater than 0")
+ }
+
+ args := []string{"lvcreate", "-v", "-n", name, "-L", fmt.Sprintf("%db", size)}
+ if mirrors > 0 {
+ args = append(args, "-m", fmt.Sprintf("%d", mirrors), "--nosync")
+ }
+ for _, tag := range tags {
+ args = append(args, "--add-tag", tag)
+ }
+
+ args = append(args, vg)
+ cmd := strings.Join(args, " ")
+ out, err := Run(cmd)
+ return string(out), err
+}
+
+// RemoveLV ...
+func (nl *NodeLVM) RemoveLV(vg, name string) (string, error) {
+ lvs, err := nl.ListLV(fmt.Sprintf("%s/%s", vg, name))
+ if err != nil {
+ return "", fmt.Errorf("failed to list LVs: %v", err)
+ }
+ if len(lvs) == 0 {
+ return "lvm " + vg + "/" + name + " is not exist, skip remove", nil
+ }
+ if len(lvs) != 1 {
+ return "", fmt.Errorf("expected 1 LV, got %d", len(lvs))
+ }
+ for _, tag := range lvs[0].Tags {
+ if tag == ProtectedTagName {
+ return "", errors.New("volume is protected")
+ }
+ }
+
+ args := []string{NsenterCmd, "lvremove", "-v", "-f", fmt.Sprintf("%s/%s", vg, name)}
+ cmd := strings.Join(args, " ")
+ out, err := Run(cmd)
+
+ return string(out), err
+
+}
+
+// CloneLV ...
+func (nl *NodeLVM) CloneLV(src, dest string) (string, error) {
+ args := []string{NsenterCmd, "dd", fmt.Sprintf("if=%s", src), fmt.Sprintf("of=%s", dest), "bs=4M"}
+ cmd := strings.Join(args, " ")
+ out, err := Run(cmd)
+
+ return string(out), err
+}
+
+// ListVG ...
+func (nl *NodeLVM) ListVG() ([]*model.VG, error) {
+ args := []string{NsenterCmd, "vgs", "--units=b", "--separator=\"<:SEP:>\"", "--nosuffix", "--noheadings",
+ "-o", "vg_name,vg_size,vg_free,vg_uuid,vg_tags", "--nameprefixes", "-a"}
+ cmd := strings.Join(args, " ")
+ out, err := Run(cmd)
+ if err != nil {
+ return nil, err
+ }
+ vgs := []*model.VG{}
+ outStr := strings.TrimSpace(string(out))
+ if outStr == "" {
+ return vgs, nil
+ }
+ outLines := strings.Split(outStr, "\n")
+ for _, line := range outLines {
+ line = strings.TrimSpace(line)
+ if strings.HasPrefix(line, "WARNING") {
+ continue
+ }
+ vg, err := model.ParseVG(line)
+ if err != nil {
+ return nil, err
+ }
+ vgs = append(vgs, vg)
+ }
+ return vgs, nil
+}
+
+// ListPhysicalVolume ...
+func (nl *NodeLVM) ListPhysicalVolume() ([]*model.PV, error) {
+ args := []string{NsenterCmd, "pvs", "--units=b", "--separator=\"<:SEP:>\"", "--nosuffix", "--noheadings",
+ "-o", "vg_name,pv_name,pv_size,pv_uuid", "--nameprefixes", "-a"}
+ cmd := strings.Join(args, " ")
+ out, err := Run(cmd)
+ if err != nil {
+ return nil, err
+ }
+ outStr := strings.TrimSpace(string(out))
+ pvs := []*model.PV{}
+ if outStr == "" {
+ return pvs, nil
+ }
+ outLines := strings.Split(outStr, "\n")
+ for _, line := range outLines {
+ line = strings.TrimSpace(line)
+ if strings.HasPrefix(line, "WARNING") {
+ continue
+ }
+ pv, err := model.ParsePV(line)
+ if err != nil {
+ return nil, err
+ }
+ if pv.VgName != "" && pv.Name != "" {
+ pvs = append(pvs, pv)
+ }
+ }
+ return pvs, nil
+}
+
+// CreateVG ...
+func (nl *NodeLVM) CreateVG(name, physicalVolume string, tags []string) (string, error) {
+ args := []string{NsenterCmd, "vgcreate", name, physicalVolume, "-v"}
+ for _, tag := range tags {
+ args = append(args, "--add-tag", tag)
+ }
+ cmd := strings.Join(args, " ")
+ out, err := Run(cmd)
+
+ return string(out), err
+}
+
+// ExtendVG ...
+func (nl *NodeLVM) ExtendVG(name, physicalVolume string) (string, error) {
+ args := []string{NsenterCmd, "vgextend", name, physicalVolume, "-v"}
+ cmd := strings.Join(args, " ")
+ out, err := Run(cmd)
+
+ return string(out), err
+}
+
+// RemoveVG ...
+func (nl *NodeLVM) RemoveVG(name string) (string, error) {
+ vgs, err := nl.ListVG()
+ if err != nil {
+ return "", fmt.Errorf("failed to list VGs: %v", err)
+ }
+ var vg *model.VG
+ for _, v := range vgs {
+ if v.Name == name {
+ vg = v
+ break
+ }
+ }
+ if vg == nil {
+ return "", fmt.Errorf("could not find vg to delete")
+ }
+ for _, tag := range vg.Tags {
+ if tag == ProtectedTagName {
+ return "", errors.New("volume is protected")
+ }
+ }
+
+ args := []string{NsenterCmd, "vgremove", "-v", "-f", name}
+ cmd := strings.Join(args, " ")
+ out, err := Run(cmd)
+
+ return string(out), err
+
+}
+
+// AddTagLV ...
+func (nl *NodeLVM) AddTagLV(vg, name string, tags []string) (string, error) {
+ lvs, err := nl.ListLV(fmt.Sprintf("%s/%s", vg, name))
+ if err != nil {
+ return "", fmt.Errorf("failed to list LVs: %v", err)
+ }
+ if len(lvs) != 1 {
+ return "", fmt.Errorf("expected 1 LV, got %d", len(lvs))
+ }
+
+ args := make([]string, 0)
+ args = append(args, NsenterCmd)
+ args = append(args, "lvchange")
+ for _, tag := range tags {
+ args = append(args, "--addtag", tag)
+ }
+
+ args = append(args, fmt.Sprintf("%s/%s", vg, name))
+ cmd := strings.Join(args, " ")
+ out, err := Run(cmd)
+
+ return string(out), err
+}
+
+// RemoveTagLV ....
+func (nl *NodeLVM) RemoveTagLV(vg, name string, tags []string) (string, error) {
+
+ lvs, err := nl.ListLV(fmt.Sprintf("%s/%s", vg, name))
+ if err != nil {
+ return "", fmt.Errorf("failed to list LVs: %v", err)
+ }
+ if len(lvs) != 1 {
+ return "", fmt.Errorf("expected 1 LV, got %d", len(lvs))
+ }
+
+ args := make([]string, 0)
+ args = append(args, NsenterCmd)
+ args = append(args, "lvchange")
+ for _, tag := range tags {
+ args = append(args, "--deltag", tag)
+ }
+
+ args = append(args, fmt.Sprintf("%s/%s", vg, name))
+ cmd := strings.Join(args, " ")
+ out, err := Run(cmd)
+ return string(out), err
+}
diff --git a/pkg/utils/lvm_mock.go b/pkg/utils/lvm_mock.go
new file mode 100644
index 0000000..03b1aba
--- /dev/null
+++ b/pkg/utils/lvm_mock.go
@@ -0,0 +1,214 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package utils
+
+import (
+ "reflect"
+
+ "github.com/golang/mock/gomock"
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+)
+
+// MockLVM ...
+type MockLVM struct {
+ ctrl *gomock.Controller
+ recorder *MockLVMMockRecorder
+}
+
+// MockLVMMockRecorder ...
+type MockLVMMockRecorder struct {
+ mock *MockLVM
+}
+
+// EXPECT ...
+func (m *MockLVM) EXPECT() *MockLVMMockRecorder {
+ return m.recorder
+}
+
+// NewMockLVM ...
+func NewMockLVM(ctrl *gomock.Controller) *MockLVM {
+ mock := &MockLVM{ctrl: ctrl}
+ mock.recorder = &MockLVMMockRecorder{mock}
+ return mock
+}
+
+// ListLV ...
+func (m *MockLVM) ListLV(listSpec string) ([]*model.LV, error) {
+
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListLV", listSpec)
+ ret0, _ := ret[0].([]*model.LV)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListLV ...
+func (mr *MockLVMMockRecorder) ListLV(arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLV", reflect.TypeOf((*MockLVM)(nil).ListLV), arg1)
+}
+
+// CreateLV ...
+func (m *MockLVM) CreateLV(vg, name string, size uint64, mirrors uint32, tags []string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateLV", vg, name, size, mirrors, tags)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreateLV ...
+func (mr *MockLVMMockRecorder) CreateLV(arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLV", reflect.TypeOf((*MockLVM)(nil).CreateLV), arg1, arg2, arg3, arg4, arg5)
+}
+
+// RemoveLV ...
+func (m *MockLVM) RemoveLV(vg, name string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RemoveLV", vg, name)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// RemoveLV ...
+func (mr *MockLVMMockRecorder) RemoveLV(arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveLV", reflect.TypeOf((*MockLVM)(nil).RemoveLV), arg1, arg2)
+}
+
+// CloneLV ...
+func (m *MockLVM) CloneLV(src, dest string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CloneLV", src, dest)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CloneLV ...
+func (mr *MockLVMMockRecorder) CloneLV(arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloneLV", reflect.TypeOf((*MockLVM)(nil).CloneLV), arg1, arg2)
+}
+
+// ListVG ...
+func (m *MockLVM) ListVG() ([]*model.VG, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListVG")
+ ret0, _ := ret[0].([]*model.VG)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListVG ...
+func (mr *MockLVMMockRecorder) ListVG() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListVG", reflect.TypeOf((*MockLVM)(nil).ListVG))
+}
+
+// ListPhysicalVolume ...
+func (m *MockLVM) ListPhysicalVolume() ([]*model.PV, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListPhysicalVolume")
+ ret0, _ := ret[0].([]*model.PV)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPhysicalVolume ...
+func (mr *MockLVMMockRecorder) ListPhysicalVolume() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPhysicalVolume", reflect.TypeOf((*MockLVM)(nil).ListPhysicalVolume))
+}
+
+// CreateVG ...
+func (m *MockLVM) CreateVG(name, physicalVolume string, tags []string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateVG", name, physicalVolume, tags)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+
+}
+
+// CreateVG ...
+func (mr *MockLVMMockRecorder) CreateVG(arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVG", reflect.TypeOf((*MockLVM)(nil).CreateVG), arg1, arg2, arg3)
+}
+
+// ExtendVG ...
+func (m *MockLVM) ExtendVG(name, physicalVolume string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ExtendVG", name, physicalVolume)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ExtendVG ...
+func (mr *MockLVMMockRecorder) ExtendVG(arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtendVG", reflect.TypeOf((*MockLVM)(nil).ExtendVG), arg1, arg2)
+}
+
+// RemoveVG ...
+func (m *MockLVM) RemoveVG(name string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ExtendVG", name)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// RemoveVG ...
+func (mr *MockLVMMockRecorder) RemoveVG(arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveVG", reflect.TypeOf((*MockLVM)(nil).RemoveVG), arg1)
+}
+
+// AddTagLV ...
+func (m *MockLVM) AddTagLV(vg, name string, tags []string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddTagLV", vg, name, tags)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// AddTagLV ...
+func (mr *MockLVMMockRecorder) AddTagLV(arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTagLV", reflect.TypeOf((*MockLVM)(nil).AddTagLV), arg1, arg2, arg3)
+}
+
+// RemoveTagLV ...
+func (m *MockLVM) RemoveTagLV(vg, name string, tags []string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RemoveTagLV", vg, name, tags)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// RemoveTagLV ...
+func (mr *MockLVMMockRecorder) RemoveTagLV(arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTagLV", reflect.TypeOf((*MockLVM)(nil).RemoveTagLV), arg1, arg2, arg3)
+}
diff --git a/pkg/utils/mounter.go b/pkg/utils/mounter.go
new file mode 100644
index 0000000..56f2110
--- /dev/null
+++ b/pkg/utils/mounter.go
@@ -0,0 +1,263 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package utils
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "strings"
+
+ log "github.com/sirupsen/logrus"
+ utilexec "k8s.io/utils/exec"
+ k8smount "k8s.io/utils/mount"
+)
+
+const (
+ // fsckErrorsCorrected tag
+ fsckErrorsCorrected = 1
+ // fsckErrorsUncorrected tag
+ fsckErrorsUncorrected = 4
+)
+
+type findmntResponse struct {
+ FileSystems []fileSystem `json:"filesystems"`
+}
+
+type fileSystem struct {
+ Target string `json:"target"`
+ Propagation string `json:"propagation"`
+ FsType string `json:"fstype"`
+ Options string `json:"options"`
+}
+
+// Mounter is responsible for formatting and mounting volumes
+type Mounter interface {
+ k8smount.Interface
+ utilexec.Interface
+ // If the folder doesn't exist, it will call 'mkdir -p'
+ EnsureFolder(string) error
+ // FormatAndMount ...
+ FormatAndMount(string, string, string, []string, string) error
+
+ // IsMounted checks whether the target path is a correct mount (i.e:
+ // propagated). It returns true if it's mounted. An error is returned in
+ // case of system errors or if it's mounted incorrectly.
+ IsMounted(target string) (bool, error)
+
+ SafePathRemove(target string) error
+}
+
+// TODO(arslan): this is Linux only for now. Refactor this into a package with
+// architecture specific code in the future, such as mounter_darwin.go,
+// mounter_linux.go, etc..
+type NodeMounter struct {
+ k8smount.SafeFormatAndMount
+ utilexec.Interface
+}
+
+// NewMounter returns a new mounter instance
+func NewMounter() Mounter {
+ return &NodeMounter{
+ k8smount.SafeFormatAndMount{
+ Interface: k8smount.New(""),
+ Exec: utilexec.New(),
+ },
+ utilexec.New(),
+ }
+}
+
+// EnsureFolder ...
+func (m *NodeMounter) EnsureFolder(target string) error {
+ mkdirCmd := "mkdir"
+ _, err := m.LookPath(mkdirCmd)
+ if err != nil {
+ if err == exec.ErrNotFound {
+ return fmt.Errorf("%q executable not found in $PATH", mkdirCmd)
+ }
+ return err
+ }
+
+ mkdirCmd = NsenterCmd + mkdirCmd
+ mkdirCmd += fmt.Sprintf(" -p %s", target)
+ log.Infof("mkdir for folder, the command is %s", mkdirCmd)
+ output, err := Run(mkdirCmd)
+ if err != nil {
+ return fmt.Errorf("EnsureFolder:: mkdir for folder output: %s error: %v", output, err)
+ }
+ return nil
+}
+
+// FormatAndMount ...
+func (m *NodeMounter) FormatAndMount(source, target, fstype string, mkfsOptions []string, mountOptions string) error {
+ // diskMounter.Interface = m.K8smounter
+ readOnly := false
+
+ if !readOnly {
+ // Run fsck on the disk to fix repairable issues, only do this for volumes requested as rw.
+ args := []string{"-a", source}
+
+ out, err := m.Exec.Command("fsck", args...).CombinedOutput()
+ if err != nil {
+ ee, isExitError := err.(utilexec.ExitError)
+ switch {
+ case err == utilexec.ErrExecutableNotFound:
+ log.Warningf("'fsck' not found on system; continuing mount without running 'fsck'.")
+ case isExitError && ee.ExitStatus() == fsckErrorsCorrected:
+ log.Infof("Device %s has errors which were corrected by fsck.", source)
+ case isExitError && ee.ExitStatus() == fsckErrorsUncorrected:
+ return fmt.Errorf("'fsck' found errors on device %s but could not correct them: %s", source, string(out))
+ case isExitError && ee.ExitStatus() > fsckErrorsUncorrected:
+ }
+ }
+ }
+
+ // Try to mount the disk
+ cmd := fmt.Sprintf("%smount -o %s %s %s", NsenterCmd, mountOptions, source, target)
+ log.Infof("FormatAndMount:: cmd: %s", cmd)
+ output, mountErr := Run(cmd)
+ if mountErr != nil {
+ // Mount failed. This indicates either that the disk is unformatted or
+ // it contains an unexpected filesystem.
+ existingFormat, err := m.GetDiskFormat(source)
+
+ if err != nil {
+ return err
+ }
+ if existingFormat == "" {
+ if readOnly {
+ // Don't attempt to format if mounting as readonly, return an error to reflect this.
+ return errors.New("failed to mount unformatted volume as read only")
+ }
+
+ // Disk is unformatted so format it.
+ args := []string{source}
+ // Use 'ext4' as the default
+ if len(fstype) == 0 {
+ fstype = "ext4"
+ }
+
+ if fstype == "ext4" || fstype == "ext3" {
+ args = []string{
+ "-F", // Force flag
+ "-m0", // Zero blocks reserved for super-user
+ source,
+ }
+ // add mkfs options
+ if len(mkfsOptions) != 0 {
+ args = []string{}
+ for _, opts := range mkfsOptions {
+ args = append(args, opts)
+ }
+ args = append(args, source)
+ }
+ }
+ log.Infof("Disk %q appears to be unformatted, attempting to format as type: %q with options: %v", source, fstype, args)
+
+ mkfsCmd := fmt.Sprintf("%s mkfs.%s %s", NsenterCmd, fstype, strings.Join(args, " "))
+ log.Infof("FormatAndMount:: mkfscmd: %s", mkfsCmd)
+ _, err = Run(mkfsCmd)
+ if err == nil {
+ // the disk has been formatted successfully try to mount it again.
+ output, mountErr := Run(cmd)
+ log.Infof("FormatAndMount:: cmd output %s", output)
+ return mountErr
+ }
+ log.Errorf("format of disk %q failed: type:(%q) target:(%q) options:(%q) output: (%s) error:(%v)", source, fstype, target, mkfsOptions, output, err)
+ return err
+ }
+ // Disk is already formatted and failed to mount
+ if len(fstype) == 0 || fstype == existingFormat {
+ // This is mount error
+ return mountErr
+ }
+ // Block device is formatted with unexpected filesystem, let the user know
+ return fmt.Errorf("failed to mount the volume as %q, it already contains %s. Mount error: %v", fstype, existingFormat, mountErr)
+ }
+
+ return mountErr
+}
+
+// IsMounted ...
+func (m *NodeMounter) IsMounted(target string) (bool, error) {
+ if target == "" {
+ return false, errors.New("target is not specified for checking the mount")
+ }
+ findmntCmd := "grep"
+ findmntArgs := []string{target, "/proc/mounts"}
+ out, err := exec.Command(findmntCmd, findmntArgs...).CombinedOutput()
+ outStr := strings.TrimSpace(string(out))
+ if err != nil {
+ if outStr == "" {
+ return false, nil
+ }
+ return false, fmt.Errorf("checking mounted failed: %v cmd: %q output: %q",
+ err, findmntCmd, outStr)
+ }
+ if strings.Contains(outStr, target) {
+ return true, nil
+ }
+ return false, nil
+}
+
+// SafePathRemove ...
+func (m *NodeMounter) SafePathRemove(targetPath string) error {
+ fo, err := os.Lstat(targetPath)
+ if err != nil {
+ return err
+ }
+ isMounted, err := m.IsMounted(targetPath)
+ if err != nil {
+ return err
+ }
+ if isMounted {
+ return errors.New("Path is mounted, not remove: " + targetPath)
+ }
+ if fo.IsDir() {
+ empty, err := IsDirEmpty(targetPath)
+ if err != nil {
+ return errors.New("Check path empty error: " + targetPath + err.Error())
+ }
+ if !empty {
+ return errors.New("Cannot remove Path not empty: " + targetPath)
+ }
+ }
+ err = os.Remove(targetPath)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// IsDirEmpty return status of dir empty or not
+func IsDirEmpty(name string) (bool, error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return false, err
+ }
+ defer f.Close()
+
+ // read in ONLY one file
+ _, err = f.Readdir(1)
+ // and if the file is EOF... well, the dir is empty.
+ if err == io.EOF {
+ return true, nil
+ }
+ return false, err
+}
diff --git a/pkg/utils/mounter_mock.go b/pkg/utils/mounter_mock.go
new file mode 100644
index 0000000..9f67cf6
--- /dev/null
+++ b/pkg/utils/mounter_mock.go
@@ -0,0 +1,107 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package utils
+
+import (
+ "reflect"
+
+ "github.com/golang/mock/gomock"
+ utilexec "k8s.io/utils/exec"
+ k8smount "k8s.io/utils/mount"
+)
+
+// MockMounter ...
+type MockMounter struct {
+ k8smount.SafeFormatAndMount
+ utilexec.Interface
+ ctrl *gomock.Controller
+ recorder *MockMounterMockRecorder
+}
+
+// MockMounterMockRecorder ...
+type MockMounterMockRecorder struct {
+ mock *MockMounter
+}
+
+// NewMockMounter ...
+func NewMockMounter(ctrl *gomock.Controller) *MockMounter {
+ mock := &MockMounter{ctrl: ctrl}
+ mock.recorder = &MockMounterMockRecorder{mock}
+ return mock
+}
+
+// EXPECT ...
+func (m *MockMounter) EXPECT() *MockMounterMockRecorder {
+ return m.recorder
+}
+
+// EnsureFolder ...
+func (m *MockMounter) EnsureFolder(target string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "EnsureFolder", target)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// EnsureFolder ...
+func (mr MockMounterMockRecorder) EnsureFolder(target interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureFolder", reflect.TypeOf((*MockMounter)(nil).EnsureFolder), target)
+}
+
+// FormatAndMount ...
+func (m *MockMounter) FormatAndMount(source, target, fstype string, mkfsOptions []string, mountOptions string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FormatAndMount", source, target, fstype, mkfsOptions, mountOptions)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// FormatAndMount ...
+func (mr MockMounterMockRecorder) FormatAndMount(arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatAndMount", reflect.TypeOf((*MockMounter)(nil).FormatAndMount), arg1, arg2, arg3, arg4, arg5)
+}
+
+// IsMounted ...
+func (m *MockMounter) IsMounted(target string) (bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IsMounted", target)
+ ret0, _ := ret[0].(bool)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// IsMounted ...
+func (mr MockMounterMockRecorder) IsMounted(target interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsMounted", reflect.TypeOf((*MockMounter)(nil).IsMounted), target)
+}
+
+// SafePathRemove ...
+func (m MockMounter) SafePathRemove(targetPath string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SafePathRemove", targetPath)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SafePathRemove ...
+func (mr MockMounterMockRecorder) SafePathRemove(target interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafePathRemove", reflect.TypeOf((*MockMounter)(nil).SafePathRemove), target)
+}
diff --git a/pkg/utils/pmem.go b/pkg/utils/pmem.go
new file mode 100644
index 0000000..af7d6b0
--- /dev/null
+++ b/pkg/utils/pmem.go
@@ -0,0 +1,173 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package utils
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+ log "github.com/sirupsen/logrus"
+)
+
+// Pmemer ...
+type Pmemer interface {
+ GetRegions() (*model.PmemRegions, error)
+ CreateNamespace(string, string) error
+ CheckNamespaceUsed(string) bool
+ GetPmemNamespaceDeivcePath(string, string) (string, string, error)
+ MakeNamespaceMemory(chardev string) error
+ CheckKMEMCreated(chardev string) (bool, error)
+}
+
+// NodePmemer ...
+type NodePmemer struct {
+}
+
+// NewNodePmemer create new NodePmemer struct
+func NewNodePmemer() *NodePmemer {
+ return &NodePmemer{}
+}
+
+// GetRegions ...
+func (np *NodePmemer) GetRegions() (*model.PmemRegions, error) {
+ regions := &model.PmemRegions{}
+ getRegionCmd := fmt.Sprintf("%s ndctl list -RN", NsenterCmd)
+ regionOut, err := Run(getRegionCmd)
+ if err != nil {
+ return regions, err
+ }
+ err = json.Unmarshal(([]byte)(regionOut), regions)
+ if err != nil {
+ if strings.HasPrefix(regionOut, "[") {
+ regionList := []model.PmemRegion{}
+ err = json.Unmarshal(([]byte)(regionOut), ®ionList)
+ if err != nil {
+ return regions, err
+ }
+ regions.Regions = regionList
+ } else {
+ return regions, err
+ }
+ }
+ return regions, nil
+}
+
+// CreateNamespace ...
+func (np *NodePmemer) CreateNamespace(region, pmemType string) error {
+ var createCmd string
+ if pmemType == "lvm" {
+ createCmd = fmt.Sprintf("%s ndctl create-namespace -r %s", NsenterCmd, region)
+ } else {
+ createCmd = fmt.Sprintf("%s ndctl create-namespace -r %s --mode=devdax", NsenterCmd, region)
+ }
+ _, err := Run(createCmd)
+ if err != nil {
+ log.Errorf("Create NameSpace for region %s error: %v", region, err)
+ return err
+ }
+ log.Infof("Create NameSpace for region %s successful", region)
+ return nil
+}
+
+// CheckNamespaceUsed device used in block
+func (np *NodePmemer) CheckNamespaceUsed(devicePath string) bool {
+ pvCheckCmd := fmt.Sprintf("%s pvs %s 2>&1 | grep -v \"Failed to \" | grep /dev | awk '{print $2}' | wc -l", NsenterCmd, devicePath)
+ out, err := Run(pvCheckCmd)
+ if err == nil && strings.TrimSpace(out) != "0" {
+ log.Infof("CheckNamespaceUsed: NameSpace %s used for pv", devicePath)
+ return true
+ }
+
+ out, err = checkFSType(devicePath)
+ if err == nil && strings.TrimSpace(out) != "" {
+ log.Infof("CheckNamespaceUsed: NameSpace %s format as %s", devicePath, out)
+ return true
+ }
+ return false
+}
+
+// GetPmemNamespaceDeivcePath ...
+func (np *NodePmemer) GetPmemNamespaceDeivcePath(region, mode string) (devicePath string, namespaceName string, err error) {
+ regions, err := np.getRegionNamespaceInfo(region)
+ if err != nil {
+ return "", "", err
+ }
+ namespace := regions.Regions[0].Namespaces[0]
+ if namespace.Mode != mode {
+ log.Errorf("GetPmemNamespaceDeivcePath namespace mode %s wrong with: %s", namespace.Mode, mode)
+ return "", "", errors.New("GetPmemNamespaceDeivcePath pmem namespace wrong mode" + namespace.Mode)
+ }
+ if mode == "fsdax" {
+ devicePath = "/dev/" + namespace.BlockDev
+ } else {
+ devicePath = "/dev/" + namespace.CharDev
+ }
+ return devicePath, namespace.Dev, nil
+}
+
+func (np *NodePmemer) getRegionNamespaceInfo(region string) (*model.PmemRegions, error) {
+ listCmd := fmt.Sprintf("%s ndctl list -RN -r %s", NsenterCmd, region)
+
+ out, err := Run(listCmd)
+ if err != nil {
+ log.Errorf("List NameSpace for region %s error: %v", region, err)
+ return nil, err
+ }
+ regions := &model.PmemRegions{}
+ err = json.Unmarshal(([]byte)(out), regions)
+ if len(regions.Regions) == 0 {
+ log.Errorf("list Namespace for region %s get 0 region, out: %s", region, out)
+ return nil, errors.New("list Namespace get 0 region by " + region)
+ }
+
+ if len(regions.Regions[0].Namespaces) != 1 {
+ log.Errorf("list Namespace for region %s get 0 or multi namespaces", region)
+ return nil, errors.New("list Namespace for region get 0 or multi namespaces" + region)
+ }
+ return regions, nil
+}
+
+// MakeNamespaceMemory ...
+func (np *NodePmemer) MakeNamespaceMemory(chardev string) error {
+ makeCmd := fmt.Sprintf("%s daxctl reconfigure-device -m system-ram %s", NsenterCmd, chardev)
+ _, err := Run(makeCmd)
+ return err
+}
+
+// CheckKMEMCreated ...
+func (np *NodePmemer) CheckKMEMCreated(chardev string) (bool, error) {
+ listCmd := fmt.Sprintf("%s daxctl list", NsenterCmd)
+ out, err := Run(listCmd)
+ if err != nil {
+ log.Errorf("CheckKMEMCreated:: List daxctl error: %v", err)
+ return false, err
+ }
+ memList := []*model.DaxctrlMem{}
+ err = json.Unmarshal(([]byte)(out), &memList)
+ if err != nil {
+ return false, err
+ }
+ for _, mem := range memList {
+ if mem.Chardev == chardev && mem.Mode == "system-ram" {
+ return true, nil
+ }
+ }
+ return false, nil
+}
diff --git a/pkg/utils/pmem_mock.go b/pkg/utils/pmem_mock.go
new file mode 100644
index 0000000..481c15a
--- /dev/null
+++ b/pkg/utils/pmem_mock.go
@@ -0,0 +1,135 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package utils
+
+import (
+ "reflect"
+
+ "github.com/golang/mock/gomock"
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+)
+
+// MockPmemer ..
+type MockPmemer struct {
+ ctrl *gomock.Controller
+ recorder *MockPmemerMockRecorder
+}
+
+// MockPmemerMockRecorder ...
+type MockPmemerMockRecorder struct {
+ mock *MockPmemer
+}
+
+// EXPECT ...
+func (m *MockPmemer) EXPECT() *MockPmemerMockRecorder {
+ return m.recorder
+}
+
+// NewMockPmemer ...
+func NewMockPmemer(ctrl *gomock.Controller) *MockPmemer {
+ mock := &MockPmemer{ctrl: ctrl}
+ mock.recorder = &MockPmemerMockRecorder{mock}
+ return mock
+}
+
+// GetRegions ...
+func (m *MockPmemer) GetRegions() (*model.PmemRegions, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetRegions")
+ ret0, _ := ret[0].(*model.PmemRegions)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetRegions ...
+func (mr *MockPmemerMockRecorder) GetRegions() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegions", reflect.TypeOf((*MockPmemer)(nil).GetRegions))
+}
+
+// CreateNamespace ...
+func (m *MockPmemer) CreateNamespace(arg1, arg2 string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateNamespace", arg1, arg2)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// CreateNamespace ...
+func (mr *MockPmemerMockRecorder) CreateNamespace(arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNamespace", reflect.TypeOf((*MockPmemer)(nil).CreateNamespace), arg1, arg2)
+}
+
+// CheckNamespaceUsed ...
+func (m *MockPmemer) CheckNamespaceUsed(arg1 string) bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CheckNamespaceUsed", arg1)
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// CheckNamespaceUsed ...
+func (mr *MockPmemerMockRecorder) CheckNamespaceUsed(arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckNamespaceUsed", reflect.TypeOf((*MockPmemer)(nil).CheckNamespaceUsed), arg1)
+}
+
+// GetPmemNamespaceDeivcePath ...
+func (m *MockPmemer) GetPmemNamespaceDeivcePath(arg1, arg2 string) (string, string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPmemNamespaceDeivcePath", arg1, arg2)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(string)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// GetPmemNamespaceDeivcePath ...
+func (mr *MockPmemerMockRecorder) GetPmemNamespaceDeivcePath(arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPmemNamespaceDeivcePath", reflect.TypeOf((*MockPmemer)(nil).GetPmemNamespaceDeivcePath), arg1, arg2)
+}
+
+// CheckKMEMCreated ...
+func (m *MockPmemer) CheckKMEMCreated(arg1 string) (bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CheckKMEMCreated", arg1)
+ ret0, _ := ret[0].(bool)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CheckKMEMCreated ...
+func (mr *MockPmemerMockRecorder) CheckKMEMCreated(arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckKMEMCreated", reflect.TypeOf((*MockPmemer)(nil).CheckKMEMCreated), arg1)
+}
+
+// MakeNamespaceMemory ...
+func (m *MockPmemer) MakeNamespaceMemory(arg1 string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "MakeNamespaceMemory", arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// MakeNamespaceMemory ...
+func (mr *MockPmemerMockRecorder) MakeNamespaceMemory(arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeNamespaceMemory", reflect.TypeOf((*MockPmemer)(nil).MakeNamespaceMemory), arg1)
+}
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
new file mode 100644
index 0000000..ca59b50
--- /dev/null
+++ b/pkg/utils/utils.go
@@ -0,0 +1,176 @@
+/*
+Copyright 2021 The OpenYurt Authors.
+
+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.
+*/
+
+package utils
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/openyurtio/node-resource-manager/pkg/model"
+ log "github.com/sirupsen/logrus"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+ // MetadataURL is metadata url
+ MetadataURL = "http://100.100.100.200/latest/meta-data/"
+
+ // NsenterCmd use to init resource
+ NsenterCmd = "/usr/bin/nsenter --mount=/proc/1/ns/mnt --ipc=/proc/1/ns/ipc --net=/proc/1/ns/net --uts=/proc/1/ns/uts "
+)
+
+// ErrParse ...
+var ErrParse = errors.New("Cannot parse output of blkid")
+
+//GetMetaData get metadata from ecs meta-server
+func GetMetaData(resource string) (string, error) {
+ resp, err := http.Get(MetadataURL + resource)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ return string(body), nil
+}
+
+// Run run shell command
+func Run(cmd string) (string, error) {
+ out, err := exec.Command("sh", "-c", cmd).CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("Failed to run cmd: " + cmd + ", with out: " + string(out) + ", with error: " + err.Error())
+ }
+ return string(out), nil
+}
+
+// NodeFilter go through all configmap to find current node config
+func NodeFilter(configOperator metav1.LabelSelectorOperator, configKey, configValue string, nodeInfo *v1.Node) bool {
+
+ isMatched := false
+ switch configOperator {
+ case metav1.LabelSelectorOpIn:
+ for key, value := range nodeInfo.Labels {
+ if key == configKey && value == configValue {
+ isMatched = true
+ }
+ }
+ case metav1.LabelSelectorOpNotIn:
+ flag := false
+ for key, value := range nodeInfo.Labels {
+ if key == configKey && value == configValue {
+ flag = true
+ }
+ }
+ if flag == false {
+ isMatched = true
+ }
+ case metav1.LabelSelectorOpExists:
+ for key := range nodeInfo.Labels {
+ if key == configKey {
+ isMatched = true
+ }
+ }
+ case metav1.LabelSelectorOpDoesNotExist:
+ flag := false
+ for key := range nodeInfo.Labels {
+ if key == configKey {
+ flag = true
+ }
+ }
+ if flag == false {
+ isMatched = true
+ }
+ default:
+ log.Errorf("Get unsupported operator: %s", configOperator)
+ }
+ return isMatched
+}
+
+// ConvertRegion2Namespace ...
+func ConvertRegion2Namespace(region string) string {
+ regionIndex := region[6:]
+ return fmt.Sprintf("namespace%s.0", regionIndex)
+}
+
+// ConvertNamespace2LVMDevicePath ...
+func ConvertNamespace2LVMDevicePath(namespace string, regions *model.PmemRegions) string {
+ for _, region := range regions.Regions {
+ for _, actualNamespace := range region.Namespaces {
+ if actualNamespace.Dev == namespace {
+ return filepath.Join("/dev", actualNamespace.BlockDev)
+ }
+ }
+ }
+ return ""
+}
+
+func checkFSType(devicePath string) (string, error) {
+ // We use `file -bsL` to determine whether any filesystem type is detected.
+ // If a filesystem is detected (ie., the output is not "data", we use
+ // `blkid` to determine what the filesystem is. We use `blkid` as `file`
+ // has inconvenient output.
+ // We do *not* use `lsblk` as that requires udev to be up-to-date which
+ // is often not the case when a device is erased using `dd`.
+ output, err := exec.Command("file", "-bsL", devicePath).CombinedOutput()
+ if err != nil {
+ return "", err
+ }
+ if strings.TrimSpace(string(output)) == "data" {
+ return "", nil
+ }
+ output, err = exec.Command("blkid", "-c", "/dev/null", "-o", "export", devicePath).CombinedOutput()
+ if err != nil {
+ return "", err
+ }
+
+ lines := strings.Split(string(output), "\n")
+ for _, line := range lines {
+ fields := strings.Split(strings.TrimSpace(line), "=")
+ if len(fields) != 2 {
+ return "", ErrParse
+ }
+ if fields[0] == "TYPE" {
+ return fields[1], nil
+ }
+ }
+ return "", ErrParse
+}
+
+// IsPart if smallList is part of or equal to largeList, return true;
+func IsPart(largeList, smallList []string) bool {
+ isPartFlag := true
+ for _, smalltmp := range smallList {
+ flag := false
+ for _, largetmp := range largeList {
+ if smalltmp == largetmp {
+ flag = true
+ }
+ }
+ if flag == false {
+ isPartFlag = false
+ }
+ }
+ return isPartFlag
+}