feat(settings): Add runtime settings management, node & baremetal managers, and settings UI
This commit is contained in:
104
ts/manager.baremetal/classes.baremetal.ts
Normal file
104
ts/manager.baremetal/classes.baremetal.ts
Normal 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;
|
||||
}
|
||||
}
|
176
ts/manager.baremetal/classes.baremetalmanager.ts
Normal file
176
ts/manager.baremetal/classes.baremetalmanager.ts
Normal 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`);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user