Browse Source

Monster commit; work on Node.js rewrite so far

feature/node-rewrite
Sven Slootweg 2 years ago
parent
commit
9c31b491d5
46 changed files with 2542 additions and 7 deletions
  1. +3
    -7
      .gitignore
  2. +52
    -0
      app.js
  3. +35
    -0
      frontend/components/task-list.riux
  4. +13
    -0
      frontend/components/task-list/component.tag
  5. +8
    -0
      frontend/components/task.riux
  6. +1
    -0
      frontend/index.js
  7. +132
    -0
      gulpfile.js
  8. +17
    -0
      knexfile.js
  9. +57
    -0
      lib/build/wait-for-server.js
  10. +36
    -0
      lib/disk-store.js
  11. +16
    -0
      lib/errors.js
  12. +9
    -0
      lib/find-and-splice.js
  13. +21
    -0
      lib/form-validators/disk-images/add.js
  14. +60
    -0
      lib/image-store.js
  15. +11
    -0
      lib/tag-message.js
  16. +11
    -0
      lib/tag-websocket.js
  17. +21
    -0
      lib/tasks/fake-task.js
  18. +25
    -0
      lib/tasks/progress-indicator.js
  19. +107
    -0
      lib/tasks/tracker.js
  20. +7
    -0
      lib/validators/one-of.js
  21. +108
    -0
      lib/vm/kvm/api.js
  22. +17
    -0
      lib/vm/kvm/index.js
  23. +94
    -0
      lib/vm/kvm/qmp.js
  24. +43
    -0
      lib/websockets/deduplicator.js
  25. +71
    -0
      lib/websockets/tracker.js
  26. +51
    -0
      migrations/20160906073423_initial.js
  27. +13
    -0
      migrations/20160907033335_instance-uuid.js
  28. +14
    -0
      migrations/20160907034042_disks.js
  29. +506
    -0
      notes/notes.txt
  30. +96
    -0
      notes/qmp-cdrom-mounted.json
  31. +67
    -0
      notes/qmp-cdrom-unmounted.js
  32. +273
    -0
      notes/qmp-commands.json
  33. +67
    -0
      package.json
  34. +50
    -0
      public/bundle.js
  35. +36
    -0
      public/css/style.css
  36. +70
    -0
      routes/disk-images.js
  37. +9
    -0
      routes/index.js
  38. +27
    -0
      routes/instances.js
  39. +112
    -0
      routes/tasks.js
  40. +65
    -0
      scss/style.scss
  41. +40
    -0
      views/disk-images/add.pug
  42. +19
    -0
      views/disk-images/list.pug
  43. +6
    -0
      views/error.pug
  44. +4
    -0
      views/index.pug
  45. +25
    -0
      views/instances/list.pug
  46. +17
    -0
      views/layout.pug

+ 3
- 7
.gitignore View File

@@ -1,7 +1,3 @@
installer/slave_sfx.py
installer/master_sfx.py
*.pyc
testing
.geanyprj
.sass-cache
sasswatch.*
config.json
node_modules
images

+ 52
- 0
app.js View File

@@ -0,0 +1,52 @@
'use strict';

const express = require("express");
const expressWs = require("express-ws");
const knex = require("knex");
const path = require("path");
const bodyParser = require("body-parser");

let db = knex(require("./knexfile"));
let imageStore = require("./lib/image-store")(path.join(__dirname, "./images"));
let taskTracker = require("./lib/tasks/tracker")();

let state = {db, imageStore, taskTracker};

let app = express();
expressWs(app);

app.set("view engine", "pug");
app.set("views", path.join(__dirname, "views"));

app.use((req, res, next) => {
res.locals.isUnderPrefix = function isUnderPrefix(path, resultingClass) {
// FIXME: Proper path segment parsing...
if (req.originalUrl.indexOf(path) === 0) {
return resultingClass;
} else {
return "";
}
}

next();
});

app.use(express.static(path.join(__dirname, "public")));

app.use(bodyParser.urlencoded({
extended: true
}));

app.use(require("./routes/index"));
app.use("/disk-images", require("./routes/disk-images")(state));
app.use("/instances", require("./routes/instances")(state));

app.use((err, req, res, next) => {
res.render("error", {
error: err
});
});

app.listen(3000).on("listening", () => {
console.log("Listening...");
});

+ 35
- 0
frontend/components/task-list.riux View File

@@ -0,0 +1,35 @@
task-list
use(path="./task", as="task")
h1 Task List
rx-each(for="task", in="{data.tasks}")
task(data="{task}", parent="{tag}", on-click="{logTask(task)}")
style(type="text/scss").
.rx-scope {
border: 1px solid black;
h1 {
color: red;
}
task {
background-color: rgb(204, 204, 204);
border-bottom: 1px solid rgb(50, 50, 50);
}
button.rx-all {
font-size: 18px;
font-weight: bold;
}
}
script.
console.log("data:", data);
console.log("tag:", tag);

function logTask(tag, task) {
console.log("task:", task);
console.log("this:", tag);
}

+ 13
- 0
frontend/components/task-list/component.tag View File

@@ -0,0 +1,13 @@
task-list
task(each="{task in tasks}", task-data="{task}")

style(type="scss").
foo {
bar {
asddffasdf: "foo"
}
}

script.
// fooas
foo.style("foo")

+ 8
- 0
frontend/components/task.riux View File

@@ -0,0 +1,8 @@
task
.name { attributes.task.name }
.progress(style="width: {progress(attributes.task)}%;")

script.
function progress(task) {
return (task.progress / task.total);
}

+ 1
- 0
frontend/index.js View File

@@ -0,0 +1 @@
'use strict';

+ 132
- 0
gulpfile.js View File

@@ -0,0 +1,132 @@
'use strict';

const Promise = require("bluebird");
const path = require("path");
const gulp = require("gulp");
const webpackStream = require("webpack-stream");
const webpack = require("webpack");
const gulpCached = require("gulp-cached");
const gulpRename = require("gulp-rename");
const gulpNodemon = require("gulp-nodemon");
const gulpLivereload = require("gulp-livereload");
const gulpNamedLog = require("gulp-named-log");

const presetSCSS = require("@joepie91/gulp-preset-scss");
const partialPatchLivereload = require("@joepie91/gulp-partial-patch-livereload-logger");

const serverWaiter = require("./lib/build/wait-for-server")(3000);

const nodemonLogger = gulpNamedLog("nodemon");
const livereloadLogger = gulpNamedLog("livereload");

partialPatchLivereload(gulpLivereload);

let nodemon;

let sources = {
scss: ["./scss/**/*.scss"]
}

/* The following resolves JacksonGariety/gulp-nodemon#33 */
process.once("SIGINT", function() {
process.exit(0);
});

function tryReload(target) {
if (!serverWaiter.isWaiting()) {
if (typeof target === "string") {
gulpLivereload.changed(target);
} else if (target.path != null) {
if (Array.isArray(target.path)) {
gulpLivereload.changed(target.path[0]);
} else {
gulpLivereload.changed(target.path);
}
}
}
}

function queueFullReload() {
return Promise.try(() => {
nodemonLogger.log("Waiting for server to start listening...")
return serverWaiter.wait();
}).then(() => {
nodemonLogger.log("Server started!")
tryReload("*");
});
}

gulp.task("scss", () => {
return gulp.src(sources.scss)
.pipe(presetSCSS({
basePath: __dirname
}))
.pipe(gulp.dest("./public/css"));
});

gulp.task("webpack", () => {
return gulp.src("./frontend/index.js")
.pipe(webpackStream({
watch: true,
module: {
preLoaders: [{
test: /\.tag$/,
loader: "riotjs-loader",
exclude: /node_modules/,
query: {
type: "babel",
template: "pug",
parserOptions: {
js: {
presets: ["es2015-riot"]
}
}
}
}],
loaders: [
{ test: /\.js$/, loader: "babel-loader" },
{ test: /\.json$/, loader: "json-loader" }
]
},
plugins: [ new webpack.ProvidePlugin({riot: "riot"}) ],
resolveLoader: { root: path.join(__dirname, "node_modules") },
resolve: {
extensions: [
"",
".tag",
".web.js", ".js",
".web.json", ".json"
]
},
debug: false
}))
.pipe(gulpRename("bundle.js"))
.pipe(gulp.dest("./public"));
});

gulp.task("nodemon", ["scss"], () => {
nodemon = gulpNodemon({
script: "./app.js",
delay: 500,
ignore: ["node_modules", "public", "gulpfile.js"],
quiet: true
}).on("restart", (cause) => {
nodemonLogger.log("Restarting... caused by:", cause.join(" / "));
serverWaiter.reportRestart();
}).on("start", () => {
return queueFullReload()
});
});

gulp.task("watch-server", ["nodemon"], () => {
gulpLivereload.listen({
quiet: true
});

gulp.watch(sources.scss, ["scss"]);

gulp.watch("./public/**/*", tryReload);
});

gulp.task("watch", ["watch-server", "webpack"]);
gulp.task("default", ["watch"]);

+ 17
- 0
knexfile.js View File

@@ -0,0 +1,17 @@
'use strict';

const config = require("./config.json");

module.exports = {
client: "pg",
connection: {
database: "cvm",
charset: "utf8",
username: config.database.username,
password: config.database.password
},
pool: {
min: 2,
max: 10
}
}

+ 57
- 0
lib/build/wait-for-server.js View File

@@ -0,0 +1,57 @@
'use strict';

const net = require("net");
const Promise = require("bluebird");

function listening(port, cb) {
let done = false;
let sock = new net.Socket();
sock.setTimeout(50);
sock.on("connect", () => {
done = true;
cb(null, true);
sock.destroy();
}).on("timeout", () => {
if (done) { return; }
cb(null, false);
}).on("error", (err) => {
if (done) { return; }
cb(null, false);
}).connect(port);
}

function waitForServer(port, cb) {
setTimeout(() => {
listening(port, (err, isListening) => {
if (err != null) {
cb(err);
} else {
if (isListening) {
cb(null, true);
} else {
waitForServer(port, cb);
}
}
});
}, 100);
}

module.exports = function serverWaiter(port) {
let isWaiting = false;

return {
reportRestart: function() {
isWaiting = true;
},
isWaiting: function() {
return isWaiting;
},
wait: function() {
return Promise.try(() => {
return Promise.promisify(waitForServer)(port);
}).then((result) => {
isWaiting = false;
});
}
}
}

+ 36
- 0
lib/disk-store.js View File

@@ -0,0 +1,36 @@
'use strict';

const Promise = require("bluebird");
const uuid = require("uuid");
const childProcess = Promise.promisifyAll(require("child-process"), {multiArgs: true});

module.exports = function(storePath) {
function getPath(id) {
return path.join(storePath, `${id}.img`);
}

return {
getPath: function getDisk(id) {
return getPath(id);
},
create: function createDisk(size, {type} = {type: "qcow2"}) {
return Promise.try(() => {
let diskId = uuid.v4();

return Promise.try(() => {
childProcess.execFileAsync("qemu-img", [
"create",
"-f", type,
getPath(diskId),
size
]);
}).then(([stdout, stderr]) => {
return diskId;
});
});
},
resize: function resizeDisk(id, size) {

}
}
}

+ 16
- 0
lib/errors.js View File

@@ -0,0 +1,16 @@
'use strict';

const createError = require("create-error");

let HttpError = createError("HttpError", {
exposeToUser: true
});

module.exports = {
UnauthorizedError: createError(HttpError, "UnauthorizedError", {
statusCode: 401
}),
ForbiddenError: createError(HttpError, "ForbiddenError", {
statusCode: 403
})
}

+ 9
- 0
lib/find-and-splice.js View File

@@ -0,0 +1,9 @@
'use strict';

module.exports = function findAndSplice(array, object) {
let index = array.indexOf(object);

if (index !== -1) {
array.splice(index);
}
}

+ 21
- 0
lib/form-validators/disk-images/add.js View File

@@ -0,0 +1,21 @@
'use strict';

const checkit = require("checkit");
const oneOf = require("../../validators/one-of");

module.exports = checkit({
name: "string",
description: "string",
source: ["required", "string", oneOf([
"local",
"http"
])]
}).maybe({
url: ["required", "string"]
}, (input) => {
return (input.source === "http");
}).maybe({
path: ["required", "string"]
}, (input) => {
return (input.source === "local");
});

+ 60
- 0
lib/image-store.js View File

@@ -0,0 +1,60 @@
'use strict';

const Promise = require("bluebird");
const bhttp = require("bhttp");
const uuid = require("uuid");
const fs = Promise.promisifyAll(require("fs-extra"));
const path = require("path");
const endOfStreamAsync = Promise.promisify(require("end-of-stream"));

const progressIndicator = require("./tasks/progress-indicator");

module.exports = function createImageStore(storagePath) {
function getPath(id) {
return path.join(storagePath, `${id}.iso`);
}

return {
addFromUrl: function addFromUrl(url) {
return Promise.try(() => {
return bhttp.get(url, {stream: true});
}).then((response) => {
if (response.statusCode !== 200) {
throw new Error(`Encountered a non-200 status code while attempting to download image (status code was ${response.statusCode})`);
}

let imageId = uuid.v4();
let targetStream = fs.createWriteStream(getPath(imageId));
let totalSize = null;

if (response.headers["content-length"] != null) {
totalSize = parseInt(response.headers["content-length"]);
}

let downloadTracker = progressIndicator(totalSize, imageId);

response.on("progress", (completedBytes, totalBytes) => {
downloadTracker.report(completedBytes);
});

response.pipe(targetStream);

// FIXME: Potential race condition? Are writes handled asynchronously? If so, then we may run into issues here, if the downloadTracker completion is based on the last source read, rather than the last destination write.
return downloadTracker;
});
},
addFromPath: function addFromPath(path) {
return Promise.try(() => {
let imageId = uuid.v4();

return Promise.try(() => {
return fs.copyAsync(path, getPath(imageId), {
overwrite: false
});
}).then(() => {
return imageId;
});
})
}
}
}

+ 11
- 0
lib/tag-message.js View File

@@ -0,0 +1,11 @@
'use strict';

const uuid = require("uuid");

module.exports = function tagMessage(message) {
if (message.messageId == null) {
message.messageId = uuid.v4();
}

return message;
}

+ 11
- 0
lib/tag-websocket.js View File

@@ -0,0 +1,11 @@
'use strict';

const uuid = require("uuid");

module.exports = function(socket) {
if (socket.socketId == null) {
socket.socketId = uuid.v4();
}

return socket;
}

+ 21
- 0
lib/tasks/fake-task.js View File

@@ -0,0 +1,21 @@
'use strict';

const progressIndicator = require("./progress-indicator");

let maxProgressValue = 10000;

module.exports = function createFakeTask(duration) {
let fakeProgressTracker = progressIndicator(maxProgressValue);
let currentProgress = 0;

function addProgress() {
currentProgress += 1;
fakeProgressTracker.report(currentProgress);

if (currentProgress < maxProgressValue) {
setTimeout(addProgress, duration / maxProgressValue);
}
}

return fakeProgressTracker;
};

+ 25
- 0
lib/tasks/progress-indicator.js View File

@@ -0,0 +1,25 @@
'use strict';

const Promise = require("bluebird");
const createEventEmitter = require("create-event-emitter");

module.exports = function(total, completionValue) {
return createEventEmitter({
await: function awaitCompletion() {
return new Promise((resolve, reject) => {
this.once("completed", () => {
resolve(completionValue);
}).once("error", (err) => {
reject(err);
});
});
},
report: function reportStatus(status, description) {
this.emit("progress", status / total, description, status, total);

if (status >= total) {
this.emit("completed");
}
}
});
}

+ 107
- 0
lib/tasks/tracker.js View File

@@ -0,0 +1,107 @@
'use strict';

const assureArray = require("assure-array");
const createEventEmitter = require("create-event-emitter");
const uuid = require("uuid");

const findAndSplice = require("../find-and-splice");

module.exports = function createTaskTracker() {
let tasks = [];

return createEventEmitter{
addTask: function addTask(name, indicator, userIds, roleIds, options) {
let mergedOptions = Object.assign({
removeWhenDone: true
}, options);

let indicatorType;

if (typeof indicator.completed === "function" && typeof indicator.finish === "function") {
indicatorType = "are-we-there-yet";
} else if (typeof indicator.await === "function" && typeof indicator.report === "function") {
indicatorType = "progress-indicator";
} else {
throw new Error("Unrecognized indicator type");
}

let task = createEventEmitter({
id: uuid.v4(),
name: name,
started: Date.now(),
userIds: assureArray(userIds),
roleIds: assureArray(roleIds),
indicator: indicator,
type: indicatorType,
progress: 0,
lastOperation: null,
lastUpdated: null
});

let reportChange = () => {
this.emit("progress", task, task.progress, task.lastOperation);
task.emit("progress", task.progress, task.lastOperation);
}

let reportCompleted = () => {
this.emit("completed", task);
task.emit("completed");

if (mergedOptions.removeWhenDone) {
this.removeTask(task);
}
}

tasks.push(task);

if (indicatorType === "are-we-there-yet") {
indicator.on("change", (name, completed, tracker) => {
task.progress = completed;
task.lastOperation = name;
task.lastUpdated = Date.now();
reportChange();

if (completed === 1) {
reportCompleted();
}
});
} else if (indicatorType === "progress-indicator") {
indicator.on("progress", (progress, description, completed, total) => {
task.progress = progress;
task.lastOperation = description;
task.lastUpdated = Date.now();
reportChange();
});

indicator.on("completed", () => {
reportCompleted();
});
}

this.emit("newTask", task);

assureArray(userIds).forEach((userId) => {
this.emit(`newTask:user:${userId}`, task);
});

assureArray(roleIds).forEach((roleId) => {
this.emit(`newTask:role:${roleId}`, task);
});

return task;
},
removeTask: function removeTask(task) {
// FIXME: Do we need to remove event handlers to prevent a memory leak here?
findAndSplice(tasks, task);
},
byUser: function getTasksByUser(userId) {
return tasks.filter((task) => task.userIds.includes(userId));
},
byRole: function getTasksByRole(roleId) {
return tasks.filter((task) => task.roleIds.includes(roleId))
},
all: function getAllTasks() {
return tasks;
}
});
}

+ 7
- 0
lib/validators/one-of.js View File

@@ -0,0 +1,7 @@
'use strict';

module.exports = function createOneOf(validValues) {
return function oneOf(value) {
return (validValues.indexOf(value) !== -1);
}
}

+ 108
- 0
lib/vm/kvm/api.js View File

@@ -0,0 +1,108 @@
'use strict';

module.exports = function(socket, options) {
return Promise.try(() => {
return Promise.all([
socket.execute("query-commands"),
socket.execute("query-block")
]);
}).then((commands, blockDevices) => {
let commandList = commands.map((item) => item.name);

let supportsBlockdevChangeMedium = commandList.includes("blockdev-change-medium");

let cdromDevices = blockDevices.filter((devices) => device.device.match(/^ide[0-9]-cd[0-9]$/));

return {
create: function() {
},
getSupportedFeatures: function() {
return [
"start", "stop", "forceStop", "forceReset",
"insertDisk", "ejectDisk", "forceEjectDisk", "inspectDisk",
"vncPassword"
];
},
getStatus: function() {
return socket.execute("query-status");
},
start: function() {
// FIXME
},
stop: function(force = false) {
return Promise.try(() => {
if (force) {
socket.execute("quit");
} else {
socket.execute("system_powerdown");
}
});
},
reset: function(force = true) {
return Promise.try(() => {
if (force) {
socket.execute("system_reset");
} else {
throw new NotImplementedError("ACPI reset is not available for QEMU/KVM");
}
});
},
suspend: function() {
return socket.execute("stop");
},
unsuspend: function() {
return socket.execute("cont");
},
insertDisk: function(path, options = {}) {
return Promise.try(() => {
if (cdromDevices.length === 0) {
throw new Error("No CD-ROM devices available");
} else {
if (supportsBlockdevChangeMedium) {
return socket.execute("blockdev-change-medium", {
device: cdromDevices[0].device,
filename: path,
format: options.format
});
} else {
if (options.format != null) {
throw new Error("QEMU/QMP version does not allow for specifying the image format");
} else {
return socket.execute("change", {
device: cdromDevices[0].device,
target: path
});
}
}
}
})
},
ejectDisk: function(force = false) {
return Promise.try(() => {
if (cdromDevices.length === 0) {
throw new Error("No CD-ROM devices available");
} else {
return socket.execute("eject", {
device: cdromDevices[0].device
});
}
})
},
inspectDisk: function() {
// FIXME: Normalized format?
return Promise.try(() => {
if (cdromDevices.length === 0) {
throw new Error("No CD-ROM devices available");
} else {
return Promise.try(() => {
return socket.execute("query-block");
}).then((blockDevices) => {
return blockDevices.filter((device) => device.device === cdromDevices[0].device);
});
}
});
}
}
});
}

+ 17
- 0
lib/vm/kvm/index.js View File

@@ -0,0 +1,17 @@
'use strict';

const path = require("path");
const createError = require("create-error");

const qmp = require("./qmp");
const api = require("./api");

const NotImplementedError = createError("NotImplementedError");

module.exports = function createKvmWrapper(instanceId, options) {
return Promise.try(() => {
return qmp(path.join(__dirname, `../../../qmp-sockets/${instanceId}`));
}).then((socket) => {
return api(socket, Object.assign({instanceId: instanceId}, options));
});
}

+ 94
- 0
lib/vm/kvm/qmp.js View File

@@ -0,0 +1,94 @@
'use strict';

const net = require("net");
const createEventEmitter = require("create-event-emitter");
const jsonStream = require("JSONStream");
const createError = require("create-error");
const defaultValue = require("default-value");

const QMPError = createError("QMPError");
const CommandNotFoundError = createError(QMPError, "CommandNotFoundError");

module.exports = function connect(socketPath) {
return Promise.try(() => {
let socket = net.createConnection({path: socketPath});
let commandQueue = [];
let currentCommand;

function trySendCommand() {
/* This enforces single-concurrency by only executing a command when there's not already a command being processed. Every time a command is either queued or completed, this function is called again, so that eventually the command queue will drain, executing each command in order. */
if (currentCommand == null) {
currentCommand = commandQueue.shift();
socket.write(JSON.stringify(currentCommand.payload));
}
}

function commandResult(result) {
if (currentCommand != null) {
currentCommand.resolve(result.return);
} else {
// FIXME: Log a warning!
}

currentCommand = null;
trySendCommand();
}

function commandFailed(result) {
if (currentCommand != null) {
let err;

if (result.error.class === "CommandNotFound") {
err = new CommandNotFoundError(result.error.desc, result.error);
} else {
err = new QMPError(defaultValue(result.error.desc, "Unknown error occurred"), result.error);
}

currentCommand.reject(err);
} else {
// FIXME: Log a warning!
}

currentCommand = null;
trySendCommand();
}

let emitter = createEventEmitter({
execute: function executeCommand(command, args) {
return new Promise((resolve, reject) => {
/* We need to implement a defer here, because the QMP API doesn't tie responses to requests in any way. We can't really do this with .once event listeners either, because 1) that gets messy quickly and is hard to debug when it breaks, and 2) we need a command queue so that we are only ever executing a single command at a time. */

commandQueue.push({
resolve: resolve,
reject: reject,
payload: {
execute: command,
arguments: args
}
});

trySendCommand();
});
}
});

socket.pipe(jsonStream.parse()).on("data", (obj) => {
if (obj.event != null) {
emitter.emit(obj.event, obj);
} else if (obj.error != null) {
commandFailed(obj);
} else if (obj.return != null) {
commandResult(obj);
} else {
throw new Error("Encountered unexpected message type", obj);
}
});

return Promise.try(() => {
/* This initializes the QMP API. If it fails, our `connect` Promise will fail as well (as it should). */
emitter.execute("qmp_capabilities");
}).then((result) => {
return emitter;
});
});
}

+ 43
- 0
lib/websockets/deduplicator.js View File

@@ -0,0 +1,43 @@
'use strict';

const events = require("events");

module.exports = function createDeduplicator(options) {
let mergedOptions = Object.assign({
historyLength: 10
}, options);

let seenMessages = {}; // FIXME: Use a Set instead?

function tryMessage(client, data, callback) {
if (client.socketId == null) {
// FIXME: Warning!
console.error("Not deduplicating message because client.socketId is null");
callback();
} else if (data.messageId == null) {
// FIXME: Warning!
console.error("Not deduplicating message because data.messageId is null");
callback();
} else {
if (seenMessages[client.socketId] == null) {
seenMessages[client.socketId] = [];
}

let clientSeenMessages = seenMessages[client.socketId];

if (clientSeenMessages.indexOf(data.messageId) !== 1) {
clientSeenMessages.push(data);

if (clientSeenMessages.length > mergedOptions.historyLength) {
clientSeenMessages.shift();
}

callback();
}
}
}

tryMessage.forgetClient = function forgetClient(client) {
delete seenMessages[client.socketId];
}
}

+ 71
- 0
lib/websockets/tracker.js View File

@@ -0,0 +1,71 @@
'use strict';

const websocketDeduplicator = require("./deduplicator");
const findAndSplice = require("../find-and-splice");

module.exports = function(options) {
let deduplicator, externalDeduplicatorProvided;

if (options.deduplicator != null) {
deduplicator = options.deduplicator;
externalDeduplicatorProvided = true;
} else {
deduplicator = websocketDeduplicator();
externalDeduplicatorProvided = false;
}

if (options.namespaced) {
let clients = {};

return {
add: function addClient(namespace, client) {
if (clients[namespace] == null) {
clients[namespace] = [];
}

clients[namespace].push(client);
},
remove: function removeClient(client) {
Object.keys(clients).forEach((namespace) => {
this.removeFromNamespace(namespace, client);
});

if (externalDeduplicatorProvided === false) {
deduplicator.forgetClient(client);
}
},
removeFromNamespace: function removeClientFromNamespace(namespace, client) {
findAndSplice(clients[namespace], client);
},
emit: function emit(namespace, data) {
let namespacedClients = clients[namespace];

if (namespacedClients != null) {
namespacedClients.forEach((client) => {
deduplicator(client, data, () => {
client.send(JSON.stringify(data));
});
});
}
}
}
} else {
let clients = [];

return {
add: function addClient(client) {
clients.push(client);
},
remove: function removeClient(client) {
findAndSplice(clients, client);
},
emit: function emit(data) {
clients.forEach((client) => {
deduplicator(client, data, () => {
client.send(JSON.stringify(data));
});
});
}
}
}
}

+ 51
- 0
migrations/20160906073423_initial.js View File

@@ -0,0 +1,51 @@
'use strict';

exports.up = function(knex, Promise) {
return Promise.all([
knex.schema.createTable("images", (table) => {
table.increments("id");
table.integer("userId").notNullable(); // user that added it
table.uuid("fileId").notNullable();
table.text("name");
table.text("description");
table.enum("sourceType", ["local", "http", "upload"]).notNullable();
table.text("source"); // URL, path, etc.
table.enum("imageType", ["disk", "tarball"]).notNullable(); // eg. tarballs for OpenVZ
table.boolean("public").notNullable(); // whether the image should be visible to everybody, or just its owner
table.boolean("isInstallMedium").notNullable(); // whether the image is just for installation (if not, it will be directly clonable)
}),
knex.schema.createTable("instances", (table) => {
table.increments("id");
table.integer("userId").notNullable();
table.integer("imageId");
table.integer("lastInstallationMediaId");
table.text("comment");
table.text("customIdentifier");
table.enum("virtualizationType", ["kvm"]).notNullable();
table.integer("memory").notNullable(); // in MB
table.integer("swap"); // in MB
table.integer("diskSpace").notNullable(); // in MB
table.integer("traffic"); // in MB
table.boolean("suspended").notNullable();
table.text("suspensionReason");
table.boolean("terminated").notNullable();
table.text("terminationReason");
table.boolean("running");
}),
knex.schema.createTable("users", (table) => {
table.increments("id");
table.text("username").notNullable();
table.text("hash").notNullable();
table.text("emailAddress").notNullable();
table.boolean("active").notNullable();
})
]);
};

exports.down = function(knex, Promise) {
return Promise.all([
knex.schema.dropTable("images"),
knex.schema.dropTable("instances"),
knex.schema.dropTable("users")
]);
};

+ 13
- 0
migrations/20160907033335_instance-uuid.js View File

@@ -0,0 +1,13 @@
'use strict';

exports.up = function(knex, Promise) {
return knex.schema.table("instances", (table) => {
table.uuid("instanceUuid").notNullable();
});
};

exports.down = function(knex, Promise) {
return knex.schema.table("instances", (table) => {
table.dropColumn("instanceUuid");
})
};

+ 14
- 0
migrations/20160907034042_disks.js View File

@@ -0,0 +1,14 @@
'use strict';

exports.up = function(knex, Promise) {
return knex.table("storage_volumes", (table) => {
table.increments("id");
table.integer("instanceId");
table.uuid("volumeUuid").notNullable();
table.enum("format", ["qcow2"]).notNullable();
})
};

exports.down = function(knex, Promise) {

};

+ 506
- 0
notes/notes.txt View File

@@ -0,0 +1,506 @@
qemu-img create -f qcow2 test.qcow 5G
qemu-kvm -cdrom ~/Downloads/debian-8.5.0-amd64-netinst.iso -hda test.qcow -boot d -netdev user,id=user.0 -device e1000,netdev=user.0 -m 196 -localtime

# Getting Started
https://fedoraproject.org/wiki/How_to_use_qemu
https://en.wikibooks.org/wiki/QEMU/Images
https://nixos.org/wiki/QEMU_guest_with_networking_and_virtfs
https://mbharatkumar.wordpress.com/2010/10/09/qemu-getting-started/
http://www.nico.schottelius.org/blog/control-and-shutdown-qemu-kvm-vm-via-unix-socket/
http://man.cx/qemu-system-x86_64(1)

# Flags
https://wiki.gentoo.org/wiki/QEMU/Options

# Monitor
https://en.wikibooks.org/wiki/QEMU/Monitor

# QMP
https://raw.githubusercontent.com/qemu/qemu/master/docs/qmp-intro.txt
https://raw.githubusercontent.com/qemu/qemu/master/docs/qmp-spec.txt
https://raw.githubusercontent.com/qemu/qemu/master/qmp-commands.hx
https://raw.githubusercontent.com/qemu/qemu/master/docs/qmp-events.txt

http://wiki.qemu.org/QMP
https://kashyapc.com/2013/03/31/multiple-ways-to-access-qemu-machine-protocol-qmp/
https://www.npmjs.com/package/qemu-qmp

# QMP (old)
https://qemu.weilnetz.de/w64/2012/2012-06-28/qmp-commands.txt
https://lxr.missinglinkelectronics.com/qemu/qmp-commands.hx

# Networking
http://www.linux-kvm.org/page/Networking
http://wiki.qemu.org/Documentation/Networking
https://pve.proxmox.com/wiki/Network_Model
http://hyperlogos.org/page/HOWTO-kvm-vde-networking-Ubuntu-Debian-et-cetera

# Disk Images
https://alexeytorkhov.blogspot.nl/2009/09/mounting-raw-and-qcow2-vm-disk-images.html
https://edoceo.com/cli/qemu
http://libguestfs.org/
http://www.linux-kvm.org/page/Change_cdrom

# Storage Pools
https://libvirt.org/storage.html

# Virtio
http://www.linux-kvm.org/page/Boot_from_virtio_block_device

# Resource throttling
https://vpsboard.com/topic/4601-kvm-anti-abuse-how-do-you-counter-abuse-with-kvm-users/ - "cgroups cpuacct not a workable solution?"
https://vpsboard.com/topic/4601-kvm-anti-abuse-how-do-you-counter-abuse-with-kvm-users/?do=findComment&comment=66078 - "aka as each vm is a prossess use nice to limit cpu."
http://wiki.qemu.org/Features/AutoconvergeLiveMigration - dynamic CPU throttling lead...
https://lists.gnu.org/archive/html/qemu-devel/2015-06/msg06737.html

# References
http://www.linux-kvm.org/page/HOWTO
http://www.linux-kvm.org/page/Management_Tools
https://github.com/ChoHag/nbsvm/blob/master/nbsvm
https://github.com/digitalocean/go-qemu/

# Full documentation
http://wiki.qemu.org/download/qemu-doc.html



{
"return": [{
"io-status": "ok",
"device": "ide1-cd0",
"locked": false,
"removable": true,
"inserted": {
"iops_rd": 0,
"detect_zeroes": "off",
"image": {
"virtual-size": 258998272,
"filename": "/home/sven/Downloads/debian-8.5.0-amd64-netinst.iso",
"format": "raw",
"actual-size": 259006464,
"dirty-flag": false
},
"iops_wr": 0,
"ro": true,
"node-name": "#block112",
"backing_file_depth": 0,
"drv": "raw",
"iops": 0,
"bps_wr": 0,
"write_threshold": 0,
"encrypted": false,
"bps": 0,
"bps_rd": 0,
"cache": {
"no-flush": false,
"direct": false,
"writeback": true
},
"file": "/home/sven/Downloads/debian-8.5.0-amd64-netinst.iso",
"encryption_key_missing": false
},
"tray_open": false,
"type": "unknown"
}, {
"io-status": "ok",
"device": "ide0-hd0",
"locked": false,
"removable": false,
"inserted": {
"iops_rd": 0,
"detect_zeroes": "off",
"image": {
"virtual-size": 5368709120,
"filename": "test.qcow",
"cluster-size": 65536,
"format": "qcow2",
"actual-size": 1939349504,
"format-specific": {
"type": "qcow2",
"data": {
"compat": "1.1",
"lazy-refcounts": false,
"refcount-bits": 16,
"corrupt": false
}
},
"dirty-flag": false
},
"iops_wr": 0,
"ro": false,
"node-name": "#block321",
"backing_file_depth": 0,
"drv": "qcow2",
"iops": 0,
"bps_wr": 0,
"write_threshold": 0,
"encrypted": false,
"bps": 0,
"bps_rd": 0,
"cache": {
"no-flush": false,
"direct": false,
"writeback": true
},
"file": "test.qcow",
"encryption_key_missing": false
},
"type": "unknown"
}, {
"device": "floppy0",
"locked": false,
"removable": true,
"tray_open": true,
"type": "unknown"
}, {
"device": "sd0",
"locked": false,
"removable": true,
"tray_open": false,
"type": "unknown"
}]
}

{
"return": [{
"io-status": "ok",
"device": "ide0-hd0",
"locked": false,
"removable": false,
"inserted": {
"iops_rd": 0,
"detect_zeroes": "off",
"image": {
"virtual-size": 5368709120,
"filename": "test.qcow",
"cluster-size": 65536,
"format": "qcow2",
"actual-size": 1939349504,
"format-specific": {
"type": "qcow2",
"data": {
"compat": "1.1",
"lazy-refcounts": false,
"refcount-bits": 16,
"corrupt": false
}
},
"dirty-flag": false
},
"iops_wr": 0,
"ro": false,
"node-name": "#block121",
"backing_file_depth": 0,
"drv": "qcow2",
"iops": 0,
"bps_wr": 0,
"write_threshold": 0,
"encrypted": false,
"bps": 0,
"bps_rd": 0,
"cache": {
"no-flush": false,
"direct": false,
"writeback": true
},
"file": "test.qcow",
"encryption_key_missing": false
},
"type": "unknown"
}, {
"io-status": "ok",
"device": "ide1-cd0",
"locked": false,
"removable": true,
"tray_open": false,
"type": "unknown"
}, {
"device": "floppy0",
"locked": false,
"removable": true,
"tray_open": true,
"type": "unknown"
}, {
"device": "sd0",
"locked": false,
"removable": true,
"tray_open": false,
"type": "unknown"
}]
}



{
"return": [{
"name": "query-rocker-of-dpa-groups"
}, {
"name": "query-rocker-of-dpa-flows"
}, {
"name": "query-rocker-ports"
}, {
"name": "query-rocker"
}, {
"name": "block-set-write-threshold"
}, {
"name": "x-input-send-event"
}, {
"name": "trace-event-set-state"
}, {
"name": "trace-event-get-state"
}, {
"name": "rtc-reset-reinjection"
}, {
"name": "query-acpi-ospm-status"
}, {
"name": "query-memory-devices"
}, {
"name": "query-memdev"
}, {
"name": "blockdev-change-medium"
}, {
"name": "query-named-block-nodes"
}, {
"name": "x-blockdev-insert-medium"
}, {
"name": "x-blockdev-remove-medium"
}, {
"name": "blockdev-close-tray"
}, {
"name": "blockdev-open-tray"
}, {
"name": "x-blockdev-del"
}, {
"name": "blockdev-add"
}, {
"name": "query-rx-filter"
}, {
"name": "chardev-remove"
}, {
"name": "chardev-add"
}, {
"name": "query-tpm-types"
}, {
"name": "query-tpm-models"
}, {
"name": "query-tpm"
}, {
"name": "query-target"
}, {
"name": "query-cpu-definitions"
}, {
"name": "query-machines"
}, {
"name": "device-list-properties"
}, {
"name": "qom-list-types"
}, {
"name": "change-vnc-password"
}, {
"name": "nbd-server-stop"
}, {
"name": "nbd-server-add"
}, {
"name": "nbd-server-start"
}, {
"name": "qom-get"
}, {
"name": "qom-set"
}, {
"name": "qom-list"
}, {
"name": "query-block-jobs"
}, {
"name": "query-balloon"
}, {
"name": "query-migrate-parameters"
}, {
"name": "migrate-set-parameters"
}, {
"name": "query-migrate-capabilities"
}, {
"name": "migrate-set-capabilities"
}, {
"name": "query-migrate"
}, {
"name": "query-command-line-options"
}, {
"name": "query-uuid"
}, {
"name": "query-name"
}, {
"name": "query-spice"
}, {
"name": "query-vnc-servers"
}, {
"name": "query-vnc"
}, {
"name": "query-mice"
}, {
"name": "query-status"
}, {
"name": "query-kvm"
}, {
"name": "query-pci"
}, {
"name": "query-iothreads"
}, {
"name": "query-cpus"
}, {
"name": "query-blockstats"
}, {
"name": "query-block"
}, {
"name": "query-chardev-backends"
}, {
"name": "query-chardev"
}, {
"name": "query-qmp-schema"
}, {
"name": "query-events"
}, {
"name": "query-commands"
}, {
"name": "query-version"
}, {
"name": "human-monitor-command"
}, {
"name": "qmp_capabilities"
}, {
"name": "add_client"
}, {
"name": "expire_password"
}, {
"name": "set_password"
}, {
"name": "block_set_io_throttle"
}, {
"name": "block_passwd"
}, {
"name": "query-fdsets"
}, {
"name": "remove-fd"
}, {
"name": "add-fd"
}, {
"name": "closefd"
}, {
"name": "getfd"
}, {
"name": "set_link"
}, {
"name": "balloon"
}, {
"name": "change-backing-file"
}, {
"name": "drive-mirror"
}, {
"name": "blockdev-snapshot-delete-internal-sync"
}, {
"name": "blockdev-snapshot-internal-sync"
}, {
"name": "blockdev-snapshot"
}, {
"name": "blockdev-snapshot-sync"
}, {
"name": "block-dirty-bitmap-clear"
}, {
"name": "block-dirty-bitmap-remove"
}, {
"name": "block-dirty-bitmap-add"
}, {
"name": "transaction"
}, {
"name": "block-job-complete"
}, {
"name": "block-job-resume"
}, {
"name": "block-job-pause"
}, {
"name": "block-job-cancel"
}, {
"name": "block-job-set-speed"
}, {
"name": "blockdev-backup"
}, {
"name": "drive-backup"
}, {
"name": "block-commit"
}, {
"name": "block-stream"
}, {
"name": "block_resize"
}, {
"name": "object-del"
}, {
"name": "object-add"
}, {
"name": "netdev_del"
}, {
"name": "netdev_add"
}, {
"name": "query-dump-guest-memory-capability"
}, {
"name": "dump-guest-memory"
}, {
"name": "client_migrate_info"
}, {
"name": "migrate_set_downtime"
}, {
"name": "migrate_set_speed"
}, {
"name": "query-migrate-cache-size"
}, {
"name": "migrate-start-postcopy"
}, {
"name": "migrate-set-cache-size"
}, {
"name": "migrate-incoming"
}, {
"name": "migrate_cancel"
}, {
"name": "migrate"
}, {
"name": "xen-set-global-dirty-log"
}, {
"name": "xen-save-devices-state"
}, {
"name": "ringbuf-read"
}, {
"name": "ringbuf-write"
}, {
"name": "inject-nmi"
}, {
"name": "pmemsave"
}, {
"name": "memsave"
}, {
"name": "cpu-add"
}, {
"name": "cpu"
}, {
"name": "send-key"
}, {
"name": "device_del"
}, {
"name": "device_add"
}, {
"name": "system_powerdown"
}, {
"name": "system_reset"
}, {
"name": "system_wakeup"
}, {
"name": "cont"
}, {
"name": "stop"
}, {
"name": "screendump"
}, {
"name": "change"
}, {
"name": "eject"
}, {
"name": "quit"
}]
}

+ 96
- 0
notes/qmp-cdrom-mounted.json View File

@@ -0,0 +1,96 @@
{
"return": [{
"io-status": "ok",
"device": "ide1-cd0",
"locked": false,
"removable": true,
"inserted": {
"iops_rd": 0,
"detect_zeroes": "off",
"image": {
"virtual-size": 258998272,
"filename": "/home/sven/Downloads/debian-8.5.0-amd64-netinst.iso",
"format": "raw",
"actual-size": 259006464,
"dirty-flag": false
},
"iops_wr": 0,
"ro": true,
"node-name": "#block112",
"backing_file_depth": 0,
"drv": "raw",
"iops": 0,
"bps_wr": 0,
"write_threshold": 0,
"encrypted": false,
"bps": 0,
"bps_rd": 0,
"cache": {
"no-flush": false,
"direct": false,
"writeback": true
},
"file": "/home/sven/Downloads/debian-8.5.0-amd64-netinst.iso",
"encryption_key_missing": false
},
"tray_open": false,
"type": "unknown"
}, {
"io-status": "ok",
"device": "ide0-hd0",
"locked": false,
"removable": false,
"inserted": {
"iops_rd": 0,
"detect_zeroes": "off",
"image": {
"virtual-size": 5368709120,
"filename": "test.qcow",
"cluster-size": 65536,
"format": "qcow2",
"actual-size": 1939349504,
"format-specific": {
"type": "qcow2",
"data": {
"compat": "1.1",
"lazy-refcounts": false,
"refcount-bits": 16,
"corrupt": false
}
},
"dirty-flag": false
},
"iops_wr": 0,
"ro": false,
"node-name": "#block321",
"backing_file_depth": 0,
"drv": "qcow2",
"iops": 0,
"bps_wr": 0,
"write_threshold": 0,
"encrypted": false,
"bps": 0,
"bps_rd": 0,
"cache": {
"no-flush": false,
"direct": false,
"writeback": true
},
"file": "test.qcow",
"encryption_key_missing": false
},
"type": "unknown"
}, {
"device": "floppy0",
"locked": false,
"removable": true,
"tray_open": true,
"type": "unknown"
}, {
"device": "sd0",
"locked": false,
"removable": true,
"tray_open": false,
"type": "unknown"
}]
}

+ 67
- 0
notes/qmp-cdrom-unmounted.js View File

@@ -0,0 +1,67 @@
{
"return": [{
"io-status": "ok",
"device": "ide0-hd0",
"locked": false,
"removable": false,
"inserted": {
"iops_rd": 0,
"detect_zeroes": "off",
"image": {
"virtual-size": 5368709120,
"filename": "test.qcow",
"cluster-size": 65536,
"format": "qcow2",
"actual-size": 1939349504,
"format-specific": {
"type": "qcow2",
"data": {
"compat": "1.1",
"lazy-refcounts": false,
"refcount-bits": 16,
"corrupt": false
}
},
"dirty-flag": false
},
"iops_wr": 0,
"ro": false,
"node-name": "#block121",
"backing_file_depth": 0,
"drv": "qcow2",
"iops": 0,
"bps_wr": 0,
"write_threshold": 0,
"encrypted": false,
"bps": 0,
"bps_rd": 0,
"cache": {
"no-flush": false,
"direct": false,
"writeback": true
},
"file": "test.qcow",
"encryption_key_missing": false
},
"type": "unknown"
}, {
"io-status": "ok",
"device": "ide1-cd0",
"locked": false,
"removable": true,
"tray_open": false,
"type": "unknown"
}, {
"device": "floppy0",
"locked": false,
"removable": true,
"tray_open": true,
"type": "unknown"
}, {
"device": "sd0",
"locked": false,
"removable": true,
"tray_open": false,
"type": "unknown"
}]
}

+ 273
- 0
notes/qmp-commands.json View File

@@ -0,0 +1,273 @@
{
"return": [{
"name": "query-rocker-of-dpa-groups"
}, {
"name": "query-rocker-of-dpa-flows"
}, {
"name": "query-rocker-ports"
}, {
"name": "query-rocker"
}, {
"name": "block-set-write-threshold"
}, {
"name": "x-input-send-event"
}, {
"name": "trace-event-set-state"
}, {
"name": "trace-event-get-state"
}, {
"name": "rtc-reset-reinjection"
}, {
"name": "query-acpi-ospm-status"
}, {
"name": "query-memory-devices"
}, {
"name": "query-memdev"
}, {
"name": "blockdev-change-medium"
}, {
"name": "query-named-block-nodes"
}, {
"name": "x-blockdev-insert-medium"
}, {
"name": "x-blockdev-remove-medium"
}, {
"name": "blockdev-close-tray"
}, {
"name": "blockdev-open-tray"
}, {
"name": "x-blockdev-del"
}, {
"name": "blockdev-add"
}, {
"name": "query-rx-filter"
}, {
"name": "chardev-remove"
}, {
"name": "chardev-add"
}, {
"name": "query-tpm-types"
}, {
"name": "query-tpm-models"
}, {
"name": "query-tpm"
}, {
"name": "query-target"
}, {
"name": "query-cpu-definitions"
}, {
"name": "query-machines"
}, {
"name": "device-list-properties"
}, {
"name": "qom-list-types"
}, {
"name": "change-vnc-password"
}, {
"name": "nbd-server-stop"
}, {
"name": "nbd-server-add"
}, {
"name": "nbd-server-start"
}, {
"name": "qom-get"
}, {
"name": "qom-set"
}, {
"name": "qom-list"
}, {
"name": "query-block-jobs"
}, {
"name": "query-balloon"
}, {
"name": "query-migrate-parameters"
}, {
"name": "migrate-set-parameters"
}, {
"name": "query-migrate-capabilities"
}, {
"name": "migrate-set-capabilities"
}, {
"name": "query-migrate"
}, {
"name": "query-command-line-options"
}, {
"name": "query-uuid"
}, {
"name": "query-name"
}, {
"name": "query-spice"
}, {
"name": "query-vnc-servers"
}, {
"name": "query-vnc"
}, {
"name": "query-mice"
}, {
"name": "query-status"
}, {
"name": "query-kvm"
}, {
"name": "query-pci"
}, {
"name": "query-iothreads"
}, {
"name": "query-cpus"
}, {
"name": "query-blockstats"
}, {
"name": "query-block"
}, {
"name": "query-chardev-backends"
}, {
"name": "query-chardev"
}, {
"name": "query-qmp-schema"
}, {
"name": "query-events"
}, {
"name": "query-commands"
}, {
"name": "query-version"
}, {
"name": "human-monitor-command"
}, {
"name": "qmp_capabilities"
}, {
"name": "add_client"
}, {
"name": "expire_password"
}, {
"name": "set_password"
}, {
"name": "block_set_io_throttle"
}, {
"name": "block_passwd"
}, {
"name": "query-fdsets"
}, {
"name": "remove-fd"
}, {
"name": "add-fd"
}, {
"name": "closefd"
}, {
"name": "getfd"
}, {
"name": "set_link"
}, {
"name": "balloon"
}, {
"name": "change-backing-file"
}, {
"name": "drive-mirror"
}, {
"name": "blockdev-snapshot-delete-internal-sync"
}, {
"name": "blockdev-snapshot-internal-sync"
}, {
"name": "blockdev-snapshot"
}, {
"name": "blockdev-snapshot-sync"
}, {
"name": "block-dirty-bitmap-clear"
}, {
"name": "block-dirty-bitmap-remove"
}, {
"name": "block-dirty-bitmap-add"
}, {
"name": "transaction"
}, {
"name": "block-job-complete"
}, {
"name": "block-job-resume"
}, {
"name": "block-job-pause"
}, {
"name": "block-job-cancel"
}, {
"name": "block-job-set-speed"
}, {
"name": "blockdev-backup"
}, {
"name": "drive-backup"
}, {
"name": "block-commit"
}, {
"name": "block-stream"
}, {
"name": "block_resize"
}, {
"name": "object-del"
}, {
"name": "object-add"
}, {
"name": "netdev_del"
}, {
"name": "netdev_add"
}, {
"name": "query-dump-guest-memory-capability"
}, {
"name": "dump-guest-memory"
}, {
"name": "client_migrate_info"
}, {
"name": "migrate_set_downtime"
}, {
"name": "migrate_set_speed"
}, {
"name": "query-migrate-cache-size"
}, {
"name": "migrate-start-postcopy"
}, {
"name": "migrate-set-cache-size"
}, {
"name": "migrate-incoming"
}, {
"name": "migrate_cancel"
}, {
"name": "migrate"
}, {
"name": "xen-set-global-dirty-log"
}, {
"name": "xen-save-devices-state"
}, {
"name": "ringbuf-read"
}, {
"name": "ringbuf-write"
}, {
"name": "inject-nmi"
}, {
"name": "pmemsave"
}, {
"name": "memsave"
}, {
"name": "cpu-add"
}, {
"name": "cpu"
}, {
"name": "send-key"
}, {
"name": "device_del"
}, {
"name": "device_add"
}, {
"name": "system_powerdown"
}, {
"name": "system_reset"
}, {
"name": "system_wakeup"
}, {
"name": "cont"
}, {
"name": "stop"
}, {
"name": "screendump"
}, {
"name": "change"
}, {
"name": "eject"
}, {
"name": "quit"
}]
}

+ 67
- 0
package.json View File

@@ -0,0 +1,67 @@
{
"name": "cvm",
"version": "1.0.0",
"description": "A VPS management panel",
"main": "index.js",
"scripts": {
"watch": "gulp watch",
"gulp": "gulp",
"knex": "knex"
},
"repository": {
"type": "git",
"url": "git@git.cryto.net:cvm"
},
"author": "Sven Slootweg",
"license": "WTFPL",
"dependencies": {
"@joepie91/gulp-partial-patch-livereload-logger": "^1.0.1",
"JSONStream": "^1.1.4",
"assure-array": "^1.0.0",
"bhttp": "^1.2.4",
"bluebird": "^3.4.6",
"body-parser": "^1.15.2",
"checkit": "^0.7.0",
"create-error": "^0.3.1",
"create-event-emitter": "^1.0.0",
"debounce": "^1.0.0",
"default-value": "^1.0.0",
"end-of-stream": "^1.1.0",
"express": "^4.14.0",
"express-promise-router": "^1.1.0",
"express-ws": "^3.0.0",
"fs-extra": "^3.0.1",
"knex": "^0.13.0",
"pg": "^6.1.0",
"pug": "^2.0.0-beta6",
"rfr": "^1.2.3",
"scrypt-for-humans": "^2.0.5",