diff --git a/Cargo.toml b/Cargo.toml index de3a27e..8c2db7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ name = "lnvps" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "api" [dependencies] tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/dev_setup.sql b/dev_setup.sql new file mode 100644 index 0000000..1735a00 --- /dev/null +++ b/dev_setup.sql @@ -0,0 +1,4 @@ +insert ignore into vm_host(id,kind,name,ip,cpu,memory,enabled,api_token) values(1, 0, "lab", "https://185.18.221.8:8006", 4, 4096*1024, 1, "root@pam!tester=c82f8a57-f876-4ca4-8610-c086d8d9d51c"); +insert ignore into vm_host_disk(id,host_id,name,size,kind,interface,enabled) values(1,1,"local-lvm",1000*1000*1000*100, 0, 0, 1); +insert ignore into vm_os_image(id,name,enabled) values(1,"Ubuntu 24.04",1); +insert ignore into ip_range(id,cidr,enabled) values(1,"185.18.221.80/28",1); \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index b6b92ae..b65e96e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,6 +3,7 @@ volumes: services: db: image: mariadb + restart: unless-stopped environment: - "MARIADB_ROOT_PASSWORD=root" - "MARIADB_DATABASE=lnvps" diff --git a/migrations/20241103155733_init.sql b/migrations/20241103155733_init.sql index 99dc027..04b6cb9 100644 --- a/migrations/20241103155733_init.sql +++ b/migrations/20241103155733_init.sql @@ -75,6 +75,7 @@ create table vm_payment expires timestamp not null, amount bigint unsigned not null, invoice varchar(2048) not null, + time_value integer unsigned not null, is_paid bit(1) not null, constraint fk_vm_payment_vm foreign key (vm_id) references vm (id) diff --git a/src/api.rs b/src/api.rs index 1b5c67b..57ef0d7 100644 --- a/src/api.rs +++ b/src/api.rs @@ -4,7 +4,7 @@ use crate::provisioner::Provisioner; use anyhow::Error; use rocket::serde::json::Json; use rocket::{get, routes, Responder, Route, State}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; pub fn routes() -> Vec { routes![v1_list_vms] diff --git a/src/main.rs b/src/bin/api.rs similarity index 52% rename from src/main.rs rename to src/bin/api.rs index 0132619..a4b33b2 100644 --- a/src/main.rs +++ b/src/bin/api.rs @@ -1,22 +1,16 @@ -extern crate core; - -use crate::cors::CORS; -use crate::provisioner::Provisioner; -use crate::settings::Settings; use anyhow::Error; use config::{Config, File}; +use lnvps::api; +use lnvps::cors::CORS; +use lnvps::provisioner::Provisioner; use log::error; -use rocket::routes; -use sqlx::MySqlPool; +use serde::{Deserialize, Serialize}; +use sqlx::{Executor, MySqlPool}; -mod api; -mod cors; -mod db; -mod nip98; -mod provisioner; -mod proxmox; -mod settings; -mod vm; +#[derive(Debug, Deserialize, Serialize)] +pub struct Settings { + pub db: String, +} #[rocket::main] async fn main() -> Result<(), Error> { @@ -28,8 +22,14 @@ async fn main() -> Result<(), Error> { .try_deserialize()?; let db = MySqlPool::connect(&config.db).await?; - sqlx::migrate!("./migrations").run(&db).await?; - let provisioner = Provisioner::new(db); + sqlx::migrate!().run(&db).await?; + let provisioner = Provisioner::new(db.clone()); + #[cfg(debug_assertions)] + { + let setup_script = include_str!("../../dev_setup.sql"); + db.execute(setup_script).await?; + provisioner.auto_discover().await?; + } if let Err(e) = rocket::build() .attach(CORS) diff --git a/src/db.rs b/src/db.rs index eea875b..8886f5c 100644 --- a/src/db.rs +++ b/src/db.rs @@ -13,13 +13,14 @@ pub struct User { pub created: DateTime, } -#[derive(Serialize)] +#[derive(Serialize, sqlx::Type, Clone, Debug)] +#[repr(u8)] /// The type of VM host pub enum VmHostKind { - Proxmox, + Proxmox = 0, } -#[derive(Serialize, FromRow)] +#[derive(Serialize, FromRow, Clone, Debug)] /// A VM host pub struct VmHost { pub id: u64, @@ -35,7 +36,7 @@ pub struct VmHost { pub api_token: String, } -#[derive(Serialize, FromRow)] +#[derive(Serialize, FromRow, Clone, Debug)] pub struct VmHostDisk { pub id: u64, pub host_id: u64, @@ -46,34 +47,36 @@ pub struct VmHostDisk { pub enabled: bool, } -#[derive(Serialize)] +#[derive(Serialize, sqlx::Type, Clone, Debug)] +#[repr(u8)] pub enum DiskType { - HDD, - SSD, + HDD = 0, + SSD = 1, } -#[derive(Serialize)] +#[derive(Serialize, sqlx::Type, Clone, Debug)] +#[repr(u8)] pub enum DiskInterface { - SATA, - SCSI, - PCIe, + SATA = 0, + SCSI = 1, + PCIe = 2, } -#[derive(Serialize, FromRow)] +#[derive(Serialize, FromRow, Clone, Debug)] pub struct VmOsImage { pub id: u64, pub name: String, pub enabled: bool, } -#[derive(Serialize, FromRow)] +#[derive(Serialize, FromRow, Clone, Debug)] pub struct IpRange { pub id: u64, pub cidr: String, pub enabled: bool, } -#[derive(Serialize, FromRow)] +#[derive(Serialize, FromRow, Clone, Debug)] pub struct Vm { /// Unique VM ID (Same in proxmox) pub id: u64, diff --git a/src/host/mod.rs b/src/host/mod.rs new file mode 100644 index 0000000..f1a8841 --- /dev/null +++ b/src/host/mod.rs @@ -0,0 +1,2 @@ +pub mod proxmox; +pub trait VmHostClient {} diff --git a/src/proxmox.rs b/src/host/proxmox.rs similarity index 73% rename from src/proxmox.rs rename to src/host/proxmox.rs index 200460f..ddd2c0f 100644 --- a/src/proxmox.rs +++ b/src/host/proxmox.rs @@ -1,43 +1,32 @@ use anyhow::Error; use reqwest::{Body, ClientBuilder, Url}; use serde::de::DeserializeOwned; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::Deserialize; use std::fmt::Debug; -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct ClientToken { - username: String, - realm: String, - password: String, -} - -pub struct Client { +pub struct ProxmoxClient { base: Url, - token: ClientToken, + token: String, client: reqwest::Client, } -impl Client { +impl ProxmoxClient { pub fn new(base: Url) -> Self { - let mut client = ClientBuilder::new() + let client = ClientBuilder::new() .danger_accept_invalid_certs(true) .build() .expect("Failed to build client"); Self { base, - token: ClientToken::default(), + token: String::new(), client, } } - pub fn with_api_token(mut self, user: &str, realm: &str, token_id: &str, secret: &str) -> Self { + pub fn with_api_token(mut self, token: &str) -> Self { // PVEAPIToken=USER@REALM!TOKENID=UUID - self.token = ClientToken { - username: user.to_string(), - realm: realm.to_string(), - password: format!("{}@{}!{}={}", user, realm, token_id, secret), - }; + self.token = token.to_string(); self } @@ -58,15 +47,16 @@ impl Client { self.get(&format!("/api2/json/nodes/{node}/qemu")).await?; Ok(rsp.data) } + pub async fn list_storage(&self) -> Result, Error> { + let rsp: ResponseBase> = self.get("/api2/json/storage").await?; + Ok(rsp.data) + } async fn get(&self, path: &str) -> Result { Ok(self .client .get(self.base.join(path)?) - .header( - "Authorization", - format!("PVEAPIToken={}", self.token.password), - ) + .header("Authorization", format!("PVEAPIToken={}", self.token)) .send() .await? .json::() @@ -82,10 +72,7 @@ impl Client { Ok(self .client .post(self.base.join(path)?) - .header( - "Authorization", - format!("PVEAPIToken={}", self.token.password), - ) + .header("Authorization", format!("PVEAPIToken={}", self.token)) .body(body) .send() .await? @@ -152,3 +139,29 @@ pub struct VmInfo { pub tags: Option, pub uptime: Option, } + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum StorageType { + LVMThin, + Dir, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum StorageContent { + Images, + RootDir, + Backup, + ISO, + VZTmpL, +} + +#[derive(Debug, Deserialize)] +pub struct NodeStorage { + pub storage: String, + #[serde(rename = "type")] + pub kind: Option, + #[serde(rename = "thinpool")] + pub thin_pool: Option, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a647722 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +pub mod api; +pub mod cors; +pub mod db; +pub mod host; +mod nip98; +pub mod provisioner; +mod vm; diff --git a/src/provisioner.rs b/src/provisioner.rs index 956182f..69a587d 100644 --- a/src/provisioner.rs +++ b/src/provisioner.rs @@ -1,6 +1,9 @@ use crate::db; +use crate::db::{VmHost, VmHostDisk}; +use crate::host::proxmox::ProxmoxClient; use crate::vm::VMSpec; use anyhow::Error; +use log::{info, warn}; use sqlx::{MySqlPool, Row}; #[derive(Debug, Clone)] @@ -13,6 +16,47 @@ impl Provisioner { Self { db } } + /// Auto-discover resources + pub async fn auto_discover(&self) -> Result<(), Error> { + let hosts = self.list_hosts().await?; + for host in hosts { + let api = ProxmoxClient::new(host.ip.parse()?).with_api_token(&host.api_token); + + let nodes = api.list_nodes().await?; + if let Some(node) = nodes.iter().find(|n| n.name == host.name) { + // Update host resources + if node.max_cpu.unwrap_or(host.cpu) != host.cpu + || node.max_mem.unwrap_or(host.memory) != host.memory + { + let mut host = host.clone(); + host.cpu = node.max_cpu.unwrap_or(host.cpu); + host.memory = node.max_mem.unwrap_or(host.memory); + info!("Patching host: {:?}", host); + self.update_host(host).await?; + } + // Update disk info + let storages = api.list_storage().await?; + let host_disks = self.list_host_disks(host.id).await?; + for storage in storages { + let host_storage = + if let Some(s) = host_disks.iter().find(|d| d.name == storage.storage) { + s + } else { + warn!("Disk not found: {} on {}", storage.storage, host.name); + continue; + }; + } + } + info!( + "Discovering resources from: {} v{}", + &host.name, + api.version().await?.version + ); + } + + Ok(()) + } + /// Provision a new VM pub async fn provision(&self, spec: VMSpec) -> Result { todo!() @@ -43,4 +87,33 @@ impl Provisioner { .await .map_err(Error::new) } + + /// List VM's owned by a specific user + pub async fn list_hosts(&self) -> Result, Error> { + sqlx::query_as("select * from vm_host") + .fetch_all(&self.db) + .await + .map_err(Error::new) + } + + /// List VM's owned by a specific user + pub async fn list_host_disks(&self, host_id: u64) -> Result, Error> { + sqlx::query_as("select * from vm_host_disk where host_id = ?") + .bind(&host_id) + .fetch_all(&self.db) + .await + .map_err(Error::new) + } + + /// Update host resources (usually from [auto_discover]) + pub async fn update_host(&self, host: VmHost) -> Result<(), Error> { + sqlx::query("update vm_host set name = ?, cpu = ?, memory = ? where id = ?") + .bind(&host.name) + .bind(&host.cpu) + .bind(&host.memory) + .bind(&host.id) + .execute(&self.db) + .await?; + Ok(()) + } } diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100644 index 59da24e..0000000 --- a/src/settings.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize)] -pub struct Settings { - pub db: String, -}