feat: patch vm

This commit is contained in:
kieran 2024-12-29 19:16:02 +00:00
parent e51bd2722e
commit 265d91dd83
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
5 changed files with 167 additions and 75 deletions

View File

@ -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<u64>;

View File

@ -31,7 +31,8 @@ impl LNVpsDb for LNVpsDbMysql {
}
async fn upsert_user(&self, pubkey: &[u8; 32]) -> Result<u64> {
let res = sqlx::query("insert ignore into users(pubkey,contact_nip17) values(?,1) returning id")
let res =
sqlx::query("insert ignore into users(pubkey,contact_nip17) values(?,1) returning id")
.bind(pubkey.as_slice())
.fetch_optional(&self.db)
.await?;
@ -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<u64> {
Ok(sqlx::query(
"insert into vm_ip_assignment(vm_id,ip_range_id,ip) values(?, ?, ?) returning id",

View File

@ -30,7 +30,8 @@ pub fn routes() -> Vec<Route> {
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<u64>,
}
#[get("/api/v1/vm")]
async fn v1_list_vms(
auth: Nip98Auth,
@ -123,6 +129,35 @@ async fn v1_get_vm(
})
}
#[patch("/api/v1/vm/<id>", data = "<data>", format = "json")]
async fn v1_patch_vm(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
provisioner: &State<Box<dyn Provisioner>>,
id: u64,
data: Json<VMPatchRequest>,
) -> 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<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmOsImage>> {
let vms = db.list_os_image().await?;

View File

@ -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<VmConfig> {
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::<IpNetwork>() {
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::<Vec<_>>();
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::<IpNetwork>() {
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::<Vec<_>>();
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(())
}
}

View File

@ -50,4 +50,7 @@ pub trait Provisioner: Send + Sync {
&self,
vm_id: u64,
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>>;
/// Re-Configure VM
async fn patch_vm(&self, vm_id: u64) -> Result<()>;
}