node-xd
Version:
Node LXD Client
712 lines (683 loc) • 17.9 kB
JavaScript
const { match } = require("assert");
const { EventEmitter } = require("events");
const awaitOperation = require("../lib/awaitOperation");
var fs = require('fs')
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
class Instance {
/**
* @returns {string}
*/
name() {
return this._name;
}
/**
* @returns {{key: string, data: string}[]}
*/
config() {
var conf = [];
Object.keys(this._metadata.meta.expanded_config).forEach((el) => {
conf.push({ key: el, data: this._metadata.meta.expanded_config[el] });
});
return conf;
}
/**
* Gets the current state of the instance
* @returns {Promise<"Running"|"Stopped">}
*/
async state() {
return new Promise((resolve, reject) => {
this.client.get("/1.0/instances/" + this._name + "/state").then(data => {
resolve(data.metadata.status)
}).catch(err => {
reject(err)
})
})
}
/**
* Gets the type of the instance
* @returns {"container" | "virtual-machine"}
*/
type() {
return this._metadata.meta.type;
}
/**
*
* @param {string} path
* @param {fs.WriteStream} writeStream
*/
download(path, writeStream) {
return new Promise(async (resolve, reject) => {
try {
if (!path) throw new Error('Path not defined')
var events = new EventEmitter()
const url = encodeURI("/1.0/instances/" + this._name + "/files?path=" + path)
const { data, headers } = await this.client.axios({
url,
method: 'GET',
responseType: 'stream'
})
events.emit('open')
const totalLength = headers['content-length']
var done = 0
if (data.headers["content-type"] == "application/json") {
data.on('data', (chunk) => {
resolve({
type: 'dir',
list: JSON.parse(chunk.toString()).metadata
})
})
} else {
data.on('data', (chunk) => {
done += chunk.length;
var percent = (done * 100) / parseFloat(totalLength)
var progress = {
bytes: {
sent: done,
total: parseFloat(totalLength)
},
percent: percent
}
events.emit('progress', progress)
if (progress.percent == 100) {
events.emit('finish')
}
})
data.pipe(writeStream)
resolve({ type: 'file', events })
}
} catch (error) {
reject(error)
}
})
}
/**
* Upload file to instance
* @param {fs.ReadStream} ReadStream
* @param {string} destPath
* @returns {EventEmitter}
*/
upload(ReadStream, destPath) {
return new Promise(async (resolve, reject) => {
var events = new EventEmitter()
var https = require('https')
var parsedURL = new URL(this.rootClient.host)
if (this.rootClient.connectionType == "unix") {
var opts = {
rejectUnauthorized: false,
method: "POST",
socketPath: this.rootClient.unixpath,
path: encodeURI("/1.0/instances/" + this._name + "/files?path=" + destPath),
headers: {
"Content-Type": `application/octet-stream`
},
}
} else if (this.rootClient.connectionType == "http") {
var opts = {
cert: this.rootClient.cert,
key: this.rootClient.key,
rejectUnauthorized: false,
method: "POST",
hostname: parsedURL.hostname,
port: parsedURL.port,
path: encodeURI("/1.0/instances/" + this._name + "/files?path=" + destPath),
headers: {
"Content-Type": `application/octet-stream`
},
}
}
var request = https.request(opts, function (response) {
response.on('error', (err) => {
reject(err)
})
});
request.on('error', error => {
reject(error)
})
var bytes = 0
var size = fs.lstatSync(ReadStream.path).size;
ReadStream.on('data', (chunk) => {
bytes += chunk.length;
var percent = ((bytes) * 100) / size
var data = {
bytes: {
sent: bytes,
total: size
},
percent: percent
}
events.emit('progress', data)
if (data.percent == 100) {
events.emit("finish")
}
}).pipe(request)
resolve(events)
})
}
/**
*
* @param {string} name
* @param {boolean} stateful
* @returns {Promise<EventEmitter>}
*/
async createSnapshot(name, stateful) {
return new Promise(async (resolve, reject) => {
try {
var snapshots = await this.client.post('/1.0/instances/' + this.name() + '/snapshots?recursion=1', {
"name": name,
stateful: stateful
})
var s = new (require('events')).EventEmitter
var eventsws = await this.client.ws('/1.0/events?type=operation')
eventsws.on('message', (datam) => {
var datap = JSON.parse(datam.toString());
if (datap.metadata.id == snapshots.data.metadata.id)
s.emit('finish', datap)
})
resolve(s)
await awaitOperation(this.rootClient, snapshots.data.metadata.id)
s.emit("completed")
} catch (error) {
reject(error)
}
})
}
/**
*
* @param {string} name
* @returns {{}}
*/
async fetchSnapshot(name) {
return new Promise((resolve, reject) => {
this.client.get('/1.0/instances/' + this.name() + '/snapshots/' + name).then(data => {
resolve(data)
}).catch(err => {
reject(err)
})
})
}
/**
*
* @returns {{}}
*/
async listSnapshot() {
return new Promise((resolve, reject) => {
this.client.get('/1.0/instances/' + this.name() + '/snapshots?recursion=1').then(data => { resolve(data) }).catch(err => {
reject(err)
})
})
}
/**
*
* @param {string} name
* @param {boolean} stateful
* @returns {{}}
*/
async restoreSnapshot(name, stateful) {
return new Promise((resolve, reject) => {
this.client.put('/1.0/instances/' + this.name(), {
restore: name,
stateful: stateful
}).then(data => { resolve(data) }).catch(err => {
reject(err)
})
})
}
async deleteSnapshot(name) {
return new Promise((resolve, reject) => {
this.client.delete('/1.0/instances/' + this.name() + '/snapshots/' + name).then(data => { resolve(data) }).catch(err => {
reject(err)
})
})
}
/**
* Returns instances IP on bridge
* @param {"ipv4"|"ipv6"} family
* @returns {Promise<string>}
*/
async ip(family) {
return new Promise(async (resolve, reject) => {
try {
var data = await this.client.get("/1.0/instances/" + this._name + "/state")
if (!family) {
resolve(data.metadata.network.eth0.addresses.find(val => val.family == "inet").address)
} else if (family == "ipv4") {
resolve(data.metadata.network.eth0.addresses.find(val => val.family == "inet").address)
} else if (family == "ipv6") {
resolve(data.metadata.network.eth0.addresses.find(val => val.family == "inet6").address)
}
} catch (error) {
reject(error)
}
})
}
async stop(force) {
return new Promise(async (resolve, reject) => {
try {
var data = await this.client.put(
"/1.0/instances/" + this._name + "/state",
{
action: "stop",
force: force ? force : false,
stateful: false,
timeout: 30,
}
);
if (data.metadata.err == "The instance is already stopped")
return resolve();
await awaitOperation(this.rootClient, data.metadata.id);
resolve();
} catch (error) {
reject(error);
}
});
}
/**
*
* @param {string} command
* @param {object} options
* @param {{}?} options.env
* @param {string?} options.cwd
* @param {number?} options.user
* @param {boolean?} options.interactive
* @returns {Promise<import('ws').WebSocket | string>}
*/
exec(command, options) {
if (!options) var options = {}
return new Promise(async (resolve, reject) => {
try {
var data = await this.client.post("/1.0/instances/" + this._name + "/exec", {
"command": command.split(' '),
"environment": options.env ? options.env : { TERM: "linux" },
"interactive": true,
"wait-for-websocket": true,
})
var r = await this.client.ws(
data.data.operation +
"/websocket?secret=" +
data.data.metadata.metadata.fds["0"]
)
//await awaitOperation(this.rootClient, data.data.metadata.id)
if (options.interactive == true) {
r.on('message', (d) => {
if (d == "") {
r.close()
}
})
resolve(r)
} else {
var str = ""
function exit() {
r.close()
resolve(str)
}
r.on("message", async (d) => {
if (d == "") {
exit()
} else {
str += d
}
})
}
} catch (error) {
reject(error)
}
})
}
async usage(system) {
return new Promise(async (resolve, reject) => {
try {
var state = await this.client.get("/1.0/instances/" + this._name + "/state")
if (state.metadata.status == "Running") {
var os = require('os')
if (system == true) {
var cpuCount = os.cpus().length
} else {
var s = (await this.client.get("/1.0/instances/" + this._name)).metadata.config["limits.cpu"]
var cpuCount = s ? s : os.cpus().length; // thats probs why i did / 2
}
var multiplier = 100000 / cpuCount
var startTime = Date.now()
var usage1 = ((await this.client.get("/1.0/instances/" + this._name + "/state")).metadata.cpu.usage / 1000000000)
var usage2 = ((await this.client.get("/1.0/instances/" + this._name + "/state")).metadata.cpu.usage / 1000000000)
var cpu_usage = ((usage2 - usage1) / (Date.now() - startTime)) * multiplier
if (cpu_usage > 100) {
cpu_usage = 100;
}
resolve({
state: state.metadata.status,
cpu: (cpu_usage),
swap: {
usage: (state.metadata.memory.swap_usage * 0.00000095367432)
},
memory: {
usage: (state.metadata.memory.usage),
percent: (((state.metadata.memory.usage / os.totalmem()) * 100))
},
disk: {
usage: state.metadata.disk ? state.metadata.disk.root ? state.metadata.disk.root.usage : 0 : 0
}
})
} else {
resolve({
state: state.metadata.status,
cpu: 0,
swap: {
usage: 0
},
memory: {
usage: 0,
percent: 0
},
disk: {
usage: state.metadata.disk ? state.metadata.disk.root ? state.metadata.disk.root.usage : 0 : 0
}
})
}
} catch (error) {
reject(error)
}
})
}
async start(force) {
return new Promise(async (resolve, reject) => {
try {
var data = await this.client.put(
"/1.0/instances/" + this._name + "/state",
{
action: "start",
force: force ? force : false,
stateful: false,
timeout: 30,
}
);
if (data.metadata.err == "The instance is already running")
return resolve();
await awaitOperation(this.rootClient, data.metadata.id);
resolve();
} catch (error) {
reject(error);
}
});
}
/**
* Creates new console websocket (sending commands over text websocket must be sent as binary)
* @param {"vga"|"console"} type
* @param {{endpoint: "exec" | "console", command: string[], env: {},raw:{}}} options
* @returns {Promise<{operation: WebSocket,control: WebSocket, proxy: function(WebSocket): {send: function(string), close: function(), removeAllListeners: function()}, proxyctrl: function(WebSocket): {send: function(string), close: function(), removeAllListeners: function()}}>}
*/
async console(type, options) {
if (!options.raw) options.raw = {}
return new Promise(async (resolve, reject) => {
try {
switch (type) {
case "vga":
var data = await this.client.post(
"/1.0/instances/" + this._name + "/console",
{
height: 0,
type: "vga",
width: 0,
...options.raw
}
);
break;
case "console":
if (options.endpoint == "console") {
var data = await this.client.post("/1.0/instances/" + this._name + "/console", {
"height": 24,
"type": "console",
"width": 80,
...options.raw
})
// use console endpoint instead of exec, both work
} else if (options.endpoint == "exec") {
var data = await this.client.post(
"/1.0/instances/" + this._name + "/exec",
{
command: options.command ? options.command : ["/bin/bash"],
environment: {
TERM: "linux",
...options.env
},
interactive: true,
"wait-for-websocket": true,
...options.raw
}
);
} else {
var data = await this.client.post("/1.0/instances/" + this._name + "/console", {
"height": 24,
"type": "console",
"width": 80,
...options.raw
})
//
}
break;
default:
break;
}
//console.log(data.data)
//if (!data.data.operation || data.data.metadata.metadata.fds["0"] || data.data.metadata.metadata.fds["control"]) return reject(new Error('Operation failed to start'))
try {
var r = await this.client.ws(
data.data.operation +
"/websocket?secret=" +
data.data.metadata.metadata.fds["0"]
)
var ctrl = await this.client.ws(
data.data.operation +
"/websocket?secret=" +
data.data.metadata.metadata.fds["control"]
)
} catch (error) {
return reject(new Error('Failed to connect to operation'))
}
/**
*
* @param {import('ws').WebSocket} ws
* @param {function(ws)} auth
* @returns {{send: <Function(command:string)>}}
*/
var proxyctrl = (ws) => {
ws.on('message', (data) => {
ctrl.send(data, { binary: true })
})
var s = (data) => {
ws.send(data, { binary: true })
}
ctrl.on('message', s)
return {
send: function (command) {
ws.send(command + '\n', { binary: true })
},
close: function () {
ctrl.removeListener("message", s)
ws.close()
},
removeAllListeners: function () {
ctrl.removeListener("message", s)
},
};
}
var proxy = (ws) => {
ws.on('message', (data) => {
r.send(data, { binary: true })
})
var s = (data) => {
ws.send(data, { binary: true })
}
r.on('message', s)
return {
send: function (command) {
ws.send(command + '\n', { binary: true })
},
close: function () {
r.removeListener("message", s)
ws.close()
},
removeAllListeners: function () {
r.removeListener("message", s)
},
};
}
resolve(
{ proxy: proxy, operation: r, control: ctrl, proxyctrl }
);
} catch (error) {
reject(error);
}
});
}
async delete() {
return new Promise(async (resolve, reject) => {
try {
var res = await this.client.delete('/1.0/instances/' + this._name)
await awaitOperation(this.rootClient, res.data.metadata.id)
} catch (error) {
reject(error)
}
resolve()
})
}
/**
*
* @param {string} name
* @returns {Promise<EventEmitter>}
*/
async scheduleBackup(name) {
return new Promise(async (resolve, reject) => {
if (!name) reject('no backup name specified')
try {
var res = await this.client.post('/1.0/instances/' + this._name + '/backups', {
"compression_algorithm": "gzip",
"container_only": false,
"instance_only": false,
"name": name,
"optimized_storage": true
})
var s = new (require('events')).EventEmitter
var eventsws = await this.client.ws('/1.0/events?type=operation')
eventsws.on('message', (datam) => {
var datap = JSON.parse(datam.toString());
if (datap.metadata.id == res.data.metadata.id)
s.emit('finish', datap)
})
resolve(s)
await awaitOperation(this.rootClient, res.data.metadata.id)
s.emit("completed")
} catch (error) {
reject(error)
}
})
}
async deleteBackup(backup) {
return new Promise(async (resolve, reject) => {
try {
await this.client.delete("/1.0/instances/" + this._name + "/backups/" + backup);
} catch (error) {
return reject(error);
}
return resolve("Success");
})
}
/**
*
* @param {*} backup
*/
async downloadBackup(backup, pipe) {
return new Promise(async (resolve, reject) => {
try {
var events = new EventEmitter();
try {
var { data, headers } = await this.client.axios({
url: "/1.0/instances/" + this._name + "/backups/" + backup + "/export",
method: 'GET',
responseType: 'stream'
});
} catch (error) {
reject(error);
}
events.emit("open");
var length = headers["content-length"]
var done = 0;
data.on('data', (chunk) => {
done += chunk.length;
events.emit("progress", done / length);
});
data.on('end', () => {
events.emit("finish");
console.log(events)
resolve(data);
});
data.on('error', (error) => {
events.emit("error", error);
reject(error);
});
data.pipe(pipe);
} catch (error) {
reject(error)
}
})
}
/**
*
* @returns {Promise<string>}
*/
async logs() {
return new Promise(async (resolve, reject) => {
this.client.get("/1.0/instances/" + this._name + "/logs").catch(err => {
reject(err)
}).then(data => {
resolve(data)
})
})
}
restart(force) {
return new Promise(async (resolve, reject) => {
try {
var data = await this.client.put(
"/1.0/instances/" + this._name + "/state",
{
action: "restart",
force: force ? force : false,
stateful: false,
timeout: 30,
}
);
await awaitOperation(this.rootClient, data.metadata.id);
resolve();
} catch (error) {
reject(error);
}
});
}
/**
* @param {import('./Client')} self
*/
constructor(self, data) {
/**
* @private
*/
this._metadata = data;
/**
* @private
*/
this._name = data.meta.name;
/**
* @private
* @type {import('../lib/RequestClient')}
*/
this.client = self.client;
/**
* @private
* @type {import('./Client')}
*/
this.rootClient = self;
}
}
module.exports = Instance;