democratic-csi/src/driver/controller-synology/http/index.js

640 lines
17 KiB
JavaScript

const _ = require("lodash");
const http = require("http");
const https = require("https");
const { axios_request, stringify } = require("../../../utils/general");
const Mutex = require("async-mutex").Mutex;
const registry = require("../../../utils/registry");
const { GrpcError, grpc } = require("../../../utils/grpc");
const USER_AGENT = "democratic-csi";
const __REGISTRY_NS__ = "SynologyHttpClient";
SYNO_ERRORS = {
18990002: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The synology volume is out of disk space." },
18990538: { status: grpc.status.ALREADY_EXISTS, message: "A LUN with this name already exists." },
18990541: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The maximum number of LUNS has been reached." },
18990542: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The maximum number if iSCSI target has been reached." },
18990744: { status: grpc.status.ALREADY_EXISTS, message: "An iSCSI target with this name already exists." },
18990532: { status: grpc.status.NOT_FOUND, message: "No such snapshot." },
18990500: { status: grpc.status.INVALID_ARGUMENT, message: "Bad LUN type" },
18990543: { status: grpc.status.RESOURCE_EXHAUSTED, message: "Maximum number of snapshots reached." },
18990635: { status: grpc.status.INVALID_ARGUMENT, message: "Invalid ioPolicy." }
}
class SynologyError extends GrpcError {
constructor(code, httpCode = undefined) {
super(0, "");
this.synoCode = code;
this.httpCode = httpCode;
if (code > 0) {
const error = SYNO_ERRORS[code]
this.code = error?.status ?? grpc.status.UNKNOWN;
this.message = error?.message ?? `An unknown error occurred when executing a synology command (code = ${code}).`;
} else {
this.code = grpc.status.UNKNOWN;
this.message = `The synology webserver returned a status code ${httpCode}`;
}
}
}
class SynologyHttpClient {
constructor(options = {}) {
this.options = JSON.parse(JSON.stringify(options));
this.logger = console;
this.doLoginMutex = new Mutex();
this.apiSerializeMutex = new Mutex();
if (false) {
setInterval(() => {
console.log("WIPING OUT SYNOLOGY SID");
this.sid = null;
}, 5 * 1000);
}
}
getHttpAgent() {
return registry.get(`${__REGISTRY_NS__}:http_agent`, () => {
return new http.Agent({
keepAlive: true,
maxSockets: Infinity,
rejectUnauthorized: !!!this.options.allowInsecure,
});
});
}
getHttpsAgent() {
return registry.get(`${__REGISTRY_NS__}:https_agent`, () => {
return new https.Agent({
keepAlive: true,
maxSockets: Infinity,
rejectUnauthorized: !!!this.options.allowInsecure,
});
});
}
log_response(error, response, body, options) {
let prop;
let val;
prop = "auth.username";
val = _.get(options, prop, false);
if (val) {
_.set(options, prop, "redacted");
}
prop = "auth.password";
val = _.get(options, prop, false);
if (val) {
_.set(options, prop, "redacted");
}
prop = "headers.Authorization";
val = _.get(options, prop, false);
if (val) {
_.set(options, prop, "redacted");
}
this.logger.debug("SYNOLOGY HTTP REQUEST: " + stringify(options));
this.logger.debug("SYNOLOGY HTTP ERROR: " + error);
this.logger.debug("SYNOLOGY HTTP STATUS: " + response.statusCode);
this.logger.debug("SYNOLOGY HTTP HEADERS: " + stringify(response.headers));
this.logger.debug("SYNOLOGY HTTP BODY: " + stringify(body));
}
async do_request(method, path, data = {}, options = {}) {
const client = this;
const isAuth = data.api == "SYNO.API.Auth" && data.method == "login";
let sid;
let apiMutexRelease;
if (!isAuth) {
sid = await this.doLoginMutex.runExclusive(async () => {
return await this.login();
});
}
const invoke_options = options;
if (!isAuth) {
if (this.options.serialize) {
apiMutexRelease = await this.apiSerializeMutex.acquire();
}
}
return new Promise((resolve, reject) => {
if (!isAuth) {
data._sid = 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": invoke_options.use_form_encoded
? "application/x-www-form-urlencoded"
: "application/json",
},
responseType: "json",
httpAgent: this.getHttpAgent(),
httpsAgent: this.getHttpsAgent(),
timeout: 60 * 1000,
};
switch (method) {
case "GET":
let qsData = JSON.parse(JSON.stringify(data));
for (let p in qsData) {
if (Array.isArray(qsData[p]) || typeof qsData[p] == "boolean") {
qsData[p] = JSON.stringify(qsData[p]);
}
}
options.params = qsData;
break;
default:
if (invoke_options.use_form_encoded) {
options.data = URLSearchParams(data).toString();
} else {
options.data = data;
}
break;
}
try {
axios_request(options, function (error, response, body) {
client.log_response(...arguments, options);
if (error) {
reject(error);
}
if (
typeof response.body !== "object" &&
response.body !== null &&
response.headers["content-type"] &&
response.headers["content-type"].includes("application/json")
) {
response.body = JSON.parse(response.body);
}
if (response.statusCode > 299 || response.statusCode < 200) {
reject(new SynologyError(-1, response.statusCode))
}
if (response.body.success === false) {
// remove invalid sid
if (response.body.error.code == 119 && sid == client.sid) {
client.sid = null;
}
reject(new SynologyError(response.body.error.code, response.statusCode));
}
resolve(response);
});
} finally {
if (typeof apiMutexRelease == "function") {
apiMutexRelease();
}
}
});
}
async login() {
if (!this.sid) {
// See https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf
const data = {
api: "SYNO.API.Auth",
version: "6",
method: "login",
account: this.options.username,
passwd: this.options.password,
session: this.options.session,
format: "sid",
};
let response = await this.do_request("GET", "auth.cgi", data);
this.sid = response.body.data.sid;
}
return this.sid;
}
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",
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 GetLunIDByName(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.lun_id;
}
}
async GetLunByID(lun_id) {
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.lun_id == lun_id;
});
if (lun) {
return lun;
}
}
async GetLunByName(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;
}
}
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 GetSnapshotByLunUUIDAndName(lun_uuid, name) {
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);
if (response.body.data.snapshots) {
let snapshot = response.body.data.snapshots.find((i) => {
return i.description == name;
});
if (snapshot) {
return snapshot;
}
}
}
async GetSnapshotByLunUUIDAndSnapshotUUID(lun_uuid, snapshot_uuid) {
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);
if (response.body.data.snapshots) {
let snapshot = response.body.data.snapshots.find((i) => {
return i.uuid == snapshot_uuid;
});
if (snapshot) {
return snapshot;
}
}
}
async DeleteSnapshot(snapshot_uuid) {
const iscsi_snapshot_delete = {
api: "SYNO.Core.ISCSI.LUN",
method: "delete_snapshot",
version: 1,
snapshot_uuid: JSON.stringify(snapshot_uuid), // snapshot_id
deleted_by: "democratic_csi", // ?
};
let response = await this.do_request(
"GET",
"entry.cgi",
iscsi_snapshot_delete
);
// return?
}
async GetVolumeInfo(volume_path) {
let data = {
api: "SYNO.Core.Storage.Volume",
method: "get",
version: "1",
//volume_path: "/volume1",
volume_path,
};
return await this.do_request("GET", "entry.cgi", data);
}
async GetTargetByTargetID(target_id) {
let targets = await this.ListTargets();
let target = targets.find((i) => {
return i.target_id == target_id;
});
return target;
}
async GetTargetByIQN(iqn) {
let targets = await this.ListTargets();
let target = targets.find((i) => {
return i.iqn == iqn;
});
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 (err.synoCode === 18990538) {
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.uuid = JSON.stringify(iscsi_target_map.uuid);
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) {
uuid = uuid || "";
let iscsi_lun_delete = {
api: "SYNO.Core.ISCSI.LUN",
method: "delete",
version: 1,
//uuid: uuid,
uuid: JSON.stringify(""),
uuids: JSON.stringify([uuid]),
//is_soft_feas_ignored: false,
is_soft_feas_ignored: true,
//feasibility_precheck: true,
};
await this.do_request("GET", "entry.cgi", iscsi_lun_delete);
}
async DeleteAllLuns() {
const lun_list = {
api: "SYNO.Core.ISCSI.LUN",
version: "1",
method: "list",
};
let response = await this.do_request("GET", "entry.cgi", lun_list);
for (let lun of response.body.data.luns) {
await this.DeleteLun(lun.uuid);
}
}
async CreateSnapshot(data) {
data = Object.assign({}, data, {
api: "SYNO.Core.ISCSI.LUN",
method: "take_snapshot",
version: 1,
});
data.src_lun_uuid = JSON.stringify(data.src_lun_uuid);
return await this.do_request("GET", "entry.cgi", data);
}
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 (err.synoCode === 18990744) {
//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;
});
if (target) {
return target.target_id;
} else {
throw err;
}
} 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 (err.synoCode !== 18990710) {
throw err;
//}
}
}
async ExpandISCSILun(uuid, size) {
const iscsi_lun_extend = {
api: "SYNO.Core.ISCSI.LUN",
method: "set",
version: 1,
};
return await this.do_request(
"GET",
"entry.cgi",
Object.assign({}, iscsi_lun_extend, {
uuid: JSON.stringify(uuid),
new_size: size,
})
);
}
async CreateClonedVolume(src_lun_uuid, dst_lun_name, dst_location, description) {
const create_cloned_volume = {
api: "SYNO.Core.ISCSI.LUN",
version: 1,
method: "clone",
src_lun_uuid: JSON.stringify(src_lun_uuid), // src lun uuid
dst_lun_name: dst_lun_name, // dst lun name
dst_location: dst_location,
is_same_pool: true, // always true? string?
clone_type: "democratic-csi", // check
};
if (description) {
create_cloned_volume.description = description;
}
return await this.do_request("GET", "entry.cgi", create_cloned_volume);
}
async CreateVolumeFromSnapshot(src_lun_uuid, snapshot_uuid, cloned_lun_name) {
const create_volume_from_snapshot = {
api: "SYNO.Core.ISCSI.LUN",
version: 1,
method: "clone_snapshot",
src_lun_uuid: JSON.stringify(src_lun_uuid), // src lun uuid, snapshot id?
snapshot_uuid: JSON.stringify(snapshot_uuid), // shaptop uuid
cloned_lun_name: cloned_lun_name, // cloned lun name
clone_type: "democratic-csi", // check
};
return await this.do_request(
"GET",
"entry.cgi",
create_volume_from_snapshot
);
}
}
module.exports.SynologyHttpClient = SynologyHttpClient;