closes #10
This commit is contained in:
@ -48,6 +48,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
v1_start_vm,
|
v1_start_vm,
|
||||||
v1_stop_vm,
|
v1_stop_vm,
|
||||||
v1_restart_vm,
|
v1_restart_vm,
|
||||||
|
v1_reinstall_vm,
|
||||||
v1_patch_vm,
|
v1_patch_vm,
|
||||||
v1_time_series,
|
v1_time_series,
|
||||||
v1_custom_template_calc,
|
v1_custom_template_calc,
|
||||||
@ -592,6 +593,32 @@ async fn v1_restart_vm(
|
|||||||
ApiData::ok(())
|
ApiData::ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Re-install a VM
|
||||||
|
#[openapi(tag = "VM")]
|
||||||
|
#[patch("/api/v1/vm/<id>/re-install")]
|
||||||
|
async fn v1_reinstall_vm(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
settings: &State<Settings>,
|
||||||
|
worker: &State<UnboundedSender<WorkJob>>,
|
||||||
|
id: u64,
|
||||||
|
) -> ApiResult<()> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
let vm = db.get_vm(id).await?;
|
||||||
|
if uid != vm.user_id {
|
||||||
|
return ApiData::err("VM does not belong to you");
|
||||||
|
}
|
||||||
|
|
||||||
|
let host = db.get_host(vm.host_id).await?;
|
||||||
|
let client = get_host_client(&host, &settings.provisioner)?;
|
||||||
|
let info = FullVmInfo::load(vm.id, (*db).clone()).await?;
|
||||||
|
client.reinstall_vm(&info).await?;
|
||||||
|
|
||||||
|
worker.send(WorkJob::CheckVm { vm_id: id })?;
|
||||||
|
ApiData::ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[openapi(tag = "VM")]
|
#[openapi(tag = "VM")]
|
||||||
#[get("/api/v1/vm/<id>/time-series")]
|
#[get("/api/v1/vm/<id>/time-series")]
|
||||||
async fn v1_time_series(
|
async fn v1_time_series(
|
||||||
|
@ -20,6 +20,7 @@ use std::net::{IpAddr, SocketAddr};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use rocket::http::Method;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(about, version, author)]
|
#[clap(about, version, author)]
|
||||||
@ -159,7 +160,6 @@ async fn main() -> Result<(), Error> {
|
|||||||
config.port = ip.port();
|
config.port = ip.port();
|
||||||
|
|
||||||
if let Err(e) = rocket::Rocket::custom(config)
|
if let Err(e) = rocket::Rocket::custom(config)
|
||||||
.attach(CORS)
|
|
||||||
.manage(db.clone())
|
.manage(db.clone())
|
||||||
.manage(provisioner.clone())
|
.manage(provisioner.clone())
|
||||||
.manage(status.clone())
|
.manage(status.clone())
|
||||||
@ -174,6 +174,15 @@ async fn main() -> Result<(), Error> {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.attach(CORS)
|
||||||
|
.mount("/", vec![
|
||||||
|
rocket::Route::ranked(
|
||||||
|
isize::MAX,
|
||||||
|
Method::Options,
|
||||||
|
"/<catch_all_options_route..>",
|
||||||
|
CORS,
|
||||||
|
)
|
||||||
|
])
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
20
src/cors.rs
20
src/cors.rs
@ -1,8 +1,9 @@
|
|||||||
use rocket::fairing::{Fairing, Info, Kind};
|
use rocket::fairing::{Fairing, Info, Kind};
|
||||||
use rocket::http::{Header, Method, Status};
|
use rocket::http::Header;
|
||||||
use rocket::{Request, Response};
|
use rocket::route::{Handler, Outcome};
|
||||||
use std::io::Cursor;
|
use rocket::{Data, Request, Response};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct CORS;
|
pub struct CORS;
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
@ -14,7 +15,7 @@ impl Fairing for CORS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_response<'r>(&self, req: &'r Request<'_>, response: &mut Response<'r>) {
|
async fn on_response<'r>(&self, _req: &'r Request<'_>, response: &mut Response<'r>) {
|
||||||
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
|
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
|
||||||
response.set_header(Header::new(
|
response.set_header(Header::new(
|
||||||
"Access-Control-Allow-Methods",
|
"Access-Control-Allow-Methods",
|
||||||
@ -22,11 +23,12 @@ impl Fairing for CORS {
|
|||||||
));
|
));
|
||||||
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
|
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
|
||||||
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
|
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// force status 200 for options requests
|
#[rocket::async_trait]
|
||||||
if req.method() == Method::Options {
|
impl Handler for CORS {
|
||||||
response.set_status(Status::Ok);
|
async fn handle<'r>(&self, _request: &'r Request<'_>, _data: Data<'r>) -> Outcome<'r> {
|
||||||
response.set_sized_body(None, Cursor::new(""))
|
Outcome::Success(Response::new())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,9 @@ pub trait VmHostClient: Send + Sync {
|
|||||||
/// Spawn a VM
|
/// Spawn a VM
|
||||||
async fn create_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
async fn create_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
||||||
|
|
||||||
|
/// Re-install a vm OS
|
||||||
|
async fn reinstall_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
||||||
|
|
||||||
/// Get the running status of a VM
|
/// Get the running status of a VM
|
||||||
async fn get_vm_state(&self, vm: &Vm) -> Result<VmState>;
|
async fn get_vm_state(&self, vm: &Vm) -> Result<VmState>;
|
||||||
|
|
||||||
|
@ -365,6 +365,30 @@ impl ProxmoxClient {
|
|||||||
node: node.to_string(),
|
node: node.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete disks from VM
|
||||||
|
pub async fn unlink_disk(
|
||||||
|
&self,
|
||||||
|
node: &str,
|
||||||
|
vm: ProxmoxVmId,
|
||||||
|
disks: Vec<String>,
|
||||||
|
force: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.api
|
||||||
|
.req_status(
|
||||||
|
Method::PUT,
|
||||||
|
&format!(
|
||||||
|
"/api2/json/nodes/{}/qemu/{}/unlink?idlist={}&force={}",
|
||||||
|
node,
|
||||||
|
vm,
|
||||||
|
disks.join(","),
|
||||||
|
if force { "1" } else { "0" }
|
||||||
|
),
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProxmoxClient {
|
impl ProxmoxClient {
|
||||||
@ -418,7 +442,7 @@ impl ProxmoxClient {
|
|||||||
bios: Some(VmBios::OVMF),
|
bios: Some(VmBios::OVMF),
|
||||||
boot: Some("order=scsi0".to_string()),
|
boot: Some("order=scsi0".to_string()),
|
||||||
cores: Some(vm_resources.cpu as i32),
|
cores: Some(vm_resources.cpu as i32),
|
||||||
memory: Some((vm_resources.memory / crate::GB).to_string()),
|
memory: Some((vm_resources.memory / crate::MB).to_string()),
|
||||||
scsi_hw: Some("virtio-scsi-pci".to_string()),
|
scsi_hw: Some("virtio-scsi-pci".to_string()),
|
||||||
serial_0: Some("socket".to_string()),
|
serial_0: Some("socket".to_string()),
|
||||||
scsi_1: Some(format!("{}:cloudinit", &value.disk.name)),
|
scsi_1: Some(format!("{}:cloudinit", &value.disk.name)),
|
||||||
@ -427,7 +451,38 @@ impl ProxmoxClient {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Import main disk image from the template
|
||||||
|
async fn import_template_disk(&self, req: &FullVmInfo) -> Result<()> {
|
||||||
|
let vm_id = req.vm.id.into();
|
||||||
|
|
||||||
|
// import primary disk from image (scsi0)
|
||||||
|
self.import_disk_image(ImportDiskImageRequest {
|
||||||
|
vm_id,
|
||||||
|
node: self.node.clone(),
|
||||||
|
storage: req.disk.name.clone(),
|
||||||
|
disk: "scsi0".to_string(),
|
||||||
|
image: req.image.filename()?,
|
||||||
|
is_ssd: matches!(req.disk.kind, DiskType::SSD),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// resize disk to match template
|
||||||
|
let j_resize = self
|
||||||
|
.resize_disk(ResizeDiskRequest {
|
||||||
|
node: self.node.clone(),
|
||||||
|
vm_id,
|
||||||
|
disk: "scsi0".to_string(),
|
||||||
|
size: req.resources()?.disk_size.to_string(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
// TODO: rollback
|
||||||
|
self.wait_for_task(&j_resize).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl VmHostClient for ProxmoxClient {
|
impl VmHostClient for ProxmoxClient {
|
||||||
async fn download_os_image(&self, image: &VmOsImage) -> Result<()> {
|
async fn download_os_image(&self, image: &VmOsImage) -> Result<()> {
|
||||||
@ -499,28 +554,35 @@ impl VmHostClient for ProxmoxClient {
|
|||||||
.await?;
|
.await?;
|
||||||
self.wait_for_task(&t_create).await?;
|
self.wait_for_task(&t_create).await?;
|
||||||
|
|
||||||
// import primary disk from image (scsi0)
|
// import template image
|
||||||
self.import_disk_image(ImportDiskImageRequest {
|
self.import_template_disk(&req).await?;
|
||||||
vm_id,
|
|
||||||
node: self.node.clone(),
|
// try start, otherwise ignore error (maybe its already running)
|
||||||
storage: req.disk.name.clone(),
|
if let Ok(j_start) = self.start_vm(&self.node, vm_id).await {
|
||||||
disk: "scsi0".to_string(),
|
if let Err(e) = self.wait_for_task(&j_start).await {
|
||||||
image: req.image.filename()?,
|
warn!("Failed to start vm: {}", e);
|
||||||
is_ssd: matches!(req.disk.kind, DiskType::SSD),
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reinstall_vm(&self, req: &FullVmInfo) -> Result<()> {
|
||||||
|
let vm_id = req.vm.id.into();
|
||||||
|
|
||||||
|
// try stop, otherwise ignore error (maybe its already running)
|
||||||
|
if let Ok(j_stop) = self.stop_vm(&self.node, vm_id).await {
|
||||||
|
if let Err(e) = self.wait_for_task(&j_stop).await {
|
||||||
|
warn!("Failed to stop vm: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlink the existing main disk
|
||||||
|
self.unlink_disk(&self.node, vm_id, vec!["scsi0".to_string()], true)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// resize disk to match template
|
// import disk from template again
|
||||||
let j_resize = self
|
self.import_template_disk(&req).await?;
|
||||||
.resize_disk(ResizeDiskRequest {
|
|
||||||
node: self.node.clone(),
|
|
||||||
vm_id,
|
|
||||||
disk: "scsi0".to_string(),
|
|
||||||
size: req.resources()?.disk_size.to_string(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
// TODO: rollback
|
|
||||||
self.wait_for_task(&j_resize).await?;
|
|
||||||
|
|
||||||
// try start, otherwise ignore error (maybe its already running)
|
// try start, otherwise ignore error (maybe its already running)
|
||||||
if let Ok(j_start) = self.start_vm(&self.node, vm_id).await {
|
if let Ok(j_start) = self.start_vm(&self.node, vm_id).await {
|
||||||
@ -1092,7 +1154,10 @@ mod tests {
|
|||||||
assert_eq!(vm.cores, Some(template.cpu as i32));
|
assert_eq!(vm.cores, Some(template.cpu as i32));
|
||||||
assert_eq!(vm.memory, Some((template.memory / MB).to_string()));
|
assert_eq!(vm.memory, Some((template.memory / MB).to_string()));
|
||||||
assert_eq!(vm.on_boot, Some(true));
|
assert_eq!(vm.on_boot, Some(true));
|
||||||
assert_eq!(vm.ip_config, Some("ip=192.168.1.2/16,gw=192.168.1.1,ip6=auto".to_string()));
|
assert_eq!(
|
||||||
|
vm.ip_config,
|
||||||
|
Some("ip=192.168.1.2/16,gw=192.168.1.1,ip6=auto".to_string())
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user