diff --git a/lnvps_db/src/lib.rs b/lnvps_db/src/lib.rs index cc1f704..12b6906 100644 --- a/lnvps_db/src/lib.rs +++ b/lnvps_db/src/lib.rs @@ -93,6 +93,9 @@ pub trait LNVpsDb: Sync + Send { /// Delete a VM by id async fn delete_vm(&self, vm_id: u64) -> Result<()>; + /// Update a VM + async fn update_vm(&self, vm: &Vm) -> Result<()>; + /// List VM ip assignments async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result; diff --git a/lnvps_db/src/mysql.rs b/lnvps_db/src/mysql.rs index e1affd4..86b6235 100644 --- a/lnvps_db/src/mysql.rs +++ b/lnvps_db/src/mysql.rs @@ -31,10 +31,11 @@ impl LNVpsDb for LNVpsDbMysql { } async fn upsert_user(&self, pubkey: &[u8; 32]) -> Result { - let res = sqlx::query("insert ignore into users(pubkey,contact_nip17) values(?,1) returning id") - .bind(pubkey.as_slice()) - .fetch_optional(&self.db) - .await?; + let res = + sqlx::query("insert ignore into users(pubkey,contact_nip17) values(?,1) returning id") + .bind(pubkey.as_slice()) + .fetch_optional(&self.db) + .await?; match res { None => sqlx::query("select id from users where pubkey = ?") .bind(pubkey.as_slice()) @@ -249,6 +250,23 @@ impl LNVpsDb for LNVpsDbMysql { Ok(()) } + async fn update_vm(&self, vm: &Vm) -> Result<()> { + sqlx::query("update vm set image_id=?,template_id=?,ssh_key_id=?,expires=?,cpu=?,memory=?,disk_size=?,disk_id=? where id=?") + .bind(vm.image_id) + .bind(vm.template_id) + .bind(vm.ssh_key_id) + .bind(vm.expires) + .bind(vm.cpu) + .bind(vm.memory) + .bind(vm.disk_size) + .bind(vm.disk_id) + .bind(vm.id) + .execute(&self.db) + .await + .map_err(Error::new)?; + Ok(()) + } + async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result { Ok(sqlx::query( "insert into vm_ip_assignment(vm_id,ip_range_id,ip) values(?, ?, ?) returning id", diff --git a/src/api.rs b/src/api.rs index c68cf90..e040ea3 100644 --- a/src/api.rs +++ b/src/api.rs @@ -30,7 +30,8 @@ pub fn routes() -> Vec { v1_start_vm, v1_stop_vm, v1_restart_vm, - v1_terminal_proxy + v1_terminal_proxy, + v1_patch_vm ] } @@ -71,6 +72,11 @@ struct ApiVmStatus { pub status: VmState, } +#[derive(Serialize, Deserialize)] +struct VMPatchRequest { + pub ssh_key_id: Option, +} + #[get("/api/v1/vm")] async fn v1_list_vms( auth: Nip98Auth, @@ -123,6 +129,35 @@ async fn v1_get_vm( }) } +#[patch("/api/v1/vm/", data = "", format = "json")] +async fn v1_patch_vm( + auth: Nip98Auth, + db: &State>, + provisioner: &State>, + id: u64, + data: Json, +) -> ApiResult<()> { + let pubkey = auth.event.pubkey.to_bytes(); + let uid = db.upsert_user(&pubkey).await?; + let mut vm = db.get_vm(id).await?; + if vm.user_id != uid { + return ApiData::err("VM doesnt belong to you"); + } + + if let Some(k) = data.ssh_key_id { + let ssh_key = db.get_user_ssh_key(k).await?; + if ssh_key.user_id != uid { + return ApiData::err("SSH key doesnt belong to you"); + } + vm.ssh_key_id = ssh_key.id; + } + + db.update_vm(&vm).await?; + provisioner.patch_vm(vm.id).await?; + + ApiData::ok(()) +} + #[get("/api/v1/image")] async fn v1_list_vm_images(db: &State>) -> ApiResult> { let vms = db.list_os_image().await?; diff --git a/src/provisioner/lnvps.rs b/src/provisioner/lnvps.rs index b948aa7..56bfbdf 100644 --- a/src/provisioner/lnvps.rs +++ b/src/provisioner/lnvps.rs @@ -1,8 +1,8 @@ use crate::exchange::{ExchangeRateCache, Ticker}; use crate::host::get_host_client; use crate::host::proxmox::{ - CreateVm, DownloadUrlRequest, ProxmoxClient, ResizeDiskRequest, StorageContent, VmBios, - VmConfig, + ConfigureVm, CreateVm, DownloadUrlRequest, ProxmoxClient, ResizeDiskRequest, StorageContent, + VmBios, VmConfig, }; use crate::provisioner::Provisioner; use crate::router::Router; @@ -72,6 +72,76 @@ impl LNVpsProvisioner { bail!("No image storage found"); } } + + async fn get_vm_config(&self, vm: &Vm) -> Result { + let ssh_key = self.db.get_user_ssh_key(vm.ssh_key_id).await?; + + let mut ips = self.db.list_vm_ip_assignments(vm.id).await?; + if ips.is_empty() { + ips = self.allocate_ips(vm.id).await?; + } + + // load ranges + for ip in &mut ips { + ip.hydrate_up(&self.db).await?; + } + + let mut ip_config = ips + .iter() + .map_while(|ip| { + if let Ok(net) = ip.ip.parse::() { + Some(match net { + IpNetwork::V4(addr) => { + format!( + "ip={},gw={}", + addr, + ip.ip_range.as_ref().map(|r| &r.gateway).unwrap() + ) + } + IpNetwork::V6(addr) => format!("ip6={}", addr), + }) + } else { + None + } + }) + .collect::>(); + ip_config.push("ip6=auto".to_string()); + + let mut net = vec![ + format!("virtio={}", vm.mac_address), + format!("bridge={}", self.config.bridge), + ]; + if let Some(t) = self.config.vlan { + net.push(format!("tag={}", t)); + } + + let drives = self.db.list_host_disks(vm.host_id).await?; + let drive = if let Some(d) = drives.iter().find(|d| d.enabled) { + d + } else { + bail!("No host drive found!") + }; + + Ok(VmConfig { + cpu: Some(self.config.cpu.clone()), + kvm: Some(self.config.kvm), + ip_config: Some(ip_config.join(",")), + machine: Some(self.config.machine.clone()), + net: Some(net.join(",")), + os_type: Some(self.config.os_type.clone()), + on_boot: Some(true), + bios: Some(VmBios::OVMF), + boot: Some("order=scsi0".to_string()), + cores: Some(vm.cpu as i32), + memory: Some((vm.memory / 1024 / 1024).to_string()), + scsi_hw: Some("virtio-scsi-pci".to_string()), + serial_0: Some("socket".to_string()), + scsi_1: Some(format!("{}:cloudinit", &drive.name)), + ssh_keys: Some(urlencoding::encode(&ssh_key.key_data).to_string()), + efi_disk_0: Some(format!("{}:0,efitype=4m", &drive.name)), + ..Default::default() + }) + } } #[async_trait] @@ -302,55 +372,6 @@ impl Provisioner for LNVpsProvisioner { let vm = self.db.get_vm(vm_id).await?; let host = self.db.get_host(vm.host_id).await?; let client = get_host_client(&host)?; - - let mut ips = self.db.list_vm_ip_assignments(vm.id).await?; - if ips.is_empty() { - ips = self.allocate_ips(vm.id).await?; - } - - // load ranges - for ip in &mut ips { - ip.hydrate_up(&self.db).await?; - } - - let mut ip_config = ips - .iter() - .map_while(|ip| { - if let Ok(net) = ip.ip.parse::() { - Some(match net { - IpNetwork::V4(addr) => { - format!( - "ip={},gw={}", - addr, - ip.ip_range.as_ref().map(|r| &r.gateway).unwrap() - ) - } - IpNetwork::V6(addr) => format!("ip6={}", addr), - }) - } else { - None - } - }) - .collect::>(); - ip_config.push("ip6=auto".to_string()); - - let drives = self.db.list_host_disks(vm.host_id).await?; - let drive = if let Some(d) = drives.iter().find(|d| d.enabled) { - d - } else { - bail!("No host drive found!") - }; - - let ssh_key = self.db.get_user_ssh_key(vm.ssh_key_id).await?; - - let mut net = vec![ - format!("virtio={}", vm.mac_address), - format!("bridge={}", self.config.bridge), - ]; - if let Some(t) = self.config.vlan { - net.push(format!("tag={}", t)); - } - let vm_id = 100 + vm.id as i32; // create VM @@ -358,25 +379,7 @@ impl Provisioner for LNVpsProvisioner { .create_vm(CreateVm { node: host.name.clone(), vm_id, - config: VmConfig { - on_boot: Some(true), - bios: Some(VmBios::OVMF), - boot: Some("order=scsi0".to_string()), - cores: Some(vm.cpu as i32), - cpu: Some(self.config.cpu.clone()), - kvm: Some(self.config.kvm), - ip_config: Some(ip_config.join(",")), - machine: Some(self.config.machine.clone()), - memory: Some((vm.memory / 1024 / 1024).to_string()), - net: Some(net.join(",")), - os_type: Some(self.config.os_type.clone()), - scsi_1: Some(format!("{}:cloudinit", &drive.name)), - scsi_hw: Some("virtio-scsi-pci".to_string()), - ssh_keys: Some(urlencoding::encode(&ssh_key.key_data).to_string()), - efi_disk_0: Some(format!("{}:0,efitype=4m", &drive.name)), - serial_0: Some("socket".to_string()), - ..Default::default() - }, + config: self.get_vm_config(&vm).await?, }) .await?; client.wait_for_task(&t_create).await?; @@ -394,6 +397,12 @@ impl Provisioner for LNVpsProvisioner { ) .await?; + let drives = self.db.list_host_disks(vm.host_id).await?; + let drive = if let Some(d) = drives.iter().find(|d| d.enabled) { + d + } else { + bail!("No host drive found!") + }; let cmd = format!( "/usr/sbin/qm set {} --scsi0 {}:0,import-from=/var/lib/vz/template/iso/{}", vm_id, @@ -515,4 +524,28 @@ impl Provisioner for LNVpsProvisioner { ws.send(Message::Text("1:86:24:".to_string())).await?; Ok(ws) } + + async fn patch_vm(&self, vm_id: u64) -> Result<()> { + let vm = self.db.get_vm(vm_id).await?; + let host = self.db.get_host(vm.host_id).await?; + let client = get_host_client(&host)?; + let host_vm_id = vm.id + 100; + + let t = client + .configure_vm(ConfigureVm { + node: host.name.clone(), + vm_id: host_vm_id as i32, + current: None, + snapshot: None, + config: VmConfig { + scsi_0: None, + scsi_1: None, + efi_disk_0: None, + ..self.get_vm_config(&vm).await? + }, + }) + .await?; + client.wait_for_task(&t).await?; + Ok(()) + } } diff --git a/src/provisioner/mod.rs b/src/provisioner/mod.rs index 5404dd8..d2a0bd8 100644 --- a/src/provisioner/mod.rs +++ b/src/provisioner/mod.rs @@ -50,4 +50,7 @@ pub trait Provisioner: Send + Sync { &self, vm_id: u64, ) -> Result>>; + + /// Re-Configure VM + async fn patch_vm(&self, vm_id: u64) -> Result<()>; }