csi-test conformance, relax iscsi name length

Signed-off-by: Travis Glenn Hansen <travisghansen@yahoo.com>
This commit is contained in:
Travis Glenn Hansen 2021-09-21 21:36:14 -06:00
parent 109d493cfc
commit 855f48d3af
14 changed files with 951 additions and 129 deletions

View File

@ -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

View File

@ -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=<id> sudo=true
account user update id=<id> sudo_nopasswd=true
# optional if you want to disable password
#account user update id=<id> password_disabled=true

View File

@ -37,7 +37,7 @@ zfs:
# total volume name (zvol/<datasetParentName>/<pvc name>) 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

View File

@ -47,7 +47,7 @@ zfs:
# total volume name (zvol/<datasetParentName>/<pvc name>) 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

64
package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -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);
// 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) {

View File

@ -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?

View File

@ -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);
@ -752,6 +831,19 @@ class ControllerSynologyDriver extends CsiBaseDriver {
);
}
// 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);
if (snapshot) {
@ -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 };
}

View File

@ -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;
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;
}
}
}
/**
@ -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) {

View File

@ -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 };

View File

@ -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}`
);
}

View File

@ -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`
);
}

View File

@ -1153,14 +1153,15 @@ class Zetabyte {
args.push("'" + command.join(" ") + "'");
zb.exec("/bin/sh", args, { timeout: zb.options.timeout }, function (
error,
stdout,
stderr
) {
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 = {}) {