From 855f48d3af12379bbd4fb41b7676ba80c4dd748d Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 21 Sep 2021 21:36:14 -0600 Subject: [PATCH] csi-test conformance, relax iscsi name length Signed-off-by: Travis Glenn Hansen --- CHANGELOG.md | 13 + README.md | 1 + examples/freenas-api-iscsi.yaml | 2 +- examples/freenas-iscsi.yaml | 2 +- package-lock.json | 64 ++-- package.json | 2 +- src/driver/controller-client-common/index.js | 55 +++- src/driver/controller-synology/http/index.js | 35 ++ src/driver/controller-synology/index.js | 154 ++++++++- src/driver/controller-zfs-ssh/index.js | 298 +++++++++++++++-- src/driver/freenas/api.js | 321 ++++++++++++++++--- src/driver/freenas/ssh.js | 9 +- src/driver/index.js | 106 +++++- src/utils/zfs.js | 18 +- 14 files changed, 951 insertions(+), 129 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ba857..183c4f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# v1.4.0 + +Released 2021-09-21 + +- more advanced logic for iscsi naming limits (allowing > 63 chars in certain + circumstances, SCALE, linux, FreeBSD 13+) +- various updates to support running the csi-test tool and conform to expected + responses/behaviors (full conformance for several drivers!) +- default `fs_type` during `NodeStageVolume` when omitted by `CO` +- automatcally add `guest` mount option to `cifs` shares when creds are absent +- fix `ListVolumes` and `ListSnapshot` behavior on various `zfs-generic-*` and + `freenas-*` drivers + # v1.3.2 Released 2021-09-09 diff --git a/README.md b/README.md index 7ae0083..1a14ded 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ non-`root` user when connecting to the FreeNAS server: account user query select=id,username,uid,sudo_nopasswd # find the `id` of the user you want to update (note, this is distinct from the `uid`) + account user update id= sudo=true account user update id= sudo_nopasswd=true # optional if you want to disable password #account user update id= password_disabled=true diff --git a/examples/freenas-api-iscsi.yaml b/examples/freenas-api-iscsi.yaml index b49407e..2dea051 100644 --- a/examples/freenas-api-iscsi.yaml +++ b/examples/freenas-api-iscsi.yaml @@ -37,7 +37,7 @@ zfs: # 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 + # 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 diff --git a/examples/freenas-iscsi.yaml b/examples/freenas-iscsi.yaml index 1e67356..9db8e8d 100644 --- a/examples/freenas-iscsi.yaml +++ b/examples/freenas-iscsi.yaml @@ -47,7 +47,7 @@ zfs: # 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 + # 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 diff --git a/package-lock.json b/package-lock.json index 85fc637..1fbe629 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "democratic-csi", - "version": "1.3.2", + "version": "1.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "democratic-csi", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { "@grpc/grpc-js": "^1.3.6", @@ -40,9 +40,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", - "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -197,9 +197,9 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.4.tgz", - "integrity": "sha512-7xvDvW/vJEcmLUltCUGOgWRPM8Oofv0eCFSVMuKqaqWJaXSzmB+m9hiyqe34QofAl4WAzIKUZZlinIF9FOHyTQ==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.5.tgz", + "integrity": "sha512-GZdzyVQI1Bln/kCzIYgTKu+rQJ5dno0gVrfmLe4jqQu7T2e7svSwJzpCBqVU5hhBSJP3peuPjOMWsj5GR61YmQ==", "dependencies": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", @@ -311,9 +311,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "node_modules/@types/node": { - "version": "16.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz", - "integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==" + "version": "16.9.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz", + "integrity": "sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==" }, "node_modules/acorn": { "version": "7.4.1", @@ -361,9 +361,9 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { "node": ">=8" } @@ -2793,9 +2793,9 @@ } }, "node_modules/table/node_modules/ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", @@ -3108,9 +3108,9 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", - "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", "dev": true }, "@babel/highlight": { @@ -3239,9 +3239,9 @@ } }, "@grpc/proto-loader": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.4.tgz", - "integrity": "sha512-7xvDvW/vJEcmLUltCUGOgWRPM8Oofv0eCFSVMuKqaqWJaXSzmB+m9hiyqe34QofAl4WAzIKUZZlinIF9FOHyTQ==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.5.tgz", + "integrity": "sha512-GZdzyVQI1Bln/kCzIYgTKu+rQJ5dno0gVrfmLe4jqQu7T2e7svSwJzpCBqVU5hhBSJP3peuPjOMWsj5GR61YmQ==", "requires": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", @@ -3343,9 +3343,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "16.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz", - "integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==" + "version": "16.9.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz", + "integrity": "sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==" }, "acorn": { "version": "7.4.1", @@ -3378,9 +3378,9 @@ "dev": true }, "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "4.3.0", @@ -5240,9 +5240,9 @@ }, "dependencies": { "ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", diff --git a/package.json b/package.json index 356aae8..c1e3914 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "democratic-csi", - "version": "1.3.2", + "version": "1.4.0", "description": "kubernetes csi driver framework", "main": "bin/democratic-csi", "scripts": { diff --git a/src/driver/controller-client-common/index.js b/src/driver/controller-client-common/index.js index 18c30ee..f08e1e2 100644 --- a/src/driver/controller-client-common/index.js +++ b/src/driver/controller-client-common/index.js @@ -1,6 +1,7 @@ const { CsiBaseDriver } = require("../index"); const { GrpcError, grpc } = require("../../utils/grpc"); const cp = require("child_process"); +const fs = require("fs"); const semver = require("semver"); /** @@ -329,6 +330,10 @@ class ControllerClientCommonDriver extends CsiBaseDriver { ]); } + async directoryExists(path) { + return fs.existsSync(path); + } + /** * Create a volume doing in essence the following: * 1. create directory @@ -353,11 +358,28 @@ class ControllerClientCommonDriver extends CsiBaseDriver { ); } - if (call.request.volume_capabilities) { + 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 ( @@ -431,6 +453,13 @@ class ControllerClientCommonDriver extends CsiBaseDriver { break; } + 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 source path: %s", source_path); response = await driver.cloneDir(source_path, volume_path); } @@ -630,7 +659,10 @@ class ControllerClientCommonDriver extends CsiBaseDriver { const volume_path = driver.getControllerVolumePath(source_volume_id); const snapshot_path = driver.getControllerSnapshotPath(snapshot_id); - await driver.cloneDir(volume_path, snapshot_path); + // do NOT overwrite existing snapshot + if (!(await driver.directoryExists(snapshot_path))) { + await driver.cloneDir(volume_path, snapshot_path); + } return { snapshot: { @@ -681,6 +713,25 @@ class ControllerClientCommonDriver extends CsiBaseDriver { */ 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) { diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index 9693024..e1f29f9 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -146,6 +146,17 @@ class SynologyHttpClient { }); } + 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", @@ -214,6 +225,30 @@ class SynologyHttpClient { } } + 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 GetSnapshotByLunIDAndName(lun_id, name) { const get_snapshot_info = { lid: lun_id, //check? diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index 6efe715..df25669 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -247,11 +247,28 @@ class ControllerSynologyDriver extends CsiBaseDriver { ); } - if (call.request.volume_capabilities) { + 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 ( @@ -314,17 +331,69 @@ class ControllerSynologyDriver extends CsiBaseDriver { let lun_uuid; let existingLun; + // 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; let src_lun_id; switch (volume_content_source.type) { case "snapshot": let parts = volume_content_source.snapshot.snapshot_id.split("/"); + src_lun_id = parts[2]; + if (!src_lun_id) { + 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}` + ); + } + let src_lun = await httpClient.GetLunByID(src_lun_id); src_lun_uuid = src_lun.uuid; + let snapshot = await httpClient.GetSnapshotByLunIDAndSnapshotUUID( + src_lun_id, + 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( @@ -340,8 +409,20 @@ class ControllerSynologyDriver extends CsiBaseDriver { 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); } break; @@ -740,8 +821,6 @@ class ControllerSynologyDriver extends CsiBaseDriver { ); } - // create snapshot here - let iscsiName = driver.buildIscsiName(source_volume_id); let lun = await httpClient.GetLunByName(iscsiName); @@ -751,6 +830,19 @@ class ControllerSynologyDriver extends CsiBaseDriver { `invalid source_volume_id: ${source_volume_id}` ); } + + // 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 = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name); @@ -827,7 +919,14 @@ class ControllerSynologyDriver extends CsiBaseDriver { let parts = snapshot_id.split("/"); let lun_id = parts[2]; + if (!lun_id) { + return {}; + } + let snapshot_uuid = parts[3]; + if (!snapshot_uuid) { + return {}; + } // TODO: delete snapshot let snapshot = await httpClient.GetSnapshotByLunIDAndSnapshotUUID( @@ -848,8 +947,55 @@ class ControllerSynologyDriver extends CsiBaseDriver { */ async ValidateVolumeCapabilities(call) { const driver = this; - const result = this.assertCapabilities(call.request.volume_capabilities); + 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 }; } diff --git a/src/driver/controller-zfs-ssh/index.js b/src/driver/controller-zfs-ssh/index.js index 92b8516..074f457 100644 --- a/src/driver/controller-zfs-ssh/index.js +++ b/src/driver/controller-zfs-ssh/index.js @@ -392,6 +392,62 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { 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 sshClient = driver.getSshClient(); + + let response; + let command; + let kernel; + let kernel_release; + + // get kernel + command = "uname -s"; + driver.ctx.logger.verbose("uname command: %s", command); + response = await sshClient.exec(command); + if (response.code !== 0) { + throw new Error("failed to run uname to determine max zvol name length"); + } else { + kernel = response.stdout.trim(); + } + + 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": + return 255; + case "freebsd": + // get kernel_release + command = "uname -r"; + driver.ctx.logger.verbose("uname command: %s", command); + response = await sshClient.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) { + return 255; + } else { + return 63; + } + } + default: + throw new Error(`unknown kernel: ${kernel}`); + } + } + /** * Ensure sane options are used etc * true = ready @@ -498,11 +554,28 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { ); } - if (call.request.volume_capabilities) { + 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 ( @@ -554,18 +627,75 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { ); } + // 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; - if (extentDiskName.length > 63) { + 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 63 characters: ${extentDiskName}` + `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` ); } } @@ -658,7 +788,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` ); } @@ -700,7 +830,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, "dataset does not exists" ); } @@ -720,7 +850,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` ); } @@ -766,7 +896,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, "dataset does not exists" ); } @@ -816,7 +946,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, "dataset does not exists" ); } @@ -1274,10 +1404,20 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { const datasetName = datasetParentName; let properties; - properties = await zb.zfs.get(datasetName, ["avail"]); - properties = properties[datasetName]; + try { + properties = await zb.zfs.get(datasetName, ["avail"]); + properties = properties[datasetName]; - return { available_capacity: properties.available.value }; + 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; + } + } } /** @@ -1375,7 +1515,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { let entries = []; let entries_length = 0; let next_token; - let uuid, page, next_page; + let uuid; let response; const max_entries = call.request.max_entries; @@ -1385,15 +1525,18 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { if (starting_token) { let parts = starting_token.split(":"); uuid = parts[0]; - page = parseInt(parts[1]); + 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 = JSON.parse(JSON.stringify(entries)); entries_length = entries.length; - entries = entries.splice((page - 1) * max_entries, max_entries); - if (page * max_entries < entries_length) { - next_page = page + 1; - next_token = `${uuid}:${next_page}`; + entries = entries.slice(start_position, end_position); + if (max_entries > 0 && end_position > entries_length) { + next_token = `${uuid}:${end_position}`; } else { next_token = null; } @@ -1404,7 +1547,10 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { return data; } else { - // TODO: throw error / cache expired + throw new GrpcError( + grpc.status.ABORTED, + `invalid starting_token: ${starting_token}` + ); } } @@ -1469,7 +1615,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { for (let row of response.indexed) { // ignore rows were csi_name is empty if (row[MANAGED_PROPERTY_NAME] != "true") { - return; + continue; } let volume = await driver.populateCsiVolumeFromData(row); @@ -1487,8 +1633,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { `ListVolumes:result:${uuid}`, JSON.parse(JSON.stringify(entries)) ); - next_token = `${uuid}:2`; - entries = entries.splice(0, max_entries); + next_token = `${uuid}:${max_entries}`; + entries = entries.slice(0, max_entries); } const data = { @@ -1511,7 +1657,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { let entries = []; let entries_length = 0; let next_token; - let uuid, page, next_page; + let uuid; const max_entries = call.request.max_entries; const starting_token = call.request.starting_token; @@ -1526,15 +1672,18 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { if (starting_token) { let parts = starting_token.split(":"); uuid = parts[0]; - page = parseInt(parts[1]); + 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 = JSON.parse(JSON.stringify(entries)); entries_length = entries.length; - entries = entries.splice((page - 1) * max_entries, max_entries); - if (page * max_entries < entries_length) { - next_page = page + 1; - next_token = `${uuid}:${next_page}`; + entries = entries.slice(start_position, end_position); + if (max_entries > 0 && end_position > entries_length) { + next_token = `${uuid}:${end_position}`; } else { next_token = null; } @@ -1545,7 +1694,10 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { return data; } else { - // TODO: throw error / cache expired + throw new GrpcError( + grpc.status.ABORTED, + `invalid starting_token: ${starting_token}` + ); } } @@ -1639,9 +1791,11 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { 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); @@ -1720,8 +1874,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { `ListSnapshots:result:${uuid}`, JSON.parse(JSON.stringify(entries)) ); - next_token = `${uuid}:2`; - entries = entries.splice(0, max_entries); + next_token = `${uuid}:${max_entries}`; + entries = entries.slice(0, max_entries); } const data = { @@ -1822,6 +1976,54 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { 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; @@ -2043,6 +2245,42 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { */ 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) { diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index bd06a3c..187eb20 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -170,6 +170,7 @@ class FreeNASApiDriver extends CsiBaseDriver { * @param {*} datasetName */ async createShare(call, datasetName) { + const driver = this; const driverShareType = this.getDriverShareType(); const httpClient = await this.getHttpClient(); const httpApiClient = await this.getTrueNASHttpApiClient(); @@ -515,7 +516,7 @@ class FreeNASApiDriver extends CsiBaseDriver { } //set zfs property - await zb.zfs.set(datasetName, { + await httpApiClient.DatasetSet(datasetName, { [FREENAS_SMB_SHARE_PROPERTY_NAME]: response.body.id, }); } else { @@ -620,15 +621,17 @@ class FreeNASApiDriver extends CsiBaseDriver { 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 > 63) { + if (extentDiskName.length > maxZvolNameLength) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `extent disk name cannot exceed 63 characters: ${extentDiskName}` + `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` ); } @@ -1431,7 +1434,7 @@ class FreeNASApiDriver extends CsiBaseDriver { // remove property to prevent delete race conditions // due to id re-use by FreeNAS/TrueNAS - await zb.zfs.inherit( + await httpApiClient.DatasetInherit( datasetName, FREENAS_SMB_SHARE_PROPERTY_NAME ); @@ -1922,6 +1925,32 @@ class FreeNASApiDriver extends CsiBaseDriver { return { valid, message }; } + /** + * 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 + * https://www.ixsystems.com/documentation/freenas/11.3-BETA1/intro.html#path-and-name-lengths + */ + async getMaxZvolNameLength() { + const driver = this; + const httpApiClient = await driver.getTrueNASHttpApiClient(); + + // 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 + if (await httpApiClient.getIsScale()) { + return 255; + } + + let major = await httpApiClient.getSystemVersionMajor(); + if (parseInt(major) >= 13) { + return 255; + } else { + return 63; + } + } + /** * Ensure sane options are used etc * true = ready @@ -1989,11 +2018,28 @@ class FreeNASApiDriver extends CsiBaseDriver { ); } - if (call.request.volume_capabilities) { + 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 ( @@ -2045,6 +2091,61 @@ class FreeNASApiDriver extends CsiBaseDriver { ); } + // ensure volumes with the same name being requested a 2nd time but with a different size fails + try { + let properties = await httpApiClient.DatasetGet(datasetName, [ + "volsize", + "refquota", + ]); + 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 @@ -2053,10 +2154,12 @@ class FreeNASApiDriver extends CsiBaseDriver { */ if (driverZfsResourceType == "volume") { let extentDiskName = "zvol/" + datasetName; - if (extentDiskName.length > 63) { + 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 63 characters: ${extentDiskName}` + `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` ); } } @@ -2147,9 +2250,12 @@ class FreeNASApiDriver extends CsiBaseDriver { try { await httpApiClient.SnapshotCreate(fullSnapshotName); } catch (err) { - if (err.toString().includes("dataset does not exist")) { + if ( + err.toString().includes("dataset does not exist") || + err.toString().includes("not found") + ) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` ); } @@ -2235,9 +2341,12 @@ class FreeNASApiDriver extends CsiBaseDriver { } ); } catch (err) { - if (err.toString().includes("dataset does not exist")) { + if ( + err.toString().includes("dataset does not exist") || + err.toString().includes("not found") + ) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, "dataset does not exists" ); } @@ -2253,9 +2362,12 @@ class FreeNASApiDriver extends CsiBaseDriver { defer: true, }); } catch (err) { - if (err.toString().includes("dataset does not exist")) { + if ( + err.toString().includes("dataset does not exist") || + err.toString().includes("not found") + ) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` ); } @@ -2299,9 +2411,12 @@ class FreeNASApiDriver extends CsiBaseDriver { try { response = await httpApiClient.SnapshotCreate(fullSnapshotName); } catch (err) { - if (err.toString().includes("dataset does not exist")) { + if ( + err.toString().includes("dataset does not exist") || + err.toString().includes("not found") + ) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, "dataset does not exists" ); } @@ -2393,9 +2508,12 @@ class FreeNASApiDriver extends CsiBaseDriver { } ); } catch (err) { - if (err.toString().includes("dataset does not exist")) { + if ( + err.toString().includes("dataset does not exist") || + err.toString().includes("not found") + ) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, "dataset does not exists" ); } @@ -2942,7 +3060,7 @@ class FreeNASApiDriver extends CsiBaseDriver { let entries = []; let entries_length = 0; let next_token; - let uuid, page, next_page; + let uuid; let response; let endpoint; @@ -2953,15 +3071,18 @@ class FreeNASApiDriver extends CsiBaseDriver { if (starting_token) { let parts = starting_token.split(":"); uuid = parts[0]; - page = parseInt(parts[1]); + 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 = JSON.parse(JSON.stringify(entries)); entries_length = entries.length; - entries = entries.splice((page - 1) * max_entries, max_entries); - if (page * max_entries < entries_length) { - next_page = page + 1; - next_token = `${uuid}:${next_page}`; + entries = entries.slice(start_position, end_position); + if (max_entries > 0 && end_position > entries_length) { + next_token = `${uuid}:${end_position}`; } else { next_token = null; } @@ -2972,7 +3093,10 @@ class FreeNASApiDriver extends CsiBaseDriver { return data; } else { - // TODO: throw error / cache expired + throw new GrpcError( + grpc.status.ABORTED, + `invalid starting_token: ${starting_token}` + ); } } @@ -3031,7 +3155,7 @@ class FreeNASApiDriver extends CsiBaseDriver { for (let row of rows) { // ignore rows were csi_name is empty if (row[MANAGED_PROPERTY_NAME] != "true") { - return; + continue; } let volume_id = row["name"].replace( @@ -3054,8 +3178,8 @@ class FreeNASApiDriver extends CsiBaseDriver { `ListVolumes:result:${uuid}`, JSON.parse(JSON.stringify(entries)) ); - next_token = `${uuid}:2`; - entries = entries.splice(0, max_entries); + next_token = `${uuid}:${max_entries}`; + entries = entries.slice(0, max_entries); } const data = { @@ -3080,7 +3204,7 @@ class FreeNASApiDriver extends CsiBaseDriver { let entries = []; let entries_length = 0; let next_token; - let uuid, page, next_page; + let uuid; const max_entries = call.request.max_entries; const starting_token = call.request.starting_token; @@ -3095,15 +3219,18 @@ class FreeNASApiDriver extends CsiBaseDriver { if (starting_token) { let parts = starting_token.split(":"); uuid = parts[0]; - page = parseInt(parts[1]); + 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 = JSON.parse(JSON.stringify(entries)); entries_length = entries.length; - entries = entries.splice((page - 1) * max_entries, max_entries); - if (page * max_entries < entries_length) { - next_page = page + 1; - next_token = `${uuid}:${next_page}`; + entries = entries.slice(start_position, end_position); + if (max_entries > 0 && end_position > entries_length) { + next_token = `${uuid}:${end_position}`; } else { next_token = null; } @@ -3114,7 +3241,10 @@ class FreeNASApiDriver extends CsiBaseDriver { return data; } else { - // TODO: throw error / cache expired + throw new GrpcError( + grpc.status.ABORTED, + `invalid starting_token: ${starting_token}` + ); } } @@ -3364,9 +3494,11 @@ class FreeNASApiDriver extends CsiBaseDriver { 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); @@ -3447,8 +3579,8 @@ class FreeNASApiDriver extends CsiBaseDriver { `ListSnapshots:result:${uuid}`, JSON.parse(JSON.stringify(entries)) ); - next_token = `${uuid}:2`; - entries = entries.splice(0, max_entries); + next_token = `${uuid}:${max_entries}`; + entries = entries.slice(0, max_entries); } const data = { @@ -3466,6 +3598,7 @@ class FreeNASApiDriver extends CsiBaseDriver { async CreateSnapshot(call) { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); + const httpClient = await this.getHttpClient(); const httpApiClient = await this.getTrueNASHttpApiClient(); const zb = await this.getZetabyte(); @@ -3550,6 +3683,80 @@ class FreeNASApiDriver extends CsiBaseDriver { driver.ctx.logger.verbose("cleansed snapshot name: %s", name); + // check for other snapshopts with the same name on other volumes and fail as appropriate + { + let endpoint; + let response; + + let datasets = []; + endpoint = `/pool/dataset/id/${encodeURIComponent( + this.getDetachedSnapshotParentDatasetName() + )}`; + response = await httpClient.get(endpoint); + + switch (response.statusCode) { + case 200: + for (let child of response.body.children) { + datasets = datasets.concat(child.children); + } + //console.log(datasets); + for (let dataset of datasets) { + 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` + ); + } + } + break; + case 404: + break; + default: + throw new Error(JSON.stringify(response.body)); + } + + // get all snapshot recursively from the parent dataset + let snapshots = []; + endpoint = `/pool/dataset/id/${encodeURIComponent( + this.getVolumeParentDatasetName() + )}`; + response = await httpClient.get(endpoint, { + "extra.snapshots": 1, + //"extra.snapshots_properties": JSON.stringify(zfsProperties), + }); + + switch (response.statusCode) { + case 200: + for (let child of response.body.children) { + snapshots = snapshots.concat(child.snapshots); + } + //console.log(snapshots); + for (let snapshot of snapshots) { + 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` + ); + } + } + break; + case 404: + break; + default: + throw new Error(JSON.stringify(response.body)); + } + } + let fullSnapshotName; let snapshotDatasetName; let tmpSnapshotName; @@ -3671,7 +3878,10 @@ class FreeNASApiDriver extends CsiBaseDriver { properties: snapshotProperties, }); } catch (err) { - if (err.toString().includes("dataset does not exist")) { + if ( + err.toString().includes("dataset does not exist") || + err.toString().includes("not found") + ) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `snapshot source_volume_id ${source_volume_id} does not exist` @@ -3840,7 +4050,42 @@ class FreeNASApiDriver extends CsiBaseDriver { */ async ValidateVolumeCapabilities(call) { const driver = this; - const result = this.assertCapabilities(call.request.volume_capabilities); + const httpApiClient = await this.getTrueNASHttpApiClient(); + + 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 httpApiClient.DatasetGet(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(capabilities); if (result.valid !== true) { return { message: result.message }; diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 74c9e18..c688895 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -167,7 +167,9 @@ class FreeNASSshDriver extends ControllerZfsSshBaseDriver { * @param {*} datasetName */ async createShare(call, datasetName) { + const driver = this; const driverShareType = this.getDriverShareType(); + const sshClient = this.getSshClient(); const httpClient = await this.getHttpClient(); const apiVersion = httpClient.getApiVersion(); const zb = await this.getZetabyte(); @@ -617,15 +619,18 @@ class FreeNASSshDriver extends ControllerZfsSshBaseDriver { 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 > 63) { + + if (extentDiskName.length > maxZvolNameLength) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `extent disk name cannot exceed 63 characters: ${extentDiskName}` + `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` ); } diff --git a/src/driver/index.js b/src/driver/index.js index c4f3316..51b9968 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -280,8 +280,20 @@ class CsiBaseDriver { let device; const volume_id = call.request.volume_id; + if (!volume_id) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); + } const staging_target_path = call.request.staging_target_path; + if (!staging_target_path) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `missing staging_target_path` + ); + } const capability = call.request.volume_capability; + if (!capability || Object.keys(capability).length === 0) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capability`); + } const access_type = capability.access_type || "mount"; const volume_context = call.request.volume_context; let fs_type; @@ -360,6 +372,24 @@ class CsiBaseDriver { break; case "smb": device = `//${volume_context.server}/${volume_context.share}`; + + // if not present add guest + let has_username = mount_flags.some((element) => { + element = element.trim().toLowerCase(); + return element.startsWith("username="); + }); + + // prevents driver from hanging on stdin waiting for a password to be entered at the cli + if (!has_username) { + let has_guest = mount_flags.some((element) => { + element = element.trim().toLowerCase(); + return element === "guest"; + }); + + if (!has_guest) { + mount_flags.push("guest"); + } + } break; case "iscsi": let portals = []; @@ -547,6 +577,10 @@ class CsiBaseDriver { switch (node_attach_driver) { // block specific logic case "iscsi": + if (!fs_type) { + fs_type = "ext4"; + } + if (await filesystem.isBlockDevice(device)) { // format result = await filesystem.deviceIsFormatted(device); @@ -591,6 +625,24 @@ class CsiBaseDriver { result = await mount.deviceIsMountedAtPath(device, staging_target_path); if (!result) { + if (!fs_type) { + switch (node_attach_driver) { + case "nfs": + fs_type = "nfs"; + break; + case "lustre": + fs_type = "lustre"; + break; + case "smb": + fs_type = "cifs"; + break; + case "iscsi": + fs_type = "ext4"; + break; + default: + break; + } + } await mount.mount( device, staging_target_path, @@ -697,18 +749,20 @@ class CsiBaseDriver { let access_type = "mount"; const volume_id = call.request.volume_id; + if (!volume_id) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); + } const staging_target_path = call.request.staging_target_path; - const block_path = staging_target_path + "/block_device"; - let normalized_staging_path = staging_target_path; - const umount_args = []; - const umount_force_extra_args = ["--force", "--lazy"]; - if (!staging_target_path) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `missing staging_target_path` ); } + const block_path = staging_target_path + "/block_device"; + let normalized_staging_path = staging_target_path; + const umount_args = []; + const umount_force_extra_args = ["--force", "--lazy"]; //result = await mount.pathIsMounted(block_path); //result = await mount.pathIsMounted(staging_target_path) @@ -910,9 +964,18 @@ class CsiBaseDriver { let result; const volume_id = call.request.volume_id; + if (!volume_id) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); + } const staging_target_path = call.request.staging_target_path || ""; const target_path = call.request.target_path; + if (!target_path) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing target_path`); + } const capability = call.request.volume_capability; + if (!capability || Object.keys(capability).length === 0) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capability`); + } const access_type = capability.access_type || "mount"; let mount_flags; let volume_mount_group; @@ -1044,7 +1107,13 @@ class CsiBaseDriver { let result; const volume_id = call.request.volume_id; + if (!volume_id) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); + } const target_path = call.request.target_path; + if (!target_path) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing target_path`); + } const umount_args = []; const umount_force_extra_args = ["--force", "--lazy"]; @@ -1119,6 +1188,9 @@ class CsiBaseDriver { let device_path; let access_type; const volume_id = call.request.volume_id; + if (!volume_id) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); + } const volume_path = call.request.volume_path; const block_path = volume_path + "/block_device"; @@ -1152,6 +1224,12 @@ class CsiBaseDriver { switch (access_type) { case "mount": + if (!(await mount.pathIsMounted(device_path))) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `nothing mounted at path: ${device_path}` + ); + } result = await mount.getMountDetails(device_path, [ "avail", "size", @@ -1168,6 +1246,12 @@ class CsiBaseDriver { ]; break; case "block": + if (!(await filesystem.pathExists(device_path))) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `nothing mounted at path: ${device_path}` + ); + } result = await filesystem.getBlockDevice(device_path); res.usage = [ @@ -1208,14 +1292,16 @@ class CsiBaseDriver { let is_device_mapper = false; const volume_id = call.request.volume_id; + if (!volume_id) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); + } const volume_path = call.request.volume_path; - const block_path = volume_path + "/block_device"; - const capacity_range = call.request.capacity_range; - const volume_capability = call.request.volume_capability; - if (!volume_path) { throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_path`); } + const block_path = volume_path + "/block_device"; + const capacity_range = call.request.capacity_range; + const volume_capability = call.request.volume_capability; if ( (await mount.isBindMountedBlockDevice(volume_path)) || @@ -1235,7 +1321,7 @@ class CsiBaseDriver { } catch (err) { if (err.code == 1) { throw new GrpcError( - grpc.status.FAILED_PRECONDITION, + grpc.status.NOT_FOUND, `volume_path ${volume_path} is not currently mounted` ); } diff --git a/src/utils/zfs.js b/src/utils/zfs.js index 040a70d..464cc69 100644 --- a/src/utils/zfs.js +++ b/src/utils/zfs.js @@ -1153,14 +1153,15 @@ class Zetabyte { args.push("'" + command.join(" ") + "'"); - zb.exec("/bin/sh", args, { timeout: zb.options.timeout }, function ( - error, - stdout, - stderr - ) { - if (error) return reject(zb.helpers.zfsError(error, stderr)); - return resolve(stdout); - }); + zb.exec( + "/bin/sh", + args, + { timeout: zb.options.timeout }, + function (error, stdout, stderr) { + if (error) return reject(zb.helpers.zfsError(error, stderr)); + return resolve(stdout); + } + ); }); }, @@ -1230,6 +1231,7 @@ class Zetabyte { * filesystem|volume|snapshot... * * @param {*} dataset + * @param {*} properties * @param {*} options */ list: function (dataset, properties, options = {}) {