zfs-localpv/pkg/zfs/zfs_util.go
Pawan Prakash Sharma 287606b78a
feat(zfspv): adding snapshot and clone support for ZFSPV (#39)
This commits support snapshot and clone commands via CSI driver. User can create snap and clone using the following steps. 

Note:
- Snapshot is created via reconciliation CR
- Cloned volume will be on the same zpool where the snapshot is taken
- Cloned volume will have same properties as source volume. 

-----------------------------------
Create a Snapshotclass
```
kind: VolumeSnapshotClass
apiVersion: snapshot.storage.k8s.io/v1beta1
metadata:
  name: zfspv-snapclass
  annotations:
    snapshot.storage.kubernetes.io/is-default-class: "true"
driver: zfs.csi.openebs.io
deletionPolicy: Delete
```
Once snapshotclass is created, we can use this class to create a Snapshot 
```
apiVersion: snapshot.storage.k8s.io/v1beta1
kind: VolumeSnapshot
metadata:
  name: zfspv-snap
spec:
  volumeSnapshotClassName: zfspv-snapclass
  source:
    persistentVolumeClaimName: csi-zfspv
```
```
$ kubectl get volumesnapshot
NAME          AGE
zfspv-snap    7m52s
```
```
$ kubectl get volumesnapshot -o yaml
apiVersion: v1
items:
- apiVersion: snapshot.storage.k8s.io/v1beta1
  kind: VolumeSnapshot
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"snapshot.storage.k8s.io/v1beta1","kind":"VolumeSnapshot","metadata":{"annotations":{},"name":"zfspv-snap","namespace":"default"},"spec":{"source":{"persistentVolumeClaimName":"csi-zfspv"},"volumeSnapshotClassName":"zfspv-snapclass"}}
    creationTimestamp: "2020-01-30T10:31:24Z"
    finalizers:
    - snapshot.storage.kubernetes.io/volumesnapshot-as-source-protection
    - snapshot.storage.kubernetes.io/volumesnapshot-bound-protection
    generation: 1
    name: zfspv-snap
    namespace: default
    resourceVersion: "30040"
    selfLink: /apis/snapshot.storage.k8s.io/v1beta1/namespaces/default/volumesnapshots/zfspv-snap
    uid: 1a5cf166-c599-4f58-9f3c-f1148be47fca
  spec:
    source:
      persistentVolumeClaimName: csi-zfspv
    volumeSnapshotClassName: zfspv-snapclass
  status:
    boundVolumeSnapshotContentName: snapcontent-1a5cf166-c599-4f58-9f3c-f1148be47fca
    creationTime: "2020-01-30T10:31:24Z"
    readyToUse: true
    restoreSize: "0"
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""
```


Openebs resource for the created snapshot 
```
$ kubectl get snap -n openebs -o yaml
apiVersion: v1
items:
- apiVersion: openebs.io/v1alpha1
  kind: ZFSSnapshot
  metadata:
    creationTimestamp: "2020-01-30T10:31:24Z"
    finalizers:
    - zfs.openebs.io/finalizer
    generation: 2
    labels:
      kubernetes.io/nodename: pawan-2
      openebs.io/persistent-volume: pvc-18cab7c3-ec5e-4264-8507-e6f7df4c789a
    name: snapshot-1a5cf166-c599-4f58-9f3c-f1148be47fca
    namespace: openebs
    resourceVersion: "30035"
    selfLink: /apis/openebs.io/v1alpha1/namespaces/openebs/zfssnapshots/snapshot-1a5cf166-c599-4f58-9f3c-f1148be47fca
    uid: e29d571c-42b5-4fb7-9110-e1cfc9b96641
  spec:
    capacity: "4294967296"
    fsType: zfs
    ownerNodeID: pawan-2
    poolName: zfspv-pool
    status: Ready
    volumeType: DATASET
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""
```

Create a clone volume
    
 We can provide a datasource as snapshot name to create a clone volume
    
```yaml
    kind: PersistentVolumeClaim
    apiVersion: v1
    metadata:
      name: zfspv-clone
    spec:
      storageClassName: openebs-zfspv
      dataSource:
        name: zfspv-snap
        kind: VolumeSnapshot
        apiGroup: snapshot.storage.k8s.io
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 4Gi
```
It will create a ZFS clone volume from the mentioned snapshot and create the PV on the same node where original volume is there.
    
Here, As resize is not supported yet, the clone PVC size should match the size of the snapshot.
Also, all the properties from the storageclass will not be considered for the clone case, it will take the properties from the snapshot and create the clone volume. One thing to note here is that, the storageclass in clone PVC should have the same poolname as that of the original volume as across the pool, clone is not supported.


Signed-off-by: Pawan <pawan@mayadata.io>
2020-02-13 13:31:17 +05:30

498 lines
14 KiB
Go

/*
Copyright 2017 The Kubernetes 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 zfs
import (
"os/exec"
"path/filepath"
"github.com/Sirupsen/logrus"
apis "github.com/openebs/zfs-localpv/pkg/apis/openebs.io/core/v1alpha1"
)
// zfs related constants
const (
ZFS_DEVPATH = "/dev/zvol/"
FSTYPE_ZFS = "zfs"
)
// zfs command related constants
const (
ZFSVolCmd = "zfs"
ZFSCreateArg = "create"
ZFSCloneArg = "clone"
ZFSDestroyArg = "destroy"
ZFSSetArg = "set"
ZFSGetArg = "get"
ZFSListArg = "list"
ZFSSnapshotArg = "snapshot"
)
// constants to define volume type
const (
VOLTYPE_DATASET = "DATASET"
VOLTYPE_ZVOL = "ZVOL"
)
func PropertyChanged(oldVol *apis.ZFSVolume, newVol *apis.ZFSVolume) bool {
if oldVol.Spec.VolumeType == VOLTYPE_DATASET &&
newVol.Spec.VolumeType == VOLTYPE_DATASET &&
oldVol.Spec.RecordSize != newVol.Spec.RecordSize {
return true
}
return oldVol.Spec.Compression != newVol.Spec.Compression ||
oldVol.Spec.Dedup != newVol.Spec.Dedup
}
// GetVolumeType returns the volume type
// whether it is a zvol or dataset
func GetVolumeType(fstype string) string {
/*
* if fstype is provided as zfs then a zfs dataset will be created
* otherwise a zvol will be created
*/
switch fstype {
case FSTYPE_ZFS:
return VOLTYPE_DATASET
default:
return VOLTYPE_ZVOL
}
}
// builldZvolCreateArgs returns zfs create command for zvol along with attributes as a string array
func buildZvolCreateArgs(vol *apis.ZFSVolume) []string {
var ZFSVolArg []string
volume := vol.Spec.PoolName + "/" + vol.Name
ZFSVolArg = append(ZFSVolArg, ZFSCreateArg)
if vol.Spec.ThinProvision == "yes" {
ZFSVolArg = append(ZFSVolArg, "-s")
}
if len(vol.Spec.Capacity) != 0 {
ZFSVolArg = append(ZFSVolArg, "-V", vol.Spec.Capacity)
}
if len(vol.Spec.VolBlockSize) != 0 {
ZFSVolArg = append(ZFSVolArg, "-b", vol.Spec.VolBlockSize)
}
if len(vol.Spec.Dedup) != 0 {
dedupProperty := "dedup=" + vol.Spec.Dedup
ZFSVolArg = append(ZFSVolArg, "-o", dedupProperty)
}
if len(vol.Spec.Compression) != 0 {
compressionProperty := "compression=" + vol.Spec.Compression
ZFSVolArg = append(ZFSVolArg, "-o", compressionProperty)
}
if len(vol.Spec.Encryption) != 0 {
encryptionProperty := "encryption=" + vol.Spec.Encryption
ZFSVolArg = append(ZFSVolArg, "-o", encryptionProperty)
}
if len(vol.Spec.KeyLocation) != 0 {
keyLocation := "keylocation=" + vol.Spec.KeyLocation
ZFSVolArg = append(ZFSVolArg, "-o", keyLocation)
}
if len(vol.Spec.KeyFormat) != 0 {
keyFormat := "keyformat=" + vol.Spec.KeyFormat
ZFSVolArg = append(ZFSVolArg, "-o", keyFormat)
}
ZFSVolArg = append(ZFSVolArg, volume)
return ZFSVolArg
}
// builldCloneCreateArgs returns zfs clone commands for zfs volume/dataset along with attributes as a string array
func buildCloneCreateArgs(vol *apis.ZFSVolume) []string {
var ZFSVolArg []string
volume := vol.Spec.PoolName + "/" + vol.Name
snapshot := vol.Spec.PoolName + "/" + vol.Spec.SnapName
ZFSVolArg = append(ZFSVolArg, ZFSCloneArg)
if vol.Spec.VolumeType == VOLTYPE_DATASET {
ZFSVolArg = append(ZFSVolArg, "-o", "mountpoint=none")
}
ZFSVolArg = append(ZFSVolArg, snapshot, volume)
return ZFSVolArg
}
// buildZFSSnapCreateArgs returns zfs create command for zfs snapshot
// zfs snapshot <poolname>/<volname>@<snapname>
func buildZFSSnapCreateArgs(snap *apis.ZFSSnapshot) []string {
var ZFSSnapArg []string
volname := snap.Labels[ZFSVolKey]
snapDataset := snap.Spec.PoolName + "/" + volname + "@" + snap.Name
ZFSSnapArg = append(ZFSSnapArg, ZFSSnapshotArg, snapDataset)
return ZFSSnapArg
}
// builldZFSSnapDestroyArgs returns zfs destroy command for zfs snapshot
// zfs destroy <poolname>/<volname>@<snapname>
func buildZFSSnapDestroyArgs(snap *apis.ZFSSnapshot) []string {
var ZFSSnapArg []string
volname := snap.Labels[ZFSVolKey]
snapDataset := snap.Spec.PoolName + "/" + volname + "@" + snap.Name
ZFSSnapArg = append(ZFSSnapArg, ZFSDestroyArg, snapDataset)
return ZFSSnapArg
}
// builldDatasetCreateArgs returns zfs create command for dataset along with attributes as a string array
func buildDatasetCreateArgs(vol *apis.ZFSVolume) []string {
var ZFSVolArg []string
volume := vol.Spec.PoolName + "/" + vol.Name
ZFSVolArg = append(ZFSVolArg, ZFSCreateArg)
if len(vol.Spec.Capacity) != 0 {
quotaProperty := "quota=" + vol.Spec.Capacity
ZFSVolArg = append(ZFSVolArg, "-o", quotaProperty)
}
if len(vol.Spec.RecordSize) != 0 {
recordsizeProperty := "recordsize=" + vol.Spec.RecordSize
ZFSVolArg = append(ZFSVolArg, "-o", recordsizeProperty)
}
if vol.Spec.ThinProvision == "no" {
reservationProperty := "reservation=" + vol.Spec.Capacity
ZFSVolArg = append(ZFSVolArg, "-o", reservationProperty)
}
if len(vol.Spec.Dedup) != 0 {
dedupProperty := "dedup=" + vol.Spec.Dedup
ZFSVolArg = append(ZFSVolArg, "-o", dedupProperty)
}
if len(vol.Spec.Compression) != 0 {
compressionProperty := "compression=" + vol.Spec.Compression
ZFSVolArg = append(ZFSVolArg, "-o", compressionProperty)
}
if len(vol.Spec.Encryption) != 0 {
encryptionProperty := "encryption=" + vol.Spec.Encryption
ZFSVolArg = append(ZFSVolArg, "-o", encryptionProperty)
}
if len(vol.Spec.KeyLocation) != 0 {
keyLocation := "keylocation=" + vol.Spec.KeyLocation
ZFSVolArg = append(ZFSVolArg, "-o", keyLocation)
}
if len(vol.Spec.KeyFormat) != 0 {
keyFormat := "keyformat=" + vol.Spec.KeyFormat
ZFSVolArg = append(ZFSVolArg, "-o", keyFormat)
}
// set the mount path to none, by default zfs mounts it to the default dataset path
ZFSVolArg = append(ZFSVolArg, "-o", "mountpoint=none", volume)
return ZFSVolArg
}
// builldVolumeSetArgs returns volume set command along with attributes as a string array
// TODO(pawan) need to find a way to identify which property has changed
func buildVolumeSetArgs(vol *apis.ZFSVolume) []string {
var ZFSVolArg []string
volume := vol.Spec.PoolName + "/" + vol.Name
ZFSVolArg = append(ZFSVolArg, ZFSSetArg)
if vol.Spec.VolumeType == VOLTYPE_DATASET &&
len(vol.Spec.RecordSize) != 0 {
recordsizeProperty := "recordsize=" + vol.Spec.RecordSize
ZFSVolArg = append(ZFSVolArg, recordsizeProperty)
}
if len(vol.Spec.Dedup) != 0 {
dedupProperty := "dedup=" + vol.Spec.Dedup
ZFSVolArg = append(ZFSVolArg, dedupProperty)
}
if len(vol.Spec.Compression) != 0 {
compressionProperty := "compression=" + vol.Spec.Compression
ZFSVolArg = append(ZFSVolArg, compressionProperty)
}
ZFSVolArg = append(ZFSVolArg, volume)
return ZFSVolArg
}
// builldVolumeDestroyArgs returns volume destroy command along with attributes as a string array
func buildVolumeDestroyArgs(vol *apis.ZFSVolume) []string {
var ZFSVolArg []string
volume := vol.Spec.PoolName + "/" + vol.Name
ZFSVolArg = append(ZFSVolArg, ZFSDestroyArg, volume)
return ZFSVolArg
}
func getVolume(volume string) error {
var ZFSVolArg []string
ZFSVolArg = append(ZFSVolArg, ZFSListArg, volume)
cmd := exec.Command(ZFSVolCmd, ZFSVolArg...)
_, err := cmd.CombinedOutput()
return err
}
// CreateVolume creates the zvol/dataset as per
// info provided in ZFSVolume object
func CreateVolume(vol *apis.ZFSVolume) error {
volume := vol.Spec.PoolName + "/" + vol.Name
if err := getVolume(volume); err != nil {
var args []string
if vol.Spec.VolumeType == VOLTYPE_DATASET {
args = buildDatasetCreateArgs(vol)
} else {
args = buildZvolCreateArgs(vol)
}
cmd := exec.Command(ZFSVolCmd, args...)
out, err := cmd.CombinedOutput()
if err != nil {
logrus.Errorf(
"zfs: could not create volume %v cmd %v error: %s", volume, args, string(out),
)
return err
}
logrus.Infof("created volume %s", volume)
} else if err == nil {
logrus.Infof("using existing volume %v", volume)
}
return nil
}
// CreateClone creates clone for the zvol/dataset as per
// info provided in ZFSVolume object
func CreateClone(vol *apis.ZFSVolume) error {
volume := vol.Spec.PoolName + "/" + vol.Name
if err := getVolume(volume); err != nil {
var args []string
args = buildCloneCreateArgs(vol)
cmd := exec.Command(ZFSVolCmd, args...)
out, err := cmd.CombinedOutput()
if err != nil {
logrus.Errorf(
"zfs: could not clone volume %v cmd %v error: %s", volume, args, string(out),
)
return err
}
logrus.Infof("created clone %s", volume)
} else if err == nil {
logrus.Infof("using existing clone volume %v", volume)
}
return nil
}
// SetDatasetMountProp sets mountpoint for the volume
func SetDatasetMountProp(volume string, mountpath string) error {
var ZFSVolArg []string
mountProperty := "mountpoint=" + mountpath
ZFSVolArg = append(ZFSVolArg, ZFSSetArg, mountProperty, volume)
cmd := exec.Command(ZFSVolCmd, ZFSVolArg...)
out, err := cmd.CombinedOutput()
if err != nil {
logrus.Errorf("zfs: could not set mountpoint on dataset %v cmd %v error: %s",
volume, ZFSVolArg, string(out))
}
return err
}
// MountZFSDataset mounts the dataset to the given mountpoint
func MountZFSDataset(vol *apis.ZFSVolume, mountpath string) error {
volume := vol.Spec.PoolName + "/" + vol.Name
return SetDatasetMountProp(volume, mountpath)
}
// UmountZFSDataset umounts the dataset
func UmountZFSDataset(vol *apis.ZFSVolume) error {
volume := vol.Spec.PoolName + "/" + vol.Name
return SetDatasetMountProp(volume, "none")
}
// GetVolumeProperty gets zfs properties for the volume
func GetVolumeProperty(vol *apis.ZFSVolume, prop string) (string, error) {
var ZFSVolArg []string
volume := vol.Spec.PoolName + "/" + vol.Name
ZFSVolArg = append(ZFSVolArg, ZFSGetArg, "-pH", "-o", "value", prop, volume)
cmd := exec.Command(ZFSVolCmd, ZFSVolArg...)
out, err := cmd.CombinedOutput()
if err != nil {
logrus.Errorf("zfs: could not get %s on dataset %v cmd %v error: %s",
prop, volume, ZFSVolArg, string(out))
return "", err
}
val := out[:len(out)-1]
return string(val), nil
}
// SetVolumeProp sets the volume property
func SetVolumeProp(vol *apis.ZFSVolume) error {
var err error
volume := vol.Spec.PoolName + "/" + vol.Name
if len(vol.Spec.Compression) == 0 &&
len(vol.Spec.Dedup) == 0 &&
(vol.Spec.VolumeType != VOLTYPE_DATASET ||
len(vol.Spec.RecordSize) == 0) {
//nothing to set, just return
return nil
}
/* Case: Restart =>
* In this case we get the add event but here we don't know which
* property has changed when we were down, so firing the zfs set
* command with the all property present on the ZFSVolume.
* Case: Property Change =>
* TODO(pawan) When we get the update event, we make sure at least
* one property has changed before adding it to the event queue for
* handling. At this stage, since we haven't stored the
* ZFSVolume object as it will be too heavy, we are firing the set
* command with the all property preset in the ZFSVolume object since
* it is guaranteed that at least one property has changed.
*/
args := buildVolumeSetArgs(vol)
cmd := exec.Command(ZFSVolCmd, args...)
out, err := cmd.CombinedOutput()
if err != nil {
logrus.Errorf(
"zfs: could not set property on volume %v cmd %v error: %s", volume, args, string(out),
)
return err
}
logrus.Infof("property set on volume %s", volume)
return err
}
// DestroyVolume deletes the zfs volume
func DestroyVolume(vol *apis.ZFSVolume) error {
volume := vol.Spec.PoolName + "/" + vol.Name
if err := getVolume(volume); err != nil {
logrus.Errorf(
"destroy: volume %v is not present, error: %s", volume, err.Error(),
)
return nil
}
args := buildVolumeDestroyArgs(vol)
cmd := exec.Command(ZFSVolCmd, args...)
out, err := cmd.CombinedOutput()
if err != nil {
logrus.Errorf(
"zfs: could not destroy volume %v cmd %v error: %s", volume, args, string(out),
)
return err
}
logrus.Infof("destroyed volume %s", volume)
return nil
}
// CreateSnapshot creates the zfs volume snapshot
func CreateSnapshot(snap *apis.ZFSSnapshot) error {
volume := snap.Labels[ZFSVolKey]
snapDataset := snap.Spec.PoolName + "/" + volume + "@" + snap.Name
if err := getVolume(snapDataset); err == nil {
logrus.Infof("snapshot already there %s", snapDataset)
// snapshot already there just return
return nil
}
args := buildZFSSnapCreateArgs(snap)
cmd := exec.Command(ZFSVolCmd, args...)
out, err := cmd.CombinedOutput()
if err != nil {
logrus.Errorf(
"zfs: could not create snapshot %v@%v cmd %v error: %s", volume, snap.Name, args, string(out),
)
return err
}
logrus.Infof("created snapshot %s@%s", volume, snap.Name)
return nil
}
// DestroySnapshot deletes the zfs volume snapshot
func DestroySnapshot(snap *apis.ZFSSnapshot) error {
volume := snap.Labels[ZFSVolKey]
snapDataset := snap.Spec.PoolName + "/" + volume + "@" + snap.Name
if err := getVolume(snapDataset); err != nil {
logrus.Errorf(
"destroy: snapshot %v is not present, error: %s", volume, err.Error(),
)
return nil
}
args := buildZFSSnapDestroyArgs(snap)
cmd := exec.Command(ZFSVolCmd, args...)
out, err := cmd.CombinedOutput()
if err != nil {
logrus.Errorf(
"zfs: could not destroy snapshot %v@%v cmd %v error: %s", volume, snap.Name, args, string(out),
)
return err
}
logrus.Infof("deleted snapshot %s@%s", volume, snap.Name)
return nil
}
// GetVolumeDevPath returns devpath for the given volume
func GetVolumeDevPath(vol *apis.ZFSVolume) (string, error) {
volume := vol.Spec.PoolName + "/" + vol.Name
if vol.Spec.VolumeType == VOLTYPE_DATASET {
return volume, nil
}
devicePath := ZFS_DEVPATH + volume
// evaluate the symlink to get the dev path for zvol
dev, err := filepath.EvalSymlinks(devicePath)
if err != nil {
return "", err
}
return dev, nil
}