diff --git a/CHANGELOG.md b/CHANGELOG.md index 463b254..55bd69e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v1.1.0 + +Released 2021-02-XX + +- support for csi-v1.3.0 + # v1.0.1 Released 2021-01-29 diff --git a/bin/democratic-csi b/bin/democratic-csi index 5b301d1..e479bf2 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -81,7 +81,8 @@ const GeneralUtils = require("../src/utils/general"); if (args.logLevel) { logger.level = args.logLevel; } -const csiVersion = process.env.CSI_VERSION || "1.2.0"; + +const csiVersion = process.env.CSI_VERSION || args.csiVersion || "1.2.0"; const PROTO_PATH = __dirname + "/../csi_proto/csi-v" + csiVersion + ".proto"; // Suggested options for similarity to existing grpc.load behavior @@ -101,7 +102,7 @@ logger.info("initializing csi driver: %s", options.driver); let driver; try { driver = require("../src/driver/factory").factory( - { logger, args, cache, package }, + { logger, args, cache, package, csiVersion }, options ); } catch (err) { @@ -239,6 +240,9 @@ function getServer() { async ValidateVolumeCapabilities(call, callback) { requestHandlerProxy(call, callback, arguments.callee.name); }, + async ControllerGetVolume(call, callback) { + requestHandlerProxy(call, callback, arguments.callee.name); + }, async ListVolumes(call, callback) { requestHandlerProxy(call, callback, arguments.callee.name); }, diff --git a/package-lock.json b/package-lock.json index 3074d13..4d307fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "democratic-csi", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", "requires": { - "@babel/highlight": "^7.10.4" + "@babel/highlight": "^7.12.13" } }, "@babel/helper-validator-identifier": { @@ -18,11 +18,11 @@ "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" }, "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", + "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -567,9 +567,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz", - "integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.19.0.tgz", + "integrity": "sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg==", "requires": { "@babel/code-frame": "^7.0.0", "@eslint/eslintrc": "^0.3.0", @@ -681,9 +681,9 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "requires": { "estraverse": "^5.1.0" }, @@ -2052,9 +2052,9 @@ }, "dependencies": { "ajv": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz", - "integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.4.tgz", + "integrity": "sha512-xzzzaqgEQfmuhbhAoqjJ8T/1okb6gAzXn/eQRNpAN1AEUoHJTNF9xCDRTtf/s3SKldtZfa+RJeTs+BQq+eZ/sw==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", diff --git a/package.json b/package.json index d146f02..a7f109d 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,13 @@ "dependencies": { "@grpc/proto-loader": "^0.5.6", "bunyan": "^1.8.15", - "eslint": "^7.18.0", + "eslint": "^7.19.0", "grpc-uds": "^0.1.6", "handlebars": "^4.7.6", "js-yaml": "^4.0.0", "lru-cache": "^6.0.0", "request": "^2.88.2", + "semver": "^7.3.4", "ssh2": "^0.8.9", "uri-js": "^4.4.1", "uuid": "^8.3.2", diff --git a/src/driver/controller-zfs-ssh/index.js b/src/driver/controller-zfs-ssh/index.js index ad6ce6e..2a72f0f 100644 --- a/src/driver/controller-zfs-ssh/index.js +++ b/src/driver/controller-zfs-ssh/index.js @@ -6,6 +6,7 @@ const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); const Handlebars = require("handlebars"); const uuidv4 = require("uuid").v4; +const semver = require("semver"); // zfs common properties const MANAGED_PROPERTY_NAME = "democratic-csi:managed_resource"; @@ -81,6 +82,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { //"UNKNOWN", "CREATE_DELETE_VOLUME", //"PUBLISH_UNPUBLISH_VOLUME", + //"LIST_VOLUMES_PUBLISHED_NODES", "LIST_VOLUMES", "GET_CAPACITY", "CREATE_DELETE_SNAPSHOT", @@ -88,6 +90,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { "CLONE_VOLUME", //"PUBLISH_READONLY", "EXPAND_VOLUME", + //"VOLUME_CONDITION", // added in v1.3.0 + //"GET_VOLUME", // added in v1.3.0 ]; } @@ -100,7 +104,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { //"UNKNOWN", "STAGE_UNSTAGE_VOLUME", "GET_VOLUME_STATS", - //"EXPAND_VOLUME" + //"EXPAND_VOLUME", + //"VOLUME_CONDITION", ]; break; case "volume": @@ -109,6 +114,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { "STAGE_UNSTAGE_VOLUME", "GET_VOLUME_STATS", "EXPAND_VOLUME", + //"VOLUME_CONDITION", ]; break; } @@ -257,6 +263,108 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { return { valid, message }; } + async getVolumeStatus(volume_id) { + const driver = this; + + if (!!!semver.satisfies(driver.ctx.csiVersion, ">=1.2.0")) { + return; + } + + let abnormal = false; + let message = "OK"; + let volume_status = {}; + + //LIST_VOLUMES_PUBLISHED_NODES + if ( + semver.satisfies(driver.ctx.csiVersion, ">=1.2.0") && + driver.options.service.controller.capabilities.rpc.includes( + "LIST_VOLUMES_PUBLISHED_NODES" + ) + ) { + // TODO: let drivers fill this in + volume_status.published_node_ids = []; + } + + //VOLUME_CONDITION + if ( + semver.satisfies(driver.ctx.csiVersion, ">=1.3.0") && + driver.options.service.controller.capabilities.rpc.includes( + "VOLUME_CONDITION" + ) + ) { + // TODO: let drivers fill ths in + volume_condition = { abnormal, message }; + volume_status.volume_condition = volume_condition; + } + + return volume_status; + } + + async populateCsiVolumeFromData(row) { + const driver = this; + const zb = await this.getZetabyte(); + const driverZfsResourceType = this.getDriverZfsResourceType(); + let datasetName = this.getVolumeParentDatasetName(); + + // ignore rows were csi_name is empty + if (row[MANAGED_PROPERTY_NAME] != "true") { + return; + } + + let volume_content_source; + let volume_context = JSON.parse(row[SHARE_VOLUME_CONTEXT_PROPERTY_NAME]); + if ( + zb.helpers.isPropertyValueSet( + row[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME] + ) + ) { + volume_context["provisioner_driver"] = + row[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME]; + } + + if ( + zb.helpers.isPropertyValueSet( + row[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME] + ) + ) { + volume_context["provisioner_driver_instance_id"] = + row[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME]; + } + + if ( + zb.helpers.isPropertyValueSet( + row[VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME] + ) + ) { + volume_content_source = {}; + switch (row[VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME]) { + case "snapshot": + volume_content_source.snapshot = {}; + volume_content_source.snapshot.snapshot_id = + row[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME]; + break; + case "volume": + volume_content_source.volume = {}; + volume_content_source.volume.volume_id = + row[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME]; + break; + } + } + + let volume = { + // remove parent dataset info + volume_id: row["name"].replace(new RegExp("^" + datasetName + "/"), ""), + capacity_bytes: + driverZfsResourceType == "filesystem" + ? row["refquota"] + : row["volsize"], + content_source: volume_content_source, + volume_context, + }; + + return volume; + } + /** * Ensure sane options are used etc * true = ready @@ -1107,6 +1215,86 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { return { available_capacity: properties.available.value }; } + /** + * Get a single volume + * + * @param {*} call + */ + async ControllerGetVolume(call) { + const driver = this; + const driverZfsResourceType = this.getDriverZfsResourceType(); + const zb = await this.getZetabyte(); + + let datasetParentName = this.getVolumeParentDatasetName(); + let response; + let name = call.request.volume_id; + + if (!datasetParentName) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: missing datasetParentName` + ); + } + + if (!name) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `volume_id is required` + ); + } + + const datasetName = datasetParentName + "/" + name; + + let types = []; + switch (driverZfsResourceType) { + case "filesystem": + types = ["filesystem"]; + break; + case "volume": + types = ["volume"]; + break; + } + try { + response = await zb.zfs.list( + datasetName, + [ + "name", + "mountpoint", + "refquota", + "avail", + "used", + VOLUME_CSI_NAME_PROPERTY_NAME, + VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME, + VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME, + "volsize", + MANAGED_PROPERTY_NAME, + SHARE_VOLUME_CONTEXT_PROPERTY_NAME, + SUCCESS_PROPERTY_NAME, + VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME, + VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME, + ], + { types, recurse: false } + ); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + throw new GrpcError(grpc.status.NOT_FOUND, `volume_id is missing`); + } + + throw err; + } + + driver.ctx.logger.debug("list volumes result: %j", response); + let volume = await driver.populateCsiVolumeFromData(response.indexed[0]); + let status = await driver.getVolumeStatus(datasetName); + + let res = { volume }; + if (status) { + res.status = status; + } + + return res; + } + /** * * TODO: check capability to ensure not asking about block volumes @@ -1213,68 +1401,20 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { } entries = []; - response.indexed.forEach((row) => { + for (let row of response.indexed) { // ignore rows were csi_name is empty if (row[MANAGED_PROPERTY_NAME] != "true") { return; } - let volume_content_source; - let volume_context = JSON.parse(row[SHARE_VOLUME_CONTEXT_PROPERTY_NAME]); - if ( - zb.helpers.isPropertyValueSet( - row[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME] - ) - ) { - volume_context["provisioner_driver"] = - row[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME]; - } - - if ( - zb.helpers.isPropertyValueSet( - row[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME] - ) - ) { - volume_context["provisioner_driver_instance_id"] = - row[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME]; - } - - if ( - zb.helpers.isPropertyValueSet( - row[VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME] - ) - ) { - volume_content_source = {}; - switch (row[VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME]) { - case "snapshot": - volume_content_source.snapshot = {}; - volume_content_source.snapshot.snapshot_id = - row[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME]; - break; - case "volume": - volume_content_source.volume = {}; - volume_content_source.volume.volume_id = - row[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME]; - break; - } - } + let volume = await driver.populateCsiVolumeFromData(row); + let status = await driver.getVolumeStatus(datasetName); entries.push({ - volume: { - // remove parent dataset info - volume_id: row["name"].replace( - new RegExp("^" + datasetName + "/"), - "" - ), - capacity_bytes: - driverZfsResourceType == "filesystem" - ? row["refquota"] - : row["volsize"], - content_source: volume_content_source, - volume_context, - }, + volume, + status, }); - }); + } if (max_entries && entries.length > max_entries) { uuid = uuidv4(); diff --git a/src/driver/index.js b/src/driver/index.js index 763aea7..869268a 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -4,6 +4,7 @@ const { GrpcError, grpc } = require("../utils/grpc"); const { Mount } = require("../utils/mount"); const { Filesystem } = require("../utils/filesystem"); const { ISCSI } = require("../utils/iscsi"); +const semver = require("semver"); const sleep = require("../utils/general").sleep; /** @@ -867,6 +868,7 @@ class CsiBaseDriver { } async NodeGetVolumeStats(call) { + const driver = this; const mount = new Mount(); const filesystem = new Filesystem(); let result; @@ -880,6 +882,19 @@ class CsiBaseDriver { throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_path`); } + let res = {}; + + //VOLUME_CONDITION + if ( + semver.satisfies(driver.ctx.csiVersion, ">=1.3.0") && + options.service.node.capabilities.rpc.includes("VOLUME_CONDITION") + ) { + // TODO: let drivers fill ths in + let abnormal = false; + let message = "OK"; + res.volume_condition = { abnormal, message }; + } + if ( (await mount.isBindMountedBlockDevice(volume_path)) || (await mount.isBindMountedBlockDevice(block_path)) @@ -895,33 +910,33 @@ class CsiBaseDriver { case "mount": result = await mount.getMountDetails(device_path); - return { - usage: [ - { - available: result.avail, - total: result.size, - used: result.used, - unit: "BYTES", - }, - ], - }; + res.usage = [ + { + available: result.avail, + total: result.size, + used: result.used, + unit: "BYTES", + }, + ]; + break; case "block": result = await filesystem.getBlockDevice(device_path); - return { - usage: [ - { - total: result.size, - unit: "BYTES", - }, - ], - }; + res.usage = [ + { + total: result.size, + unit: "BYTES", + }, + ]; + break; default: throw new GrpcError( grpc.status.INVALID_ARGUMENT, `unsupported/unknown access_type ${access_type}` ); } + + return res; } /**