api progress

This commit is contained in:
kieran 2024-11-25 22:45:27 +00:00
parent 13f59908fb
commit a0e49d83bd
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
13 changed files with 1414 additions and 163 deletions

890
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -20,3 +20,6 @@ rocket = { version = "0.5.1", features = ["json"] }
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
nostr = { version = "0.36.0", default-features = false, features = ["std"] } nostr = { version = "0.36.0", default-features = false, features = ["std"] }
base64 = "0.22.1" base64 = "0.22.1"
ssh-key = "0.6.7"
urlencoding = "2.1.3"
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] }

View File

@ -1,2 +1,6 @@
# MySQL database connection string # MySQL database connection string
db: "mysql://root:root@localhost:3376/lnvps" db: "mysql://root:root@localhost:3376/lnvps"
lnd:
url: "https://127.0.0.1:10003"
cert: "/home/kieran/.polar/networks/2/volumes/lnd/alice/tls.cert"
macaroon: "/home/kieran/.polar/networks/2/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon"

View File

@ -1,15 +1,62 @@
insert insert
ignore into vm_host_region(id,name,enabled) values(1,"uat",1); ignore into vm_host_region(id,name,enabled) values(1,"uat",1);
insert insert
ignore into vm_host(id,kind,region_id,name,ip,cpu,memory,enabled,api_token) values(1, 0, 1, "lab", "https://185.18.221.8:8006", 4, 4096*1024, 1, "root@pam!tester=c82f8a57-f876-4ca4-8610-c086d8d9d51c"); ignore into vm_host(id,kind,region_id,name,ip,cpu,memory,enabled,api_token)
values(1, 0, 1, "lab", "https://185.18.221.8:8006", 4, 4096*1024, 1, "root@pam!tester=c82f8a57-f876-4ca4-8610-c086d8d9d51c");
insert insert
ignore into vm_host_disk(id,host_id,name,size,kind,interface,enabled) values(1,1,"local-lvm",1000*1000*1000*1000, 0, 0, 1); ignore into vm_host_disk(id,host_id,name,size,kind,interface,enabled)
values(1,1,"local-lvm",1000*1000*1000*1000, 0, 0, 1);
insert insert
ignore into vm_os_image(id,name,distribution,flavour,version,enabled,url) values(1,0,"Server","24.04",1,"https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"); ignore into vm_os_image(id,distribution,flavour,version,enabled,url,release_date)
values(1, 0,"Server","24.04",1,"https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img","2024-04-25");
insert insert
ignore into ip_range(id,cidr,enabled) values(1,"185.18.221.80/28",1); ignore into vm_os_image(id,distribution,flavour,version,enabled,url,release_date)
values(2, 0,"Server","22.04",1,"https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img","2022-04-21");
insert insert
ignore into vm_cost_plan(id,name,amount,currency,interval_amount,interval_type) values(1,"tiny_monthly",3,"EUR",1,1); ignore into vm_os_image(id,distribution,flavour,version,enabled,url,release_date)
values(3, 0,"Server","20.04",1,"https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img","2020-04-23");
insert
ignore into vm_os_image(id,distribution,flavour,version,enabled,url,release_date)
values(4, 1,"Server","12",1,"https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.raw","2023-06-10");
insert
ignore into vm_os_image(id,distribution,flavour,version,enabled,url,release_date)
values(5, 1,"Server","11",1,"https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-genericcloud-amd64.raw","2021-08-14");
insert
ignore into ip_range(id,cidr,enabled)
values(1,"185.18.221.80/28",1);
insert
ignore into vm_cost_plan(id,name,amount,currency,interval_amount,interval_type)
values(1,"tiny_monthly",2,"EUR",1,1);
insert
ignore into vm_cost_plan(id,name,amount,currency,interval_amount,interval_type)
values(2,"small_monthly",4,"EUR",1,1);
insert
ignore into vm_cost_plan(id,name,amount,currency,interval_amount,interval_type)
values(3,"medium_monthly",8,"EUR",1,1);
insert
ignore into vm_cost_plan(id,name,amount,currency,interval_amount,interval_type)
values(4,"large_monthly",17,"EUR",1,1);
insert
ignore into vm_cost_plan(id,name,amount,currency,interval_amount,interval_type)
values(5,"xlarge_monthly",30,"EUR",1,1);
insert
ignore into vm_cost_plan(id,name,amount,currency,interval_amount,interval_type)
values(6,"xxlarge_monthly",45,"EUR",1,1);
insert insert
ignore into vm_template(id,name,enabled,cpu,memory,disk_size,disk_type,disk_interface,cost_plan_id,region_id) ignore into vm_template(id,name,enabled,cpu,memory,disk_size,disk_type,disk_interface,cost_plan_id,region_id)
values(1,"Tiny",1,2,1024*1024*2,1000*1000*1000*80,1,2,1,1); values(1,"Tiny",1,1,1024*1024*1024*1,1024*1024*1024*40,1,2,1,1);
insert
ignore into vm_template(id,name,enabled,cpu,memory,disk_size,disk_type,disk_interface,cost_plan_id,region_id)
values(2,"Small",1,2,1024*1024*1024*2,1024*1024*1024*80,1,2,2,1);
insert
ignore into vm_template(id,name,enabled,cpu,memory,disk_size,disk_type,disk_interface,cost_plan_id,region_id)
values(3,"Medium",1,4,1024*1024*1024*4,1024*1024*1024*160,1,2,3,1);
insert
ignore into vm_template(id,name,enabled,cpu,memory,disk_size,disk_type,disk_interface,cost_plan_id,region_id)
values(4,"Large",1,8,1024*1024*1024*8,1024*1024*1024*400,1,2,4,1);
insert
ignore into vm_template(id,name,enabled,cpu,memory,disk_size,disk_type,disk_interface,cost_plan_id,region_id)
values(5,"X-Large",1,12,1024*1024*1024*16,1024*1024*1024*800,1,2,5,1);
insert
ignore into vm_template(id,name,enabled,cpu,memory,disk_size,disk_type,disk_interface,cost_plan_id,region_id)
values(6,"XX-Large",1,20,1024*1024*1024*24,1024*1024*1024*1000,1,2,6,1);

View File

@ -6,7 +6,7 @@ create table users
email varchar(200), email varchar(200),
contact_nip4 bit(1) not null, contact_nip4 bit(1) not null,
contact_nip17 bit(1) not null, contact_nip17 bit(1) not null,
contact_email bit(1) not null, contact_email bit(1) not null
); );
create unique index ix_user_pubkey on users (pubkey); create unique index ix_user_pubkey on users (pubkey);
create unique index ix_user_email on users (email); create unique index ix_user_email on users (email);
@ -55,14 +55,14 @@ create table vm_host_disk
create table vm_os_image create table vm_os_image
( (
id integer unsigned not null auto_increment primary key, id integer unsigned not null auto_increment primary key,
name varchar(200) not null,
distribution smallint unsigned not null, distribution smallint unsigned not null,
flavour varchar(50) not null, flavour varchar(50) not null,
version varchar(50) not null, version varchar(50) not null,
enabled bit(1) not null, enabled bit(1) not null,
url varchar(1024) not null, release_date timestamp not null,
url varchar(1024) not null
); );
create unique index ix_vm_os_image_name on vm_os_image (name); create unique index ix_vm_os_image on vm_os_image (distribution, flavour, version);
create table ip_range create table ip_range
( (
id integer unsigned not null auto_increment primary key, id integer unsigned not null auto_increment primary key,
@ -72,6 +72,7 @@ create table ip_range
constraint fk_ip_range_region foreign key (region_id) references vm_host_region (id) constraint fk_ip_range_region foreign key (region_id) references vm_host_region (id)
); );
create unique index ix_ip_range_cidr on ip_range (cidr);
create table vm_cost_plan create table vm_cost_plan
( (
id integer unsigned not null auto_increment primary key, id integer unsigned not null auto_increment primary key,

48
lnvps_db/src/hydrate.rs Normal file
View File

@ -0,0 +1,48 @@
use crate::{LNVpsDb, Vm, VmTemplate};
use anyhow::Result;
use async_trait::async_trait;
#[async_trait]
pub trait Hydrate {
/// Load parent resources
async fn hydrate_up(&mut self, db: &Box<dyn LNVpsDb>) -> Result<()>;
/// Load child resources
async fn hydrate_down(&mut self, db: &Box<dyn LNVpsDb>) -> Result<()>;
}
#[async_trait]
impl Hydrate for Vm {
async fn hydrate_up(&mut self, db: &Box<dyn LNVpsDb>) -> Result<()> {
let image = db.get_os_image(self.image_id).await?;
let template = db.get_vm_template(self.template_id).await?;
let ssh_key = db.get_user_ssh_key(self.ssh_key_id).await?;
self.image = Some(image);
self.template = Some(template);
self.ssh_key = Some(ssh_key);
Ok(())
}
async fn hydrate_down(&mut self, db: &Box<dyn LNVpsDb>) -> Result<()> {
let payments = db.list_vm_payment(self.id).await?;
self.payments = Some(payments);
Ok(())
}
}
#[async_trait]
impl Hydrate for VmTemplate {
async fn hydrate_up(&mut self, db: &Box<dyn LNVpsDb>) -> Result<()> {
let cost_plan = db.get_cost_plan(self.cost_plan_id).await?;
let region = db.get_host_region(self.region_id).await?;
self.cost_plan = Some(cost_plan);
self.region = Some(region);
Ok(())
}
async fn hydrate_down(&mut self, db: &Box<dyn LNVpsDb>) -> Result<()> {
todo!()
}
}

View File

@ -4,6 +4,7 @@ use async_trait::async_trait;
mod model; mod model;
#[cfg(feature = "mysql")] #[cfg(feature = "mysql")]
mod mysql; mod mysql;
pub mod hydrate;
pub use model::*; pub use model::*;
#[cfg(feature = "mysql")] #[cfg(feature = "mysql")]
@ -27,7 +28,7 @@ pub trait LNVpsDb: Sync + Send {
async fn delete_user(&self, id: u64) -> Result<()>; async fn delete_user(&self, id: u64) -> Result<()>;
/// Insert a new user ssh key /// Insert a new user ssh key
async fn insert_user_ssh_key(&self, new_key: UserSshKey) -> Result<u64>; async fn insert_user_ssh_key(&self, new_key: &UserSshKey) -> Result<u64>;
/// Get user ssh key by id /// Get user ssh key by id
async fn get_user_ssh_key(&self, id: u64) -> Result<UserSshKey>; async fn get_user_ssh_key(&self, id: u64) -> Result<UserSshKey>;
@ -45,11 +46,14 @@ pub trait LNVpsDb: Sync + Send {
async fn list_hosts(&self) -> Result<Vec<VmHost>>; async fn list_hosts(&self) -> Result<Vec<VmHost>>;
/// Update host resources (usually from [auto_discover]) /// Update host resources (usually from [auto_discover])
async fn update_host(&self, host: VmHost) -> Result<()>; async fn update_host(&self, host: &VmHost) -> Result<()>;
/// List VM's owned by a specific user /// List VM's owned by a specific user
async fn list_host_disks(&self, host_id: u64) -> Result<Vec<VmHostDisk>>; async fn list_host_disks(&self, host_id: u64) -> Result<Vec<VmHostDisk>>;
/// Get OS image by id
async fn get_os_image(&self, id: u64) -> Result<VmOsImage>;
/// List available OS images /// List available OS images
async fn list_os_image(&self) -> Result<Vec<VmOsImage>>; async fn list_os_image(&self) -> Result<Vec<VmOsImage>>;
@ -59,14 +63,20 @@ pub trait LNVpsDb: Sync + Send {
/// Get a VM cost plan by id /// Get a VM cost plan by id
async fn get_cost_plan(&self, id: u64) -> Result<VmCostPlan>; async fn get_cost_plan(&self, id: u64) -> Result<VmCostPlan>;
/// Get VM template by id
async fn get_vm_template(&self, id: u64) -> Result<VmTemplate>;
/// List VM templates /// List VM templates
async fn list_vm_templates(&self) -> Result<Vec<VmTemplate>>; async fn list_vm_templates(&self) -> Result<Vec<VmTemplate>>;
/// List VM's owned by a specific user /// List VM's owned by a specific user
async fn list_user_vms(&self, id: u64) -> Result<Vec<Vm>>; async fn list_user_vms(&self, id: u64) -> Result<Vec<Vm>>;
/// Get a VM by id
async fn get_vm(&self, vm_id: u64) -> Result<Vm>;
/// Insert a new VM record /// Insert a new VM record
async fn insert_vm(&self, vm: Vm) -> Result<u64>; async fn insert_vm(&self, vm: &Vm) -> Result<u64>;
/// List VM ip assignments /// List VM ip assignments
async fn get_vm_ip_assignments(&self, vm_id: u64) -> Result<Vec<VmIpAssignment>>; async fn get_vm_ip_assignments(&self, vm_id: u64) -> Result<Vec<VmIpAssignment>>;
@ -75,8 +85,8 @@ pub trait LNVpsDb: Sync + Send {
async fn list_vm_payment(&self, vm_id: u64) -> Result<Vec<VmPayment>>; async fn list_vm_payment(&self, vm_id: u64) -> Result<Vec<VmPayment>>;
/// Insert a new VM payment record /// Insert a new VM payment record
async fn insert_vm_payment(&self, vm_payment: VmPayment) -> Result<u64>; async fn insert_vm_payment(&self, vm_payment: &VmPayment) -> Result<u64>;
/// Update a VM payment record /// Update a VM payment record
async fn update_vm_payment(&self, vm_payment: VmPayment) -> Result<()>; async fn update_vm_payment(&self, vm_payment: &VmPayment) -> Result<()>;
} }

View File

@ -18,12 +18,13 @@ pub struct User {
pub contact_email: bool, pub contact_email: bool,
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)]
pub struct UserSshKey { pub struct UserSshKey {
pub id: u64, pub id: u64,
pub name: String, pub name: String,
pub user_id: u64, pub user_id: u64,
pub created: DateTime<Utc>, pub created: DateTime<Utc>,
#[serde(skip_serializing)]
pub key_data: String, pub key_data: String,
#[sqlx(skip)] #[sqlx(skip)]
@ -79,24 +80,29 @@ pub struct VmHostDisk {
pub enabled: bool, pub enabled: bool,
} }
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)] #[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)]
#[serde(rename_all = "lowercase")]
#[repr(u16)] #[repr(u16)]
pub enum DiskType { pub enum DiskType {
#[default]
HDD = 0, HDD = 0,
SSD = 1, SSD = 1,
} }
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)] #[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)]
#[serde(rename_all = "lowercase")]
#[repr(u16)] #[repr(u16)]
pub enum DiskInterface { pub enum DiskInterface {
#[default]
SATA = 0, SATA = 0,
SCSI = 1, SCSI = 1,
PCIe = 2, PCIe = 2,
} }
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)] #[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)]
#[repr(u16)] #[repr(u16)]
pub enum OsDistribution { pub enum OsDistribution {
#[default]
Ubuntu = 0, Ubuntu = 0,
Debian = 1, Debian = 1,
} }
@ -106,11 +112,12 @@ pub enum OsDistribution {
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
pub struct VmOsImage { pub struct VmOsImage {
pub id: u64, pub id: u64,
pub name: String,
pub distribution: OsDistribution, pub distribution: OsDistribution,
pub flavour: String, pub flavour: String,
pub version: String, pub version: String,
pub enabled: bool, pub enabled: bool,
pub release_date: DateTime<Utc>,
#[serde(skip_serializing)]
/// URL location of cloud image /// URL location of cloud image
pub url: String, pub url: String,
} }
@ -124,6 +131,7 @@ pub struct IpRange {
} }
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)] #[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)]
#[serde(rename_all = "lowercase")]
#[repr(u16)] #[repr(u16)]
pub enum VmCostPlanIntervalType { pub enum VmCostPlanIntervalType {
Day = 0, Day = 0,
@ -144,7 +152,7 @@ pub struct VmCostPlan {
/// Offers. /// Offers.
/// These are the same as the offers visible to customers /// These are the same as the offers visible to customers
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)]
pub struct VmTemplate { pub struct VmTemplate {
pub id: u64, pub id: u64,
pub name: String, pub name: String,
@ -154,6 +162,7 @@ pub struct VmTemplate {
pub expires: Option<DateTime<Utc>>, pub expires: Option<DateTime<Utc>>,
pub cpu: u16, pub cpu: u16,
pub memory: u64, pub memory: u64,
pub disk_size: u64,
pub disk_type: DiskType, pub disk_type: DiskType,
pub disk_interface: DiskInterface, pub disk_interface: DiskInterface,
pub cost_plan_id: u64, pub cost_plan_id: u64,
@ -167,7 +176,7 @@ pub struct VmTemplate {
pub region: Option<VmHostRegion>, pub region: Option<VmHostRegion>,
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)]
pub struct Vm { pub struct Vm {
/// Unique VM ID (Same in proxmox) /// Unique VM ID (Same in proxmox)
pub id: u64, pub id: u64,
@ -193,6 +202,19 @@ pub struct Vm {
pub disk_size: u64, pub disk_size: u64,
/// The [VmHostDisk] this VM is on /// The [VmHostDisk] this VM is on
pub disk_id: u64, pub disk_id: u64,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<VmOsImage>,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<VmTemplate>,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_key: Option<UserSshKey>,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub payments: Option<Vec<VmPayment>>,
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(Serialize, Deserialize, FromRow, Clone, Debug)]

View File

@ -25,11 +25,11 @@ impl LNVpsDbMysql {
#[async_trait] #[async_trait]
impl LNVpsDb for LNVpsDbMysql { impl LNVpsDb for LNVpsDbMysql {
async fn migrate(&self) -> anyhow::Result<()> { async fn migrate(&self) -> Result<()> {
sqlx::migrate!().run(&self.db).await.map_err(Error::new) sqlx::migrate!().run(&self.db).await.map_err(Error::new)
} }
async fn upsert_user(&self, pubkey: &[u8; 32]) -> anyhow::Result<u64> { async fn upsert_user(&self, pubkey: &[u8; 32]) -> Result<u64> {
let res = sqlx::query("insert ignore into users(pubkey) values(?) returning id") let res = sqlx::query("insert ignore into users(pubkey) values(?) returning id")
.bind(pubkey.as_slice()) .bind(pubkey.as_slice())
.fetch_optional(&self.db) .fetch_optional(&self.db)
@ -46,7 +46,11 @@ impl LNVpsDb for LNVpsDbMysql {
} }
async fn get_user(&self, id: u64) -> Result<User> { async fn get_user(&self, id: u64) -> Result<User> {
todo!() sqlx::query_as("select * from users where id=?")
.bind(id)
.fetch_one(&self.db)
.await
.map_err(Error::new)
} }
async fn update_user(&self, user: &User) -> Result<()> { async fn update_user(&self, user: &User) -> Result<()> {
@ -57,12 +61,23 @@ impl LNVpsDb for LNVpsDbMysql {
todo!() todo!()
} }
async fn insert_user_ssh_key(&self, new_key: UserSshKey) -> Result<u64> { async fn insert_user_ssh_key(&self, new_key: &UserSshKey) -> Result<u64> {
todo!() Ok(sqlx::query("insert into user_ssh_key(name,user_id,key_data) values(?, ?, ?) returning id")
.bind(&new_key.name)
.bind(&new_key.user_id)
.bind(&new_key.key_data)
.fetch_one(&self.db)
.await
.map_err(Error::new)?
.try_get(0)?)
} }
async fn get_user_ssh_key(&self, id: u64) -> Result<UserSshKey> { async fn get_user_ssh_key(&self, id: u64) -> Result<UserSshKey> {
todo!() sqlx::query_as("select * from user_ssh_key where id=?")
.bind(id)
.fetch_one(&self.db)
.await
.map_err(Error::new)
} }
async fn delete_user_ssh_key(&self, id: u64) -> Result<()> { async fn delete_user_ssh_key(&self, id: u64) -> Result<()> {
@ -70,21 +85,29 @@ impl LNVpsDb for LNVpsDbMysql {
} }
async fn list_user_ssh_key(&self, user_id: u64) -> Result<Vec<UserSshKey>> { async fn list_user_ssh_key(&self, user_id: u64) -> Result<Vec<UserSshKey>> {
todo!() sqlx::query_as("select * from user_ssh_key where user_id = ?")
.bind(user_id)
.fetch_all(&self.db)
.await
.map_err(Error::new)
} }
async fn get_host_region(&self, id: u64) -> Result<VmHostRegion> { async fn get_host_region(&self, id: u64) -> Result<VmHostRegion> {
todo!() sqlx::query_as("select * from vm_host_region where id=?")
.bind(id)
.fetch_one(&self.db)
.await
.map_err(Error::new)
} }
async fn list_hosts(&self) -> anyhow::Result<Vec<VmHost>> { async fn list_hosts(&self) -> Result<Vec<VmHost>> {
sqlx::query_as("select * from vm_host") sqlx::query_as("select * from vm_host")
.fetch_all(&self.db) .fetch_all(&self.db)
.await .await
.map_err(Error::new) .map_err(Error::new)
} }
async fn update_host(&self, host: VmHost) -> anyhow::Result<()> { async fn update_host(&self, host: &VmHost) -> Result<()> {
sqlx::query("update vm_host set name = ?, cpu = ?, memory = ? where id = ?") sqlx::query("update vm_host set name = ?, cpu = ?, memory = ? where id = ?")
.bind(&host.name) .bind(&host.name)
.bind(&host.cpu) .bind(&host.cpu)
@ -95,7 +118,7 @@ impl LNVpsDb for LNVpsDbMysql {
Ok(()) Ok(())
} }
async fn list_host_disks(&self, host_id: u64) -> anyhow::Result<Vec<VmHostDisk>> { async fn list_host_disks(&self, host_id: u64) -> Result<Vec<VmHostDisk>> {
sqlx::query_as("select * from vm_host_disk where host_id = ?") sqlx::query_as("select * from vm_host_disk where host_id = ?")
.bind(&host_id) .bind(&host_id)
.fetch_all(&self.db) .fetch_all(&self.db)
@ -103,20 +126,46 @@ impl LNVpsDb for LNVpsDbMysql {
.map_err(Error::new) .map_err(Error::new)
} }
async fn get_os_image(&self, id: u64) -> Result<VmOsImage> {
sqlx::query_as("select * from vm_os_image where id=?")
.bind(id)
.fetch_one(&self.db)
.await
.map_err(Error::new)
}
async fn list_os_image(&self) -> Result<Vec<VmOsImage>> { async fn list_os_image(&self) -> Result<Vec<VmOsImage>> {
todo!() sqlx::query_as("select * from vm_os_image")
.fetch_all(&self.db)
.await
.map_err(Error::new)
} }
async fn list_ip_range(&self) -> Result<Vec<IpRange>> { async fn list_ip_range(&self) -> Result<Vec<IpRange>> {
todo!() sqlx::query_as("select * from ip_range")
.fetch_all(&self.db)
.await
.map_err(Error::new)
} }
async fn get_cost_plan(&self, id: u64) -> Result<VmCostPlan> { async fn get_cost_plan(&self, id: u64) -> Result<VmCostPlan> {
todo!() sqlx::query_as("select * from vm_cost_plan where id=?")
.bind(id)
.fetch_one(&self.db)
.await
.map_err(Error::new)
} }
async fn list_vm_templates(&self) -> anyhow::Result<Vec<VmTemplate>> { async fn get_vm_template(&self, id: u64) -> Result<VmTemplate> {
sqlx::query_as("select * from vm_template where enabled = 1 and (expires is null or expires > now())") sqlx::query_as("select * from vm_template where id=?")
.bind(id)
.fetch_one(&self.db)
.await
.map_err(Error::new)
}
async fn list_vm_templates(&self) -> Result<Vec<VmTemplate>> {
sqlx::query_as("select * from vm_template")
.fetch_all(&self.db) .fetch_all(&self.db)
.await .await
.map_err(Error::new) .map_err(Error::new)
@ -130,23 +179,71 @@ impl LNVpsDb for LNVpsDbMysql {
.map_err(Error::new) .map_err(Error::new)
} }
async fn insert_vm(&self, vm: Vm) -> Result<u64> { async fn get_vm(&self, vm_id: u64) -> Result<Vm> {
todo!() sqlx::query_as("select * from vm where id = ?")
.bind(&vm_id)
.fetch_one(&self.db)
.await
.map_err(Error::new)
}
async fn insert_vm(&self, vm: &Vm) -> Result<u64> {
Ok(sqlx::query("insert into vm(host_id,user_id,image_id,template_id,ssh_key_id,created,expires,cpu,memory,disk_size,disk_id) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) returning id")
.bind(&vm.host_id)
.bind(&vm.user_id)
.bind(&vm.image_id)
.bind(&vm.template_id)
.bind(&vm.ssh_key_id)
.bind(&vm.created)
.bind(&vm.expires)
.bind(&vm.cpu)
.bind(&vm.memory)
.bind(&vm.disk_size)
.bind(&vm.disk_id)
.fetch_one(&self.db)
.await
.map_err(Error::new)?
.try_get(0)?)
} }
async fn get_vm_ip_assignments(&self, vm_id: u64) -> Result<Vec<VmIpAssignment>> { async fn get_vm_ip_assignments(&self, vm_id: u64) -> Result<Vec<VmIpAssignment>> {
todo!() sqlx::query_as("select * from vm_ip_assignment where vm_id=?")
.bind(vm_id)
.fetch_all(&self.db)
.await
.map_err(Error::new)
} }
async fn list_vm_payment(&self, vm_id: u64) -> Result<Vec<VmPayment>> { async fn list_vm_payment(&self, vm_id: u64) -> Result<Vec<VmPayment>> {
todo!() sqlx::query_as("select * from vm_payment where vm_id=?")
.bind(vm_id)
.fetch_all(&self.db)
.await
.map_err(Error::new)
} }
async fn insert_vm_payment(&self, vm_payment: VmPayment) -> Result<u64> { async fn insert_vm_payment(&self, vm_payment: &VmPayment) -> Result<u64> {
todo!() Ok(sqlx::query("insert into vm_payment(vm_id,created,expires,amount,invoice,time_value,is_paid) values(?,?,?,?,?,?,?) returning id")
.bind(&vm_payment.vm_id)
.bind(&vm_payment.created)
.bind(&vm_payment.expires)
.bind(&vm_payment.amount)
.bind(&vm_payment.invoice)
.bind(&vm_payment.time_value)
.bind(&vm_payment.is_paid)
.fetch_one(&self.db)
.await
.map_err(Error::new)?
.try_get(0)?)
} }
async fn update_vm_payment(&self, vm_payment: VmPayment) -> Result<()> { async fn update_vm_payment(&self, vm_payment: &VmPayment) -> Result<()> {
todo!() sqlx::query("update vm_payment set is_paid = ? where id = ?")
.bind(&vm_payment.is_paid)
.bind(&vm_payment.id)
.execute(&self.db)
.await
.map_err(Error::new)?;
Ok(())
} }
} }

View File

@ -1,13 +1,22 @@
use crate::nip98::Nip98Auth; use crate::nip98::Nip98Auth;
use crate::provisioner::Provisioner; use crate::provisioner::Provisioner;
use anyhow::Error; use lnvps_db::hydrate::Hydrate;
use lnvps_db::{LNVpsDb, Vm, VmTemplate}; use lnvps_db::{LNVpsDb, UserSshKey, Vm, VmOsImage, VmPayment, VmTemplate};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::{get, post, routes, Responder, Route, State}; use rocket::{get, post, routes, Responder, Route, State};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ssh_key::PublicKey;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![v1_list_vms, v1_list_vm_templates, v1_provision_vm] routes![
v1_list_vms,
v1_list_vm_templates,
v1_list_vm_images,
v1_list_ssh_keys,
v1_add_ssh_key,
v1_create_vm_order,
v1_renew_vm
]
} }
type ApiResult<T> = Result<Json<ApiData<T>>, ApiError>; type ApiResult<T> = Result<Json<ApiData<T>>, ApiError>;
@ -21,6 +30,9 @@ impl<T: Serialize> ApiData<T> {
pub fn ok(data: T) -> ApiResult<T> { pub fn ok(data: T) -> ApiResult<T> {
Ok(Json::from(ApiData { data })) Ok(Json::from(ApiData { data }))
} }
pub fn err(msg: &str) -> ApiResult<T> {
Err(msg.into())
}
} }
#[derive(Responder)] #[derive(Responder)]
@ -29,30 +41,86 @@ struct ApiError {
pub error: String, pub error: String,
} }
impl From<Error> for ApiError { impl ApiError {
fn from(value: Error) -> Self { pub fn new(error: &str) -> Self {
Self {
error: error.to_owned(),
}
}
}
impl<T: ToString> From<T> for ApiError {
fn from(value: T) -> Self {
Self { Self {
error: value.to_string(), error: value.to_string(),
} }
} }
} }
#[get("/api/v1/vms")] #[get("/api/v1/vm")]
async fn v1_list_vms(auth: Nip98Auth, db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<Vm>> { async fn v1_list_vms(auth: Nip98Auth, db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<Vm>> {
let pubkey = auth.event.pubkey.to_bytes(); let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?; let uid = db.upsert_user(&pubkey).await?;
let vms = db.list_user_vms(uid).await?; let mut vms = db.list_user_vms(uid).await?;
for vm in &mut vms {
vm.hydrate_up(db).await?;
}
ApiData::ok(vms)
}
#[get("/api/v1/image")]
async fn v1_list_vm_images(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmOsImage>> {
let vms = db.list_os_image().await?;
ApiData::ok(vms) ApiData::ok(vms)
} }
#[get("/api/v1/vm/templates")] #[get("/api/v1/vm/templates")]
async fn v1_list_vm_templates(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmTemplate>> { async fn v1_list_vm_templates(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmTemplate>> {
let vms = db.list_vm_templates().await?; let mut vms = db.list_vm_templates().await?;
for vm in &mut vms {
vm.hydrate_up(db).await?;
}
ApiData::ok(vms) ApiData::ok(vms)
} }
#[get("/api/v1/ssh-key")]
async fn v1_list_ssh_keys(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
) -> ApiResult<Vec<UserSshKey>> {
let uid = db.upsert_user(&auth.event.pubkey.to_bytes()).await?;
let keys = db.list_user_ssh_key(uid).await?;
ApiData::ok(keys)
}
#[post("/api/v1/ssh-key", data = "<req>", format = "json")]
async fn v1_add_ssh_key(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
req: Json<CreateSshKey>,
) -> ApiResult<UserSshKey> {
let uid = db.upsert_user(&auth.event.pubkey.to_bytes()).await?;
let pk: PublicKey = req.key_data.parse()?;
let key_name = if !req.name.is_empty() {
&req.name
} else {
pk.comment()
};
let mut new_key = UserSshKey {
name: key_name.to_string(),
user_id: uid,
key_data: pk.to_openssh()?,
..Default::default()
};
let key_id = db.insert_user_ssh_key(&new_key).await?;
new_key.id = key_id;
ApiData::ok(new_key)
}
#[post("/api/v1/vm", data = "<req>", format = "json")] #[post("/api/v1/vm", data = "<req>", format = "json")]
async fn v1_provision_vm( async fn v1_create_vm_order(
auth: Nip98Auth, auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>, db: &State<Box<dyn LNVpsDb>>,
provisioner: &State<Box<dyn Provisioner>>, provisioner: &State<Box<dyn Provisioner>>,
@ -62,15 +130,50 @@ async fn v1_provision_vm(
let uid = db.upsert_user(&pubkey).await?; let uid = db.upsert_user(&pubkey).await?;
let req = req.0; let req = req.0;
let rsp = provisioner.provision(req.into()).await?; let mut rsp = provisioner
.provision(uid, req.template_id, req.image_id, req.ssh_key_id)
.await?;
rsp.hydrate_up(db).await?;
ApiData::ok(rsp)
}
#[get("/api/v1/vm/<id>/renew")]
async fn v1_renew_vm(
auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>,
provisioner: &State<Box<dyn Provisioner>>,
id: u64,
) -> ApiResult<VmPayment> {
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 rsp = provisioner.renew(id).await?;
ApiData::ok(rsp) ApiData::ok(rsp)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CreateVmRequest {} struct CreateVmRequest {
template_id: u64,
image_id: u64,
ssh_key_id: u64,
}
impl Into<VmTemplate> for CreateVmRequest { impl From<CreateVmRequest> for VmTemplate {
fn into(self) -> VmTemplate { fn from(val: CreateVmRequest) -> Self {
todo!() VmTemplate {
id: val.template_id,
..Default::default()
}
} }
} }
#[derive(Deserialize)]
struct CreateSshKey {
name: String,
key_data: String,
}

View File

@ -1,15 +1,25 @@
use anyhow::Error; use anyhow::Error;
use config::{Config, File}; use config::{Config, File};
use fedimint_tonic_lnd::connect;
use lnvps::api; use lnvps::api;
use lnvps::cors::CORS; use lnvps::cors::CORS;
use lnvps::provisioner::{LNVpsProvisioner, Provisioner}; use lnvps::provisioner::{LNVpsProvisioner, Provisioner};
use lnvps_db::{LNVpsDb, LNVpsDbMysql}; use lnvps_db::{LNVpsDb, LNVpsDbMysql};
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Settings { pub struct Settings {
pub db: String, pub db: String,
pub lnd: LndConfig,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LndConfig {
pub url: String,
pub cert: PathBuf,
pub macaroon: PathBuf,
} }
#[rocket::main] #[rocket::main]
@ -24,7 +34,8 @@ async fn main() -> Result<(), Error> {
let db = LNVpsDbMysql::new(&config.db).await?; let db = LNVpsDbMysql::new(&config.db).await?;
db.migrate().await?; db.migrate().await?;
let provisioner = LNVpsProvisioner::new(db.clone()); let lnd = connect(config.lnd.url, config.lnd.cert, config.lnd.macaroon).await?;
let provisioner = LNVpsProvisioner::new(db.clone(), lnd.clone());
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
let setup_script = include_str!("../../dev_setup.sql"); let setup_script = include_str!("../../dev_setup.sql");

View File

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use log::info;
use reqwest::{ClientBuilder, Url}; use reqwest::{ClientBuilder, Url};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -42,46 +43,56 @@ impl ProxmoxClient {
Ok(rsp.data) Ok(rsp.data)
} }
pub async fn get_vm_status(&self, node: &str, vm_id: i32) -> Result<VmInfo> {
let rsp: ResponseBase<VmInfo> = self
.get(&format!(
"/api2/json/nodes/{node}/qemu/{vm_id}/status/current"
))
.await?;
Ok(rsp.data)
}
pub async fn list_vms(&self, node: &str, full: bool) -> Result<Vec<VmInfo>> { pub async fn list_vms(&self, node: &str, full: bool) -> Result<Vec<VmInfo>> {
let rsp: ResponseBase<Vec<VmInfo>> = let rsp: ResponseBase<Vec<VmInfo>> =
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>> { pub async fn list_storage(&self) -> Result<Vec<NodeStorage>> {
let rsp: ResponseBase<Vec<NodeStorage>> = self.get("/api2/json/storage").await?; let rsp: ResponseBase<Vec<NodeStorage>> = self.get("/api2/json/storage").await?;
Ok(rsp.data) Ok(rsp.data)
} }
pub async fn create_vm(&self, node: &str, req: CreateVm) -> Result<VmInfo> { pub async fn create_vm(&self, req: CreateVm) -> Result<VmInfo> {
let rsp: ResponseBase<VmInfo> = self info!("{}", serde_json::to_string_pretty(&req)?);
.post(&format!("/api2/json/nodes/{node}/qemu"), req) let _rsp: ResponseBase<Option<String>> = self
.post(&format!("/api2/json/nodes/{}/qemu", req.node), &req)
.await?; .await?;
Ok(rsp.data) self.get_vm_status(&req.node, req.vm_id).await
} }
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> { async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
Ok(self self.client
.client
.get(self.base.join(path)?) .get(self.base.join(path)?)
.header("Authorization", format!("PVEAPIToken={}", self.token)) .header("Authorization", format!("PVEAPIToken={}", self.token))
.send() .send()
.await? .await?
.json::<T>() .json::<T>()
.await .await
.map_err(|e| anyhow::Error::new(e))?) .map_err(anyhow::Error::new)
} }
async fn post<T: DeserializeOwned, R: Serialize>(&self, path: &str, body: R) -> Result<T> { async fn post<T: DeserializeOwned, R: Serialize>(&self, path: &str, body: R) -> Result<T> {
Ok(self let rsp = self
.client .client
.post(self.base.join(path)?) .post(self.base.join(path)?)
.header("Authorization", format!("PVEAPIToken={}", self.token)) .header("Authorization", format!("PVEAPIToken={}", self.token))
.json(&body) .json::<R>(&body)
.send() .send()
.await? .await?;
.error_for_status()? let rsp = rsp.text().await?;
.json() info!("<< {}", rsp);
.await?) Ok(serde_json::from_str(&rsp)?)
} }
} }

View File

@ -1,22 +1,42 @@
use crate::host::proxmox::{CreateVm, ProxmoxClient, VmBios}; use crate::host::proxmox::ProxmoxClient;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use lnvps_db::{LNVpsDb, Vm, VmTemplate}; use chrono::{Days, Months, Utc};
use fedimint_tonic_lnd::lnrpc::Invoice;
use fedimint_tonic_lnd::Client;
use lnvps_db::{LNVpsDb, Vm, VmCostPlanIntervalType, VmOsImage, VmPayment};
use log::{info, warn}; use log::{info, warn};
use rocket::async_trait; use rocket::async_trait;
use rocket::yansi::Paint;
use std::ops::Add;
use std::path::PathBuf;
use std::time::Duration;
#[async_trait] #[async_trait]
pub trait Provisioner: Send + Sync { pub trait Provisioner: Send + Sync {
/// Provision a new VM /// Provision a new VM
async fn provision(&self, spec: VmTemplate) -> Result<Vm>; async fn provision(
&self,
user_id: u64,
template_id: u64,
image_id: u64,
ssh_key_id: u64,
) -> Result<Vm>;
/// Create a renewal payment
async fn renew(&self, vm_id: u64) -> Result<VmPayment>;
} }
pub struct LNVpsProvisioner { pub struct LNVpsProvisioner {
db: Box<dyn LNVpsDb>, db: Box<dyn LNVpsDb>,
lnd: Client,
} }
impl LNVpsProvisioner { impl LNVpsProvisioner {
pub fn new(db: impl LNVpsDb + 'static) -> Self { pub fn new(db: impl LNVpsDb + 'static, lnd: Client) -> Self {
Self { db: Box::new(db) } Self {
db: Box::new(db),
lnd,
}
} }
/// Auto-discover resources /// Auto-discover resources
@ -35,7 +55,7 @@ impl LNVpsProvisioner {
host.cpu = node.max_cpu.unwrap_or(host.cpu); host.cpu = node.max_cpu.unwrap_or(host.cpu);
host.memory = node.max_mem.unwrap_or(host.memory); host.memory = node.max_mem.unwrap_or(host.memory);
info!("Patching host: {:?}", host); info!("Patching host: {:?}", host);
self.db.update_host(host).await?; self.db.update_host(&host).await?;
} }
// Update disk info // Update disk info
let storages = api.list_storage().await?; let storages = api.list_storage().await?;
@ -61,67 +81,113 @@ impl LNVpsProvisioner {
Ok(()) Ok(())
} }
fn map_os_image(image: &VmOsImage) -> PathBuf {
PathBuf::from("/var/lib/vz/images/").join(format!(
"{:?}_{}_{}.img",
image.distribution, image.flavour, image.version
))
}
} }
#[async_trait] #[async_trait]
impl Provisioner for LNVpsProvisioner { impl Provisioner for LNVpsProvisioner {
async fn provision(&self, spec: VmTemplate) -> Result<Vm> { async fn provision(
&self,
user_id: u64,
template_id: u64,
image_id: u64,
ssh_key_id: u64,
) -> Result<Vm> {
let user = self.db.get_user(user_id).await?;
let template = self.db.get_vm_template(template_id).await?;
let image = self.db.get_os_image(image_id).await?;
let ssh_key = self.db.get_user_ssh_key(ssh_key_id).await?;
let hosts = self.db.list_hosts().await?; let hosts = self.db.list_hosts().await?;
// try any host
// TODO: impl resource usage based provisioning // TODO: impl resource usage based provisioning
for host in hosts { let pick_host = if let Some(h) = hosts.first() {
let api = ProxmoxClient::new(host.ip.parse()?).with_api_token(&host.api_token); h
} else {
bail!("No host found")
};
let host_disks = self.db.list_host_disks(pick_host.id).await?;
let pick_disk = if let Some(hd) = host_disks.first() {
hd
} else {
bail!("No host disk found")
};
let nodes = api.list_nodes().await?; let mut new_vm = Vm {
let node = if let Some(n) = nodes.iter().find(|n| n.name == host.name) { host_id: pick_host.id,
n user_id: user.id,
} else { image_id: image.id,
continue; template_id: template.id,
ssh_key_id: ssh_key.id,
created: Utc::now(),
expires: Utc::now(),
cpu: template.cpu,
memory: template.memory,
disk_size: template.disk_size,
disk_id: pick_disk.id,
..Default::default()
};
let new_id = self.db.insert_vm(&new_vm).await?;
new_vm.id = new_id;
Ok(new_vm)
}
async fn renew(&self, vm_id: u64) -> Result<VmPayment> {
let vm = self.db.get_vm(vm_id).await?;
let template = self.db.get_vm_template(vm.template_id).await?;
let cost_plan = self.db.get_cost_plan(template.cost_plan_id).await?;
// push the expiration forward by cost plan interval amount
let new_expire = match cost_plan.interval_type {
VmCostPlanIntervalType::Day => vm.expires.add(Days::new(cost_plan.interval_amount)),
VmCostPlanIntervalType::Month => vm
.expires
.add(Months::new(cost_plan.interval_amount as u32)),
VmCostPlanIntervalType::Year => vm
.expires
.add(Months::new((12 * cost_plan.interval_amount) as u32)),
};
const BTC_MILLI_SATS: u64 = 100_000_000_000;
const INVOICE_EXPIRE: i64 = 3600;
let cost = cost_plan.amount
* match cost_plan.currency.as_str() {
"EUR" => 1_100_000, //TODO: rates
"BTC" => 1, // BTC amounts are always millisats
c => bail!("Unknown currency {c}"),
}; };
let host_disks = self.db.list_host_disks(host.id).await?; info!("Creating invoice for {vm_id} for {cost} mSats");
let disk_name = if let Some(d) = host_disks.first() { let mut lnd = self.lnd.clone();
d let invoice = lnd
} else { .lightning()
continue; .add_invoice(Invoice {
}; memo: format!("VM renewal {vm_id} to {new_expire}"),
let next_id = 101; value_msat: cost as i64,
let vm_result = api expiry: INVOICE_EXPIRE,
.create_vm( ..Default::default()
&node.name, })
CreateVm { .await?;
vm_id: next_id,
bios: Some(VmBios::OVMF),
boot: Some("order=scsi0".to_string()),
cores: Some(spec.cpu as i32),
cpu: Some("kvm64".to_string()),
memory: Some((spec.memory / 1024 / 1024).to_string()),
machine: Some("q35".to_string()),
scsi_hw: Some("virtio-scsi-pci".to_string()),
efi_disk_0: Some(format!("{}:vm-{next_id}-efi,size=1M", &disk_name.name)),
net: Some("virtio=auto,bridge=vmbr0,tag=100".to_string()),
ip_config: Some(format!("ip=auto,ipv6=auto")),
..Default::default()
},
)
.await?;
return Ok(Vm { let mut vm_payment = VmPayment {
id: 0, id: 0,
host_id: 0, vm_id,
user_id: 0, created: Utc::now(),
image_id: 0, expires: Utc::now().add(Duration::from_secs(INVOICE_EXPIRE as u64)),
template_id: 0, amount: cost,
ssh_key_id: 0, invoice: invoice.into_inner().payment_request,
created: Default::default(), time_value: (new_expire - vm.expires).num_seconds() as u64,
expires: Default::default(), is_paid: false,
cpu: 0, };
memory: 0, let payment_id = self.db.insert_vm_payment(&vm_payment).await?;
disk_size: 0, vm_payment.id = payment_id;
disk_id: 0,
});
}
bail!("Failed to create VM") Ok(vm_payment)
} }
} }