feat: patch vm
This commit is contained in:
parent
e51bd2722e
commit
265d91dd83
@ -93,6 +93,9 @@ pub trait LNVpsDb: Sync + Send {
|
|||||||
/// Delete a VM by id
|
/// Delete a VM by id
|
||||||
async fn delete_vm(&self, vm_id: u64) -> Result<()>;
|
async fn delete_vm(&self, vm_id: u64) -> Result<()>;
|
||||||
|
|
||||||
|
/// Update a VM
|
||||||
|
async fn update_vm(&self, vm: &Vm) -> Result<()>;
|
||||||
|
|
||||||
/// List VM ip assignments
|
/// List VM ip assignments
|
||||||
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64>;
|
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64>;
|
||||||
|
|
||||||
|
@ -31,7 +31,8 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn upsert_user(&self, pubkey: &[u8; 32]) -> Result<u64> {
|
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())
|
.bind(pubkey.as_slice())
|
||||||
.fetch_optional(&self.db)
|
.fetch_optional(&self.db)
|
||||||
.await?;
|
.await?;
|
||||||
@ -249,6 +250,23 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
Ok(())
|
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> {
|
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64> {
|
||||||
Ok(sqlx::query(
|
Ok(sqlx::query(
|
||||||
"insert into vm_ip_assignment(vm_id,ip_range_id,ip) values(?, ?, ?) returning id",
|
"insert into vm_ip_assignment(vm_id,ip_range_id,ip) values(?, ?, ?) returning id",
|
||||||
|
37
src/api.rs
37
src/api.rs
@ -30,7 +30,8 @@ pub fn routes() -> Vec<Route> {
|
|||||||
v1_start_vm,
|
v1_start_vm,
|
||||||
v1_stop_vm,
|
v1_stop_vm,
|
||||||
v1_restart_vm,
|
v1_restart_vm,
|
||||||
v1_terminal_proxy
|
v1_terminal_proxy,
|
||||||
|
v1_patch_vm
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +72,11 @@ struct ApiVmStatus {
|
|||||||
pub status: VmState,
|
pub status: VmState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct VMPatchRequest {
|
||||||
|
pub ssh_key_id: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/api/v1/vm")]
|
#[get("/api/v1/vm")]
|
||||||
async fn v1_list_vms(
|
async fn v1_list_vms(
|
||||||
auth: Nip98Auth,
|
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")]
|
#[get("/api/v1/image")]
|
||||||
async fn v1_list_vm_images(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmOsImage>> {
|
async fn v1_list_vm_images(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmOsImage>> {
|
||||||
let vms = db.list_os_image().await?;
|
let vms = db.list_os_image().await?;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use crate::exchange::{ExchangeRateCache, Ticker};
|
use crate::exchange::{ExchangeRateCache, Ticker};
|
||||||
use crate::host::get_host_client;
|
use crate::host::get_host_client;
|
||||||
use crate::host::proxmox::{
|
use crate::host::proxmox::{
|
||||||
CreateVm, DownloadUrlRequest, ProxmoxClient, ResizeDiskRequest, StorageContent, VmBios,
|
ConfigureVm, CreateVm, DownloadUrlRequest, ProxmoxClient, ResizeDiskRequest, StorageContent,
|
||||||
VmConfig,
|
VmBios, VmConfig,
|
||||||
};
|
};
|
||||||
use crate::provisioner::Provisioner;
|
use crate::provisioner::Provisioner;
|
||||||
use crate::router::Router;
|
use crate::router::Router;
|
||||||
@ -72,6 +72,76 @@ impl LNVpsProvisioner {
|
|||||||
bail!("No image storage found");
|
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]
|
#[async_trait]
|
||||||
@ -302,55 +372,6 @@ impl Provisioner for LNVpsProvisioner {
|
|||||||
let vm = self.db.get_vm(vm_id).await?;
|
let vm = self.db.get_vm(vm_id).await?;
|
||||||
let host = self.db.get_host(vm.host_id).await?;
|
let host = self.db.get_host(vm.host_id).await?;
|
||||||
let client = get_host_client(&host)?;
|
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;
|
let vm_id = 100 + vm.id as i32;
|
||||||
|
|
||||||
// create VM
|
// create VM
|
||||||
@ -358,25 +379,7 @@ impl Provisioner for LNVpsProvisioner {
|
|||||||
.create_vm(CreateVm {
|
.create_vm(CreateVm {
|
||||||
node: host.name.clone(),
|
node: host.name.clone(),
|
||||||
vm_id,
|
vm_id,
|
||||||
config: VmConfig {
|
config: self.get_vm_config(&vm).await?,
|
||||||
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()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
client.wait_for_task(&t_create).await?;
|
client.wait_for_task(&t_create).await?;
|
||||||
@ -394,6 +397,12 @@ impl Provisioner for LNVpsProvisioner {
|
|||||||
)
|
)
|
||||||
.await?;
|
.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!(
|
let cmd = format!(
|
||||||
"/usr/sbin/qm set {} --scsi0 {}:0,import-from=/var/lib/vz/template/iso/{}",
|
"/usr/sbin/qm set {} --scsi0 {}:0,import-from=/var/lib/vz/template/iso/{}",
|
||||||
vm_id,
|
vm_id,
|
||||||
@ -515,4 +524,28 @@ impl Provisioner for LNVpsProvisioner {
|
|||||||
ws.send(Message::Text("1:86:24:".to_string())).await?;
|
ws.send(Message::Text("1:86:24:".to_string())).await?;
|
||||||
Ok(ws)
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,4 +50,7 @@ pub trait Provisioner: Send + Sync {
|
|||||||
&self,
|
&self,
|
||||||
vm_id: u64,
|
vm_id: u64,
|
||||||
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>>;
|
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>>;
|
||||||
|
|
||||||
|
/// Re-Configure VM
|
||||||
|
async fn patch_vm(&self, vm_id: u64) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user