1164 lines
33 KiB
JavaScript
1164 lines
33 KiB
JavaScript
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;
|