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