feat(settings): Add runtime settings management, node & baremetal managers, and settings UI

This commit is contained in:
2025-09-07 17:21:30 +00:00
parent 83abe37d8c
commit 54ef62e7af
36 changed files with 1914 additions and 301 deletions

View File

@@ -0,0 +1,104 @@
import * as plugins from '../plugins.js';
/**
* BareMetal represents an actual physical server
*/
@plugins.smartdata.Manager()
export class BareMetal extends plugins.smartdata.SmartDataDbDoc<
BareMetal,
plugins.servezoneInterfaces.data.IBareMetal
> {
// STATIC
public static async createFromHetznerServer(
hetznerServerArg: plugins.hetznercloud.HetznerServer,
) {
const newBareMetal = new BareMetal();
newBareMetal.id = plugins.smartunique.shortId(8);
const data: plugins.servezoneInterfaces.data.IBareMetal['data'] = {
hostname: hetznerServerArg.data.name,
primaryIp: hetznerServerArg.data.public_net.ipv4.ip,
provider: 'hetzner',
location: hetznerServerArg.data.datacenter.name,
specs: {
cpuModel: hetznerServerArg.data.server_type.cpu_type,
cpuCores: hetznerServerArg.data.server_type.cores,
memoryGB: hetznerServerArg.data.server_type.memory,
storageGB: hetznerServerArg.data.server_type.disk,
storageType: 'nvme',
},
powerState: hetznerServerArg.data.status === 'running' ? 'on' : 'off',
osInfo: {
name: 'Debian',
version: '12',
},
assignedNodeIds: [],
providerMetadata: {
hetznerServerId: hetznerServerArg.data.id,
hetznerServerName: hetznerServerArg.data.name,
},
};
Object.assign(newBareMetal, { data });
await newBareMetal.save();
return newBareMetal;
}
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IBareMetal['data'];
constructor() {
super();
}
public async assignNode(nodeId: string) {
if (!this.data.assignedNodeIds.includes(nodeId)) {
this.data.assignedNodeIds.push(nodeId);
await this.save();
}
}
public async removeNode(nodeId: string) {
this.data.assignedNodeIds = this.data.assignedNodeIds.filter(id => id !== nodeId);
await this.save();
}
public async updatePowerState(state: 'on' | 'off' | 'unknown') {
this.data.powerState = state;
await this.save();
}
public async powerOn(): Promise<boolean> {
// TODO: Implement IPMI power on
if (this.data.ipmiAddress && this.data.ipmiCredentials) {
// Implement IPMI power on command
console.log(`Powering on BareMetal ${this.id} via IPMI`);
await this.updatePowerState('on');
return true;
}
return false;
}
public async powerOff(): Promise<boolean> {
// TODO: Implement IPMI power off
if (this.data.ipmiAddress && this.data.ipmiCredentials) {
// Implement IPMI power off command
console.log(`Powering off BareMetal ${this.id} via IPMI`);
await this.updatePowerState('off');
return true;
}
return false;
}
public async reset(): Promise<boolean> {
// TODO: Implement IPMI reset
if (this.data.ipmiAddress && this.data.ipmiCredentials) {
// Implement IPMI reset command
console.log(`Resetting BareMetal ${this.id} via IPMI`);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,176 @@
import * as plugins from '../plugins.js';
import { Cloudly } from '../classes.cloudly.js';
import { BareMetal } from './classes.baremetal.js';
import { logger } from '../logger.js';
export class CloudlyBaremetalManager {
public cloudlyRef: Cloudly;
public typedRouter = new plugins.typedrequest.TypedRouter();
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CBareMetal = plugins.smartdata.setDefaultManagerForDoc(this, BareMetal);
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedRouter);
// API endpoint to get baremetal servers
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.baremetal.IRequest_Any_Cloudly_GetBaremetalServers>(
'getBaremetalServers',
async (requestData) => {
const baremetals = await this.getAllBaremetals();
return {
baremetals: await Promise.all(
baremetals.map((baremetal) => baremetal.createSavableObject())
),
};
},
),
);
// API endpoint to control baremetal via IPMI
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.baremetal.IRequest_Any_Cloudly_ControlBaremetal>(
'controlBaremetal',
async (requestData) => {
const baremetal = await this.CBareMetal.getInstance({
id: requestData.baremetalId,
});
if (!baremetal) {
return {
success: false,
message: 'BareMetal not found',
};
}
let success = false;
switch (requestData.action) {
case 'powerOn':
success = await baremetal.powerOn();
break;
case 'powerOff':
success = await baremetal.powerOff();
break;
case 'reset':
success = await baremetal.reset();
break;
}
return {
success,
message: success ? `Action ${requestData.action} completed` : `Action ${requestData.action} failed`,
};
},
),
);
}
public async start() {
const hetznerToken = await this.cloudlyRef.settingsManager.getSetting('hetznerToken');
if (hetznerToken) {
this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(hetznerToken);
}
logger.log('info', 'BareMetal manager started');
}
public async stop() {
logger.log('info', 'BareMetal manager stopped');
}
/**
* Get all baremetal servers
*/
public async getAllBaremetals(): Promise<BareMetal[]> {
const baremetals = await this.CBareMetal.getInstances({});
return baremetals;
}
/**
* Get baremetal by ID
*/
public async getBaremetalById(id: string): Promise<BareMetal | null> {
const baremetal = await this.CBareMetal.getInstance({
id,
});
return baremetal;
}
/**
* Get baremetals by provider
*/
public async getBaremetalsByProvider(provider: 'hetzner' | 'aws' | 'digitalocean' | 'onpremise'): Promise<BareMetal[]> {
const baremetals = await this.CBareMetal.getInstances({
data: {
provider,
},
});
return baremetals;
}
/**
* Create baremetal from Hetzner server
*/
public async createBaremetalFromHetznerServer(hetznerServer: plugins.hetznercloud.HetznerServer): Promise<BareMetal> {
// Check if baremetal already exists for this Hetzner server
const existingBaremetals = await this.CBareMetal.getInstances({});
for (const baremetal of existingBaremetals) {
if (baremetal.data.providerMetadata?.hetznerServerId === hetznerServer.data.id) {
logger.log('info', `BareMetal already exists for Hetzner server ${hetznerServer.data.id}`);
return baremetal;
}
}
// Create new baremetal
const newBaremetal = await BareMetal.createFromHetznerServer(hetznerServer);
logger.log('success', `Created new BareMetal ${newBaremetal.id} from Hetzner server ${hetznerServer.data.id}`);
return newBaremetal;
}
/**
* Sync baremetals with Hetzner
*/
public async syncWithHetzner() {
if (!this.hetznerAccount) {
logger.log('warn', 'Cannot sync with Hetzner - no account configured');
return;
}
const hetznerServers = await this.hetznerAccount.getServers();
for (const hetznerServer of hetznerServers) {
await this.createBaremetalFromHetznerServer(hetznerServer);
}
logger.log('success', `Synced ${hetznerServers.length} servers from Hetzner`);
}
/**
* Provision a new baremetal server
*/
public async provisionBaremetal(options: {
provider: 'hetzner' | 'aws' | 'digitalocean';
location: any; // TODO: Import proper type from hetznercloud when available
type: any; // TODO: Import proper type from hetznercloud when available
}): Promise<BareMetal> {
if (options.provider === 'hetzner' && this.hetznerAccount) {
const hetznerServer = await this.hetznerAccount.createServer({
name: plugins.smartunique.uniSimple('baremetal'),
location: options.location,
type: options.type,
});
const baremetal = await this.createBaremetalFromHetznerServer(hetznerServer);
return baremetal;
}
throw new Error(`Provider ${options.provider} not supported or not configured`);
}
}