From a04d5eebe69103712c45791f91724374679de9dc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Nov 2025 18:58:36 +0000 Subject: [PATCH] Refactor to TrueNAS SCALE 25.04+ WebSocket JSON-RPC API only BREAKING CHANGES: - Removed support for all systems except TrueNAS SCALE 25.04+ - Removed SSH-based driver (FreeNASSshDriver) and all SSH functionality - Removed legacy HTTP REST API support (API v1.0 and v2.0) - Removed generic ZFS, Synology, and other non-TrueNAS drivers - Migrated to WebSocket JSON-RPC 2.0 protocol exclusively Changes: - Implemented new WebSocket JSON-RPC client using 'ws' package - Completely rewrote API wrapper (reduced from 4,469 to 468 lines) - Removed all legacy version detection and compatibility code - Updated driver factory to only support truenas-nfs, truenas-iscsi, truenas-nvmeof - Removed ssh2 and axios dependencies, added ws dependency - Deleted 22 example configuration files for unsupported systems - Deleted 15 driver implementation files for unsupported systems The new implementation provides a clean, modern interface to TrueNAS SCALE using versioned JSON-RPC over WebSocket (/api/current endpoint). API methods now use direct JSON-RPC calls: - pool.dataset.* for dataset operations - zfs.snapshot.* for snapshot operations - iscsi.* for iSCSI configuration (to be implemented) - sharing.nfs.* for NFS shares (to be implemented) - nvmet.* for NVMe-oF (to be implemented) --- examples/freenas-iscsi.yaml | 100 - examples/freenas-nfs.yaml | 70 - examples/freenas-smb.yaml | 119 - examples/local-hostpath.yaml | 59 - examples/lustre-client.yaml | 58 - examples/nfs-client.yaml | 57 - examples/node-manual-iscsi-pv.yaml | 40 - examples/node-manual-nfs-pv.yaml | 25 - examples/node-manual-nvmeof-pv.yaml | 26 - examples/node-manual-objectivefs-pv.yaml | 51 - examples/node-manual-smb-pv.yaml | 29 - examples/node-manual.yaml | 2 - examples/objectivefs.yaml | 32 - examples/smb-client.yaml | 57 - examples/synology-iscsi.yaml | 94 - examples/zfs-generic-iscsi.yaml | 89 - examples/zfs-generic-nfs.yaml | 55 - examples/zfs-generic-nvmeof.yaml | 103 - examples/zfs-generic-smb.yaml | 58 - examples/zfs-local-dataset.yaml | 11 - examples/zfs-local-ephemeral-inline.yaml | 12 - examples/zfs-local-zvol.yaml | 13 - package.json | 3 +- src/driver/controller-client-common/index.js | 1433 ---------- src/driver/controller-local-hostpath/index.js | 92 - src/driver/controller-lustre-client/index.js | 31 - src/driver/controller-nfs-client/index.js | 31 - src/driver/controller-objectivefs/index.js | 670 ----- src/driver/controller-smb-client/index.js | 31 - src/driver/controller-synology/http/index.js | 712 ----- src/driver/controller-synology/index.js | 1168 -------- src/driver/controller-zfs-generic/index.js | 1059 ------- src/driver/controller-zfs-local/index.js | 241 -- src/driver/controller-zfs/index.js | 2480 ----------------- src/driver/factory.js | 54 +- src/driver/freenas/http/api.js | 1070 +++---- src/driver/freenas/http/index.js | 510 ++-- src/driver/freenas/ssh.js | 2316 --------------- src/driver/node-manual/index.js | 341 --- .../zfs-local-ephemeral-inline/index.js | 509 ---- src/utils/zfs_ssh_exec_client.js | 248 -- 41 files changed, 672 insertions(+), 13487 deletions(-) delete mode 100644 examples/freenas-iscsi.yaml delete mode 100644 examples/freenas-nfs.yaml delete mode 100644 examples/freenas-smb.yaml delete mode 100644 examples/local-hostpath.yaml delete mode 100644 examples/lustre-client.yaml delete mode 100644 examples/nfs-client.yaml delete mode 100644 examples/node-manual-iscsi-pv.yaml delete mode 100644 examples/node-manual-nfs-pv.yaml delete mode 100644 examples/node-manual-nvmeof-pv.yaml delete mode 100644 examples/node-manual-objectivefs-pv.yaml delete mode 100644 examples/node-manual-smb-pv.yaml delete mode 100644 examples/node-manual.yaml delete mode 100644 examples/objectivefs.yaml delete mode 100644 examples/smb-client.yaml delete mode 100644 examples/synology-iscsi.yaml delete mode 100644 examples/zfs-generic-iscsi.yaml delete mode 100644 examples/zfs-generic-nfs.yaml delete mode 100644 examples/zfs-generic-nvmeof.yaml delete mode 100644 examples/zfs-generic-smb.yaml delete mode 100644 examples/zfs-local-dataset.yaml delete mode 100644 examples/zfs-local-ephemeral-inline.yaml delete mode 100644 examples/zfs-local-zvol.yaml delete mode 100644 src/driver/controller-client-common/index.js delete mode 100644 src/driver/controller-local-hostpath/index.js delete mode 100644 src/driver/controller-lustre-client/index.js delete mode 100644 src/driver/controller-nfs-client/index.js delete mode 100644 src/driver/controller-objectivefs/index.js delete mode 100644 src/driver/controller-smb-client/index.js delete mode 100644 src/driver/controller-synology/http/index.js delete mode 100644 src/driver/controller-synology/index.js delete mode 100644 src/driver/controller-zfs-generic/index.js delete mode 100644 src/driver/controller-zfs-local/index.js delete mode 100644 src/driver/controller-zfs/index.js delete mode 100644 src/driver/freenas/ssh.js delete mode 100644 src/driver/node-manual/index.js delete mode 100644 src/driver/zfs-local-ephemeral-inline/index.js delete mode 100644 src/utils/zfs_ssh_exec_client.js diff --git a/examples/freenas-iscsi.yaml b/examples/freenas-iscsi.yaml deleted file mode 100644 index 6a20b6b..0000000 --- a/examples/freenas-iscsi.yaml +++ /dev/null @@ -1,100 +0,0 @@ -driver: freenas-iscsi -instance_id: -httpConnection: - protocol: http - host: server address - port: 80 - # use only 1 of apiKey or username/password - # if both are present, apiKey is preferred - # apiKey is only available starting in TrueNAS-12 - #apiKey: - username: root - password: - allowInsecure: true - # use apiVersion 2 for TrueNAS-12 and up (will work on 11.x in some scenarios as well) - # leave unset for auto-detection - #apiVersion: 2 -sshConnection: - host: server address - port: 22 - username: root - # use either password or key - password: "" - privateKey: | - -----BEGIN RSA PRIVATE KEY----- - ... - -----END RSA PRIVATE KEY----- -zfs: - # can be used to override defaults if necessary - # the example below is useful for TrueNAS 12 - #cli: - # sudoEnabled: true - # - # leave paths unset for auto-detection - # paths: - # zfs: /usr/local/sbin/zfs - # zpool: /usr/local/sbin/zpool - # sudo: /usr/local/bin/sudo - # chroot: /usr/sbin/chroot - - # can be used to set arbitrary values on the dataset/zvol - # can use handlebars templates with the parameters from the storage class/CO - #datasetProperties: - # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - # "org.freenas:test": "{{ parameters.foo }}" - # "org.freenas:test2": "some value" - - # total volume name (zvol//) length cannot exceed 63 chars - # https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab - # standard volume naming overhead is 46 chars - # datasetParentName should therefore be 17 chars or less when using TrueNAS 12 or below - datasetParentName: tank/k8s/b/vols - # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap - # they may be siblings, but neither should be nested in the other - # do NOT comment this option out even if you don't plan to use snapshots, just leave it with dummy value - detachedSnapshotsDatasetParentName: tanks/k8s/b/snaps - # "" (inherit), lz4, gzip-9, etc - zvolCompression: - # "" (inherit), on, off, verify - zvolDedup: - zvolEnableReservation: false - # 512, 1K, 2K, 4K, 8K, 16K, 64K, 128K default is 16K - zvolBlocksize: -iscsi: - targetPortal: "server[:port]" - # for multipath - targetPortals: [] # [ "server[:port]", "server[:port]", ... ] - # leave empty to omit usage of -I with iscsiadm - interface: - - # MUST ensure uniqueness - # full iqn limit is 223 bytes, plan accordingly - # default is "{{ name }}" - #nameTemplate: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - namePrefix: csi- - nameSuffix: "-clustera" - - # add as many as needed - targetGroups: - # get the correct ID from the "portal" section in the UI - # https://github.com/democratic-csi/democratic-csi/issues/302 - # NOTE: the ID in the UI does NOT always match the ID in the DB, you must use the DB value - - targetGroupPortalGroup: 1 - # get the correct ID from the "initiators" section in the UI - targetGroupInitiatorGroup: 1 - # None, CHAP, or CHAP Mutual - targetGroupAuthType: None - # get the correct ID from the "Authorized Access" section of the UI - # only required if using Chap - targetGroupAuthGroup: - - #extentCommentTemplate: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - extentInsecureTpc: true - extentXenCompat: false - extentDisablePhysicalBlocksize: true - # 512, 1024, 2048, or 4096, - extentBlocksize: 512 - # "" (let FreeNAS decide, currently defaults to SSD), Unknown, SSD, 5400, 7200, 10000, 15000 - extentRpm: "SSD" - # 0-100 (0 == ignore) - extentAvailThreshold: 0 diff --git a/examples/freenas-nfs.yaml b/examples/freenas-nfs.yaml deleted file mode 100644 index 3ed9ec4..0000000 --- a/examples/freenas-nfs.yaml +++ /dev/null @@ -1,70 +0,0 @@ -driver: freenas-nfs -instance_id: -httpConnection: - protocol: http - host: server address - port: 80 - # use only 1 of apiKey or username/password - # if both are present, apiKey is preferred - # apiKey is only available starting in TrueNAS-12 - #apiKey: - username: root - password: - allowInsecure: true - # use apiVersion 2 for TrueNAS-12 and up (will work on 11.x in some scenarios as well) - # leave unset for auto-detection - #apiVersion: 2 -sshConnection: - host: server address - port: 22 - username: root - # use either password or key - password: "" - privateKey: | - -----BEGIN RSA PRIVATE KEY----- - ... - -----END RSA PRIVATE KEY----- -zfs: - # can be used to override defaults if necessary - # the example below is useful for TrueNAS 12 - #cli: - # sudoEnabled: true - # - # leave paths unset for auto-detection - # paths: - # zfs: /usr/local/sbin/zfs - # zpool: /usr/local/sbin/zpool - # sudo: /usr/local/bin/sudo - # chroot: /usr/sbin/chroot - - # can be used to set arbitrary values on the dataset/zvol - # can use handlebars templates with the parameters from the storage class/CO - #datasetProperties: - # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - # "org.freenas:test": "{{ parameters.foo }}" - # "org.freenas:test2": "some value" - - datasetParentName: tank/k8s/a/vols - # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap - # they may be siblings, but neither should be nested in the other - # do NOT comment this option out even if you don't plan to use snapshots, just leave it with dummy value - detachedSnapshotsDatasetParentName: tank/k8s/a/snaps - datasetEnableQuotas: true - datasetEnableReservation: false - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 - #datasetPermissionsAcls: - #- "-m everyone@:full_set:allow" - #- "-m u:kube:full_set:allow" - -nfs: - #shareCommentTemplate: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - shareHost: server address - shareAlldirs: false - shareAllowedHosts: [] - shareAllowedNetworks: [] - shareMaprootUser: root - shareMaprootGroup: wheel - shareMapallUser: "" - shareMapallGroup: "" diff --git a/examples/freenas-smb.yaml b/examples/freenas-smb.yaml deleted file mode 100644 index 8124e17..0000000 --- a/examples/freenas-smb.yaml +++ /dev/null @@ -1,119 +0,0 @@ -driver: freenas-smb -instance_id: -httpConnection: - protocol: http - host: server address - port: 80 - # use only 1 of apiKey or username/password - # if both are present, apiKey is preferred - # apiKey is only available starting in TrueNAS-12 - #apiKey: - username: root - password: - allowInsecure: true - # use apiVersion 2 for TrueNAS-12 and up (will work on 11.x in some scenarios as well) - # leave unset for auto-detection - #apiVersion: 2 -sshConnection: - host: server address - port: 22 - username: root - # use either password or key - password: "" - privateKey: | - -----BEGIN RSA PRIVATE KEY----- - ... - -----END RSA PRIVATE KEY----- -zfs: - # can be used to override defaults if necessary - # the example below is useful for TrueNAS 12 - #cli: - # sudoEnabled: true - # - # leave paths unset for auto-detection - # paths: - # zfs: /usr/local/sbin/zfs - # zpool: /usr/local/sbin/zpool - # sudo: /usr/local/bin/sudo - # chroot: /usr/sbin/chroot - - # can be used to set arbitrary values on the dataset/zvol - # can use handlebars templates with the parameters from the storage class/CO - #datasetProperties: - # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - # "org.freenas:test": "{{ parameters.foo }}" - # "org.freenas:test2": "some value" - - datasetProperties: - aclmode: restricted - aclinherit: passthrough - acltype: nfsv4 - casesensitivity: insensitive - - datasetParentName: tank/k8s/a/vols - # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap - # they may be siblings, but neither should be nested in the other - # do NOT comment this option out even if you don't plan to use snapshots, just leave it with dummy value - detachedSnapshotsDatasetParentName: tank/k8s/a/snaps - datasetEnableQuotas: true - datasetEnableReservation: false - datasetPermissionsMode: "0770" - - # as appropriate create a dedicated user for smb connections - # and set this - datasetPermissionsUser: 65534 - datasetPermissionsGroup: 65534 - - # CORE - #datasetPermissionsAclsBinary: setfacl - - # SCALE - #datasetPermissionsAclsBinary: nfs4xdr_setfacl - - # if using a user other than guest/nobody comment the 'everyone@' acl - # and uncomment the appropriate block below - datasetPermissionsAcls: - - "-m everyone@:full_set:fd:allow" - - # CORE - # in CORE you cannot have multiple entries for the same principle - # or said differently, they are declarative so using -m will replace - # whatever the current value is for the principle rather than adding a - # entry in the acl list - #- "-m g:builtin_users:full_set:fd:allow" - #- "-m group@:modify_set:fd:allow" - #- "-m owner@:full_set:fd:allow" - - # SCALE - # https://www.truenas.com/community/threads/get-setfacl-on-scale-with-nfsv4-acls.95231/ - # -s replaces everything - # so we put this in specific order to mimic the defaults of SCALE when using the api - #- -s group:builtin_users:full_set:fd:allow - #- -a group:builtin_users:modify_set:fd:allow - #- -a group@:modify_set:fd:allow - #- -a owner@:full_set:fd:allow - -smb: - shareHost: server address - nameTemplate: "" - namePrefix: "" - nameSuffix: "" - - # if any of the shareFoo parameters do not work with your version of FreeNAS - # simply comment the param (and use the configuration template if necessary) - - shareAuxiliaryConfigurationTemplate: | - #guest ok = yes - #guest only = yes - shareHome: false - shareAllowedHosts: [] - shareDeniedHosts: [] - #shareDefaultPermissions: true - shareGuestOk: false - #shareGuestOnly: true - #shareShowHiddenFiles: true - shareRecycleBin: true - shareBrowsable: false - shareAccessBasedEnumeration: true - shareTimeMachine: false - #shareStorageTask: diff --git a/examples/local-hostpath.yaml b/examples/local-hostpath.yaml deleted file mode 100644 index e566381..0000000 --- a/examples/local-hostpath.yaml +++ /dev/null @@ -1,59 +0,0 @@ -driver: local-hostpath -instance_id: -local-hostpath: - # generally shareBasePath and controllerBasePath should be the same for this - # driver, this path should be mounted into the csi-driver container - shareBasePath: "/var/lib/csi-local-hostpath" - controllerBasePath: "/var/lib/csi-local-hostpath" - dirPermissionsMode: "0777" - dirPermissionsUser: 0 - dirPermissionsGroup: 0 - snapshots: - # can create multiple snapshot classes each with a parameters.driver value which - # overrides the default, a single install can use all 3 simultaneously if desired - # - # available options: - # - filecopy = rsync/cp - # - restic - # - kopia - # - default_driver: filecopy - - # snapshot hostname will be set to the csiDriver.name value, in the case - # of local-hostpath the node name will be appended - # it is assumed that the repo has been created beforehand - restic: - global_flags: [] - # - --insecure-tls - - # these are added to snapshots, but are NOT used for querying/selectors by democratic-csi - # it is *HIGHLY* recommended to set the instance_id parameter when using restic, it should be a universally unique ID for every deployment - # host will be set to csi driver name - tags: [] - # - foobar - # - baz=bar - - # automatically prune when a snapshot is deleted - prune: true - - # at a minimum RESTIC_PASSWORD and RESTIC_REPOSITORY must be set, additionally - # any relevant env vars for connecting to RESTIC_REPOSITORY should be set - env: {} - # RESTIC_PASSWORD - # RESTIC_REPOSITORY - # AWS_ACCESS_KEY_ID= - # AWS_SECRET_ACCESS_KEY= - # B2_ACCOUNT_ID= - # B2_ACCOUNT_KEY= - - # snapshot hostname will be set to the csiDriver.name value, in the case - # of local-hostpath the node name will be appended - # it is assumed that the repo has been created beforehand - kopia: - # kopia repository status -t -s - config_token: - global_flags: [] - # : - tags: [] - # - "foobar:true" - env: {} diff --git a/examples/lustre-client.yaml b/examples/lustre-client.yaml deleted file mode 100644 index 4bf7e89..0000000 --- a/examples/lustre-client.yaml +++ /dev/null @@ -1,58 +0,0 @@ -driver: lustre-client -instance_id: -lustre: - # [:] - shareHost: server address - shareBasePath: "/some/path" - # shareHost:shareBasePath should be mounted at this location in the controller container - controllerBasePath: "/storage" - dirPermissionsMode: "0777" - dirPermissionsUser: root - dirPermissionsGroup: wheel - snapshots: - # can create multiple snapshot classes each with a parameters.driver value which - # overrides the default, a single install can use all 3 simultaneously if desired - # - # available options: - # - filecopy = rsync/cp - # - restic - # - kopia - # - default_driver: filecopy - - # snapshot hostname will be set to the csiDriver.name value, in the case - # it is assumed that the repo has been created beforehand - restic: - global_flags: [] - # - --insecure-tls - - # these are added to snapshots, but are NOT used for querying/selectors by democratic-csi - # it is *HIGHLY* recommended to set the instance_id parameter when using restic, it should be a universally unique ID for every deployment - # host will be set to csi driver name - tags: [] - # - foobar - # - baz=bar - - # automatically prune when a snapshot is deleted - prune: true - - # at a minimum RESTIC_PASSWORD and RESTIC_REPOSITORY must be set, additionally - # any relevant env vars for connecting to RESTIC_REPOSITORY should be set - env: {} - # RESTIC_PASSWORD - # RESTIC_REPOSITORY - # AWS_ACCESS_KEY_ID= - # AWS_SECRET_ACCESS_KEY= - # B2_ACCOUNT_ID= - # B2_ACCOUNT_KEY= - - # backup hostname will be set to the csiDriver.name value, in the case - # it is assumed that the repo has been created beforehand - kopia: - # kopia repository status -t -s - config_token: - global_flags: [] - # : - tags: [] - # - "foobar:true" - env: {} diff --git a/examples/nfs-client.yaml b/examples/nfs-client.yaml deleted file mode 100644 index 5b49e55..0000000 --- a/examples/nfs-client.yaml +++ /dev/null @@ -1,57 +0,0 @@ -driver: nfs-client -instance_id: -nfs: - shareHost: server address - shareBasePath: "/some/path" - # shareHost:shareBasePath should be mounted at this location in the controller container - controllerBasePath: "/storage" - dirPermissionsMode: "0777" - dirPermissionsUser: root - dirPermissionsGroup: wheel - snapshots: - # can create multiple snapshot classes each with a parameters.driver value which - # overrides the default, a single install can use all 3 simultaneously if desired - # - # available options: - # - filecopy = rsync/cp - # - restic - # - kopia - # - default_driver: filecopy - - # snapshot hostname will be set to the csiDriver.name value, in the case - # it is assumed that the repo has been created beforehand - restic: - global_flags: [] - # - --insecure-tls - - # these are added to snapshots, but are NOT used for querying/selectors by democratic-csi - # it is *HIGHLY* recommended to set the instance_id parameter when using restic, it should be a universally unique ID for every deployment - # host will be set to csi driver name - tags: [] - # - foobar - # - baz=bar - - # automatically prune when a snapshot is deleted - prune: true - - # at a minimum RESTIC_PASSWORD and RESTIC_REPOSITORY must be set, additionally - # any relevant env vars for connecting to RESTIC_REPOSITORY should be set - env: {} - # RESTIC_PASSWORD - # RESTIC_REPOSITORY - # AWS_ACCESS_KEY_ID= - # AWS_SECRET_ACCESS_KEY= - # B2_ACCOUNT_ID= - # B2_ACCOUNT_KEY= - - # snapshot hostname will be set to the csiDriver.name value, in the case - # it is assumed that the repo has been created beforehand - kopia: - # kopia repository status -t -s - config_token: - global_flags: [] - # : - tags: [] - # - "foobar:true" - env: {} diff --git a/examples/node-manual-iscsi-pv.yaml b/examples/node-manual-iscsi-pv.yaml deleted file mode 100644 index 8e94871..0000000 --- a/examples/node-manual-iscsi-pv.yaml +++ /dev/null @@ -1,40 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolume -metadata: - name: iscsi-manual -spec: - capacity: - storage: 1Gi - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain - mountOptions: [] - csi: - driver: org.democratic-csi.node-manual - readOnly: false - # can be ext4 or xfs - fsType: ext4 - volumeHandle: unique-volumeid # make sure it's a unique id in the cluster - # can be used to handle CHAP - # in the secret create the following keys: - # - # # any arbitrary iscsiadm entries can be add by creating keys starting with node-db. - # # if doing CHAP - # node-db.node.session.auth.authmethod: CHAP - # node-db.node.session.auth.username: foo - # node-db.node.session.auth.password: bar - # - # # if doing mutual CHAP - # node-db.node.session.auth.username_in: baz - # node-db.node.session.auth.password_in: bar - #nodeStageSecretRef: - # name: some name - # namespace: some namespace - volumeAttributes: - portal: - #portals: ,,... - iqn: - lun: - node_attach_driver: iscsi - provisioner_driver: node-manual diff --git a/examples/node-manual-nfs-pv.yaml b/examples/node-manual-nfs-pv.yaml deleted file mode 100644 index b1ab44c..0000000 --- a/examples/node-manual-nfs-pv.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolume -metadata: - name: nfs-manual -spec: - capacity: - storage: 1Gi - accessModes: - - ReadWriteMany - persistentVolumeReclaimPolicy: Retain - mountOptions: - - nfsvers=3 - - nolock - - noatime - csi: - driver: org.democratic-csi.node-manual - readOnly: false - fsType: nfs - volumeHandle: unique-volumeid # make sure it's a unique id in the cluster - volumeAttributes: - server: host or ip - share: /some/share - node_attach_driver: nfs - provisioner_driver: node-manual diff --git a/examples/node-manual-nvmeof-pv.yaml b/examples/node-manual-nvmeof-pv.yaml deleted file mode 100644 index 5d2abf2..0000000 --- a/examples/node-manual-nvmeof-pv.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolume -metadata: - name: nvmeof-manual -spec: - capacity: - storage: 1Gi - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain - mountOptions: [] - csi: - driver: org.democratic-csi.node-manual - readOnly: false - # can be ext4 or xfs - fsType: ext4 - volumeHandle: unique-volumeid # make sure it's a unique id in the cluster - volumeAttributes: - # rdma and fc are also available - transport: tcp://, - #transports: ,,... - nqn: - nsid: - node_attach_driver: "nvmeof" - provisioner_driver: node-manual diff --git a/examples/node-manual-objectivefs-pv.yaml b/examples/node-manual-objectivefs-pv.yaml deleted file mode 100644 index 6cb92ae..0000000 --- a/examples/node-manual-objectivefs-pv.yaml +++ /dev/null @@ -1,51 +0,0 @@ ---- -apiVersion: v1 -kind: Secret -metadata: - name: objectivefs-secret - namespace: kube-system -stringData: - # these can be defined here OR in volumeAttributes - # secrets are processed *before* volumeAttributes and therefore volumeAttributes will take precedence - "env.OBJECTSTORE": "" - "env.ACCESS_KEY": "" - "env.SECRET_KEY": "" - "env.OBJECTIVEFS_PASSPHRASE": "" - # does NOT need admin key appended for node-manual operations - "env.OBJECTIVEFS_LICENSE": "" - "env.ENDPOINT": "" - # ... ---- -apiVersion: v1 -kind: PersistentVolume -metadata: - name: objectivefs-manual -spec: - capacity: - storage: 1Gi - accessModes: - - ReadWriteMany - persistentVolumeReclaimPolicy: Retain - mountOptions: - [] - # https://objectivefs.com/userguide#mount - #- nodiratime - #- noatime - #- fsavail= - csi: - driver: org.democratic-csi.node-manual - readOnly: false - fsType: objectivefs - volumeHandle: unique-volumeid # make sure it's a unique id in the cluster - nodeStageSecretRef: - name: objectivefs-secret - namespace: kube-system - volumeAttributes: - node_attach_driver: objectivefs - provisioner_driver: node-manual - filesystem: "ofs/test" - # these can be defined here OR in the secret referenced above - # secrets are processed *before* volumeAttributes and therefore volumeAttributes will take precedence - #"env.OBJECTSTORE": "minio://" - #"env.ACCESS_KEY": "" - # ... diff --git a/examples/node-manual-smb-pv.yaml b/examples/node-manual-smb-pv.yaml deleted file mode 100644 index 1a44ec0..0000000 --- a/examples/node-manual-smb-pv.yaml +++ /dev/null @@ -1,29 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolume -metadata: - name: smb-manual -spec: - capacity: - storage: 1Gi - accessModes: - - ReadWriteMany - persistentVolumeReclaimPolicy: Retain - mountOptions: - # creds can be entered into the node-stage-secret in the `mount_flags` key - # the value should be: username=foo,password=bar - - username=foo - - password=bar - csi: - driver: org.democratic-csi.node-manual - readOnly: false - fsType: cifs - volumeHandle: unique-volumeid # make sure it's a unique id in the cluster - #nodeStageSecretRef: - # name: some name - # namespace: some namespace - volumeAttributes: - server: host or ip - share: someshare - node_attach_driver: smb - provisioner_driver: node-manual diff --git a/examples/node-manual.yaml b/examples/node-manual.yaml deleted file mode 100644 index 65a40e4..0000000 --- a/examples/node-manual.yaml +++ /dev/null @@ -1,2 +0,0 @@ -driver: node-manual - diff --git a/examples/objectivefs.yaml b/examples/objectivefs.yaml deleted file mode 100644 index bb31d61..0000000 --- a/examples/objectivefs.yaml +++ /dev/null @@ -1,32 +0,0 @@ -driver: objectivefs -objectivefs: - # note, ALL provisioned filesystems will be created in this pool / bucket - # with the same passphrase entered below - # - # in general this pool should be considered as fully managed by democratic-csi - # so a dedicated pool per-cluster / deployment would be best practice - # - pool: ofscsi - cli: - sudoEnabled: false - env: - # NOTE: this must be the license key + admin key - # admin key feature must be activated on your account - # https://objectivefs.com/howto/objectivefs-admin-key-setup - OBJECTIVEFS_LICENSE: - OBJECTSTORE: - ENDPOINT: - SECRET_KEY: - ACCESS_KEY: - # do NOT change this once it has been set and deployed - OBJECTIVEFS_PASSPHRASE: - # ... - -_private: - csi: - volume: - idHash: - # due to 63 char limit on objectivefs fs name, we should - # hash volume names to prevent fs names which are too long - # can be 1 of md5, crc8, crc16, crc32 - strategy: crc32 diff --git a/examples/smb-client.yaml b/examples/smb-client.yaml deleted file mode 100644 index 7a5d4e0..0000000 --- a/examples/smb-client.yaml +++ /dev/null @@ -1,57 +0,0 @@ -driver: smb-client -instance_id: -smb: - shareHost: server address - shareBasePath: "someshare/path" - # shareHost:shareBasePath should be mounted at this location in the controller container - controllerBasePath: "/storage" - dirPermissionsMode: "0777" - dirPermissionsUser: root - dirPermissionsGroup: wheel - snapshots: - # can create multiple snapshot classes each with a parameters.driver value which - # overrides the default, a single install can use all 3 simultaneously if desired - # - # available options: - # - filecopy = rsync/cp - # - restic - # - kopia - # - default_driver: filecopy - - # snapshot hostname will be set to the csiDriver.name value, in the case - # it is assumed that the repo has been created beforehand - restic: - global_flags: [] - # - --insecure-tls - - # these are added to snapshots, but are NOT used for querying/selectors by democratic-csi - # it is *HIGHLY* recommended to set the instance_id parameter when using restic, it should be a universally unique ID for every deployment - # host will be set to csi driver name - tags: [] - # - foobar - # - baz=bar - - # automatically prune when a snapshot is deleted - prune: true - - # at a minimum RESTIC_PASSWORD and RESTIC_REPOSITORY must be set, additionally - # any relevant env vars for connecting to RESTIC_REPOSITORY should be set - env: {} - # RESTIC_PASSWORD - # RESTIC_REPOSITORY - # AWS_ACCESS_KEY_ID= - # AWS_SECRET_ACCESS_KEY= - # B2_ACCOUNT_ID= - # B2_ACCOUNT_KEY= - - # snapshot hostname will be set to the csiDriver.name value, in the case - # it is assumed that the repo has been created beforehand - kopia: - # kopia repository status -t -s - config_token: - global_flags: [] - # : - tags: [] - # - "foobar:true" - env: {} diff --git a/examples/synology-iscsi.yaml b/examples/synology-iscsi.yaml deleted file mode 100644 index e0515b2..0000000 --- a/examples/synology-iscsi.yaml +++ /dev/null @@ -1,94 +0,0 @@ -driver: synology-iscsi -httpConnection: - protocol: http - host: server address - port: 5000 - username: admin - password: password - allowInsecure: true - # should be uniqe across all installs to the same nas - session: "democratic-csi" - serialize: true - -# Choose the DSM volume this driver operates on. The default value is /volume1. -# synology: -# volume: /volume1 - -iscsi: - targetPortal: "server[:port]" - # for multipath - targetPortals: [] # [ "server[:port]", "server[:port]", ... ] - # leave empty to omit usage of -I with iscsiadm - interface: "" - # can be whatever you would like - baseiqn: "iqn.2000-01.com.synology:csi." - - # MUST ensure uniqueness - # full iqn limit is 223 bytes, plan accordingly - namePrefix: "" - nameSuffix: "" - - # documented below are several blocks - # pick the option appropriate for you based on what your backing fs is and desired features - # you do not need to alter dev_attribs under normal circumstances but they may be altered in advanced use-cases - # These options can also be configured per storage-class: - # See https://github.com/democratic-csi/democratic-csi/blob/master/docs/storage-class-parameters.md - lunTemplate: - # can be static value or handlebars template - #description: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - - # btrfs thin provisioning - type: "BLUN" - # tpws = Hardware-assisted zeroing - # caw = Hardware-assisted locking - # 3pc = Hardware-assisted data transfer - # tpu = Space reclamation - # can_snapshot = Snapshot - #dev_attribs: - #- dev_attrib: emulate_tpws - # enable: 1 - #- dev_attrib: emulate_caw - # enable: 1 - #- dev_attrib: emulate_3pc - # enable: 1 - #- dev_attrib: emulate_tpu - # enable: 0 - #- dev_attrib: can_snapshot - # enable: 1 - - # btfs thick provisioning - # only zeroing and locking supported - #type: "BLUN_THICK" - # tpws = Hardware-assisted zeroing - # caw = Hardware-assisted locking - #dev_attribs: - #- dev_attrib: emulate_tpws - # enable: 1 - #- dev_attrib: emulate_caw - # enable: 1 - - # ext4 thinn provisioning UI sends everything with enabled=0 - #type: "THIN" - - # ext4 thin with advanced legacy features set - # can only alter tpu (all others are set as enabled=1) - #type: "ADV" - #dev_attribs: - #- dev_attrib: emulate_tpu - # enable: 1 - - # ext4 thick - # can only alter caw - #type: "FILE" - #dev_attribs: - #- dev_attrib: emulate_caw - # enable: 1 - - lunSnapshotTemplate: - is_locked: true - # https://kb.synology.com/en-me/DSM/tutorial/What_is_file_system_consistent_snapshot - is_app_consistent: true - - targetTemplate: - auth_type: 0 - max_sessions: 0 diff --git a/examples/zfs-generic-iscsi.yaml b/examples/zfs-generic-iscsi.yaml deleted file mode 100644 index af5df37..0000000 --- a/examples/zfs-generic-iscsi.yaml +++ /dev/null @@ -1,89 +0,0 @@ -driver: zfs-generic-iscsi -sshConnection: - host: server address - port: 22 - username: root - # use either password or key - password: "" - privateKey: | - -----BEGIN RSA PRIVATE KEY----- - ... - -----END RSA PRIVATE KEY----- - -zfs: - # can be used to override defaults if necessary - # the example below is useful for TrueNAS 12 - #cli: - # sudoEnabled: true - # paths: - # zfs: /usr/local/sbin/zfs - # zpool: /usr/local/sbin/zpool - # sudo: /usr/local/bin/sudo - # chroot: /usr/sbin/chroot - - # can be used to set arbitrary values on the dataset/zvol - # can use handlebars templates with the parameters from the storage class/CO - #datasetProperties: - # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - # "org.freenas:test": "{{ parameters.foo }}" - # "org.freenas:test2": "some value" - - datasetParentName: tank/k8s/test - # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap - # they may be siblings, but neither should be nested in the other - # do NOT comment this option out even if you don't plan to use snapshots, just leave it with dummy value - detachedSnapshotsDatasetParentName: tanks/k8s/test-snapshots - - # "" (inherit), lz4, gzip-9, etc - zvolCompression: - # "" (inherit), on, off, verify - zvolDedup: - zvolEnableReservation: false - # 512, 1K, 2K, 4K, 8K, 16K, 64K, 128K default is 16K - zvolBlocksize: - -iscsi: - shareStrategy: "targetCli" - - # https://kifarunix.com/how-to-install-and-configure-iscsi-storage-server-on-ubuntu-18-04/ - # https://kifarunix.com/how-install-and-configure-iscsi-storage-server-on-centos-7/ - # https://linuxlasse.net/linux/howtos/ISCSI_and_ZFS_ZVOL - # http://www.linux-iscsi.org/wiki/ISCSI - # https://bugzilla.redhat.com/show_bug.cgi?id=1659195 - # http://atodorov.org/blog/2015/04/07/how-to-configure-iscsi-target-on-red-hat-enterprise-linux-7/ - shareStrategyTargetCli: - #sudoEnabled: true - basename: "iqn.2003-01.org.linux-iscsi.ubuntu-19.x8664" - tpg: - attributes: - # set to 1 to enable CHAP - authentication: 0 - # this is required currently as we do not register all node iqns - # the effective outcome of this is, allow all iqns to connect - generate_node_acls: 1 - cache_dynamic_acls: 1 - # if generate_node_acls is 1 then must turn this off as well (assuming you want write ability) - demo_mode_write_protect: 0 - auth: - # CHAP - #userid: "foo" - #password: "bar" - # mutual CHAP - #mutual_userid: "baz" - #mutual_password: "bar" - block: - attributes: - # set to 1 to enable Thin Provisioning Unmap - emulate_tpu: 0 - targetPortal: "server[:port]" - # for multipath - targetPortals: [] # [ "server[:port]", "server[:port]", ... ] - # leave empty to omit usage of -I with iscsiadm - interface: "" - - # MUST ensure uniqueness - # full iqn limit is 223 bytes, plan accordingly - # default is "{{ name }}" - #nameTemplate: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - namePrefix: - nameSuffix: diff --git a/examples/zfs-generic-nfs.yaml b/examples/zfs-generic-nfs.yaml deleted file mode 100644 index 7b6a2d2..0000000 --- a/examples/zfs-generic-nfs.yaml +++ /dev/null @@ -1,55 +0,0 @@ -driver: zfs-generic-nfs -sshConnection: - host: server address - port: 22 - username: root - # use either password or key - password: "" - privateKey: | - -----BEGIN RSA PRIVATE KEY----- - ... - -----END RSA PRIVATE KEY----- - -zfs: - # can be used to override defaults if necessary - # the example below is useful for TrueNAS 12 - #cli: - # sudoEnabled: true - # paths: - # zfs: /usr/local/sbin/zfs - # zpool: /usr/local/sbin/zpool - # sudo: /usr/local/bin/sudo - # chroot: /usr/sbin/chroot - - # can be used to set arbitrary values on the dataset/zvol - # can use handlebars templates with the parameters from the storage class/CO - #datasetProperties: - # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - # "org.freenas:test": "{{ parameters.foo }}" - # "org.freenas:test2": "some value" - - datasetParentName: tank/k8s/test - # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap - # they may be siblings, but neither should be nested in the other - # do NOT comment this option out even if you don't plan to use snapshots, just leave it with dummy value - detachedSnapshotsDatasetParentName: tanks/k8s/test-snapshots - - datasetEnableQuotas: true - datasetEnableReservation: false - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 - #datasetPermissionsAcls: - #- "-m everyone@:full_set:allow" - #- "-m u:kube:full_set:allow" - -nfs: - # https://docs.oracle.com/cd/E23824_01/html/821-1448/gayne.html - # https://www.hiroom2.com/2016/05/18/ubuntu-16-04-share-zfs-storage-via-nfs-smb/ - shareStrategy: "setDatasetProperties" - shareStrategySetDatasetProperties: - properties: - #sharenfs: "rw,no_subtree_check,no_root_squash" - sharenfs: "on" - # share: "" - shareHost: "server address" diff --git a/examples/zfs-generic-nvmeof.yaml b/examples/zfs-generic-nvmeof.yaml deleted file mode 100644 index b56b3ae..0000000 --- a/examples/zfs-generic-nvmeof.yaml +++ /dev/null @@ -1,103 +0,0 @@ -driver: zfs-generic-nvmeof -sshConnection: - host: server address - port: 22 - username: root - # use either password or key - password: "" - privateKey: | - -----BEGIN RSA PRIVATE KEY----- - ... - -----END RSA PRIVATE KEY----- - -zfs: - # can be used to override defaults if necessary - # the example below is useful for TrueNAS 12 - #cli: - # sudoEnabled: true - # paths: - # zfs: /usr/local/sbin/zfs - # zpool: /usr/local/sbin/zpool - # sudo: /usr/local/bin/sudo - # chroot: /usr/sbin/chroot - - # can be used to set arbitrary values on the dataset/zvol - # can use handlebars templates with the parameters from the storage class/CO - #datasetProperties: - # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - # "org.freenas:test": "{{ parameters.foo }}" - # "org.freenas:test2": "some value" - - datasetParentName: tank/k8s/test - # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap - # they may be siblings, but neither should be nested in the other - detachedSnapshotsDatasetParentName: tanks/k8s/test-snapshots - - # "" (inherit), lz4, gzip-9, etc - zvolCompression: - # "" (inherit), on, off, verify - zvolDedup: - zvolEnableReservation: false - # 512, 1K, 2K, 4K, 8K, 16K, 64K, 128K default is 16K - zvolBlocksize: - -nvmeof: - # these are for the node/client aspect - transports: - - tcp://server:port - #- "tcp://127.0.0.1:4420?host-iface=eth0" - #- "tcp://[2001:123:456::1]:4420" - #- "rdma://127.0.0.1:4420" - #- "fc://[nn-0x203b00a098cbcac6:pn-0x203d00a098cbcac6]" - - # MUST ensure uniqueness - # full iqn limit is 223 bytes, plan accordingly - # default is "{{ name }}" - #nameTemplate: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}" - namePrefix: - nameSuffix: - - shareStrategy: "nvmetCli" - #shareStrategy: "spdkCli" - - # https://documentation.suse.com/es-es/sles/15-SP1/html/SLES-all/cha-nvmeof.html - # https://www.linuxjournal.com/content/data-flash-part-iii-nvme-over-fabrics-using-tcp - # http://git.infradead.org/users/hch/nvmetcli.git - shareStrategyNvmetCli: - #sudoEnabled: true - # /root/.local/bin/nvmetcli - #nvmetcliPath: nvmetcli - # prevent startup race conditions by ensuring the config on disk has been imported - # before we start messing with things - #configIsImportedFilePath: /var/run/nvmet-config-loaded - #configPath: /etc/nvmet/config.json - basename: "nqn.2003-01.org.linux-nvme" - # add more ports here as appropriate if you have multipath - ports: - - "1" - subsystem: - attributes: - allow_any_host: 1 - # not supported yet in nvmetcli - #namespace: - # attributes: - # buffered_io: 1 - - shareStrategySpdkCli: - # spdkcli.py - #spdkcliPath: spdkcli - configPath: /etc/spdk/spdk.json - basename: "nqn.2003-01.org.linux-nvmeof" - bdev: - type: uring - #type: aio - attributes: - block_size: 512 - subsystem: - attributes: - allow_any_host: "true" - listeners: - - trtype: tcp - traddr: server - trsvcid: port - adrfam: ipv4 diff --git a/examples/zfs-generic-smb.yaml b/examples/zfs-generic-smb.yaml deleted file mode 100644 index cbc8f8f..0000000 --- a/examples/zfs-generic-smb.yaml +++ /dev/null @@ -1,58 +0,0 @@ -driver: zfs-generic-smb -sshConnection: - host: server address - port: 22 - username: root - # use either password or key - password: "" - privateKey: | - -----BEGIN RSA PRIVATE KEY----- - ... - -----END RSA PRIVATE KEY----- - -zfs: - # can be used to override defaults if necessary - # the example below is useful for TrueNAS 12 - #cli: - # sudoEnabled: true - # paths: - # zfs: /usr/local/sbin/zfs - # zpool: /usr/local/sbin/zpool - # sudo: /usr/local/bin/sudo - # chroot: /usr/sbin/chroot - - # can be used to set arbitrary values on the dataset/zvol - # can use handlebars templates with the parameters from the storage class/CO - datasetProperties: - #aclmode: restricted - #aclinherit: passthrough - #acltype: nfsv4 - casesensitivity: insensitive - - datasetParentName: tank/k8s/test - # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap - # they may be siblings, but neither should be nested in the other - # do NOT comment this option out even if you don't plan to use snapshots, just leave it with dummy value - detachedSnapshotsDatasetParentName: tanks/k8s/test-snapshots - - datasetEnableQuotas: true - datasetEnableReservation: false - datasetPermissionsMode: "0770" - datasetPermissionsUser: smbroot - datasetPermissionsGroup: smbroot - - #datasetPermissionsAclsBinary: nfs4_setfacl - #datasetPermissionsAcls: - #- "-m everyone@:full_set:allow" - #- -s group@:modify_set:fd:allow - #- -a owner@:full_set:fd:allow - -smb: - # https://docs.oracle.com/cd/E23824_01/html/821-1448/gayne.html - # https://www.hiroom2.com/2016/05/18/ubuntu-16-04-share-zfs-storage-via-nfs-smb/ - shareStrategy: "setDatasetProperties" - shareStrategySetDatasetProperties: - properties: - sharesmb: "on" - # share: "" - shareHost: "server address" diff --git a/examples/zfs-local-dataset.yaml b/examples/zfs-local-dataset.yaml deleted file mode 100644 index fd91346..0000000 --- a/examples/zfs-local-dataset.yaml +++ /dev/null @@ -1,11 +0,0 @@ -driver: zfs-local-dataset - -zfs: - datasetParentName: tank/k8s/local/v - detachedSnapshotsDatasetParentName: tank/k8s/local/s - - datasetProperties: - # key: value - - datasetEnableQuotas: true - datasetEnableReservation: false diff --git a/examples/zfs-local-ephemeral-inline.yaml b/examples/zfs-local-ephemeral-inline.yaml deleted file mode 100644 index 48fb7af..0000000 --- a/examples/zfs-local-ephemeral-inline.yaml +++ /dev/null @@ -1,12 +0,0 @@ -driver: zfs-local-ephemeral-inline -zfs: - #chroot: "/host" - datasetParentName: tank/k8s/inline - properties: - # add any arbitrary properties you want here - #refquota: - # value: 10M - # allowOverride: false # default is to allow inline settings to override - #refreservation: - # value: 5M - # ... diff --git a/examples/zfs-local-zvol.yaml b/examples/zfs-local-zvol.yaml deleted file mode 100644 index e08da1d..0000000 --- a/examples/zfs-local-zvol.yaml +++ /dev/null @@ -1,13 +0,0 @@ -driver: zfs-local-zvol - -zfs: - datasetParentName: tank/k8s/local/v - detachedSnapshotsDatasetParentName: tank/k8s/local/s - - datasetProperties: - # key: value - - zvolCompression: - zvolDedup: - zvolEnableReservation: false - zvolBlocksize: diff --git a/package.json b/package.json index 099fd4e..62341f2 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@grpc/proto-loader": "^0.7.0", "@kubernetes/client-node": "^0.18.0", "async-mutex": "^0.4.0", - "axios": "^1.1.3", "bunyan": "^1.8.15", "crc": "^4.3.2", "fs-extra": "^11.1.0", @@ -32,10 +31,10 @@ "lru-cache": "^7.4.0", "prompt": "^1.2.2", "semver": "^7.3.4", - "ssh2": "^1.1.0", "uri-js": "^4.4.1", "uuid": "^9.0.0", "winston": "^3.6.0", + "ws": "^8.14.0", "yargs": "^17.0.1" }, "devDependencies": { diff --git a/src/driver/controller-client-common/index.js b/src/driver/controller-client-common/index.js deleted file mode 100644 index b2a7272..0000000 --- a/src/driver/controller-client-common/index.js +++ /dev/null @@ -1,1433 +0,0 @@ -const _ = require("lodash"); -const { CsiBaseDriver } = require("../index"); -const { GrpcError, grpc } = require("../../utils/grpc"); -const cp = require("child_process"); -const fs = require("fs"); -const fse = require("fs-extra"); -const Kopia = require("../../utils/kopia").Kopia; -const os = require("os"); -const path = require("path"); -const Restic = require("../../utils/restic").Restic; -const semver = require("semver"); - -const __REGISTRY_NS__ = "ControllerClientCommonDriver"; - -// https://forum.restic.net/t/how-to-prevent-two-restic-tasks-concurrently/6859/5 -const SNAPSHOTS_CUT_IN_FLIGHT = new Set(); -const SNAPSHOTS_RESTORE_IN_FLIGHT = new Set(); -const DEFAULT_SNAPSHOT_DRIVER = "filecopy"; - -/** - * Crude nfs-client driver which simply creates directories to be mounted - * and uses rsync for cloning/snapshots - */ -class ControllerClientCommonDriver extends CsiBaseDriver { - constructor(ctx, options) { - super(...arguments); - - options = options || {}; - options.service = options.service || {}; - options.service.identity = options.service.identity || {}; - options.service.controller = options.service.controller || {}; - options.service.node = options.service.node || {}; - - options.service.identity.capabilities = - options.service.identity.capabilities || {}; - - options.service.controller.capabilities = - options.service.controller.capabilities || {}; - - options.service.node.capabilities = options.service.node.capabilities || {}; - - if (!("service" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity service caps"); - - options.service.identity.capabilities.service = [ - //"UNKNOWN", - "CONTROLLER_SERVICE", - //"VOLUME_ACCESSIBILITY_CONSTRAINTS" - ]; - } - - if (!("volume_expansion" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity volume_expansion caps"); - - options.service.identity.capabilities.volume_expansion = [ - //"UNKNOWN", - //"ONLINE", - //"OFFLINE" - ]; - } - - if (!("rpc" in options.service.controller.capabilities)) { - this.ctx.logger.debug("setting default controller caps"); - - options.service.controller.capabilities.rpc = [ - //"UNKNOWN", - "CREATE_DELETE_VOLUME", - //"PUBLISH_UNPUBLISH_VOLUME", - //"LIST_VOLUMES", - //"GET_CAPACITY", - "CREATE_DELETE_SNAPSHOT", - //"LIST_SNAPSHOTS", - "CLONE_VOLUME", - //"PUBLISH_READONLY", - //"EXPAND_VOLUME", - ]; - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - options.service.controller.capabilities.rpc - .push - //"VOLUME_CONDITION", - //"GET_VOLUME" - (); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.controller.capabilities.rpc.push( - "SINGLE_NODE_MULTI_WRITER" - ); - } - } - - if (!("rpc" in options.service.node.capabilities)) { - this.ctx.logger.debug("setting default node caps"); - - options.service.node.capabilities.rpc = [ - //"UNKNOWN", - "STAGE_UNSTAGE_VOLUME", - "GET_VOLUME_STATS", - //"EXPAND_VOLUME" - ]; - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - //options.service.node.capabilities.rpc.push("VOLUME_CONDITION"); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER"); - /** - * This is for volumes that support a mount time gid such as smb or fat - */ - //options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP"); - } - } - - if (this.ctx.args.csiMode.includes("controller")) { - setInterval(() => { - this.ctx.logger.info("snapshots cut in flight", { - names: [...SNAPSHOTS_CUT_IN_FLIGHT], - count: SNAPSHOTS_CUT_IN_FLIGHT.size, - }); - }, 30 * 1000); - setInterval(() => { - this.ctx.logger.info("snapshots restore in flight", { - names: [...SNAPSHOTS_RESTORE_IN_FLIGHT], - count: SNAPSHOTS_RESTORE_IN_FLIGHT.size, - }); - }, 30 * 1000); - } - } - - getAccessModes(capability) { - let access_modes = _.get(this.options, "csi.access_modes", null); - if (access_modes !== null) { - return access_modes; - } - - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - "MULTI_NODE_READER_ONLY", - "MULTI_NODE_SINGLE_WRITER", - "MULTI_NODE_MULTI_WRITER", - ]; - - if ( - capability.access_type == "block" && - !access_modes.includes("MULTI_NODE_MULTI_WRITER") - ) { - access_modes.push("MULTI_NODE_MULTI_WRITER"); - } - - return access_modes; - } - - assertCapabilities(capabilities) { - const driver = this; - this.ctx.logger.verbose("validating capabilities: %j", capabilities); - - let message = null; - let fs_types = driver.getFsTypes(); - //[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}] - const valid = capabilities.every((capability) => { - if (capability.access_type != "mount") { - message = `invalid access_type ${capability.access_type}`; - return false; - } - - if ( - capability.mount.fs_type && - !fs_types.includes(capability.mount.fs_type) - ) { - message = `invalid fs_type ${capability.mount.fs_type}`; - return false; - } - - if ( - !this.getAccessModes(capability).includes(capability.access_mode.mode) - ) { - message = `invalid access_mode, ${capability.access_mode.mode}`; - return false; - } - - return true; - }); - - return { valid, message }; - } - // share paths - getShareBasePath() { - let config_key = this.getConfigKey(); - let path = this.options[config_key].shareBasePath; - if (!path) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing shareBasePath` - ); - } - - path = path.replace(/\/$/, ""); - if (!path) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing shareBasePath` - ); - } - - return path; - } - - // controller paths - getControllerBasePath() { - let config_key = this.getConfigKey(); - let path = this.options[config_key].controllerBasePath; - if (!path) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing controllerBasePath` - ); - } - - path = path.replace(/\/$/, ""); - if (!path) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing controllerBasePath` - ); - } - - return path; - } - - // path helpers - getVolumeExtraPath() { - return "/v"; - } - - getSnapshotExtraPath() { - return "/s"; - } - - getShareVolumeBasePath() { - return this.getShareBasePath() + this.getVolumeExtraPath(); - } - - getShareSnapshotBasePath() { - return this.getShareBasePath() + this.getSnapshotExtraPath(); - } - - getShareVolumePath(volume_id) { - return this.getShareVolumeBasePath() + "/" + volume_id; - } - - getShareSnapshotPath(snapshot_id) { - return this.getShareSnapshotBasePath() + "/" + snapshot_id; - } - - getControllerVolumeBasePath() { - return this.getControllerBasePath() + this.getVolumeExtraPath(); - } - - getControllerSnapshotBasePath() { - return this.getControllerBasePath() + this.getSnapshotExtraPath(); - } - - getControllerVolumePath(volume_id) { - return this.getControllerVolumeBasePath() + "/" + volume_id; - } - - getControllerSnapshotPath(snapshot_id) { - return this.getControllerSnapshotBasePath() + "/" + snapshot_id; - } - - async getDirectoryUsage(path) { - if (this.getNodeIsWindows()) { - this.ctx.logger.warn("du not implemented on windows"); - return 0; - } else { - let result = await this.exec("du", ["-s", "--block-size=1", path]); - let size = result.stdout.split("\t", 1)[0]; - return size; - } - } - - exec(command, args, options = {}) { - args = args || []; - - let timeout; - let stdout = ""; - let stderr = ""; - - if (options.sudo) { - args.unshift(command); - command = "sudo"; - } - console.log("executing command: %s %s", command, args.join(" ")); - const child = cp.spawn(command, args, options); - - let didTimeout = false; - if (options && options.timeout) { - timeout = setTimeout(() => { - didTimeout = true; - child.kill(options.killSignal || "SIGTERM"); - }, options.timeout); - } - - return new Promise((resolve, reject) => { - child.stdout.on("data", function (data) { - stdout = stdout + data; - }); - - child.stderr.on("data", function (data) { - stderr = stderr + data; - }); - - child.on("close", function (code) { - const result = { code, stdout, stderr }; - if (timeout) { - clearTimeout(timeout); - } - if (code) { - reject(result); - } else { - resolve(result); - } - }); - }); - } - - stripTrailingSlash(s) { - if (s.length > 1) { - return s.replace(/\/$/, ""); - } - - return s; - } - - stripLeadingSlash(s) { - if (s.length > 1) { - return s.replace(/^\/+/, ""); - } - - return s; - } - - async cloneDir(source_path, target_path) { - if (this.getNodeIsWindows()) { - fse.copySync( - this.stripTrailingSlash(source_path), - this.stripTrailingSlash(target_path), - { - overwrite: true, - dereference: true, - preserveTimestamps: true, - //errorOnExist: true, - } - ); - } else { - await this.createDir(target_path); - - /** - * trailing / is important - * rsync -a /mnt/storage/s/foo/ /mnt/storage/v/PVC-111/ - */ - await this.exec("rsync", [ - "-a", - this.stripTrailingSlash(source_path) + "/", - this.stripTrailingSlash(target_path) + "/", - ]); - } - } - - async getAvailableSpaceAtPath(path) { - // https://www.npmjs.com/package/diskusage - // https://www.npmjs.com/package/check-disk-space - if (this.getNodeIsWindows()) { - this.ctx.logger.warn("df not implemented on windows"); - return 0; - } - //df --block-size=1 --output=avail /mnt/storage/ - // Avail - //1481334328 - - const response = await this.exec("df", [ - "--block-size=1", - "--output=avail", - path, - ]); - - return response.stdout.split("\n")[1].trim(); - } - - async createDir(path) { - fs.mkdirSync(path, { - recursive: true, - mode: "755", - }); - } - - async deleteDir(path) { - fs.rmSync(path, { recursive: true, force: true }); - - return; - - /** - * trailing / is important - * rsync -a /mnt/storage/s/foo/ /mnt/storage/v/PVC-111/ - */ - await this.exec("rsync", [ - "-a", - "--delete", - this.stripTrailingSlash(empty_path) + "/", - this.stripTrailingSlash(path) + "/", - ]); - } - - async directoryExists(path) { - let r; - r = fs.existsSync(path); - if (!r) { - return r; - } - - if (!fs.statSync(path).isDirectory()) { - throw new Error(`path [${path}] exists but is not a directory`); - } - - return true; - } - - /** - * Have to be careful with the logic here as the controller could be running - * on win32 for *-client vs local-hostpath - * - * @param {*} path - * @returns - */ - async normalizePath(path) { - if (process.platform == "win32") { - return await this.noramlizePathWin32(path); - } else { - return await this.normalizePathPosix(path); - } - } - - async normalizePathPosix(p) { - return p.replaceAll(path.win32.sep, path.posix.sep); - } - - async noramlizePathWin32(p) { - return p.replaceAll(path.posix.sep, path.win32.sep); - } - - async getResticClient() { - const driver = this; - - return this.ctx.registry.get(`${__REGISTRY_NS__}:restic`, () => { - const config_key = driver.getConfigKey(); - - const restic_env = _.get( - driver.options[config_key], - "snapshots.restic.env", - {} - ); - - const restic_global_flags = _.get( - driver.options[config_key], - "snapshots.restic.global_flags", - [] - ); - const client = new Restic({ - env: restic_env, - logger: driver.ctx.logger, - global_flags: restic_global_flags, - }); - - let hostname = driver.ctx.args.csiName; - if (driver.options.driver == "local-hostpath") { - let nodename = process.env.CSI_NODE_ID || os.hostname(); - hostname = `${hostname}-${nodename}`; - } - - return client; - }); - } - - async getKopiaClient() { - const driver = this; - - return this.ctx.registry.getAsync(`${__REGISTRY_NS__}:kopia`, async () => { - const config_key = driver.getConfigKey(); - - const kopia_env = _.get( - driver.options[config_key], - "snapshots.kopia.env", - {} - ); - - const kopia_global_flags = _.get( - driver.options[config_key], - "snapshots.kopia.global_flags", - [] - ); - const client = new Kopia({ - env: kopia_env, - logger: driver.ctx.logger, - global_flags: kopia_global_flags, - }); - - let hostname = driver.ctx.args.csiName; - if (driver.options.driver == "local-hostpath") { - let nodename = process.env.CSI_NODE_ID || os.hostname(); - hostname = `${hostname}-${nodename}`; - } - - let username = "democratic-csi"; - - await client.repositoryConnect([ - "--override-hostname", - hostname, - "--override-username", - username, - "from-config", - "--token", - _.get(driver.options[config_key], "snapshots.kopia.config_token", ""), - ]); - - //let repositoryStatus = await client.repositoryStatus(); - //console.log(repositoryStatus); - - client.hostname = hostname; - client.username = username; - - return client; - }); - } - - /** - * Create a volume doing in essence the following: - * 1. create directory - * - * Should return 2 parameters - * 1. `server` - host/ip of the nfs server - * 2. `share` - path of the mount shared - * - * @param {*} call - */ - async CreateVolume(call) { - const driver = this; - - const config_key = driver.getConfigKey(); - const volume_id = await driver.getVolumeIdFromCall(call); - const volume_content_source = call.request.volume_content_source; - const instance_id = driver.options.instance_id; - - if ( - call.request.volume_capabilities && - call.request.volume_capabilities.length > 0 - ) { - const result = this.assertCapabilities(call.request.volume_capabilities); - if (result.valid !== true) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); - } - } else { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - "missing volume_capabilities" - ); - } - - if ( - !call.request.capacity_range || - Object.keys(call.request.capacity_range).length === 0 - ) { - call.request.capacity_range = { - required_bytes: 1073741824, // meaningless - }; - } - - if ( - call.request.capacity_range.required_bytes > 0 && - call.request.capacity_range.limit_bytes > 0 && - call.request.capacity_range.required_bytes > - call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.OUT_OF_RANGE, - `required_bytes is greather than limit_bytes` - ); - } - - let capacity_bytes = - call.request.capacity_range.required_bytes || - call.request.capacity_range.limit_bytes; - - if (!capacity_bytes) { - //should never happen, value must be set - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume capacity is required (either required_bytes or limit_bytes)` - ); - } - - // ensure *actual* capacity is not greater than limit - if ( - call.request.capacity_range.limit_bytes && - call.request.capacity_range.limit_bytes > 0 && - capacity_bytes > call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.OUT_OF_RANGE, - `required volume capacity is greater than limit` - ); - } - - const volume_path = driver.getControllerVolumePath(volume_id); - - let response; - let source_path; - //let volume_content_source_snapshot_id; - //let volume_content_source_volume_id; - - // create target dir - await driver.createDir(volume_path); - - // create dataset - if (volume_content_source) { - let snapshot_driver; - let snapshot_id; - - if (volume_content_source.type == "snapshot") { - snapshot_id = volume_content_source.snapshot.snapshot_id; - - // get parsed variant of driver to allow snapshotter to work with all - // drivers simultaneously - const parsed_snapshot_id = new URLSearchParams(snapshot_id); - if (parsed_snapshot_id.get("snapshot_driver")) { - snapshot_id = parsed_snapshot_id.get("snapshot_id"); - snapshot_driver = parsed_snapshot_id.get("snapshot_driver"); - } else { - snapshot_driver = "filecopy"; - } - } - - switch (volume_content_source.type) { - // must be available when adverstising CREATE_DELETE_SNAPSHOT - // simply clone - case "snapshot": - switch (snapshot_driver) { - case "filecopy": - { - source_path = driver.getControllerSnapshotPath(snapshot_id); - - if (!(await driver.directoryExists(source_path))) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid volume_content_source path: ${source_path}` - ); - } - - driver.ctx.logger.debug( - "controller volume source path: %s", - source_path - ); - await driver.cloneDir(source_path, volume_path); - } - break; - case "restic": - { - const restic = await driver.getResticClient(); - - let options = []; - await restic.init(); - - // find snapshot - options = [snapshot_id]; - const snapshots = await restic.snapshots(options); - if (!snapshots.length > 0) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid restic snapshot volume_content_source: ${snapshot_id}` - ); - } - const snapshot = snapshots[snapshots.length - 1]; - - // restore snapshot - // --verify? - options = [ - `${snapshot.id}:${snapshot.paths[0]}`, - "--target", - volume_path, - "--sparse", - "--host", - restic.hostname, - ]; - - // technically same snapshot could be getting restored to multiple volumes simultaneously - // ensure we add target path as part of the key - SNAPSHOTS_RESTORE_IN_FLIGHT.add( - `${snapshot_id}:${volume_path}` - ); - await restic.restore(options).finally(() => { - SNAPSHOTS_RESTORE_IN_FLIGHT.delete( - `${snapshot_id}:${volume_path}` - ); - }); - } - break; - case "kopia": - { - const kopia = await driver.getKopiaClient(); - const snapshot = await kopia.snapshotGet(snapshot_id); - - if (!snapshot) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid restic snapshot volume_content_source: ${snapshot_id}` - ); - } - - /** - * --[no-]write-files-atomically - * --[no-]write-sparse-files - */ - let options = [ - "--write-sparse-files", - snapshot_id, - volume_path, - ]; - await kopia.snapshotRestore(options); - } - break; - default: - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unknown snapthot driver: ${snapshot_driver}` - ); - } - break; - // must be available when adverstising CLONE_VOLUME - // create snapshot first, then clone - case "volume": - source_path = driver.getControllerVolumePath( - volume_content_source.volume.volume_id - ); - - if (!(await driver.directoryExists(source_path))) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid volume_content_source path: ${source_path}` - ); - } - - driver.ctx.logger.debug( - "controller volume source path: %s", - source_path - ); - await driver.cloneDir(source_path, volume_path); - break; - default: - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `invalid volume_content_source type: ${volume_content_source.type}` - ); - } - } - - // set mode - if (this.options[config_key].dirPermissionsMode) { - driver.ctx.logger.verbose( - "setting dir mode to: %s on dir: %s", - this.options[config_key].dirPermissionsMode, - volume_path - ); - fs.chmodSync(volume_path, this.options[config_key].dirPermissionsMode); - } - - // set ownership - if ( - this.options[config_key].dirPermissionsUser || - this.options[config_key].dirPermissionsGroup - ) { - driver.ctx.logger.verbose( - "setting ownership to: %s:%s on dir: %s", - this.options[config_key].dirPermissionsUser, - this.options[config_key].dirPermissionsGroup, - volume_path - ); - if (this.getNodeIsWindows()) { - driver.ctx.logger.warn("chown not implemented on windows"); - } else { - await driver.exec("chown", [ - (this.options[config_key].dirPermissionsUser - ? this.options[config_key].dirPermissionsUser - : "") + - ":" + - (this.options[config_key].dirPermissionsGroup - ? this.options[config_key].dirPermissionsGroup - : ""), - volume_path, - ]); - } - } - - let volume_context = driver.getVolumeContext(volume_id); - - volume_context["provisioner_driver"] = driver.options.driver; - if (driver.options.instance_id) { - volume_context["provisioner_driver_instance_id"] = - driver.options.instance_id; - } - - let accessible_topology; - if (typeof this.getAccessibleTopology === "function") { - accessible_topology = await this.getAccessibleTopology(); - } - - const res = { - volume: { - volume_id, - //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 - capacity_bytes: 0, - content_source: volume_content_source, - volume_context, - accessible_topology, - }, - }; - - return res; - } - - /** - * Delete a volume - * - * Deleting a volume consists of the following steps: - * 1. delete directory - * - * @param {*} call - */ - async DeleteVolume(call) { - const driver = this; - - const volume_id = call.request.volume_id; - - if (!volume_id) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume_id is required` - ); - } - - // deleteStrategy - const delete_strategy = _.get( - driver.options, - "_private.csi.volume.deleteStrategy", - "" - ); - - if (delete_strategy == "retain") { - return {}; - } - - const volume_path = driver.getControllerVolumePath(volume_id); - await driver.deleteDir(volume_path); - - return {}; - } - - /** - * - * @param {*} call - */ - async ControllerExpandVolume(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * TODO: consider volume_capabilities? - * - * @param {*} call - */ - async GetCapacity(call) { - const driver = this; - - if ( - !driver.options.service.controller.capabilities.rpc.includes( - "GET_CAPACITY" - ) - ) { - // really capacity is not used at all with nfs in this fashion, so no reason to enable - // here even though it is technically feasible. - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - if (call.request.volume_capabilities) { - const result = this.assertCapabilities(call.request.volume_capabilities); - - if (result.valid !== true) { - return { available_capacity: 0 }; - } - } - - if (!(await driver.directoryExists(driver.getControllerBasePath()))) { - await driver.createDir(driver.getControllerBasePath()); - } - - const available_capacity = await driver.getAvailableSpaceAtPath( - driver.getControllerBasePath() - ); - return { available_capacity }; - } - - /** - * - * TODO: check capability to ensure not asking about block volumes - * - * @param {*} call - */ - async ListVolumes(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async ListSnapshots(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * Create snapshot is meant to be a syncronous call to 'cut' the snapshot - * in the case of rsync/restic/kopia/etc tooling a 'cut' can take a very - * long time. It was deemed appropriate to continue to wait vs making the - * call async with `ready_to_use` false. - * - * Restic: - * With restic the idea is to keep the tree scoped to each volume. Each - * new snapshot for the same volume should have a parent of the most recently - * cut snapshot for the same volume. Behind the scenes restic is applying - * dedup logic globally in the repo so efficiency should still be extremely - * efficient. - * - * Kopia: - * - * - * https://github.com/container-storage-interface/spec/blob/master/spec.md#createsnapshot - * - * @param {*} call - */ - async CreateSnapshot(call) { - const driver = this; - - const config_key = driver.getConfigKey(); - let snapshot_driver = _.get( - driver.options[config_key], - "snapshots.default_driver", - DEFAULT_SNAPSHOT_DRIVER - ); - - // randomize driver for testing - //if (process.env.CSI_SANITY == "1") { - // call.request.parameters.driver = ["filecopy", "restic", "kopia"].random(); - //} - - if (call.request.parameters.driver) { - snapshot_driver = call.request.parameters.driver; - } - - const instance_id = driver.options.instance_id; - let response; - - // both these are required - const source_volume_id = call.request.source_volume_id; - let name = call.request.name; - - if (!source_volume_id) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot source_volume_id is required` - ); - } - - if (!name) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot name is required` - ); - } - - driver.ctx.logger.verbose("requested snapshot name: %s", name); - - let invalid_chars; - invalid_chars = name.match(/[^a-z0-9_\-:.+]+/gi); - if (invalid_chars) { - invalid_chars = String.prototype.concat( - ...new Set(invalid_chars.join("")) - ); - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot name contains invalid characters: ${invalid_chars}` - ); - } - - // https://stackoverflow.com/questions/32106243/regex-to-remove-all-non-alpha-numeric-and-replace-spaces-with/32106277 - name = name.replace(/[^a-z0-9_\-:.+]+/gi, ""); - - driver.ctx.logger.verbose("cleansed snapshot name: %s", name); - const volume_path = driver.getControllerVolumePath(source_volume_id); - //const volume_path = "/home/thansen/beets/"; - //const volume_path = "/var/lib/docker/"; - - let snapshot_id; - let size_bytes = 0; - let ready_to_use = true; - let snapshot_date = new Date(); - - switch (snapshot_driver) { - case "filecopy": - { - snapshot_id = `${source_volume_id}-${name}`; - const snapshot_path = driver.getControllerSnapshotPath(snapshot_id); - const snapshot_dir_exists = await driver.directoryExists( - snapshot_path - ); - // do NOT overwrite existing snapshot - if (!snapshot_dir_exists) { - SNAPSHOTS_CUT_IN_FLIGHT.add(name); - await driver.cloneDir(volume_path, snapshot_path).finally(() => { - SNAPSHOTS_CUT_IN_FLIGHT.delete(name); - }); - driver.ctx.logger.info( - `filecopy backup finished: snapshot_id=${snapshot_id}, path=${volume_path}` - ); - } else { - driver.ctx.logger.debug( - `filecopy backup already cut: ${snapshot_id}` - ); - } - - size_bytes = await driver.getDirectoryUsage(snapshot_path); - } - break; - case "restic": - { - const restic = await driver.getResticClient(); - const group_by_options = ["--group-by", "host,paths,tags"]; - let snapshot_exists = false; - - // --tag specified multiple times is OR logic, comma-separated is AND logic - let base_tag_option = `source=democratic-csi`; - base_tag_option += `,csi_volume_id=${source_volume_id}`; - if (instance_id) { - base_tag_option += `csi_instance_id=${instance_id}`; - } - - let options = []; - - /** - * ensure repo has been initted - * - * it is expected that at a minimum the following env vars are set - * RESTIC_PASSWORD - * RESTIC_REPOSITORY - */ - options = []; - await restic.init(); - - // see if snapshot already exist with matching tags, etc - options = [ - "--path", - volume_path.replace(/\/$/, ""), - "--host", - restic.hostname, - ]; - - // when searching for existing snapshot include name - response = await restic.snapshots( - options - .concat(group_by_options) - .concat(["--tag", base_tag_option + `,csi_snapshot_name=${name}`]) - ); - - if (response.length > 0) { - snapshot_exists = true; - const snapshot = response[response.length - 1]; - driver.ctx.logger.debug( - `restic backup already cut: ${snapshot.id}` - ); - const stats = await restic.stats([snapshot.id]); - - snapshot_id = snapshot.id; - snapshot_date = new Date(snapshot.time); - size_bytes = stats.total_size; - } - - if (!snapshot_exists) { - // --no-scan do not run scanner to estimate size of backup - // -x, --one-file-system exclude other file systems, don't cross filesystem boundaries and subvolumes - options = [ - "--host", - restic.hostname, - "--one-file-system", - //"--no-scan", - ]; - - // backup with minimal tags to ensure a sane parent for the volume (since tags are included in group_by) - SNAPSHOTS_CUT_IN_FLIGHT.add(name); - response = await restic - .backup( - volume_path, - options - .concat(group_by_options) - .concat(["--tag", base_tag_option]) - ) - .finally(() => { - SNAPSHOTS_CUT_IN_FLIGHT.delete(name); - }); - response.parsed.reverse(); - let summary = response.parsed.find((message) => { - return message.message_type == "summary"; - }); - snapshot_id = summary.snapshot_id; - driver.ctx.logger.info( - `restic backup finished: snapshot_id=${snapshot_id}, path=${volume_path}, total_duration=${ - summary.total_duration | 0 - }s` - ); - const stats = await restic.stats([snapshot_id]); - size_bytes = stats.total_size; - - // only apply these tags at creation, do NOT use for search above etc - let add_tags = `csi_snapshot_name=${name}`; - let config_tags = _.get( - driver.options[config_key], - "snapshots.restic.tags", - [] - ); - - if (config_tags.length > 0) { - add_tags += `,${config_tags.join(",")}`; - } - - await restic.tag([ - "--path", - volume_path.replace(/\/$/, ""), - "--host", - restic.hostname, - "--add", - add_tags, - snapshot_id, - ]); - - // this is ugly, the tag operation should output the new id, so we - // must resort to full query of all snapshots for the volume - // find snapshot using `original` id as adding tags creates a new id - options = [ - "--path", - volume_path.replace(/\/$/, ""), - "--host", - restic.hostname, - ]; - response = await restic.snapshots( - options - .concat(group_by_options) - .concat([ - "--tag", - `${base_tag_option},csi_snapshot_name=${name}`, - ]) - ); - let original_snapshot_id = snapshot_id; - let snapshot = response.find((snapshot) => { - return snapshot.original == original_snapshot_id; - }); - if (!snapshot) { - throw new GrpcError( - grpc.status.UNKNOWN, - `failed to find snapshot post-tag operation: snapshot_id=${original_snapshot_id}` - ); - } - snapshot_id = snapshot.id; - driver.ctx.logger.info( - `restic backup successfully applied additional tags: new_snapshot_id=${snapshot_id}, original_snapshot_id=${original_snapshot_id} path=${volume_path}` - ); - } - } - break; - case "kopia": - { - const kopia = await driver.getKopiaClient(); - let options = []; - - let snapshot_exists = false; - - // --tags specified multiple times means snapshot must contain ALL supplied tags - let tags = []; - tags.push(`source:democratic-csi`); - tags.push(`csi_volume_id:${source_volume_id}`); - if (instance_id) { - tags.push(`csi_instance_id:${instance_id}`); - } - tags.push(`csi_snapshot_name:${name}`); - - options = ["--no-storage-stats", "--no-delta"]; - tags.forEach((item) => { - options.push("--tags", item); - }); - - options.push( - `${kopia.username}@${kopia.hostname}:${volume_path.replace( - /\/$/, - "" - )}` - ); - - response = await kopia.snapshotList(options); - - if (response.length > 0) { - snapshot_exists = true; - const snapshot = response[response.length - 1]; - driver.ctx.logger.debug( - `kopia snapshot already cut: ${snapshot.id}` - ); - - snapshot_id = snapshot.id; - snapshot_date = new Date(snapshot.startTime); // maybe use endTime? - size_bytes = snapshot.stats.totalSize; - } - - if (!snapshot_exists) { - // create snapshot - options = []; - tags.forEach((item) => { - options.push("--tags", item); - }); - options.push(volume_path); - SNAPSHOTS_CUT_IN_FLIGHT.add(name); - response = await kopia.snapshotCreate(options).finally(() => { - SNAPSHOTS_CUT_IN_FLIGHT.delete(name); - }); - - snapshot_id = response.id; - snapshot_date = new Date(response.startTime); // maybe use endTime? - let snapshot_end_date = new Date(response.endTime); - let total_duration = - Math.abs(snapshot_end_date.getTime() - snapshot_date.getTime()) / - 1000; - size_bytes = response.rootEntry.summ.size; - - driver.ctx.logger.info( - `kopia backup finished: snapshot_id=${snapshot_id}, path=${volume_path}, total_duration=${ - total_duration | 0 - }s` - ); - } - } - break; - default: - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unknown snapthot driver: ${snapshot_driver}` - ); - } - - return { - snapshot: { - /** - * The purpose of this field is to give CO guidance on how much space - * is needed to create a volume from this snapshot. - */ - size_bytes, - snapshot_id: new URLSearchParams({ - snapshot_driver, - snapshot_id, - }).toString(), - source_volume_id: source_volume_id, - //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto - creation_time: { - seconds: Math.round(snapshot_date.getTime() / 1000), - nanos: 0, - }, - ready_to_use, - }, - }; - } - - /** - * In addition, if clones have been created from a snapshot, then they must - * be destroyed before the snapshot can be destroyed. - * - * @param {*} call - */ - async DeleteSnapshot(call) { - const driver = this; - - let snapshot_id = call.request.snapshot_id; - let snapshot_driver; - const config_key = driver.getConfigKey(); - const instance_id = driver.options.instance_id; - let response; - - if (!snapshot_id) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot_id is required` - ); - } - - // get parsed variant of driver to allow snapshotter to work with all - // drivers simultaneously - const parsed_snapshot_id = new URLSearchParams(snapshot_id); - if (parsed_snapshot_id.get("snapshot_driver")) { - snapshot_id = parsed_snapshot_id.get("snapshot_id"); - snapshot_driver = parsed_snapshot_id.get("snapshot_driver"); - } else { - snapshot_driver = "filecopy"; - } - - switch (snapshot_driver) { - case "filecopy": - { - const snapshot_path = driver.getControllerSnapshotPath(snapshot_id); - await driver.deleteDir(snapshot_path); - } - break; - case "restic": - { - let prune = _.get( - driver.options[config_key], - "snapshots.restic.prune", - false - ); - - if (typeof prune != "boolean") { - prune = String(prune); - if (["true", "yes", "1"].includes(prune.toLowerCase())) { - prune = true; - } else { - prune = false; - } - } - - const restic = await driver.getResticClient(); - - let options = []; - await restic.init(); - - // we preempt with this check to prevent locking the repo when snapshot does not exist - const snapshot_exists = await restic.snapshot_exists(snapshot_id); - if (snapshot_exists) { - options = []; - if (prune) { - options.push("--prune"); - } - options.push(snapshot_id); - await restic.forget(options); - } - } - break; - case "kopia": - { - const kopia = await driver.getKopiaClient(); - let options = [snapshot_id]; - await kopia.snapshotDelete(options); - } - break; - default: - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unknown snapthot driver: ${snapshot_driver}` - ); - } - - return {}; - } - - /** - * - * @param {*} call - */ - async ValidateVolumeCapabilities(call) { - const driver = this; - - const volume_id = call.request.volume_id; - if (!volume_id) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); - } - - const capabilities = call.request.volume_capabilities; - if (!capabilities || capabilities.length === 0) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); - } - - const volume_path = driver.getControllerVolumePath(volume_id); - if (!(await driver.directoryExists(volume_path))) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid volume_id: ${volume_id}` - ); - } - - const result = this.assertCapabilities(call.request.volume_capabilities); - - if (result.valid !== true) { - return { message: result.message }; - } - - return { - confirmed: { - volume_context: call.request.volume_context, - volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested - parameters: call.request.parameters, - }, - }; - } -} - -module.exports.ControllerClientCommonDriver = ControllerClientCommonDriver; diff --git a/src/driver/controller-local-hostpath/index.js b/src/driver/controller-local-hostpath/index.js deleted file mode 100644 index a838cac..0000000 --- a/src/driver/controller-local-hostpath/index.js +++ /dev/null @@ -1,92 +0,0 @@ -const _ = require("lodash"); - -const { ControllerClientCommonDriver } = require("../controller-client-common"); - -const NODE_TOPOLOGY_KEY_NAME = "org.democratic-csi.topology/node"; - -/** - * Crude local-hostpath driver which simply creates directories to be mounted - * and uses rsync for cloning/snapshots - */ -class ControllerLocalHostpathDriver extends ControllerClientCommonDriver { - constructor(ctx, options) { - const i_caps = _.get( - options, - "service.identity.capabilities.service", - false - ); - - const c_caps = _.get(options, "service.controller.capabilities", false); - super(...arguments); - - if (!i_caps) { - this.ctx.logger.debug("setting local-hostpath identity service caps"); - - options.service.identity.capabilities.service = [ - //"UNKNOWN", - "CONTROLLER_SERVICE", - "VOLUME_ACCESSIBILITY_CONSTRAINTS", - ]; - } - - if (!c_caps) { - this.ctx.logger.debug("setting local-hostpath controller service caps"); - - if ( - !options.service.controller.capabilities.rpc.includes("GET_CAPACITY") - ) { - options.service.controller.capabilities.rpc.push("GET_CAPACITY"); - } - } - } - - getConfigKey() { - return "local-hostpath"; - } - - getVolumeContext(volume_id) { - const driver = this; - return { - node_attach_driver: "hostpath", - path: driver.getShareVolumePath(volume_id), - }; - } - - getFsTypes() { - return []; - } - - /** - * List of topologies associated with the *volume* - * - * @returns array - */ - async getAccessibleTopology() { - const response = await super.NodeGetInfo(...arguments); - return [ - { - segments: { - [NODE_TOPOLOGY_KEY_NAME]: response.node_id, - }, - }, - ]; - } - - /** - * Add node topologies - * - * @param {*} call - * @returns - */ - async NodeGetInfo(call) { - const response = await super.NodeGetInfo(...arguments); - response.accessible_topology = { - segments: { - [NODE_TOPOLOGY_KEY_NAME]: response.node_id, - }, - }; - return response; - } -} - -module.exports.ControllerLocalHostpathDriver = ControllerLocalHostpathDriver; diff --git a/src/driver/controller-lustre-client/index.js b/src/driver/controller-lustre-client/index.js deleted file mode 100644 index f504f2a..0000000 --- a/src/driver/controller-lustre-client/index.js +++ /dev/null @@ -1,31 +0,0 @@ -const { ControllerClientCommonDriver } = require("../controller-client-common"); - -/** - * Crude lustre-client driver which simply creates directories to be mounted - * and uses rsync for cloning/snapshots - */ -class ControllerLustreClientDriver extends ControllerClientCommonDriver { - constructor(ctx, options) { - super(...arguments); - } - - getConfigKey() { - return "lustre"; - } - - getVolumeContext(volume_id) { - const driver = this; - const config_key = driver.getConfigKey(); - return { - node_attach_driver: "lustre", - server: this.options[config_key].shareHost, - share: driver.getShareVolumePath(volume_id), - }; - } - - getFsTypes() { - return ["lustre"]; - } -} - -module.exports.ControllerLustreClientDriver = ControllerLustreClientDriver; diff --git a/src/driver/controller-nfs-client/index.js b/src/driver/controller-nfs-client/index.js deleted file mode 100644 index 13afff0..0000000 --- a/src/driver/controller-nfs-client/index.js +++ /dev/null @@ -1,31 +0,0 @@ -const { ControllerClientCommonDriver } = require("../controller-client-common"); - -/** - * Crude nfs-client driver which simply creates directories to be mounted - * and uses rsync for cloning/snapshots - */ -class ControllerNfsClientDriver extends ControllerClientCommonDriver { - constructor(ctx, options) { - super(...arguments); - } - - getConfigKey() { - return "nfs"; - } - - getVolumeContext(volume_id) { - const driver = this; - const config_key = driver.getConfigKey(); - return { - node_attach_driver: "nfs", - server: this.options[config_key].shareHost, - share: driver.getShareVolumePath(volume_id), - }; - } - - getFsTypes() { - return ["nfs"]; - } -} - -module.exports.ControllerNfsClientDriver = ControllerNfsClientDriver; diff --git a/src/driver/controller-objectivefs/index.js b/src/driver/controller-objectivefs/index.js deleted file mode 100644 index 121570f..0000000 --- a/src/driver/controller-objectivefs/index.js +++ /dev/null @@ -1,670 +0,0 @@ -const _ = require("lodash"); -const { CsiBaseDriver } = require("../index"); -const { GrpcError, grpc } = require("../../utils/grpc"); -const GeneralUtils = require("../../utils/general"); -const { ObjectiveFS } = require("../../utils/objectivefs"); -const semver = require("semver"); -const uuidv4 = require("uuid").v4; - -const __REGISTRY_NS__ = "ControllerZfsLocalDriver"; -const MAX_VOLUME_NAME_LENGTH = 63; - -class ControllerObjectiveFSDriver extends CsiBaseDriver { - constructor(ctx, options) { - super(...arguments); - - options = options || {}; - options.service = options.service || {}; - options.service.identity = options.service.identity || {}; - options.service.controller = options.service.controller || {}; - options.service.node = options.service.node || {}; - - options.service.identity.capabilities = - options.service.identity.capabilities || {}; - - options.service.controller.capabilities = - options.service.controller.capabilities || {}; - - options.service.node.capabilities = options.service.node.capabilities || {}; - - if (!("service" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity service caps"); - - options.service.identity.capabilities.service = [ - //"UNKNOWN", - "CONTROLLER_SERVICE", - //"VOLUME_ACCESSIBILITY_CONSTRAINTS" - ]; - } - - if (!("volume_expansion" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity volume_expansion caps"); - - options.service.identity.capabilities.volume_expansion = [ - //"UNKNOWN", - //"ONLINE", - //"OFFLINE" - ]; - } - - if (!("rpc" in options.service.controller.capabilities)) { - this.ctx.logger.debug("setting default controller caps"); - - options.service.controller.capabilities.rpc = [ - //"UNKNOWN", - "CREATE_DELETE_VOLUME", - //"PUBLISH_UNPUBLISH_VOLUME", - "LIST_VOLUMES", - //"GET_CAPACITY", - //"CREATE_DELETE_SNAPSHOT", - //"LIST_SNAPSHOTS", - //"CLONE_VOLUME", - //"PUBLISH_READONLY", - //"EXPAND_VOLUME", - ]; - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - options.service.controller.capabilities.rpc - .push - //"VOLUME_CONDITION", - //"GET_VOLUME" - (); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.controller.capabilities.rpc.push( - "SINGLE_NODE_MULTI_WRITER" - ); - } - } - - if (!("rpc" in options.service.node.capabilities)) { - this.ctx.logger.debug("setting default node caps"); - - options.service.node.capabilities.rpc = [ - //"UNKNOWN", - "STAGE_UNSTAGE_VOLUME", - "GET_VOLUME_STATS", - //"EXPAND_VOLUME" - ]; - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - //options.service.node.capabilities.rpc.push("VOLUME_CONDITION"); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER"); - /** - * This is for volumes that support a mount time gid such as smb or fat - */ - //options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP"); - } - } - } - - async getObjectiveFSClient() { - const driver = this; - return this.ctx.registry.getAsync( - `${__REGISTRY_NS__}:objectivefsclient`, - async () => { - const options = {}; - options.sudo = _.get( - driver.options, - "objectivefs.cli.sudoEnabled", - false - ); - - options.pool = _.get(driver.options, "objectivefs.pool"); - - return new ObjectiveFS({ - ...options, - env: _.get(driver.options, "objectivefs.env", {}), - }); - } - ); - } - - /** - * - * @returns Array - */ - getAccessModes(capability) { - let access_modes = _.get(this.options, "csi.access_modes", null); - if (access_modes !== null) { - return access_modes; - } - - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - "MULTI_NODE_READER_ONLY", - "MULTI_NODE_SINGLE_WRITER", - "MULTI_NODE_MULTI_WRITER", - ]; - - if ( - capability.access_type == "block" && - !access_modes.includes("MULTI_NODE_MULTI_WRITER") - ) { - access_modes.push("MULTI_NODE_MULTI_WRITER"); - } - - return access_modes; - } - - getFsTypes() { - return ["fuse.objectivefs", "objectivefs"]; - } - - assertCapabilities(capabilities) { - const driver = this; - this.ctx.logger.verbose("validating capabilities: %j", capabilities); - - let message = null; - let fs_types = driver.getFsTypes(); - const valid = capabilities.every((capability) => { - if (capability.access_type != "mount") { - message = `invalid access_type ${capability.access_type}`; - return false; - } - - if ( - capability.mount.fs_type && - !fs_types.includes(capability.mount.fs_type) - ) { - message = `invalid fs_type ${capability.mount.fs_type}`; - return false; - } - - if ( - !this.getAccessModes(capability).includes(capability.access_mode.mode) - ) { - message = `invalid access_mode, ${capability.access_mode.mode}`; - return false; - } - - return true; - }); - - return { valid, message }; - } - - async getVolumeStatus(entry) { - const driver = this; - const object_store = _.get(driver.options, "objectivefs.env.OBJECTSTORE"); - const volume_id = entry.NAME.replace(object_store, "").split("/")[1]; - - if (!!!semver.satisfies(driver.ctx.csiVersion, ">=1.2.0")) { - return; - } - - let abnormal = false; - let message = "OK"; - let volume_status = {}; - - //LIST_VOLUMES_PUBLISHED_NODES - if ( - semver.satisfies(driver.ctx.csiVersion, ">=1.2.0") && - driver.options.service.controller.capabilities.rpc.includes( - "LIST_VOLUMES_PUBLISHED_NODES" - ) - ) { - // TODO: let drivers fill this in - volume_status.published_node_ids = []; - } - - //VOLUME_CONDITION - if ( - semver.satisfies(driver.ctx.csiVersion, ">=1.3.0") && - driver.options.service.controller.capabilities.rpc.includes( - "VOLUME_CONDITION" - ) - ) { - // TODO: let drivers fill ths in - volume_condition = { abnormal, message }; - volume_status.volume_condition = volume_condition; - } - - return volume_status; - } - - async populateCsiVolumeFromData(entry) { - const driver = this; - const object_store = _.get(driver.options, "objectivefs.env.OBJECTSTORE"); - let filesystem = entry.NAME.replace(object_store, ""); - - let volume_content_source; - let volume_context = { - provisioner_driver: driver.options.driver, - node_attach_driver: "objectivefs", - filesystem, - object_store, - "env.OBJECTSTORE": object_store, - }; - - if (driver.options.instance_id) { - volume_context["provisioner_driver_instance_id"] = - driver.options.instance_id; - } - let accessible_topology; - - let volume = { - volume_id: filesystem.split("/")[1], - capacity_bytes: 0, - content_source: volume_content_source, - volume_context, - accessible_topology, - }; - - return volume; - } - - /** - * Ensure sane options are used etc - * true = ready - * false = not ready, but progressiong towards ready - * throw error = faulty setup - * - * @param {*} call - */ - async Probe(call) { - const driver = this; - const pool = _.get(driver.options, "objectivefs.pool"); - const object_store = _.get(driver.options, "objectivefs.env.OBJECTSTORE"); - - if (driver.ctx.args.csiMode.includes("controller")) { - if (!pool) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `objectivefs.pool not configured` - ); - } - - if (!object_store) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `env.OBJECTSTORE not configured` - ); - } - - return { ready: { value: true } }; - } else { - return { ready: { value: true } }; - } - } - - /** - * Create an objectivefs filesystem as a new volume - * - * @param {*} call - */ - async CreateVolume(call) { - const driver = this; - const ofsClient = await driver.getObjectiveFSClient(); - const pool = _.get(driver.options, "objectivefs.pool"); - const object_store = _.get(driver.options, "objectivefs.env.OBJECTSTORE"); - const parameters = call.request.parameters; - - if (!pool) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `objectivefs.pool not configured` - ); - } - - if (!object_store) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `env.OBJECTSTORE not configured` - ); - } - - const context_env = {}; - for (const key in parameters) { - if (key.startsWith("env.")) { - context_env[key] = parameters[key]; - } - } - context_env["env.OBJECTSTORE"] = object_store; - - // filesystem names are always lower-cased by ofs - let volume_id = await driver.getVolumeIdFromCall(call); - let volume_content_source = call.request.volume_content_source; - volume_id = volume_id.toLowerCase(); - const filesystem = `${pool}/${volume_id}`; - - if (volume_id.length >= MAX_VOLUME_NAME_LENGTH) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `derived volume_id ${volume_id} is too long for objectivefs` - ); - } - - if ( - call.request.volume_capabilities && - call.request.volume_capabilities.length > 0 - ) { - const result = this.assertCapabilities(call.request.volume_capabilities); - if (result.valid !== true) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); - } - } else { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - "missing volume_capabilities" - ); - } - - if ( - !call.request.capacity_range || - Object.keys(call.request.capacity_range).length === 0 - ) { - call.request.capacity_range = { - required_bytes: 1073741824, // meaningless - }; - } - - if ( - call.request.capacity_range.required_bytes > 0 && - call.request.capacity_range.limit_bytes > 0 && - call.request.capacity_range.required_bytes > - call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.OUT_OF_RANGE, - `required_bytes is greather than limit_bytes` - ); - } - - let capacity_bytes = - call.request.capacity_range.required_bytes || - call.request.capacity_range.limit_bytes; - - if (!capacity_bytes) { - //should never happen, value must be set - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume capacity is required (either required_bytes or limit_bytes)` - ); - } - - // ensure *actual* capacity is not greater than limit - if ( - call.request.capacity_range.limit_bytes && - call.request.capacity_range.limit_bytes > 0 && - capacity_bytes > call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.OUT_OF_RANGE, - `required volume capacity is greater than limit` - ); - } - - if (volume_content_source) { - //should never happen, cannot clone with this driver - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `cloning is not enabled` - ); - } - - await ofsClient.create({}, filesystem, ["-f"]); - - let volume_context = { - provisioner_driver: driver.options.driver, - node_attach_driver: "objectivefs", - filesystem, - ...context_env, - }; - - if (driver.options.instance_id) { - volume_context["provisioner_driver_instance_id"] = - driver.options.instance_id; - } - - const res = { - volume: { - volume_id, - //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 - capacity_bytes: 0, - content_source: volume_content_source, - volume_context, - }, - }; - - return res; - } - - /** - * Delete a volume - * - * Deleting a volume consists of the following steps: - * 1. delete directory - * - * @param {*} call - */ - async DeleteVolume(call) { - const driver = this; - const ofsClient = await driver.getObjectiveFSClient(); - const pool = _.get(driver.options, "objectivefs.pool"); - - let volume_id = call.request.volume_id; - if (!volume_id) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume_id is required` - ); - } - - // deleteStrategy - const delete_strategy = _.get( - driver.options, - "_private.csi.volume.deleteStrategy", - "" - ); - - if (delete_strategy == "retain") { - return {}; - } - - volume_id = volume_id.toLowerCase(); - const filesystem = `${pool}/${volume_id}`; - await ofsClient.destroy({}, filesystem, []); - - return {}; - } - - /** - * - * @param {*} call - */ - async ControllerExpandVolume(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * TODO: consider volume_capabilities? - * - * @param {*} call - */ - async GetCapacity(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * TODO: check capability to ensure not asking about block volumes - * - * @param {*} call - */ - async ListVolumes(call) { - const driver = this; - const ofsClient = await driver.getObjectiveFSClient(); - const pool = _.get(driver.options, "objectivefs.pool"); - - let entries = []; - let entries_length = 0; - let next_token; - let uuid; - let response; - - const max_entries = call.request.max_entries; - const starting_token = call.request.starting_token; - - // get data from cache and return immediately - if (starting_token) { - let parts = starting_token.split(":"); - uuid = parts[0]; - let start_position = parseInt(parts[1]); - let end_position; - if (max_entries > 0) { - end_position = start_position + max_entries; - } - entries = this.ctx.cache.get(`ListVolumes:result:${uuid}`); - if (entries) { - entries_length = entries.length; - entries = entries.slice(start_position, end_position); - if (max_entries > 0 && end_position > entries_length) { - next_token = `${uuid}:${end_position}`; - } else { - next_token = null; - } - const data = { - entries: entries, - next_token: next_token, - }; - - return data; - } else { - throw new GrpcError( - grpc.status.ABORTED, - `invalid starting_token: ${starting_token}` - ); - } - } - - entries = []; - const list_entries = await ofsClient.list({}); - for (const entry of list_entries) { - if (entry.KIND != "ofs") { - continue; - } - - let volume = await driver.populateCsiVolumeFromData(entry); - if (volume) { - let status = await driver.getVolumeStatus(entry); - entries.push({ - volume, - status, - }); - } - } - - if (max_entries && entries.length > max_entries) { - uuid = uuidv4(); - this.ctx.cache.set(`ListVolumes:result:${uuid}`, entries); - next_token = `${uuid}:${max_entries}`; - entries = entries.slice(0, max_entries); - } - - const data = { - entries: entries, - next_token: next_token, - }; - - return data; - } - - /** - * - * @param {*} call - */ - async ListSnapshots(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async CreateSnapshot(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * In addition, if clones have been created from a snapshot, then they must - * be destroyed before the snapshot can be destroyed. - * - * @param {*} call - */ - async DeleteSnapshot(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async ValidateVolumeCapabilities(call) { - const driver = this; - const ofsClient = await driver.getObjectiveFSClient(); - const pool = _.get(driver.options, "objectivefs.pool"); - - const volume_id = call.request.volume_id; - if (!volume_id) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); - } - - const filesystem = `${pool}/${volume_id}`; - const entries = await ofsClient.list({}, filesystem); - const exists = entries.some((entry) => { - return entry.NAME.endsWith(filesystem) && entry.KIND == "ofs"; - }); - - if (!exists) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid volume_id: ${volume_id}` - ); - } - - const capabilities = call.request.volume_capabilities; - if (!capabilities || capabilities.length === 0) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); - } - - const result = this.assertCapabilities(call.request.volume_capabilities); - - if (result.valid !== true) { - return { message: result.message }; - } - - return { - confirmed: { - volume_context: call.request.volume_context, - volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested - parameters: call.request.parameters, - }, - }; - } -} - -module.exports.ControllerObjectiveFSDriver = ControllerObjectiveFSDriver; diff --git a/src/driver/controller-smb-client/index.js b/src/driver/controller-smb-client/index.js deleted file mode 100644 index 1d9a1ad..0000000 --- a/src/driver/controller-smb-client/index.js +++ /dev/null @@ -1,31 +0,0 @@ -const { ControllerClientCommonDriver } = require("../controller-client-common"); - -/** - * Crude smb-client driver which simply creates directories to be mounted - * and uses rsync for cloning/snapshots - */ -class ControllerSmbClientDriver extends ControllerClientCommonDriver { - constructor(ctx, options) { - super(...arguments); - } - - getConfigKey() { - return "smb"; - } - - getVolumeContext(volume_id) { - const driver = this; - const config_key = driver.getConfigKey(); - return { - node_attach_driver: "smb", - server: this.options[config_key].shareHost, - share: driver.stripLeadingSlash(driver.getShareVolumePath(volume_id)), - }; - } - - getFsTypes() { - return ["cifs"]; - } -} - -module.exports.ControllerSmbClientDriver = ControllerSmbClientDriver; diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js deleted file mode 100644 index a16be94..0000000 --- a/src/driver/controller-synology/http/index.js +++ /dev/null @@ -1,712 +0,0 @@ -const _ = require("lodash"); -const http = require("http"); -const https = require("https"); -const { axios_request, stringify } = require("../../../utils/general"); -const Mutex = require("async-mutex").Mutex; -const { GrpcError, grpc } = require("../../../utils/grpc"); - -const USER_AGENT = "democratic-csi"; -const __REGISTRY_NS__ = "SynologyHttpClient"; - -SYNO_ERRORS = { - 400: { - status: grpc.status.UNAUTHENTICATED, - message: "Failed to authenticate to the Synology DSM.", - }, - 407: { - status: grpc.status.UNAUTHENTICATED, - message: - "IP has been blocked to the Synology DSM due to too many failed attempts.", - }, - 18990002: { - status: grpc.status.RESOURCE_EXHAUSTED, - message: "The synology volume is out of disk space.", - }, - 18990318: { - status: grpc.status.INVALID_ARGUMENT, - message: - "The requested lun type is incompatible with the Synology filesystem.", - }, - 18990538: { - status: grpc.status.ALREADY_EXISTS, - message: "A LUN with this name already exists.", - }, - 18990541: { - status: grpc.status.RESOURCE_EXHAUSTED, - message: "The maximum number of LUNS has been reached.", - }, - 18990542: { - status: grpc.status.RESOURCE_EXHAUSTED, - message: "The maximum number if iSCSI target has been reached.", - }, - 18990708: { - status: grpc.status.INVALID_ARGUMENT, - message: "Bad target auth info.", - }, - 18990744: { - status: grpc.status.ALREADY_EXISTS, - message: "An iSCSI target with this name already exists.", - }, - 18990532: { status: grpc.status.NOT_FOUND, message: "No such snapshot." }, - 18990500: { status: grpc.status.INVALID_ARGUMENT, message: "Bad LUN type" }, - 18990543: { - status: grpc.status.RESOURCE_EXHAUSTED, - message: "Maximum number of snapshots reached.", - }, - 18990635: { - status: grpc.status.INVALID_ARGUMENT, - message: "Invalid ioPolicy.", - }, -}; - -class SynologyError extends GrpcError { - constructor(code, httpCode = undefined) { - super(0, ""); - this.synoCode = code; - this.httpCode = httpCode; - if (code > 0) { - const error = SYNO_ERRORS[code]; - this.code = error && error.status ? error.status : grpc.status.UNKNOWN; - this.message = - error && error.message - ? error.message - : `An unknown error occurred when executing a synology command (code = ${code}).`; - } else { - this.code = grpc.status.UNKNOWN; - this.message = `The synology webserver returned a status code ${httpCode}`; - } - } -} - -class SynologyHttpClient { - constructor(options = {}) { - this.options = JSON.parse(JSON.stringify(options)); - this.logger = console; - this.doLoginMutex = new Mutex(); - this.apiSerializeMutex = new Mutex(); - - if (false) { - setInterval(() => { - console.log("WIPING OUT SYNOLOGY SID"); - this.sid = null; - }, 5 * 1000); - } - } - - getHttpAgent() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:http_agent`, () => { - return new http.Agent({ - keepAlive: true, - maxSockets: Infinity, - rejectUnauthorized: !!!this.options.allowInsecure, - }); - }); - } - - getHttpsAgent() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:https_agent`, () => { - return new https.Agent({ - keepAlive: true, - maxSockets: Infinity, - rejectUnauthorized: !!!this.options.allowInsecure, - }); - }); - } - - log_response(error, response, body, options) { - const cleansedBody = JSON.parse(stringify(body)); - const cleansedOptions = JSON.parse(stringify(options)); - // This function handles arrays and objects - function recursiveCleanse(obj) { - for (const k in obj) { - if (typeof obj[k] == "object" && obj[k] !== null) { - recursiveCleanse(obj[k]); - } else { - if ( - [ - "account", - "passwd", - "username", - "password", - "_sid", - "sid", - "Authorization", - "authorization", - "user", - "mutual_user", - "mutual_password", - ].includes(k) - ) { - obj[k] = "redacted"; - } - } - } - } - recursiveCleanse(cleansedBody); - recursiveCleanse(cleansedOptions); - - delete cleansedOptions.httpAgent; - delete cleansedOptions.httpsAgent; - - this.logger.debug("SYNOLOGY HTTP REQUEST: " + stringify(cleansedOptions)); - this.logger.debug("SYNOLOGY HTTP ERROR: " + error); - this.logger.debug( - "SYNOLOGY HTTP STATUS: " + _.get(response, "statusCode", "") - ); - this.logger.debug( - "SYNOLOGY HTTP HEADERS: " + stringify(_.get(response, "headers", "")) - ); - this.logger.debug("SYNOLOGY HTTP BODY: " + stringify(cleansedBody)); - } - - async do_request(method, path, data = {}, options = {}) { - const client = this; - const isAuth = data.api == "SYNO.API.Auth" && data.method == "login"; - let sid; - let apiMutexRelease; - if (!isAuth) { - sid = await this.doLoginMutex.runExclusive(async () => { - return await this.login(); - }); - } - - const invoke_options = options; - - if (!isAuth) { - if (this.options.serialize) { - apiMutexRelease = await this.apiSerializeMutex.acquire(); - } - } - - return new Promise((resolve, reject) => { - if (!isAuth) { - data._sid = sid; - } - - const options = { - method: method, - url: `${this.options.protocol}://${this.options.host}:${this.options.port}/webapi/${path}`, - headers: { - Accept: "application/json", - "User-Agent": USER_AGENT, - "Content-Type": invoke_options.use_form_encoded - ? "application/x-www-form-urlencoded" - : "application/json", - }, - responseType: "json", - httpAgent: this.getHttpAgent(), - httpsAgent: this.getHttpsAgent(), - timeout: 60 * 1000, - }; - - switch (method) { - case "GET": - let qsData = JSON.parse(JSON.stringify(data)); - for (let p in qsData) { - if (Array.isArray(qsData[p]) || typeof qsData[p] == "boolean") { - qsData[p] = JSON.stringify(qsData[p]); - } - } - options.params = qsData; - break; - default: - if (invoke_options.use_form_encoded) { - options.data = URLSearchParams(data).toString(); - } else { - options.data = data; - } - break; - } - - try { - axios_request(options, function (error, response, body) { - client.log_response(...arguments, options); - - if (error) { - reject(error); - } - - if ( - typeof response.body !== "object" && - response.body !== null && - response.headers["content-type"] && - response.headers["content-type"].includes("application/json") - ) { - response.body = JSON.parse(response.body); - } - - if (response.statusCode > 299 || response.statusCode < 200) { - reject(new SynologyError(null, response.statusCode)); - } - - if (response.body.success === false) { - // remove invalid sid - if (response.body.error.code == 119 && sid == client.sid) { - client.sid = null; - } - reject( - new SynologyError(response.body.error.code, response.statusCode) - ); - } - - resolve(response); - }); - } finally { - if (typeof apiMutexRelease == "function") { - apiMutexRelease(); - } - } - }); - } - - async login() { - if (!this.sid) { - // See https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf - const data = { - api: "SYNO.API.Auth", - version: "6", - method: "login", - account: this.options.username, - passwd: this.options.password, - session: this.options.session, - format: "sid", - }; - - let response = await this.do_request("GET", "auth.cgi", data); - this.sid = response.body.data.sid; - } - - return this.sid; - } - - async GetLuns() { - const lun_list = { - api: "SYNO.Core.ISCSI.LUN", - version: "1", - method: "list", - }; - - let response = await this.do_request("GET", "entry.cgi", lun_list); - return response.body.data.luns; - } - - async GetLunUUIDByName(name) { - const lun_list = { - api: "SYNO.Core.ISCSI.LUN", - version: "1", - method: "list", - }; - - let response = await this.do_request("GET", "entry.cgi", lun_list); - let lun = response.body.data.luns.find((i) => { - return i.name == name; - }); - - if (lun) { - return lun.uuid; - } - } - - async GetLunIDByName(name) { - const lun_list = { - api: "SYNO.Core.ISCSI.LUN", - version: "1", - method: "list", - }; - - let response = await this.do_request("GET", "entry.cgi", lun_list); - let lun = response.body.data.luns.find((i) => { - return i.name == name; - }); - - if (lun) { - return lun.lun_id; - } - } - - async GetLunByID(lun_id) { - const lun_list = { - api: "SYNO.Core.ISCSI.LUN", - version: "1", - method: "list", - }; - - let response = await this.do_request("GET", "entry.cgi", lun_list); - let lun = response.body.data.luns.find((i) => { - return i.lun_id == lun_id; - }); - - if (lun) { - return lun; - } - } - - async GetLunByName(name) { - const lun_list = { - api: "SYNO.Core.ISCSI.LUN", - version: "1", - method: "list", - }; - - let response = await this.do_request("GET", "entry.cgi", lun_list); - let lun = response.body.data.luns.find((i) => { - return i.name == name; - }); - - if (lun) { - return lun; - } - } - - async GetSnapshots() { - let luns = await this.GetLuns(); - let snapshots = []; - - for (let lun of luns) { - const get_snapshot_info = { - api: "SYNO.Core.ISCSI.LUN", - method: "list_snapshot", - version: 1, - src_lun_uuid: JSON.stringify(lun.uuid), - }; - - let response = await this.do_request( - "GET", - "entry.cgi", - get_snapshot_info - ); - - snapshots = snapshots.concat(response.body.data.snapshots); - } - - return snapshots; - } - - async GetSnapshotByLunUUIDAndName(lun_uuid, name) { - const get_snapshot_info = { - api: "SYNO.Core.ISCSI.LUN", - method: "list_snapshot", - version: 1, - src_lun_uuid: JSON.stringify(lun_uuid), - }; - - let response = await this.do_request("GET", "entry.cgi", get_snapshot_info); - - if (response.body.data.snapshots) { - let snapshot = response.body.data.snapshots.find((i) => { - return i.description == name; - }); - - if (snapshot) { - return snapshot; - } - } - } - - async GetSnapshotByLunUUIDAndSnapshotUUID(lun_uuid, snapshot_uuid) { - const get_snapshot_info = { - api: "SYNO.Core.ISCSI.LUN", - method: "list_snapshot", - version: 1, - src_lun_uuid: JSON.stringify(lun_uuid), - }; - - let response = await this.do_request("GET", "entry.cgi", get_snapshot_info); - - if (response.body.data.snapshots) { - let snapshot = response.body.data.snapshots.find((i) => { - return i.uuid == snapshot_uuid; - }); - - if (snapshot) { - return snapshot; - } - } - } - - async DeleteSnapshot(snapshot_uuid) { - const iscsi_snapshot_delete = { - api: "SYNO.Core.ISCSI.LUN", - method: "delete_snapshot", - version: 1, - snapshot_uuid: JSON.stringify(snapshot_uuid), // snapshot_id - deleted_by: "democratic_csi", // ? - }; - - let response = await this.do_request( - "GET", - "entry.cgi", - iscsi_snapshot_delete - ); - // return? - } - - async GetVolumeInfo(volume_path) { - let data = { - api: "SYNO.Core.Storage.Volume", - method: "get", - version: "1", - //volume_path: "/volume1", - volume_path, - }; - - return await this.do_request("GET", "entry.cgi", data); - } - - async GetTargetByTargetID(target_id) { - let targets = await this.ListTargets(); - let target = targets.find((i) => { - return i.target_id == target_id; - }); - - return target; - } - - async GetTargetByIQN(iqn) { - let targets = await this.ListTargets(); - let target = targets.find((i) => { - return i.iqn == iqn; - }); - - return target; - } - - async ListTargets() { - const iscsi_target_list = { - api: "SYNO.Core.ISCSI.Target", - version: "1", - path: "entry.cgi", - method: "list", - additional: '["mapped_lun", "status", "acls", "connected_sessions"]', - }; - let response = await this.do_request("GET", "entry.cgi", iscsi_target_list); - return response.body.data.targets; - } - - async CreateLun(data = {}) { - let response; - let iscsi_lun_create = Object.assign({}, data, { - api: "SYNO.Core.ISCSI.LUN", - version: "1", - method: "create", - }); - - const lun_list = { - api: "SYNO.Core.ISCSI.LUN", - version: "1", - method: "list", - }; - - try { - response = await this.do_request("GET", "entry.cgi", iscsi_lun_create); - return response.body.data.uuid; - } catch (err) { - if (err.synoCode === 18990538) { - response = await this.do_request("GET", "entry.cgi", lun_list); - let lun = response.body.data.luns.find((i) => { - return i.name == iscsi_lun_create.name; - }); - return lun.uuid; - } else { - throw err; - } - } - } - - async MapLun(data = {}) { - // this is mapping from the perspective of the lun - let iscsi_target_map = Object.assign({}, data, { - api: "SYNO.Core.ISCSI.LUN", - method: "map_target", - version: "1", - }); - iscsi_target_map.uuid = JSON.stringify(iscsi_target_map.uuid); - iscsi_target_map.target_ids = JSON.stringify(iscsi_target_map.target_ids); - - // this is mapping from the perspective of the target - /* - iscsi_target_map = Object.assign(data, { - api: "SYNO.Core.ISCSI.Target", - method: "map_lun", - version: "1", - }); - iscsi_target_map.lun_uuids = JSON.stringify(iscsi_target_map.lun_uuids); - */ - - await this.do_request("GET", "entry.cgi", iscsi_target_map); - } - - async DeleteLun(uuid) { - uuid = uuid || ""; - let iscsi_lun_delete = { - api: "SYNO.Core.ISCSI.LUN", - method: "delete", - version: 1, - //uuid: uuid, - uuid: JSON.stringify(""), - uuids: JSON.stringify([uuid]), - //is_soft_feas_ignored: false, - is_soft_feas_ignored: true, - //feasibility_precheck: true, - }; - - await this.do_request("GET", "entry.cgi", iscsi_lun_delete); - } - - async DeleteAllLuns() { - const lun_list = { - api: "SYNO.Core.ISCSI.LUN", - version: "1", - method: "list", - }; - - let response = await this.do_request("GET", "entry.cgi", lun_list); - for (let lun of response.body.data.luns) { - await this.DeleteLun(lun.uuid); - } - } - - async CreateSnapshot(data) { - data = Object.assign({}, data, { - api: "SYNO.Core.ISCSI.LUN", - method: "take_snapshot", - version: 1, - }); - - data.src_lun_uuid = JSON.stringify(data.src_lun_uuid); - - return await this.do_request("GET", "entry.cgi", data); - } - - async CreateTarget(data = {}) { - let iscsi_target_create = Object.assign({}, data, { - api: "SYNO.Core.ISCSI.Target", - version: "1", - method: "create", - }); - - let response; - - try { - response = await this.do_request("GET", "entry.cgi", iscsi_target_create); - - return response.body.data.target_id; - } catch (err) { - if (err.synoCode === 18990744) { - //do lookup - const iscsi_target_list = { - api: "SYNO.Core.ISCSI.Target", - version: "1", - path: "entry.cgi", - method: "list", - additional: '["mapped_lun", "status", "acls", "connected_sessions"]', - }; - - response = await this.do_request("GET", "entry.cgi", iscsi_target_list); - let target = response.body.data.targets.find((i) => { - return i.iqn == iscsi_target_create.iqn; - }); - - if (target) { - return target.target_id; - } else { - throw err; - } - } else { - throw err; - } - } - } - - async DeleteTarget(target_id) { - const iscsi_target_delete = { - api: "SYNO.Core.ISCSI.Target", - method: "delete", - version: "1", - path: "entry.cgi", - }; - - try { - await this.do_request( - "GET", - "entry.cgi", - Object.assign({}, iscsi_target_delete, { - target_id: JSON.stringify(String(target_id || "")), - }) - ); - } catch (err) { - /** - * 18990710 = non-existant - */ - //if (err.synoCode !== 18990710) { - throw err; - //} - } - } - - async ExpandISCSILun(uuid, size) { - const iscsi_lun_extend = { - api: "SYNO.Core.ISCSI.LUN", - method: "set", - version: 1, - }; - - return await this.do_request( - "GET", - "entry.cgi", - Object.assign({}, iscsi_lun_extend, { - uuid: JSON.stringify(uuid), - new_size: size, - }) - ); - } - - async CreateClonedVolume( - src_lun_uuid, - dst_lun_name, - dst_location, - description - ) { - const create_cloned_volume = { - api: "SYNO.Core.ISCSI.LUN", - version: 1, - method: "clone", - src_lun_uuid: JSON.stringify(src_lun_uuid), // src lun uuid - dst_lun_name: dst_lun_name, // dst lun name - dst_location: dst_location, - is_same_pool: true, // always true? string? - clone_type: "democratic-csi", // check - }; - if (description) { - create_cloned_volume.description = description; - } - return await this.do_request("GET", "entry.cgi", create_cloned_volume); - } - - async CreateVolumeFromSnapshot( - src_lun_uuid, - snapshot_uuid, - cloned_lun_name, - description - ) { - const create_volume_from_snapshot = { - api: "SYNO.Core.ISCSI.LUN", - version: 1, - method: "clone_snapshot", - src_lun_uuid: JSON.stringify(src_lun_uuid), // src lun uuid, snapshot id? - snapshot_uuid: JSON.stringify(snapshot_uuid), // shaptop uuid - cloned_lun_name: cloned_lun_name, // cloned lun name - clone_type: "democratic-csi", // check - }; - if (description) { - create_volume_from_snapshot.description = description; - } - return await this.do_request( - "GET", - "entry.cgi", - create_volume_from_snapshot - ); - } -} - -module.exports.SynologyHttpClient = SynologyHttpClient; diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js deleted file mode 100644 index bbae748..0000000 --- a/src/driver/controller-synology/index.js +++ /dev/null @@ -1,1168 +0,0 @@ -const _ = require("lodash"); -const { CsiBaseDriver } = require("../index"); -const GeneralUtils = require("../../utils/general"); -const { GrpcError, grpc } = require("../../utils/grpc"); -const Handlebars = require("handlebars"); -const SynologyHttpClient = require("./http").SynologyHttpClient; -const semver = require("semver"); -const yaml = require("js-yaml"); - -const __REGISTRY_NS__ = "ControllerSynologyDriver"; - -/** - * - * Driver to provision storage on a synology device - * - */ -class ControllerSynologyDriver extends CsiBaseDriver { - constructor(ctx, options) { - super(...arguments); - - options = options || {}; - options.service = options.service || {}; - options.service.identity = options.service.identity || {}; - options.service.controller = options.service.controller || {}; - options.service.node = options.service.node || {}; - - options.service.identity.capabilities = - options.service.identity.capabilities || {}; - - options.service.controller.capabilities = - options.service.controller.capabilities || {}; - - options.service.node.capabilities = options.service.node.capabilities || {}; - - const driverResourceType = this.getDriverResourceType(); - - if (!("service" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity service caps"); - - options.service.identity.capabilities.service = [ - //"UNKNOWN", - "CONTROLLER_SERVICE", - //"VOLUME_ACCESSIBILITY_CONSTRAINTS" - ]; - } - - if (!("volume_expansion" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity volume_expansion caps"); - - options.service.identity.capabilities.volume_expansion = [ - //"UNKNOWN", - "ONLINE", - //"OFFLINE" - ]; - } - - if (!("rpc" in options.service.controller.capabilities)) { - this.ctx.logger.debug("setting default controller caps"); - - options.service.controller.capabilities.rpc = [ - //"UNKNOWN", - "CREATE_DELETE_VOLUME", - //"PUBLISH_UNPUBLISH_VOLUME", - //"LIST_VOLUMES", - "GET_CAPACITY", - "CREATE_DELETE_SNAPSHOT", - //"LIST_SNAPSHOTS", - "CLONE_VOLUME", - //"PUBLISH_READONLY", - "EXPAND_VOLUME", - ]; - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - options.service.controller.capabilities.rpc - .push - //"VOLUME_CONDITION", - //"GET_VOLUME" (would need to properly handle volume_content_source) - (); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.controller.capabilities.rpc.push( - "SINGLE_NODE_MULTI_WRITER" - ); - } - } - - if (!("rpc" in options.service.node.capabilities)) { - this.ctx.logger.debug("setting default node caps"); - - options.service.node.capabilities.rpc = [ - //"UNKNOWN", - "STAGE_UNSTAGE_VOLUME", - "GET_VOLUME_STATS", - //"EXPAND_VOLUME", - ]; - - if (driverResourceType == "volume") { - options.service.node.capabilities.rpc.push("EXPAND_VOLUME"); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - //options.service.node.capabilities.rpc.push("VOLUME_CONDITION"); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER"); - /** - * This is for volumes that support a mount time gid such as smb or fat - */ - //options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP"); - } - } - } - - async getHttpClient() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:http_client`, () => { - return new SynologyHttpClient(this.options.httpConnection); - }); - } - - getDriverResourceType() { - switch (this.options.driver) { - case "synology-nfs": - case "synology-smb": - return "filesystem"; - case "synology-iscsi": - return "volume"; - default: - throw new Error("unknown driver: " + this.ctx.args.driver); - } - } - - getDriverShareType() { - switch (this.options.driver) { - case "synology-nfs": - return "nfs"; - case "synology-smb": - return "smb"; - case "synology-iscsi": - return "iscsi"; - default: - throw new Error("unknown driver: " + this.ctx.args.driver); - } - } - - getObjectFromDevAttribs(list = []) { - if (!list) { - return {}; - } - return list.reduce( - (obj, item) => Object.assign(obj, { [item.dev_attrib]: item.enable }), - {} - ); - } - - getDevAttribsFromObject(obj, keepNull = false) { - return Object.entries(obj) - .filter((e) => keepNull || e[1] != null) - .map((e) => ({ dev_attrib: e[0], enable: e[1] })); - } - - parseParameterYamlData(data, fieldHint = "") { - try { - return yaml.load(data); - } catch { - if (err instanceof yaml.YAMLException) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `${fieldHint} not a valid YAML document.`.trim() - ); - } else { - throw err; - } - } - } - - buildIscsiName(volume_id) { - let iscsiName = volume_id; - if (this.options.iscsi.namePrefix) { - iscsiName = this.options.iscsi.namePrefix + iscsiName; - } - - if (this.options.iscsi.nameSuffix) { - iscsiName += this.options.iscsi.nameSuffix; - } - - return iscsiName.toLowerCase(); - } - - /** - * Returns the value for the 'location' parameter indicating on which volume - * a LUN is to be created. - * - * @param {Object} parameters - Parameters received from a StorageClass - * @param {String} parameters.volume - The volume specified by the StorageClass - * @returns {String} The location of the volume. - */ - getLocation() { - let location = _.get(this.options, "synology.volume"); - if (!location) { - location = "volume1"; - } - if (!location.startsWith("/")) { - location = "/" + location; - } - return location; - } - - getAccessModes(capability) { - let access_modes = _.get(this.options, "csi.access_modes", null); - if (access_modes !== null) { - return access_modes; - } - - const driverResourceType = this.getDriverResourceType(); - switch (driverResourceType) { - case "filesystem": - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - "MULTI_NODE_READER_ONLY", - "MULTI_NODE_SINGLE_WRITER", - "MULTI_NODE_MULTI_WRITER", - ]; - break; - case "volume": - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - "MULTI_NODE_READER_ONLY", - "MULTI_NODE_SINGLE_WRITER", - ]; - break; - } - - if ( - capability.access_type == "block" && - !access_modes.includes("MULTI_NODE_MULTI_WRITER") - ) { - access_modes.push("MULTI_NODE_MULTI_WRITER"); - } - - return access_modes; - } - - assertCapabilities(capabilities) { - const driverResourceType = this.getDriverResourceType(); - this.ctx.logger.verbose("validating capabilities: %j", capabilities); - - let message = null; - //[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}] - const valid = capabilities.every((capability) => { - switch (driverResourceType) { - case "filesystem": - if (capability.access_type != "mount") { - message = `invalid access_type ${capability.access_type}`; - return false; - } - - if ( - capability.mount.fs_type && - !GeneralUtils.default_supported_file_filesystems().includes( - capability.mount.fs_type - ) - ) { - message = `invalid fs_type ${capability.mount.fs_type}`; - return false; - } - - if ( - !this.getAccessModes(capability).includes( - capability.access_mode.mode - ) - ) { - message = `invalid access_mode, ${capability.access_mode.mode}`; - return false; - } - - return true; - case "volume": - if (capability.access_type == "mount") { - if ( - capability.mount.fs_type && - !GeneralUtils.default_supported_block_filesystems().includes( - capability.mount.fs_type - ) - ) { - message = `invalid fs_type ${capability.mount.fs_type}`; - return false; - } - } - - if ( - !this.getAccessModes(capability).includes( - capability.access_mode.mode - ) - ) { - message = `invalid access_mode, ${capability.access_mode.mode}`; - return false; - } - - return true; - } - }); - - return { valid, message }; - } - - /** - * - * CreateVolume - * - * @param {*} call - */ - async CreateVolume(call) { - const driver = this; - const httpClient = await driver.getHttpClient(); - - let volume_id = await driver.getVolumeIdFromCall(call); - let volume_content_source = call.request.volume_content_source; - - if ( - call.request.volume_capabilities && - call.request.volume_capabilities.length > 0 - ) { - const result = this.assertCapabilities(call.request.volume_capabilities); - if (result.valid !== true) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); - } - } else { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - "missing volume_capabilities" - ); - } - - if ( - !call.request.capacity_range || - Object.keys(call.request.capacity_range).length === 0 - ) { - call.request.capacity_range = { - required_bytes: 1073741824, - }; - } - - if ( - call.request.capacity_range.required_bytes > 0 && - call.request.capacity_range.limit_bytes > 0 && - call.request.capacity_range.required_bytes > - call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.OUT_OF_RANGE, - `required_bytes is greather than limit_bytes` - ); - } - - let capacity_bytes = - call.request.capacity_range.required_bytes || - call.request.capacity_range.limit_bytes; - - if (!capacity_bytes) { - //should never happen, value must be set - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume capacity is required (either required_bytes or limit_bytes)` - ); - } - - // ensure *actual* capacity is not greater than limit - if ( - call.request.capacity_range.limit_bytes && - call.request.capacity_range.limit_bytes > 0 && - capacity_bytes > call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.OUT_OF_RANGE, - `required volume capacity is greater than limit` - ); - } - - let volume_context = {}; - const normalizedParameters = driver.getNormalizedParameters( - call.request.parameters - ); - switch (driver.getDriverShareType()) { - case "nfs": - // TODO: create volume here - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - case "smb": - // TODO: create volume here - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - case "iscsi": - let iscsiName = driver.buildIscsiName(volume_id); - let lunTemplate; - let targetTemplate; - let data; - let target; - let lun_mapping; - let lun_uuid; - let existingLun; - - lunTemplate = Object.assign( - {}, - _.get(driver.options, "iscsi.lunTemplate", {}), - driver.parseParameterYamlData( - _.get(normalizedParameters, "lunTemplate", "{}"), - "parameters.lunTemplate" - ), - driver.parseParameterYamlData( - _.get(call.request, "secrets.lunTemplate", "{}"), - "secrets.lunTemplate" - ) - ); - targetTemplate = Object.assign( - {}, - _.get(driver.options, "iscsi.targetTemplate", {}), - driver.parseParameterYamlData( - _.get(normalizedParameters, "targetTemplate", "{}"), - "parameters.targetTemplate" - ), - driver.parseParameterYamlData( - _.get(call.request, "secrets.targetTemplate", "{}"), - "secrets.targetTemplate" - ) - ); - - // render the template for description - if (lunTemplate.description) { - lunTemplate.description = Handlebars.compile(lunTemplate.description)( - { - name: call.request.name, - parameters: call.request.parameters, - csi: { - name: this.ctx.args.csiName, - version: this.ctx.args.csiVersion, - }, - } - ); - } - - // ensure volumes with the same name being requested a 2nd time but with a different size fails - try { - let lun = await httpClient.GetLunByName(iscsiName); - if (lun) { - let size = lun.size; - let check = true; - if (check) { - if ( - (call.request.capacity_range.required_bytes && - call.request.capacity_range.required_bytes > 0 && - size < call.request.capacity_range.required_bytes) || - (call.request.capacity_range.limit_bytes && - call.request.capacity_range.limit_bytes > 0 && - size > call.request.capacity_range.limit_bytes) - ) { - throw new GrpcError( - grpc.status.ALREADY_EXISTS, - `volume has already been created with a different size, existing size: ${size}, required_bytes: ${call.request.capacity_range.required_bytes}, limit_bytes: ${call.request.capacity_range.limit_bytes}` - ); - } - } - } - } catch (err) { - throw err; - } - - if (volume_content_source) { - let src_lun_uuid; - switch (volume_content_source.type) { - case "snapshot": - let parts = volume_content_source.snapshot.snapshot_id.split("/"); - - src_lun_uuid = parts[2]; - if (!src_lun_uuid) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` - ); - } - - let snapshot_uuid = parts[3]; - if (!snapshot_uuid) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` - ); - } - - // This is for backwards compatibility. Previous versions of this driver used the LUN ID instead of the - // UUID. If this is the case we need to get the LUN UUID before we can proceed. - if (!src_lun_uuid.includes("-")) { - src_lun_uuid = await httpClient.GetLunByID(src_lun_uuid).uuid; - } - - let snapshot = - await httpClient.GetSnapshotByLunUUIDAndSnapshotUUID( - src_lun_uuid, - snapshot_uuid - ); - if (!snapshot) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` - ); - } - - existingLun = await httpClient.GetLunByName(iscsiName); - if (!existingLun) { - await httpClient.CreateVolumeFromSnapshot( - src_lun_uuid, - snapshot_uuid, - iscsiName, - lunTemplate.description - ); - } - break; - case "volume": - existingLun = await httpClient.GetLunByName(iscsiName); - if (!existingLun) { - let srcLunName = driver.buildIscsiName( - volume_content_source.volume.volume_id - ); - if (!srcLunName) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid volume_id: ${volume_content_source.volume.volume_id}` - ); - } - - src_lun_uuid = await httpClient.GetLunUUIDByName(srcLunName); - if (!src_lun_uuid) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid volume_id: ${volume_content_source.volume.volume_id}` - ); - } - await httpClient.CreateClonedVolume( - src_lun_uuid, - iscsiName, - driver.getLocation(), - lunTemplate.description - ); - } - break; - default: - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `invalid volume_content_source type: ${volume_content_source.type}` - ); - break; - } - // resize to requested amount - - let lun = await httpClient.GetLunByName(iscsiName); - lun_uuid = lun.uuid; - if (lun.size < capacity_bytes) { - await httpClient.ExpandISCSILun(lun_uuid, capacity_bytes); - } - } else { - // create lun - data = Object.assign({}, lunTemplate, { - name: iscsiName, - location: driver.getLocation(), - size: capacity_bytes, - }); - - lun_uuid = await httpClient.CreateLun(data); - } - - // create target - let iqn = driver.options.iscsi.baseiqn + iscsiName; - data = Object.assign({}, targetTemplate, { - name: iscsiName, - iqn, - }); - - let target_id = await httpClient.CreateTarget(data); - //target = await httpClient.GetTargetByTargetID(target_id); - target = await httpClient.GetTargetByIQN(iqn); - if (!target) { - throw new GrpcError( - grpc.status.UNKNOWN, - `failed to lookup target: ${iqn}` - ); - } - - target_id = target.target_id; - - // check if mapping of lun <-> target already exists - lun_mapping = target.mapped_luns.find((lun) => { - return lun.lun_uuid == lun_uuid; - }); - - // create mapping if not present already - if (!lun_mapping) { - data = { - uuid: lun_uuid, - target_ids: [target_id], - }; - /* - data = { - lun_uuids: [lun_uuid], - target_id: target_id, - }; - */ - await httpClient.MapLun(data); - - // re-retrieve target to ensure proper lun (mapping_index) value is returned - target = await httpClient.GetTargetByTargetID(target_id); - lun_mapping = target.mapped_luns.find((lun) => { - return lun.lun_uuid == lun_uuid; - }); - } - - if (!lun_mapping) { - throw new GrpcError( - grpc.status.UNKNOWN, - `failed to lookup lun_mapping_id` - ); - } - - volume_context = { - node_attach_driver: "iscsi", - portal: driver.options.iscsi.targetPortal || "", - portals: driver.options.iscsi.targetPortals - ? driver.options.iscsi.targetPortals.join(",") - : "", - interface: driver.options.iscsi.interface || "", - iqn, - lun: lun_mapping.mapping_index, - }; - break; - default: - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - } - - volume_context["provisioner_driver"] = driver.options.driver; - if (driver.options.instance_id) { - volume_context["provisioner_driver_instance_id"] = - driver.options.instance_id; - } - - const res = { - volume: { - volume_id, - capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 - content_source: volume_content_source, - volume_context, - }, - }; - - return res; - } - - /** - * DeleteVolume - * - * @param {*} call - */ - async DeleteVolume(call) { - const driver = this; - const httpClient = await driver.getHttpClient(); - - let volume_id = call.request.volume_id; - - if (!volume_id) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume_id is required` - ); - } - - // deleteStrategy - const delete_strategy = _.get( - driver.options, - "_private.csi.volume.deleteStrategy", - "" - ); - - if (delete_strategy == "retain") { - return {}; - } - - let response; - - switch (driver.getDriverShareType()) { - case "nfs": - // TODO: delete volume here - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - case "smb": - // TODO: delete volume here - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - case "iscsi": - //await httpClient.DeleteAllLuns(); - - let iscsiName = driver.buildIscsiName(volume_id); - let iqn = driver.options.iscsi.baseiqn + iscsiName; - - let target = await httpClient.GetTargetByIQN(iqn); - if (target) { - await httpClient.DeleteTarget(target.target_id); - } - - let lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); - if (lun_uuid) { - // this is an async process where a success is returned but delete is happening still behind the scenes - // therefore we continue to search for the lun after delete success call to ensure full deletion - await httpClient.DeleteLun(lun_uuid); - - //let settleEnabled = driver.options.api.lunDelete.settleEnabled; - let settleEnabled = true; - - if (settleEnabled) { - let currentCheck = 0; - - /* - let settleMaxRetries = - driver.options.api.lunDelete.settleMaxRetries || 6; - let settleSeconds = driver.options.api.lunDelete.settleSeconds || 5; - */ - - let settleMaxRetries = 6; - let settleSeconds = 5; - - let waitTimeBetweenChecks = settleSeconds * 1000; - - await GeneralUtils.sleep(waitTimeBetweenChecks); - lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); - - while (currentCheck <= settleMaxRetries && lun_uuid) { - currentCheck++; - await GeneralUtils.sleep(waitTimeBetweenChecks); - lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); - } - - if (lun_uuid) { - throw new GrpcError( - grpc.status.UNKNOWN, - `failed to remove lun: ${lun_uuid}` - ); - } - } - } - break; - default: - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - } - - return {}; - } - - /** - * - * @param {*} call - */ - async ControllerExpandVolume(call) { - const driver = this; - const httpClient = await driver.getHttpClient(); - - let volume_id = call.request.volume_id; - - if (!volume_id) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume_id is required` - ); - } - - let capacity_bytes = - call.request.capacity_range.required_bytes || - call.request.capacity_range.limit_bytes; - if (!capacity_bytes) { - //should never happen, value must be set - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume capacity is required (either required_bytes or limit_bytes)` - ); - } - - if ( - call.request.capacity_range.required_bytes > 0 && - call.request.capacity_range.limit_bytes > 0 && - call.request.capacity_range.required_bytes > - call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `required_bytes is greather than limit_bytes` - ); - } - - // ensure *actual* capacity is not greater than limit - if ( - call.request.capacity_range.limit_bytes && - call.request.capacity_range.limit_bytes > 0 && - capacity_bytes > call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.OUT_OF_RANGE, - `required volume capacity is greater than limit` - ); - } - - let node_expansion_required = false; - let response; - - switch (driver.getDriverShareType()) { - case "nfs": - // TODO: expand volume here - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - case "smb": - // TODO: expand volume here - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - case "iscsi": - node_expansion_required = true; - let iscsiName = driver.buildIscsiName(volume_id); - - response = await httpClient.GetLunUUIDByName(iscsiName); - await httpClient.ExpandISCSILun(response, capacity_bytes); - break; - default: - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - } - - return { - capacity_bytes, - node_expansion_required, - }; - } - - /** - * TODO: consider volume_capabilities? - * - * @param {*} call - */ - async GetCapacity(call) { - const driver = this; - const httpClient = await driver.getHttpClient(); - const location = driver.getLocation(); - - if (!location) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing volume` - ); - } - - if (call.request.volume_capabilities) { - const result = this.assertCapabilities(call.request.volume_capabilities); - - if (result.valid !== true) { - return { available_capacity: 0 }; - } - } - - let response = await httpClient.GetVolumeInfo(location); - return { available_capacity: response.body.data.volume.size_free_byte }; - } - - /** - * - * TODO: check capability to ensure not asking about block volumes - * - * @param {*} call - */ - async ListVolumes(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async ListSnapshots(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async CreateSnapshot(call) { - const driver = this; - const httpClient = await driver.getHttpClient(); - - // both these are required - let source_volume_id = call.request.source_volume_id; - let name = call.request.name; - - if (!source_volume_id) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot source_volume_id is required` - ); - } - - if (!name) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot name is required` - ); - } - - driver.ctx.logger.verbose("requested snapshot name: %s", name); - - let invalid_chars; - invalid_chars = name.match(/[^a-z0-9_\-:.+]+/gi); - if (invalid_chars) { - invalid_chars = String.prototype.concat( - ...new Set(invalid_chars.join("")) - ); - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot name contains invalid characters: ${invalid_chars}` - ); - } - - let iscsiName = driver.buildIscsiName(source_volume_id); - let lun = await httpClient.GetLunByName(iscsiName); - - if (!lun) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `invalid source_volume_id: ${source_volume_id}` - ); - } - - const normalizedParameters = driver.getNormalizedParameters( - call.request.parameters - ); - let lunSnapshotTemplate; - - lunSnapshotTemplate = Object.assign( - {}, - _.get(driver.options, "iscsi.lunSnapshotTemplate", {}), - driver.parseParameterYamlData( - _.get(normalizedParameters, "lunSnapshotTemplate", "{}"), - "parameters.lunSnapshotTemplate" - ), - driver.parseParameterYamlData( - _.get(call.request, "secrets.lunSnapshotTemplate", "{}"), - "secrets.lunSnapshotTemplate" - ) - ); - - // check for other snapshopts with the same name on other volumes and fail as appropriate - // TODO: technically this should only be checking lun/snapshots relevant to this specific install of the driver - // but alas an isolation/namespacing mechanism does not exist in synology - let snapshots = await httpClient.GetSnapshots(); - for (let snapshot of snapshots) { - if (snapshot.description == name && snapshot.parent_uuid != lun.uuid) { - throw new GrpcError( - grpc.status.ALREADY_EXISTS, - `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` - ); - } - } - - // check for already exists - let snapshot; - snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); - if (!snapshot) { - let data = Object.assign({}, lunSnapshotTemplate, { - src_lun_uuid: lun.uuid, - taken_by: "democratic-csi", - description: name, //check - }); - - await httpClient.CreateSnapshot(data); - snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); - - if (!snapshot) { - throw new Error(`failed to create snapshot`); - } - } - - return { - snapshot: { - /** - * The purpose of this field is to give CO guidance on how much space - * is needed to create a volume from this snapshot. - */ - size_bytes: snapshot.total_size, - snapshot_id: `/lun/${lun.uuid}/${snapshot.uuid}`, - source_volume_id: source_volume_id, - //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto - creation_time: { - seconds: snapshot.time, - nanos: 0, - }, - ready_to_use: true, - }, - }; - } - - /** - * In addition, if clones have been created from a snapshot, then they must - * be destroyed before the snapshot can be destroyed. - * - * @param {*} call - */ - async DeleteSnapshot(call) { - // throw new GrpcError( - // grpc.status.UNIMPLEMENTED, - // `operation not supported by driver` - // ); - - const driver = this; - const httpClient = await driver.getHttpClient(); - - const snapshot_id = call.request.snapshot_id; - - if (!snapshot_id) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot_id is required` - ); - } - - let parts = snapshot_id.split("/"); - let lun_uuid = parts[2]; - if (!lun_uuid) { - return {}; - } - - let snapshot_uuid = parts[3]; - if (!snapshot_uuid) { - return {}; - } - - // This is for backwards compatibility. Previous versions of this driver used the LUN ID instead of the UUID. If - // this is the case we need to get the LUN UUID before we can proceed. - if (!lun_uuid.includes("-")) { - lun_uuid = await httpClient.GetLunByID(lun_uuid).uuid; - } - - let snapshot = await httpClient.GetSnapshotByLunUUIDAndSnapshotUUID( - lun_uuid, - snapshot_uuid - ); - - if (snapshot) { - await httpClient.DeleteSnapshot(snapshot.uuid); - } - - return {}; - } - - /** - * - * @param {*} call - */ - async ValidateVolumeCapabilities(call) { - const driver = this; - const httpClient = await driver.getHttpClient(); - - let response; - - const volume_id = call.request.volume_id; - if (!volume_id) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); - } - - const capabilities = call.request.volume_capabilities; - if (!capabilities || capabilities.length === 0) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); - } - - switch (driver.getDriverShareType()) { - case "nfs": - // TODO: expand volume here - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - case "smb": - // TODO: expand volume here - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - case "iscsi": - let iscsiName = driver.buildIscsiName(volume_id); - - response = await httpClient.GetLunUUIDByName(iscsiName); - if (!response) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid volume_id: ${volume_id}` - ); - } - break; - default: - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - break; - } - - const result = this.assertCapabilities(call.request.volume_capabilities); - if (result.valid !== true) { - return { message: result.message }; - } - - return { - confirmed: { - volume_context: call.request.volume_context, - volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested - parameters: call.request.parameters, - }, - }; - } -} - -module.exports.ControllerSynologyDriver = ControllerSynologyDriver; diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js deleted file mode 100644 index 3e4836f..0000000 --- a/src/driver/controller-zfs-generic/index.js +++ /dev/null @@ -1,1059 +0,0 @@ -const _ = require("lodash"); -const { ControllerZfsBaseDriver } = require("../controller-zfs"); -const { GrpcError, grpc } = require("../../utils/grpc"); -const GeneralUtils = require("../../utils/general"); -const LocalCliExecClient = - require("../../utils/zfs_local_exec_client").LocalCliClient; -const SshClient = require("../../utils/zfs_ssh_exec_client").SshClient; -const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); - -const Handlebars = require("handlebars"); - -const ISCSI_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:iscsi_assets_name"; -const NVMEOF_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:nvmeof_assets_name"; -const __REGISTRY_NS__ = "ControllerZfsGenericDriver"; -class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { - getExecClient() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:exec_client`, () => { - if (this.options.sshConnection) { - return new SshClient({ - logger: this.ctx.logger, - connection: this.options.sshConnection, - }); - } else { - return new LocalCliExecClient({ - logger: this.ctx.logger, - }); - } - }); - } - - async getZetabyte() { - return this.ctx.registry.getAsync(`${__REGISTRY_NS__}:zb`, async () => { - const execClient = this.getExecClient(); - const options = {}; - if (this.options.sshConnection) { - options.executor = new ZfsSshProcessManager(execClient); - } else { - options.executor = execClient; - } - options.idempotent = true; - - if ( - this.options.zfs.hasOwnProperty("cli") && - this.options.zfs.cli && - this.options.zfs.cli.hasOwnProperty("paths") - ) { - options.paths = this.options.zfs.cli.paths; - } - - options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); - - if (typeof this.setZetabyteCustomOptions === "function") { - await this.setZetabyteCustomOptions(options); - } - - return new Zetabyte(options); - }); - } - - /** - * cannot make this a storage class parameter as storage class/etc context is *not* sent - * into various calls such as GetControllerCapabilities etc - */ - getDriverZfsResourceType() { - switch (this.options.driver) { - case "zfs-generic-nfs": - case "zfs-generic-smb": - return "filesystem"; - case "zfs-generic-iscsi": - case "zfs-generic-nvmeof": - return "volume"; - default: - throw new Error("unknown driver: " + this.ctx.args.driver); - } - } - - generateSmbShareName(datasetName) { - const driver = this; - - driver.ctx.logger.verbose( - `generating smb share name for dataset: ${datasetName}` - ); - - let name = datasetName || ""; - name = name.replaceAll("/", "_"); - name = name.replaceAll("-", "_"); - - driver.ctx.logger.verbose( - `generated smb share name for dataset (${datasetName}): ${name}` - ); - - return name; - } - - /** - * should create any necessary share resources - * should set the SHARE_VOLUME_CONTEXT_PROPERTY_NAME propery - * - * @param {*} datasetName - */ - async createShare(call, datasetName) { - const driver = this; - const zb = await this.getZetabyte(); - const execClient = this.getExecClient(); - - let properties; - let response; - let share = {}; - let volume_context = {}; - - switch (this.options.driver) { - case "zfs-generic-nfs": - switch (this.options.nfs.shareStrategy) { - case "setDatasetProperties": - for (let key of ["share", "sharenfs"]) { - if ( - this.options.nfs.shareStrategySetDatasetProperties.properties[ - key - ] - ) { - await zb.zfs.set(datasetName, { - [key]: - this.options.nfs.shareStrategySetDatasetProperties - .properties[key], - }); - } - } - - break; - default: - break; - } - - properties = await zb.zfs.get(datasetName, ["mountpoint"]); - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - volume_context = { - node_attach_driver: "nfs", - server: this.options.nfs.shareHost, - share: properties.mountpoint.value, - }; - return volume_context; - - case "zfs-generic-smb": - let share; - switch (this.options.smb.shareStrategy) { - case "setDatasetProperties": - for (let key of ["share", "sharesmb"]) { - if ( - this.options.smb.shareStrategySetDatasetProperties.properties[ - key - ] - ) { - await zb.zfs.set(datasetName, { - [key]: - this.options.smb.shareStrategySetDatasetProperties - .properties[key], - }); - } - } - - share = driver.generateSmbShareName(datasetName); - break; - default: - break; - } - - properties = await zb.zfs.get(datasetName, ["mountpoint"]); - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - volume_context = { - node_attach_driver: "smb", - server: this.options.smb.shareHost, - share, - }; - return volume_context; - - case "zfs-generic-iscsi": { - let basename; - let assetName; - - if (this.options.iscsi.nameTemplate) { - assetName = Handlebars.compile(this.options.iscsi.nameTemplate)({ - name: call.request.name, - parameters: call.request.parameters, - }); - } else { - assetName = zb.helpers.extractLeafName(datasetName); - } - - if (this.options.iscsi.namePrefix) { - assetName = this.options.iscsi.namePrefix + assetName; - } - - if (this.options.iscsi.nameSuffix) { - assetName += this.options.iscsi.nameSuffix; - } - - assetName = assetName.toLowerCase(); - - let extentDiskName = "zvol/" + datasetName; - - /** - * limit is a FreeBSD limitation - * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab - */ - //if (extentDiskName.length > 63) { - // throw new GrpcError( - // grpc.status.FAILED_PRECONDITION, - // `extent disk name cannot exceed 63 characters: ${extentDiskName}` - // ); - //} - - switch (this.options.iscsi.shareStrategy) { - case "targetCli": - basename = this.options.iscsi.shareStrategyTargetCli.basename; - let setAttributesText = ""; - let setAuthText = ""; - let setBlockAttributesText = ""; - - if (this.options.iscsi.shareStrategyTargetCli.block) { - if (this.options.iscsi.shareStrategyTargetCli.block.attributes) { - for (const attributeName in this.options.iscsi - .shareStrategyTargetCli.block.attributes) { - const attributeValue = - this.options.iscsi.shareStrategyTargetCli.block.attributes[ - attributeName - ]; - setBlockAttributesText += "\n"; - setBlockAttributesText += `set attribute ${attributeName}=${attributeValue}`; - } - } - } - - if (this.options.iscsi.shareStrategyTargetCli.tpg) { - if (this.options.iscsi.shareStrategyTargetCli.tpg.attributes) { - for (const attributeName in this.options.iscsi - .shareStrategyTargetCli.tpg.attributes) { - const attributeValue = - this.options.iscsi.shareStrategyTargetCli.tpg.attributes[ - attributeName - ]; - setAttributesText += "\n"; - setAttributesText += `set attribute ${attributeName}=${attributeValue}`; - } - } - - if (this.options.iscsi.shareStrategyTargetCli.tpg.auth) { - for (const attributeName in this.options.iscsi - .shareStrategyTargetCli.tpg.auth) { - const attributeValue = - this.options.iscsi.shareStrategyTargetCli.tpg.auth[ - attributeName - ]; - setAttributesText += "\n"; - setAttributesText += `set auth ${attributeName}=${attributeValue}`; - } - } - } - - await GeneralUtils.retry( - 3, - 2000, - async () => { - await this.targetCliCommand( - ` -# create target -cd /iscsi -create ${basename}:${assetName} - -# setup tpg -cd /iscsi/${basename}:${assetName}/tpg1 -${setAttributesText} -${setAuthText} - -# create extent -cd /backstores/block -create ${assetName} /dev/${extentDiskName} -cd /backstores/block/${assetName} -${setBlockAttributesText} - -# add extent to target/tpg -cd /iscsi/${basename}:${assetName}/tpg1/luns -create /backstores/block/${assetName} -` - ); - }, - { - retryCondition: (err) => { - if (err.stdout && err.stdout.includes("Ran out of input")) { - return true; - } - return false; - }, - } - ); - break; - default: - break; - } - - // iqn = target - let iqn = basename + ":" + assetName; - this.ctx.logger.info("iqn: " + iqn); - - // store this off to make delete process more bullet proof - await zb.zfs.set(datasetName, { - [ISCSI_ASSETS_NAME_PROPERTY_NAME]: assetName, - }); - - volume_context = { - node_attach_driver: "iscsi", - portal: this.options.iscsi.targetPortal || "", - portals: this.options.iscsi.targetPortals - ? this.options.iscsi.targetPortals.join(",") - : "", - interface: this.options.iscsi.interface || "", - iqn: iqn, - lun: 0, - }; - return volume_context; - } - - case "zfs-generic-nvmeof": { - let basename; - let assetName; - - if (this.options.nvmeof.nameTemplate) { - assetName = Handlebars.compile(this.options.nvmeof.nameTemplate)({ - name: call.request.name, - parameters: call.request.parameters, - }); - } else { - assetName = zb.helpers.extractLeafName(datasetName); - } - - if (this.options.nvmeof.namePrefix) { - assetName = this.options.nvmeof.namePrefix + assetName; - } - - if (this.options.nvmeof.nameSuffix) { - assetName += this.options.nvmeof.nameSuffix; - } - - assetName = assetName.toLowerCase(); - - let extentDiskName = "zvol/" + datasetName; - - /** - * limit is a FreeBSD limitation - * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab - */ - //if (extentDiskName.length > 63) { - // throw new GrpcError( - // grpc.status.FAILED_PRECONDITION, - // `extent disk name cannot exceed 63 characters: ${extentDiskName}` - // ); - //} - - let namespace = 1; - - switch (this.options.nvmeof.shareStrategy) { - case "nvmetCli": - { - basename = this.options.nvmeof.shareStrategyNvmetCli.basename; - let savefile = _.get( - this.options, - "nvmeof.shareStrategyNvmetCli.configPath", - "" - ); - if (savefile) { - savefile = `savefile=${savefile}`; - } - let setSubsystemAttributesText = ""; - if (this.options.nvmeof.shareStrategyNvmetCli.subsystem) { - if ( - this.options.nvmeof.shareStrategyNvmetCli.subsystem.attributes - ) { - for (const attributeName in this.options.nvmeof - .shareStrategyNvmetCli.subsystem.attributes) { - const attributeValue = - this.options.nvmeof.shareStrategyNvmetCli.subsystem - .attributes[attributeName]; - setSubsystemAttributesText += "\n"; - setSubsystemAttributesText += `set attr ${attributeName}=${attributeValue}`; - } - } - } - - let portCommands = ""; - this.options.nvmeof.shareStrategyNvmetCli.ports.forEach( - (port) => { - portCommands += ` -cd /ports/${port}/subsystems -create ${basename}:${assetName} -`; - } - ); - - await GeneralUtils.retry( - 3, - 2000, - async () => { - await this.nvmetCliCommand( - ` -# create subsystem -cd /subsystems -create ${basename}:${assetName} -cd ${basename}:${assetName} -${setSubsystemAttributesText} - -# create subsystem namespace -cd namespaces -create ${namespace} -cd ${namespace} -set device path=/dev/${extentDiskName} -enable - -# associate subsystem/target to port(al) -${portCommands} - -saveconfig ${savefile} -` - ); - }, - { - retryCondition: (err) => { - if (err.stdout && err.stdout.includes("Ran out of input")) { - return true; - } - return false; - }, - } - ); - } - break; - - case "spdkCli": - { - basename = this.options.nvmeof.shareStrategySpdkCli.basename; - let bdevAttributesText = ""; - if (this.options.nvmeof.shareStrategySpdkCli.bdev) { - if (this.options.nvmeof.shareStrategySpdkCli.bdev.attributes) { - for (const attributeName in this.options.nvmeof - .shareStrategySpdkCli.bdev.attributes) { - const attributeValue = - this.options.nvmeof.shareStrategySpdkCli.bdev.attributes[ - attributeName - ]; - bdevAttributesText += `${attributeName}=${attributeValue}`; - } - } - } - - let subsystemAttributesText = ""; - if (this.options.nvmeof.shareStrategySpdkCli.subsystem) { - if ( - this.options.nvmeof.shareStrategySpdkCli.subsystem.attributes - ) { - for (const attributeName in this.options.nvmeof - .shareStrategySpdkCli.subsystem.attributes) { - const attributeValue = - this.options.nvmeof.shareStrategySpdkCli.subsystem - .attributes[attributeName]; - subsystemAttributesText += `${attributeName}=${attributeValue}`; - } - } - } - - let listenerCommands = `cd /nvmf/subsystem/${basename}:${assetName}/listen_addresses\n`; - this.options.nvmeof.shareStrategySpdkCli.listeners.forEach( - (listener) => { - let listenerAttributesText = ""; - for (const attributeName in listener) { - const attributeValue = listener[attributeName]; - listenerAttributesText += ` ${attributeName}=${attributeValue} `; - } - listenerCommands += ` -create ${listenerAttributesText} -`; - } - ); - - await GeneralUtils.retry( - 3, - 2000, - async () => { - await this.spdkCliCommand( - ` -# create bdev -cd /bdevs/${this.options.nvmeof.shareStrategySpdkCli.bdev.type} -create filename=/dev/${extentDiskName} name=${basename}:${assetName} ${bdevAttributesText} - -# create subsystem -cd /nvmf/subsystem -create nqn=${basename}:${assetName} ${subsystemAttributesText} -cd ${basename}:${assetName} - -# create namespace -cd /nvmf/subsystem/${basename}:${assetName}/namespaces -create bdev_name=${basename}:${assetName} nsid=${namespace} - -# add listener -${listenerCommands} - -cd / -save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} -` - ); - }, - { - retryCondition: (err) => { - if (err.stdout && err.stdout.includes("Ran out of input")) { - return true; - } - return false; - }, - } - ); - } - break; - - default: - break; - } - - // iqn = target - let nqn = basename + ":" + assetName; - this.ctx.logger.info("nqn: " + nqn); - - // store this off to make delete process more bullet proof - await zb.zfs.set(datasetName, { - [NVMEOF_ASSETS_NAME_PROPERTY_NAME]: assetName, - }); - - volume_context = { - node_attach_driver: "nvmeof", - transport: this.options.nvmeof.transport || "", - transports: this.options.nvmeof.transports - ? this.options.nvmeof.transports.join(",") - : "", - nqn, - nsid: namespace, - }; - return volume_context; - } - - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown driver ${this.options.driver}` - ); - } - } - - async deleteShare(call, datasetName) { - const zb = await this.getZetabyte(); - const execClient = this.getExecClient(); - - let response; - let properties; - - switch (this.options.driver) { - case "zfs-generic-nfs": - switch (this.options.nfs.shareStrategy) { - case "setDatasetProperties": - for (let key of ["share", "sharenfs"]) { - if ( - this.options.nfs.shareStrategySetDatasetProperties.properties[ - key - ] - ) { - try { - await zb.zfs.inherit(datasetName, key); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - // do nothing - } else { - throw err; - } - } - } - } - await GeneralUtils.sleep(2000); // let things settle - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown shareStrategy ${this.options.nfs.shareStrategy}` - ); - } - break; - - case "zfs-generic-smb": - switch (this.options.smb.shareStrategy) { - case "setDatasetProperties": - for (let key of ["share", "sharesmb"]) { - if ( - this.options.smb.shareStrategySetDatasetProperties.properties[ - key - ] - ) { - try { - await zb.zfs.inherit(datasetName, key); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - // do nothing - } else { - throw err; - } - } - } - } - await GeneralUtils.sleep(2000); // let things settle - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown shareStrategy ${this.options.smb.shareStrategy}` - ); - } - break; - - case "zfs-generic-iscsi": { - let basename; - let assetName; - - // Delete iscsi assets - try { - properties = await zb.zfs.get(datasetName, [ - ISCSI_ASSETS_NAME_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; - } - throw err; - } - - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - assetName = properties[ISCSI_ASSETS_NAME_PROPERTY_NAME].value; - - if (zb.helpers.isPropertyValueSet(assetName)) { - //do nothing - } else { - assetName = zb.helpers.extractLeafName(datasetName); - - if (this.options.iscsi.namePrefix) { - assetName = this.options.iscsi.namePrefix + assetName; - } - - if (this.options.iscsi.nameSuffix) { - assetName += this.options.iscsi.nameSuffix; - } - } - - assetName = assetName.toLowerCase(); - switch (this.options.iscsi.shareStrategy) { - case "targetCli": - basename = this.options.iscsi.shareStrategyTargetCli.basename; - await GeneralUtils.retry( - 3, - 2000, - async () => { - await this.targetCliCommand( - ` -# delete target -cd /iscsi -delete ${basename}:${assetName} - -# delete extent -cd /backstores/block -delete ${assetName} -` - ); - }, - { - retryCondition: (err) => { - if (err.stdout && err.stdout.includes("Ran out of input")) { - return true; - } - return false; - }, - } - ); - - break; - default: - break; - } - break; - } - - case "zfs-generic-nvmeof": { - let basename; - let assetName; - - // Delete nvmeof assets - try { - properties = await zb.zfs.get(datasetName, [ - NVMEOF_ASSETS_NAME_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; - } - throw err; - } - - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - assetName = properties[NVMEOF_ASSETS_NAME_PROPERTY_NAME].value; - - if (zb.helpers.isPropertyValueSet(assetName)) { - //do nothing - } else { - assetName = zb.helpers.extractLeafName(datasetName); - - if (this.options.nvmeof.namePrefix) { - assetName = this.options.nvmeof.namePrefix + assetName; - } - - if (this.options.nvmeof.nameSuffix) { - assetName += this.options.nvmeof.nameSuffix; - } - } - - assetName = assetName.toLowerCase(); - switch (this.options.nvmeof.shareStrategy) { - case "nvmetCli": - { - basename = this.options.nvmeof.shareStrategyNvmetCli.basename; - let savefile = _.get( - this.options, - "nvmeof.shareStrategyNvmetCli.configPath", - "" - ); - if (savefile) { - savefile = `savefile=${savefile}`; - } - let portCommands = ""; - this.options.nvmeof.shareStrategyNvmetCli.ports.forEach( - (port) => { - portCommands += ` -cd /ports/${port}/subsystems -delete ${basename}:${assetName} -`; - } - ); - await GeneralUtils.retry( - 3, - 2000, - async () => { - await this.nvmetCliCommand( - ` -# delete subsystem from port -${portCommands} - -# delete subsystem -cd /subsystems -delete ${basename}:${assetName} - -saveconfig ${savefile} -` - ); - }, - { - retryCondition: (err) => { - if (err.stdout && err.stdout.includes("Ran out of input")) { - return true; - } - return false; - }, - } - ); - } - break; - case "spdkCli": - { - basename = this.options.nvmeof.shareStrategySpdkCli.basename; - await GeneralUtils.retry( - 3, - 2000, - async () => { - await this.spdkCliCommand( - ` -# delete subsystem -cd /nvmf/subsystem/ -delete subsystem_nqn=${basename}:${assetName} - -# delete bdev -cd /bdevs/${this.options.nvmeof.shareStrategySpdkCli.bdev.type} -delete name=${basename}:${assetName} - -cd / -save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} -` - ); - }, - { - retryCondition: (err) => { - if (err.stdout && err.stdout.includes("Ran out of input")) { - return true; - } - return false; - }, - } - ); - } - break; - - default: - break; - } - break; - } - - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown driver ${this.options.driver}` - ); - } - - return {}; - } - - async expandVolume(call, datasetName) { - switch (this.options.driver) { - case "zfs-generic-nfs": - break; - - case "zfs-generic-iscsi": - switch (this.options.iscsi.shareStrategy) { - case "targetCli": - // nothing required, just need to rescan on the node - break; - default: - break; - } - break; - - default: - break; - } - } - - async targetCliCommand(data) { - const execClient = this.getExecClient(); - const driver = this; - - data = data.trim(); - - let command = "sh"; - let args = ["-c"]; - - let cliArgs = ["targetcli"]; - if ( - _.get(this.options, "iscsi.shareStrategyTargetCli.sudoEnabled", false) - ) { - cliArgs.unshift("sudo"); - } - - let cliCommand = []; - cliCommand.push(`echo "${data}"`.trim()); - cliCommand.push("|"); - cliCommand.push(cliArgs.join(" ")); - args.push("'" + cliCommand.join(" ") + "'"); - - let logCommandTmp = command + " " + args.join(" "); - let logCommand = ""; - - logCommandTmp.split("\n").forEach((line) => { - if (line.startsWith("set auth password=")) { - logCommand += "set auth password="; - } else if (line.startsWith("set auth mutual_password=")) { - logCommand += "set auth mutual_password="; - } else { - logCommand += line; - } - - logCommand += "\n"; - }); - - driver.ctx.logger.verbose("TargetCLI command: " + logCommand); - - // https://github.com/democratic-csi/democratic-csi/issues/127 - // https://bugs.launchpad.net/ubuntu/+source/python-configshell-fb/+bug/1776761 - // can apply the linked patch with some modifications to overcome the - // KeyErrors or we can simply start a fake tty which does not seem to have - // a detrimental effect, only affects Ubuntu 18.04 and older - let options = { - pty: true, - }; - let response = await execClient.exec( - execClient.buildCommand(command, args), - options - ); - driver.ctx.logger.verbose( - "TargetCLI response: " + JSON.stringify(response) - ); - if (response.code != 0) { - throw response; - } - return response; - } - - async nvmetCliCommand(data) { - const execClient = this.getExecClient(); - const driver = this; - - if ( - _.get( - this.options, - "nvmeof.shareStrategyNvmetCli.configIsImportedFilePath" - ) - ) { - try { - let response = await execClient.exec( - execClient.buildCommand("test", [ - "-f", - _.get( - this.options, - "nvmeof.shareStrategyNvmetCli.configIsImportedFilePath" - ), - ]) - ); - } catch (err) { - throw new Error("nvmet has not been fully configured"); - } - } - - data = data.trim(); - - let command = "sh"; - let args = ["-c"]; - - let cliArgs = [ - _.get( - this.options, - "nvmeof.shareStrategyNvmetCli.nvmetcliPath", - "nvmetcli" - ), - ]; - if ( - _.get(this.options, "nvmeof.shareStrategyNvmetCli.sudoEnabled", false) - ) { - cliArgs.unshift("sudo"); - } - - let cliCommand = []; - cliCommand.push(`echo "${data}"`.trim()); - cliCommand.push("|"); - cliCommand.push(cliArgs.join(" ")); - args.push("'" + cliCommand.join(" ") + "'"); - - let logCommandTmp = command + " " + args.join(" "); - let logCommand = ""; - - logCommandTmp.split("\n").forEach((line) => { - if (line.startsWith("set auth password=")) { - logCommand += "set auth password="; - } else if (line.startsWith("set auth mutual_password=")) { - logCommand += "set auth mutual_password="; - } else { - logCommand += line; - } - - logCommand += "\n"; - }); - - driver.ctx.logger.verbose("nvmetCLI command: " + logCommand); - //process.exit(0); - - // https://github.com/democratic-csi/democratic-csi/issues/127 - // https://bugs.launchpad.net/ubuntu/+source/python-configshell-fb/+bug/1776761 - // can apply the linked patch with some modifications to overcome the - // KeyErrors or we can simply start a fake tty which does not seem to have - // a detrimental effect, only affects Ubuntu 18.04 and older - let options = { - pty: true, - }; - let response = await execClient.exec( - execClient.buildCommand(command, args), - options - ); - driver.ctx.logger.verbose("nvmetCLI response: " + JSON.stringify(response)); - if (response.code != 0) { - throw response; - } - return response; - } - - async spdkCliCommand(data) { - const execClient = this.getExecClient(); - const driver = this; - - data = data.trim(); - - let command = "sh"; - let args = ["-c"]; - - let cliArgs = [ - _.get(this.options, "nvmeof.shareStrategySpdkCli.spdkcliPath", "spdkcli"), - ]; - if (_.get(this.options, "nvmeof.shareStrategySpdkCli.sudoEnabled", false)) { - cliArgs.unshift("sudo"); - } - - let cliCommand = []; - cliCommand.push(`echo "${data}"`.trim()); - cliCommand.push("|"); - cliCommand.push(cliArgs.join(" ")); - args.push("'" + cliCommand.join(" ") + "'"); - - let logCommandTmp = command + " " + args.join(" "); - let logCommand = ""; - - logCommandTmp.split("\n").forEach((line) => { - if (line.startsWith("set auth password=")) { - logCommand += "set auth password="; - } else if (line.startsWith("set auth mutual_password=")) { - logCommand += "set auth mutual_password="; - } else { - logCommand += line; - } - - logCommand += "\n"; - }); - - driver.ctx.logger.verbose("spdkCLI command: " + logCommand); - //process.exit(0); - - // https://github.com/democratic-csi/democratic-csi/issues/127 - // https://bugs.launchpad.net/ubuntu/+source/python-configshell-fb/+bug/1776761 - // can apply the linked patch with some modifications to overcome the - // KeyErrors or we can simply start a fake tty which does not seem to have - // a detrimental effect, only affects Ubuntu 18.04 and older - let options = { - pty: true, - }; - let response = await execClient.exec( - execClient.buildCommand(command, args), - options - ); - driver.ctx.logger.verbose("spdkCLI response: " + JSON.stringify(response)); - if (response.code != 0) { - throw response; - } - return response; - } -} - -module.exports.ControllerZfsGenericDriver = ControllerZfsGenericDriver; diff --git a/src/driver/controller-zfs-local/index.js b/src/driver/controller-zfs-local/index.js deleted file mode 100644 index bfc3295..0000000 --- a/src/driver/controller-zfs-local/index.js +++ /dev/null @@ -1,241 +0,0 @@ -const _ = require("lodash"); -const { ControllerZfsBaseDriver } = require("../controller-zfs"); -const { GrpcError, grpc } = require("../../utils/grpc"); -const GeneralUtils = require("../../utils/general"); -const LocalCliExecClient = - require("../../utils/zfs_local_exec_client").LocalCliClient; -const { Zetabyte } = require("../../utils/zfs"); - -const ZFS_ASSET_NAME_PROPERTY_NAME = "zfs_asset_name"; -const NODE_TOPOLOGY_KEY_NAME = "org.democratic-csi.topology/node"; - -const __REGISTRY_NS__ = "ControllerZfsLocalDriver"; - -class ControllerZfsLocalDriver extends ControllerZfsBaseDriver { - constructor(ctx, options) { - const i_caps = _.get( - options, - "service.identity.capabilities.service", - false - ); - super(...arguments); - - if (!i_caps) { - this.ctx.logger.debug("setting zfs-local identity service caps"); - - options.service.identity.capabilities.service = [ - //"UNKNOWN", - "CONTROLLER_SERVICE", - "VOLUME_ACCESSIBILITY_CONSTRAINTS", - ]; - } - } - - getExecClient() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:exec_client`, () => { - return new LocalCliExecClient({ - logger: this.ctx.logger, - }); - }); - } - - async getZetabyte() { - return this.ctx.registry.getAsync(`${__REGISTRY_NS__}:zb`, async () => { - const execClient = this.getExecClient(); - - const options = {}; - options.executor = execClient; - options.idempotent = true; - - /* - if ( - this.options.zfs.hasOwnProperty("cli") && - this.options.zfs.cli && - this.options.zfs.cli.hasOwnProperty("paths") - ) { - options.paths = this.options.zfs.cli.paths; - } - */ - - // use env based paths to allow for custom wrapper scripts to chroot to the host - options.paths = { - zfs: "zfs", - zpool: "zpool", - sudo: "sudo", - chroot: "chroot", - }; - - options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); - - if (typeof this.setZetabyteCustomOptions === "function") { - await this.setZetabyteCustomOptions(options); - } - - return new Zetabyte(options); - }); - } - - /** - * cannot make this a storage class parameter as storage class/etc context is *not* sent - * into various calls such as GetControllerCapabilities etc - */ - getDriverZfsResourceType() { - switch (this.options.driver) { - case "zfs-local-dataset": - return "filesystem"; - case "zfs-local-zvol": - return "volume"; - default: - throw new Error("unknown driver: " + this.ctx.args.driver); - } - } - - getFSTypes() { - const driverZfsResourceType = this.getDriverZfsResourceType(); - switch (driverZfsResourceType) { - case "filesystem": - return ["zfs"]; - case "volume": - return GeneralUtils.default_supported_block_filesystems(); - } - } - - /** - * Although it is conter-intuitive to advertise node-local volumes as RWX we - * do so here to provide an easy out-of-the-box experience as users will by - * default want to provision volumes of RWX. The topology contraints - * implicity will enforce only a single node can use the volume at a given - * time. - * - * @returns Array - */ - getAccessModes(capability) { - let access_modes = _.get(this.options, "csi.access_modes", null); - if (access_modes !== null) { - return access_modes; - } - - const driverZfsResourceType = this.getDriverZfsResourceType(); - switch (driverZfsResourceType) { - case "filesystem": - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - "MULTI_NODE_READER_ONLY", - "MULTI_NODE_SINGLE_WRITER", - "MULTI_NODE_MULTI_WRITER", - ]; - break; - case "volume": - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - "MULTI_NODE_READER_ONLY", - "MULTI_NODE_SINGLE_WRITER", - "MULTI_NODE_MULTI_WRITER", - ]; - break; - } - - if ( - capability.access_type == "block" && - !access_modes.includes("MULTI_NODE_MULTI_WRITER") - ) { - access_modes.push("MULTI_NODE_MULTI_WRITER"); - } - - return access_modes; - } - - /** - * csi controller service - * - * should create any necessary share resources and return volume context - * - * @param {*} datasetName - */ - async createShare(call, datasetName) { - let volume_context = {}; - - switch (this.options.driver) { - case "zfs-local-dataset": - volume_context = { - node_attach_driver: "zfs-local", - [ZFS_ASSET_NAME_PROPERTY_NAME]: datasetName, - }; - return volume_context; - - case "zfs-local-zvol": - volume_context = { - node_attach_driver: "zfs-local", - [ZFS_ASSET_NAME_PROPERTY_NAME]: datasetName, - }; - return volume_context; - - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown driver ${this.options.driver}` - ); - } - } - - /** - * csi controller service - * - * @param {*} call - * @param {*} datasetName - * @returns - */ - async deleteShare(call, datasetName) { - return {}; - } - - /** - * csi controller service - * - * @param {*} call - * @param {*} datasetName - */ - async expandVolume(call, datasetName) {} - - /** - * List of topologies associated with the *volume* - * - * @returns array - */ - async getAccessibleTopology() { - const response = await super.NodeGetInfo(...arguments); - return [ - { - segments: { - [NODE_TOPOLOGY_KEY_NAME]: response.node_id, - }, - }, - ]; - } - - /** - * Add node topologies - * - * @param {*} call - * @returns - */ - async NodeGetInfo(call) { - const response = await super.NodeGetInfo(...arguments); - response.accessible_topology = { - segments: { - [NODE_TOPOLOGY_KEY_NAME]: response.node_id, - }, - }; - return response; - } -} - -module.exports.ControllerZfsLocalDriver = ControllerZfsLocalDriver; diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js deleted file mode 100644 index d665979..0000000 --- a/src/driver/controller-zfs/index.js +++ /dev/null @@ -1,2480 +0,0 @@ -const _ = require("lodash"); -const { CsiBaseDriver } = require("../index"); -const { GrpcError, grpc } = require("../../utils/grpc"); -const GeneralUtils = require("../../utils/general"); -const getLargestNumber = require("../../utils/general").getLargestNumber; - -const Handlebars = require("handlebars"); -const uuidv4 = require("uuid").v4; -const semver = require("semver"); - -// zfs common properties -const MANAGED_PROPERTY_NAME = "democratic-csi:managed_resource"; -const SUCCESS_PROPERTY_NAME = "democratic-csi:provision_success"; -const VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX = "volume-source-for-volume-"; -const VOLUME_SOURCE_DETACHED_SNAPSHOT_PREFIX = "volume-source-for-snapshot-"; -const VOLUME_CSI_NAME_PROPERTY_NAME = "democratic-csi:csi_volume_name"; -const SHARE_VOLUME_CONTEXT_PROPERTY_NAME = - "democratic-csi:csi_share_volume_context"; -const VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME = - "democratic-csi:csi_volume_content_source_type"; -const VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME = - "democratic-csi:csi_volume_content_source_id"; -const SNAPSHOT_CSI_NAME_PROPERTY_NAME = "democratic-csi:csi_snapshot_name"; -const SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME = - "democratic-csi:csi_snapshot_source_volume_id"; - -const VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME = - "democratic-csi:volume_context_provisioner_driver"; -const VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME = - "democratic-csi:volume_context_provisioner_instance_id"; - -const MAX_ZVOL_NAME_LENGTH_CACHE_KEY = "controller-zfs:max_zvol_name_length"; - -/** - * Base driver to provisin zfs assets using zfs cli commands. - * Derived drivers only need to implement: - * - getExecClient() - * - async getZetabyte() - * - async setZetabyteCustomOptions(options) // optional - * - getDriverZfsResourceType() // return "filesystem" or "volume" - * - getFSTypes() // optional - * - getAccessModes(capability) // optional - * - async getAccessibleTopology() // optional - * - async createShare(call, datasetName) // return appropriate volume_context for Node operations - * - async deleteShare(call, datasetName) // no return expected - * - async expandVolume(call, datasetName) // no return expected, used for restarting services etc if needed - */ -class ControllerZfsBaseDriver extends CsiBaseDriver { - constructor(ctx, options) { - super(...arguments); - - options = options || {}; - options.service = options.service || {}; - options.service.identity = options.service.identity || {}; - options.service.controller = options.service.controller || {}; - options.service.node = options.service.node || {}; - - options.service.identity.capabilities = - options.service.identity.capabilities || {}; - - options.service.controller.capabilities = - options.service.controller.capabilities || {}; - - options.service.node.capabilities = options.service.node.capabilities || {}; - - if (!("service" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity service caps"); - - options.service.identity.capabilities.service = [ - //"UNKNOWN", - "CONTROLLER_SERVICE", - //"VOLUME_ACCESSIBILITY_CONSTRAINTS" - ]; - } - - if (!("volume_expansion" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity volume_expansion caps"); - - options.service.identity.capabilities.volume_expansion = [ - //"UNKNOWN", - "ONLINE", - //"OFFLINE" - ]; - } - - if (!("rpc" in options.service.controller.capabilities)) { - this.ctx.logger.debug("setting default controller caps"); - - options.service.controller.capabilities.rpc = [ - //"UNKNOWN", - "CREATE_DELETE_VOLUME", - //"PUBLISH_UNPUBLISH_VOLUME", - //"LIST_VOLUMES_PUBLISHED_NODES", - "LIST_VOLUMES", - "GET_CAPACITY", - "CREATE_DELETE_SNAPSHOT", - "LIST_SNAPSHOTS", - "CLONE_VOLUME", - //"PUBLISH_READONLY", - "EXPAND_VOLUME", - ]; - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - options.service.controller.capabilities.rpc.push( - //"VOLUME_CONDITION", - "GET_VOLUME" - ); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.controller.capabilities.rpc.push( - "SINGLE_NODE_MULTI_WRITER" - ); - } - } - - if (!("rpc" in options.service.node.capabilities)) { - this.ctx.logger.debug("setting default node caps"); - - switch (this.getDriverZfsResourceType()) { - case "filesystem": - options.service.node.capabilities.rpc = [ - //"UNKNOWN", - "STAGE_UNSTAGE_VOLUME", - "GET_VOLUME_STATS", - //"EXPAND_VOLUME", - //"VOLUME_CONDITION", - ]; - break; - case "volume": - options.service.node.capabilities.rpc = [ - //"UNKNOWN", - "STAGE_UNSTAGE_VOLUME", - "GET_VOLUME_STATS", - "EXPAND_VOLUME", - //"VOLUME_CONDITION", - ]; - break; - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - //options.service.node.capabilities.rpc.push("VOLUME_CONDITION"); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER"); - /** - * This is for volumes that support a mount time gid such as smb or fat - */ - //options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP"); // in k8s is sent in as the security context fsgroup - } - } - } - - async getWhoAmI() { - const driver = this; - const execClient = driver.getExecClient(); - const command = "whoami"; - driver.ctx.logger.verbose("whoami command: %s", command); - const response = await execClient.exec(command); - if (response.code !== 0) { - throw new Error("failed to run uname to determine max zvol name length"); - } else { - return response.stdout.trim(); - } - } - - async getSudoPath() { - const zb = await this.getZetabyte(); - return zb.options.paths.sudo || "/usr/bin/sudo"; - } - - getDatasetParentName() { - let datasetParentName = this.options.zfs.datasetParentName; - datasetParentName = datasetParentName.replace(/\/$/, ""); - return datasetParentName; - } - - getVolumeParentDatasetName() { - let datasetParentName = this.getDatasetParentName(); - //datasetParentName += "/v"; - datasetParentName = datasetParentName.replace(/\/$/, ""); - return datasetParentName; - } - - getDetachedSnapshotParentDatasetName() { - //let datasetParentName = this.getDatasetParentName(); - let datasetParentName = this.options.zfs.detachedSnapshotsDatasetParentName; - //datasetParentName += "/s"; - datasetParentName = datasetParentName.replace(/\/$/, ""); - return datasetParentName; - } - - async removeSnapshotsFromDatatset(datasetName, options = {}) { - const zb = await this.getZetabyte(); - - await zb.zfs.destroy(datasetName + "@%", options); - } - - getFSTypes() { - const driverZfsResourceType = this.getDriverZfsResourceType(); - switch (driverZfsResourceType) { - case "filesystem": - return GeneralUtils.default_supported_file_filesystems(); - case "volume": - return GeneralUtils.default_supported_block_filesystems(); - } - } - - getAccessModes(capability) { - let access_modes = _.get(this.options, "csi.access_modes", null); - if (access_modes !== null) { - return access_modes; - } - - const driverZfsResourceType = this.getDriverZfsResourceType(); - switch (driverZfsResourceType) { - case "filesystem": - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - "MULTI_NODE_READER_ONLY", - "MULTI_NODE_SINGLE_WRITER", - "MULTI_NODE_MULTI_WRITER", - ]; - break; - case "volume": - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - "MULTI_NODE_READER_ONLY", - "MULTI_NODE_SINGLE_WRITER", - ]; - break; - } - - if ( - capability.access_type == "block" && - !access_modes.includes("MULTI_NODE_MULTI_WRITER") - ) { - access_modes.push("MULTI_NODE_MULTI_WRITER"); - } - - return access_modes; - } - - assertCapabilities(capabilities) { - const driverZfsResourceType = this.getDriverZfsResourceType(); - this.ctx.logger.verbose("validating capabilities: %j", capabilities); - - let message = null; - //[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}] - const valid = capabilities.every((capability) => { - switch (driverZfsResourceType) { - case "filesystem": - if (capability.access_type != "mount") { - message = `invalid access_type ${capability.access_type}`; - return false; - } - - if ( - capability.mount.fs_type && - !this.getFSTypes().includes(capability.mount.fs_type) - ) { - message = `invalid fs_type ${capability.mount.fs_type}`; - return false; - } - - if ( - !this.getAccessModes(capability).includes( - capability.access_mode.mode - ) - ) { - message = `invalid access_mode, ${capability.access_mode.mode}`; - return false; - } - - return true; - case "volume": - if (capability.access_type == "mount") { - if ( - capability.mount.fs_type && - !this.getFSTypes().includes(capability.mount.fs_type) - ) { - message = `invalid fs_type ${capability.mount.fs_type}`; - return false; - } - } - - if ( - !this.getAccessModes(capability).includes( - capability.access_mode.mode - ) - ) { - message = `invalid access_mode, ${capability.access_mode.mode}`; - return false; - } - - return true; - } - }); - - return { valid, message }; - } - - async getVolumeStatus(volume_id) { - const driver = this; - - if (!!!semver.satisfies(driver.ctx.csiVersion, ">=1.2.0")) { - return; - } - - let abnormal = false; - let message = "OK"; - let volume_status = {}; - - //LIST_VOLUMES_PUBLISHED_NODES - if ( - semver.satisfies(driver.ctx.csiVersion, ">=1.2.0") && - driver.options.service.controller.capabilities.rpc.includes( - "LIST_VOLUMES_PUBLISHED_NODES" - ) - ) { - // TODO: let drivers fill this in - volume_status.published_node_ids = []; - } - - //VOLUME_CONDITION - if ( - semver.satisfies(driver.ctx.csiVersion, ">=1.3.0") && - driver.options.service.controller.capabilities.rpc.includes( - "VOLUME_CONDITION" - ) - ) { - // TODO: let drivers fill ths in - volume_condition = { abnormal, message }; - volume_status.volume_condition = volume_condition; - } - - return volume_status; - } - - async populateCsiVolumeFromData(row) { - const driver = this; - const zb = await this.getZetabyte(); - const driverZfsResourceType = this.getDriverZfsResourceType(); - let datasetName = this.getVolumeParentDatasetName(); - - // ignore rows were csi_name is empty - if (row[MANAGED_PROPERTY_NAME] != "true") { - return; - } - - if ( - !zb.helpers.isPropertyValueSet(row[SHARE_VOLUME_CONTEXT_PROPERTY_NAME]) - ) { - driver.ctx.logger.warn(`${row.name} is missing share context`); - return; - } - - let volume_content_source; - let volume_context = JSON.parse(row[SHARE_VOLUME_CONTEXT_PROPERTY_NAME]); - if ( - zb.helpers.isPropertyValueSet( - row[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME] - ) - ) { - volume_context["provisioner_driver"] = - row[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME]; - } - - if ( - zb.helpers.isPropertyValueSet( - row[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME] - ) - ) { - volume_context["provisioner_driver_instance_id"] = - row[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME]; - } - - if ( - zb.helpers.isPropertyValueSet( - row[VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME] - ) - ) { - volume_content_source = {}; - switch (row[VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME]) { - case "snapshot": - volume_content_source.snapshot = {}; - volume_content_source.snapshot.snapshot_id = - row[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME]; - break; - case "volume": - volume_content_source.volume = {}; - volume_content_source.volume.volume_id = - row[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME]; - break; - } - } - - let accessible_topology; - if (typeof this.getAccessibleTopology === "function") { - accessible_topology = await this.getAccessibleTopology(); - } - - let volume = { - // remove parent dataset info - volume_id: row["name"].replace(new RegExp("^" + datasetName + "/"), ""), - capacity_bytes: - driverZfsResourceType == "filesystem" - ? row["refquota"] - : row["volsize"], - content_source: volume_content_source, - volume_context, - accessible_topology, - }; - - return volume; - } - - /** - * Get the max size a zvol name can be - * - * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238112 - * https://svnweb.freebsd.org/base?view=revision&revision=343485 - */ - async getMaxZvolNameLength() { - const driver = this; - const execClient = driver.getExecClient(); - - let response; - let command; - let kernel; - let kernel_release; - - const cachedValue = await driver.ctx.cache.get( - MAX_ZVOL_NAME_LENGTH_CACHE_KEY - ); - - if (cachedValue) { - return cachedValue; - } - - // get kernel - command = "uname -s"; - driver.ctx.logger.verbose("uname command: %s", command); - response = await execClient.exec(command); - if (response.code !== 0) { - throw new Error("failed to run uname to determine max zvol name length"); - } else { - kernel = response.stdout.trim(); - } - - let max; - switch (kernel.toLowerCase().trim()) { - // Linux is 255 (probably larger 4096) but scst may have a 255 limit - // https://ngelinux.com/what-is-the-maximum-file-name-length-in-linux-and-how-to-see-this-is-this-really-255-characters-answer-is-no/ - // https://github.com/dmeister/scst/blob/master/iscsi-scst/include/iscsi_scst.h#L28 - case "linux": - max = 255; - break; - case "freebsd": - // get kernel_release - command = "uname -r"; - driver.ctx.logger.verbose("uname command: %s", command); - response = await execClient.exec(command); - if (response.code !== 0) { - throw new Error( - "failed to run uname to determine max zvol name length" - ); - } else { - kernel_release = response.stdout; - let parts = kernel_release.split("."); - let kernel_release_major = parts[0]; - - if (kernel_release_major >= 13) { - max = 255; - } else { - max = 63; - } - } - break; - default: - throw new Error(`unknown kernel: ${kernel}`); - } - - await driver.ctx.cache.set(MAX_ZVOL_NAME_LENGTH_CACHE_KEY, max, { - ttl: 60 * 1000, - }); - return max; - } - - async setFilesystemMode(path, mode) { - const driver = this; - const execClient = this.getExecClient(); - - let command = execClient.buildCommand("chmod", [mode, path]); - if ((await driver.getWhoAmI()) != "root") { - command = (await driver.getSudoPath()) + " " + command; - } - - driver.ctx.logger.verbose("set permission command: %s", command); - - let response = await execClient.exec(command); - if (response.code != 0) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error setting permissions on dataset: ${JSON.stringify(response)}` - ); - } - } - - async setFilesystemOwnership(path, user = false, group = false) { - const driver = this; - const execClient = this.getExecClient(); - - if (user === false || typeof user == "undefined" || user === null) { - user = ""; - } - - if (group === false || typeof group == "undefined" || group === null) { - group = ""; - } - - user = String(user); - group = String(group); - - if (user.length < 1 && group.length < 1) { - return; - } - - let command = execClient.buildCommand("chown", [ - (user.length > 0 ? user : "") + ":" + (group.length > 0 ? group : ""), - path, - ]); - if ((await driver.getWhoAmI()) != "root") { - command = (await driver.getSudoPath()) + " " + command; - } - - driver.ctx.logger.verbose("set ownership command: %s", command); - - let response = await execClient.exec(command); - if (response.code != 0) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error setting ownership on dataset: ${JSON.stringify(response)}` - ); - } - } - - /** - * Ensure sane options are used etc - * true = ready - * false = not ready, but progressiong towards ready - * throw error = faulty setup - * - * @param {*} call - */ - async Probe(call) { - const driver = this; - - if (driver.ctx.args.csiMode.includes("controller")) { - let datasetParentName = this.getVolumeParentDatasetName() + "/"; - let snapshotParentDatasetName = - this.getDetachedSnapshotParentDatasetName() + "/"; - if ( - datasetParentName.startsWith(snapshotParentDatasetName) || - snapshotParentDatasetName.startsWith(datasetParentName) - ) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `datasetParentName and detachedSnapshotsDatasetParentName must not overlap` - ); - } - - const timerEnabled = false; - - /** - * limit the actual checks semi sanely - * health checks should kick in an restart the pod - * this process is 2 checks in 1 - * - ensure basic exec connectivity - * - ensure csh is not the operative shell - */ - if (!driver.currentExecShell || timerEnabled === false) { - const execClient = this.getExecClient(); - driver.ctx.logger.debug("performing exec sanity check.."); - const response = await execClient.exec("echo $0"); - driver.currentExecShell = response.stdout.split("\n")[0]; - } - - // update in the background every X interval to prevent incessant checks - if (timerEnabled && !driver.currentExecShellInterval) { - const intervalTime = 60000; - driver.currentExecShellInterval = setInterval(async () => { - try { - driver.ctx.logger.debug("performing exec sanity check.."); - const execClient = this.getExecClient(); - const response = await execClient.exec("echo $0"); - driver.currentExecShell = response.stdout.split("\n")[0]; - } catch (e) { - delete driver.currentExecShell; - } - }, intervalTime); - } - - if (driver.currentExecShell.includes("csh")) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `csh is an unsupported shell, please update the default shell of your exec user` - ); - } - - return super.Probe(...arguments); - } else { - return super.Probe(...arguments); - } - } - - /** - * Create a volume doing in essence the following: - * 1. create dataset - * 2. create nfs share - * - * Should return 2 parameters - * 1. `server` - host/ip of the nfs server - * 2. `share` - path of the mount shared - * - * @param {*} call - */ - async CreateVolume(call) { - const driver = this; - const driverZfsResourceType = this.getDriverZfsResourceType(); - const execClient = this.getExecClient(); - const zb = await this.getZetabyte(); - - let datasetParentName = this.getVolumeParentDatasetName(); - let snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName(); - let zvolBlocksize = this.options.zfs.zvolBlocksize || "16K"; - let name = call.request.name; - let volume_id = await driver.getVolumeIdFromCall(call); - let volume_content_source = call.request.volume_content_source; - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - if ( - call.request.volume_capabilities && - call.request.volume_capabilities.length > 0 - ) { - const result = this.assertCapabilities(call.request.volume_capabilities); - if (result.valid !== true) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); - } - } else { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - "missing volume_capabilities" - ); - } - - // if no capacity_range specified set a required_bytes at least - if ( - !call.request.capacity_range || - Object.keys(call.request.capacity_range).length === 0 - ) { - call.request.capacity_range = { - required_bytes: 1073741824, - }; - } - - if ( - call.request.capacity_range.required_bytes > 0 && - call.request.capacity_range.limit_bytes > 0 && - call.request.capacity_range.required_bytes > - call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.OUT_OF_RANGE, - `required_bytes is greather than limit_bytes` - ); - } - - let capacity_bytes = - call.request.capacity_range.required_bytes || - call.request.capacity_range.limit_bytes; - - if (!capacity_bytes) { - //should never happen, value must be set - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume capacity is required (either required_bytes or limit_bytes)` - ); - } - - if (capacity_bytes && driverZfsResourceType == "volume") { - //make sure to align capacity_bytes with zvol blocksize - //volume size must be a multiple of volume block size - capacity_bytes = zb.helpers.generateZvolSize( - capacity_bytes, - zvolBlocksize - ); - } - - // ensure *actual* capacity is not greater than limit - if ( - call.request.capacity_range.limit_bytes && - call.request.capacity_range.limit_bytes > 0 && - capacity_bytes > call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.OUT_OF_RANGE, - `required volume capacity is greater than limit` - ); - } - - /** - * NOTE: avoid the urge to templatize this given the name length limits for zvols - * ie: namespace-name may quite easily exceed 58 chars - */ - const datasetName = datasetParentName + "/" + volume_id; - - // ensure volumes with the same name being requested a 2nd time but with a different size fails - try { - let properties = await zb.zfs.get(datasetName, ["volsize", "refquota"]); - properties = properties[datasetName]; - let size; - switch (driverZfsResourceType) { - case "volume": - size = properties["volsize"].value; - break; - case "filesystem": - size = properties["refquota"].value; - break; - default: - throw new Error( - `unknown zfs resource type: ${driverZfsResourceType}` - ); - } - - let check = false; - if (driverZfsResourceType == "volume") { - check = true; - } - - if ( - driverZfsResourceType == "filesystem" && - this.options.zfs.datasetEnableQuotas - ) { - check = true; - } - - if (check) { - if ( - (call.request.capacity_range.required_bytes && - call.request.capacity_range.required_bytes > 0 && - size < call.request.capacity_range.required_bytes) || - (call.request.capacity_range.limit_bytes && - call.request.capacity_range.limit_bytes > 0 && - size > call.request.capacity_range.limit_bytes) - ) { - throw new GrpcError( - grpc.status.ALREADY_EXISTS, - `volume has already been created with a different size, existing size: ${size}, required_bytes: ${call.request.capacity_range.required_bytes}, limit_bytes: ${call.request.capacity_range.limit_bytes}` - ); - } - } - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - // does NOT already exist - } else { - throw err; - } - } - - /** - * This is specifically a FreeBSD limitation, not sure what linux limit is - * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab - * https://www.ixsystems.com/documentation/freenas/11.3-BETA1/intro.html#path-and-name-lengths - * https://www.freebsd.org/cgi/man.cgi?query=devfs - * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238112 - */ - if (driverZfsResourceType == "volume") { - let extentDiskName = "zvol/" + datasetName; - let maxZvolNameLength = await driver.getMaxZvolNameLength(); - driver.ctx.logger.debug("max zvol name length: %s", maxZvolNameLength); - - if (extentDiskName.length > maxZvolNameLength) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` - ); - } - } - - let response, command; - let volume_content_source_snapshot_id; - let volume_content_source_volume_id; - let fullSnapshotName; - let volumeProperties = {}; - - // user-supplied properties - // put early to prevent stupid (user-supplied values overwriting system values) - if (driver.options.zfs.datasetProperties) { - for (let property in driver.options.zfs.datasetProperties) { - let value = driver.options.zfs.datasetProperties[property]; - const template = Handlebars.compile(value); - - volumeProperties[property] = template({ - parameters: call.request.parameters, - }); - } - } - - volumeProperties[VOLUME_CSI_NAME_PROPERTY_NAME] = name; - volumeProperties[MANAGED_PROPERTY_NAME] = "true"; - volumeProperties[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME] = - driver.options.driver; - if (driver.options.instance_id) { - volumeProperties[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME] = - driver.options.instance_id; - } - - // TODO: also set access_mode as property? - // TODO: also set fsType as property? - - // zvol enables reservation by default - // this implements 'sparse' zvols - if (driverZfsResourceType == "volume") { - if (!this.options.zfs.zvolEnableReservation) { - volumeProperties.refreservation = 0; - } - } - - let detachedClone = false; - - // create dataset - if (volume_content_source) { - volumeProperties[VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME] = - volume_content_source.type; - switch (volume_content_source.type) { - // must be available when adverstising CREATE_DELETE_SNAPSHOT - // simply clone - case "snapshot": - try { - let tmpDetachedClone = JSON.parse( - driver.getNormalizedParameterValue( - call.request.parameters, - "detachedVolumesFromSnapshots" - ) - ); - if (typeof tmpDetachedClone === "boolean") { - detachedClone = tmpDetachedClone; - } - } catch (e) {} - - volumeProperties[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME] = - volume_content_source.snapshot.snapshot_id; - volume_content_source_snapshot_id = - volume_content_source.snapshot.snapshot_id; - - // zfs origin property contains parent info, ie: pool0/k8s/test/PVC-111@clone-test - if (zb.helpers.isZfsSnapshot(volume_content_source_snapshot_id)) { - fullSnapshotName = - datasetParentName + "/" + volume_content_source_snapshot_id; - } else { - fullSnapshotName = - snapshotParentDatasetName + - "/" + - volume_content_source_snapshot_id + - "@" + - VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + - volume_id; - } - - driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); - - if (!zb.helpers.isZfsSnapshot(volume_content_source_snapshot_id)) { - try { - await zb.zfs.snapshot(fullSnapshotName); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` - ); - } - - throw err; - } - } - - if (detachedClone) { - try { - response = await zb.zfs.send_receive( - fullSnapshotName, - [], - datasetName, - [] - ); - - response = await zb.zfs.set(datasetName, volumeProperties); - } catch (err) { - if ( - err.toString().includes("destination") && - err.toString().includes("exists") - ) { - // move along - } else { - throw err; - } - } - - // remove snapshots from target - await this.removeSnapshotsFromDatatset(datasetName, { - force: true, - }); - } else { - try { - // remove readonly/undesired props - let cloneProperties = volumeProperties; - delete cloneProperties["aclmode"]; - delete cloneProperties["aclinherit"]; - delete cloneProperties["acltype"]; - delete cloneProperties["casesensitivity"]; - response = await zb.zfs.clone(fullSnapshotName, datasetName, { - properties: volumeProperties, - }); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - throw new GrpcError( - grpc.status.NOT_FOUND, - "dataset does not exists" - ); - } - - throw err; - } - } - - if (!zb.helpers.isZfsSnapshot(volume_content_source_snapshot_id)) { - try { - // schedule snapshot removal from source - await zb.zfs.destroy(fullSnapshotName, { - recurse: true, - force: true, - defer: true, - }); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` - ); - } - - throw err; - } - } - - break; - // must be available when adverstising CLONE_VOLUME - // create snapshot first, then clone - case "volume": - try { - let tmpDetachedClone = JSON.parse( - driver.getNormalizedParameterValue( - call.request.parameters, - "detachedVolumesFromVolumes" - ) - ); - if (typeof tmpDetachedClone === "boolean") { - detachedClone = tmpDetachedClone; - } - } catch (e) {} - - volumeProperties[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME] = - volume_content_source.volume.volume_id; - volume_content_source_volume_id = - volume_content_source.volume.volume_id; - - fullSnapshotName = - datasetParentName + - "/" + - volume_content_source_volume_id + - "@" + - VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + - volume_id; - - driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); - - // create snapshot - try { - response = await zb.zfs.snapshot(fullSnapshotName); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - throw new GrpcError( - grpc.status.NOT_FOUND, - "dataset does not exists" - ); - } - - throw err; - } - - if (detachedClone) { - try { - response = await zb.zfs.send_receive( - fullSnapshotName, - [], - datasetName, - [] - ); - } catch (err) { - if ( - err.toString().includes("destination") && - err.toString().includes("exists") - ) { - // move along - } else { - throw err; - } - } - - response = await zb.zfs.set(datasetName, volumeProperties); - - // remove snapshots from target - await this.removeSnapshotsFromDatatset(datasetName, { - force: true, - }); - - // remove snapshot from source - await zb.zfs.destroy(fullSnapshotName, { - recurse: true, - force: true, - defer: true, - }); - } else { - // create clone - // zfs origin property contains parent info, ie: pool0/k8s/test/PVC-111@clone-test - // remove readonly/undesired props - let cloneProperties = volumeProperties; - delete cloneProperties["aclmode"]; - delete cloneProperties["aclinherit"]; - delete cloneProperties["acltype"]; - delete cloneProperties["casesensitivity"]; - try { - response = await zb.zfs.clone(fullSnapshotName, datasetName, { - properties: cloneProperties, - }); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - throw new GrpcError( - grpc.status.NOT_FOUND, - "dataset does not exists" - ); - } - - throw err; - } - } - break; - default: - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `invalid volume_content_source type: ${volume_content_source.type}` - ); - break; - } - } else { - // force blocksize on newly created zvols - if (driverZfsResourceType == "volume") { - volumeProperties.volblocksize = zvolBlocksize; - } - - await zb.zfs.create(datasetName, { - parents: true, - properties: volumeProperties, - size: driverZfsResourceType == "volume" ? capacity_bytes : false, - }); - } - - let setProps = false; - let properties = {}; - let volume_context = {}; - - switch (driverZfsResourceType) { - case "filesystem": - // set quota - if (this.options.zfs.datasetEnableQuotas) { - setProps = true; - properties.refquota = capacity_bytes; - } - - // set reserve - if (this.options.zfs.datasetEnableReservation) { - setProps = true; - properties.refreservation = capacity_bytes; - } - - // quota for dataset and all children - // reserved for dataset and all children - - // dedup - // ro? - // record size - - // set properties - if (setProps) { - await zb.zfs.set(datasetName, properties); - } - - // get properties needed for remaining calls - properties = await zb.zfs.get(datasetName, [ - "mountpoint", - "refquota", - "compression", - VOLUME_CSI_NAME_PROPERTY_NAME, - VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME, - VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME, - ]); - properties = properties[datasetName]; - driver.ctx.logger.debug("zfs props data: %j", properties); - - // set mode - if (this.options.zfs.datasetPermissionsMode) { - await driver.setFilesystemMode( - properties.mountpoint.value, - this.options.zfs.datasetPermissionsMode - ); - } - - // set ownership - if ( - String(_.get(this.options, "zfs.datasetPermissionsUser", "")).length > - 0 || - String(_.get(this.options, "zfs.datasetPermissionsGroup", "")) - .length > 0 - ) { - await driver.setFilesystemOwnership( - properties.mountpoint.value, - this.options.zfs.datasetPermissionsUser, - this.options.zfs.datasetPermissionsGroup - ); - } - - // set acls - // TODO: this is unsfafe approach, make it better - // probably could see if ^-.*\s and split and then shell escape - if (this.options.zfs.datasetPermissionsAcls) { - let aclBinary = _.get( - driver.options, - "zfs.datasetPermissionsAclsBinary", - "setfacl" - ); - for (const acl of this.options.zfs.datasetPermissionsAcls) { - command = execClient.buildCommand(aclBinary, [ - acl, - properties.mountpoint.value, - ]); - if ((await this.getWhoAmI()) != "root") { - command = (await this.getSudoPath()) + " " + command; - } - - driver.ctx.logger.verbose("set acl command: %s", command); - response = await execClient.exec(command); - if (response.code != 0) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error setting acl on dataset: ${JSON.stringify(response)}` - ); - } - } - } - break; - case "volume": - // set properties - // set reserve - setProps = true; - - // this should be already set, but when coming from a volume source - // it may not match that of the source - properties.volsize = capacity_bytes; - - // dedup - // on, off, verify - // zfs set dedup=on tank/home - // restore default must use the below - // zfs inherit [-rS] property filesystem|volume|snapshot… - if ( - (typeof this.options.zfs.zvolDedup === "string" || - this.options.zfs.zvolDedup instanceof String) && - this.options.zfs.zvolDedup.length > 0 - ) { - properties.dedup = this.options.zfs.zvolDedup; - } - - // compression - // lz4, gzip-9, etc - if ( - (typeof this.options.zfs.zvolCompression === "string" || - this.options.zfs.zvolCompression instanceof String) && - this.options.zfs.zvolCompression > 0 - ) { - properties.compression = this.options.zfs.zvolCompression; - } - - if (setProps) { - await zb.zfs.set(datasetName, properties); - } - - break; - } - - volume_context = await this.createShare(call, datasetName); - await zb.zfs.set(datasetName, { - [SHARE_VOLUME_CONTEXT_PROPERTY_NAME]: JSON.stringify(volume_context), - }); - - volume_context["provisioner_driver"] = driver.options.driver; - if (driver.options.instance_id) { - volume_context["provisioner_driver_instance_id"] = - driver.options.instance_id; - } - - // set this just before sending out response so we know if volume completed - // this should give us a relatively sane way to clean up artifacts over time - await zb.zfs.set(datasetName, { [SUCCESS_PROPERTY_NAME]: "true" }); - - let accessible_topology; - if (typeof this.getAccessibleTopology === "function") { - accessible_topology = await this.getAccessibleTopology(); - } - - const res = { - volume: { - volume_id, - //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 - capacity_bytes: - this.options.zfs.datasetEnableQuotas || - driverZfsResourceType == "volume" - ? capacity_bytes - : 0, - content_source: volume_content_source, - volume_context, - accessible_topology, - }, - }; - - return res; - } - - /** - * Delete a volume - * - * Deleting a volume consists of the following steps: - * 1. delete the nfs share - * 2. delete the dataset - * - * @param {*} call - */ - async DeleteVolume(call) { - const driver = this; - const zb = await this.getZetabyte(); - - let datasetParentName = this.getVolumeParentDatasetName(); - let name = call.request.volume_id; - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - if (!name) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume_id is required` - ); - } - - const datasetName = datasetParentName + "/" + name; - let properties; - - // get properties needed for remaining calls - try { - properties = await zb.zfs.get(datasetName, [ - "mountpoint", - "origin", - "refquota", - "compression", - VOLUME_CSI_NAME_PROPERTY_NAME, - ]); - properties = properties[datasetName]; - } catch (err) { - let ignore = false; - if (err.toString().includes("dataset does not exist")) { - ignore = true; - } - - if (!ignore) { - throw err; - } - } - - driver.ctx.logger.debug("dataset properties: %j", properties); - - // deleteStrategy - const delete_strategy = _.get( - driver.options, - "_private.csi.volume.deleteStrategy", - "" - ); - - if (delete_strategy == "retain") { - return {}; - } - - // remove share resources - await this.deleteShare(call, datasetName); - - // remove parent snapshot if appropriate with defer - if ( - properties && - properties.origin && - properties.origin.value != "-" && - zb.helpers - .extractSnapshotName(properties.origin.value) - .startsWith(VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX) - ) { - driver.ctx.logger.debug( - "removing with defer source snapshot: %s", - properties.origin.value - ); - - try { - await zb.zfs.destroy(properties.origin.value, { - recurse: true, - force: true, - defer: true, - }); - } catch (err) { - if (err.toString().includes("snapshot has dependent clones")) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - "snapshot has dependent clones" - ); - } - throw err; - } - } - - // NOTE: -f does NOT allow deletes if dependent filesets exist - // NOTE: -R will recursively delete items + dependent filesets - // delete dataset - try { - await GeneralUtils.retry( - 12, - 5000, - async () => { - await zb.zfs.destroy(datasetName, { recurse: true, force: true }); - }, - { - retryCondition: (err) => { - if ( - err.toString().includes("dataset is busy") || - err.toString().includes("target is busy") - ) { - return true; - } - return false; - }, - } - ); - } catch (err) { - if (err.toString().includes("filesystem has dependent clones")) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - "filesystem has dependent clones" - ); - } - - throw err; - } - - return {}; - } - - /** - * - * @param {*} call - */ - async ControllerExpandVolume(call) { - const driver = this; - const driverZfsResourceType = this.getDriverZfsResourceType(); - const zb = await this.getZetabyte(); - - let datasetParentName = this.getVolumeParentDatasetName(); - let name = call.request.volume_id; - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - if (!name) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume_id is required` - ); - } - - const datasetName = datasetParentName + "/" + name; - - let capacity_bytes = - call.request.capacity_range.required_bytes || - call.request.capacity_range.limit_bytes; - if (!capacity_bytes) { - //should never happen, value must be set - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume capacity is required (either required_bytes or limit_bytes)` - ); - } - - if (capacity_bytes && driverZfsResourceType == "volume") { - //make sure to align capacity_bytes with zvol blocksize - //volume size must be a multiple of volume block size - let properties = await zb.zfs.get(datasetName, ["volblocksize"]); - properties = properties[datasetName]; - capacity_bytes = zb.helpers.generateZvolSize( - capacity_bytes, - properties.volblocksize.value - ); - } - - if ( - call.request.capacity_range.required_bytes > 0 && - call.request.capacity_range.limit_bytes > 0 && - call.request.capacity_range.required_bytes > - call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `required_bytes is greather than limit_bytes` - ); - } - - // ensure *actual* capacity is not greater than limit - if ( - call.request.capacity_range.limit_bytes && - call.request.capacity_range.limit_bytes > 0 && - capacity_bytes > call.request.capacity_range.limit_bytes - ) { - throw new GrpcError( - grpc.status.OUT_OF_RANGE, - `required volume capacity is greater than limit` - ); - } - - let setProps = false; - let properties = {}; - - switch (driverZfsResourceType) { - case "filesystem": - // set quota - if (this.options.zfs.datasetEnableQuotas) { - setProps = true; - properties.refquota = capacity_bytes; - } - - // set reserve - if (this.options.zfs.datasetEnableReservation) { - setProps = true; - properties.refreservation = capacity_bytes; - } - break; - case "volume": - properties.volsize = capacity_bytes; - setProps = true; - - // managed automatically for zvols - //if (this.options.zfs.zvolEnableReservation) { - // properties.refreservation = capacity_bytes; - //} - break; - } - - if (setProps) { - await zb.zfs.set(datasetName, properties); - } - - await this.expandVolume(call, datasetName); - - return { - capacity_bytes: - this.options.zfs.datasetEnableQuotas || - driverZfsResourceType == "volume" - ? capacity_bytes - : 0, - node_expansion_required: driverZfsResourceType == "volume" ? true : false, - }; - } - - /** - * TODO: consider volume_capabilities? - * - * @param {*} call - */ - async GetCapacity(call) { - const driver = this; - const zb = await this.getZetabyte(); - - let datasetParentName = this.getVolumeParentDatasetName(); - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - if (call.request.volume_capabilities) { - const result = this.assertCapabilities(call.request.volume_capabilities); - - if (result.valid !== true) { - return { available_capacity: 0 }; - } - } - - const datasetName = datasetParentName; - - await zb.zfs.create(datasetName, { - parents: true, - }); - - let properties; - try { - properties = await zb.zfs.get(datasetName, ["avail"]); - properties = properties[datasetName]; - - return { available_capacity: properties.available.value }; - } catch (err) { - throw err; - // gracefully handle csi-test suite when parent dataset does not yet exist - if (err.toString().includes("dataset does not exist")) { - return { available_capacity: 0 }; - } else { - throw err; - } - } - } - - /** - * Get a single volume - * - * @param {*} call - */ - async ControllerGetVolume(call) { - const driver = this; - const driverZfsResourceType = this.getDriverZfsResourceType(); - const zb = await this.getZetabyte(); - - let datasetParentName = this.getVolumeParentDatasetName(); - let response; - let name = call.request.volume_id; - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - if (!name) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume_id is required` - ); - } - - const datasetName = datasetParentName + "/" + name; - - let types = []; - switch (driverZfsResourceType) { - case "filesystem": - types = ["filesystem"]; - break; - case "volume": - types = ["volume"]; - break; - } - try { - response = await zb.zfs.list( - datasetName, - [ - "name", - "mountpoint", - "refquota", - "avail", - "used", - VOLUME_CSI_NAME_PROPERTY_NAME, - VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME, - VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME, - "volsize", - MANAGED_PROPERTY_NAME, - SHARE_VOLUME_CONTEXT_PROPERTY_NAME, - SUCCESS_PROPERTY_NAME, - VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME, - VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME, - ], - { types, recurse: false } - ); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - throw new GrpcError(grpc.status.NOT_FOUND, `volume_id is missing`); - } - - throw err; - } - - driver.ctx.logger.debug("list volumes result: %j", response); - let volume = await driver.populateCsiVolumeFromData(response.indexed[0]); - let status = await driver.getVolumeStatus(datasetName); - - let res = { volume }; - if (status) { - res.status = status; - } - - return res; - } - - /** - * - * TODO: check capability to ensure not asking about block volumes - * - * @param {*} call - */ - async ListVolumes(call) { - const driver = this; - const driverZfsResourceType = this.getDriverZfsResourceType(); - const zb = await this.getZetabyte(); - - let datasetParentName = this.getVolumeParentDatasetName(); - let entries = []; - let entries_length = 0; - let next_token; - let uuid; - let response; - - const max_entries = call.request.max_entries; - const starting_token = call.request.starting_token; - - // get data from cache and return immediately - if (starting_token) { - let parts = starting_token.split(":"); - uuid = parts[0]; - let start_position = parseInt(parts[1]); - let end_position; - if (max_entries > 0) { - end_position = start_position + max_entries; - } - entries = this.ctx.cache.get(`ListVolumes:result:${uuid}`); - if (entries) { - entries_length = entries.length; - entries = entries.slice(start_position, end_position); - if (max_entries > 0 && end_position > entries_length) { - next_token = `${uuid}:${end_position}`; - } else { - next_token = null; - } - const data = { - entries: entries, - next_token: next_token, - }; - - return data; - } else { - throw new GrpcError( - grpc.status.ABORTED, - `invalid starting_token: ${starting_token}` - ); - } - } - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - const datasetName = datasetParentName; - - let types = []; - switch (driverZfsResourceType) { - case "filesystem": - types = ["filesystem"]; - break; - case "volume": - types = ["volume"]; - break; - } - try { - response = await zb.zfs.list( - datasetName, - [ - "name", - "mountpoint", - "refquota", - "avail", - "used", - VOLUME_CSI_NAME_PROPERTY_NAME, - VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME, - VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME, - "volsize", - MANAGED_PROPERTY_NAME, - SHARE_VOLUME_CONTEXT_PROPERTY_NAME, - SUCCESS_PROPERTY_NAME, - VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME, - VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME, - ], - { types, recurse: true } - ); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return { - entries: [], - next_token: null, - }; - } - - throw err; - } - - driver.ctx.logger.debug("list volumes result: %j", response); - - // remove parent dataset from results - if (driverZfsResourceType == "filesystem") { - response.data.shift(); - } - - entries = []; - for (let row of response.indexed) { - // ignore rows were csi_name is empty - if (row[MANAGED_PROPERTY_NAME] != "true") { - continue; - } - - let volume = await driver.populateCsiVolumeFromData(row); - if (volume) { - let status = await driver.getVolumeStatus(datasetName); - entries.push({ - volume, - status, - }); - } - } - - if (max_entries && entries.length > max_entries) { - uuid = uuidv4(); - this.ctx.cache.set(`ListVolumes:result:${uuid}`, entries); - next_token = `${uuid}:${max_entries}`; - entries = entries.slice(0, max_entries); - } - - const data = { - entries: entries, - next_token: next_token, - }; - - return data; - } - - /** - * - * @param {*} call - */ - async ListSnapshots(call) { - const driver = this; - const driverZfsResourceType = this.getDriverZfsResourceType(); - const zb = await this.getZetabyte(); - - let entries = []; - let entries_length = 0; - let next_token; - let uuid; - - const max_entries = call.request.max_entries; - const starting_token = call.request.starting_token; - - let types = []; - - const volumeParentDatasetName = this.getVolumeParentDatasetName(); - const snapshotParentDatasetName = - this.getDetachedSnapshotParentDatasetName(); - - // get data from cache and return immediately - if (starting_token) { - let parts = starting_token.split(":"); - uuid = parts[0]; - let start_position = parseInt(parts[1]); - let end_position; - if (max_entries > 0) { - end_position = start_position + max_entries; - } - entries = this.ctx.cache.get(`ListSnapshots:result:${uuid}`); - if (entries) { - entries_length = entries.length; - entries = entries.slice(start_position, end_position); - if (max_entries > 0 && end_position > entries_length) { - next_token = `${uuid}:${end_position}`; - } else { - next_token = null; - } - const data = { - entries: entries, - next_token: next_token, - }; - - return data; - } else { - throw new GrpcError( - grpc.status.ABORTED, - `invalid starting_token: ${starting_token}` - ); - } - } - - if (!volumeParentDatasetName) { - // throw error - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - let snapshot_id = call.request.snapshot_id; - let source_volume_id = call.request.source_volume_id; - - entries = []; - for (let loopType of ["snapshot", "filesystem"]) { - let response, operativeFilesystem, operativeFilesystemType; - let datasetParentName; - switch (loopType) { - case "snapshot": - datasetParentName = volumeParentDatasetName; - types = ["snapshot"]; - // should only send 1 of snapshot_id or source_volume_id, preferring the former if sent - if (snapshot_id) { - if (!zb.helpers.isZfsSnapshot(snapshot_id)) { - continue; - } - operativeFilesystem = volumeParentDatasetName + "/" + snapshot_id; - operativeFilesystemType = 3; - } else if (source_volume_id) { - operativeFilesystem = - volumeParentDatasetName + "/" + source_volume_id; - operativeFilesystemType = 2; - } else { - operativeFilesystem = volumeParentDatasetName; - operativeFilesystemType = 1; - } - break; - case "filesystem": - datasetParentName = snapshotParentDatasetName; - if (!datasetParentName) { - continue; - } - if (driverZfsResourceType == "filesystem") { - types = ["filesystem"]; - } else { - types = ["volume"]; - } - - // should only send 1 of snapshot_id or source_volume_id, preferring the former if sent - if (snapshot_id) { - if (zb.helpers.isZfsSnapshot(snapshot_id)) { - continue; - } - operativeFilesystem = snapshotParentDatasetName + "/" + snapshot_id; - operativeFilesystemType = 3; - } else if (source_volume_id) { - operativeFilesystem = - snapshotParentDatasetName + "/" + source_volume_id; - operativeFilesystemType = 2; - } else { - operativeFilesystem = snapshotParentDatasetName; - operativeFilesystemType = 1; - } - break; - } - - try { - response = await zb.zfs.list( - operativeFilesystem, - [ - "name", - "creation", - "mountpoint", - "refquota", - "avail", - "used", - "volsize", - "referenced", - "logicalreferenced", - VOLUME_CSI_NAME_PROPERTY_NAME, - SNAPSHOT_CSI_NAME_PROPERTY_NAME, - MANAGED_PROPERTY_NAME, - ], - { types, recurse: true } - ); - } catch (err) { - let message; - if (err.toString().includes("dataset does not exist")) { - switch (operativeFilesystemType) { - case 1: - //message = `invalid configuration: datasetParentName ${datasetParentName} does not exist`; - continue; - break; - case 2: - message = `source_volume_id ${source_volume_id} does not exist`; - continue; - break; - case 3: - message = `snapshot_id ${snapshot_id} does not exist`; - continue; - break; - } - throw new GrpcError(grpc.status.NOT_FOUND, message); - } - throw new GrpcError(grpc.status.FAILED_PRECONDITION, err.toString()); - } - - response.indexed.forEach((row) => { - // skip any snapshots not explicitly created by CO - if (row[MANAGED_PROPERTY_NAME] != "true") { - return; - } - - // ignore snapshots that are not explicit CO snapshots - if ( - !zb.helpers.isPropertyValueSet(row[SNAPSHOT_CSI_NAME_PROPERTY_NAME]) - ) { - return; - } - - // strip parent dataset - let source_volume_id = row["name"].replace( - new RegExp("^" + datasetParentName + "/"), - "" - ); - - // strip snapshot details (@snapshot-name) - if (source_volume_id.includes("@")) { - source_volume_id = source_volume_id.substring( - 0, - source_volume_id.indexOf("@") - ); - } else { - source_volume_id = source_volume_id.replace( - new RegExp("/" + row[SNAPSHOT_CSI_NAME_PROPERTY_NAME] + "$"), - "" - ); - } - - if (source_volume_id == datasetParentName) { - return; - } - - // TODO: properly handle use-case where datasetEnableQuotas is not turned on - let size_bytes = 0; - if (driverZfsResourceType == "filesystem") { - // independent of detached snapshots when creating a volume from a 'snapshot' - // we could be using detached clones (ie: send/receive) - // so we must be cognizant and use the highest possible value here - // note that whatever value is returned here can/will essentially impact the refquota - // value of a derived volume - size_bytes = getLargestNumber(row.referenced, row.logicalreferenced); - } else { - // get the size of the parent volume - size_bytes = row.volsize; - } - - if (source_volume_id) - entries.push({ - snapshot: { - /** - * The purpose of this field is to give CO guidance on how much space - * is needed to create a volume from this snapshot. - * - * In that vein, I think it's best to return 0 here given the - * unknowns of 'cow' implications. - */ - size_bytes, - - // remove parent dataset details - snapshot_id: row["name"].replace( - new RegExp("^" + datasetParentName + "/"), - "" - ), - source_volume_id: source_volume_id, - //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto - creation_time: { - seconds: row["creation"], - nanos: 0, - }, - ready_to_use: true, - }, - }); - }); - } - - if (max_entries && entries.length > max_entries) { - uuid = uuidv4(); - this.ctx.cache.set(`ListSnapshots:result:${uuid}`, entries); - next_token = `${uuid}:${max_entries}`; - entries = entries.slice(0, max_entries); - } - - const data = { - entries: entries, - next_token: next_token, - }; - - return data; - } - - /** - * - * @param {*} call - */ - async CreateSnapshot(call) { - const driver = this; - const driverZfsResourceType = this.getDriverZfsResourceType(); - const zb = await this.getZetabyte(); - - let size_bytes = 0; - let detachedSnapshot = false; - try { - let tmpDetachedSnapshot = JSON.parse( - driver.getNormalizedParameterValue( - call.request.parameters, - "detachedSnapshots" - ) - ); // snapshot class parameter - if (typeof tmpDetachedSnapshot === "boolean") { - detachedSnapshot = tmpDetachedSnapshot; - } - } catch (e) {} - - let response; - const volumeParentDatasetName = this.getVolumeParentDatasetName(); - let datasetParentName; - let snapshotProperties = {}; - let types = []; - - if (detachedSnapshot) { - datasetParentName = this.getDetachedSnapshotParentDatasetName(); - if (driverZfsResourceType == "filesystem") { - types.push("filesystem"); - } else { - types.push("volume"); - } - } else { - datasetParentName = this.getVolumeParentDatasetName(); - types.push("snapshot"); - } - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - // both these are required - let source_volume_id = call.request.source_volume_id; - let name = call.request.name; - - if (!source_volume_id) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot source_volume_id is required` - ); - } - - if (!name) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot name is required` - ); - } - - const volumeDatasetName = volumeParentDatasetName + "/" + source_volume_id; - const datasetName = datasetParentName + "/" + source_volume_id; - snapshotProperties[SNAPSHOT_CSI_NAME_PROPERTY_NAME] = name; - snapshotProperties[SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME] = - source_volume_id; - snapshotProperties[MANAGED_PROPERTY_NAME] = "true"; - - driver.ctx.logger.verbose("requested snapshot name: %s", name); - - let invalid_chars; - invalid_chars = name.match(/[^a-z0-9_\-:.+]+/gi); - if (invalid_chars) { - invalid_chars = String.prototype.concat( - ...new Set(invalid_chars.join("")) - ); - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot name contains invalid characters: ${invalid_chars}` - ); - } - - // https://stackoverflow.com/questions/32106243/regex-to-remove-all-non-alpha-numeric-and-replace-spaces-with/32106277 - name = name.replace(/[^a-z0-9_\-:.+]+/gi, ""); - - driver.ctx.logger.verbose("cleansed snapshot name: %s", name); - - // check for other snapshopts with the same name on other volumes and fail as appropriate - { - try { - let datasets = []; - datasets = await zb.zfs.list( - this.getDetachedSnapshotParentDatasetName(), - [], - { recurse: true, types } - ); - for (let dataset of datasets.indexed) { - let parts = dataset.name.split("/").slice(-2); - if (parts[1] != name) { - continue; - } - - if (parts[0] != source_volume_id) { - throw new GrpcError( - grpc.status.ALREADY_EXISTS, - `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` - ); - } - } - } catch (err) { - if (!err.toString().includes("dataset does not exist")) { - throw err; - } - } - - let snapshots = []; - snapshots = await zb.zfs.list(this.getVolumeParentDatasetName(), [], { - recurse: true, - types, - }); - for (let snapshot of snapshots.indexed) { - let parts = zb.helpers.extractLeafName(snapshot.name).split("@"); - if (parts[1] != name) { - continue; - } - - if (parts[0] != source_volume_id) { - throw new GrpcError( - grpc.status.ALREADY_EXISTS, - `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` - ); - } - } - } - - let fullSnapshotName; - let snapshotDatasetName; - let tmpSnapshotName; - if (detachedSnapshot) { - fullSnapshotName = datasetName + "/" + name; - } else { - fullSnapshotName = datasetName + "@" + name; - } - - driver.ctx.logger.verbose("full snapshot name: %s", fullSnapshotName); - - if (detachedSnapshot) { - tmpSnapshotName = - volumeDatasetName + "@" + VOLUME_SOURCE_DETACHED_SNAPSHOT_PREFIX + name; - snapshotDatasetName = datasetName + "/" + name; - - await zb.zfs.create(datasetName, { parents: true }); - - try { - await zb.zfs.snapshot(tmpSnapshotName); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `snapshot source_volume_id ${source_volume_id} does not exist` - ); - } - - throw err; - } - - try { - response = await zb.zfs.send_receive( - tmpSnapshotName, - [], - snapshotDatasetName, - [] - ); - - response = await zb.zfs.set(snapshotDatasetName, snapshotProperties); - } catch (err) { - if ( - err.toString().includes("destination") && - err.toString().includes("exists") - ) { - // move along - } else { - throw err; - } - } - - // remove snapshot from target - await zb.zfs.destroy( - snapshotDatasetName + - "@" + - zb.helpers.extractSnapshotName(tmpSnapshotName), - { - recurse: true, - force: true, - defer: true, - } - ); - - // remove snapshot from source - await zb.zfs.destroy(tmpSnapshotName, { - recurse: true, - force: true, - defer: true, - }); - - // let things settle down - //await GneralUtils.sleep(3000); - } else { - try { - await zb.zfs.snapshot(fullSnapshotName, { - properties: snapshotProperties, - }); - - // let things settle down - //await GeneralUtils.sleep(3000); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `snapshot source_volume_id ${source_volume_id} does not exist` - ); - } - - throw err; - } - } - - // TODO: let things settle to ensure proper size_bytes is reported - // sysctl -d vfs.zfs.txg.timeout # vfs.zfs.txg.timeout: Max seconds worth of delta per txg - let properties; - properties = await zb.zfs.get( - fullSnapshotName, - [ - "name", - "creation", - "mountpoint", - "refquota", - "avail", - "used", - "volsize", - "referenced", - "refreservation", - "logicalused", - "logicalreferenced", - VOLUME_CSI_NAME_PROPERTY_NAME, - SNAPSHOT_CSI_NAME_PROPERTY_NAME, - SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME, - MANAGED_PROPERTY_NAME, - ], - { types } - ); - properties = properties[fullSnapshotName]; - driver.ctx.logger.verbose("snapshot properties: %j", properties); - - // TODO: properly handle use-case where datasetEnableQuotas is not turned on - if (driverZfsResourceType == "filesystem") { - // independent of detached snapshots when creating a volume from a 'snapshot' - // we could be using detached clones (ie: send/receive) - // so we must be cognizant and use the highest possible value here - // note that whatever value is returned here can/will essentially impact the refquota - // value of a derived volume - size_bytes = getLargestNumber( - properties.referenced.value, - properties.logicalreferenced.value - ); - } else { - // get the size of the parent volume - size_bytes = properties.volsize.value; - } - - // set this just before sending out response so we know if volume completed - // this should give us a relatively sane way to clean up artifacts over time - await zb.zfs.set(fullSnapshotName, { [SUCCESS_PROPERTY_NAME]: "true" }); - - return { - snapshot: { - /** - * The purpose of this field is to give CO guidance on how much space - * is needed to create a volume from this snapshot. - * - * In that vein, I think it's best to return 0 here given the - * unknowns of 'cow' implications. - */ - size_bytes, - - // remove parent dataset details - snapshot_id: properties.name.value.replace( - new RegExp("^" + datasetParentName + "/"), - "" - ), - source_volume_id: source_volume_id, - //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto - creation_time: { - seconds: properties.creation.value, - nanos: 0, - }, - ready_to_use: true, - }, - }; - } - - /** - * In addition, if clones have been created from a snapshot, then they must - * be destroyed before the snapshot can be destroyed. - * - * @param {*} call - */ - async DeleteSnapshot(call) { - const driver = this; - const zb = await this.getZetabyte(); - - const snapshot_id = call.request.snapshot_id; - - if (!snapshot_id) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `snapshot_id is required` - ); - } - - const detachedSnapshot = !zb.helpers.isZfsSnapshot(snapshot_id); - let datasetParentName; - - if (detachedSnapshot) { - datasetParentName = this.getDetachedSnapshotParentDatasetName(); - } else { - datasetParentName = this.getVolumeParentDatasetName(); - } - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - const fullSnapshotName = datasetParentName + "/" + snapshot_id; - - driver.ctx.logger.verbose("deleting snapshot: %s", fullSnapshotName); - - try { - await zb.zfs.destroy(fullSnapshotName, { - recurse: true, - force: true, - defer: zb.helpers.isZfsSnapshot(snapshot_id), // only defer when snapshot - }); - } catch (err) { - if (err.toString().includes("snapshot has dependent clones")) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - "snapshot has dependent clones" - ); - } - - throw err; - } - - // cleanup parent dataset if possible - if (detachedSnapshot) { - let containerDataset = - zb.helpers.extractParentDatasetName(fullSnapshotName); - try { - await this.removeSnapshotsFromDatatset(containerDataset); - await zb.zfs.destroy(containerDataset); - } catch (err) { - if (!err.toString().includes("filesystem has children")) { - throw err; - } - } - } - - return {}; - } - - /** - * - * @param {*} call - */ - async ValidateVolumeCapabilities(call) { - const driver = this; - const zb = await this.getZetabyte(); - - const volume_id = call.request.volume_id; - if (!volume_id) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); - } - - const capabilities = call.request.volume_capabilities; - if (!capabilities || capabilities.length === 0) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); - } - - let datasetParentName = this.getVolumeParentDatasetName(); - let name = volume_id; - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - const datasetName = datasetParentName + "/" + name; - try { - await zb.zfs.get(datasetName, []); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `invalid volume_id: ${volume_id}` - ); - } else { - throw err; - } - } - - const result = this.assertCapabilities(call.request.volume_capabilities); - - if (result.valid !== true) { - return { message: result.message }; - } - - return { - confirmed: { - volume_context: call.request.volume_context, - volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested - parameters: call.request.parameters, - }, - }; - } -} - -module.exports.ControllerZfsBaseDriver = ControllerZfsBaseDriver; diff --git a/src/driver/factory.js b/src/driver/factory.js index 0235758..204fc46 100644 --- a/src/driver/factory.js +++ b/src/driver/factory.js @@ -1,62 +1,14 @@ -const { FreeNASSshDriver } = require("./freenas/ssh"); const { FreeNASApiDriver } = require("./freenas/api"); -const { - ControllerLocalHostpathDriver, -} = require("./controller-local-hostpath"); -const { ControllerZfsGenericDriver } = require("./controller-zfs-generic"); -const { ControllerZfsLocalDriver } = require("./controller-zfs-local"); -const { - ZfsLocalEphemeralInlineDriver, -} = require("./zfs-local-ephemeral-inline"); - -const { ControllerNfsClientDriver } = require("./controller-nfs-client"); -const { ControllerSmbClientDriver } = require("./controller-smb-client"); -const { ControllerLustreClientDriver } = require("./controller-lustre-client"); -const { ControllerObjectiveFSDriver } = require("./controller-objectivefs"); -const { ControllerSynologyDriver } = require("./controller-synology"); -const { NodeManualDriver } = require("./node-manual"); function factory(ctx, options) { switch (options.driver) { - case "freenas-nfs": - case "freenas-smb": - case "freenas-iscsi": + // TrueNAS SCALE 25.04+ drivers using JSON-RPC over WebSocket case "truenas-nfs": - case "truenas-smb": case "truenas-iscsi": - return new FreeNASSshDriver(ctx, options); - case "freenas-api-iscsi": - case "freenas-api-nfs": - case "freenas-api-smb": + case "truenas-nvmeof": return new FreeNASApiDriver(ctx, options); - case "synology-nfs": - case "synology-smb": - case "synology-iscsi": - return new ControllerSynologyDriver(ctx, options); - case "zfs-generic-nfs": - case "zfs-generic-smb": - case "zfs-generic-iscsi": - case "zfs-generic-nvmeof": - return new ControllerZfsGenericDriver(ctx, options); - case "zfs-local-dataset": - case "zfs-local-zvol": - return new ControllerZfsLocalDriver(ctx, options); - case "zfs-local-ephemeral-inline": - return new ZfsLocalEphemeralInlineDriver(ctx, options); - case "smb-client": - return new ControllerSmbClientDriver(ctx, options); - case "nfs-client": - return new ControllerNfsClientDriver(ctx, options); - case "local-hostpath": - return new ControllerLocalHostpathDriver(ctx, options); - case "lustre-client": - return new ControllerLustreClientDriver(ctx, options); - case "objectivefs": - return new ControllerObjectiveFSDriver(ctx, options); - case "node-manual": - return new NodeManualDriver(ctx, options); default: - throw new Error("invalid csi driver: " + options.driver); + throw new Error("invalid csi driver: " + options.driver + ". Only truenas-nfs, truenas-iscsi, and truenas-nvmeof are supported."); } } diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index d4ec1c2..a1457b0 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -1,16 +1,25 @@ - const { sleep, stringify } = require("../../../utils/general"); const { Zetabyte } = require("../../../utils/zfs"); -// used for in-memory cache of the version info -const FREENAS_SYSTEM_VERSION_CACHE_KEY = "freenas:system_version"; -const __REGISTRY_NS__ = "FreeNASHttpApi"; +// Registry namespace for cached objects +const __REGISTRY_NS__ = "TrueNASWebSocketApi"; +/** + * TrueNAS SCALE 25.04+ JSON-RPC API Wrapper + * + * This class provides a clean interface to TrueNAS SCALE 25.04+ API + * using WebSocket JSON-RPC 2.0 protocol only. + * + * All legacy support for FreeNAS and old TrueNAS versions has been removed. + * + * API Documentation: https://api.truenas.com/v25.04.2/ + */ class Api { constructor(client, cache, options = {}) { this.client = client; this.cache = cache; this.options = options; + this.ctx = options.ctx; } async getHttpClient() { @@ -18,8 +27,7 @@ class Api { } /** - * only here for the helpers - * @returns + * Get ZFS helper utility (for local operations only) */ async getZetabyte() { return this.ctx.registry.get(`${__REGISTRY_NS__}:zb`, () => { @@ -27,7 +35,7 @@ class Api { executor: { spawn: function () { throw new Error( - "cannot use the zb implementation to execute zfs commands, must use the http api" + "Cannot use ZFS executor directly - must use WebSocket API" ); }, }, @@ -35,781 +43,425 @@ class Api { }); } - async findResourceByProperties(endpoint, match) { - if (!match) { - return; - } - - if (typeof match === "object" && Object.keys(match).length < 1) { - return; - } - - const httpClient = await this.getHttpClient(); - let target; - let page = 0; - let lastReponse; - - // loop and find target - let queryParams = {}; - queryParams.limit = 100; - queryParams.offset = 0; - - while (!target) { - //Content-Range: items 0-2/3 (full set) - //Content-Range: items 0--1/3 (invalid offset) - if (queryParams.hasOwnProperty("offset")) { - queryParams.offset = queryParams.limit * page; - } - - // crude stoppage attempt - let response = await httpClient.get(endpoint, queryParams); - if (lastReponse) { - if (JSON.stringify(lastReponse) == JSON.stringify(response)) { - break; - } - } - lastReponse = response; - - if (response.statusCode == 200) { - if (response.body.length < 1) { - break; - } - response.body.some((i) => { - let isMatch = true; - - if (typeof match === "function") { - isMatch = match(i); - } else { - for (let property in match) { - if (match[property] != i[property]) { - isMatch = false; - break; - } - } - } - - if (isMatch) { - target = i; - return true; - } - - return false; - }); - } else { - throw new Error( - "FreeNAS http error - code: " + - response.statusCode + - " body: " + - JSON.stringify(response.body) - ); - } - page++; - } - - return target; - } - - async getApiVersion() { - const systemVersion = await this.getSystemVersion(); - - if (systemVersion.v2) { - if ((await this.getSystemVersionMajorMinor()) == 11.2) { - return 1; - } - return 2; - } - - if (systemVersion.v1) { - return 1; - } - - return 2; - } - - async getIsFreeNAS() { - const systemVersion = await this.getSystemVersion(); - let version; - - if (systemVersion.v2) { - version = systemVersion.v2; - } else { - version = systemVersion.v1.fullversion; - } - - if (version.toLowerCase().includes("freenas")) { - return true; - } - - return false; - } - - async getIsTrueNAS() { - const systemVersion = await this.getSystemVersion(); - let version; - - if (systemVersion.v2) { - version = systemVersion.v2; - } else { - version = systemVersion.v1.fullversion; - } - - if (version.toLowerCase().includes("truenas")) { - return true; - } - - return false; - } - - async getIsScale() { - const systemVersion = await this.getSystemVersion(); - - if (systemVersion.v2 && systemVersion.v2.toLowerCase().includes("scale")) { - return true; - } - - return false; - } - - async getSystemVersionMajorMinor() { - const systemVersion = await this.getSystemVersion(); - let parts; - let parts_i; - let version; - - /* - systemVersion.v2 = "FreeNAS-11.2-U5"; - systemVersion.v2 = "TrueNAS-SCALE-20.11-MASTER-20201127-092915"; - systemVersion.v1 = { - fullversion: "FreeNAS-9.3-STABLE-201503200528", - fullversion: "FreeNAS-11.2-U5 (c129415c52)", - }; - - systemVersion.v2 = null; - */ - - if (systemVersion.v2) { - version = systemVersion.v2; - } else { - version = systemVersion.v1.fullversion; - } - - if (version) { - parts = version.split("-"); - parts_i = []; - parts.forEach((value) => { - let i = value.replace(/[^\d.]/g, ""); - if (i.length > 0) { - parts_i.push(i); - } - }); - - // join and resplit to deal with single elements which contain a decimal - parts_i = parts_i.join(".").split("."); - parts_i.splice(2); - return parts_i.join("."); - } - } - - async getSystemVersionMajor() { - const majorMinor = await this.getSystemVersionMajorMinor(); - return majorMinor.split(".")[0]; - } - - async setVersionInfoCache(versionInfo) { - await this.cache.set(FREENAS_SYSTEM_VERSION_CACHE_KEY, versionInfo, { - ttl: 60 * 1000, - }); - } - - async getSystemVersion() { - let cacheData = await this.cache.get(FREENAS_SYSTEM_VERSION_CACHE_KEY); - - if (cacheData) { - return cacheData; - } - - const httpClient = await this.getHttpClient(false); - const endpoint = "/system/version/"; - let response; - const startApiVersion = httpClient.getApiVersion(); - const versionInfo = {}; - const versionErrors = {}; - const versionResponses = {}; - - httpClient.setApiVersion(2); - /** - * FreeNAS-11.2-U5 - * TrueNAS-12.0-RELEASE - * TrueNAS-SCALE-20.11-MASTER-20201127-092915 - */ - try { - response = await httpClient.get(endpoint, null, { timeout: 5 * 1000 }); - versionResponses.v2 = response; - if (response.statusCode == 200) { - versionInfo.v2 = response.body; - - // return immediately to save on resources and silly requests - await this.setVersionInfoCache(versionInfo); - - // reset apiVersion - httpClient.setApiVersion(startApiVersion); - - return versionInfo; - } - } catch (e) { - // if more info is needed use e.stack - versionErrors.v2 = e.toString(); - } - - httpClient.setApiVersion(1); - /** - * {"fullversion": "FreeNAS-9.3-STABLE-201503200528", "name": "FreeNAS", "version": "9.3"} - * {"fullversion": "FreeNAS-11.2-U5 (c129415c52)", "name": "FreeNAS", "version": ""} - */ - try { - response = await httpClient.get(endpoint, null, { timeout: 5 * 1000 }); - versionResponses.v1 = response; - if (response.statusCode == 200 && IsJsonString(response.body)) { - versionInfo.v1 = response.body; - await this.setVersionInfoCache(versionInfo); - - // reset apiVersion - httpClient.setApiVersion(startApiVersion); - - return versionInfo; - } - } catch (e) { - // if more info is needed use e.stack - versionErrors.v1 = e.toString(); - } - - // throw error if cannot get v1 or v2 data - // likely bad creds/url - throw new Error( - `FreeNAS error getting system version info: ${stringify({ - errors: versionErrors, - responses: versionResponses, - })}` - ); - } - - getIsUserProperty(property) { - if (property.includes(":")) { - return true; - } - return false; - } - - getUserProperties(properties) { - let user_properties = {}; - for (const property in properties) { - if (this.getIsUserProperty(property)) { - user_properties[property] = properties[property]; - } - } - - return user_properties; - } - - getSystemProperties(properties) { - let system_properties = {}; - for (const property in properties) { - if (!this.getIsUserProperty(property)) { - system_properties[property] = properties[property]; - } - } - - return system_properties; - } - - getPropertiesKeyValueArray(properties) { - let arr = []; - for (const property in properties) { - arr.push({ key: property, value: properties[property] }); - } - - return arr; - } - - normalizeProperties(dataset, properties) { - let res = {}; - for (const property of properties) { - let p; - if (dataset.hasOwnProperty(property)) { - p = dataset[property]; - } else if ( - dataset.properties && - dataset.properties.hasOwnProperty(property) - ) { - p = dataset.properties[property]; - } else if ( - dataset.user_properties && - dataset.user_properties.hasOwnProperty(property) - ) { - p = dataset.user_properties[property]; - } else { - p = { - value: "-", - rawvalue: "-", - source: "-", - }; - } - - if (typeof p === "object" && p !== null) { - // nothing, leave as is - } else { - p = { - value: p, - rawvalue: p, - source: "-", - }; - } - - res[property] = p; - } - - return res; - } - - async DatasetCreate(datasetName, data) { - const httpClient = await this.getHttpClient(false); - let response; - let endpoint; - - data.name = datasetName; - - endpoint = "/pool/dataset"; - response = await httpClient.post(endpoint, data); - - if (response.statusCode == 200) { - return; - } - - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes("already exists") - ) { - return; - } - - throw new Error(JSON.stringify(response.body)); + /** + * Call a JSON-RPC method on the TrueNAS API + */ + async call(method, params = []) { + const client = await this.getHttpClient(); + return await client.call(method, params); } /** - * - * @param {*} datasetName - * @param {*} data - * @returns + * Query resources with optional filters + * @param {string} method - The query method (e.g., "pool.dataset.query") + * @param {array} filters - Query filters + * @param {object} options - Query options (limit, offset, etc.) */ - async DatasetDelete(datasetName, data) { - const httpClient = await this.getHttpClient(false); - let response; - let endpoint; - - endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; - response = await httpClient.delete(endpoint, data); - - if (response.statusCode == 200) { - return; - } - - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes("does not exist") - ) { - return; - } - - throw new Error(JSON.stringify(response.body)); + async query(method, filters = [], options = {}) { + return await this.call(method, [filters, options]); } - async DatasetSet(datasetName, properties) { - const httpClient = await this.getHttpClient(false); - let response; - let endpoint; + /** + * Find a single resource by properties + * @param {string} method - The query method + * @param {object|function} match - Properties to match or matcher function + */ + async findResourceByProperties(method, match) { + if (!match) { + return null; + } - endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; - response = await httpClient.put(endpoint, { + if (typeof match === "object" && Object.keys(match).length < 1) { + return null; + } + + const results = await this.query(method); + + if (!Array.isArray(results)) { + return null; + } + + return results.find((item) => { + if (typeof match === "function") { + return match(item); + } + + for (let property in match) { + if (match[property] !== item[property]) { + return false; + } + } + return true; + }); + } + + // ============================================================================ + // SYSTEM VERSION & INFO + // ============================================================================ + + /** + * Get TrueNAS system version info + * This is cached to avoid repeated calls + */ + async getSystemVersion() { + const cacheKey = "truenas:system_version"; + let cached = this.cache.get(cacheKey); + + if (cached) { + return cached; + } + + const version = await this.call("system.version"); + this.cache.set(cacheKey, version, 300); // Cache for 5 minutes + return version; + } + + /** + * Get system info (hostname, version, etc.) + */ + async getSystemInfo() { + return await this.call("system.info"); + } + + // ============================================================================ + // POOL & DATASET OPERATIONS + // ============================================================================ + + /** + * Create a new dataset + * @param {string} datasetName - Full dataset name (e.g., "pool/dataset") + * @param {object} data - Dataset properties + */ + async DatasetCreate(datasetName, data = {}) { + try { + const params = { + name: datasetName, + ...data, + }; + + await this.call("pool.dataset.create", [params]); + } catch (error) { + // Ignore "already exists" errors + if (error.message && error.message.includes("already exists")) { + return; + } + throw error; + } + } + + /** + * Delete a dataset + * @param {string} datasetName - Full dataset name + * @param {object} data - Delete options (recursive, force, etc.) + */ + async DatasetDelete(datasetName, data = {}) { + try { + await this.call("pool.dataset.delete", [datasetName, data]); + } catch (error) { + // Ignore "does not exist" errors + if (error.message && error.message.includes("does not exist")) { + return; + } + throw error; + } + } + + /** + * Update dataset properties + * @param {string} datasetName - Full dataset name + * @param {object} properties - Properties to update + */ + async DatasetSet(datasetName, properties) { + const params = { ...this.getSystemProperties(properties), user_properties_update: this.getPropertiesKeyValueArray( this.getUserProperties(properties) ), - }); + }; - if (response.statusCode == 200) { - return; - } - - throw new Error(JSON.stringify(response.body)); - } - - async DatasetInherit(datasetName, property) { - const httpClient = await this.getHttpClient(false); - let response; - let endpoint; - let system_properties = {}; - let user_properties_update = []; - - const isUserProperty = this.getIsUserProperty(property); - if (isUserProperty) { - user_properties_update = [{ key: property, remove: true }]; - } else { - system_properties[property] = "INHERIT"; - } - - endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; - response = await httpClient.put(endpoint, { - ...system_properties, - user_properties_update, - }); - - if (response.statusCode == 200) { - return; - } - - throw new Error(JSON.stringify(response.body)); + await this.call("pool.dataset.update", [datasetName, params]); } /** - * - * zfs get -Hp all tank/k8s/test/PVC-111 - * - * @param {*} datasetName - * @param {*} properties - * @returns + * Inherit a dataset property from parent + * @param {string} datasetName - Full dataset name + * @param {string} property - Property name to inherit */ - async DatasetGet(datasetName, properties) { - const httpClient = await this.getHttpClient(false); - let response; - let endpoint; + async DatasetInherit(datasetName, property) { + const isUserProperty = this.getIsUserProperty(property); + let params = {}; - endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; - response = await httpClient.get(endpoint); - - if (response.statusCode == 200) { - return this.normalizeProperties(response.body, properties); + if (isUserProperty) { + params.user_properties_update = [{ key: property, remove: true }]; + } else { + params[property] = { source: "INHERIT" }; } - if (response.statusCode == 404) { - throw new Error("dataset does not exist"); - } - - throw new Error(JSON.stringify(response.body)); + await this.call("pool.dataset.update", [datasetName, params]); } + /** + * Get dataset properties + * @param {string} datasetName - Full dataset name + * @param {array} properties - Specific properties to retrieve (optional) + */ + async DatasetGet(datasetName, properties = []) { + const filters = [["id", "=", datasetName]]; + const options = {}; + + if (properties && properties.length > 0) { + options.select = properties; + } + + const results = await this.query("pool.dataset.query", filters, options); + + if (!results || results.length === 0) { + throw new Error(`Dataset not found: ${datasetName}`); + } + + return results[0]; + } + + /** + * Destroy snapshots matching criteria + * @param {string} datasetName - Dataset name + * @param {object} data - Snapshot destruction criteria + */ async DatasetDestroySnapshots(datasetName, data = {}) { - const httpClient = await this.getHttpClient(false); - let response; - let endpoint; - - data.name = datasetName; - - endpoint = "/pool/dataset/destroy_snapshots"; - response = await httpClient.post(endpoint, data); - - if (response.statusCode == 200) { - return response.body; - } - - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes("already exists") - ) { - return; - } - - throw new Error(JSON.stringify(response.body)); + await this.call("pool.dataset.destroy_snapshots", [datasetName, data]); } - async SnapshotSet(snapshotName, properties) { - const httpClient = await this.getHttpClient(false); - let response; - let endpoint; + // ============================================================================ + // SNAPSHOT OPERATIONS + // ============================================================================ - endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; - response = await httpClient.put(endpoint, { - //...this.getSystemProperties(properties), + /** + * Create a snapshot + * @param {string} snapshotName - Full snapshot name (dataset@snapshot) + * @param {object} data - Snapshot options + */ + async SnapshotCreate(snapshotName, data = {}) { + const parts = snapshotName.split("@"); + if (parts.length !== 2) { + throw new Error(`Invalid snapshot name: ${snapshotName}`); + } + + const params = { + dataset: parts[0], + name: parts[1], + ...data, + }; + + await this.call("zfs.snapshot.create", [params]); + } + + /** + * Delete a snapshot + * @param {string} snapshotName - Full snapshot name (dataset@snapshot) + * @param {object} data - Delete options + */ + async SnapshotDelete(snapshotName, data = {}) { + try { + await this.call("zfs.snapshot.delete", [snapshotName, data]); + } catch (error) { + // Ignore "does not exist" errors + if (error.message && error.message.includes("does not exist")) { + return; + } + throw error; + } + } + + /** + * Update snapshot properties + * @param {string} snapshotName - Full snapshot name + * @param {object} properties - Properties to update + */ + async SnapshotSet(snapshotName, properties) { + const params = { + ...this.getSystemProperties(properties), user_properties_update: this.getPropertiesKeyValueArray( this.getUserProperties(properties) ), - }); + }; - if (response.statusCode == 200) { - return; - } - - throw new Error(JSON.stringify(response.body)); + await this.call("zfs.snapshot.update", [snapshotName, params]); } /** - * - * zfs get -Hp all tank/k8s/test/PVC-111 - * - * @param {*} snapshotName - * @param {*} properties - * @returns + * Get snapshot properties + * @param {string} snapshotName - Full snapshot name + * @param {array} properties - Specific properties to retrieve (optional) */ - async SnapshotGet(snapshotName, properties) { - const httpClient = await this.getHttpClient(false); - let response; - let endpoint; + async SnapshotGet(snapshotName, properties = []) { + const filters = [["id", "=", snapshotName]]; + const options = {}; - endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; - response = await httpClient.get(endpoint); - - if (response.statusCode == 200) { - return this.normalizeProperties(response.body, properties); + if (properties && properties.length > 0) { + options.select = properties; } - if (response.statusCode == 404) { - throw new Error("dataset does not exist"); + const results = await this.query("zfs.snapshot.query", filters, options); + + if (!results || results.length === 0) { + throw new Error(`Snapshot not found: ${snapshotName}`); } - throw new Error(JSON.stringify(response.body)); + return results[0]; } - async SnapshotCreate(snapshotName, data = {}) { - const httpClient = await this.getHttpClient(false); - const zb = await this.getZetabyte(); - - let response; - let endpoint; - - const dataset = zb.helpers.extractDatasetName(snapshotName); - const snapshot = zb.helpers.extractSnapshotName(snapshotName); - - data.dataset = dataset; - data.name = snapshot; - - endpoint = "/zfs/snapshot"; - response = await httpClient.post(endpoint, data); - - if (response.statusCode == 200) { - return; - } - - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes("already exists") - ) { - return; - } - - throw new Error(JSON.stringify(response.body)); - } - - async SnapshotDelete(snapshotName, data = {}) { - const httpClient = await this.getHttpClient(false); - const zb = await this.getZetabyte(); - - let response; - let endpoint; - - endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; - response = await httpClient.delete(endpoint, data); - - if (response.statusCode == 200) { - return; - } - - if (response.statusCode == 404) { - return; - } - - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes("not found") - ) { - return; - } - - throw new Error(JSON.stringify(response.body)); - } - - async CloneCreate(snapshotName, datasetName, data = {}) { - const httpClient = await this.getHttpClient(false); - const zb = await this.getZetabyte(); - - let response; - let endpoint; - - data.snapshot = snapshotName; - data.dataset_dst = datasetName; - - endpoint = "/zfs/snapshot/clone"; - response = await httpClient.post(endpoint, data); - - if (response.statusCode == 200) { - return; - } - - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes("already exists") - ) { - return; - } - - throw new Error(JSON.stringify(response.body)); - } - - // get all dataset snapshots - // https://github.com/truenas/middleware/pull/6934 - // then use core.bulk to delete all + // ============================================================================ + // CLONE OPERATIONS + // ============================================================================ /** - * - * /usr/lib/python3/dist-packages/middlewared/plugins/replication.py - * readonly enum=["SET", "REQUIRE", "IGNORE"] - * - * @param {*} data - * @returns + * Clone a snapshot to create a new dataset + * @param {string} snapshotName - Source snapshot name + * @param {string} datasetName - Target dataset name + * @param {object} data - Clone options + */ + async CloneCreate(snapshotName, datasetName, data = {}) { + const params = { + snapshot: snapshotName, + dataset_dst: datasetName, + ...data, + }; + + await this.call("zfs.snapshot.clone", [params]); + } + + // ============================================================================ + // REPLICATION + // ============================================================================ + + /** + * Run a one-time replication task + * @param {object} data - Replication configuration */ async ReplicationRunOnetime(data) { - const httpClient = await this.getHttpClient(false); - - let response; - let endpoint; - - endpoint = "/replication/run_onetime"; - response = await httpClient.post(endpoint, data); - - // 200 means the 'job' was accepted only - // must continue to check the status of the job to know when it has finished and if it was successful - // /core/get_jobs [["id", "=", jobidhere]] - if (response.statusCode == 200) { - return response.body; - } - - throw new Error(JSON.stringify(response.body)); + const jobId = await this.call("replication.run_onetime", [data]); + return jobId; } + // ============================================================================ + // JOB MANAGEMENT + // ============================================================================ + /** - * - * @param {*} job_id - * @param {*} timeout in seconds - * @returns + * Wait for a job to complete + * @param {number} jobId - Job ID to wait for + * @param {number} timeout - Timeout in seconds (0 = no timeout) + * @param {number} checkInterval - Interval between checks in milliseconds */ - async CoreWaitForJob(job_id, timeout = 0, check_interval = 3000) { - if (!job_id) { - throw new Error("invalid job_id"); - } + async CoreWaitForJob(jobId, timeout = 0, checkInterval = 3000) { + const startTime = Date.now(); - const startTime = Date.now() / 1000; - let currentTime; + while (true) { + const job = await this.call("core.get_jobs", [[["id", "=", jobId]]]); - let job; - - // wait for job to finish - do { - currentTime = Date.now() / 1000; - if (timeout > 0 && currentTime > startTime + timeout) { - throw new Error("timeout waiting for job to complete"); + if (!job || job.length === 0) { + throw new Error(`Job ${jobId} not found`); } - if (job) { - await sleep(check_interval); + const jobInfo = job[0]; + + // Check job state + if (jobInfo.state === "SUCCESS") { + return jobInfo; + } else if (jobInfo.state === "FAILED") { + throw new Error(`Job ${jobId} failed: ${jobInfo.error || "Unknown error"}`); + } else if (jobInfo.state === "ABORTED") { + throw new Error(`Job ${jobId} was aborted`); } - job = await this.CoreGetJobs({ id: job_id }); - job = job[0]; - } while (!["SUCCESS", "ABORTED", "FAILED"].includes(job.state)); - return job; - } + // Check timeout + if (timeout > 0) { + const elapsed = (Date.now() - startTime) / 1000; + if (elapsed >= timeout) { + throw new Error(`Job ${jobId} timed out after ${timeout} seconds`); + } + } - async CoreGetJobs(data) { - const httpClient = await this.getHttpClient(false); - - let response; - let endpoint; - - endpoint = "/core/get_jobs"; - response = await httpClient.get(endpoint, data); - - // 200 means the 'job' was accepted only - // must continue to check the status of the job to know when it has finished and if it was successful - // /core/get_jobs [["id", "=", jobidhere]] - // state = SUCCESS/ABORTED/FAILED means finality has been reached - // state = RUNNING - if (response.statusCode == 200) { - return response.body; + // Wait before checking again + await sleep(checkInterval); } - - throw new Error(JSON.stringify(response.body)); } /** - * - * @param {*} data + * Get jobs matching filters + * @param {array} filters - Job filters + */ + async CoreGetJobs(filters = []) { + return await this.call("core.get_jobs", [filters]); + } + + // ============================================================================ + // FILESYSTEM PERMISSIONS + // ============================================================================ + + /** + * Set filesystem permissions + * @param {object} data - Permission data (path, mode, uid, gid, options) */ async FilesystemSetperm(data) { - /* - { - "path": "string", - "mode": "string", - "uid": 0, - "gid": 0, - "options": { - "stripacl": false, - "recursive": false, - "traverse": false - } - } - */ - - const httpClient = await this.getHttpClient(false); - let response; - let endpoint; - - endpoint = `/filesystem/setperm`; - response = await httpClient.post(endpoint, data); - - if (response.statusCode == 200) { - return response.body; - } - - throw new Error(JSON.stringify(response.body)); + const jobId = await this.call("filesystem.setperm", [data]); + return jobId; } /** - * - * @param {*} data + * Change filesystem ownership + * @param {object} data - Ownership data (path, uid, gid, options) */ async FilesystemChown(data) { - /* - { - "path": "string", - "uid": 0, - "gid": 0, - "options": { - "recursive": false, - "traverse": false - } - } - */ + await this.call("filesystem.chown", [data]); + } - const httpClient = await this.getHttpClient(false); - let response; - let endpoint; + // ============================================================================ + // PROPERTY HELPERS + // ============================================================================ - endpoint = `/filesystem/chown`; - response = await httpClient.post(endpoint, data); + /** + * Check if a property is a user property + */ + getIsUserProperty(property) { + return property.includes(":"); + } - if (response.statusCode == 200) { - return response.body; + /** + * Split properties into system and user properties + */ + getSystemProperties(properties) { + const systemProps = {}; + for (const [key, value] of Object.entries(properties)) { + if (!this.getIsUserProperty(key)) { + systemProps[key] = value; + } } - - throw new Error(JSON.stringify(response.body)); + return systemProps; } -} -function IsJsonString(str) { - try { - JSON.parse(str); - } catch (e) { - return false; + /** + * Get only user properties + */ + getUserProperties(properties) { + const userProps = {}; + for (const [key, value] of Object.entries(properties)) { + if (this.getIsUserProperty(key)) { + userProps[key] = value; + } + } + return userProps; + } + + /** + * Convert properties object to key-value array format + */ + getPropertiesKeyValueArray(properties) { + return Object.entries(properties).map(([key, value]) => ({ + key, + value: String(value), + })); } - return true; } module.exports.Api = Api; diff --git a/src/driver/freenas/http/index.js b/src/driver/freenas/http/index.js index 54f4a34..7be1b22 100644 --- a/src/driver/freenas/http/index.js +++ b/src/driver/freenas/http/index.js @@ -1,244 +1,348 @@ const _ = require("lodash"); -const http = require("http"); -const https = require("https"); -const URI = require("uri-js"); -const { axios_request, stringify } = require("../../../utils/general"); +const WebSocket = require("ws"); +const { stringify } = require("../../../utils/general"); const USER_AGENT = "democratic-csi-driver"; +/** + * TrueNAS SCALE 25.04+ WebSocket JSON-RPC 2.0 Client + * + * This client implements the JSON-RPC 2.0 protocol over WebSocket + * for communication with TrueNAS SCALE 25.04 and later versions. + * + * References: + * - https://api.truenas.com/v25.04.2/jsonrpc.html + * - https://github.com/truenas/api_client + */ class Client { constructor(options = {}) { this.options = JSON.parse(JSON.stringify(options)); this.logger = console; - - // default to v1.0 for now - if (!this.options.apiVersion) { - this.options.apiVersion = 2; - } + this.ws = null; + this.authenticated = false; + this.messageId = 0; + this.pendingRequests = new Map(); + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 5; + this.reconnectDelay = 2000; // Start with 2 seconds + this.eventSubscriptions = new Map(); + this.connectPromise = null; } - getHttpAgent() { - if (!this.httpAgent) { - this.httpAgent = new http.Agent({ - keepAlive: true, - maxSockets: Infinity, - rejectUnauthorized: !!!this.options.allowInsecure, - }); - } - - return this.httpAgent; - } - - getHttpsAgent() { - if (!this.httpsAgent) { - this.httpsAgent = new https.Agent({ - keepAlive: true, - maxSockets: Infinity, - rejectUnauthorized: !!!this.options.allowInsecure, - }); - } - - return this.httpsAgent; - } - - getBaseURL() { + /** + * Get WebSocket URL for TrueNAS SCALE 25.04+ + * Format: ws://host:port/api/current or wss://host:port/api/current + */ + getWebSocketURL() { const server = this.options; - if (!server.protocol) { - if (server.port) { - if (String(server.port).includes("80")) { - server.protocol = "http"; - } - if (String(server.port).includes("443")) { - server.protocol = "https"; - } + + // Determine protocol + let protocol = server.protocol || "http"; + if (server.port) { + if (String(server.port).includes("443")) { + protocol = "https"; + } else if (String(server.port).includes("80")) { + protocol = "http"; } } - if (!server.protocol) { - server.protocol = "http"; + + // Convert http/https to ws/wss + const wsProtocol = protocol === "https" ? "wss" : "ws"; + + // Build WebSocket URL - use /api/current for versioned JSON-RPC API + const port = server.port ? `:${server.port}` : ""; + return `${wsProtocol}://${server.host}${port}/api/current`; + } + + /** + * Connect to TrueNAS WebSocket API and authenticate + */ + async connect() { + // Return existing connection promise if already connecting + if (this.connectPromise) { + return this.connectPromise; } - const options = { - scheme: server.protocol, - host: server.host, - port: server.port, - //userinfo: server.username + ":" + server.password, - path: server.apiVersion == 1 ? "/api/v1.0" : "/api/v2.0", - }; - return URI.serialize(options); + // Return immediately if already connected and authenticated + if (this.ws && this.ws.readyState === WebSocket.OPEN && this.authenticated) { + return Promise.resolve(); + } + + this.connectPromise = this._doConnect(); + + try { + await this.connectPromise; + } finally { + this.connectPromise = null; + } } - setApiVersion(apiVersion) { - this.options.apiVersion = apiVersion; - } + async _doConnect() { + const url = this.getWebSocketURL(); + this.logger.debug(`Connecting to TrueNAS WebSocket API: ${url}`); - getApiVersion() { - return this.options.apiVersion; - } + return new Promise((resolve, reject) => { + try { + const wsOptions = { + headers: { + "User-Agent": USER_AGENT, + }, + }; - getRequestCommonOptions() { - const client = this; - const options = { - headers: { - Accept: "application/json", - "User-Agent": USER_AGENT, - "Content-Type": "application/json", - }, - responseType: "json", - httpAgent: this.getHttpAgent(), - httpsAgent: this.getHttpsAgent(), - timeout: 60 * 1000, - validateStatus: function (status) { - if (status >= 500) { - return false; + // Handle insecure TLS + if (this.options.allowInsecure) { + wsOptions.rejectUnauthorized = false; } - return true; - }, + + this.ws = new WebSocket(url, wsOptions); + + this.ws.on("open", async () => { + this.logger.debug("WebSocket connection established"); + this.reconnectAttempts = 0; + + try { + // Authenticate immediately after connection + await this._authenticate(); + this.authenticated = true; + resolve(); + } catch (error) { + this.logger.error("Authentication failed:", error); + reject(error); + } + }); + + this.ws.on("message", (data) => { + this._handleMessage(data); + }); + + this.ws.on("error", (error) => { + this.logger.error("WebSocket error:", error); + if (!this.authenticated) { + reject(error); + } + }); + + this.ws.on("close", (code, reason) => { + this.logger.warn(`WebSocket closed: ${code} - ${reason}`); + this.authenticated = false; + + // Reject all pending requests + this.pendingRequests.forEach((pending) => { + pending.reject(new Error("WebSocket connection closed")); + }); + this.pendingRequests.clear(); + + // Attempt to reconnect + this._scheduleReconnect(); + }); + + } catch (error) { + reject(error); + } + }); + } + + /** + * Authenticate with TrueNAS API + */ + async _authenticate() { + if (this.options.apiKey) { + // Authenticate with API key + this.logger.debug("Authenticating with API key"); + await this.call("auth.login_with_api_key", [this.options.apiKey]); + } else if (this.options.username && this.options.password) { + // Authenticate with username/password + this.logger.debug(`Authenticating with username: ${this.options.username}`); + await this.call("auth.login", [this.options.username, this.options.password]); + } else { + throw new Error("No authentication credentials provided (apiKey or username/password required)"); + } + this.logger.debug("Authentication successful"); + } + + /** + * Schedule reconnection with exponential backoff + */ + _scheduleReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + this.logger.error("Max reconnection attempts reached"); + return; + } + + this.reconnectAttempts++; + const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); + + this.logger.info(`Scheduling reconnection attempt ${this.reconnectAttempts} in ${delay}ms`); + + setTimeout(() => { + this.connect().catch((error) => { + this.logger.error("Reconnection failed:", error); + }); + }, delay); + } + + /** + * Handle incoming WebSocket messages + */ + _handleMessage(data) { + try { + const message = JSON.parse(data.toString()); + + this.logger.debug("Received message:", stringify(message)); + + // Handle JSON-RPC response + if (message.id !== undefined && this.pendingRequests.has(message.id)) { + const pending = this.pendingRequests.get(message.id); + this.pendingRequests.delete(message.id); + + if (message.error) { + pending.reject(this._createError(message.error)); + } else { + pending.resolve(message.result); + } + } + // Handle JSON-RPC notification (event) + else if (message.method && message.id === undefined) { + this._handleEvent(message.method, message.params); + } + } catch (error) { + this.logger.error("Error parsing WebSocket message:", error); + } + } + + /** + * Handle event notifications + */ + _handleEvent(method, params) { + const handlers = this.eventSubscriptions.get(method) || []; + handlers.forEach((handler) => { + try { + handler(params); + } catch (error) { + this.logger.error(`Error in event handler for ${method}:`, error); + } + }); + } + + /** + * Create error object from JSON-RPC error + */ + _createError(error) { + const err = new Error(error.message || "TrueNAS API Error"); + err.code = error.code; + err.data = error.data; + return err; + } + + /** + * Make a JSON-RPC 2.0 method call + * + * @param {string} method - The API method to call (e.g., "pool.dataset.query") + * @param {array} params - Array of parameters for the method + * @param {object} options - Additional options (timeout, etc.) + * @returns {Promise} - Promise that resolves with the result + */ + async call(method, params = [], options = {}) { + // Ensure connection is established + if (!this.authenticated) { + await this.connect(); + } + + const messageId = ++this.messageId; + const request = { + jsonrpc: "2.0", + id: messageId, + method: method, + params: params, }; - if (client.options.apiKey) { - options.headers.Authorization = `Bearer ${client.options.apiKey}`; - } else if (client.options.username && client.options.password) { - options.auth = { - username: client.options.username, - password: client.options.password, - }; - } - - return options; - } - - log_repsonse(error, response, body, options) { - let prop; - let val; - - prop = "auth.username"; - val = _.get(options, prop, false); - if (val) { - _.set(options, prop, "redacted"); - } - - prop = "auth.password"; - val = _.get(options, prop, false); - if (val) { - _.set(options, prop, "redacted"); - } - - prop = "headers.Authorization"; - val = _.get(options, prop, false); - if (val) { - _.set(options, prop, "redacted"); - } - - delete options.httpAgent; - delete options.httpsAgent; - - let duration = parseFloat( - Math.round((_.get(response, "duration", 0) + Number.EPSILON) * 100) / - 100 / - 1000 - ).toFixed(2); - - this.logger.debug("FREENAS HTTP REQUEST DETAILS: " + stringify(options)); - this.logger.debug("FREENAS HTTP REQUEST DURATION: " + duration + "s"); - this.logger.debug("FREENAS HTTP ERROR: " + error); - this.logger.debug( - "FREENAS HTTP RESPONSE STATUS CODE: " + _.get(response, "statusCode", "") - ); - this.logger.debug( - "FREENAS HTTP RESPONSE HEADERS: " + - stringify(_.get(response, "headers", "")) - ); - this.logger.debug("FREENAS HTTP RESPONSE BODY: " + stringify(body)); - } - - async get(endpoint, data, options = {}) { - const client = this; - if (this.options.apiVersion == 1 && !endpoint.endsWith("/")) { - endpoint += "/"; - } + this.logger.debug(`Calling method ${method} with params:`, stringify(params)); return new Promise((resolve, reject) => { - options = { ...client.getRequestCommonOptions(), ...options }; - options.method = "GET"; - options.url = this.getBaseURL() + endpoint; - options.params = data; - - axios_request(options, function (err, res, body) { - client.log_repsonse(...arguments, options); - if (err) { - reject(err); + // Set timeout + const timeout = options.timeout || 60000; + const timeoutHandle = setTimeout(() => { + if (this.pendingRequests.has(messageId)) { + this.pendingRequests.delete(messageId); + reject(new Error(`Request timeout: ${method}`)); } - resolve(res); + }, timeout); + + // Store pending request + this.pendingRequests.set(messageId, { + resolve: (result) => { + clearTimeout(timeoutHandle); + resolve(result); + }, + reject: (error) => { + clearTimeout(timeoutHandle); + reject(error); + }, }); + + // Send request + try { + this.ws.send(JSON.stringify(request)); + } catch (error) { + this.pendingRequests.delete(messageId); + clearTimeout(timeoutHandle); + reject(error); + } }); } - async post(endpoint, data, options = {}) { - const client = this; - if (this.options.apiVersion == 1 && !endpoint.endsWith("/")) { - endpoint += "/"; + /** + * Subscribe to an event + * + * @param {string} event - Event name to subscribe to + * @param {function} handler - Handler function to call when event is received + */ + subscribe(event, handler) { + if (!this.eventSubscriptions.has(event)) { + this.eventSubscriptions.set(event, []); } - - return new Promise((resolve, reject) => { - options = { ...client.getRequestCommonOptions(), ...options }; - options.method = "POST"; - options.url = this.getBaseURL() + endpoint; - options.data = data; - - axios_request(options, function (err, res, body) { - client.log_repsonse(...arguments, options); - if (err) { - reject(err); - } - - resolve(res); - }); - }); + this.eventSubscriptions.get(event).push(handler); } - async put(endpoint, data, options = {}) { - const client = this; - if (this.options.apiVersion == 1 && !endpoint.endsWith("/")) { - endpoint += "/"; + /** + * Unsubscribe from an event + */ + unsubscribe(event, handler) { + if (!this.eventSubscriptions.has(event)) { + return; + } + const handlers = this.eventSubscriptions.get(event); + const index = handlers.indexOf(handler); + if (index > -1) { + handlers.splice(index, 1); } - - return new Promise((resolve, reject) => { - options = { ...client.getRequestCommonOptions(), ...options }; - options.method = "PUT"; - options.url = this.getBaseURL() + endpoint; - options.data = data; - - axios_request(options, function (err, res, body) { - client.log_repsonse(...arguments, options); - if (err) { - reject(err); - } - - resolve(res); - }); - }); } - async delete(endpoint, data, options = {}) { - const client = this; - if (this.options.apiVersion == 1 && !endpoint.endsWith("/")) { - endpoint += "/"; + /** + * Close the WebSocket connection + */ + async close() { + if (this.ws) { + this.ws.close(); + this.ws = null; + this.authenticated = false; } + } - return new Promise((resolve, reject) => { - options = { ...client.getRequestCommonOptions(), ...options }; - options.method = "DELETE"; - options.url = this.getBaseURL() + endpoint; - options.data = data; + // Legacy compatibility methods (will be removed after full migration) + async get() { + throw new Error("HTTP GET is not supported. Use call() method with appropriate JSON-RPC method."); + } - axios_request(options, function (err, res, body) { - client.log_repsonse(...arguments, options); - if (err) { - reject(err); - } + async post() { + throw new Error("HTTP POST is not supported. Use call() method with appropriate JSON-RPC method."); + } - resolve(res); - }); - }); + async put() { + throw new Error("HTTP PUT is not supported. Use call() method with appropriate JSON-RPC method."); + } + + async delete() { + throw new Error("HTTP DELETE is not supported. Use call() method with appropriate JSON-RPC method."); } } diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js deleted file mode 100644 index 3f635ac..0000000 --- a/src/driver/freenas/ssh.js +++ /dev/null @@ -1,2316 +0,0 @@ -const _ = require("lodash"); -const { ControllerZfsBaseDriver } = require("../controller-zfs"); -const { GrpcError, grpc } = require("../../utils/grpc"); -const SshClient = require("../../utils/zfs_ssh_exec_client").SshClient; -const HttpClient = require("./http").Client; -const TrueNASApiClient = require("./http/api").Api; -const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); -const GeneralUtils = require("../../utils/general"); - -const Handlebars = require("handlebars"); -const semver = require("semver"); - -// freenas properties -const FREENAS_NFS_SHARE_PROPERTY_NAME = "democratic-csi:freenas_nfs_share_id"; -const FREENAS_SMB_SHARE_PROPERTY_NAME = "democratic-csi:freenas_smb_share_id"; -const FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME = - "democratic-csi:freenas_iscsi_target_id"; -const FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME = - "democratic-csi:freenas_iscsi_extent_id"; -const FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME = - "democratic-csi:freenas_iscsi_targettoextent_id"; -const FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME = - "democratic-csi:freenas_iscsi_assets_name"; - -// used for in-memory cache of the version info -const FREENAS_SYSTEM_VERSION_CACHE_KEY = "freenas:system_version"; -const __REGISTRY_NS__ = "FreeNASSshDriver"; - -class FreeNASSshDriver extends ControllerZfsBaseDriver { - /** - * Ensure sane options are used etc - * true = ready - * false = not ready, but progressiong towards ready - * throw error = faulty setup - * - * @param {*} call - */ - async Probe(call) { - const driver = this; - - if (driver.ctx.args.csiMode.includes("controller")) { - const httpApiClient = await driver.getTrueNASHttpApiClient(); - try { - await httpApiClient.getSystemVersion(); - } catch (err) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `TrueNAS api is unavailable: ${String(err)}` - ); - } - - return super.Probe(...arguments); - } else { - return super.Probe(...arguments); - } - } - - getExecClient() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:exec_client`, () => { - return new SshClient({ - logger: this.ctx.logger, - connection: this.options.sshConnection, - }); - }); - } - - async getZetabyte() { - return this.ctx.registry.getAsync(`${__REGISTRY_NS__}:zb`, async () => { - const sshClient = this.getExecClient(); - const options = {}; - options.executor = new ZfsSshProcessManager(sshClient); - options.idempotent = true; - - if ( - this.options.zfs.hasOwnProperty("cli") && - this.options.zfs.cli && - this.options.zfs.cli.hasOwnProperty("paths") - ) { - options.paths = this.options.zfs.cli.paths; - } - - options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); - - if (typeof this.setZetabyteCustomOptions === "function") { - await this.setZetabyteCustomOptions(options); - } - - return new Zetabyte(options); - }); - } - - /** - * cannot make this a storage class parameter as storage class/etc context is *not* sent - * into various calls such as GetControllerCapabilities etc - */ - getDriverZfsResourceType() { - switch (this.options.driver) { - case "freenas-nfs": - case "truenas-nfs": - case "freenas-smb": - case "truenas-smb": - return "filesystem"; - case "freenas-iscsi": - case "truenas-iscsi": - return "volume"; - default: - throw new Error("unknown driver: " + this.ctx.args.driver); - } - } - - async setZetabyteCustomOptions(options) { - if (!options.hasOwnProperty("paths")) { - const majorMinor = await this.getSystemVersionMajorMinor(); - const isScale = await this.getIsScale(); - if (!isScale && Number(majorMinor) >= 12) { - options.paths = { - zfs: "/usr/local/sbin/zfs", - zpool: "/usr/local/sbin/zpool", - sudo: "/usr/local/bin/sudo", - chroot: "/usr/sbin/chroot", - }; - } - } - } - - async getHttpClient(autoDetectVersion = true) { - const autodetectkey = autoDetectVersion === true ? 1 : 0; - return this.ctx.registry.getAsync( - `${__REGISTRY_NS__}:http_client:autoDetectVersion_${autodetectkey}`, - async () => { - const client = new HttpClient(this.options.httpConnection); - client.logger = this.ctx.logger; - - if (autoDetectVersion && !!!this.options.httpConnection.apiVersion) { - const apiVersion = await this.getApiVersion(); - client.setApiVersion(apiVersion); - } - - return client; - } - ); - } - - async getTrueNASHttpApiClient() { - return this.ctx.registry.getAsync(`${__REGISTRY_NS__}:api_client`, async () => { - const httpClient = await this.getHttpClient(); - return new TrueNASApiClient(httpClient, this.ctx.cache); - }); - } - - getDriverShareType() { - switch (this.options.driver) { - case "freenas-nfs": - case "truenas-nfs": - return "nfs"; - case "freenas-smb": - case "truenas-smb": - return "smb"; - case "freenas-iscsi": - case "truenas-iscsi": - return "iscsi"; - default: - throw new Error("unknown driver: " + this.ctx.args.driver); - } - } - - async findResourceByProperties(endpoint, match) { - if (!match) { - return; - } - - if (typeof match === "object" && Object.keys(match).length < 1) { - return; - } - - const httpClient = await this.getHttpClient(); - let target; - let page = 0; - let lastReponse; - - // loop and find target - let queryParams = {}; - // TODO: relax this using getSystemVersion perhaps - // https://jira.ixsystems.com/browse/NAS-103916 - // NOTE: if using apiVersion 2 with 11.2 you will have issues - if (httpClient.getApiVersion() == 1 || httpClient.getApiVersion() == 2) { - queryParams.limit = 100; - queryParams.offset = 0; - } - - while (!target) { - //Content-Range: items 0-2/3 (full set) - //Content-Range: items 0--1/3 (invalid offset) - if (queryParams.hasOwnProperty("offset")) { - queryParams.offset = queryParams.limit * page; - } - - // crude stoppage attempt - let response = await httpClient.get(endpoint, queryParams); - if (lastReponse) { - if (JSON.stringify(lastReponse) == JSON.stringify(response)) { - break; - } - } - lastReponse = response; - - if (response.statusCode == 200) { - if (response.body.length < 1) { - break; - } - response.body.some((i) => { - let isMatch = true; - - if (typeof match === "function") { - isMatch = match(i); - } else { - for (let property in match) { - if (match[property] != i[property]) { - isMatch = false; - break; - } - } - } - - if (isMatch) { - target = i; - return true; - } - - return false; - }); - } else { - throw new Error( - "FreeNAS http error - code: " + - response.statusCode + - " body: " + - JSON.stringify(response.body) - ); - } - page++; - } - - return target; - } - - /** - * should create any necessary share resources - * should set the SHARE_VOLUME_CONTEXT_PROPERTY_NAME propery - * - * @param {*} datasetName - */ - async createShare(call, datasetName) { - const driver = this; - const driverShareType = this.getDriverShareType(); - const execClient = this.getExecClient(); - const httpClient = await this.getHttpClient(); - const httpApiClient = await this.getTrueNASHttpApiClient(); - const apiVersion = httpClient.getApiVersion(); - const zb = await this.getZetabyte(); - const truenasVersion = semver.coerce( - await httpApiClient.getSystemVersionMajorMinor(), - { loose: true } - ); - - if (!truenasVersion) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unable to detect TrueNAS version` - ); - } - - const isScale = await httpApiClient.getIsScale(); - - let volume_context; - let properties; - let endpoint; - let response; - let share = {}; - - switch (driverShareType) { - case "nfs": - properties = await zb.zfs.get(datasetName, [ - "mountpoint", - FREENAS_NFS_SHARE_PROPERTY_NAME, - ]); - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - // create nfs share - if ( - !zb.helpers.isPropertyValueSet( - properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value - ) - ) { - let nfsShareComment; - if (this.options.nfs.shareCommentTemplate) { - nfsShareComment = Handlebars.compile( - this.options.nfs.shareCommentTemplate - )({ - name: call.request.name, - parameters: call.request.parameters, - csi: { - name: this.ctx.args.csiName, - version: this.ctx.args.csiVersion, - }, - zfs: { - datasetName: datasetName, - }, - }); - } else { - nfsShareComment = `democratic-csi (${this.ctx.args.csiName}): ${datasetName}`; - } - - switch (apiVersion) { - case 1: - case 2: - switch (apiVersion) { - case 1: - share = { - nfs_paths: [properties.mountpoint.value], - nfs_comment: nfsShareComment || "", - nfs_network: - this.options.nfs.shareAllowedNetworks.join(","), - nfs_hosts: this.options.nfs.shareAllowedHosts.join(","), - nfs_alldirs: this.options.nfs.shareAlldirs, - nfs_ro: false, - nfs_quiet: false, - nfs_maproot_user: this.options.nfs.shareMaprootUser, - nfs_maproot_group: this.options.nfs.shareMaprootGroup, - nfs_mapall_user: this.options.nfs.shareMapallUser, - nfs_mapall_group: this.options.nfs.shareMapallGroup, - nfs_security: [], - }; - break; - case 2: - share = { - paths: [properties.mountpoint.value], - comment: nfsShareComment || "", - networks: this.options.nfs.shareAllowedNetworks, - hosts: this.options.nfs.shareAllowedHosts, - alldirs: this.options.nfs.shareAlldirs, - ro: false, - quiet: false, - maproot_user: this.options.nfs.shareMaprootUser, - maproot_group: this.options.nfs.shareMaprootGroup, - mapall_user: this.options.nfs.shareMapallUser, - mapall_group: this.options.nfs.shareMapallGroup, - security: [], - }; - break; - } - - if (isScale && semver.satisfies(truenasVersion, ">=23.10")) { - delete share.quiet; - delete share.nfs_quiet; - } - - if (isScale && semver.satisfies(truenasVersion, ">=22.12")) { - share.path = share.paths[0]; - delete share.paths; - delete share.alldirs; - } - - response = await GeneralUtils.retry( - 3, - 1000, - async () => { - return await httpClient.post("/sharing/nfs", share); - }, - { - retryCondition: (err) => { - if (err.code == "ECONNRESET") { - return true; - } - if (err.code == "ECONNABORTED") { - return true; - } - if (err.response && err.response.statusCode == 504) { - return true; - } - return false; - }, - } - ); - - /** - * v1 = 201 - * v2 = 200 - */ - if ([200, 201].includes(response.statusCode)) { - let sharePaths; - switch (apiVersion) { - case 1: - sharePaths = response.body.nfs_paths; - break; - case 2: - if (response.body.path) { - sharePaths = [response.body.path]; - } else { - sharePaths = response.body.paths; - } - break; - } - - // FreeNAS responding with bad data - if (!sharePaths.includes(properties.mountpoint.value)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS responded with incorrect share data: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - //set zfs property - await zb.zfs.set(datasetName, { - [FREENAS_NFS_SHARE_PROPERTY_NAME]: response.body.id, - }); - } else { - /** - * v1 = 409 - * v2 = 422 - */ - if ( - [409, 422].includes(response.statusCode) && - (JSON.stringify(response.body).includes( - "You can't share same filesystem with all hosts twice." - ) || - JSON.stringify(response.body).includes( - "Another NFS share already exports this dataset for some network" - )) - ) { - let lookupShare = await this.findResourceByProperties( - "/sharing/nfs", - (item) => { - if ( - (item.nfs_paths && - item.nfs_paths.includes( - properties.mountpoint.value - )) || - (item.paths && - item.paths.includes(properties.mountpoint.value)) || - (item.path && item.path == properties.mountpoint.value) - ) { - return true; - } - return false; - } - ); - - if (!lookupShare) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS failed to find matching share` - ); - } - - //set zfs property - await zb.zfs.set(datasetName, { - [FREENAS_NFS_SHARE_PROPERTY_NAME]: lookupShare.id, - }); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating nfs share - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - - volume_context = { - node_attach_driver: "nfs", - server: this.options.nfs.shareHost, - share: properties.mountpoint.value, - }; - return volume_context; - - break; - /** - * TODO: smb need to be more defensive like iscsi and nfs - * ensuring the path is valid and the shareName - */ - case "smb": - properties = await zb.zfs.get(datasetName, [ - "mountpoint", - FREENAS_SMB_SHARE_PROPERTY_NAME, - ]); - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - let smbName; - - if (this.options.smb.nameTemplate) { - smbName = Handlebars.compile(this.options.smb.nameTemplate)({ - name: call.request.name, - parameters: call.request.parameters, - }); - } else { - smbName = zb.helpers.extractLeafName(datasetName); - } - - if (this.options.smb.namePrefix) { - smbName = this.options.smb.namePrefix + smbName; - } - - if (this.options.smb.nameSuffix) { - smbName += this.options.smb.nameSuffix; - } - - smbName = smbName.toLowerCase(); - - this.ctx.logger.info( - "FreeNAS creating smb share with name: " + smbName - ); - - // create smb share - if ( - !zb.helpers.isPropertyValueSet( - properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value - ) - ) { - /** - * The only required parameters are: - * - path - * - name - * - * Note that over time it appears the list of available parameters has increased - * so in an effort to best support old versions of FreeNAS we should check the - * presense of each parameter in the config and set the corresponding parameter in - * the API request *only* if present in the config. - */ - switch (apiVersion) { - case 1: - case 2: - share = { - name: smbName, - path: properties.mountpoint.value, - }; - - let propertyMapping = { - shareAuxiliaryConfigurationTemplate: "auxsmbconf", - shareHome: "home", - shareAllowedHosts: "hostsallow", - shareDeniedHosts: "hostsdeny", - shareDefaultPermissions: "default_permissions", - shareGuestOk: "guestok", - shareGuestOnly: "guestonly", - shareShowHiddenFiles: "showhiddenfiles", - shareRecycleBin: "recyclebin", - shareBrowsable: "browsable", - shareAccessBasedEnumeration: "abe", - shareTimeMachine: "timemachine", - shareStorageTask: "storage_task", - }; - - for (const key in propertyMapping) { - if (this.options.smb.hasOwnProperty(key)) { - let value; - switch (key) { - case "shareAuxiliaryConfigurationTemplate": - value = Handlebars.compile( - this.options.smb.shareAuxiliaryConfigurationTemplate - )({ - name: call.request.name, - parameters: call.request.parameters, - }); - break; - default: - value = this.options.smb[key]; - break; - } - share[propertyMapping[key]] = value; - } - } - - switch (apiVersion) { - case 1: - endpoint = "/sharing/cifs"; - - // rename keys with cifs_ prefix - for (const key in share) { - share["cifs_" + key] = share[key]; - delete share[key]; - } - - // convert to comma-separated list - if (share.cifs_hostsallow) { - share.cifs_hostsallow = share.cifs_hostsallow.join(","); - } - - // convert to comma-separated list - if (share.cifs_hostsdeny) { - share.cifs_hostsdeny = share.cifs_hostsdeny.join(","); - } - break; - case 2: - endpoint = "/sharing/smb"; - break; - } - - response = await GeneralUtils.retry( - 3, - 1000, - async () => { - return await httpClient.post(endpoint, share); - }, - { - retryCondition: (err) => { - if (err.code == "ECONNRESET") { - return true; - } - if (err.code == "ECONNABORTED") { - return true; - } - if (err.response && err.response.statusCode == 504) { - return true; - } - return false; - }, - } - ); - - /** - * v1 = 201 - * v2 = 200 - */ - if ([200, 201].includes(response.statusCode)) { - share = response.body; - let sharePath; - let shareName; - switch (apiVersion) { - case 1: - sharePath = response.body.cifs_path; - shareName = response.body.cifs_name; - break; - case 2: - sharePath = response.body.path; - shareName = response.body.name; - break; - } - - if (shareName != smbName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS responded with incorrect share data: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - if (sharePath != properties.mountpoint.value) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS responded with incorrect share data: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - //set zfs property - await zb.zfs.set(datasetName, { - [FREENAS_SMB_SHARE_PROPERTY_NAME]: response.body.id, - }); - } else { - /** - * v1 = 409 - * v2 = 422 - */ - if ( - [409, 422].includes(response.statusCode) && - JSON.stringify(response.body).includes( - "A share with this name already exists." - ) - ) { - let lookupShare = await this.findResourceByProperties( - endpoint, - (item) => { - if ( - (item.cifs_path && - item.cifs_path == properties.mountpoint.value && - item.cifs_name && - item.cifs_name == smbName) || - (item.path && - item.path == properties.mountpoint.value && - item.name && - item.name == smbName) - ) { - return true; - } - return false; - } - ); - - if (!lookupShare) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS failed to find matching share` - ); - } - - //set zfs property - await zb.zfs.set(datasetName, { - [FREENAS_SMB_SHARE_PROPERTY_NAME]: lookupShare.id, - }); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating smb share - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - - volume_context = { - node_attach_driver: "smb", - server: this.options.smb.shareHost, - share: smbName, - }; - return volume_context; - - break; - case "iscsi": - properties = await zb.zfs.get(datasetName, [ - FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, - FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME, - FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME, - ]); - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - let basename; - let iscsiName; - - if (this.options.iscsi.nameTemplate) { - iscsiName = Handlebars.compile(this.options.iscsi.nameTemplate)({ - name: call.request.name, - parameters: call.request.parameters, - }); - } else { - iscsiName = zb.helpers.extractLeafName(datasetName); - } - - if (this.options.iscsi.namePrefix) { - iscsiName = this.options.iscsi.namePrefix + iscsiName; - } - - if (this.options.iscsi.nameSuffix) { - iscsiName += this.options.iscsi.nameSuffix; - } - - // According to RFC3270, 'Each iSCSI node, whether an initiator or target, MUST have an iSCSI name. Initiators and targets MUST support the receipt of iSCSI names of up to the maximum length of 223 bytes.' - // https://kb.netapp.com/Advice_and_Troubleshooting/Miscellaneous/What_is_the_maximum_length_of_a_iSCSI_iqn_name - // https://tools.ietf.org/html/rfc3720 - // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 - iscsiName = iscsiName.toLowerCase(); - - let extentDiskName = "zvol/" + datasetName; - let maxZvolNameLength = await driver.getMaxZvolNameLength(); - driver.ctx.logger.debug("max zvol name length: %s", maxZvolNameLength); - - /** - * limit is a FreeBSD limitation - * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab - */ - - if (extentDiskName.length > maxZvolNameLength) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` - ); - } - - // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 - if (isScale && iscsiName.length > 64) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `extent name cannot exceed 64 characters: ${iscsiName}` - ); - } - - this.ctx.logger.info( - "FreeNAS creating iscsi assets with name: " + iscsiName - ); - - let extentComment; - if (this.options.iscsi.extentCommentTemplate) { - extentComment = Handlebars.compile( - this.options.iscsi.extentCommentTemplate - )({ - name: call.request.name, - parameters: call.request.parameters, - csi: { - name: this.ctx.args.csiName, - version: this.ctx.args.csiVersion, - }, - zfs: { - datasetName: datasetName, - }, - }); - } else { - extentComment = ""; - } - - const extentInsecureTpc = this.options.iscsi.hasOwnProperty( - "extentInsecureTpc" - ) - ? this.options.iscsi.extentInsecureTpc - : true; - - const extentXenCompat = this.options.iscsi.hasOwnProperty( - "extentXenCompat" - ) - ? this.options.iscsi.extentXenCompat - : false; - - const extentBlocksize = this.options.iscsi.hasOwnProperty( - "extentBlocksize" - ) - ? this.options.iscsi.extentBlocksize - : 512; - - const extentDisablePhysicalBlocksize = - this.options.iscsi.hasOwnProperty("extentDisablePhysicalBlocksize") - ? this.options.iscsi.extentDisablePhysicalBlocksize - : true; - - const extentRpm = this.options.iscsi.hasOwnProperty("extentRpm") - ? this.options.iscsi.extentRpm - : "SSD"; - - let extentAvailThreshold = this.options.iscsi.hasOwnProperty( - "extentAvailThreshold" - ) - ? Number(this.options.iscsi.extentAvailThreshold) - : null; - - if (!(extentAvailThreshold > 0 && extentAvailThreshold <= 100)) { - extentAvailThreshold = null; - } - - switch (apiVersion) { - case 1: - response = await httpClient.get( - "/services/iscsi/globalconfiguration" - ); - if (response.statusCode != 200) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error getting iscsi configuration - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - basename = response.body.iscsi_basename; - this.ctx.logger.verbose("FreeNAS ISCSI BASENAME: " + basename); - break; - case 2: - response = await httpClient.get("/iscsi/global"); - if (response.statusCode != 200) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error getting iscsi configuration - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - basename = response.body.basename; - this.ctx.logger.verbose("FreeNAS ISCSI BASENAME: " + basename); - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - - // if we got all the way to the TARGETTOEXTENT then we fully finished - // otherwise we must do all assets every time due to the interdependence of IDs etc - if ( - !zb.helpers.isPropertyValueSet( - properties[FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME].value - ) - ) { - switch (apiVersion) { - case 1: { - // create target - let target = { - iscsi_target_name: iscsiName, - iscsi_target_alias: "", // TODO: allow template for this - }; - - response = await httpClient.post( - "/services/iscsi/target", - target - ); - - // 409 if invalid - if (response.statusCode != 201) { - target = null; - if ( - response.statusCode == 409 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) - ) { - target = await this.findResourceByProperties( - "/services/iscsi/target", - { - iscsi_target_name: iscsiName, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi target - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - target = response.body; - } - - if (!target) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi target` - ); - } - - if (target.iscsi_target_name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi target` - ); - } - - this.ctx.logger.verbose("FreeNAS ISCSI TARGET: %j", target); - - // set target.id on zvol - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME]: target.id, - }); - - // create targetgroup(s) - // targetgroups do have IDs - for (let targetGroupConfig of this.options.iscsi.targetGroups) { - let targetGroup = { - iscsi_target: target.id, - iscsi_target_authgroup: - targetGroupConfig.targetGroupAuthGroup, - iscsi_target_authtype: targetGroupConfig.targetGroupAuthType - ? targetGroupConfig.targetGroupAuthType - : "None", - iscsi_target_portalgroup: - targetGroupConfig.targetGroupPortalGroup, - iscsi_target_initiatorgroup: - targetGroupConfig.targetGroupInitiatorGroup, - iscsi_target_initialdigest: "Auto", - }; - response = await httpClient.post( - "/services/iscsi/targetgroup", - targetGroup - ); - - // 409 if invalid - if (response.statusCode != 201) { - targetGroup = null; - /** - * 404 gets returned with an unable to process response when the DB is corrupted (has invalid entries in essense) - * - * To resolve properly the DB should be cleaned up - * /usr/local/etc/rc.d/django stop - * /usr/local/etc/rc.d/nginx stop - * sqlite3 /data/freenas-v1.db - * - * // this deletes everything, probably not what you want - * // should have a better query to only find entries where associated assets no longer exist - * DELETE from services_iscsitargetgroups; - * - * /usr/local/etc/rc.d/django restart - * /usr/local/etc/rc.d/nginx restart - */ - if ( - response.statusCode == 404 || - (response.statusCode == 409 && - JSON.stringify(response.body).includes( - "cannot be duplicated on a target" - )) - ) { - targetGroup = await this.findResourceByProperties( - "/services/iscsi/targetgroup", - { - iscsi_target: target.id, - iscsi_target_portalgroup: - targetGroupConfig.targetGroupPortalGroup, - iscsi_target_initiatorgroup: - targetGroupConfig.targetGroupInitiatorGroup, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi targetgroup - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - targetGroup = response.body; - } - - if (!targetGroup) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi targetgroup` - ); - } - - this.ctx.logger.verbose( - "FreeNAS ISCSI TARGET_GROUP: %j", - targetGroup - ); - } - - let extent = { - iscsi_target_extent_comment: extentComment, - iscsi_target_extent_type: "Disk", // Disk/File, after save Disk becomes "ZVOL" - iscsi_target_extent_name: iscsiName, - iscsi_target_extent_insecure_tpc: extentInsecureTpc, - //iscsi_target_extent_naa: "0x3822690834aae6c5", - iscsi_target_extent_disk: extentDiskName, - iscsi_target_extent_xen: extentXenCompat, - iscsi_target_extent_avail_threshold: extentAvailThreshold, - iscsi_target_extent_blocksize: Number(extentBlocksize), - iscsi_target_extent_pblocksize: extentDisablePhysicalBlocksize, - iscsi_target_extent_rpm: isNaN(Number(extentRpm)) - ? "SSD" - : Number(extentRpm), - iscsi_target_extent_ro: false, - }; - response = await httpClient.post( - "/services/iscsi/extent", - extent - ); - - // 409 if invalid - if (response.statusCode != 201) { - extent = null; - if ( - response.statusCode == 409 && - JSON.stringify(response.body).includes( - "Extent name must be unique" - ) - ) { - extent = await this.findResourceByProperties( - "/services/iscsi/extent", - { iscsi_target_extent_name: iscsiName } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi extent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - extent = response.body; - } - - if (!extent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi extent` - ); - } - - if (extent.iscsi_target_extent_name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi extent` - ); - } - - this.ctx.logger.verbose("FreeNAS ISCSI EXTENT: %j", extent); - - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME]: extent.id, - }); - - // create targettoextent - let targetToExtent = { - iscsi_target: target.id, - iscsi_extent: extent.id, - iscsi_lunid: 0, - }; - response = await httpClient.post( - "/services/iscsi/targettoextent", - targetToExtent - ); - - // 409 if invalid - if (response.statusCode != 201) { - targetToExtent = null; - - // LUN ID is already being used for this target. - // Extent is already in this target. - if ( - response.statusCode == 409 && - (JSON.stringify(response.body).includes( - "Extent is already in this target." - ) || - JSON.stringify(response.body).includes( - "LUN ID is already being used for this target." - )) - ) { - targetToExtent = await this.findResourceByProperties( - "/services/iscsi/targettoextent", - { - iscsi_target: target.id, - iscsi_extent: extent.id, - iscsi_lunid: 0, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi targettoextent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - targetToExtent = response.body; - } - - if (!targetToExtent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi targettoextent` - ); - } - this.ctx.logger.verbose( - "FreeNAS ISCSI TARGET_TO_EXTENT: %j", - targetToExtent - ); - - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME]: - targetToExtent.id, - }); - - break; - } - case 2: - // create target and targetgroup - //let targetId; - let targetGroups = []; - for (let targetGroupConfig of this.options.iscsi.targetGroups) { - targetGroups.push({ - portal: targetGroupConfig.targetGroupPortalGroup, - initiator: targetGroupConfig.targetGroupInitiatorGroup, - auth: - targetGroupConfig.targetGroupAuthGroup > 0 - ? targetGroupConfig.targetGroupAuthGroup - : null, - authmethod: - targetGroupConfig.targetGroupAuthType.length > 0 - ? targetGroupConfig.targetGroupAuthType - .toUpperCase() - .replace(" ", "_") - : "NONE", - }); - } - let target = { - name: iscsiName, - alias: null, // cannot send "" error: handler error - driver: FreeNASDriver method: CreateVolume error: {"name":"GrpcError","code":2,"message":"received error creating iscsi target - code: 422 body: {\"iscsi_target_create.alias\":[{\"message\":\"Alias already exists\",\"errno\":22}]}"} - mode: "ISCSI", - groups: targetGroups, - }; - - response = await httpClient.post("/iscsi/target", target); - - // 409 if invalid - if (response.statusCode != 200) { - target = null; - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) - ) { - target = await this.findResourceByProperties( - "/iscsi/target", - { - name: iscsiName, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi target - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - target = response.body; - } - - if (!target) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi target` - ); - } - - if (target.name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi target` - ); - } - - // handle situations/race conditions where groups failed to be added/created on the target - // groups":[{"portal":1,"initiator":1,"auth":null,"authmethod":"NONE"},{"portal":2,"initiator":1,"auth":null,"authmethod":"NONE"}] - // TODO: this logic could be more intelligent but this should do for now as it appears in the failure scenario no groups are added - // in other words, I have never seen them invalid, only omitted so this should be enough - if (target.groups.length != targetGroups.length) { - response = await httpClient.put( - `/iscsi/target/id/${target.id}`, - { - groups: targetGroups, - } - ); - - if (response.statusCode != 200) { - throw new GrpcError( - grpc.status.UNKNOWN, - `failed setting target groups` - ); - } else { - target = response.body; - - // re-run sanity checks - if (!target) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi target` - ); - } - - if (target.name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi target` - ); - } - - if (target.groups.length != targetGroups.length) { - throw new GrpcError( - grpc.status.UNKNOWN, - `failed setting target groups` - ); - } - } - } - - this.ctx.logger.verbose("FreeNAS ISCSI TARGET: %j", target); - - // set target.id on zvol - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME]: target.id, - }); - - let extent = { - comment: extentComment, - type: "DISK", // Disk/File, after save Disk becomes "ZVOL" - name: iscsiName, - //iscsi_target_extent_naa: "0x3822690834aae6c5", - disk: extentDiskName, - insecure_tpc: extentInsecureTpc, - xen: extentXenCompat, - avail_threshold: extentAvailThreshold, - blocksize: Number(extentBlocksize), - pblocksize: extentDisablePhysicalBlocksize, - rpm: "" + extentRpm, // should be a string - ro: false, - }; - - response = await httpClient.post("/iscsi/extent", extent); - - // 409 if invalid - if (response.statusCode != 200) { - extent = null; - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes( - "Extent name must be unique" - ) - ) { - extent = await this.findResourceByProperties( - "/iscsi/extent", - { - name: iscsiName, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi extent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - extent = response.body; - } - - if (!extent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi extent` - ); - } - - if (extent.name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi extent` - ); - } - - this.ctx.logger.verbose("FreeNAS ISCSI EXTENT: %j", extent); - - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME]: extent.id, - }); - - // create targettoextent - let targetToExtent = { - target: target.id, - extent: extent.id, - lunid: 0, - }; - response = await httpClient.post( - "/iscsi/targetextent", - targetToExtent - ); - - if (response.statusCode != 200) { - targetToExtent = null; - - // LUN ID is already being used for this target. - // Extent is already in this target. - if ( - response.statusCode == 422 && - (JSON.stringify(response.body).includes( - "Extent is already in this target." - ) || - JSON.stringify(response.body).includes( - "LUN ID is already being used for this target." - )) - ) { - targetToExtent = await this.findResourceByProperties( - "/iscsi/targetextent", - { - target: target.id, - extent: extent.id, - lunid: 0, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi targetextent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - targetToExtent = response.body; - } - - if (!targetToExtent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi targetextent` - ); - } - this.ctx.logger.verbose( - "FreeNAS ISCSI TARGET_TO_EXTENT: %j", - targetToExtent - ); - - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME]: - targetToExtent.id, - }); - - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - - // iqn = target - let iqn = basename + ":" + iscsiName; - this.ctx.logger.info("FreeNAS iqn: " + iqn); - - // store this off to make delete process more bullet proof - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME]: iscsiName, - }); - - volume_context = { - node_attach_driver: "iscsi", - portal: this.options.iscsi.targetPortal || "", - portals: this.options.iscsi.targetPortals - ? this.options.iscsi.targetPortals.join(",") - : "", - interface: this.options.iscsi.interface || "", - iqn: iqn, - lun: 0, - }; - return volume_context; - - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown driverShareType ${driverShareType}` - ); - } - } - - async deleteShare(call, datasetName) { - const driverShareType = this.getDriverShareType(); - const httpClient = await this.getHttpClient(); - const apiVersion = httpClient.getApiVersion(); - const zb = await this.getZetabyte(); - - let properties; - let response; - let endpoint; - let shareId; - let deleteAsset; - let sharePaths; - - switch (driverShareType) { - case "nfs": - try { - properties = await zb.zfs.get(datasetName, [ - "mountpoint", - FREENAS_NFS_SHARE_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; - } - throw err; - } - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - shareId = properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value; - - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(shareId)) { - // remove nfs share - switch (apiVersion) { - case 1: - case 2: - endpoint = "/sharing/nfs/"; - if (apiVersion == 2) { - endpoint += "id/"; - } - endpoint += shareId; - - response = await httpClient.get(endpoint); - - // assume share is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { - switch (apiVersion) { - case 1: - sharePaths = response.body.nfs_paths; - break; - case 2: - if (response.body.path) { - sharePaths = [response.body.path]; - } else { - sharePaths = response.body.paths; - } - break; - } - - deleteAsset = sharePaths.some((value) => { - return value == properties.mountpoint.value; - }); - - if (deleteAsset) { - response = await GeneralUtils.retry( - 3, - 1000, - async () => { - return await httpClient.delete(endpoint); - }, - { - retryCondition: (err) => { - if (err.code == "ECONNRESET") { - return true; - } - if (err.code == "ECONNABORTED") { - return true; - } - if (err.response && err.response.statusCode == 504) { - return true; - } - return false; - }, - } - ); - - // returns a 500 if does not exist - // v1 = 204 - // v2 = 200 - if (![200, 204].includes(response.statusCode)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error deleting nfs share - share: ${shareId} code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await zb.zfs.inherit( - datasetName, - FREENAS_NFS_SHARE_PROPERTY_NAME - ); - } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - break; - case "smb": - try { - properties = await zb.zfs.get(datasetName, [ - "mountpoint", - FREENAS_SMB_SHARE_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; - } - throw err; - } - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - shareId = properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value; - - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(shareId)) { - // remove smb share - switch (apiVersion) { - case 1: - case 2: - switch (apiVersion) { - case 1: - endpoint = `/sharing/cifs/${shareId}`; - break; - case 2: - endpoint = `/sharing/smb/id/${shareId}`; - break; - } - - response = await httpClient.get(endpoint); - - // assume share is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { - switch (apiVersion) { - case 1: - sharePaths = [response.body.cifs_path]; - break; - case 2: - sharePaths = [response.body.path]; - break; - } - - deleteAsset = sharePaths.some((value) => { - return value == properties.mountpoint.value; - }); - - if (deleteAsset) { - response = await GeneralUtils.retry( - 3, - 1000, - async () => { - return await httpClient.delete(endpoint); - }, - { - retryCondition: (err) => { - if (err.code == "ECONNRESET") { - return true; - } - if (err.code == "ECONNABORTED") { - return true; - } - if (err.response && err.response.statusCode == 504) { - return true; - } - return false; - }, - } - ); - - // returns a 500 if does not exist - // v1 = 204 - // v2 = 200 - if ( - ![200, 204].includes(response.statusCode) && - !JSON.stringify(response.body).includes("does not exist") - ) { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error deleting smb share - share: ${shareId} code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await zb.zfs.inherit( - datasetName, - FREENAS_SMB_SHARE_PROPERTY_NAME - ); - } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - break; - case "iscsi": - // Delete target - // NOTE: deletting a target inherently deletes associated targetgroup(s) and targettoextent(s) - - // Delete extent - try { - properties = await zb.zfs.get(datasetName, [ - FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, - FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME, - FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME, - FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; - } - throw err; - } - - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - let targetId = properties[FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME].value; - let extentId = properties[FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME].value; - let iscsiName = - properties[FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME].value; - let assetName; - - switch (apiVersion) { - case 1: - case 2: - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(targetId)) { - // https://jira.ixsystems.com/browse/NAS-103952 - - // v1 - /services/iscsi/target/{id}/ - // v2 - /iscsi/target/id/{id} - endpoint = ""; - if (apiVersion == 1) { - endpoint += "/services"; - } - endpoint += "/iscsi/target/"; - if (apiVersion == 2) { - endpoint += "id/"; - } - endpoint += targetId; - response = await httpClient.get(endpoint); - - // assume is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { - deleteAsset = true; - assetName = null; - - // checking if set for backwards compatibility - if (zb.helpers.isPropertyValueSet(iscsiName)) { - switch (apiVersion) { - case 1: - assetName = response.body.iscsi_target_name; - break; - case 2: - assetName = response.body.name; - break; - } - - if (assetName != iscsiName) { - deleteAsset = false; - } - } - - if (deleteAsset) { - let retries = 0; - let maxRetries = 5; - let retryWait = 1000; - response = await httpClient.delete(endpoint); - - // sometimes after an initiator has detached it takes a moment for TrueNAS to settle - // code: 422 body: {\"message\":\"Target csi-ci-55877e95sanity-node-expand-volume-e54f81fa-cd38e798 is in use.\",\"errno\":14} - while ( - response.statusCode == 422 && - retries < maxRetries && - _.get(response, "body.message").includes("Target") && - _.get(response, "body.message").includes("is in use") && - _.get(response, "body.errno") == 14 - ) { - retries++; - this.ctx.logger.debug( - "target: %s is in use, retry %s shortly", - targetId, - retries - ); - await GeneralUtils.sleep(retryWait); - response = await httpClient.delete(endpoint); - } - - if (![200, 204].includes(response.statusCode)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error deleting iscsi target - target: ${targetId} code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await zb.zfs.inherit( - datasetName, - FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME - ); - } else { - this.ctx.logger.debug( - "not deleting iscsitarget asset as it appears ID %s has been re-used: zfs name - %s, iscsitarget name - %s", - targetId, - iscsiName, - assetName - ); - } - } - } - - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(extentId)) { - // v1 - /services/iscsi/targettoextent/{id}/ - // v2 - /iscsi/targetextent/id/{id} - if (apiVersion == 1) { - endpoint = "/services/iscsi/extent/"; - } else { - endpoint = "/iscsi/extent/id/"; - } - endpoint += extentId; - response = await httpClient.get(endpoint); - - // assume is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { - deleteAsset = true; - assetName = null; - - // checking if set for backwards compatibility - if (zb.helpers.isPropertyValueSet(iscsiName)) { - switch (apiVersion) { - case 1: - assetName = response.body.iscsi_target_extent_name; - break; - case 2: - assetName = response.body.name; - break; - } - - if (assetName != iscsiName) { - deleteAsset = false; - } - } - - if (deleteAsset) { - response = await httpClient.delete(endpoint); - if (![200, 204].includes(response.statusCode)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error deleting iscsi extent - extent: ${extentId} code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await zb.zfs.inherit( - datasetName, - FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME - ); - } else { - this.ctx.logger.debug( - "not deleting iscsiextent asset as it appears ID %s has been re-used: zfs name - %s, iscsiextent name - %s", - extentId, - iscsiName, - assetName - ); - } - } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown driverShareType ${driverShareType}` - ); - } - } - - async setFilesystemMode(path, mode) { - const httpClient = await this.getHttpClient(); - const apiVersion = httpClient.getApiVersion(); - const httpApiClient = await this.getTrueNASHttpApiClient(); - - switch (apiVersion) { - case 1: - return super.setFilesystemMode(...arguments); - case 2: - let perms = { - path, - mode: String(mode), - }; - - /* - { - "path": "string", - "mode": "string", - "uid": 0, - "gid": 0, - "options": { - "stripacl": false, - "recursive": false, - "traverse": false - } - } - */ - - let response; - let endpoint; - - endpoint = `/filesystem/setperm`; - response = await httpClient.post(endpoint, perms); - - if (response.statusCode == 200) { - await httpApiClient.CoreWaitForJob(response.body, 30); - return; - } - - throw new Error(JSON.stringify(response.body)); - - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - - async setFilesystemOwnership(path, user = false, group = false) { - const httpClient = await this.getHttpClient(); - const apiVersion = httpClient.getApiVersion(); - const httpApiClient = await this.getTrueNASHttpApiClient(); - - if (user === false || typeof user == "undefined" || user === null) { - user = ""; - } - - if (group === false || typeof group == "undefined" || group === null) { - group = ""; - } - - user = String(user); - group = String(group); - - if (user.length < 1 && group.length < 1) { - return; - } - - switch (apiVersion) { - case 1: - return super.setFilesystemOwnership(...arguments); - case 2: - let perms = { - path, - }; - // set ownership - - // user - if (user.length > 0) { - if (String(user).match(/^[0-9]+$/) == null) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `BREAKING CHANGE since v1.5.3! datasetPermissionsUser must be numeric: ${user} is invalid` - ); - } - perms.uid = Number(user); - } - - // group - if (group.length > 0) { - if (String(group).match(/^[0-9]+$/) == null) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `BREAKING CHANGE since v1.5.3! datasetPermissionsGroup must be numeric: ${group} is invalid` - ); - } - perms.gid = Number(group); - } - - /* - { - "path": "string", - "mode": "string", - "uid": 0, - "gid": 0, - "options": { - "stripacl": false, - "recursive": false, - "traverse": false - } - } - */ - - let response; - let endpoint; - - endpoint = `/filesystem/setperm`; - response = await httpClient.post(endpoint, perms); - - if (response.statusCode == 200) { - await httpApiClient.CoreWaitForJob(response.body, 30); - return; - } - - throw new Error(JSON.stringify(response.body)); - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - - async expandVolume(call, datasetName) { - const driverShareType = this.getDriverShareType(); - const execClient = this.getExecClient(); - const httpClient = await this.getHttpClient(); - const apiVersion = httpClient.getApiVersion(); - const zb = await this.getZetabyte(); - - switch (driverShareType) { - case "iscsi": - const isScale = await this.getIsScale(); - let command; - let reload = false; - if (isScale) { - let properties; - properties = await zb.zfs.get(datasetName, [ - FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME, - ]); - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - let iscsiName = - properties[FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME].value; - - // name correlates to the extent NOT the target - let kName = iscsiName.replaceAll(".", "_"); - - /** - * command = execClient.buildCommand("systemctl", ["reload", "scst"]); - * does not help ^ - * - * echo 1 > /sys/kernel/scst_tgt/devices/${iscsiName}/resync_size - * works ^ - * - * scstadmin -resync_dev ${iscsiName} - * works but always give a exit code of 1 ^ - * - * midclt resync_lun_size_for_zvol tank/foo/bar - * works on SCALE only ^ - * - */ - command = execClient.buildCommand("sh", [ - "-c", - `"echo 1 > /sys/kernel/scst_tgt/devices/${kName}/resync_size"`, - ]); - reload = true; - } else { - switch (apiVersion) { - case 1: - // use cli for now - command = execClient.buildCommand("/etc/rc.d/ctld", ["reload"]); - reload = true; - break; - case 2: - this.ctx.logger.verbose( - "FreeNAS reloading iscsi daemon using api" - ); - // POST /service/reload - let payload = { - service: "iscsitarget", // api version of ctld, same name in SCALE as well - "service-control": { - ha_propagate: true, - }, - }; - let response = await httpClient.post("/service/reload", payload); - if (![200, 204].includes(response.statusCode)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error reloading iscsi daemon - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - return; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - - if (reload) { - if ((await this.getWhoAmI()) != "root") { - command = (await this.getSudoPath()) + " " + command; - } - - this.ctx.logger.verbose( - "FreeNAS reloading iscsi daemon: %s", - command - ); - - let response = await execClient.exec(command); - if (response.code != 0) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error reloading iscsi daemon: ${JSON.stringify(response)}` - ); - } - } - break; - } - } - - async getApiVersion() { - const systemVersion = await this.getSystemVersion(); - - if (systemVersion.v2) { - if ((await this.getSystemVersionMajorMinor()) == 11.2) { - return 1; - } - return 2; - } - - if (systemVersion.v1) { - return 1; - } - - return 2; - } - - async getIsFreeNAS() { - const systemVersion = await this.getSystemVersion(); - let version; - - if (systemVersion.v2) { - version = systemVersion.v2; - } else { - version = systemVersion.v1.fullversion; - } - - if (version.toLowerCase().includes("freenas")) { - return true; - } - - return false; - } - - async getIsTrueNAS() { - const systemVersion = await this.getSystemVersion(); - let version; - - if (systemVersion.v2) { - version = systemVersion.v2; - } else { - version = systemVersion.v1.fullversion; - } - - if (version.toLowerCase().includes("truenas")) { - return true; - } - - return false; - } - - async getIsScale() { - const systemVersion = await this.getSystemVersion(); - - if (systemVersion.v2 && systemVersion.v2.toLowerCase().includes("scale")) { - return true; - } - - return false; - } - - async getSystemVersionMajorMinor() { - const systemVersion = await this.getSystemVersion(); - let parts; - let parts_i; - let version; - - /* - systemVersion.v2 = "FreeNAS-11.2-U5"; - systemVersion.v2 = "TrueNAS-SCALE-20.11-MASTER-20201127-092915"; - systemVersion.v1 = { - fullversion: "FreeNAS-9.3-STABLE-201503200528", - fullversion: "FreeNAS-11.2-U5 (c129415c52)", - }; - - systemVersion.v2 = null; - */ - - if (systemVersion.v2) { - version = systemVersion.v2; - } else { - version = systemVersion.v1.fullversion; - } - - if (version) { - parts = version.split("-"); - parts_i = []; - parts.forEach((value) => { - let i = value.replace(/[^\d.]/g, ""); - if (i.length > 0) { - parts_i.push(i); - } - }); - - // join and resplit to deal with single elements which contain a decimal - parts_i = parts_i.join(".").split("."); - parts_i.splice(2); - return parts_i.join("."); - } - } - - async getSystemVersionMajor() { - const majorMinor = await this.getSystemVersionMajorMinor(); - return majorMinor.split(".")[0]; - } - - async setVersionInfoCache(versionInfo) { - const driver = this; - - await driver.ctx.cache.set(FREENAS_SYSTEM_VERSION_CACHE_KEY, versionInfo, { - ttl: 60 * 1000, - }); - } - - async getSystemVersion() { - const driver = this; - let cacheData = await driver.ctx.cache.get( - FREENAS_SYSTEM_VERSION_CACHE_KEY - ); - - if (cacheData) { - return cacheData; - } - - const httpClient = await this.getHttpClient(false); - const endpoint = "/system/version/"; - let response; - const startApiVersion = httpClient.getApiVersion(); - const versionInfo = {}; - const versionErrors = {}; - const versionResponses = {}; - - httpClient.setApiVersion(2); - /** - * FreeNAS-11.2-U5 - * TrueNAS-12.0-RELEASE - * TrueNAS-SCALE-20.11-MASTER-20201127-092915 - */ - try { - response = await httpClient.get(endpoint, null, { timeout: 5 * 1000 }); - versionResponses.v2 = response; - if (response.statusCode == 200) { - versionInfo.v2 = response.body; - - // return immediately to save on resources and silly requests - await this.setVersionInfoCache(versionInfo); - - // reset apiVersion - httpClient.setApiVersion(startApiVersion); - - return versionInfo; - } - } catch (e) { - // if more info is needed use e.stack - versionErrors.v2 = e.toString(); - } - - httpClient.setApiVersion(1); - /** - * {"fullversion": "FreeNAS-9.3-STABLE-201503200528", "name": "FreeNAS", "version": "9.3"} - * {"fullversion": "FreeNAS-11.2-U5 (c129415c52)", "name": "FreeNAS", "version": ""} - */ - try { - response = await httpClient.get(endpoint, null, { timeout: 5 * 1000 }); - versionResponses.v1 = response; - if (response.statusCode == 200 && IsJsonString(response.body)) { - versionInfo.v1 = response.body; - await this.setVersionInfoCache(versionInfo); - - // reset apiVersion - httpClient.setApiVersion(startApiVersion); - - return versionInfo; - } - } catch (e) { - // if more info is needed use e.stack - versionErrors.v1 = e.toString(); - } - - // throw error if cannot get v1 or v2 data - // likely bad creds/url - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS error getting system version info: ${GeneralUtils.stringify({ - errors: versionErrors, - responses: versionResponses, - })}` - ); - } -} - -function IsJsonString(str) { - try { - JSON.parse(str); - } catch (e) { - return false; - } - return true; -} - -module.exports.FreeNASSshDriver = FreeNASSshDriver; diff --git a/src/driver/node-manual/index.js b/src/driver/node-manual/index.js deleted file mode 100644 index c55915e..0000000 --- a/src/driver/node-manual/index.js +++ /dev/null @@ -1,341 +0,0 @@ -const { CsiBaseDriver } = require("../index"); -const { GrpcError, grpc } = require("../../utils/grpc"); -const semver = require("semver"); - -/** - * Driver which only runs the node portion and is meant to be used entirely - * with manually created PVs - */ -class NodeManualDriver extends CsiBaseDriver { - constructor(ctx, options) { - super(...arguments); - - options = options || {}; - options.service = options.service || {}; - options.service.identity = options.service.identity || {}; - options.service.controller = options.service.controller || {}; - options.service.node = options.service.node || {}; - - options.service.identity.capabilities = - options.service.identity.capabilities || {}; - - options.service.controller.capabilities = - options.service.controller.capabilities || {}; - - options.service.node.capabilities = options.service.node.capabilities || {}; - - if (!("service" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity service caps"); - - options.service.identity.capabilities.service = [ - //"UNKNOWN", - //"CONTROLLER_SERVICE", - //"VOLUME_ACCESSIBILITY_CONSTRAINTS" - ]; - } - - if (!("volume_expansion" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity volume_expansion caps"); - - options.service.identity.capabilities.volume_expansion = [ - //"UNKNOWN", - //"ONLINE", - //"OFFLINE" - ]; - } - - if (!("rpc" in options.service.controller.capabilities)) { - this.ctx.logger.debug("setting default controller caps"); - - options.service.controller.capabilities.rpc = [ - //"UNKNOWN", - //"CREATE_DELETE_VOLUME", - //"PUBLISH_UNPUBLISH_VOLUME", - //"LIST_VOLUMES", - //"GET_CAPACITY", - //"CREATE_DELETE_SNAPSHOT", - //"LIST_SNAPSHOTS", - //"CLONE_VOLUME", - //"PUBLISH_READONLY", - //"EXPAND_VOLUME", - ]; - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - options.service.controller.capabilities.rpc - .push - //"VOLUME_CONDITION", - //"GET_VOLUME" - (); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.controller.capabilities.rpc - .push - //"SINGLE_NODE_MULTI_WRITER" - (); - } - } - - if (!("rpc" in options.service.node.capabilities)) { - this.ctx.logger.debug("setting default node caps"); - - options.service.node.capabilities.rpc = [ - //"UNKNOWN", - "STAGE_UNSTAGE_VOLUME", - "GET_VOLUME_STATS", - //"EXPAND_VOLUME" - ]; - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - //options.service.node.capabilities.rpc.push("VOLUME_CONDITION"); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER"); - /** - * This is for volumes that support a mount time gid such as smb or fat - */ - //options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP"); - } - } - } - - assertCapabilities(capabilities, node_attach_driver) { - this.ctx.logger.verbose("validating capabilities: %j", capabilities); - - let message = null; - let driverResourceType; - let fs_types = []; - let access_modes = []; - //[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}] - switch (node_attach_driver) { - case "nfs": - driverResourceType = "filesystem"; - fs_types = ["nfs"]; - break; - case "smb": - driverResourceType = "filesystem"; - fs_types = ["cifs"]; - break; - case "lustre": - driverResourceType = "filesystem"; - fs_types = ["lustre"]; - break; - case "objectivefs": - driverResourceType = "filesystem"; - fs_types = ["objectivefs", "fuse.objectivefs"]; - break; - case "oneclient": - driverResourceType = "filesystem"; - fs_types = ["oneclient", "fuse.oneclient"]; - break; - case "hostpath": - driverResourceType = "filesystem"; - break; - case "iscsi": - case "nvmeof": - driverResourceType = "volume"; - fs_types = ["btrfs", "ext3", "ext4", "ext4dev", "xfs"]; - break; - case "zfs-local": - driverResourceType = "volume"; - fs_types = ["btrfs", "ext3", "ext4", "ext4dev", "xfs", "zfs"]; - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - ]; - default: - return { - valid: false, - message: `unknown node_attach_driver: ${node_attach_driver}`, - }; - } - - const valid = capabilities.every((capability) => { - switch (driverResourceType) { - case "filesystem": - if (access_modes.length == 0) { - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - "MULTI_NODE_READER_ONLY", - "MULTI_NODE_SINGLE_WRITER", - "MULTI_NODE_MULTI_WRITER", - ]; - } - - if ( - capability.access_type == "block" && - !access_modes.includes("MULTI_NODE_MULTI_WRITER") - ) { - access_modes.push("MULTI_NODE_MULTI_WRITER"); - } - - if (capability.access_type != "mount") { - message = `invalid access_type ${capability.access_type}`; - return false; - } - - if ( - capability.mount.fs_type && - !fs_types.includes(capability.mount.fs_type) - ) { - message = `invalid fs_type ${capability.mount.fs_type}`; - return false; - } - - if (!access_modes.includes(capability.access_mode.mode)) { - message = `invalid access_mode, ${capability.access_mode.mode}`; - return false; - } - - return true; - case "volume": - if (access_modes.length == 0) { - access_modes = [ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - "MULTI_NODE_READER_ONLY", - "MULTI_NODE_SINGLE_WRITER", - ]; - } - - if ( - capability.access_type == "block" && - !access_modes.includes("MULTI_NODE_MULTI_WRITER") - ) { - access_modes.push("MULTI_NODE_MULTI_WRITER"); - } - - if (capability.access_type == "mount") { - if ( - capability.mount.fs_type && - !fs_types.includes(capability.mount.fs_type) - ) { - message = `invalid fs_type ${capability.mount.fs_type}`; - return false; - } - } - - if (!access_modes.includes(capability.access_mode.mode)) { - message = `invalid access_mode, ${capability.access_mode.mode}`; - return false; - } - - return true; - } - }); - - return { valid, message }; - } - - /** - * - * @param {*} call - */ - async CreateVolume(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async DeleteVolume(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async ControllerExpandVolume(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async GetCapacity(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async ListVolumes(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async ListSnapshots(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async CreateSnapshot(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async DeleteSnapshot(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } - - /** - * - * @param {*} call - */ - async ValidateVolumeCapabilities(call) { - throw new GrpcError( - grpc.status.UNIMPLEMENTED, - `operation not supported by driver` - ); - } -} - -module.exports.NodeManualDriver = NodeManualDriver; diff --git a/src/driver/zfs-local-ephemeral-inline/index.js b/src/driver/zfs-local-ephemeral-inline/index.js deleted file mode 100644 index 276b1f4..0000000 --- a/src/driver/zfs-local-ephemeral-inline/index.js +++ /dev/null @@ -1,509 +0,0 @@ -const fs = require("fs"); -const { CsiBaseDriver } = require("../index"); -const { GrpcError, grpc } = require("../../utils/grpc"); -const { Filesystem } = require("../../utils/filesystem"); -const semver = require("semver"); -const SshClient = require("../../utils/zfs_ssh_exec_client").SshClient; -const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); - -// zfs common properties -const MANAGED_PROPERTY_NAME = "democratic-csi:managed_resource"; -const SUCCESS_PROPERTY_NAME = "democratic-csi:provision_success"; -const VOLUME_CSI_NAME_PROPERTY_NAME = "democratic-csi:csi_volume_name"; -const VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME = - "democratic-csi:volume_context_provisioner_driver"; -const VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME = - "democratic-csi:volume_context_provisioner_instance_id"; -const __REGISTRY_NS__ = "ZfsLocalEphemeralInlineDriver"; - -/** - * https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20190122-csi-inline-volumes.md - * https://kubernetes-csi.github.io/docs/ephemeral-local-volumes.html - * - * Sample calls: - * - https://gcsweb.k8s.io/gcs/kubernetes-jenkins/pr-logs/pull/92387/pull-kubernetes-e2e-gce/1280784994997899264/artifacts/_sig-storage_CSI_Volumes/_Driver_csi-hostpath_/_Testpattern_inline_ephemeral_CSI_volume_ephemeral/should_create_read_write_inline_ephemeral_volume/ - * - https://storage.googleapis.com/kubernetes-jenkins/pr-logs/pull/92387/pull-kubernetes-e2e-gce/1280784994997899264/artifacts/_sig-storage_CSI_Volumes/_Driver_csi-hostpath_/_Testpattern_inline_ephemeral_CSI_volume_ephemeral/should_create_read-only_inline_ephemeral_volume/csi-hostpathplugin-0-hostpath.log - * - * inline drivers are assumed to be mount only (no block support) - * purposely there is no native support for size contraints - * - * TODO: support creating zvols and formatting and mounting locally instead of using zfs dataset? - * - */ -class ZfsLocalEphemeralInlineDriver extends CsiBaseDriver { - constructor(ctx, options) { - super(...arguments); - - options = options || {}; - options.service = options.service || {}; - options.service.identity = options.service.identity || {}; - options.service.controller = options.service.controller || {}; - options.service.node = options.service.node || {}; - - options.service.identity.capabilities = - options.service.identity.capabilities || {}; - - options.service.controller.capabilities = - options.service.controller.capabilities || {}; - - options.service.node.capabilities = options.service.node.capabilities || {}; - - if (!("service" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity service caps"); - - options.service.identity.capabilities.service = [ - "UNKNOWN", - //"CONTROLLER_SERVICE" - //"VOLUME_ACCESSIBILITY_CONSTRAINTS" - ]; - } - - if (!("volume_expansion" in options.service.identity.capabilities)) { - this.ctx.logger.debug("setting default identity volume_expansion caps"); - - options.service.identity.capabilities.volume_expansion = [ - "UNKNOWN", - //"ONLINE", - //"OFFLINE" - ]; - } - - if (!("rpc" in options.service.controller.capabilities)) { - this.ctx.logger.debug("setting default controller caps"); - - options.service.controller.capabilities.rpc = [ - //"UNKNOWN", - //"CREATE_DELETE_VOLUME", - //"PUBLISH_UNPUBLISH_VOLUME", - //"LIST_VOLUMES", - //"GET_CAPACITY", - //"CREATE_DELETE_SNAPSHOT", - //"LIST_SNAPSHOTS", - //"CLONE_VOLUME", - //"PUBLISH_READONLY", - //"EXPAND_VOLUME" - ]; - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - options.service.controller.capabilities.rpc - .push - //"VOLUME_CONDITION", - //"GET_VOLUME" - (); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.controller.capabilities.rpc - .push - //"SINGLE_NODE_MULTI_WRITER" - (); - } - } - - if (!("rpc" in options.service.node.capabilities)) { - this.ctx.logger.debug("setting default node caps"); - options.service.node.capabilities.rpc = [ - //"UNKNOWN", - //"STAGE_UNSTAGE_VOLUME", - "GET_VOLUME_STATS", - //"EXPAND_VOLUME", - ]; - - if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { - //options.service.node.capabilities.rpc.push("VOLUME_CONDITION"); - } - - if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { - options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER"); - /** - * This is for volumes that support a mount time gid such as smb or fat - */ - //options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP"); - } - } - } - - getSshClient() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:ssh_client`, () => { - return new SshClient({ - logger: this.ctx.logger, - connection: this.options.sshConnection, - }); - }); - } - - getZetabyte() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:zb`, () => { - let sshClient; - let executor; - if (this.options.sshConnection) { - sshClient = this.getSshClient(); - executor = new ZfsSshProcessManager(sshClient); - } - return new Zetabyte({ - executor, - idempotent: true, - chroot: this.options.zfs.chroot, - paths: { - zpool: "/usr/sbin/zpool", - zfs: "/usr/sbin/zfs", - }, - }); - }); - } - - getDatasetParentName() { - let datasetParentName = this.options.zfs.datasetParentName; - datasetParentName = datasetParentName.replace(/\/$/, ""); - return datasetParentName; - } - - getVolumeParentDatasetName() { - let datasetParentName = this.getDatasetParentName(); - datasetParentName += "/v"; - datasetParentName = datasetParentName.replace(/\/$/, ""); - return datasetParentName; - } - - assertCapabilities(capabilities) { - // hard code this for now - const driverZfsResourceType = "filesystem"; - this.ctx.logger.verbose("validating capabilities: %j", capabilities); - - let message = null; - //[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}] - const valid = capabilities.every((capability) => { - switch (driverZfsResourceType) { - case "filesystem": - if (capability.access_type != "mount") { - message = `invalid access_type ${capability.access_type}`; - return false; - } - - if ( - capability.mount.fs_type && - !["zfs"].includes(capability.mount.fs_type) - ) { - message = `invalid fs_type ${capability.mount.fs_type}`; - return false; - } - - if ( - capability.mount.mount_flags && - capability.mount.mount_flags.length > 0 - ) { - message = `invalid mount_flags ${capability.mount.mount_flags}`; - return false; - } - - if ( - ![ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - ].includes(capability.access_mode.mode) - ) { - message = `invalid access_mode, ${capability.access_mode.mode}`; - return false; - } - - return true; - case "volume": - if (capability.access_type == "mount") { - if ( - capability.mount.fs_type && - !["btrfs", "ext3", "ext4", "ext4dev", "xfs"].includes( - capability.mount.fs_type - ) - ) { - message = `invalid fs_type ${capability.mount.fs_type}`; - return false; - } - } - - if ( - ![ - "UNKNOWN", - "SINGLE_NODE_WRITER", - "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 - "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 - "SINGLE_NODE_READER_ONLY", - ].includes(capability.access_mode.mode) - ) { - message = `invalid access_mode, ${capability.access_mode.mode}`; - return false; - } - - return true; - } - }); - - return { valid, message }; - } - - /** - * This should create a dataset with appropriate volume properties, ensuring - * the mountpoint is the target_path - * - * Any volume_context attributes starting with property. will be set as zfs properties - * - * { - "target_path": "/var/lib/kubelet/pods/f8b237db-19e8-44ae-b1d2-740c9aeea702/volumes/kubernetes.io~csi/my-volume-0/mount", - "volume_capability": { - "AccessType": { - "Mount": {} - }, - "access_mode": { - "mode": 1 - } - }, - "volume_context": { - "csi.storage.k8s.io/ephemeral": "true", - "csi.storage.k8s.io/pod.name": "inline-volume-tester-2ptb7", - "csi.storage.k8s.io/pod.namespace": "ephemeral-468", - "csi.storage.k8s.io/pod.uid": "f8b237db-19e8-44ae-b1d2-740c9aeea702", - "csi.storage.k8s.io/serviceAccount.name": "default", - "foo": "bar" - }, - "volume_id": "csi-8228252978a824126924de00126e6aec7c989a48a39d577bd3ab718647df5555" - } - * - * @param {*} call - */ - async NodePublishVolume(call) { - const driver = this; - const zb = this.getZetabyte(); - - const volume_id = call.request.volume_id; - const staging_target_path = call.request.staging_target_path || ""; - const target_path = call.request.target_path; - const capability = call.request.volume_capability; - const access_type = capability.access_type || "mount"; - const readonly = call.request.readonly; - const volume_context = call.request.volume_context; - - let datasetParentName = this.getVolumeParentDatasetName(); - let name = volume_id; - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - if (!name) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume_id is required` - ); - } - - if (!target_path) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `target_path is required` - ); - } - - if (capability) { - const result = this.assertCapabilities([capability]); - - if (result.valid !== true) { - throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); - } - } - - const datasetName = datasetParentName + "/" + name; - - // TODO: support arbitrary values from config - // TODO: support arbitrary props from volume_context - let volumeProperties = {}; - - // set user-supplied properties - // this come from volume_context from keys starting with property. - const base_key = "property."; - const prefixLength = `${base_key}`.length; - Object.keys(volume_context).forEach(function (key) { - if (key.startsWith(base_key)) { - let normalizedKey = key.slice(prefixLength); - volumeProperties[normalizedKey] = volume_context[key]; - } - }); - - // set standard properties - volumeProperties[VOLUME_CSI_NAME_PROPERTY_NAME] = name; - volumeProperties[MANAGED_PROPERTY_NAME] = "true"; - volumeProperties[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME] = - driver.options.driver; - if (driver.options.instance_id) { - volumeProperties[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME] = - driver.options.instance_id; - } - volumeProperties[SUCCESS_PROPERTY_NAME] = "true"; - - // NOTE: setting mountpoint will automatically create the full path as necessary so no need for mkdir etc - volumeProperties["mountpoint"] = target_path; - - // does not really make sense for ephemeral volumes..but we'll put it here in case - if (readonly) { - volumeProperties["readonly"] = "on"; - } - - // set driver config properties - if (this.options.zfs.properties) { - Object.keys(driver.options.zfs.properties).forEach(function (key) { - const value = driver.options.zfs.properties[key]["value"]; - const allowOverride = - "allowOverride" in driver.options.zfs.properties[key] - ? driver.options.zfs.properties[key]["allowOverride"] - : true; - - if (!allowOverride || !(key in volumeProperties)) { - volumeProperties[key] = value; - } - }); - } - - // TODO: catch out of space errors and return specifc grpc message? - await zb.zfs.create(datasetName, { - parents: true, - properties: volumeProperties, - }); - - return {}; - } - - /** - * This should destroy the dataset and remove target_path as appropriate - * - *{ - "target_path": "/var/lib/kubelet/pods/f8b237db-19e8-44ae-b1d2-740c9aeea702/volumes/kubernetes.io~csi/my-volume-0/mount", - "volume_id": "csi-8228252978a824126924de00126e6aec7c989a48a39d577bd3ab718647df5555" - } - * - * @param {*} call - */ - async NodeUnpublishVolume(call) { - const zb = this.getZetabyte(); - const filesystem = new Filesystem(); - let result; - - const volume_id = call.request.volume_id; - const target_path = call.request.target_path; - - let datasetParentName = this.getVolumeParentDatasetName(); - let name = volume_id; - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - if (!name) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `volume_id is required` - ); - } - - if (!target_path) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `target_path is required` - ); - } - - const datasetName = datasetParentName + "/" + name; - - // NOTE: -f does NOT allow deletes if dependent filesets exist - // NOTE: -R will recursively delete items + dependent filesets - // delete dataset - try { - await zb.zfs.destroy(datasetName, { recurse: true, force: true }); - } catch (err) { - if (err.toString().includes("filesystem has dependent clones")) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - "filesystem has dependent clones" - ); - } - - throw err; - } - - // cleanup publish directory - result = await filesystem.pathExists(target_path); - if (result) { - if (fs.lstatSync(target_path).isDirectory()) { - result = await filesystem.rmdir(target_path); - } else { - result = await filesystem.rm([target_path]); - } - } - - return {}; - } - - /** - * TODO: consider volume_capabilities? - * - * @param {*} call - */ - async GetCapacity(call) { - const driver = this; - const zb = this.getZetabyte(); - - let datasetParentName = this.getVolumeParentDatasetName(); - - if (!datasetParentName) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: missing datasetParentName` - ); - } - - if (call.request.volume_capabilities) { - const result = this.assertCapabilities(call.request.volume_capabilities); - - if (result.valid !== true) { - return { available_capacity: 0 }; - } - } - - const datasetName = datasetParentName; - - let properties; - properties = await zb.zfs.get(datasetName, ["avail"]); - properties = properties[datasetName]; - - return { available_capacity: properties.available.value }; - } - - /** - * - * @param {*} call - */ - async ValidateVolumeCapabilities(call) { - const driver = this; - const result = this.assertCapabilities(call.request.volume_capabilities); - - if (result.valid !== true) { - return { message: result.message }; - } - - return { - confirmed: { - volume_context: call.request.volume_context, - volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested - parameters: call.request.parameters, - }, - }; - } -} - -module.exports.ZfsLocalEphemeralInlineDriver = ZfsLocalEphemeralInlineDriver; diff --git a/src/utils/zfs_ssh_exec_client.js b/src/utils/zfs_ssh_exec_client.js deleted file mode 100644 index 4a232fa..0000000 --- a/src/utils/zfs_ssh_exec_client.js +++ /dev/null @@ -1,248 +0,0 @@ -const Client = require("ssh2").Client; -const { E_CANCELED, Mutex } = require("async-mutex"); -const GeneralUtils = require("./general"); - -class SshClient { - constructor(options = {}) { - const client = this; - this.options = options; - this.options.connection = this.options.connection || {}; - if (this.options.logger) { - this.logger = this.options.logger; - } else { - this.logger = console; - console.silly = console.debug; - } - - if (!this.options.connection.hasOwnProperty("keepaliveInterval")) { - this.options.connection.keepaliveInterval = 10000; - } - - if (this.options.connection.debug === true) { - this.options.connection.debug = function (msg) { - client.debug(msg); - }; - } - - this.conn_mutex = new Mutex(); - this.conn_state; - this.conn_err; - this.ready_event_count = 0; - this.error_event_count = 0; - - this.conn = new Client(); - // invoked before close - this.conn.on("end", () => { - this.conn_state = "ended"; - this.debug("Client :: end"); - }); - // invoked after end - this.conn.on("close", () => { - this.conn_state = "closed"; - this.debug("Client :: close"); - }); - this.conn.on("error", (err) => { - this.conn_state = "error"; - this.conn_err = err; - this.error_event_count++; - this.debug("Client :: error"); - }); - this.conn.on("ready", () => { - this.conn_state = "ready"; - this.ready_event_count++; - this.debug("Client :: ready"); - }); - } - - /** - * Build a command line from the name and given args - * TODO: escape the arguments - * - * @param {*} name - * @param {*} args - */ - buildCommand(name, args = []) { - args.unshift(name); - return args.join(" "); - } - - debug() { - this.logger.silly(...arguments); - } - - async _connect() { - const start_ready_event_count = this.ready_event_count; - const start_error_event_count = this.error_event_count; - try { - await this.conn_mutex.runExclusive(async () => { - this.conn.connect(this.options.connection); - do { - if (start_error_event_count != this.error_event_count) { - throw this.conn_err; - } - - if (start_ready_event_count != this.ready_event_count) { - break; - } - - await GeneralUtils.sleep(100); - } while (true); - }); - } catch (err) { - if (err === E_CANCELED) { - return; - } - throw err; - } - } - - async connect() { - if (this.conn_state == "ready") { - return; - } - - return this._connect(); - } - - async exec(command, options = {}, stream_proxy = null) { - // default is to reuse - if (process.env.SSH_REUSE_CONNECTION == "0") { - return this._nexec(...arguments); - } else { - return this._rexec(...arguments); - } - } - - async _rexec(command, options = {}, stream_proxy = null) { - const client = this; - const conn = this.conn; - - return new Promise(async (resolve, reject) => { - do { - try { - await this.connect(); - conn.exec(command, options, function (err, stream) { - if (err) { - reject(err); - return; - } - let stderr; - let stdout; - - if (stream_proxy) { - stream_proxy.on("kill", (signal) => { - stream.destroy(); - }); - } - - stream - .on("close", function (code, signal) { - client.debug( - "Stream :: close :: code: " + code + ", signal: " + signal - ); - if (stream_proxy) { - stream_proxy.emit("close", ...arguments); - } - resolve({ stderr, stdout, code, signal }); - //conn.end(); - }) - .on("data", function (data) { - client.debug("STDOUT: " + data); - if (stream_proxy) { - stream_proxy.stdout.emit("data", ...arguments); - } - if (stdout == undefined) { - stdout = ""; - } - stdout = stdout.concat(data); - }) - .stderr.on("data", function (data) { - client.debug("STDERR: " + data); - if (stream_proxy) { - stream_proxy.stderr.emit("data", ...arguments); - } - if (stderr == undefined) { - stderr = ""; - } - stderr = stderr.concat(data); - }); - }); - break; - } catch (err) { - if (err.message && !err.message.includes("Not connected")) { - throw err; - } - } - await GeneralUtils.sleep(1000); - } while (true); - }); - } - - async _nexec(command, options = {}, stream_proxy = null) { - const client = this; - return new Promise((resolve, reject) => { - var conn = new Client(); - - conn - .on("error", function (err) { - client.debug("Client :: error"); - reject(err); - }) - .on("ready", function () { - client.debug("Client :: ready"); - //options.pty = true; - //options.env = { - // TERM: "", - //}; - conn.exec(command, options, function (err, stream) { - if (err) { - reject(err); - return; - } - let stderr; - let stdout; - stream - .on("close", function (code, signal) { - client.debug( - "Stream :: close :: code: " + code + ", signal: " + signal - ); - if (stream_proxy) { - stream_proxy.emit("close", ...arguments); - } - resolve({ stderr, stdout, code, signal }); - conn.end(); - }) - .on("data", function (data) { - client.debug("STDOUT: " + data); - if (stream_proxy) { - stream_proxy.stdout.emit("data", ...arguments); - } - if (stdout == undefined) { - stdout = ""; - } - stdout = stdout.concat(data); - }) - .stderr.on("data", function (data) { - client.debug("STDERR: " + data); - if (stream_proxy) { - stream_proxy.stderr.emit("data", ...arguments); - } - if (stderr == undefined) { - stderr = ""; - } - stderr = stderr.concat(data); - }); - }); - }) - .connect(client.options.connection); - - if (stream_proxy) { - stream_proxy.on("kill", (signal) => { - conn.end(); - }); - } - }); - } -} - -module.exports.SshClient = SshClient;