const _ = require("lodash"); const { CsiBaseDriver } = require("../index"); const GeneralUtils = require("../../utils/general"); const { GrpcError, grpc } = require("../../utils/grpc"); const Handlebars = require("handlebars"); const registry = require("../../utils/registry"); const SynologyHttpClient = require("./http").SynologyHttpClient; const semver = require("semver"); const yaml = require("js-yaml"); const __REGISTRY_NS__ = "ControllerSynologyDriver"; /** * * Driver to provision storage on a synology device * */ class ControllerSynologyDriver extends CsiBaseDriver { constructor(ctx, options) { super(...arguments); options = options || {}; options.service = options.service || {}; options.service.identity = options.service.identity || {}; options.service.controller = options.service.controller || {}; options.service.node = options.service.node || {}; options.service.identity.capabilities = options.service.identity.capabilities || {}; options.service.controller.capabilities = options.service.controller.capabilities || {}; options.service.node.capabilities = options.service.node.capabilities || {}; const driverResourceType = this.getDriverResourceType(); if (!("service" in options.service.identity.capabilities)) { this.ctx.logger.debug("setting default identity service caps"); options.service.identity.capabilities.service = [ //"UNKNOWN", "CONTROLLER_SERVICE", //"VOLUME_ACCESSIBILITY_CONSTRAINTS" ]; } if (!("volume_expansion" in options.service.identity.capabilities)) { this.ctx.logger.debug("setting default identity volume_expansion caps"); options.service.identity.capabilities.volume_expansion = [ //"UNKNOWN", "ONLINE", //"OFFLINE" ]; } if (!("rpc" in options.service.controller.capabilities)) { this.ctx.logger.debug("setting default controller caps"); options.service.controller.capabilities.rpc = [ //"UNKNOWN", "CREATE_DELETE_VOLUME", //"PUBLISH_UNPUBLISH_VOLUME", //"LIST_VOLUMES", "GET_CAPACITY", "CREATE_DELETE_SNAPSHOT", //"LIST_SNAPSHOTS", "CLONE_VOLUME", //"PUBLISH_READONLY", "EXPAND_VOLUME", ]; if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { options.service.controller.capabilities.rpc .push //"VOLUME_CONDITION", //"GET_VOLUME" (would need to properly handle volume_content_source) (); } if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { options.service.controller.capabilities.rpc.push( "SINGLE_NODE_MULTI_WRITER" ); } } if (!("rpc" in options.service.node.capabilities)) { this.ctx.logger.debug("setting default node caps"); options.service.node.capabilities.rpc = [ //"UNKNOWN", "STAGE_UNSTAGE_VOLUME", "GET_VOLUME_STATS", //"EXPAND_VOLUME", ]; if (driverResourceType == "volume") { options.service.node.capabilities.rpc.push("EXPAND_VOLUME"); } if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { //options.service.node.capabilities.rpc.push("VOLUME_CONDITION"); } if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER"); /** * This is for volumes that support a mount time gid such as smb or fat */ //options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP"); } } } async getHttpClient() { return registry.get(`${__REGISTRY_NS__}:http_client`, () => { return new SynologyHttpClient(this.options.httpConnection); }); } getDriverResourceType() { switch (this.options.driver) { case "synology-nfs": case "synology-smb": return "filesystem"; case "synology-iscsi": return "volume"; default: throw new Error("unknown driver: " + this.ctx.args.driver); } } getDriverShareType() { switch (this.options.driver) { case "synology-nfs": return "nfs"; case "synology-smb": return "smb"; case "synology-iscsi": return "iscsi"; default: throw new Error("unknown driver: " + this.ctx.args.driver); } } getObjectFromDevAttribs(list = []) { if (!list) { return {}; } return list.reduce( (obj, item) => Object.assign(obj, { [item.dev_attrib]: item.enable }), {} ); } getDevAttribsFromObject(obj, keepNull = false) { return Object.entries(obj) .filter((e) => keepNull || e[1] != null) .map((e) => ({ dev_attrib: e[0], enable: e[1] })); } parseParameterYamlData(data, fieldHint = "") { try { return yaml.load(data); } catch { if (err instanceof yaml.YAMLException) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `${fieldHint} not a valid YAML document.`.trim() ); } else { throw err; } } } buildIscsiName(name) { let iscsiName = name; if (this.options.iscsi.namePrefix) { iscsiName = this.options.iscsi.namePrefix + iscsiName; } if (this.options.iscsi.nameSuffix) { iscsiName += this.options.iscsi.nameSuffix; } return iscsiName.toLowerCase(); } /** * Returns the value for the 'location' parameter indicating on which volume * a LUN is to be created. * * @param {Object} parameters - Parameters received from a StorageClass * @param {String} parameters.volume - The volume specified by the StorageClass * @returns {String} The location of the volume. */ getLocation() { let location = _.get(this.options, "synology.volume"); if (!location) { location = "volume1"; } if (!location.startsWith("/")) { location = "/" + location; } return location; } getAccessModes(capability) { let access_modes = _.get(this.options, "csi.access_modes", null); if (access_modes !== null) { return access_modes; } const driverResourceType = this.getDriverResourceType(); switch (driverResourceType) { case "filesystem": access_modes = [ "UNKNOWN", "SINGLE_NODE_WRITER", "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 "SINGLE_NODE_READER_ONLY", "MULTI_NODE_READER_ONLY", "MULTI_NODE_SINGLE_WRITER", "MULTI_NODE_MULTI_WRITER", ]; case "volume": access_modes = [ "UNKNOWN", "SINGLE_NODE_WRITER", "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 "SINGLE_NODE_READER_ONLY", "MULTI_NODE_READER_ONLY", "MULTI_NODE_SINGLE_WRITER", ]; } if ( capability.access_type == "block" && !access_modes.includes("MULTI_NODE_MULTI_WRITER") ) { access_modes.push("MULTI_NODE_MULTI_WRITER"); } return access_modes; } assertCapabilities(capabilities) { const driverResourceType = this.getDriverResourceType(); this.ctx.logger.verbose("validating capabilities: %j", capabilities); let message = null; //[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}] const valid = capabilities.every((capability) => { switch (driverResourceType) { case "filesystem": if (capability.access_type != "mount") { message = `invalid access_type ${capability.access_type}`; return false; } if ( capability.mount.fs_type && !GeneralUtils.default_supported_file_filesystems().includes( capability.mount.fs_type ) ) { message = `invalid fs_type ${capability.mount.fs_type}`; return false; } if ( !this.getAccessModes(capability).includes( capability.access_mode.mode ) ) { message = `invalid access_mode, ${capability.access_mode.mode}`; return false; } return true; case "volume": if (capability.access_type == "mount") { if ( capability.mount.fs_type && !GeneralUtils.default_supported_block_filesystems().includes( capability.mount.fs_type ) ) { message = `invalid fs_type ${capability.mount.fs_type}`; return false; } } if ( !this.getAccessModes(capability).includes( capability.access_mode.mode ) ) { message = `invalid access_mode, ${capability.access_mode.mode}`; return false; } return true; } }); return { valid, message }; } /** * * CreateVolume * * @param {*} call */ async CreateVolume(call) { const driver = this; const httpClient = await driver.getHttpClient(); let name = call.request.name; let volume_content_source = call.request.volume_content_source; if (!name) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume name is required` ); } if ( call.request.volume_capabilities && call.request.volume_capabilities.length > 0 ) { const result = this.assertCapabilities(call.request.volume_capabilities); if (result.valid !== true) { throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); } } else { throw new GrpcError( grpc.status.INVALID_ARGUMENT, "missing volume_capabilities" ); } if ( !call.request.capacity_range || Object.keys(call.request.capacity_range).length === 0 ) { call.request.capacity_range = { required_bytes: 1073741824, }; } if ( call.request.capacity_range.required_bytes > 0 && call.request.capacity_range.limit_bytes > 0 && call.request.capacity_range.required_bytes > call.request.capacity_range.limit_bytes ) { throw new GrpcError( grpc.status.OUT_OF_RANGE, `required_bytes is greather than limit_bytes` ); } let capacity_bytes = call.request.capacity_range.required_bytes || call.request.capacity_range.limit_bytes; if (!capacity_bytes) { //should never happen, value must be set throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume capacity is required (either required_bytes or limit_bytes)` ); } // ensure *actual* capacity is not greater than limit if ( call.request.capacity_range.limit_bytes && call.request.capacity_range.limit_bytes > 0 && capacity_bytes > call.request.capacity_range.limit_bytes ) { throw new GrpcError( grpc.status.OUT_OF_RANGE, `required volume capacity is greater than limit` ); } let volume_context = {}; const normalizedParameters = driver.getNormalizedParameters( call.request.parameters ); switch (driver.getDriverShareType()) { case "nfs": // TODO: create volume here throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; case "smb": // TODO: create volume here throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; case "iscsi": let iscsiName = driver.buildIscsiName(name); let lunTemplate; let targetTemplate; let data; let target; let lun_mapping; let lun_uuid; let existingLun; lunTemplate = Object.assign( {}, _.get(driver.options, "iscsi.lunTemplate", {}), driver.parseParameterYamlData( _.get(normalizedParameters, "lunTemplate", "{}"), "parameters.lunTemplate" ), driver.parseParameterYamlData( _.get(call.request, "secrets.lunTemplate", "{}"), "secrets.lunTemplate" ) ); targetTemplate = Object.assign( {}, _.get(driver.options, "iscsi.targetTemplate", {}), driver.parseParameterYamlData( _.get(normalizedParameters, "targetTemplate", "{}"), "parameters.targetTemplate" ), driver.parseParameterYamlData( _.get(call.request, "secrets.targetTemplate", "{}"), "secrets.targetTemplate" ) ); // render the template for description if (lunTemplate.description) { lunTemplate.description = Handlebars.compile(lunTemplate.description)( { name: call.request.name, parameters: call.request.parameters, csi: { name: this.ctx.args.csiName, version: this.ctx.args.csiVersion, }, } ); } // ensure volumes with the same name being requested a 2nd time but with a different size fails try { let lun = await httpClient.GetLunByName(iscsiName); if (lun) { let size = lun.size; let check = true; if (check) { if ( (call.request.capacity_range.required_bytes && call.request.capacity_range.required_bytes > 0 && size < call.request.capacity_range.required_bytes) || (call.request.capacity_range.limit_bytes && call.request.capacity_range.limit_bytes > 0 && size > call.request.capacity_range.limit_bytes) ) { throw new GrpcError( grpc.status.ALREADY_EXISTS, `volume has already been created with a different size, existing size: ${size}, required_bytes: ${call.request.capacity_range.required_bytes}, limit_bytes: ${call.request.capacity_range.limit_bytes}` ); } } } } catch (err) { throw err; } if (volume_content_source) { let src_lun_uuid; switch (volume_content_source.type) { case "snapshot": let parts = volume_content_source.snapshot.snapshot_id.split("/"); src_lun_uuid = parts[2]; if (!src_lun_uuid) { throw new GrpcError( grpc.status.NOT_FOUND, `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` ); } let snapshot_uuid = parts[3]; if (!snapshot_uuid) { throw new GrpcError( grpc.status.NOT_FOUND, `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` ); } // This is for backwards compatibility. Previous versions of this driver used the LUN ID instead of the // UUID. If this is the case we need to get the LUN UUID before we can proceed. if (!src_lun_uuid.includes("-")) { src_lun_uuid = await httpClient.GetLunByID(src_lun_uuid).uuid; } let snapshot = await httpClient.GetSnapshotByLunUUIDAndSnapshotUUID( src_lun_uuid, snapshot_uuid ); if (!snapshot) { throw new GrpcError( grpc.status.NOT_FOUND, `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` ); } existingLun = await httpClient.GetLunByName(iscsiName); if (!existingLun) { await httpClient.CreateVolumeFromSnapshot( src_lun_uuid, snapshot_uuid, iscsiName, lunTemplate.description ); } break; case "volume": existingLun = await httpClient.GetLunByName(iscsiName); if (!existingLun) { let srcLunName = driver.buildIscsiName( volume_content_source.volume.volume_id ); if (!srcLunName) { throw new GrpcError( grpc.status.NOT_FOUND, `invalid volume_id: ${volume_content_source.volume.volume_id}` ); } src_lun_uuid = await httpClient.GetLunUUIDByName(srcLunName); if (!src_lun_uuid) { throw new GrpcError( grpc.status.NOT_FOUND, `invalid volume_id: ${volume_content_source.volume.volume_id}` ); } await httpClient.CreateClonedVolume( src_lun_uuid, iscsiName, driver.getLocation(), lunTemplate.description ); } break; default: throw new GrpcError( grpc.status.INVALID_ARGUMENT, `invalid volume_content_source type: ${volume_content_source.type}` ); break; } // resize to requested amount let lun = await httpClient.GetLunByName(iscsiName); lun_uuid = lun.uuid; if (lun.size < capacity_bytes) { await httpClient.ExpandISCSILun(lun_uuid, capacity_bytes); } } else { // create lun data = Object.assign({}, lunTemplate, { name: iscsiName, location: driver.getLocation(), size: capacity_bytes, }); lun_uuid = await httpClient.CreateLun(data); } // create target let iqn = driver.options.iscsi.baseiqn + iscsiName; data = Object.assign({}, targetTemplate, { name: iscsiName, iqn, }); let target_id = await httpClient.CreateTarget(data); //target = await httpClient.GetTargetByTargetID(target_id); target = await httpClient.GetTargetByIQN(iqn); if (!target) { throw new GrpcError( grpc.status.UNKNOWN, `failed to lookup target: ${iqn}` ); } target_id = target.target_id; // check if mapping of lun <-> target already exists lun_mapping = target.mapped_luns.find((lun) => { return lun.lun_uuid == lun_uuid; }); // create mapping if not present already if (!lun_mapping) { data = { uuid: lun_uuid, target_ids: [target_id], }; /* data = { lun_uuids: [lun_uuid], target_id: target_id, }; */ await httpClient.MapLun(data); // re-retrieve target to ensure proper lun (mapping_index) value is returned target = await httpClient.GetTargetByTargetID(target_id); lun_mapping = target.mapped_luns.find((lun) => { return lun.lun_uuid == lun_uuid; }); } if (!lun_mapping) { throw new GrpcError( grpc.status.UNKNOWN, `failed to lookup lun_mapping_id` ); } volume_context = { node_attach_driver: "iscsi", portal: driver.options.iscsi.targetPortal || "", portals: driver.options.iscsi.targetPortals ? driver.options.iscsi.targetPortals.join(",") : "", interface: driver.options.iscsi.interface || "", iqn, lun: lun_mapping.mapping_index, }; break; default: throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; } volume_context["provisioner_driver"] = driver.options.driver; if (driver.options.instance_id) { volume_context["provisioner_driver_instance_id"] = driver.options.instance_id; } const res = { volume: { volume_id: name, capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 content_source: volume_content_source, volume_context, }, }; return res; } /** * DeleteVolume * * @param {*} call */ async DeleteVolume(call) { const driver = this; const httpClient = await driver.getHttpClient(); let name = call.request.volume_id; if (!name) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume_id is required` ); } let response; switch (driver.getDriverShareType()) { case "nfs": // TODO: delete volume here throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; case "smb": // TODO: delete volume here throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; case "iscsi": //await httpClient.DeleteAllLuns(); let iscsiName = driver.buildIscsiName(name); let iqn = driver.options.iscsi.baseiqn + iscsiName; let target = await httpClient.GetTargetByIQN(iqn); if (target) { await httpClient.DeleteTarget(target.target_id); } let lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); if (lun_uuid) { // this is an async process where a success is returned but delete is happening still behind the scenes // therefore we continue to search for the lun after delete success call to ensure full deletion await httpClient.DeleteLun(lun_uuid); //let settleEnabled = driver.options.api.lunDelete.settleEnabled; let settleEnabled = true; if (settleEnabled) { let currentCheck = 0; /* let settleMaxRetries = driver.options.api.lunDelete.settleMaxRetries || 6; let settleSeconds = driver.options.api.lunDelete.settleSeconds || 5; */ let settleMaxRetries = 6; let settleSeconds = 5; let waitTimeBetweenChecks = settleSeconds * 1000; await GeneralUtils.sleep(waitTimeBetweenChecks); lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); while (currentCheck <= settleMaxRetries && lun_uuid) { currentCheck++; await GeneralUtils.sleep(waitTimeBetweenChecks); lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); } if (lun_uuid) { throw new GrpcError( grpc.status.UNKNOWN, `failed to remove lun: ${lun_uuid}` ); } } } break; default: throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; } return {}; } /** * * @param {*} call */ async ControllerExpandVolume(call) { const driver = this; const httpClient = await driver.getHttpClient(); let name = call.request.volume_id; if (!name) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume_id is required` ); } let capacity_bytes = call.request.capacity_range.required_bytes || call.request.capacity_range.limit_bytes; if (!capacity_bytes) { //should never happen, value must be set throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume capacity is required (either required_bytes or limit_bytes)` ); } if ( call.request.capacity_range.required_bytes > 0 && call.request.capacity_range.limit_bytes > 0 && call.request.capacity_range.required_bytes > call.request.capacity_range.limit_bytes ) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `required_bytes is greather than limit_bytes` ); } // ensure *actual* capacity is not greater than limit if ( call.request.capacity_range.limit_bytes && call.request.capacity_range.limit_bytes > 0 && capacity_bytes > call.request.capacity_range.limit_bytes ) { throw new GrpcError( grpc.status.OUT_OF_RANGE, `required volume capacity is greater than limit` ); } let node_expansion_required = false; let response; switch (driver.getDriverShareType()) { case "nfs": // TODO: expand volume here throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; case "smb": // TODO: expand volume here throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; case "iscsi": node_expansion_required = true; let iscsiName = driver.buildIscsiName(name); response = await httpClient.GetLunUUIDByName(iscsiName); await httpClient.ExpandISCSILun(response, capacity_bytes); break; default: throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; } return { capacity_bytes, node_expansion_required, }; } /** * TODO: consider volume_capabilities? * * @param {*} call */ async GetCapacity(call) { const driver = this; const httpClient = await driver.getHttpClient(); const location = driver.getLocation(); if (!location) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing volume` ); } if (call.request.volume_capabilities) { const result = this.assertCapabilities(call.request.volume_capabilities); if (result.valid !== true) { return { available_capacity: 0 }; } } let response = await httpClient.GetVolumeInfo(location); return { available_capacity: response.body.data.volume.size_free_byte }; } /** * * TODO: check capability to ensure not asking about block volumes * * @param {*} call */ async ListVolumes(call) { throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); } /** * * @param {*} call */ async ListSnapshots(call) { throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); } /** * * @param {*} call */ async CreateSnapshot(call) { const driver = this; const httpClient = await driver.getHttpClient(); // both these are required let source_volume_id = call.request.source_volume_id; let name = call.request.name; if (!source_volume_id) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `snapshot source_volume_id is required` ); } if (!name) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `snapshot name is required` ); } driver.ctx.logger.verbose("requested snapshot name: %s", name); let invalid_chars; invalid_chars = name.match(/[^a-z0-9_\-:.+]+/gi); if (invalid_chars) { invalid_chars = String.prototype.concat( ...new Set(invalid_chars.join("")) ); throw new GrpcError( grpc.status.INVALID_ARGUMENT, `snapshot name contains invalid characters: ${invalid_chars}` ); } let iscsiName = driver.buildIscsiName(source_volume_id); let lun = await httpClient.GetLunByName(iscsiName); if (!lun) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `invalid source_volume_id: ${source_volume_id}` ); } const normalizedParameters = driver.getNormalizedParameters( call.request.parameters ); let lunSnapshotTemplate; lunSnapshotTemplate = Object.assign( {}, _.get(driver.options, "iscsi.lunSnapshotTemplate", {}), driver.parseParameterYamlData( _.get(normalizedParameters, "lunSnapshotTemplate", "{}"), "parameters.lunSnapshotTemplate" ), driver.parseParameterYamlData( _.get(call.request, "secrets.lunSnapshotTemplate", "{}"), "secrets.lunSnapshotTemplate" ) ); // check for other snapshopts with the same name on other volumes and fail as appropriate // TODO: technically this should only be checking lun/snapshots relevant to this specific install of the driver // but alas an isolation/namespacing mechanism does not exist in synology let snapshots = await httpClient.GetSnapshots(); for (let snapshot of snapshots) { if (snapshot.description == name && snapshot.parent_uuid != lun.uuid) { throw new GrpcError( grpc.status.ALREADY_EXISTS, `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` ); } } // check for already exists let snapshot; snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); if (!snapshot) { let data = Object.assign({}, lunSnapshotTemplate, { src_lun_uuid: lun.uuid, taken_by: "democratic-csi", description: name, //check }); await httpClient.CreateSnapshot(data); snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); if (!snapshot) { throw new Error(`failed to create snapshot`); } } return { snapshot: { /** * The purpose of this field is to give CO guidance on how much space * is needed to create a volume from this snapshot. */ size_bytes: snapshot.total_size, snapshot_id: `/lun/${lun.uuid}/${snapshot.uuid}`, source_volume_id: source_volume_id, //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto creation_time: { seconds: snapshot.time, nanos: 0, }, ready_to_use: true, }, }; } /** * In addition, if clones have been created from a snapshot, then they must * be destroyed before the snapshot can be destroyed. * * @param {*} call */ async DeleteSnapshot(call) { // throw new GrpcError( // grpc.status.UNIMPLEMENTED, // `operation not supported by driver` // ); const driver = this; const httpClient = await driver.getHttpClient(); const snapshot_id = call.request.snapshot_id; if (!snapshot_id) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `snapshot_id is required` ); } let parts = snapshot_id.split("/"); let lun_uuid = parts[2]; if (!lun_uuid) { return {}; } let snapshot_uuid = parts[3]; if (!snapshot_uuid) { return {}; } // This is for backwards compatibility. Previous versions of this driver used the LUN ID instead of the UUID. If // this is the case we need to get the LUN UUID before we can proceed. if (!lun_uuid.includes("-")) { lun_uuid = await httpClient.GetLunByID(lun_uuid).uuid; } let snapshot = await httpClient.GetSnapshotByLunUUIDAndSnapshotUUID( lun_uuid, snapshot_uuid ); if (snapshot) { await httpClient.DeleteSnapshot(snapshot.uuid); } return {}; } /** * * @param {*} call */ async ValidateVolumeCapabilities(call) { const driver = this; const httpClient = await driver.getHttpClient(); let response; const volume_id = call.request.volume_id; if (!volume_id) { throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); } const capabilities = call.request.volume_capabilities; if (!capabilities || capabilities.length === 0) { throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); } switch (driver.getDriverShareType()) { case "nfs": // TODO: expand volume here throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; case "smb": // TODO: expand volume here throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; case "iscsi": let iscsiName = driver.buildIscsiName(volume_id); response = await httpClient.GetLunUUIDByName(iscsiName); if (!response) { throw new GrpcError( grpc.status.NOT_FOUND, `invalid volume_id: ${volume_id}` ); } break; default: throw new GrpcError( grpc.status.UNIMPLEMENTED, `operation not supported by driver` ); break; } const result = this.assertCapabilities(call.request.volume_capabilities); if (result.valid !== true) { return { message: result.message }; } return { confirmed: { volume_context: call.request.volume_context, volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested parameters: call.request.parameters, }, }; } } module.exports.ControllerSynologyDriver = ControllerSynologyDriver;