Dev env setup

This commit is contained in:
Kieran 2024-11-04 23:10:32 +00:00
parent d1ab59a0aa
commit f9623edd38
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
12 changed files with 165 additions and 66 deletions

View File

@ -3,7 +3,8 @@ name = "lnvps"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [[bin]]
name = "api"
[dependencies] [dependencies]
tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros"] }

4
dev_setup.sql Normal file
View File

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

View File

@ -3,6 +3,7 @@ volumes:
services: services:
db: db:
image: mariadb image: mariadb
restart: unless-stopped
environment: environment:
- "MARIADB_ROOT_PASSWORD=root" - "MARIADB_ROOT_PASSWORD=root"
- "MARIADB_DATABASE=lnvps" - "MARIADB_DATABASE=lnvps"

View File

@ -75,6 +75,7 @@ create table vm_payment
expires timestamp not null, expires timestamp not null,
amount bigint unsigned not null, amount bigint unsigned not null,
invoice varchar(2048) not null, invoice varchar(2048) not null,
time_value integer unsigned not null,
is_paid bit(1) not null, is_paid bit(1) not null,
constraint fk_vm_payment_vm foreign key (vm_id) references vm (id) constraint fk_vm_payment_vm foreign key (vm_id) references vm (id)

View File

@ -4,7 +4,7 @@ use crate::provisioner::Provisioner;
use anyhow::Error; use anyhow::Error;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::{get, routes, Responder, Route, State}; use rocket::{get, routes, Responder, Route, State};
use serde::{Deserialize, Serialize}; use serde::Serialize;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![v1_list_vms] routes![v1_list_vms]

View File

@ -1,22 +1,16 @@
extern crate core;
use crate::cors::CORS;
use crate::provisioner::Provisioner;
use crate::settings::Settings;
use anyhow::Error; use anyhow::Error;
use config::{Config, File}; use config::{Config, File};
use lnvps::api;
use lnvps::cors::CORS;
use lnvps::provisioner::Provisioner;
use log::error; use log::error;
use rocket::routes; use serde::{Deserialize, Serialize};
use sqlx::MySqlPool; use sqlx::{Executor, MySqlPool};
mod api; #[derive(Debug, Deserialize, Serialize)]
mod cors; pub struct Settings {
mod db; pub db: String,
mod nip98; }
mod provisioner;
mod proxmox;
mod settings;
mod vm;
#[rocket::main] #[rocket::main]
async fn main() -> Result<(), Error> { async fn main() -> Result<(), Error> {
@ -28,8 +22,14 @@ async fn main() -> Result<(), Error> {
.try_deserialize()?; .try_deserialize()?;
let db = MySqlPool::connect(&config.db).await?; let db = MySqlPool::connect(&config.db).await?;
sqlx::migrate!("./migrations").run(&db).await?; sqlx::migrate!().run(&db).await?;
let provisioner = Provisioner::new(db); 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() if let Err(e) = rocket::build()
.attach(CORS) .attach(CORS)

View File

@ -13,13 +13,14 @@ pub struct User {
pub created: DateTime<Utc>, pub created: DateTime<Utc>,
} }
#[derive(Serialize)] #[derive(Serialize, sqlx::Type, Clone, Debug)]
#[repr(u8)]
/// The type of VM host /// The type of VM host
pub enum VmHostKind { pub enum VmHostKind {
Proxmox, Proxmox = 0,
} }
#[derive(Serialize, FromRow)] #[derive(Serialize, FromRow, Clone, Debug)]
/// A VM host /// A VM host
pub struct VmHost { pub struct VmHost {
pub id: u64, pub id: u64,
@ -35,7 +36,7 @@ pub struct VmHost {
pub api_token: String, pub api_token: String,
} }
#[derive(Serialize, FromRow)] #[derive(Serialize, FromRow, Clone, Debug)]
pub struct VmHostDisk { pub struct VmHostDisk {
pub id: u64, pub id: u64,
pub host_id: u64, pub host_id: u64,
@ -46,34 +47,36 @@ pub struct VmHostDisk {
pub enabled: bool, pub enabled: bool,
} }
#[derive(Serialize)] #[derive(Serialize, sqlx::Type, Clone, Debug)]
#[repr(u8)]
pub enum DiskType { pub enum DiskType {
HDD, HDD = 0,
SSD, SSD = 1,
} }
#[derive(Serialize)] #[derive(Serialize, sqlx::Type, Clone, Debug)]
#[repr(u8)]
pub enum DiskInterface { pub enum DiskInterface {
SATA, SATA = 0,
SCSI, SCSI = 1,
PCIe, PCIe = 2,
} }
#[derive(Serialize, FromRow)] #[derive(Serialize, FromRow, Clone, Debug)]
pub struct VmOsImage { pub struct VmOsImage {
pub id: u64, pub id: u64,
pub name: String, pub name: String,
pub enabled: bool, pub enabled: bool,
} }
#[derive(Serialize, FromRow)] #[derive(Serialize, FromRow, Clone, Debug)]
pub struct IpRange { pub struct IpRange {
pub id: u64, pub id: u64,
pub cidr: String, pub cidr: String,
pub enabled: bool, pub enabled: bool,
} }
#[derive(Serialize, FromRow)] #[derive(Serialize, FromRow, Clone, Debug)]
pub struct Vm { pub struct Vm {
/// Unique VM ID (Same in proxmox) /// Unique VM ID (Same in proxmox)
pub id: u64, pub id: u64,

2
src/host/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod proxmox;
pub trait VmHostClient {}

View File

@ -1,43 +1,32 @@
use anyhow::Error; use anyhow::Error;
use reqwest::{Body, ClientBuilder, Url}; use reqwest::{Body, ClientBuilder, Url};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize}; use serde::Deserialize;
use std::fmt::Debug; use std::fmt::Debug;
#[derive(Debug, Default, Deserialize, Serialize)] pub struct ProxmoxClient {
pub struct ClientToken {
username: String,
realm: String,
password: String,
}
pub struct Client {
base: Url, base: Url,
token: ClientToken, token: String,
client: reqwest::Client, client: reqwest::Client,
} }
impl Client { impl ProxmoxClient {
pub fn new(base: Url) -> Self { pub fn new(base: Url) -> Self {
let mut client = ClientBuilder::new() let client = ClientBuilder::new()
.danger_accept_invalid_certs(true) .danger_accept_invalid_certs(true)
.build() .build()
.expect("Failed to build client"); .expect("Failed to build client");
Self { Self {
base, base,
token: ClientToken::default(), token: String::new(),
client, 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 // PVEAPIToken=USER@REALM!TOKENID=UUID
self.token = ClientToken { self.token = token.to_string();
username: user.to_string(),
realm: realm.to_string(),
password: format!("{}@{}!{}={}", user, realm, token_id, secret),
};
self self
} }
@ -58,15 +47,16 @@ impl Client {
self.get(&format!("/api2/json/nodes/{node}/qemu")).await?; self.get(&format!("/api2/json/nodes/{node}/qemu")).await?;
Ok(rsp.data) Ok(rsp.data)
} }
pub async fn list_storage(&self) -> Result<Vec<NodeStorage>, Error> {
let rsp: ResponseBase<Vec<NodeStorage>> = self.get("/api2/json/storage").await?;
Ok(rsp.data)
}
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> { async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
Ok(self Ok(self
.client .client
.get(self.base.join(path)?) .get(self.base.join(path)?)
.header( .header("Authorization", format!("PVEAPIToken={}", self.token))
"Authorization",
format!("PVEAPIToken={}", self.token.password),
)
.send() .send()
.await? .await?
.json::<T>() .json::<T>()
@ -82,10 +72,7 @@ impl Client {
Ok(self Ok(self
.client .client
.post(self.base.join(path)?) .post(self.base.join(path)?)
.header( .header("Authorization", format!("PVEAPIToken={}", self.token))
"Authorization",
format!("PVEAPIToken={}", self.token.password),
)
.body(body) .body(body)
.send() .send()
.await? .await?
@ -152,3 +139,29 @@ pub struct VmInfo {
pub tags: Option<String>, pub tags: Option<String>,
pub uptime: Option<u64>, pub uptime: Option<u64>,
} }
#[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<StorageType>,
#[serde(rename = "thinpool")]
pub thin_pool: Option<String>,
}

7
src/lib.rs Normal file
View File

@ -0,0 +1,7 @@
pub mod api;
pub mod cors;
pub mod db;
pub mod host;
mod nip98;
pub mod provisioner;
mod vm;

View File

@ -1,6 +1,9 @@
use crate::db; use crate::db;
use crate::db::{VmHost, VmHostDisk};
use crate::host::proxmox::ProxmoxClient;
use crate::vm::VMSpec; use crate::vm::VMSpec;
use anyhow::Error; use anyhow::Error;
use log::{info, warn};
use sqlx::{MySqlPool, Row}; use sqlx::{MySqlPool, Row};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -13,6 +16,47 @@ impl Provisioner {
Self { db } 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 /// Provision a new VM
pub async fn provision(&self, spec: VMSpec) -> Result<db::Vm, Error> { pub async fn provision(&self, spec: VMSpec) -> Result<db::Vm, Error> {
todo!() todo!()
@ -43,4 +87,33 @@ impl Provisioner {
.await .await
.map_err(Error::new) .map_err(Error::new)
} }
/// List VM's owned by a specific user
pub async fn list_hosts(&self) -> Result<Vec<VmHost>, 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<Vec<VmHostDisk>, 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(())
}
} }

View File

@ -1,6 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Settings {
pub db: String,
}