WIP: Various web-y things.
parent
102ac37020
commit
e10935c79a
@ -1,52 +0,0 @@
|
||||
'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...");
|
||||
});
|
@ -1,16 +0,0 @@
|
||||
'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
|
||||
})
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
'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");
|
||||
});
|
@ -1,112 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const rfr = require("rfr");
|
||||
|
||||
const errors = rfr("lib/errors");
|
||||
const tagWebsocket = rfr("lib/tag-websocket");
|
||||
const tagMessage = rfr("lib/tag-message");
|
||||
const websocketTracker = rfr("lib/websockets/tracker");
|
||||
const websocketDeduplicator = rfr("lib/deduplicator");
|
||||
const fakeTask = rfr("lib/tasks/fake-task");
|
||||
|
||||
module.exports = function({taskTracker}) {
|
||||
let router = require("express-promise-router")();
|
||||
|
||||
/* We manually create a deduplicator, so that we can share it across all the user-level websocket trackers. This does mean that we have to manually remove disconnected clients from our shared deduplicator. */
|
||||
let deduplicator = websocketDeduplicator();
|
||||
|
||||
let allClients = websocketTracker({deduplicator: deduplicator});
|
||||
let userClients = websocketTracker({deduplicator: deduplicator, namespaced: true});
|
||||
let roleClients = websocketTracker({deduplicator: deduplicator, namespaced: true});
|
||||
|
||||
/* The adminClient tracker is a special case, and can use its own internal deduplicator if needed */
|
||||
let adminClients = websocketTracker();
|
||||
|
||||
function emitTask(task, message) {
|
||||
task.userIds.forEach((userId) => {
|
||||
userClients.emit(userId, message);
|
||||
});
|
||||
|
||||
task.roleIds.forEach((roleId) => {
|
||||
roleClients.emit(roleId, message);
|
||||
});
|
||||
|
||||
adminClients.emit(message);
|
||||
}
|
||||
|
||||
taskTracker.on("newTask", (task) => {
|
||||
emitTask(task, tagMessage({
|
||||
// FIXME: Task state? pending/started/paused/cancelled/etc.
|
||||
taskId: task.id,
|
||||
type: "newTask",
|
||||
name: task.name,
|
||||
progress: task.progress,
|
||||
started: task.started,
|
||||
lastUpdated: task.lastUpdated,
|
||||
lastOperation: task.lastOperation
|
||||
}));
|
||||
});
|
||||
|
||||
taskTracker.on("progress", (task, progress, lastOperation) => {
|
||||
emitTask(task, tagMessage({
|
||||
taskId: task.id,
|
||||
type: "progress",
|
||||
progress: progress,
|
||||
lastOperation: lastOperation
|
||||
}));
|
||||
});
|
||||
|
||||
taskTracker.on("completed", (task) => {
|
||||
emitTask(tagMessage({
|
||||
taskId: task.id,
|
||||
type: "completed"
|
||||
}));
|
||||
});
|
||||
|
||||
// FIXME: Initial task list
|
||||
|
||||
router.ws("/feed", (ws, req) => {
|
||||
// FIXME: Auth user?
|
||||
if (req.session.user == null) {
|
||||
throw new errors.UnauthorizedError("You must be authenticated to obtain a task feed");
|
||||
} else {
|
||||
tagWebsocket(ws);
|
||||
// We ignore client messages for now
|
||||
|
||||
allClients.add(client);
|
||||
userClients.add(req.session.user.id, client);
|
||||
|
||||
req.session.user.roles.forEach((role) => {
|
||||
roleClients.add(role.id, client);
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
allClients.remove(client);
|
||||
userClients.remove(client);
|
||||
roleClients.remove(client);
|
||||
deduplicator.forgetClient(client);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.ws("/feed/all", (ws, req) => {
|
||||
// FIXME: Require admin access
|
||||
tagWebsocket(ws);
|
||||
// We ignore client messages for now
|
||||
|
||||
adminClients.add(client);
|
||||
|
||||
ws.on("close", () => {
|
||||
adminClients.remove(client);
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/fake-task", (req, res) => {
|
||||
let task = fakeTask(5000);
|
||||
addTask("Fake Task", task, [req.session.user.id], [], {});
|
||||
|
||||
res.send("Fake task added!");
|
||||
})
|
||||
|
||||
return router;
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
const express = require("express");
|
||||
// const expressWs = require("express-ws");
|
||||
const knex = require("knex");
|
||||
const path = require("path");
|
||||
const bodyParser = require("body-parser");
|
||||
|
||||
function projectPath(targetPath) {
|
||||
return path.join(__dirname, "..", targetPath);
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
let db = knex(require("../knexfile"));
|
||||
let imageStore = require("./image-store")(projectPath("./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", projectPath("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(projectPath("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("/hardware/storage-devices", require("./routes/storage-devices")(state));
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
if (err.showChain != null) {
|
||||
console.log(err.showChain());
|
||||
console.log("#####################");
|
||||
console.log(err.getAllContext());
|
||||
|
||||
} else {
|
||||
console.log(err.stack);
|
||||
}
|
||||
|
||||
res.render("error", {
|
||||
error: err
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
|
||||
const errors = require("../errors");
|
||||
|
||||
module.exports = {
|
||||
isAuthenticated: function (req, res, next) {
|
||||
if (req.session.user != null) {
|
||||
next();
|
||||
} else {
|
||||
throw new errors.UnauthorizedError("You must be authenticated to view this page");
|
||||
}
|
||||
},
|
||||
isAdministrator: function (req, res, next) {
|
||||
/* FIXME */
|
||||
next();
|
||||
}
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
|
||||
console.log("Hello world!");
|
@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
|
||||
module.exports = function ({db}) {
|
||||
return {
|
||||
getAll: function () {
|
||||
return db("images");
|
||||
},
|
||||
addInstallISO: function ({userId, fileId, name, description, sourceType, source, isPublic}) {
|
||||
return db("images").insert({
|
||||
userId: userId,
|
||||
fileId: fileId,
|
||||
name: name,
|
||||
description: description,
|
||||
sourceType: sourceType,
|
||||
source: source,
|
||||
public: isPublic,
|
||||
imageType: "disk",
|
||||
isInstallMedium: true
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function createFileStoragePool({ storePath }) {
|
||||
return {
|
||||
createDisk: function ({ size, format }) {
|
||||
|
||||
},
|
||||
resizeDisk: function ({ id, newSize }) {
|
||||
|
||||
},
|
||||
removeDisk: function ({ id }) {
|
||||
|
||||
}
|
||||
};
|
||||
};
|
@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const Promise = require("bluebird");
|
||||
|
||||
const lsblk = require("../wrappers/lsblk");
|
||||
const smartctl = require("../wrappers/smartctl");
|
||||
const lvm = require("../wrappers/lvm");
|
||||
const {B} = require("../units/bytes/iec");
|
||||
|
||||
/* FIXME: Move this to GraphQL API */
|
||||
function getSmartStatus(smartData) {
|
||||
let failed = smartData.filter((item) => {
|
||||
return (item.failingNow === true || item.failedBefore === true);
|
||||
});
|
||||
|
||||
let deteriorating = smartData.filter((item) => {
|
||||
return (item.type === "preFail" && item.worstValueSeen < 100);
|
||||
});
|
||||
|
||||
if (failed.length > 0) {
|
||||
return "failed";
|
||||
} else if (deteriorating.length > 0) {
|
||||
return "deteriorating";
|
||||
} else {
|
||||
return "healthy";
|
||||
}
|
||||
}
|
||||
|
||||
function getStorageDevices() {
|
||||
return Promise.try(() => {
|
||||
return lsblk();
|
||||
}).filter((device) => {
|
||||
return (device.type === "disk");
|
||||
}).map((device) => {
|
||||
return Object.assign({}, device, {
|
||||
path: `/dev/${device.name}`
|
||||
});
|
||||
}).map((device) => {
|
||||
/* FIXME: Check whether we need to iterate through child disks as well, when dealing with eg. RAID arrays */
|
||||
return Promise.try(() => {
|
||||
return Promise.all([
|
||||
smartctl.info({ devicePath: device.path }),
|
||||
smartctl.attributes({ devicePath: device.path })
|
||||
]);
|
||||
}).then(([info, attributes]) => {
|
||||
return Object.assign({}, device, {
|
||||
information: info,
|
||||
smartData: attributes,
|
||||
smartStatus: getSmartStatus(attributes)
|
||||
});
|
||||
});
|
||||
}).then((blockDevices) => {
|
||||
console.log(blockDevices);
|
||||
return blockDevices;
|
||||
});
|
||||
}
|
||||
|
||||
function sumDriveSizes(drives) {
|
||||
return drives.reduce((total, device) => {
|
||||
return total + device.size.toB().amount;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function roundUnit(unit) {
|
||||
return Object.assign(unit, {
|
||||
amount: Math.round(unit.amount * 100) / 100
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function({db}) {
|
||||
let router = require("express-promise-router")();
|
||||
|
||||
router.get("/", (req, res) => {
|
||||
return Promise.try(() => {
|
||||
return getStorageDevices();
|
||||
}).then((devices) => {
|
||||
/* FIXME: Auto-formatting of total sizes and units */
|
||||
let fixedDrives = devices.filter((drive) => drive.removable === false);
|
||||
let removableDrives = devices.filter((drive) => drive.removable === true);
|
||||
|
||||
let healthyFixedDrives = fixedDrives.filter((drive) => drive.smartStatus === "healthy");
|
||||
let deterioratingFixedDrives = fixedDrives.filter((drive) => drive.smartStatus === "deteriorating");
|
||||
let failingFixedDrives = fixedDrives.filter((drive) => drive.smartStatus === "failing");
|
||||
|
||||
res.render("hardware/storage-devices/list", {
|
||||
devices: devices,
|
||||
totalFixedStorage: roundUnit(B(sumDriveSizes(fixedDrives)).toTiB()),
|
||||
totalHealthyFixedStorage: roundUnit(B(sumDriveSizes(healthyFixedDrives)).toTiB()),
|
||||
totalDeterioratingFixedStorage: roundUnit(B(sumDriveSizes(deterioratingFixedDrives)).toTiB()),
|
||||
totalFailingFixedStorage: roundUnit(B(sumDriveSizes(failingFixedDrives)).toTiB()),
|
||||
totalRemovableStorage: roundUnit(B(sumDriveSizes(removableDrives)).toGiB())
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
|
||||
const sseChannel = require("@joepie91/sse-channel");
|
||||
|
||||
const errors = require("../errors");
|
||||
// const tagWebsocket = require("lib/tag-websocket");
|
||||
const tagMessage = require("../tag-message");
|
||||
// const websocketTracker = require("lib/websockets/tracker");
|
||||
// const websocketDeduplicator = require("lib/deduplicator");
|
||||
const fakeTask = require("lib/tasks/fake-task");
|
||||
|
||||
const authMiddleware = require("../auth/middleware");
|
||||
|
||||
module.exports = function({taskTracker}) {
|
||||
let router = require("express-promise-router")();
|
||||
|
||||
// /* We manually create a deduplicator, so that we can share it across all the user-level websocket trackers. This does mean that we have to manually remove disconnected clients from our shared deduplicator. */
|
||||
// let deduplicator = websocketDeduplicator();
|
||||
|
||||
// let allClients = websocketTracker({deduplicator: deduplicator});
|
||||
// let userClients = websocketTracker({deduplicator: deduplicator, namespaced: true});
|
||||
// let roleClients = websocketTracker({deduplicator: deduplicator, namespaced: true});
|
||||
|
||||
// /* The adminClient tracker is a special case, and can use its own internal deduplicator if needed */
|
||||
// let adminClients = websocketTracker();
|
||||
|
||||
// function emitTask(task, message) {
|
||||
// task.userIds.forEach((userId) => {
|
||||
// userClients.emit(userId, message);
|
||||
// });
|
||||
|
||||
// task.roleIds.forEach((roleId) => {
|
||||
// roleClients.emit(roleId, message);
|
||||
// });
|
||||
|
||||
// adminClients.emit(message);
|
||||
// }
|
||||
|
||||
taskTracker.on("newTask", (task) => {
|
||||
emitTask(task, tagMessage({
|
||||
// FIXME: Task state? pending/started/paused/cancelled/etc.
|
||||
taskId: task.id,
|
||||
type: "newTask",
|
||||
name: task.name,
|
||||
progress: task.progress,
|
||||
started: task.started,
|
||||
lastUpdated: task.lastUpdated,
|
||||
lastOperation: task.lastOperation
|
||||
}));
|
||||
});
|
||||
|
||||
taskTracker.on("progress", (task, progress, lastOperation) => {
|
||||
emitTask(task, tagMessage({
|
||||
taskId: task.id,
|
||||
type: "progress",
|
||||
progress: progress,
|
||||
lastOperation: lastOperation
|
||||
}));
|
||||
});
|
||||
|
||||
taskTracker.on("completed", (task) => {
|
||||
emitTask(tagMessage({
|
||||
taskId: task.id,
|
||||
type: "completed"
|
||||
}));
|
||||
});
|
||||
|
||||
// FIXME: Initial task list
|
||||
|
||||
router.get("/feed", authMiddleware.isAuthenticated, (req, res) => {
|
||||
// FIXME: Auth user?
|
||||
if (req.session.user == null) {
|
||||
throw new errors.UnauthorizedError("You must be authenticated to obtain a task feed");
|
||||
} else {
|
||||
// tagWebsocket(ws);
|
||||
// // We ignore client messages for now
|
||||
|
||||
// allClients.add(client);
|
||||
// userClients.add(req.session.user.id, client);
|
||||
|
||||
// req.session.user.roles.forEach((role) => {
|
||||
// roleClients.add(role.id, client);
|
||||
// });
|
||||
|
||||
// ws.on("close", () => {
|
||||
// allClients.remove(client);
|
||||
// userClients.remove(client);
|
||||
// roleClients.remove(client);
|
||||
// deduplicator.forgetClient(client);
|
||||
// });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/feed/all", authMiddleware.isAdministrator, (req, res) => {
|
||||
// FIXME: Require admin access
|
||||
tagWebsocket(ws);
|
||||
// We ignore client messages for now
|
||||
|
||||
adminClients.add(client);
|
||||
|
||||
ws.on("close", () => {
|
||||
adminClients.remove(client);
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/fake-task", (req, res) => {
|
||||
let task = fakeTask(5000);
|
||||
addTask("Fake Task", task, [req.session.user.id], [], {});
|
||||
|
||||
res.send("Fake task added!");
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const joi = require("joi");
|
||||
|
||||
// const checkit = require("checkit");
|
||||
// const oneOf = require("../../validators/one-of");
|
||||
|
||||
module.exports = joi.object({
|
||||
name: joi.string().required(),
|
||||
description: joi.string(),
|
||||
source: joi.string().required(),
|
||||
url: joi.when("source", { is: "http", then: joi.string().required() }),
|
||||
path: joi.when("source", { is: "local", then: joi.string().required() })
|
||||
});
|
||||
|
||||
// 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");
|
||||
// });
|
@ -0,0 +1,70 @@
|
||||
extends ../../layout
|
||||
|
||||
block content
|
||||
|
||||
h2 Fixed drives
|
||||
|
||||
//- FIXME: Partitions with mountpoints
|
||||
table.drives
|
||||
tr
|
||||
th SMART
|
||||
th Device
|
||||
th Total size
|
||||
th RPM
|
||||
th Serial number
|
||||
th Model
|
||||
th Family
|
||||
th Firmware version
|
||||
for device in devices.filter((device) => device.removable === false)
|
||||
tr(class=(device.children.length > 0 ? "hasPartitions" : null))
|
||||
td(class=`smart ${device.smartStatus}`, rowspan=(1 + device.children.length))
|
||||
td= device.name
|
||||
td= device.size
|
||||
td #{device.information.rpm} RPM
|
||||
td= device.information.serialNumber
|
||||
td= device.information.model
|
||||
td= device.information.modelFamily
|
||||
td= device.information.firmwareVersion
|
||||
|
||||
for partition, i in device.children
|
||||
tr.partition(class=(i === device.children.length - 1) ? "last" : null)
|
||||
td= partition.name
|
||||
td= partition.size
|
||||
td(colspan=5)
|
||||
if partition.mountpoint != null
|
||||
= partition.mountpoint
|
||||
else
|
||||
span.notMounted (not mounted)
|
||||
|
||||
|
||||
//- tr.partition
|
||||
//- td(colspan=8)= JSON.stringify(partition)
|
||||
tr
|
||||
th(colspan=2) Total
|
||||
td= totalFixedStorage
|
||||
td(colspan=5).hidden
|
||||
tr.smartStatus
|
||||
th(colspan=2).healthy Healthy
|
||||
td= totalHealthyFixedStorage
|
||||
td(colspan=5).hidden
|
||||
tr.smartStatus
|
||||
th(colspan=2).atRisk At-risk
|
||||
td= totalDeterioratingFixedStorage
|
||||
td(colspan=5).hidden
|
||||
tr.smartStatus
|
||||
th(colspan=2).failing Failing
|
||||
td= totalFailingFixedStorage
|
||||
td(colspan=5).hidden
|
||||
|
||||
h2 Removable drives
|
||||
|
||||
table
|
||||
tr
|
||||
th Path
|
||||
th Total size
|
||||
th Mounted at
|
||||
for device in devices.filter((device) => device.type === "loopDevice")
|
||||
tr
|
||||
td= device.path
|
||||
td= device.size
|
||||
td= device.mountpoint
|
Loading…
Reference in New Issue