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