You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

136 lines
5.6 KiB

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<void> {
// 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)
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])
},
)
})
}
}