From 0545c8337b3ad2007218fe760320bd88dfe3cdb7 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 1 Oct 2024 22:57:49 -0400 Subject: [PATCH] [WIP] finish implementing cross pve-host disk transfers and mounting --- .../controllers/api/v1/Volumes.controller.ts | 21 +++++++- src/app/models/Node.model.ts | 2 +- src/app/services/Provisioner.service.ts | 52 +++++++++++++++++-- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/app/http/controllers/api/v1/Volumes.controller.ts b/src/app/http/controllers/api/v1/Volumes.controller.ts index a519533..8942f91 100644 --- a/src/app/http/controllers/api/v1/Volumes.controller.ts +++ b/src/app/http/controllers/api/v1/Volumes.controller.ts @@ -44,6 +44,8 @@ export class Volumes extends Controller { mountpoint = rawMountpoint } + console.log('Mount options', this.request.input('options')) + vol = await this.provisioner.mountVolume(vol, mountpoint) return vol.toAPI() } @@ -54,7 +56,24 @@ export class Volumes extends Controller { } public async transfer(vol: Volume, toNode: Node) { - vol = await this.provisioner.transferVolume(vol, toNode) + const fromNode = await vol.getNode() + + if ( fromNode.pveId === toNode.pveId ) { + // The volume is already attached to the target node, so we're done + } else if ( fromNode.pveHost === toNode.pveHost ) { + // If the from/to nodes are on the same physical host, just transfer the volume directly + vol = await this.provisioner.transferVolume(vol, toNode) + } else { + // If the nodes are on different physical hosts, we need to create a temporary container + // on shared storage to attach the volume to. We'll then migrate that container to the + // target physical host. + let carrier = await this.provisioner.provisionCarrierContainer(fromNode) + vol = await this.provisioner.transferVolume(vol, carrier) + carrier = await this.provisioner.migrateNode(carrier, toNode.pveHost) + vol = await this.provisioner.transferVolume(vol, toNode) + await this.provisioner.unprovisionCarrierContainer(carrier) + } + return vol.toAPI() } } diff --git a/src/app/models/Node.model.ts b/src/app/models/Node.model.ts index 7e55461..79b53a2 100644 --- a/src/app/models/Node.model.ts +++ b/src/app/models/Node.model.ts @@ -55,7 +55,7 @@ export class ConfigLines extends Collection { @Injectable() export class Node extends Model { protected static table = 'p5x_nodes' - protected static key = 'node_id' + protected static key = 'id' public static async getMaster(): Promise { const master = await Node.query() diff --git a/src/app/services/Provisioner.service.ts b/src/app/services/Provisioner.service.ts index 6d9569b..bce7df1 100644 --- a/src/app/services/Provisioner.service.ts +++ b/src/app/services/Provisioner.service.ts @@ -208,14 +208,14 @@ export class Provisioner { return volume } - public async provisionCarrierContainer(node: Node): Promise { + public async provisionCarrierContainer(node: Node): Promise { // Get the Proxmox API! const proxmox = await this.getApi() const nodeName = node.pveHost.split('/')[1] const pveHost = proxmox.nodes.$(nodeName) // Ensure the empty filesystem template exists - const host = await node.getHost() // fixme this is wrong -- need pve host + const host = await this.getPVEHost(node.unqualifiedPVEHost()) const fs = await host.getFilesystem() const stat = await fs.stat({ storePath: '/var/lib/vz/template/cache/p5x-empty.tar.xz' }) if ( !stat.exists ) { @@ -239,7 +239,7 @@ export class Provisioner { cores: 1, description: 'Temporary container managed by P5x', hostname: name, - memory: 10, // in MB + memory: 16, // in MB start: false, storage: await Setting.loadOneRequired('pveStoragePool'), tags: 'p5x', @@ -249,7 +249,30 @@ export class Provisioner { await this.waitForNodeTask(nodeName, createUPID) this.logging.info(`Created carrier container: ${name}`) - return name + const carrierNode = this.container.makeNew(Node) + carrierNode.pveId = carrierVMID + carrierNode.hostname = name + carrierNode.assignedIp = carrierIP + carrierNode.assignedSubnet = ipRange.subnet + carrierNode.pveHost = node.pveHost + await carrierNode.save() + return carrierNode + } + + public async unprovisionCarrierContainer(node: Node): Promise { + this.logging.info(`Deleting carrier container ${node.pveId}`) + const api = await this.getApi() + const upid = await api + .nodes.$(node.unqualifiedPVEHost()) + .lxc.$(node.pveId) + .$delete({ + purge: true, + 'destroy-unreferenced-disks': true, + }) + + await this.waitForNodeTask(node.unqualifiedPVEHost(), upid) + await node.delete() + this.logging.success(`Deleted carrier container ${node.pveId}`) } public async provisionNode(group: HostGroup): Promise { @@ -615,6 +638,27 @@ export class Provisioner { return vol } + public async migrateNode(node: Node, qualifiedPveHost: string): Promise { + this.logging.info(`Migrating node ${node.pveId} from ${node.pveHost} to ${qualifiedPveHost}`) + const api = await this.getApi() + const originalUnqualifiedPveHost = node.unqualifiedPVEHost() + + node.pveHost = qualifiedPveHost + const upid = await api + .nodes.$(originalUnqualifiedPveHost) + .lxc.$(node.pveId) + .migrate.$post({ + target: node.unqualifiedPVEHost(), + }) + + this.logging.info(`Waiting for migrate task: ${upid}`) + await this.waitForNodeTask(originalUnqualifiedPveHost, upid) + + await node.save() + this.logging.success(`Migrated node ${node.pveId} from ${node.pveHost} to ${qualifiedPveHost}`) + return node + } + public async getPVEHost(pveHost: string): Promise { const api = await this.getApi() const ifaces = await api.nodes.$(pveHost).network.$get()