import {Directive, Inject, Injectable, OptionDefinition, uuid4} from '@extollo/lib' import { IpRange } from "../../models/IpRange.model"; import { Setting } from "../../models/Setting.model"; import { Provisioner } from "../../services/Provisioner.service"; import { IpAddress, isIpAddress, isSubnet, Subnet } from "../../types"; import * as crypto from 'crypto' @Injectable() export class StandupDirective extends Directive { @Inject() protected readonly provisioner!: Provisioner getKeywords(): string | string[] { return 'standup' } getDescription(): string { return 'perform initial configuration and setup for the control server' } getOptions(): OptionDefinition[] { return [ '--pve-master-node {name} | the name of the master node in the PVE cluster', '--pve-api-host {host} | the hostname or IP of a PVE cluster node', // '--pve-token-id {id} | the fully-qualified ID of a PVE API token', // '--pve-token-secret {secret} | secret for the PVE API token', '--pve-root-password {password} | password for the root@pam PVE user', '--ip-pool-start {start} | the starting address in the P5X IP pool', '--ip-pool-end {end} | the final address in the P5X IP pool', '--ip-pool-subnet {subnet} | the numeric subnet of the P5X IP pool (e.g. 24)', '--ip-pool-gateway {gateway} | the address of the gateway for the P5X IP pool', '--storage-pool {name} | the name of the PVE shared storage pool', '--dns-domain {name} | the DNS domain name used for K8s nodes', '--node-network-bridge {name} | the name of the PVE network bridge to which nodes will join', '--node-cpu-cores {cores} | the number of cores to provision each K8s node with', '--node-ram-mib {ram} | the amount of ram (in MiB) to provision each K8s node with', ] } public async handle(argv: string[]): Promise { // Configure the settings: await Setting.set('pveMasterNode', `${this.getOptionOrFail('pve-master-node')}`) await Setting.set('pveApiHost', `${this.getOptionOrFail('pve-api-host')}`) // await Setting.set('pveTokenId', `${this.getOptionOrFail('pve-token-id')}`) // await Setting.set('pveTokenSecret', `${this.getOptionOrFail('pve-token-secret')}`) await Setting.set('pveRootPassword', `${this.getOptionOrFail('pve-root-password')}`) await Setting.set('pveStoragePool', `${this.getOptionOrFail('storage-pool')}`) await Setting.set('dnsDomain', `${this.getOptionOrFail('dns-domain')}`) await Setting.set('nodeNetworkBridge', `${this.getOptionOrFail('node-network-bridge')}`) await Setting.set('nodeCpus', this.getIntOptionOrFail('node-cpu-cores')) await Setting.set('nodeRamMib', this.getIntOptionOrFail('node-ram-mib')) await Setting.set('rootPassword', uuid4()) const [sshPublicKey, sshPrivateKey] = await this.getSSHKeyPair() await Setting.set('sshPublicKey', sshPublicKey) await Setting.set('sshPrivateKey', sshPrivateKey) // Store the IP address pool: const ipRange = this.container().makeNew(IpRange) ipRange.name = 'Default address pool' ipRange.startIp = this.getIpOptionOrFail('ip-pool-start') ipRange.endIp = this.getIpOptionOrFail('ip-pool-end') ipRange.subnet = this.getSubnetOptionOrFail('ip-pool-subnet') ipRange.gatewayIp = this.getIpOptionOrFail('ip-pool-gateway') ipRange.save() // Provision the master node: const masterNode = await this.provisioner.provisionMasterNode() } protected getIntOptionOrFail(option: string): number { const val = parseInt(this.option(option, NaN), 10) if ( isNaN(val) ) { this.error(`Missing or malformed required integer option: --${option}`) process.exit(1) } return val } protected getSubnetOptionOrFail(option: string): Subnet { const val = parseInt(this.option(option, NaN), 10) if ( !isSubnet(val) ) { this.error(`Missing or malformed required numeric subnet: --${option}`) process.exit(1) } return val } protected getIpOptionOrFail(option: string): IpAddress { const val = `${this.option(option, '')}` if ( !isIpAddress(val) ) { this.error(`Missing or malformed required IP address: --${option}`) process.exit(1) } return val } protected getOptionOrFail(option: string): unknown { const val = this.option(option, undefined) if ( typeof val === 'undefined' ) { this.error(`Missing required option: --${option}`) process.exit(1) } return val } protected async getSSHKeyPair(): Promise<[string, string]> { return new Promise<[string, string]>((res, rej) => { crypto.generateKeyPair( 'rsa', { modulusLength: 4096, publicKeyEncoding: { type: 'spki', format: 'pem', }, privateKeyEncoding: { type: 'pkcs8', format: 'pem', cipher: 'aes-256-cbc', passphrase: '', }, }, (err, pubkey, privkey) => { if ( err ) { return rej(err) } res([pubkey, privkey]) }, ) }) } }