Browse Source

Slave API

isundil 3 months ago
parent
commit
f78df2e004

+ 3 - 0
.gitignore

@@ -28,3 +28,6 @@ build/Release
 # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
 node_modules
 
+/config.json
+package-lock.json
+/build

+ 4 - 0
config.sample.json

@@ -0,0 +1,4 @@
+{
+    "port": 9090,
+    "slave": false
+}

+ 2 - 0
front/index.ts

@@ -0,0 +1,2 @@
+
+

+ 35 - 0
front/tsconfig.json

@@ -0,0 +1,35 @@
+{
+  "compilerOptions": {
+    "module": "es2020",
+    "target": "esnext",
+    "types": [
+    ],
+    "lib": ["es5", "es6", "dom"],
+    "sourceMap": true,
+    "declaration": true,
+    "declarationMap": true,
+    "noUncheckedIndexedAccess": true,
+    "strict": true,
+    "jsx": "react-jsx",
+    "noUncheckedSideEffectImports": true,
+    "moduleDetection": "force",
+    "skipLibCheck": true,
+
+    "outDir": "../build/front",
+
+    "experimentalDecorators": true,
+    "noImplicitAny": true,
+    "strictNullChecks": true,
+    "strictFunctionTypes": true,
+    "strictBindCallApply": true,
+    "strictPropertyInitialization": true,
+    "noImplicitThis": true,
+    "alwaysStrict": true,
+    "esModuleInterop": true,
+    "removeComments": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noImplicitReturns": true,
+    "forceConsistentCasingInFileNames": true
+  }
+}

+ 16 - 0
front/webpack.config.js

@@ -0,0 +1,16 @@
+
+var webpack = require('webpack');
+
+module.exports = env => {
+    return {
+        output: {
+            filename: env.output
+        },
+        cache: true,
+        devtool: "source-map",
+        entry: [
+            "./build/front/index.js"
+        ]
+    };
+};
+

+ 42 - 0
package.json

@@ -0,0 +1,42 @@
+{
+  "name": "serman",
+  "version": "1.0.0",
+  "description": "Server monitoring and management tool",
+  "main": "index.js",
+  "scripts": {
+    "dev": "npm run build && npm run run-dev",
+    "build": "npm run back && npm run front",
+    "back": "tsoa spec-and-routes",
+    "front": "cd ./front && tsc && cd ../ && npm run webpack && npm run webpack-devel",
+    "webpack": "npx webpack -o ./build/ --mode=production -c ./front/webpack.config.js --env=output=front.mins.js",
+    "webpack-devel": "npx webpack -o ./build/ --mode=development -c ./front/webpack.config.js --env=output=front.js",
+    "run": "tsoa spec-and-routes && npx ts-node src/index.ts",
+    "run-dev": "nodemon src/index.ts"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://git.craftlab.cc/isundil/serman"
+  },
+  "author": "isundil",
+  "license": "GPG",
+  "dependencies": {
+    "@types/express": "^5.0.3",
+    "@types/express-handlebars": "^5.3.1",
+    "@types/swagger-ui-express": "^4.1.8",
+    "express": "^5.1.0",
+    "express-handlebars": "^8.0.3",
+    "linux-systemd": "^0.0.1",
+    "mariadb": "^3.4.5",
+    "swagger-jsdoc": "^6.2.8",
+    "swagger-ui-express": "^5.0.1",
+    "systeminformation": "^5.27.8",
+    "ts-node": "^10.9.2",
+    "tsconfig-paths": "^4.2.0",
+    "tsoa": "^6.6.0",
+    "webpack": "^5.101.3",
+    "webpack-cli": "^6.0.1"
+  },
+  "devDependencies": {
+    "nodemon": "^3.1.10"
+  }
+}

+ 4 - 0
public/css/style.css

@@ -0,0 +1,4 @@
+html,body {
+    margin: 0;
+    padding: 0;
+}

+ 20 - 0
src/config.ts

@@ -0,0 +1,20 @@
+import path from "path";
+
+export interface Configuration {
+    port: number;
+    slave: boolean;
+}
+
+class ConfigurationManagerLoader {
+    public static Load(fileName: string): Configuration {
+        let config = require(fileName);
+
+        return {
+            port: config.port || 9090,
+            slave: config.slave || false
+        };
+    }
+}
+
+let ConfigurationManager = ConfigurationManagerLoader.Load(path.join(__dirname, "../config.json"));
+export default ConfigurationManager;

+ 45 - 0
src/index.ts

@@ -0,0 +1,45 @@
+import express, { json, urlencoded } from "express";
+import { RegisterRoutes } from "../build/routes";
+import { HtmlController } from "./routes/htmlControllers";
+import swaggerUi from "swagger-ui-express";
+import { engine } from 'express-handlebars';
+import path from "path";
+import ConfigurationManager from "./config";
+
+const app = express();
+
+if (!ConfigurationManager.slave) {
+    // Whiskers html template
+    app.engine('handlebars', engine());
+    app.set('view engine', 'handlebars');
+    app.set('views', path.join(__dirname, '../templates'));
+}
+
+// Swagger UI
+app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(require(path.join(__dirname, "../build/swagger.json"))));
+
+// API routes
+app.use(
+    urlencoded({
+        extended: true,
+    })
+);
+app.use(json());
+if (!ConfigurationManager.slave)
+    HtmlController.RegisterHtmlPages(app);
+RegisterRoutes(app);
+
+// Static resources
+if (!ConfigurationManager.slave) {
+    app.use("/js/front.js", express.static(path.join(__dirname, "../build/"), {index: 'front.js'}));
+    app.use("/js/front.min.js", express.static(path.join(__dirname, "../build/"), {index: 'front.min.js'}));
+    app.use("/js/front.js.map", express.static(path.join(__dirname, "../build/"), {index: 'front.js.map'}));
+    app.use("/js/front.min.js.map", express.static(path.join(__dirname, "../build/"), {index: 'front.min.js.map'}));
+    app.use(express.static(path.join(__dirname, 'public')));
+}
+
+app.listen(ConfigurationManager.port,
+    () => console.log(`Server listening on port ${ConfigurationManager.port}.`))
+    .on("error", (error) => {
+        throw new Error(error.message);
+    });

+ 19 - 0
src/models/service.ts

@@ -0,0 +1,19 @@
+
+export enum eServiceStatus {
+    eUnknown,
+    eActive,
+    eFailed,
+    eInactive,
+    eActivating,
+    eDeactivating,
+    eMaintenance,
+    eReloading,
+    eRefreshing
+}
+
+export interface Service {
+    name: string;
+    status: eServiceStatus;
+    statusText: string;
+    description: string;
+}

+ 67 - 0
src/models/systemInfo.ts

@@ -0,0 +1,67 @@
+
+export interface CpuInfo {
+    model: string;
+}
+
+export interface NetworkInfo {
+    iface: string;
+    address: string;
+    netmask: string;
+    family: string;
+    mac: string;
+}
+
+export interface DriveInfo {
+    name: string;
+    type: string;
+    fsType: string;
+    mount: string;
+    size: number;
+    usedSize: number;
+    physical: string;
+    uuid: string;
+    label: string;
+    model: string;
+    serial: string;
+    removable: boolean;
+    protocol: string;
+    device: string;
+    rw: boolean|undefined;
+}
+
+export interface SystemInfo {
+    // Os
+    platform: string;
+    distribution: string;
+    osVersion: string;
+    arch: string;
+    uptime: number;
+    hostname: string;
+
+    // Service
+    nodeVersion: string;
+
+    // hardware
+    manufacturer: string;
+    model: string;
+    cpuMaxSpeed: number;
+    cpus: Array<CpuInfo>;
+    memory: number;
+    memoryLayout: Array<number>;
+
+    // Network
+    network: Array<NetworkInfo>;
+    dnsServers: Array<string>;
+
+    // Drives
+    drives: Array<DriveInfo>
+}
+
+export interface LiveSystemInfo {
+    totalMemoryUsed: number;
+    activeMemory: number;
+    currentCpuUsed: number;
+    cpuByCore: Array<number>;
+    // FIXME temperature
+}
+

+ 114 - 0
src/routes/api_sysInfoController.ts

@@ -0,0 +1,114 @@
+import {
+    Controller,
+    Get,
+    Route
+} from "tsoa";
+import {CpuInfo, DriveInfo, LiveSystemInfo, NetworkInfo, SystemInfo} from "../models/systemInfo";
+import os from 'os';
+import dns from 'dns';
+import si from 'systeminformation';
+
+@Route("/api/sysinfo")
+export class SystemdInfoController extends Controller {
+    private readCpuInfo(): Array<CpuInfo> {
+        let cpus = os.cpus();
+        return cpus.map((x: any) => <CpuInfo> {
+            model: x.model
+        });
+    }
+
+    private readNetworkInfo(): Array<NetworkInfo> {
+        let networkInfo = os.networkInterfaces();
+        let result = new Array<NetworkInfo>();
+        for (let i in networkInfo) {
+            for (let j of networkInfo[i]!) {
+                result.push(<NetworkInfo>{
+                    iface: i,
+                    address: j.address,
+                    netmask: j.netmask,
+                    family: j.family,
+                    mac: j.mac
+                });
+            }
+        }
+        return result;
+    }
+
+    private async readDriveInfo(): Promise<Array<DriveInfo>> {
+        const mountFsInfo = await si.fsSize();
+        return (await si.blockDevices()).map((x) => {
+            const mountInfo = x.mount ? mountFsInfo.find(fsInfo => x.mount === fsInfo.mount) : null;
+            return <DriveInfo> {
+                name: x.name,
+                type: x.type,
+                fsType: x.fsType,
+                mount: x.mount,
+                size: x.size, // FIXME used size
+                usedSize: mountInfo? mountInfo.used : undefined,
+                rw: mountInfo ? mountInfo.rw : undefined,
+                physical: x.physical,
+                uuid: x.uuid,
+                label: x.label,
+                model: x.model,
+                serial: x.serial,
+                removable: x.removable,
+                protocol: x.protocol,
+                device: x.device
+            };
+        });
+    }
+
+    @Get("/")
+    public async getInfo(): Promise<SystemInfo> {
+        const system = await si.system();
+        return {
+            platform: os.platform(),
+            osVersion: os.release(),
+            arch: os.machine(),
+            distribution: (await si.osInfo()).distro,
+            nodeVersion: process.version,
+            uptime: os.uptime(),
+            hostname: os.hostname(),
+            memory: os.totalmem(),
+            memoryLayout: (await si.memLayout()).map(x => x.size),
+            cpuMaxSpeed: (await si.cpu()).speedMax,
+            cpus: this.readCpuInfo(),
+            network: this.readNetworkInfo(),
+            dnsServers: dns.getServers(),
+            manufacturer: system.manufacturer,
+            model: system.model,
+            drives: (await this.readDriveInfo())
+        }
+    };
+
+    private getCpuUsage(): Promise<Array<number>> {
+        return new Promise(ok => {
+            const stats1 = os.cpus();
+            const getTotalTime = (cpuInfo: any) => cpuInfo.user + cpuInfo.nice + cpuInfo.sys + cpuInfo.idle + cpuInfo.irq;
+
+            setTimeout(function() {
+                const stats2 = os.cpus();
+                let cores = [];
+                for (var i =0; i < stats1.length; ++i)
+                    cores.push({
+                        total: getTotalTime(stats2[i]!.times) - getTotalTime(stats1[i]!.times),
+                        idle: stats2[i]!.times.idle - stats1[i]!.times.idle
+                    });
+                ok(cores.map(x => 100-(100 * x.idle / x.total)).map(x => Math.round(x*100)/100));
+            }, 500 );
+        });
+    }
+
+    @Get("/live")
+    public async getLiveInfo(): Promise<LiveSystemInfo> {
+        const memInfo = await si.mem();
+        const cpuSpeed = await this.getCpuUsage();
+
+        return <LiveSystemInfo> {
+            totalMemoryUsed: memInfo.used,
+            activeMemory: memInfo.active,
+            currentCpuUsed: cpuSpeed.reduce((a, b) => a +b, 0) /cpuSpeed.length,
+            cpuByCore: cpuSpeed
+        }
+    }
+}

+ 16 - 0
src/routes/api_systemdController.ts

@@ -0,0 +1,16 @@
+import {
+  Controller,
+  Get,
+  Route
+} from "tsoa";
+import {Service} from "../models/service";
+import systemdService from "../services/systemdService";
+
+@Route("/api/services")
+export class SystemdServiceController extends Controller {
+
+  @Get("/list")
+  public async getAllServices(): Promise<Array<Service>> {
+    return systemdService.getAllServices()
+  };
+}

+ 18 - 0
src/routes/api_userInfoController.ts

@@ -0,0 +1,18 @@
+import {
+  Controller,
+  Get,
+  Route
+} from "tsoa";
+
+@Route("/api")
+export class UserInfoController extends Controller {
+  @Get("/groups")
+  public async listGroups(): Promise<void> {
+      // FIXME
+  };
+
+  @Get("/users")
+  public async listUsers(): Promise<void> {
+      // FIXME
+  };
+}

+ 14 - 0
src/routes/htmlControllers.ts

@@ -0,0 +1,14 @@
+import Express from "express"
+
+export class HtmlController {
+    private static GenerateContext(override: any): object {
+        override = override || {};
+        override.title = override.title || "Title";
+        override.javascript = override.javascript !== undefined ? override.javascript : "js/front.js"
+        return override;
+    }
+
+    public static RegisterHtmlPages(app: Express.Express): void {
+        app.get("/", (_, res) => { res.render("index", HtmlController.GenerateContext(null)); });
+    }
+}

+ 17 - 0
src/services/systemdService.ts

@@ -0,0 +1,17 @@
+import {eServiceStatus, Service} from "../models/service";
+import SystemdWrapper from "../systemd/wrapper";
+
+class SystemdService {
+    public getById(name: string): Service {
+        return <Service> {
+            name: name,
+            status: eServiceStatus.eUnknown
+        };
+    }
+
+    public async getAllServices(): Promise<Array<Service>> {
+        return await SystemdWrapper.list();
+    }
+}
+
+export default new SystemdService();

+ 41 - 0
src/systemd/wrapper.ts

@@ -0,0 +1,41 @@
+import {eServiceStatus, Service} from "../models/service";
+
+const listServices = require("linux-systemd").list;
+
+class SystemdWrapper {
+    private getStatus(status:string): eServiceStatus {
+        status = status.toLocaleLowerCase();
+        if (status === "active")
+            return eServiceStatus.eActive;
+        else if (status === "inactive")
+            return eServiceStatus.eInactive;
+        else if (status === "failed")
+            return eServiceStatus.eFailed;
+        else if (status === "activating")
+            return eServiceStatus.eActivating;
+        else if (status === "deactivating")
+            return eServiceStatus.eDeactivating;
+        else if (status === "maintenance")
+            return eServiceStatus.eMaintenance;
+        else if (status === "reloading")
+            return eServiceStatus.eReloading;
+        else if (status === "refreshing")
+            return eServiceStatus.eRefreshing;
+        return eServiceStatus.eUnknown;
+    }
+
+    public async list(): Promise<Array<Service>> {
+        let services = await listServices();
+        return Object.keys(services)
+            .map((serviceKey: string) => services[serviceKey])
+            .map((service: any) => <Service> {
+                name: service.name,
+                description: service.description,
+                status: this.getStatus(service.activeState),
+                statusText: service.subState
+            });
+    }
+}
+
+export default new SystemdWrapper();
+

+ 1 - 0
templates/index.handlebars

@@ -0,0 +1 @@
+Test

+ 13 - 0
templates/layouts/main.handlebars

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="stylesheet" href="/css/style.css">
+    <title>{{title}}</title>
+</head>
+<body>
+    {{{body}}}
+    {{#if javascript}}<script src="{{javascript}}"></script>{{/if}}
+</body>
+</html>

+ 32 - 0
tsconfig.json

@@ -0,0 +1,32 @@
+{
+  "compilerOptions": {
+    "module": "commonjs",
+    "target": "esnext",
+    "types": [
+    ],
+    "sourceMap": false,
+    "declaration": true,
+    "declarationMap": true,
+    "noUncheckedIndexedAccess": true,
+    "strict": true,
+    "jsx": "react-jsx",
+    "isolatedModules": true,
+    "noUncheckedSideEffectImports": true,
+    "moduleDetection": "force",
+    "skipLibCheck": true,
+    "experimentalDecorators": true,
+    "noImplicitAny": true,
+    "strictNullChecks": true,
+    "strictFunctionTypes": true,
+    "strictBindCallApply": true,
+    "strictPropertyInitialization": true,
+    "noImplicitThis": true,
+    "alwaysStrict": true,
+    "esModuleInterop": true,
+    "removeComments": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noImplicitReturns": true,
+    "forceConsistentCasingInFileNames": true
+  }
+}

+ 12 - 0
tsoa.json

@@ -0,0 +1,12 @@
+{
+  "entryFile": "src/index.ts",
+  "noImplicitAdditionalProperties": "throw-on-extras",
+  "controllerPathGlobs": ["src/routes/api_*Controller.ts"],
+  "spec": {
+    "outputDirectory": "build",
+    "specVersion": 3
+  },
+  "routes": {
+    "routesDir": "build"
+  }
+}