Merge remote-tracking branch 'origin/dev' into next

This commit is contained in:
Travis Glenn Hansen 2021-06-24 23:40:12 -06:00
commit 49c0c9b16c
2 changed files with 498 additions and 13 deletions

View File

@ -0,0 +1,302 @@
const request = require("request");
const USER_AGENT = "democratic-csi";
class SynologyHttpClient {
constructor(options = {}) {
this.options = JSON.parse(JSON.stringify(options));
this.logger = console;
setInterval(() => {
console.log("WIPING OUT SYNOLOGY SID");
this.sid = null;
}, 60 * 1000);
}
async login() {
if (!this.sid) {
const data = {
api: "SYNO.API.Auth",
version: "2",
method: "login",
account: this.options.username,
passwd: this.options.password,
session: this.options.session,
format: "sid",
};
this.authenticating = true;
let response = await this.do_request("GET", "auth.cgi", data);
this.sid = response.body.data.sid;
this.authenticating = false;
}
}
log_response(error, response, body, options) {
this.logger.debug("SYNOLOGY HTTP REQUEST: " + JSON.stringify(options));
this.logger.debug("SYNOLOGY HTTP ERROR: " + error);
this.logger.debug("SYNOLOGY HTTP STATUS: " + response.statusCode);
this.logger.debug(
"SYNOLOGY HTTP HEADERS: " + JSON.stringify(response.headers)
);
this.logger.debug("SYNOLOGY HTTP BODY: " + JSON.stringify(body));
}
async do_request(method, path, data = {}) {
const client = this;
if (!this.authenticating) {
await this.login();
}
return new Promise((resolve, reject) => {
if (data.api != "SYNO.API.Auth") {
data._sid = this.sid;
}
const options = {
method: method,
url: `${this.options.protocol}://${this.options.host}:${this.options.port}/webapi/${path}`,
headers: {
Accept: "application/json",
"User-Agent": USER_AGENT,
"Content-Type": "application/json",
},
json: true,
agentOptions: {
rejectUnauthorized: !!!client.options.allowInsecure,
},
};
switch (method) {
case "GET":
options.qs = data;
break;
default:
options.body = data;
break;
}
request(options, function (error, response, body) {
client.log_response(...arguments, options);
if (error) {
reject(error);
}
if (response.statusCode > 299 || response.statusCode < 200) {
reject(response);
}
if (response.body.success === false) {
reject(response);
}
resolve(response);
});
});
}
async GetLunUUIDByName(name) {
const lun_list = {
api: "SYNO.Core.ISCSI.LUN",
version: "1",
method: "list",
};
let response = await this.do_request("GET", "entry.cgi", lun_list);
let lun = response.body.data.luns.find((i) => {
return i.name == name;
});
if (lun) {
return lun.uuid;
}
}
async GetTargetByTargetID(target_id) {
let targets = await this.ListTargets();
let target = targets.find((i) => {
return i.target_id == target_id;
});
return target;
}
async ListTargets() {
const iscsi_target_list = {
api: "SYNO.Core.ISCSI.Target",
version: "1",
path: "entry.cgi",
method: "list",
additional: '["mapped_lun", "status", "acls", "connected_sessions"]',
};
let response = await this.do_request("GET", "entry.cgi", iscsi_target_list);
return response.body.data.targets;
}
async CreateLun(data = {}) {
let response;
let iscsi_lun_create = Object.assign(data, {
api: "SYNO.Core.ISCSI.LUN",
version: "1",
method: "create",
});
const lun_list = {
api: "SYNO.Core.ISCSI.LUN",
version: "1",
method: "list",
};
try {
response = await this.do_request("GET", "entry.cgi", iscsi_lun_create);
return response.body.data.uuid;
} catch (err) {
if ([18990538].includes(err.body.error.code)) {
response = await this.do_request("GET", "entry.cgi", lun_list);
let lun = response.body.data.luns.find((i) => {
return i.name == iscsi_lun_create.name;
});
return lun.uuid;
} else {
throw err;
}
}
}
async MapLun(data = {}) {
// this is mapping from the perspective of the lun
let iscsi_target_map = Object.assign(data, {
api: "SYNO.Core.ISCSI.LUN",
method: "map_target",
version: "1",
});
iscsi_target_map.target_ids = JSON.stringify(iscsi_target_map.target_ids);
// this is mapping from the perspective of the target
/*
iscsi_target_map = Object.assign(data, {
api: "SYNO.Core.ISCSI.Target",
method: "map_lun",
version: "1",
});
iscsi_target_map.lun_uuids = JSON.stringify(iscsi_target_map.lun_uuids);
*/
await this.do_request("GET", "entry.cgi", iscsi_target_map);
}
async DeleteLun(uuid) {
let iscsi_lun_delete = {
api: "SYNO.Core.ISCSI.LUN",
method: "delete",
version: 1,
uuid: uuid || "",
};
try {
await this.do_request("GET", "entry.cgi", iscsi_lun_delete);
} catch (err) {
if (![18990505].includes(err.body.error.code)) {
throw err;
}
}
}
async GetTargetIDByIQN(iqn) {
const iscsi_target_list = {
api: "SYNO.Core.ISCSI.Target",
version: "1",
path: "entry.cgi",
method: "list",
additional: '["mapped_lun", "status", "acls", "connected_sessions"]',
};
let response = await this.do_request("GET", "entry.cgi", iscsi_target_list);
let target = response.body.data.targets.find((i) => {
return i.iqn == iqn;
});
if (target) {
return target.target_id;
}
}
async CreateTarget(data = {}) {
let iscsi_target_create = Object.assign(data, {
api: "SYNO.Core.ISCSI.Target",
version: "1",
method: "create",
});
let response;
try {
response = await this.do_request("GET", "entry.cgi", iscsi_target_create);
return response.body.data.target_id;
} catch (err) {
if ([18990744].includes(err.body.error.code)) {
//do lookup
const iscsi_target_list = {
api: "SYNO.Core.ISCSI.Target",
version: "1",
path: "entry.cgi",
method: "list",
additional: '["mapped_lun", "status", "acls", "connected_sessions"]',
};
response = await this.do_request("GET", "entry.cgi", iscsi_target_list);
let target = response.body.data.targets.find((i) => {
return i.iqn == iscsi_target_create.iqn;
});
let target_id = target.target_id;
return target_id;
} else {
throw err;
}
}
}
async DeleteTarget(target_id) {
const iscsi_target_delete = {
api: "SYNO.Core.ISCSI.Target",
method: "delete",
version: "1",
path: "entry.cgi",
};
try {
await this.do_request(
"GET",
"entry.cgi",
Object.assign(iscsi_target_delete, {
target_id: JSON.stringify(String(target_id || "")),
})
);
} catch (err) {
/**
* 18990710 = non-existant
*/
if (![18990710].includes(err.body.error.code)) {
throw err;
}
}
}
async ExpandISCSILun(uuid, size) {
const iscsi_lun_extend = {
api: "SYNO.Core.ISCSI.LUN",
method: "set",
version: 1,
};
await this.do_request(
"GET",
"entry.cgi",
Object.assign(iscsi_lun_extend, { uuid: uuid, new_size: size })
);
}
}
module.exports.SynologyHttpClient = SynologyHttpClient;

View File

@ -1,5 +1,6 @@
const { CsiBaseDriver } = require("../index"); const { CsiBaseDriver } = require("../index");
const { GrpcError, grpc } = require("../../utils/grpc"); const { GrpcError, grpc } = require("../../utils/grpc");
const SynologyHttpClient = require("./http").SynologyHttpClient;
/** /**
* *
@ -57,7 +58,7 @@ class ControllerSynologyDriver extends CsiBaseDriver {
//"LIST_SNAPSHOTS", //"LIST_SNAPSHOTS",
//"CLONE_VOLUME", //"CLONE_VOLUME",
//"PUBLISH_READONLY", //"PUBLISH_READONLY",
//"EXPAND_VOLUME", "EXPAND_VOLUME",
]; ];
} }
@ -73,6 +74,13 @@ class ControllerSynologyDriver extends CsiBaseDriver {
} }
} }
async getHttpClient() {
if (!this.httpClient) {
this.httpClient = new SynologyHttpClient(this.options.httpConnection);
}
return this.httpClient;
}
getDriverResourceType() { getDriverResourceType() {
switch (this.options.driver) { switch (this.options.driver) {
case "synology-nfs": case "synology-nfs":
@ -98,6 +106,19 @@ class ControllerSynologyDriver extends CsiBaseDriver {
} }
} }
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();
}
assertCapabilities(capabilities) { assertCapabilities(capabilities) {
const driverResourceType = this.getDriverResourceType(); const driverResourceType = this.getDriverResourceType();
this.ctx.logger.verbose("validating capabilities: %j", capabilities); this.ctx.logger.verbose("validating capabilities: %j", capabilities);
@ -176,6 +197,7 @@ class ControllerSynologyDriver extends CsiBaseDriver {
*/ */
async CreateVolume(call) { async CreateVolume(call) {
const driver = this; const driver = this;
const httpClient = await driver.getHttpClient();
let name = call.request.name; let name = call.request.name;
let volume_content_source = call.request.volume_content_source; let volume_content_source = call.request.volume_content_source;
@ -230,22 +252,83 @@ class ControllerSynologyDriver extends CsiBaseDriver {
); );
} }
let volume_context = {};
switch (driver.getDriverShareType()) { switch (driver.getDriverShareType()) {
case "nfs": case "nfs":
// TODO: create volume here // TODO: create volume here
throw new GrpcError(
grpc.status.UNIMPLEMENTED,
`operation not supported by driver`
);
break; break;
case "smb": case "smb":
// TODO: create volume here // TODO: create volume here
throw new GrpcError(
grpc.status.UNIMPLEMENTED,
`operation not supported by driver`
);
break; break;
case "iscsi": case "iscsi":
// TODO: create volume here let iscsiName = driver.buildIscsiName(name);
break; let data;
default: let iqn = driver.options.iscsi.baseiqn + iscsiName;
// throw an error data = Object.assign(driver.options.iscsi.targetAttributes, {
break; name: iscsiName,
iqn,
});
let target_id = await httpClient.CreateTarget(data);
data = Object.assign(driver.options.iscsi.lunAttributes, {
name: iscsiName,
location: driver.options.synology.location,
size: capacity_bytes,
});
let lun_uuid = await httpClient.CreateLun(data);
let target = await httpClient.GetTargetByTargetID(target_id);
if (!target) {
throw new GrpcError(
grpc.status.UNKNOWN,
`failed to lookup target: ${target_id}`
);
} }
let volume_context = driver.getVolumeContext(name); if (
!target.mapped_luns.some((lun) => {
return lun.lun_uuid == lun_uuid;
})
) {
data = {
uuid: lun_uuid,
target_ids: [target_id],
};
/*
data = {
lun_uuids: [lun_uuid],
target_id: target_id,
};
*/
await httpClient.MapLun(data);
}
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: 0,
};
break;
default:
throw new GrpcError(
grpc.status.UNIMPLEMENTED,
`operation not supported by driver`
);
break;
}
volume_context["provisioner_driver"] = driver.options.driver; volume_context["provisioner_driver"] = driver.options.driver;
if (driver.options.instance_id) { if (driver.options.instance_id) {
@ -256,8 +339,7 @@ class ControllerSynologyDriver extends CsiBaseDriver {
const res = { const res = {
volume: { volume: {
volume_id: name, volume_id: name,
//capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 capacity_bytes, // kubernetes currently pukes if capacity is returned as 0
capacity_bytes: 0,
content_source: volume_content_source, content_source: volume_content_source,
volume_context, volume_context,
}, },
@ -273,6 +355,7 @@ class ControllerSynologyDriver extends CsiBaseDriver {
*/ */
async DeleteVolume(call) { async DeleteVolume(call) {
const driver = this; const driver = this;
const httpClient = await driver.getHttpClient();
let name = call.request.volume_id; let name = call.request.volume_id;
@ -283,18 +366,38 @@ class ControllerSynologyDriver extends CsiBaseDriver {
); );
} }
let response;
switch (driver.getDriverShareType()) { switch (driver.getDriverShareType()) {
case "nfs": case "nfs":
// TODO: delete volume here // TODO: delete volume here
throw new GrpcError(
grpc.status.UNIMPLEMENTED,
`operation not supported by driver`
);
break; break;
case "smb": case "smb":
// TODO: delete volume here // TODO: delete volume here
throw new GrpcError(
grpc.status.UNIMPLEMENTED,
`operation not supported by driver`
);
break; break;
case "iscsi": case "iscsi":
// TODO: delete volume here let iscsiName = driver.buildIscsiName(name);
let iqn = driver.options.iscsi.baseiqn + iscsiName;
response = await httpClient.GetLunUUIDByName(iscsiName);
await httpClient.DeleteLun(response);
response = await httpClient.GetTargetIDByIQN(iqn);
await httpClient.DeleteTarget(response);
break; break;
default: default:
// throw an error throw new GrpcError(
grpc.status.UNIMPLEMENTED,
`operation not supported by driver`
);
break; break;
} }
@ -306,10 +409,90 @@ class ControllerSynologyDriver extends CsiBaseDriver {
* @param {*} call * @param {*} call
*/ */
async ControllerExpandVolume(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( throw new GrpcError(
grpc.status.UNIMPLEMENTED, grpc.status.UNIMPLEMENTED,
`operation not supported by driver` `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,
};
} }
/** /**