initial commit
This commit is contained in:
99
src/clients/nano-kvm/nano-kvm-client.schema.ts
Normal file
99
src/clients/nano-kvm/nano-kvm-client.schema.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import z from "zod";
|
||||
|
||||
const ResponseBody = z.object({
|
||||
code: z.number(),
|
||||
msg: z.string(),
|
||||
data: z.any(),
|
||||
});
|
||||
|
||||
const CODE_SUCCESS = z.literal(0);
|
||||
|
||||
export const NanoKVMClientOptions = z.object({
|
||||
/** host of nanokvm */
|
||||
host: z.string(),
|
||||
/** the nanokvm username */
|
||||
username: z.string(),
|
||||
/** the nanokvm password */
|
||||
password: z.string(),
|
||||
/** password secret, optional.
|
||||
* only needs to be set if a different secret is set on the nanokvm for more secure password encryption. */
|
||||
passwordSecret: z.string().default("nanokvm-sipeed-2024"),
|
||||
/** https isn't usable by default so we use http */
|
||||
protocol: z.enum(["http", "https"]).default("http"),
|
||||
/** You can provide a token which can improve reliability */
|
||||
token: z.string().optional(),
|
||||
});
|
||||
|
||||
// POST - /api/auth/login
|
||||
|
||||
export const AuthLoginSchemaSuccess = ResponseBody.extend({
|
||||
code: CODE_SUCCESS,
|
||||
data: z.object({
|
||||
token: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const AuthLoginSchemaInvalidPassword = ResponseBody.extend({
|
||||
code: z.literal(-2),
|
||||
});
|
||||
|
||||
export const AuthLoginSchema = z.discriminatedUnion("code", [
|
||||
AuthLoginSchemaSuccess,
|
||||
AuthLoginSchemaInvalidPassword,
|
||||
]);
|
||||
|
||||
// GET - /api/vm/info
|
||||
|
||||
export const InfoSchema = z.object({
|
||||
ips: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
addr: z.string(),
|
||||
version: z.string(),
|
||||
type: z.string(),
|
||||
}),
|
||||
),
|
||||
mdns: z.string(),
|
||||
image: z.string(),
|
||||
application: z.string(),
|
||||
deviceKey: z.string(),
|
||||
});
|
||||
|
||||
export const InfoSuccess = ResponseBody.extend({
|
||||
code: CODE_SUCCESS,
|
||||
data: InfoSchema,
|
||||
});
|
||||
|
||||
// GET - /api/vm/gpio
|
||||
|
||||
export const GpioSchema = z.object({
|
||||
pwr: z.boolean(),
|
||||
hdd: z.boolean(),
|
||||
});
|
||||
|
||||
export const GpioSuccess = ResponseBody.extend({
|
||||
code: CODE_SUCCESS,
|
||||
data: GpioSchema,
|
||||
});
|
||||
|
||||
// POST - /api/vm/gpio
|
||||
|
||||
export const TriggerPowerError = ResponseBody.extend({
|
||||
code: z.literal(-1),
|
||||
data: z.null(),
|
||||
});
|
||||
|
||||
export const TriggerPowerSuccess = ResponseBody.extend({
|
||||
code: CODE_SUCCESS,
|
||||
data: z.null(),
|
||||
});
|
||||
|
||||
export const TriggerPowerSchema = z.discriminatedUnion("code", [
|
||||
TriggerPowerSuccess,
|
||||
TriggerPowerError,
|
||||
]);
|
||||
|
||||
export const TriggerPowerInput = z.object({
|
||||
type: z.literal("power").default("power"),
|
||||
duration: z.number().min(0).max(20000).default(800),
|
||||
});
|
||||
90
src/clients/nano-kvm/nano-kvm-client.ts
Normal file
90
src/clients/nano-kvm/nano-kvm-client.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { BodyInit } from "bun";
|
||||
import type z from "zod";
|
||||
import { encryptPassword } from "@/encryption";
|
||||
import { logger } from "@/logger";
|
||||
import {
|
||||
type AuthLoginSchema,
|
||||
NanoKVMClientOptions,
|
||||
} from "./nano-kvm-client.schema";
|
||||
|
||||
export class NanoKVMClient {
|
||||
private baseUrl: URL;
|
||||
private options: z.output<typeof NanoKVMClientOptions>;
|
||||
private token: string | undefined;
|
||||
|
||||
constructor(options: z.input<typeof NanoKVMClientOptions>) {
|
||||
this.options = NanoKVMClientOptions.parse(options);
|
||||
|
||||
this.baseUrl = new URL(`${this.options.protocol}://${this.options.host}`);
|
||||
}
|
||||
|
||||
get authUrl() {
|
||||
return new URL("/api/auth/login", this.baseUrl);
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.options.token) {
|
||||
this.token = this.options.token;
|
||||
|
||||
logger.info("sucessfully initialized nanokvm using token from config");
|
||||
return;
|
||||
}
|
||||
|
||||
this.loginAndSetToken();
|
||||
}
|
||||
|
||||
async fetch(path: string, method?: "POST" | "GET", body?: BodyInit) {
|
||||
return fetch(new URL(path, this.baseUrl), {
|
||||
headers: {
|
||||
cookie: `nano-kvm-token=${this.token}`,
|
||||
},
|
||||
body,
|
||||
method,
|
||||
});
|
||||
}
|
||||
|
||||
/** Calls the login api and stores the returned token */
|
||||
async loginAndSetToken() {
|
||||
const response = await fetch(this.authUrl, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username: this.options.username,
|
||||
password: encryptPassword(
|
||||
this.options.password,
|
||||
this.options.passwordSecret,
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = (await response.json()) as z.output<
|
||||
typeof AuthLoginSchema
|
||||
> | null;
|
||||
|
||||
if (result === null) {
|
||||
throw new Error("No response from api!");
|
||||
}
|
||||
|
||||
switch (result.code) {
|
||||
case 0: {
|
||||
logger.debug(`Got token ${result.data.token}`);
|
||||
|
||||
this.token = result.data.token;
|
||||
return;
|
||||
}
|
||||
case -2: {
|
||||
logger.fatal(`Invalid password! Failed to get token.`);
|
||||
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
logger.fatal(
|
||||
"Unknown error while getting token.",
|
||||
(result as z.output<typeof AuthLoginSchema>).msg,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/clients/nano-kvm/nano-kvm.service.ts
Normal file
64
src/clients/nano-kvm/nano-kvm.service.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type z from "zod";
|
||||
import { NanoKVMClient } from "./nano-kvm-client";
|
||||
import {
|
||||
type GpioSchema,
|
||||
GpioSuccess,
|
||||
type InfoSchema,
|
||||
InfoSuccess,
|
||||
type NanoKVMClientOptions,
|
||||
TriggerPowerInput,
|
||||
TriggerPowerSchema,
|
||||
} from "./nano-kvm-client.schema";
|
||||
|
||||
export class NanoKVMService {
|
||||
private client: NanoKVMClient;
|
||||
|
||||
constructor(options: {
|
||||
clientOptions: z.input<typeof NanoKVMClientOptions>;
|
||||
}) {
|
||||
this.client = new NanoKVMClient(options.clientOptions);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.client.init();
|
||||
}
|
||||
|
||||
async getInfo(): Promise<z.output<typeof InfoSchema>> {
|
||||
const data = await (await this.client.fetch("/api/vm/info", "GET")).json();
|
||||
|
||||
return InfoSuccess.parse(data).data;
|
||||
}
|
||||
|
||||
async getGpio(): Promise<z.output<typeof GpioSchema>> {
|
||||
const response = await this.client.fetch("/api/vm/gpio", "GET");
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return GpioSuccess.parse(data).data;
|
||||
}
|
||||
|
||||
async triggerPower(input: z.input<typeof TriggerPowerInput>) {
|
||||
const parsedInput = TriggerPowerInput.parse(input);
|
||||
|
||||
console.log(parsedInput);
|
||||
|
||||
const body = new FormData();
|
||||
|
||||
body.append("Type", parsedInput.type);
|
||||
body.append("Duration", parsedInput.duration.toString());
|
||||
|
||||
const response = await this.client.fetch("/api/vm/gpio", "POST", body);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const parsed = TriggerPowerSchema.parse(data);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async triggerReset() {
|
||||
const result = await this.triggerPower({ duration: 8000 });
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user