Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,44 @@ The Harvester-CSI-Driver-LVM provides the following features:
- Support Volume Expansion.
- Support Volume Snapshot.
- Support Volume Clone.
- Support Encryption at Rest (LUKS2 / dm-crypt).

**NOTE**: The Snapshot/Clone feature only works on the same nodes. Clone works for different Volume Groups.

### Encryption at Rest

Volumes can be transparently encrypted at rest with LUKS2 (dm-crypt). Set
`encrypted: "true"` on the StorageClass and reference a CSI secret that follows
the same `CRYPTO_KEY_*` convention as Longhorn encrypted volumes — the passphrase
lives in `CRYPTO_KEY_VALUE`. Using the platform's existing encryption-secret
schema means the Harvester admission webhook and UI accept the StorageClass
unchanged:

```yaml
parameters:
type: dm-thin
vgName: vg01
encrypted: "true"
csi.storage.k8s.io/provisioner-secret-name: ${pvc.name}-luks
csi.storage.k8s.io/provisioner-secret-namespace: ${pvc.namespace}
csi.storage.k8s.io/node-publish-secret-name: ${pvc.name}-luks
csi.storage.k8s.io/node-publish-secret-namespace: ${pvc.namespace}
```

The secret must carry `CRYPTO_KEY_VALUE` (the passphrase); the optional
`CRYPTO_KEY_CIPHER`, `CRYPTO_KEY_HASH`, `CRYPTO_KEY_SIZE` and `CRYPTO_PBKDF`
fields tune `luksFormat` and default to `aes-xts-plain64` / `sha256` / `256` /
`argon2i` (Longhorn's defaults) when omitted.

On first `NodePublishVolume` the logical volume is LUKS2-formatted and opened as
`/dev/mapper/csi-lvm-<volID>`; the filesystem (or raw block bind-mount) is placed
on the mapper so all data on the backing LV is encrypted. The passphrase is fed
to `cryptsetup` over stdin and never appears in the host process list. The
mapping is torn down on `NodeUnpublishVolume` and grown on `NodeExpandVolume`.

See `examples/storageclass-dm-thin-encrypted.yaml`. **Losing the passphrase
makes the data unrecoverable** — manage it with a KMS-backed secret store.

## Installation ##

You can use Helm to install the Harvester-CSI-Driver-LVM by remote repo or local helm chart files.
Expand Down
22 changes: 15 additions & 7 deletions cmd/provisioner/clonelv.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"os"
"strings"
"syscall"
"time"

"github.com/urfave/cli/v2"
"k8s.io/klog/v2"
Expand Down Expand Up @@ -88,12 +87,8 @@ func clonelv(c *cli.Context) error {

klog.Infof("Clone from src: %s, to dst: %s/%s", srcLvName, dstVGName, dstLV)

if !lvm.VgExists(dstVGName) {
lvm.VgActivate()
time.Sleep(1 * time.Second) // jitter
if !lvm.VgExists(dstVGName) {
return fmt.Errorf("vg %s does not exist, please check the corresponding VG is created", dstVGName)
}
if err := ensureCloneVolumeGroups(srcVgName, dstVGName); err != nil {
return err
}

klog.Infof("clone lv %s, vg: %s, type: %s", srcLvName, srcVgName, srcType)
Expand Down Expand Up @@ -139,3 +134,16 @@ func clonelv(c *cli.Context) error {

return nil
}

func ensureCloneVolumeGroups(srcVGName, dstVGName string) error {
if err := lvm.EnsureVG(srcVGName); err != nil {
return fmt.Errorf("source volume group is unavailable: %w", err)
}
if dstVGName == srcVGName {
return nil
}
if err := lvm.EnsureVG(dstVGName); err != nil {
return fmt.Errorf("destination volume group is unavailable: %w", err)
}
return nil
}
9 changes: 2 additions & 7 deletions cmd/provisioner/createlv.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"fmt"
"time"

"github.com/urfave/cli/v2"
"k8s.io/klog/v2"
Expand Down Expand Up @@ -61,12 +60,8 @@ func createLV(c *cli.Context) error {

klog.Infof("create lv %s size:%d vg:%s type:%s", lvName, lvSize, vgName, lvmType)

if !lvm.VgExists(vgName) {
lvm.VgActivate()
time.Sleep(1 * time.Second) // jitter
if !lvm.VgExists(vgName) {
return fmt.Errorf("vg %s does not exist, please check the corresponding VG is created", vgName)
}
if err := lvm.EnsureVG(vgName); err != nil {
return err
}

output, err := lvm.CreateLVS(vgName, lvName, lvSize, lvmType)
Expand Down
9 changes: 2 additions & 7 deletions cmd/provisioner/createsnap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"fmt"
"time"

"github.com/urfave/cli/v2"
"k8s.io/klog/v2"
Expand Down Expand Up @@ -69,12 +68,8 @@ func createSnap(c *cli.Context) error {

klog.Infof("create snapshot: %s source size: %d source lv: %s/%s", snapName, lvSize, vgName, lvName)

if !lvm.VgExists(vgName) {
lvm.VgActivate()
time.Sleep(1 * time.Second) // jitter
if !lvm.VgExists(vgName) {
return fmt.Errorf("vg %s does not exist, please check the corresponding VG is created", vgName)
}
if err := lvm.EnsureVG(vgName); err != nil {
return err
}

output, err := lvm.CreateSnapshot(snapName, lvName, vgName, lvSize, lvType, !createSnapshotForClone)
Expand Down
2 changes: 1 addition & 1 deletion cmd/provisioner/deletelv.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func deleteLV(c *cli.Context) error {

klog.Infof("delete lv %s", lvName)

output, err := lvm.RemoveLVS(lvName)
output, err := lvm.RemoveLVSInVG(vgName, lvName)
if err != nil {
return fmt.Errorf("unable to delete lv: %w output:%s", err, output)
}
Expand Down
11 changes: 1 addition & 10 deletions cmd/provisioner/deletesnap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"fmt"
"time"

"github.com/urfave/cli/v2"
"k8s.io/klog/v2"
Expand Down Expand Up @@ -45,17 +44,9 @@ func deleteSnap(c *cli.Context) error {

klog.Infof("delete snapshot: %s/%s", vgName, snapName)

if !lvm.VgExists(vgName) {
lvm.VgActivate()
time.Sleep(1 * time.Second) // jitter
if !lvm.VgExists(vgName) {
return fmt.Errorf("vg %s does not exist, please check the corresponding VG is created", vgName)
}
}

output, err := lvm.DeleteSnapshot(snapName, vgName)
if err != nil {
return fmt.Errorf("unable to create Snapshot: %w output:%s", err, output)
return fmt.Errorf("unable to delete snapshot: %w output:%s", err, output)
}
klog.Infof("Snapshot: %s/%s deleted", vgName, snapName)
return nil
Expand Down
9 changes: 2 additions & 7 deletions cmd/provisioner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,8 @@ const (
snapshotPrefix = "lvm-snapshot-"
)

func cmdNotFound(_ *cli.Context, command string) {
panic(fmt.Errorf("unrecognized command: %s", command))
}

func onUsageError(_ *cli.Context, _ error, _ bool) error {
panic(fmt.Errorf("usage error, please check your command"))
func onUsageError(_ *cli.Context, err error, _ bool) error {
return fmt.Errorf("usage error: %w", err)
}

func main() {
Expand All @@ -40,7 +36,6 @@ func main() {
deleteSnapCmd(),
cloneLVCmd(),
}
p.CommandNotFound = cmdNotFound
p.OnUsageError = onUsageError

klog.Infof("starting csi-lvmplugin-provisioner")
Expand Down
33 changes: 32 additions & 1 deletion deploy/charts/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list", "get", "watch"]
# Encryption at rest: the external-provisioner sidecar resolves the
# csi.storage.k8s.io/provisioner-secret-name/-namespace referenced by an
# encrypted StorageClass. With ${pvc.namespace} templating the secret can live
# in any namespace, so this must be cluster-wide (matches Longhorn's chart).
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["storage.k8s.io"]
resources: ["csinodes"]
verbs: ["get", "list", "watch"]
Expand Down Expand Up @@ -50,6 +57,30 @@ roleRef:
name: harvester-csi-driver-lvm
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: harvester-csi-driver-lvm-snapshot-locations
namespace: {{ .Release.Namespace }}
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: harvester-csi-driver-lvm-snapshot-locations
namespace: {{ .Release.Namespace }}
subjects:
- kind: ServiceAccount
name: harvester-csi-driver-lvm
namespace: {{ .Release.Namespace }}
roleRef:
kind: Role
name: harvester-csi-driver-lvm-snapshot-locations
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
Expand Down Expand Up @@ -91,4 +122,4 @@ roleRef:
subjects:
- kind: ServiceAccount
name: harvester-csi-driver-lvm-webhook
namespace: {{ .Release.Namespace }}
namespace: {{ .Release.Namespace }}
71 changes: 71 additions & 0 deletions examples/pre-existing-volume-snapshot-source.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
apiVersion: snapshot.storage.k8s.io/v1
deletionPolicy: Retain
driver: lvm.driver.harvesterhci.io
kind: VolumeSnapshotClass
metadata:
name: lvm-snapshot-retain
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
annotations:
cdi.harvesterhci.io/storageProfileCloneStrategy: snapshot
cdi.harvesterhci.io/storageProfileVolumeModeAccessModes: '{"Block":["ReadWriteOnce"]}'
cdi.harvesterhci.io/storageProfileVolumeSnapshotClass: lvm-snapshot-retain
name: lvm-pre-existing-demo
parameters:
type: dm-thin
vgName: VOLUME_GROUP_NAME
provisioner: lvm.driver.harvesterhci.io
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: false
allowedTopologies:
- matchLabelExpressions:
- key: topology.lvm.csi/node
values:
- NODE_THAT_HAS_THE_VOLUME_GROUP
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pre-existing-source-pvc
namespace: default
spec:
storageClassName: lvm-pre-existing-demo
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
volumeMode: Block
---
apiVersion: v1
kind: Pod
metadata:
name: pre-existing-source-pod
namespace: default
spec:
containers:
- name: ubuntu-jammy-container
image: ubuntu:jammy
securityContext:
privileged: true
command: ["/bin/bash", "-c", "--"]
args: ["while true; do sleep 30; done;"]
volumeDevices:
- devicePath: "/volumes/pre-existing-source-pvc"
name: pre-existing-source-pvc
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.lvm.csi/node
operator: In
values:
- NODE_THAT_HAS_THE_VOLUME_GROUP
volumes:
- name: pre-existing-source-pvc
persistentVolumeClaim:
claimName: pre-existing-source-pvc
55 changes: 55 additions & 0 deletions examples/pre-existing-volume-snapshot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Case 1: Explicit location annotations. Use this for legacy snapshots or
# snapshots created outside this driver when no persisted location record exists.
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotContent
metadata:
name: pre-existing-lvm-snapshot-content
annotations:
lvm.driver.harvesterhci.io/nodeName: NODE_THAT_HAS_THE_LV_SNAPSHOT
lvm.driver.harvesterhci.io/vgName: VOLUME_GROUP_NAME
spec:
deletionPolicy: Delete
driver: lvm.driver.harvesterhci.io
source:
snapshotHandle: SNAPSHOT_NAME_WITHOUT_LVM_PREFIX
volumeSnapshotClassName: lvm-snapshot
volumeSnapshotRef:
name: pre-existing-lvm-snapshot
namespace: default
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: pre-existing-lvm-snapshot
namespace: default
spec:
source:
volumeSnapshotContentName: pre-existing-lvm-snapshot-content
volumeSnapshotClassName: lvm-snapshot
---
# Case 2: No location annotations. This works when the snapshot was originally
# created by this driver and its persisted location record is still available,
# including after deleting a Retain-policy VolumeSnapshotContent.
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotContent
metadata:
name: pre-existing-recorded-lvm-snapshot-content
spec:
deletionPolicy: Delete
driver: lvm.driver.harvesterhci.io
source:
snapshotHandle: SNAPSHOT_HANDLE_FROM_LOCATION_RECORD
volumeSnapshotClassName: lvm-snapshot
volumeSnapshotRef:
name: pre-existing-recorded-lvm-snapshot
namespace: default
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: pre-existing-recorded-lvm-snapshot
namespace: default
spec:
source:
volumeSnapshotContentName: pre-existing-recorded-lvm-snapshot-content
volumeSnapshotClassName: lvm-snapshot
Loading