api progress
This commit is contained in:
parent
13f59908fb
commit
a0e49d83bd
890
Cargo.lock
generated
890
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -20,3 +20,6 @@ rocket = { version = "0.5.1", features = ["json"] }
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
nostr = { version = "0.36.0", default-features = false, features = ["std"] }
|
||||
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"] }
|
||||
|
@ -1,2 +1,6 @@
|
||||
# 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"
|
@ -1,15 +1,62 @@
|
||||
insert
|
||||
ignore into vm_host_region(id,name,enabled) values(1,"uat",1);
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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);
|
@ -6,7 +6,7 @@ create table users
|
||||
email varchar(200),
|
||||
contact_nip4 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_email on users (email);
|
||||
@ -55,14 +55,14 @@ create table vm_host_disk
|
||||
create table vm_os_image
|
||||
(
|
||||
id integer unsigned not null auto_increment primary key,
|
||||
name varchar(200) not null,
|
||||
distribution smallint unsigned not null,
|
||||
flavour varchar(50) not null,
|
||||
version varchar(50) 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
|
||||
(
|
||||
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)
|
||||
);
|
||||
create unique index ix_ip_range_cidr on ip_range (cidr);
|
||||
create table vm_cost_plan
|
||||
(
|
||||
id integer unsigned not null auto_increment primary key,
|
||||
|
48
lnvps_db/src/hydrate.rs
Normal file
48
lnvps_db/src/hydrate.rs
Normal 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!()
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ use async_trait::async_trait;
|
||||
mod model;
|
||||
#[cfg(feature = "mysql")]
|
||||
mod mysql;
|
||||
pub mod hydrate;
|
||||
|
||||
pub use model::*;
|
||||
#[cfg(feature = "mysql")]
|
||||
@ -27,7 +28,7 @@ pub trait LNVpsDb: Sync + Send {
|
||||
async fn delete_user(&self, id: u64) -> Result<()>;
|
||||
|
||||
/// 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
|
||||
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>>;
|
||||
|
||||
/// 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
|
||||
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
|
||||
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
|
||||
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
|
||||
async fn list_vm_templates(&self) -> Result<Vec<VmTemplate>>;
|
||||
|
||||
/// List VM's owned by a specific user
|
||||
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
|
||||
async fn insert_vm(&self, vm: Vm) -> Result<u64>;
|
||||
async fn insert_vm(&self, vm: &Vm) -> Result<u64>;
|
||||
|
||||
/// List VM ip assignments
|
||||
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>>;
|
||||
|
||||
/// 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
|
||||
async fn update_vm_payment(&self, vm_payment: VmPayment) -> Result<()>;
|
||||
async fn update_vm_payment(&self, vm_payment: &VmPayment) -> Result<()>;
|
||||
}
|
@ -18,12 +18,13 @@ pub struct User {
|
||||
pub contact_email: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)]
|
||||
pub struct UserSshKey {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub user_id: u64,
|
||||
pub created: DateTime<Utc>,
|
||||
#[serde(skip_serializing)]
|
||||
pub key_data: String,
|
||||
|
||||
#[sqlx(skip)]
|
||||
@ -79,24 +80,29 @@ pub struct VmHostDisk {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[repr(u16)]
|
||||
pub enum DiskType {
|
||||
#[default]
|
||||
HDD = 0,
|
||||
SSD = 1,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[repr(u16)]
|
||||
pub enum DiskInterface {
|
||||
#[default]
|
||||
SATA = 0,
|
||||
SCSI = 1,
|
||||
PCIe = 2,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)]
|
||||
#[repr(u16)]
|
||||
pub enum OsDistribution {
|
||||
#[default]
|
||||
Ubuntu = 0,
|
||||
Debian = 1,
|
||||
}
|
||||
@ -106,11 +112,12 @@ pub enum OsDistribution {
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
|
||||
pub struct VmOsImage {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub distribution: OsDistribution,
|
||||
pub flavour: String,
|
||||
pub version: String,
|
||||
pub enabled: bool,
|
||||
pub release_date: DateTime<Utc>,
|
||||
#[serde(skip_serializing)]
|
||||
/// URL location of cloud image
|
||||
pub url: String,
|
||||
}
|
||||
@ -124,6 +131,7 @@ pub struct IpRange {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[repr(u16)]
|
||||
pub enum VmCostPlanIntervalType {
|
||||
Day = 0,
|
||||
@ -144,7 +152,7 @@ pub struct VmCostPlan {
|
||||
|
||||
/// Offers.
|
||||
/// 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 id: u64,
|
||||
pub name: String,
|
||||
@ -154,6 +162,7 @@ pub struct VmTemplate {
|
||||
pub expires: Option<DateTime<Utc>>,
|
||||
pub cpu: u16,
|
||||
pub memory: u64,
|
||||
pub disk_size: u64,
|
||||
pub disk_type: DiskType,
|
||||
pub disk_interface: DiskInterface,
|
||||
pub cost_plan_id: u64,
|
||||
@ -167,7 +176,7 @@ pub struct VmTemplate {
|
||||
pub region: Option<VmHostRegion>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)]
|
||||
pub struct Vm {
|
||||
/// Unique VM ID (Same in proxmox)
|
||||
pub id: u64,
|
||||
@ -193,6 +202,19 @@ pub struct Vm {
|
||||
pub disk_size: u64,
|
||||
/// The [VmHostDisk] this VM is on
|
||||
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)]
|
||||
|
@ -25,11 +25,11 @@ impl LNVpsDbMysql {
|
||||
|
||||
#[async_trait]
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
.bind(pubkey.as_slice())
|
||||
.fetch_optional(&self.db)
|
||||
@ -46,7 +46,11 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
}
|
||||
|
||||
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<()> {
|
||||
@ -57,12 +61,23 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn insert_user_ssh_key(&self, new_key: UserSshKey) -> Result<u64> {
|
||||
todo!()
|
||||
async fn insert_user_ssh_key(&self, new_key: &UserSshKey) -> Result<u64> {
|
||||
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> {
|
||||
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<()> {
|
||||
@ -70,21 +85,29 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
}
|
||||
|
||||
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> {
|
||||
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")
|
||||
.fetch_all(&self.db)
|
||||
.await
|
||||
.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 = ?")
|
||||
.bind(&host.name)
|
||||
.bind(&host.cpu)
|
||||
@ -95,7 +118,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
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 = ?")
|
||||
.bind(&host_id)
|
||||
.fetch_all(&self.db)
|
||||
@ -103,20 +126,46 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
.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>> {
|
||||
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>> {
|
||||
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> {
|
||||
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>> {
|
||||
sqlx::query_as("select * from vm_template where enabled = 1 and (expires is null or expires > now())")
|
||||
async fn get_vm_template(&self, id: u64) -> Result<VmTemplate> {
|
||||
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)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
@ -130,23 +179,71 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
.map_err(Error::new)
|
||||
}
|
||||
|
||||
async fn insert_vm(&self, vm: Vm) -> Result<u64> {
|
||||
todo!()
|
||||
async fn get_vm(&self, vm_id: u64) -> Result<Vm> {
|
||||
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>> {
|
||||
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>> {
|
||||
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> {
|
||||
todo!()
|
||||
async fn insert_vm_payment(&self, vm_payment: &VmPayment) -> Result<u64> {
|
||||
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<()> {
|
||||
todo!()
|
||||
async fn update_vm_payment(&self, vm_payment: &VmPayment) -> Result<()> {
|
||||
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(())
|
||||
}
|
||||
}
|
131
src/api.rs
131
src/api.rs
@ -1,13 +1,22 @@
|
||||
use crate::nip98::Nip98Auth;
|
||||
use crate::provisioner::Provisioner;
|
||||
use anyhow::Error;
|
||||
use lnvps_db::{LNVpsDb, Vm, VmTemplate};
|
||||
use lnvps_db::hydrate::Hydrate;
|
||||
use lnvps_db::{LNVpsDb, UserSshKey, Vm, VmOsImage, VmPayment, VmTemplate};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{get, post, routes, Responder, Route, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssh_key::PublicKey;
|
||||
|
||||
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>;
|
||||
@ -21,6 +30,9 @@ impl<T: Serialize> ApiData<T> {
|
||||
pub fn ok(data: T) -> ApiResult<T> {
|
||||
Ok(Json::from(ApiData { data }))
|
||||
}
|
||||
pub fn err(msg: &str) -> ApiResult<T> {
|
||||
Err(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Responder)]
|
||||
@ -29,30 +41,86 @@ struct ApiError {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
impl From<Error> for ApiError {
|
||||
fn from(value: Error) -> Self {
|
||||
impl ApiError {
|
||||
pub fn new(error: &str) -> Self {
|
||||
Self {
|
||||
error: error.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToString> From<T> for ApiError {
|
||||
fn from(value: T) -> Self {
|
||||
Self {
|
||||
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>> {
|
||||
let pubkey = auth.event.pubkey.to_bytes();
|
||||
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)
|
||||
}
|
||||
|
||||
#[get("/api/v1/vm/templates")]
|
||||
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)
|
||||
}
|
||||
|
||||
#[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")]
|
||||
async fn v1_provision_vm(
|
||||
async fn v1_create_vm_order(
|
||||
auth: Nip98Auth,
|
||||
db: &State<Box<dyn LNVpsDb>>,
|
||||
provisioner: &State<Box<dyn Provisioner>>,
|
||||
@ -62,15 +130,50 @@ async fn v1_provision_vm(
|
||||
let uid = db.upsert_user(&pubkey).await?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateVmRequest {}
|
||||
struct CreateVmRequest {
|
||||
template_id: u64,
|
||||
image_id: u64,
|
||||
ssh_key_id: u64,
|
||||
}
|
||||
|
||||
impl Into<VmTemplate> for CreateVmRequest {
|
||||
fn into(self) -> VmTemplate {
|
||||
todo!()
|
||||
impl From<CreateVmRequest> for VmTemplate {
|
||||
fn from(val: CreateVmRequest) -> Self {
|
||||
VmTemplate {
|
||||
id: val.template_id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateSshKey {
|
||||
name: String,
|
||||
key_data: String,
|
||||
}
|
||||
|
@ -1,15 +1,25 @@
|
||||
use anyhow::Error;
|
||||
use config::{Config, File};
|
||||
use fedimint_tonic_lnd::connect;
|
||||
use lnvps::api;
|
||||
use lnvps::cors::CORS;
|
||||
use lnvps::provisioner::{LNVpsProvisioner, Provisioner};
|
||||
use lnvps_db::{LNVpsDb, LNVpsDbMysql};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Settings {
|
||||
pub db: String,
|
||||
pub lnd: LndConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct LndConfig {
|
||||
pub url: String,
|
||||
pub cert: PathBuf,
|
||||
pub macaroon: PathBuf,
|
||||
}
|
||||
|
||||
#[rocket::main]
|
||||
@ -24,7 +34,8 @@ async fn main() -> Result<(), Error> {
|
||||
let db = LNVpsDbMysql::new(&config.db).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)]
|
||||
{
|
||||
let setup_script = include_str!("../../dev_setup.sql");
|
||||
|
@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use reqwest::{ClientBuilder, Url};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -42,46 +43,56 @@ impl ProxmoxClient {
|
||||
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>> {
|
||||
let rsp: ResponseBase<Vec<VmInfo>> =
|
||||
self.get(&format!("/api2/json/nodes/{node}/qemu")).await?;
|
||||
Ok(rsp.data)
|
||||
}
|
||||
|
||||
pub async fn list_storage(&self) -> Result<Vec<NodeStorage>> {
|
||||
let rsp: ResponseBase<Vec<NodeStorage>> = self.get("/api2/json/storage").await?;
|
||||
Ok(rsp.data)
|
||||
}
|
||||
|
||||
pub async fn create_vm(&self, node: &str, req: CreateVm) -> Result<VmInfo> {
|
||||
let rsp: ResponseBase<VmInfo> = self
|
||||
.post(&format!("/api2/json/nodes/{node}/qemu"), req)
|
||||
pub async fn create_vm(&self, req: CreateVm) -> Result<VmInfo> {
|
||||
info!("{}", serde_json::to_string_pretty(&req)?);
|
||||
let _rsp: ResponseBase<Option<String>> = self
|
||||
.post(&format!("/api2/json/nodes/{}/qemu", req.node), &req)
|
||||
.await?;
|
||||
Ok(rsp.data)
|
||||
self.get_vm_status(&req.node, req.vm_id).await
|
||||
}
|
||||
|
||||
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
|
||||
Ok(self
|
||||
.client
|
||||
self.client
|
||||
.get(self.base.join(path)?)
|
||||
.header("Authorization", format!("PVEAPIToken={}", self.token))
|
||||
.send()
|
||||
.await?
|
||||
.json::<T>()
|
||||
.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> {
|
||||
Ok(self
|
||||
let rsp = self
|
||||
.client
|
||||
.post(self.base.join(path)?)
|
||||
.header("Authorization", format!("PVEAPIToken={}", self.token))
|
||||
.json(&body)
|
||||
.json::<R>(&body)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?)
|
||||
.await?;
|
||||
let rsp = rsp.text().await?;
|
||||
info!("<< {}", rsp);
|
||||
Ok(serde_json::from_str(&rsp)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,22 +1,42 @@
|
||||
use crate::host::proxmox::{CreateVm, ProxmoxClient, VmBios};
|
||||
use crate::host::proxmox::ProxmoxClient;
|
||||
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 rocket::async_trait;
|
||||
use rocket::yansi::Paint;
|
||||
use std::ops::Add;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Provisioner: Send + Sync {
|
||||
/// 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 {
|
||||
db: Box<dyn LNVpsDb>,
|
||||
lnd: Client,
|
||||
}
|
||||
|
||||
impl LNVpsProvisioner {
|
||||
pub fn new(db: impl LNVpsDb + 'static) -> Self {
|
||||
Self { db: Box::new(db) }
|
||||
pub fn new(db: impl LNVpsDb + 'static, lnd: Client) -> Self {
|
||||
Self {
|
||||
db: Box::new(db),
|
||||
lnd,
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-discover resources
|
||||
@ -35,7 +55,7 @@ impl LNVpsProvisioner {
|
||||
host.cpu = node.max_cpu.unwrap_or(host.cpu);
|
||||
host.memory = node.max_mem.unwrap_or(host.memory);
|
||||
info!("Patching host: {:?}", host);
|
||||
self.db.update_host(host).await?;
|
||||
self.db.update_host(&host).await?;
|
||||
}
|
||||
// Update disk info
|
||||
let storages = api.list_storage().await?;
|
||||
@ -61,67 +81,113 @@ impl LNVpsProvisioner {
|
||||
|
||||
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]
|
||||
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?;
|
||||
|
||||
// try any host
|
||||
// TODO: impl resource usage based provisioning
|
||||
for host in hosts {
|
||||
let api = ProxmoxClient::new(host.ip.parse()?).with_api_token(&host.api_token);
|
||||
let pick_host = if let Some(h) = hosts.first() {
|
||||
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 node = if let Some(n) = nodes.iter().find(|n| n.name == host.name) {
|
||||
n
|
||||
} else {
|
||||
continue;
|
||||
let mut new_vm = Vm {
|
||||
host_id: pick_host.id,
|
||||
user_id: user.id,
|
||||
image_id: image.id,
|
||||
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?;
|
||||
let disk_name = if let Some(d) = host_disks.first() {
|
||||
d
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let next_id = 101;
|
||||
let vm_result = api
|
||||
.create_vm(
|
||||
&node.name,
|
||||
CreateVm {
|
||||
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?;
|
||||
info!("Creating invoice for {vm_id} for {cost} mSats");
|
||||
let mut lnd = self.lnd.clone();
|
||||
let invoice = lnd
|
||||
.lightning()
|
||||
.add_invoice(Invoice {
|
||||
memo: format!("VM renewal {vm_id} to {new_expire}"),
|
||||
value_msat: cost as i64,
|
||||
expiry: INVOICE_EXPIRE,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
return Ok(Vm {
|
||||
id: 0,
|
||||
host_id: 0,
|
||||
user_id: 0,
|
||||
image_id: 0,
|
||||
template_id: 0,
|
||||
ssh_key_id: 0,
|
||||
created: Default::default(),
|
||||
expires: Default::default(),
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
disk_size: 0,
|
||||
disk_id: 0,
|
||||
});
|
||||
}
|
||||
let mut vm_payment = VmPayment {
|
||||
id: 0,
|
||||
vm_id,
|
||||
created: Utc::now(),
|
||||
expires: Utc::now().add(Duration::from_secs(INVOICE_EXPIRE as u64)),
|
||||
amount: cost,
|
||||
invoice: invoice.into_inner().payment_request,
|
||||
time_value: (new_expire - vm.expires).num_seconds() as u64,
|
||||
is_paid: false,
|
||||
};
|
||||
let payment_id = self.db.insert_vm_payment(&vm_payment).await?;
|
||||
vm_payment.id = payment_id;
|
||||
|
||||
bail!("Failed to create VM")
|
||||
Ok(vm_payment)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user