@@ -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..."); | |||
}); |
@@ -24,50 +24,4 @@ budoExpress({ | |||
}] | |||
] | |||
}, | |||
}); | |||
// const app = require("../src/app.js")(); | |||
// if (process.env.NODE_ENV === "development") { | |||
// const budo = require("budo"); | |||
// function projectPath(targetPath) { | |||
// return path.resolve(__dirname, "..", targetPath); | |||
// } | |||
// budo(projectPath("src/client/index.jsx"), { | |||
// watchGlob: projectPath("public/css/*.css"), | |||
// live: "**/*.css", | |||
// stream: process.stdout, | |||
// port: 8000, | |||
// dir: projectPath("public"), | |||
// serve: "js/bundle.js", | |||
// debug: true, | |||
// browserify: { | |||
// extensions: [".jsx"], | |||
// plugin: [ | |||
// "browserify-hmr" | |||
// ], | |||
// transform: [ | |||
// ["babelify", { | |||
// presets: ["@babel/preset-env", "@babel/preset-react"], | |||
// plugins: ["react-hot-loader/babel"] | |||
// }] | |||
// ] | |||
// }, | |||
// middleware: function (req, res, next) { | |||
// app.handle(req, res, (err) => { | |||
// if (err != null && err instanceof Error) { | |||
// res.send("<pre>" + err.stack + "</pre>"); | |||
// } else { | |||
// next(err); | |||
// } | |||
// }); | |||
// } | |||
// }); | |||
// } else { | |||
// app.listen(3000).on("listening", () => { | |||
// console.log("Listening..."); | |||
// }); | |||
// } | |||
}); |
@@ -2,6 +2,7 @@ | |||
const Promise = require("bluebird"); | |||
const uuid = require("uuid"); | |||
const defaultValue = require("default-value"); | |||
const childProcess = Promise.promisifyAll(require("child-process"), {multiArgs: true}); | |||
module.exports = function(storePath) { | |||
@@ -13,14 +14,16 @@ module.exports = function(storePath) { | |||
getPath: function getDisk(id) { | |||
return getPath(id); | |||
}, | |||
create: function createDisk(size, {type} = {type: "qcow2"}) { | |||
create: function createDisk(size, options = {}) { | |||
return Promise.try(() => { | |||
let imageFormat = defaultValue(options.format, "qcow2"); | |||
let diskId = uuid.v4(); | |||
return Promise.try(() => { | |||
childProcess.execFileAsync("qemu-img", [ | |||
"create", | |||
"-f", type, | |||
"-f", imageFormat, | |||
getPath(diskId), | |||
size | |||
]); | |||
@@ -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"); | |||
}); |
@@ -9,7 +9,7 @@ const findAndSplice = require("../find-and-splice"); | |||
module.exports = function createTaskTracker() { | |||
let tasks = []; | |||
return createEventEmitter{ | |||
return createEventEmitter({ | |||
addTask: function addTask(name, indicator, userIds, roleIds, options) { | |||
let mergedOptions = Object.assign({ | |||
removeWhenDone: true | |||
@@ -34,3 +34,39 @@ table { | |||
border: 1px solid black; } | |||
table th { | |||
text-align: left; } | |||
table td.hidden { | |||
border: none; } | |||
table.drives td.smart.healthy { | |||
background-color: #00a500; } | |||
table.drives td.smart.deteriorating { | |||
background-color: #ff9100; } | |||
table.drives td.smart.failing { | |||
background-color: #e60000; } | |||
table.drives .hasPartitions td:not(.smart), table.drives .partition:not(.last) td:not(.smart) { | |||
border-bottom-color: transparent; } | |||
table.drives .partition { | |||
font-style: italic; | |||
font-size: .8em; } | |||
table.drives .partition td { | |||
padding: 4px 9px; } | |||
table.drives .partition .notMounted { | |||
color: gray; } | |||
table.drives tr.smartStatus { | |||
font-size: .85em; } | |||
table.drives tr.smartStatus td { | |||
padding: 4px 9px; } | |||
table.drives th.healthy { | |||
color: #006100; } | |||
table.drives th.atRisk { | |||
color: #7c4600; } | |||
table.drives th.failing { | |||
color: #c20000; } |
@@ -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 }) { | |||
} | |||
}; | |||
}; |
@@ -1,17 +1,18 @@ | |||
'use strict'; | |||
const Promise = require("bluebird"); | |||
const checkit = require("checkit"); | |||
const debounce = require("debounce"); | |||
const functionRateLimit = require("function-rate-limit"); | |||
const validatorAdd = require("../lib/form-validators/disk-images/add"); | |||
const validatorAdd = require("../validators/disk-images/add"); | |||
module.exports = function({db, imageStore}) { | |||
const dbDiskImages = require("../db/disk-images")({db}); | |||
let router = require("express-promise-router")(); | |||
router.get("/", (req, res) => { | |||
return Promise.try(() => { | |||
return db("images"); | |||
return dbDiskImages.getAll(); | |||
}).then((results) => { | |||
res.render("disk-images/list", { | |||
images: results | |||
@@ -25,16 +26,17 @@ module.exports = function({db, imageStore}) { | |||
router.post("/add", (req, res) => { | |||
return Promise.try(() => { | |||
return validatorAdd.run(req.body); | |||
return validatorAdd.validate(req.body); | |||
}).then((result) => { | |||
/* FIXME: Only allow 'local' for administrators! */ | |||
if (req.body.source === "http") { | |||
return Promise.try(() => { | |||
return imageStore.addFromUrl(req.body.url); | |||
}).then((downloadTracker) => { | |||
downloadTracker.on("progress", debounce((progress, description) => { | |||
downloadTracker.on("progress", functionRateLimit(1, 200, (progress, description) => { | |||
/* FIXME: This is getting debounced to a point of pointlessness as it will only fire when the download is completed. Needs to be replaced by SSE events anyway, and rate-limited instead of debounced. */ | |||
console.log("Download progress:", (Math.round(progress * 10000) / 100), "%"); | |||
}, 200)); | |||
})); | |||
return downloadTracker.await(); | |||
}); | |||
@@ -50,16 +52,14 @@ module.exports = function({db, imageStore}) { | |||
source = req.body.path; | |||
} | |||
return db("images").insert({ | |||
return dbDiskImages.addInstallISO({ | |||
userId: 0, | |||
fileId: imageId, | |||
name: req.body.name, | |||
description: req.body.description, | |||
sourceType: req.body.source, | |||
source: source, | |||
imageType: "disk", | |||
public: true, | |||
isInstallMedium: true | |||
isPublic: true | |||
}); | |||
}).then(() => { | |||
res.redirect("/disk-images"); |
@@ -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; | |||
} |
@@ -62,4 +62,65 @@ table { | |||
th { | |||
text-align: left; | |||
} | |||
td.hidden { | |||
border: none; | |||
} | |||
} | |||
table.drives { | |||
td.smart { | |||
&.healthy { | |||
background-color: rgb(0, 165, 0); | |||
} | |||
&.deteriorating { | |||
background-color: rgb(255, 145, 0); | |||
} | |||
&.failing { | |||
background-color: rgb(230, 0, 0); | |||
} | |||
} | |||
.hasPartitions, .partition:not(.last) { | |||
td:not(.smart) { | |||
border-bottom-color: transparent; | |||
} | |||
} | |||
.partition { | |||
font-style: italic; | |||
font-size: .8em; | |||
td { | |||
padding: 4px 9px; | |||
} | |||
.notMounted { | |||
color: gray; | |||
} | |||
} | |||
tr.smartStatus { | |||
font-size: .85em; | |||
td { | |||
padding: 4px 9px; | |||
} | |||
} | |||
th { | |||
&.healthy { | |||
color: rgb(0, 97, 0); | |||
} | |||
&.atRisk { | |||
color: rgb(124, 70, 0); | |||
} | |||
&.failing { | |||
color: rgb(194, 0, 0); | |||
} | |||
} | |||
} |
@@ -2,9 +2,11 @@ | |||
const uuid = require("uuid"); | |||
let i = 0; | |||
module.exports = function tagMessage(message) { | |||
if (message.messageId == null) { | |||
message.messageId = uuid.v4(); | |||
message.messageId = i++; | |||
} | |||
return message; |
@@ -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 |
@@ -15,3 +15,6 @@ body | |||
.content | |||
block content | |||
script(src="/js/bundle.js") | |||
script(src="/budo/livereload.js") |