Dev env setup
This commit is contained in:
parent
d1ab59a0aa
commit
f9623edd38
@ -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
4
dev_setup.sql
Normal 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);
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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]
|
||||||
|
@ -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)
|
31
src/db.rs
31
src/db.rs
@ -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
2
src/host/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod proxmox;
|
||||||
|
pub trait VmHostClient {}
|
@ -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
7
src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
pub mod api;
|
||||||
|
pub mod cors;
|
||||||
|
pub mod db;
|
||||||
|
pub mod host;
|
||||||
|
mod nip98;
|
||||||
|
pub mod provisioner;
|
||||||
|
mod vm;
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Settings {
|
|
||||||
pub db: String,
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user