Compare commits
5 Commits
82e7227f91
...
e9619f387a
Author | SHA1 | Date |
---|---|---|
Sven Slootweg | e9619f387a | 5 years ago |
Sven Slootweg | e10935c79a | 5 years ago |
Sven Slootweg | 102ac37020 | 5 years ago |
Sven Slootweg | f40ce08148 | 5 years ago |
Sven Slootweg | 6a78511d45 | 5 years ago |
@ -0,0 +1,78 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
/* Things that should effectively be syntax errors. */
|
||||
"indent": [ "error", "tab", {
|
||||
SwitchCase: 1
|
||||
}],
|
||||
"linebreak-style": [ "error", "unix" ],
|
||||
"semi": [ "error", "always" ],
|
||||
/* Things that are always mistakes. */
|
||||
"getter-return": [ "error" ],
|
||||
"no-compare-neg-zero": [ "error" ],
|
||||
"no-dupe-args": [ "error" ],
|
||||
"no-dupe-keys": [ "error" ],
|
||||
"no-duplicate-case": [ "error" ],
|
||||
"no-empty": [ "error" ],
|
||||
"no-empty-character-class": [ "error" ],
|
||||
"no-ex-assign": [ "error" ],
|
||||
"no-extra-semi": [ "error" ],
|
||||
"no-func-assign": [ "error" ],
|
||||
"no-invalid-regexp": [ "error" ],
|
||||
"no-irregular-whitespace": [ "error" ],
|
||||
"no-obj-calls": [ "error" ],
|
||||
"no-sparse-arrays": [ "error" ],
|
||||
"no-undef": [ "error" ],
|
||||
"no-unreachable": [ "error" ],
|
||||
"no-unsafe-finally": [ "error" ],
|
||||
"use-isnan": [ "error" ],
|
||||
"valid-typeof": [ "error" ],
|
||||
"curly": [ "error" ],
|
||||
"no-caller": [ "error" ],
|
||||
"no-fallthrough": [ "error" ],
|
||||
"no-extra-bind": [ "error" ],
|
||||
"no-extra-label": [ "error" ],
|
||||
"array-callback-return": [ "error" ],
|
||||
"prefer-promise-reject-errors": [ "error" ],
|
||||
"no-with": [ "error" ],
|
||||
"no-useless-concat": [ "error" ],
|
||||
"no-unused-labels": [ "error" ],
|
||||
"no-unused-expressions": [ "error" ],
|
||||
"no-unused-vars": [ "error" , { argsIgnorePattern: "^_" } ],
|
||||
"no-return-assign": [ "error" ],
|
||||
"no-self-assign": [ "error" ],
|
||||
"no-new-wrappers": [ "error" ],
|
||||
"no-redeclare": [ "error" ],
|
||||
"no-loop-func": [ "error" ],
|
||||
"no-implicit-globals": [ "error" ],
|
||||
"strict": [ "error", "global" ],
|
||||
/* Make JSX not cause 'unused variable' errors. */
|
||||
"react/jsx-uses-react": ["error"],
|
||||
"react/jsx-uses-vars": ["error"],
|
||||
/* Development code that should be removed before deployment. */
|
||||
"no-console": [ "warn" ],
|
||||
"no-constant-condition": [ "warn" ],
|
||||
"no-debugger": [ "warn" ],
|
||||
"no-alert": [ "warn" ],
|
||||
"no-warning-comments": ["warn", {
|
||||
terms: ["fixme"]
|
||||
}],
|
||||
/* Common mistakes that can *occasionally* be intentional. */
|
||||
"no-template-curly-in-string": ["warn"],
|
||||
"no-unsafe-negation": [ "warn" ],
|
||||
}
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
config.json
|
||||
node_modules
|
||||
images
|
||||
disks
|
@ -0,0 +1,37 @@
|
||||
# CVM
|
||||
|
||||
A non-commercial, fully open-source VPS management panel.
|
||||
|
||||
## Current status
|
||||
|
||||
Definitely not ready for production use yet. Check back later!
|
||||
|
||||
## Roadmap
|
||||
|
||||
TBD. Initially, only QEMU/KVM will be supported. Support for other virtualization and/or containerization technologies may follow later, if there's enough demand.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are very welcome! Please keep the following things in mind:
|
||||
|
||||
- "Contributions" don't just include code commits, they also include bug reports, translations, documentation fixes, triaging, and so on.
|
||||
- Anybody is welcome (and encouraged!) to leave code reviews on Pull Requests. You don't need to be a maintainer!
|
||||
- You agree that any contributions you make, are licensed under both the WTFPL and the CC0 (which more or less means they are public domain). You keep copyright ownership over your work.
|
||||
- If you're making a Pull Request, make sure that each PR has its own branch in your fork, so that commits between PRs don't get mixed up.
|
||||
- Because of the security-sensitive nature of CVM, we very tightly control who can directly commit to the repository, even for regular contributors. This is not because we don't trust the quality of your commits, but as an extra layer of security and review.
|
||||
|
||||
We welcome contributions from developers of any experience level, *but* we do have a very high code quality standard. Don't worry if you're not sure how to reach that standard; when you submit a PR, we'll help you get there by doing a code review.
|
||||
|
||||
This does mean that you should only create a Pull Request if you're willing to work on incrementally improving it before it gets merged! "Hit-and-run" PRs will very likely be closed.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Quite simple, really:
|
||||
|
||||
- The goal is for this project to be collaborative, and welcoming to contributors of any demographic and experience level. Arrogance, elitism, etc. are therefore not welcome.
|
||||
- Don't be an asshole. Treat people respectfully. Assume good faith.
|
||||
- Constructive criticism is fine, personal attacks are not. Don't just tell somebody that they're doing it wrong, help them understand *why*, and how to do it better.
|
||||
- No bigoted or discriminatory behaviour or remarks. That includes "jokes".
|
||||
- Anything related to the alt-right or other hateful ideologies, is banned on sight. If you feel the need to explain how your thing "isn't related", it's probably related.
|
||||
|
||||
Bans and other repercussions are at the discretion of maintainers.
|
@ -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...");
|
||||
});
|
@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
|
||||
const budoExpress = require("budo-express");
|
||||
const path = require("path");
|
||||
|
||||
budoExpress({
|
||||
port: 8000,
|
||||
debug: true,
|
||||
expressApp: require("../src/app")(),
|
||||
basePath: path.resolve(__dirname, ".."),
|
||||
entryPath: "src/client/index.jsx",
|
||||
publicPath: "public",
|
||||
bundlePath: "js/bundle.js",
|
||||
livereload: "**/*.{css,html}",
|
||||
browserify: {
|
||||
extensions: [".jsx"],
|
||||
plugin: [
|
||||
"browserify-hmr"
|
||||
],
|
||||
transform: [
|
||||
["babelify", {
|
||||
presets: ["@babel/preset-env", "@babel/preset-react"],
|
||||
// plugins: ["react-hot-loader/babel"]
|
||||
}]
|
||||
]
|
||||
},
|
||||
});
|
@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
|
||||
const buildThing = require("build-thing");
|
||||
|
||||
let scssTask = buildThing.core.defineTask(() => {
|
||||
return buildThing.core.pipeline([
|
||||
buildThing.core.watch("src/scss/**/*.scss"),
|
||||
buildThing.scssTransform(),
|
||||
buildThing.core.saveToDirectory("public/css")
|
||||
]);
|
||||
});
|
||||
|
||||
buildThing.runner(scssTask);
|
@ -1,132 +0,0 @@
|
||||
'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"]);
|
@ -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