160
lnvps_db/Cargo.lock
generated
160
lnvps_db/Cargo.lock
generated
@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
@ -245,41 +245,6 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
@ -291,16 +256,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@ -394,12 +349,6 @@ dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@ -508,12 +457,6 @@ version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
@ -725,12 +668,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
@ -752,17 +689,6 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.6.0"
|
||||
@ -771,7 +697,6 @@ checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -840,8 +765,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_with",
|
||||
"sqlx",
|
||||
"url",
|
||||
]
|
||||
@ -932,12 +855,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
@ -1072,12 +989,6 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
@ -1239,36 +1150,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.6.0",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@ -1399,7 +1280,7 @@ dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"hashlink",
|
||||
"hex",
|
||||
"indexmap 2.6.0",
|
||||
"indexmap",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
@ -1579,12 +1460,6 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@ -1646,37 +1521,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
|
49
lnvps_db/migrations/20250306113236_custom_pricing.sql
Normal file
49
lnvps_db/migrations/20250306113236_custom_pricing.sql
Normal file
@ -0,0 +1,49 @@
|
||||
-- fix this fk ref
|
||||
ALTER TABLE vm_template DROP FOREIGN KEY fk_template_region;
|
||||
alter table vm_template
|
||||
add constraint fk_template_region foreign key (region_id) references vm_host_region (id);
|
||||
|
||||
create table vm_custom_pricing
|
||||
(
|
||||
id integer unsigned not null auto_increment primary key,
|
||||
name varchar(100) not null,
|
||||
enabled bit(1) not null,
|
||||
created timestamp default current_timestamp,
|
||||
expires timestamp,
|
||||
region_id integer unsigned not null,
|
||||
currency varchar(5) not null,
|
||||
cpu_cost float not null,
|
||||
memory_cost float not null,
|
||||
ip4_cost float not null,
|
||||
ip6_cost float not null,
|
||||
|
||||
constraint fk_custom_pricing_region foreign key (region_id) references vm_host_region (id)
|
||||
);
|
||||
create table vm_custom_pricing_disk
|
||||
(
|
||||
id integer unsigned not null auto_increment primary key,
|
||||
pricing_id integer unsigned not null,
|
||||
kind smallint unsigned not null,
|
||||
interface smallint unsigned not null,
|
||||
cost float not null,
|
||||
|
||||
constraint fk_custom_pricing_disk foreign key (pricing_id) references vm_custom_pricing (id)
|
||||
);
|
||||
ALTER TABLE vm MODIFY COLUMN template_id int (10) unsigned NULL;
|
||||
ALTER TABLE vm
|
||||
add COLUMN custom_template_id int(10) unsigned NULL;
|
||||
|
||||
create table vm_custom_template
|
||||
(
|
||||
id integer unsigned not null auto_increment primary key,
|
||||
cpu tinyint unsigned not null,
|
||||
memory bigint unsigned not null,
|
||||
disk_size bigint unsigned not null,
|
||||
disk_type smallint unsigned not null,
|
||||
disk_interface smallint unsigned not null,
|
||||
pricing_id integer unsigned not null,
|
||||
|
||||
constraint fk_custom_template_pricing foreign key (pricing_id) references vm_custom_pricing (id)
|
||||
);
|
||||
alter table vm
|
||||
add constraint fk_vm_custom_template foreign key (custom_template_id) references vm_custom_template (id);
|
@ -139,4 +139,19 @@ pub trait LNVpsDb: Sync + Send {
|
||||
|
||||
/// Return the most recently settled invoice
|
||||
async fn last_paid_invoice(&self) -> Result<Option<VmPayment>>;
|
||||
|
||||
/// Return the list of active custom pricing models for a given region
|
||||
async fn list_custom_pricing(&self, region_id: u64) -> Result<Vec<VmCustomPricing>>;
|
||||
|
||||
/// Get a custom pricing model
|
||||
async fn get_custom_pricing(&self, id: u64) -> Result<VmCustomPricing>;
|
||||
|
||||
/// Get a custom pricing model
|
||||
async fn get_custom_vm_template(&self, id: u64) -> Result<VmCustomTemplate>;
|
||||
|
||||
/// Insert custom vm template
|
||||
async fn insert_custom_vm_template(&self, template: &VmCustomTemplate) -> Result<u64>;
|
||||
|
||||
/// Return the list of disk prices for a given custom pricing model
|
||||
async fn list_custom_pricing_disk(&self, pricing_id: u64) -> Result<Vec<VmCustomPricingDisk>>;
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ pub struct VmHostDisk {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, sqlx::Type, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, sqlx::Type, Default, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum DiskType {
|
||||
#[default]
|
||||
@ -100,7 +100,7 @@ pub enum DiskType {
|
||||
SSD = 1,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, sqlx::Type, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, sqlx::Type, Default, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum DiskInterface {
|
||||
#[default]
|
||||
@ -109,7 +109,7 @@ pub enum DiskInterface {
|
||||
PCIe = 2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, sqlx::Type, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, sqlx::Type, Default, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum OsDistribution {
|
||||
#[default]
|
||||
@ -203,6 +203,50 @@ pub struct VmTemplate {
|
||||
pub region_id: u64,
|
||||
}
|
||||
|
||||
/// A custom pricing template, used for billing calculation of a specific VM
|
||||
/// This mostly just stores the number of resources assigned and the specific pricing used
|
||||
#[derive(FromRow, Clone, Debug, Default)]
|
||||
pub struct VmCustomTemplate {
|
||||
pub id: u64,
|
||||
pub cpu: u16,
|
||||
pub memory: u64,
|
||||
pub disk_size: u64,
|
||||
pub disk_type: DiskType,
|
||||
pub disk_interface: DiskInterface,
|
||||
pub pricing_id: u64,
|
||||
}
|
||||
|
||||
/// Custom pricing template, usually 1 per region
|
||||
#[derive(FromRow, Clone, Debug, Default)]
|
||||
pub struct VmCustomPricing {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub enabled: bool,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expires: Option<DateTime<Utc>>,
|
||||
pub region_id: u64,
|
||||
pub currency: String,
|
||||
/// Cost per CPU core
|
||||
pub cpu_cost: f32,
|
||||
/// Cost per GB ram
|
||||
pub memory_cost: f32,
|
||||
/// Cost per IPv4 address
|
||||
pub ip4_cost: f32,
|
||||
/// Cost per IPv6 address
|
||||
pub ip6_cost: f32,
|
||||
}
|
||||
|
||||
/// Pricing per GB on a disk type (SSD/HDD)
|
||||
#[derive(FromRow, Clone, Debug, Default)]
|
||||
pub struct VmCustomPricingDisk {
|
||||
pub id: u64,
|
||||
pub pricing_id: u64,
|
||||
pub kind: DiskType,
|
||||
pub interface: DiskInterface,
|
||||
/// Cost as per the currency of the [VmCustomPricing::currency]
|
||||
pub cost: f32,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Clone, Debug, Default)]
|
||||
pub struct Vm {
|
||||
/// Unique VM ID (Same in proxmox)
|
||||
@ -213,8 +257,10 @@ pub struct Vm {
|
||||
pub user_id: u64,
|
||||
/// The base image of this VM
|
||||
pub image_id: u64,
|
||||
/// The base image of this VM
|
||||
pub template_id: u64,
|
||||
/// The base image of this VM [VmTemplate]
|
||||
pub template_id: Option<u64>,
|
||||
/// Custom pricing specification used for this vm [VmCustomTemplate]
|
||||
pub custom_template_id: Option<u64>,
|
||||
/// Users ssh-key assigned to this VM
|
||||
pub ssh_key_id: u64,
|
||||
/// When the VM was created
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
IpRange, LNVpsDb, User, UserSshKey, Vm, VmCostPlan, VmHost, VmHostDisk, VmHostRegion,
|
||||
VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
||||
IpRange, LNVpsDb, User, UserSshKey, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk,
|
||||
VmCustomTemplate, VmHost, VmHostDisk, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment,
|
||||
VmTemplate,
|
||||
};
|
||||
use anyhow::{bail, Error, Result};
|
||||
use async_trait::async_trait;
|
||||
@ -27,7 +28,9 @@ impl LNVpsDbMysql {
|
||||
#[async_trait]
|
||||
impl LNVpsDb for LNVpsDbMysql {
|
||||
async fn migrate(&self) -> Result<()> {
|
||||
sqlx::migrate!().run(&self.db).await.map_err(Error::new)
|
||||
let migrator = sqlx::migrate!();
|
||||
migrator.run(&self.db).await.map_err(Error::new)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upsert_user(&self, pubkey: &[u8; 32]) -> Result<u64> {
|
||||
@ -210,7 +213,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
}
|
||||
|
||||
async fn list_vm_templates(&self) -> Result<Vec<VmTemplate>> {
|
||||
sqlx::query_as("select * from vm_template")
|
||||
sqlx::query_as("select * from vm_template where enabled = 1")
|
||||
.fetch_all(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
@ -219,14 +222,14 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
async fn insert_vm_template(&self, template: &VmTemplate) -> Result<u64> {
|
||||
Ok(sqlx::query("insert into vm_template(name,enabled,created,expires,cpu,memory,disk_size,disk_type,disk_interface,cost_plan_id,region_id) values(?,?,?,?,?,?,?,?,?,?,?) returning id")
|
||||
.bind(&template.name)
|
||||
.bind(&template.enabled)
|
||||
.bind(&template.created)
|
||||
.bind(&template.expires)
|
||||
.bind(template.enabled)
|
||||
.bind(template.created)
|
||||
.bind(template.expires)
|
||||
.bind(template.cpu)
|
||||
.bind(template.memory)
|
||||
.bind(template.disk_size)
|
||||
.bind(&template.disk_type)
|
||||
.bind(&template.disk_interface)
|
||||
.bind(template.disk_type)
|
||||
.bind(template.disk_interface)
|
||||
.bind(template.cost_plan_id)
|
||||
.bind(template.region_id)
|
||||
.fetch_one(&self.db)
|
||||
@ -274,11 +277,12 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
}
|
||||
|
||||
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,disk_id,mac_address,ref_code) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?) returning id")
|
||||
Ok(sqlx::query("insert into vm(host_id,user_id,image_id,template_id,custom_template_id,ssh_key_id,created,expires,disk_id,mac_address,ref_code) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?) returning id")
|
||||
.bind(vm.host_id)
|
||||
.bind(vm.user_id)
|
||||
.bind(vm.image_id)
|
||||
.bind(vm.template_id)
|
||||
.bind(vm.custom_template_id)
|
||||
.bind(vm.ssh_key_id)
|
||||
.bind(vm.created)
|
||||
.bind(vm.expires)
|
||||
@ -343,7 +347,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
.bind(&ip_assignment.dns_forward_ref)
|
||||
.bind(&ip_assignment.dns_reverse)
|
||||
.bind(&ip_assignment.dns_reverse_ref)
|
||||
.bind(&ip_assignment.id)
|
||||
.bind(ip_assignment.id)
|
||||
.execute(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)?;
|
||||
@ -368,7 +372,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
|
||||
async fn delete_vm_ip_assignment(&self, vm_id: u64) -> Result<()> {
|
||||
sqlx::query("update vm_ip_assignment set deleted = 1 where vm_id = ?")
|
||||
.bind(&vm_id)
|
||||
.bind(vm_id)
|
||||
.execute(&self.db)
|
||||
.await?;
|
||||
Ok(())
|
||||
@ -448,4 +452,50 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
}
|
||||
|
||||
async fn list_custom_pricing(&self, region_id: u64) -> Result<Vec<VmCustomPricing>> {
|
||||
sqlx::query_as("select * from vm_custom_pricing where region_id = ? and enabled = 1")
|
||||
.bind(region_id)
|
||||
.fetch_all(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
}
|
||||
|
||||
async fn get_custom_pricing(&self, id: u64) -> Result<VmCustomPricing> {
|
||||
sqlx::query_as("select * from vm_custom_pricing where id=?")
|
||||
.bind(id)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
}
|
||||
|
||||
async fn get_custom_vm_template(&self, id: u64) -> Result<VmCustomTemplate> {
|
||||
sqlx::query_as("select * from vm_custom_template where id=?")
|
||||
.bind(id)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
}
|
||||
|
||||
async fn insert_custom_vm_template(&self, template: &VmCustomTemplate) -> Result<u64> {
|
||||
Ok(sqlx::query("insert into vm_custom_template(cpu,memory,disk_size,disk_type,disk_interface,pricing_id) values(?,?,?,?,?,?) returning id")
|
||||
.bind(template.cpu)
|
||||
.bind(template.memory)
|
||||
.bind(template.disk_size)
|
||||
.bind(template.disk_type)
|
||||
.bind(template.disk_interface)
|
||||
.bind(template.pricing_id)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)?
|
||||
.try_get(0)?)
|
||||
}
|
||||
|
||||
async fn list_custom_pricing_disk(&self, pricing_id: u64) -> Result<Vec<VmCustomPricingDisk>> {
|
||||
sqlx::query_as("select * from vm_custom_pricing_disk where pricing_id=?")
|
||||
.bind(pricing_id)
|
||||
.fetch_all(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
}
|
||||
}
|
||||
|
172
src/api/model.rs
172
src/api/model.rs
@ -1,11 +1,15 @@
|
||||
use crate::exchange::Currency;
|
||||
use crate::provisioner::PricingEngine;
|
||||
use crate::status::VmState;
|
||||
use anyhow::{bail, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use ipnetwork::IpNetwork;
|
||||
use lnvps_db::VmHostRegion;
|
||||
use lnvps_db::{LNVpsDb, Vm, VmCostPlan, VmCustomTemplate, VmHost, VmHostRegion, VmTemplate};
|
||||
use nostr::util::hex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApiVmStatus {
|
||||
@ -72,7 +76,7 @@ impl ApiVmIpAssignment {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DiskType {
|
||||
HDD = 0,
|
||||
@ -88,7 +92,16 @@ impl From<lnvps_db::DiskType> for DiskType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
impl Into<lnvps_db::DiskType> for DiskType {
|
||||
fn into(self) -> lnvps_db::DiskType {
|
||||
match self {
|
||||
DiskType::HDD => lnvps_db::DiskType::HDD,
|
||||
DiskType::SSD => lnvps_db::DiskType::SSD,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DiskInterface {
|
||||
SATA = 0,
|
||||
@ -106,6 +119,82 @@ impl From<lnvps_db::DiskInterface> for DiskInterface {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DiskInterface> for lnvps_db::DiskInterface {
|
||||
fn from(value: DiskInterface) -> Self {
|
||||
match value {
|
||||
DiskInterface::SATA => Self::SATA,
|
||||
DiskInterface::SCSI => Self::SCSI,
|
||||
DiskInterface::PCIe => Self::PCIe,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApiTemplatesResponse {
|
||||
pub templates: Vec<ApiVmTemplate>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_template: Option<Vec<ApiCustomTemplateParams>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApiCustomTemplateParams {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub region: ApiVmHostRegion,
|
||||
pub max_cpu: u16,
|
||||
pub min_cpu: u16,
|
||||
pub min_memory: u64,
|
||||
pub max_memory: u64,
|
||||
pub min_disk: u64,
|
||||
pub max_disk: u64,
|
||||
pub disks: Vec<ApiCustomTemplateDiskParam>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApiCustomTemplateDiskParam {
|
||||
pub disk_type: DiskType,
|
||||
pub disk_interface: DiskInterface,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApiCustomVmRequest {
|
||||
pub pricing_id: u64,
|
||||
pub cpu: u16,
|
||||
pub memory: u64,
|
||||
pub disk: u64,
|
||||
pub disk_type: DiskType,
|
||||
pub disk_interface: DiskInterface,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApiCustomVmOrder {
|
||||
#[serde(flatten)]
|
||||
pub spec: ApiCustomVmRequest,
|
||||
pub image_id: u64,
|
||||
pub ssh_key_id: u64,
|
||||
pub ref_code: Option<String>,
|
||||
}
|
||||
|
||||
impl From<ApiCustomVmRequest> for VmCustomTemplate {
|
||||
fn from(value: ApiCustomVmRequest) -> Self {
|
||||
VmCustomTemplate {
|
||||
id: 0,
|
||||
cpu: value.cpu,
|
||||
memory: value.memory,
|
||||
disk_size: value.disk,
|
||||
disk_type: value.disk_type.into(),
|
||||
disk_interface: value.disk_interface.into(),
|
||||
pricing_id: value.pricing_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApiCustomPrice {
|
||||
pub currency: String,
|
||||
pub amount: f32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApiVmTemplate {
|
||||
pub id: u64,
|
||||
@ -123,33 +212,80 @@ pub struct ApiVmTemplate {
|
||||
}
|
||||
|
||||
impl ApiVmTemplate {
|
||||
pub fn from(
|
||||
template: lnvps_db::VmTemplate,
|
||||
cost_plan: lnvps_db::VmCostPlan,
|
||||
region: VmHostRegion,
|
||||
) -> Self {
|
||||
Self {
|
||||
pub async fn from_standard(db: &Arc<dyn LNVpsDb>, template_id: u64) -> Result<Self> {
|
||||
let template = db.get_vm_template(template_id).await?;
|
||||
let cost_plan = db.get_cost_plan(template.cost_plan_id).await?;
|
||||
let region = db.get_host_region(template.region_id).await?;
|
||||
Ok(Self::from_standard_data(&template, &cost_plan, ®ion))
|
||||
}
|
||||
|
||||
pub async fn from_custom(db: &Arc<dyn LNVpsDb>, vm_id: u64, template_id: u64) -> Result<Self> {
|
||||
let template = db.get_custom_vm_template(template_id).await?;
|
||||
let pricing = db.get_custom_pricing(template.pricing_id).await?;
|
||||
let region = db.get_host_region(pricing.region_id).await?;
|
||||
let price = PricingEngine::get_custom_vm_cost_amount(db, vm_id, &template).await?;
|
||||
Ok(Self {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
created: template.created,
|
||||
expires: template.expires,
|
||||
name: "Custom".to_string(),
|
||||
created: pricing.created,
|
||||
expires: pricing.expires,
|
||||
cpu: template.cpu,
|
||||
memory: template.memory,
|
||||
disk_size: template.disk_size,
|
||||
disk_type: template.disk_type.into(),
|
||||
disk_interface: template.disk_interface.into(),
|
||||
cost_plan: ApiVmCostPlan {
|
||||
id: cost_plan.id,
|
||||
name: cost_plan.name,
|
||||
amount: cost_plan.amount,
|
||||
currency: cost_plan.currency,
|
||||
interval_amount: cost_plan.interval_amount,
|
||||
interval_type: cost_plan.interval_type.into(),
|
||||
id: pricing.id,
|
||||
name: pricing.name,
|
||||
amount: price.total(),
|
||||
currency: price.currency,
|
||||
interval_amount: 1,
|
||||
interval_type: ApiVmCostPlanIntervalType::Month,
|
||||
},
|
||||
region: ApiVmHostRegion {
|
||||
id: region.id,
|
||||
name: region.name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn from_vm(db: &Arc<dyn LNVpsDb>, vm: &Vm) -> Result<Self> {
|
||||
if let Some(t) = vm.template_id {
|
||||
return Self::from_standard(db, t).await;
|
||||
}
|
||||
if let Some(t) = vm.custom_template_id {
|
||||
return Self::from_custom(db, vm.id, t).await;
|
||||
}
|
||||
bail!("Invalid VM config, no template or custom template")
|
||||
}
|
||||
|
||||
pub fn from_standard_data(
|
||||
template: &VmTemplate,
|
||||
cost_plan: &VmCostPlan,
|
||||
region: &VmHostRegion,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: template.id,
|
||||
name: template.name.clone(),
|
||||
created: template.created,
|
||||
expires: template.expires,
|
||||
cpu: template.cpu,
|
||||
memory: template.memory,
|
||||
disk_size: template.disk_size,
|
||||
disk_type: template.disk_type.clone().into(),
|
||||
disk_interface: template.disk_interface.clone().into(),
|
||||
cost_plan: ApiVmCostPlan {
|
||||
id: cost_plan.id,
|
||||
name: cost_plan.name.clone(),
|
||||
amount: cost_plan.amount,
|
||||
currency: cost_plan.currency.clone(),
|
||||
interval_amount: cost_plan.interval_amount,
|
||||
interval_type: cost_plan.interval_type.clone().into(),
|
||||
},
|
||||
region: ApiVmHostRegion {
|
||||
id: region.id,
|
||||
name: region.name.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,18 @@
|
||||
use crate::api::model::{
|
||||
AccountPatchRequest, ApiUserSshKey, ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus,
|
||||
ApiVmTemplate, CreateSshKey, CreateVmRequest, VMPatchRequest,
|
||||
AccountPatchRequest, ApiCustomPrice, ApiCustomTemplateDiskParam, ApiCustomTemplateParams,
|
||||
ApiCustomVmOrder, ApiCustomVmRequest, ApiTemplatesResponse, ApiUserSshKey, ApiVmHostRegion,
|
||||
ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus, ApiVmTemplate, CreateSshKey,
|
||||
CreateVmRequest, VMPatchRequest,
|
||||
};
|
||||
use crate::host::{get_host_client, FullVmInfo, TimeSeries, TimeSeriesData};
|
||||
use crate::nip98::Nip98Auth;
|
||||
use crate::provisioner::{HostCapacityService, LNVpsProvisioner};
|
||||
use crate::provisioner::{HostCapacityService, LNVpsProvisioner, PricingEngine};
|
||||
use crate::settings::Settings;
|
||||
use crate::status::{VmState, VmStateCache};
|
||||
use crate::worker::WorkJob;
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use futures::future::join_all;
|
||||
use lnvps_db::{IpRange, LNVpsDb};
|
||||
use lnvps_db::{IpRange, LNVpsDb, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate};
|
||||
use nostr::util::hex;
|
||||
use rocket::futures::{SinkExt, StreamExt};
|
||||
use rocket::serde::json::Json;
|
||||
@ -43,7 +45,9 @@ pub fn routes() -> Vec<Route> {
|
||||
v1_stop_vm,
|
||||
v1_restart_vm,
|
||||
v1_patch_vm,
|
||||
v1_time_series
|
||||
v1_time_series,
|
||||
v1_custom_template_calc,
|
||||
v1_create_custom_vm_order
|
||||
]
|
||||
}
|
||||
|
||||
@ -127,9 +131,6 @@ async fn vm_to_status(
|
||||
state: Option<VmState>,
|
||||
) -> Result<ApiVmStatus> {
|
||||
let image = db.get_os_image(vm.image_id).await?;
|
||||
let template = db.get_vm_template(vm.template_id).await?;
|
||||
let region = db.get_host_region(template.region_id).await?;
|
||||
let cost_plan = db.get_cost_plan(template.cost_plan_id).await?;
|
||||
let ssh_key = db.get_user_ssh_key(vm.ssh_key_id).await?;
|
||||
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
||||
let ip_range_ids: HashSet<u64> = ips.iter().map(|i| i.ip_range_id).collect();
|
||||
@ -141,13 +142,14 @@ async fn vm_to_status(
|
||||
.map(|i| (i.id, i))
|
||||
.collect();
|
||||
|
||||
let template = ApiVmTemplate::from_vm(&db, &vm).await?;
|
||||
Ok(ApiVmStatus {
|
||||
id: vm.id,
|
||||
created: vm.created,
|
||||
expires: vm.expires,
|
||||
mac_address: vm.mac_address,
|
||||
image: image.into(),
|
||||
template: ApiVmTemplate::from(template, cost_plan, region),
|
||||
template,
|
||||
ssh_key: ssh_key.into(),
|
||||
status: state.unwrap_or_default(),
|
||||
ip_assignments: ips
|
||||
@ -264,7 +266,7 @@ async fn v1_list_vm_images(db: &State<Arc<dyn LNVpsDb>>) -> ApiResult<Vec<ApiVmO
|
||||
/// List available VM templates (Offers)
|
||||
#[openapi(tag = "VM")]
|
||||
#[get("/api/v1/vm/templates")]
|
||||
async fn v1_list_vm_templates(db: &State<Arc<dyn LNVpsDb>>) -> ApiResult<Vec<ApiVmTemplate>> {
|
||||
async fn v1_list_vm_templates(db: &State<Arc<dyn LNVpsDb>>) -> ApiResult<ApiTemplatesResponse> {
|
||||
let hc = HostCapacityService::new((*db).clone());
|
||||
let templates = hc.list_available_vm_templates().await?;
|
||||
|
||||
@ -295,14 +297,117 @@ async fn v1_list_vm_templates(db: &State<Arc<dyn LNVpsDb>>) -> ApiResult<Vec<Api
|
||||
.collect();
|
||||
|
||||
let ret = templates
|
||||
.into_iter()
|
||||
.iter()
|
||||
.filter_map(|i| {
|
||||
let cp = cost_plans.get(&i.cost_plan_id)?;
|
||||
let hr = regions.get(&i.region_id)?;
|
||||
Some(ApiVmTemplate::from(i, cp.clone(), hr.clone()))
|
||||
Some(ApiVmTemplate::from_standard_data(i, cp, hr))
|
||||
})
|
||||
.collect();
|
||||
ApiData::ok(ret)
|
||||
let custom_templates: Vec<VmCustomPricing> =
|
||||
join_all(regions.iter().map(|(k, _)| db.list_custom_pricing(*k)))
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|r| r.ok())
|
||||
.flatten()
|
||||
.collect();
|
||||
let custom_template_disks: Vec<VmCustomPricingDisk> = join_all(
|
||||
custom_templates
|
||||
.iter()
|
||||
.map(|t| db.list_custom_pricing_disk(t.id)),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|r| r.ok())
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
ApiData::ok(ApiTemplatesResponse {
|
||||
templates: ret,
|
||||
custom_template: if custom_templates.is_empty() {
|
||||
None
|
||||
} else {
|
||||
const GB: u64 = 1024 * 1024 * 1024;
|
||||
Some(
|
||||
custom_templates
|
||||
.into_iter()
|
||||
.map(|t| ApiCustomTemplateParams {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
region: regions
|
||||
.get(&t.region_id)
|
||||
.map(|r| ApiVmHostRegion {
|
||||
id: r.id,
|
||||
name: r.name.clone(),
|
||||
})
|
||||
.context("Region information missing in custom template")
|
||||
.unwrap(),
|
||||
max_cpu: templates.iter().map(|t| t.cpu).max().unwrap_or(8),
|
||||
min_cpu: 1,
|
||||
min_memory: GB,
|
||||
max_memory: templates.iter().map(|t| t.memory).max().unwrap_or(GB * 2),
|
||||
min_disk: GB * 5,
|
||||
max_disk: templates
|
||||
.iter()
|
||||
.map(|t| t.disk_size)
|
||||
.max()
|
||||
.unwrap_or(GB * 5),
|
||||
disks: custom_template_disks
|
||||
.iter()
|
||||
.filter(|d| d.pricing_id == t.id)
|
||||
.map(|d| ApiCustomTemplateDiskParam {
|
||||
disk_type: d.kind.into(),
|
||||
disk_interface: d.interface.into(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a price for a custom order
|
||||
#[openapi(tag = "VM")]
|
||||
#[post("/api/v1/vm/custom-template/price", data = "<req>", format = "json")]
|
||||
async fn v1_custom_template_calc(
|
||||
db: &State<Arc<dyn LNVpsDb>>,
|
||||
req: Json<ApiCustomVmRequest>,
|
||||
) -> ApiResult<ApiCustomPrice> {
|
||||
// create a fake template from the request to generate the price
|
||||
let template: VmCustomTemplate = req.0.into();
|
||||
|
||||
let price = PricingEngine::get_custom_vm_cost_amount(db, 0, &template).await?;
|
||||
ApiData::ok(ApiCustomPrice {
|
||||
currency: price.currency.clone(),
|
||||
amount: price.total(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new VM order
|
||||
///
|
||||
/// After order is created please use /api/v1/vm/{id}/renew to pay for VM,
|
||||
/// VM's are initially created in "expired" state
|
||||
///
|
||||
/// Unpaid VM orders will be deleted after 24hrs
|
||||
#[openapi(tag = "VM")]
|
||||
#[post("/api/v1/vm/custom-template", data = "<req>", format = "json")]
|
||||
async fn v1_create_custom_vm_order(
|
||||
auth: Nip98Auth,
|
||||
db: &State<Arc<dyn LNVpsDb>>,
|
||||
provisioner: &State<Arc<LNVpsProvisioner>>,
|
||||
req: Json<ApiCustomVmOrder>,
|
||||
) -> ApiResult<ApiVmStatus> {
|
||||
let pubkey = auth.event.pubkey.to_bytes();
|
||||
let uid = db.upsert_user(&pubkey).await?;
|
||||
|
||||
// create a fake template from the request to generate the order
|
||||
let template = req.0.spec.clone().into();
|
||||
|
||||
let rsp = provisioner
|
||||
.provision_custom(uid, template, req.image_id, req.ssh_key_id, req.0.ref_code)
|
||||
.await?;
|
||||
ApiData::ok(vm_to_status(db, rsp, None).await?)
|
||||
}
|
||||
|
||||
/// List user SSH keys
|
||||
|
@ -2,13 +2,14 @@ use anyhow::{Error, Result};
|
||||
use lnvps_db::async_trait;
|
||||
use log::info;
|
||||
use rocket::serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)]
|
||||
pub enum Currency {
|
||||
EUR,
|
||||
BTC,
|
||||
|
@ -3,8 +3,8 @@ use crate::status::VmState;
|
||||
use anyhow::{bail, Result};
|
||||
use futures::future::join_all;
|
||||
use lnvps_db::{
|
||||
async_trait, IpRange, LNVpsDb, UserSshKey, Vm, VmHost, VmHostDisk, VmHostKind, VmIpAssignment,
|
||||
VmOsImage, VmTemplate,
|
||||
async_trait, IpRange, LNVpsDb, UserSshKey, Vm, VmCustomTemplate, VmHost, VmHostDisk,
|
||||
VmHostKind, VmIpAssignment, VmOsImage, VmTemplate,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -87,7 +87,9 @@ pub struct FullVmInfo {
|
||||
/// Disk where this VM will be saved on the host
|
||||
pub disk: VmHostDisk,
|
||||
/// VM template resources
|
||||
pub template: VmTemplate,
|
||||
pub template: Option<VmTemplate>,
|
||||
/// VM custom template resources
|
||||
pub custom_template: Option<VmCustomTemplate>,
|
||||
/// The OS image used to create the VM
|
||||
pub image: VmOsImage,
|
||||
/// List of IP resources assigned to this VM
|
||||
@ -101,7 +103,6 @@ pub struct FullVmInfo {
|
||||
impl FullVmInfo {
|
||||
pub async fn load(vm_id: u64, db: Arc<dyn LNVpsDb>) -> Result<Self> {
|
||||
let vm = db.get_vm(vm_id).await?;
|
||||
let template = db.get_vm_template(vm.template_id).await?;
|
||||
let image = db.get_os_image(vm.image_id).await?;
|
||||
let disk = db.get_host_disk(vm.disk_id).await?;
|
||||
let ssh_key = db.get_user_ssh_key(vm.ssh_key_id).await?;
|
||||
@ -115,10 +116,21 @@ impl FullVmInfo {
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
|
||||
let template = if let Some(t) = vm.template_id {
|
||||
Some(db.get_vm_template(t).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let custom_template = if let Some(t) = vm.custom_template_id {
|
||||
Some(db.get_custom_vm_template(t).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// create VM
|
||||
Ok(FullVmInfo {
|
||||
vm,
|
||||
template,
|
||||
custom_template,
|
||||
image,
|
||||
ips,
|
||||
disk,
|
||||
@ -126,6 +138,32 @@ impl FullVmInfo {
|
||||
ssh_key,
|
||||
})
|
||||
}
|
||||
|
||||
/// CPU cores
|
||||
pub fn resources(&self) -> Result<VmResources> {
|
||||
if let Some(t) = &self.template {
|
||||
Ok(VmResources {
|
||||
cpu: t.cpu,
|
||||
memory: t.memory,
|
||||
disk_size: t.disk_size,
|
||||
})
|
||||
} else if let Some(t) = &self.custom_template {
|
||||
Ok(VmResources {
|
||||
cpu: t.cpu,
|
||||
memory: t.memory,
|
||||
disk_size: t.disk_size,
|
||||
})
|
||||
} else {
|
||||
bail!("Invalid VM config, no template");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VmResources {
|
||||
pub cpu: u16,
|
||||
pub memory: u64,
|
||||
pub disk_size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
|
@ -403,6 +403,7 @@ impl ProxmoxClient {
|
||||
net.push(format!("tag={}", t));
|
||||
}
|
||||
|
||||
let vm_resources = value.resources()?;
|
||||
Ok(VmConfig {
|
||||
cpu: Some(self.config.cpu.clone()),
|
||||
kvm: Some(self.config.kvm),
|
||||
@ -413,8 +414,8 @@ impl ProxmoxClient {
|
||||
on_boot: Some(true),
|
||||
bios: Some(VmBios::OVMF),
|
||||
boot: Some("order=scsi0".to_string()),
|
||||
cores: Some(value.template.cpu as i32),
|
||||
memory: Some((value.template.memory / 1024 / 1024).to_string()),
|
||||
cores: Some(vm_resources.cpu as i32),
|
||||
memory: Some((vm_resources.memory / 1024 / 1024).to_string()),
|
||||
scsi_hw: Some("virtio-scsi-pci".to_string()),
|
||||
serial_0: Some("socket".to_string()),
|
||||
scsi_1: Some(format!("{}:cloudinit", &value.disk.name)),
|
||||
@ -512,7 +513,7 @@ impl VmHostClient for ProxmoxClient {
|
||||
node: self.node.clone(),
|
||||
vm_id,
|
||||
disk: "scsi0".to_string(),
|
||||
size: req.template.disk_size.to_string(),
|
||||
size: req.resources()?.disk_size.to_string(),
|
||||
})
|
||||
.await?;
|
||||
// TODO: rollback
|
||||
|
225
src/mocks.rs
225
src/mocks.rs
@ -1,19 +1,21 @@
|
||||
#![allow(unused)]
|
||||
use crate::dns::{BasicRecord, DnsServer, RecordType};
|
||||
use crate::exchange::{ExchangeRateService, Ticker, TickerRate};
|
||||
use crate::host::{FullVmInfo, TimeSeries, TimeSeriesData, VmHostClient};
|
||||
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
||||
use crate::router::{ArpEntry, Router};
|
||||
use crate::settings::NetworkPolicy;
|
||||
use crate::status::{VmRunningState, VmState};
|
||||
use anyhow::{anyhow, bail, ensure};
|
||||
use chrono::{DateTime, Utc};
|
||||
use anyhow::{anyhow, bail, ensure, Context};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use fedimint_tonic_lnd::tonic::codegen::tokio_stream::Stream;
|
||||
use lnvps_db::{
|
||||
async_trait, DiskInterface, DiskType, IpRange, LNVpsDb, OsDistribution, User, UserSshKey, Vm,
|
||||
VmCostPlan, VmCostPlanIntervalType, VmHost, VmHostDisk, VmHostKind, VmHostRegion,
|
||||
VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
||||
VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate,
|
||||
VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Add;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use tokio::sync::Mutex;
|
||||
@ -31,21 +33,75 @@ pub struct MockDb {
|
||||
pub vms: Arc<Mutex<HashMap<u64, Vm>>>,
|
||||
pub ip_range: Arc<Mutex<HashMap<u64, IpRange>>>,
|
||||
pub ip_assignments: Arc<Mutex<HashMap<u64, VmIpAssignment>>>,
|
||||
pub custom_pricing: Arc<Mutex<HashMap<u64, VmCustomPricing>>>,
|
||||
pub custom_pricing_disk: Arc<Mutex<HashMap<u64, VmCustomPricingDisk>>>,
|
||||
pub custom_template: Arc<Mutex<HashMap<u64, VmCustomTemplate>>>,
|
||||
pub payments: Arc<Mutex<Vec<VmPayment>>>,
|
||||
}
|
||||
|
||||
impl MockDb {
|
||||
pub const KB: u64 = 1024;
|
||||
pub const MB: u64 = Self::KB * 1024;
|
||||
pub const GB: u64 = Self::MB * 1024;
|
||||
pub const TB: u64 = Self::GB * 1024;
|
||||
|
||||
pub fn empty() -> MockDb {
|
||||
Self {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_cost_plan() -> VmCostPlan {
|
||||
VmCostPlan {
|
||||
id: 1,
|
||||
name: "mock".to_string(),
|
||||
created: Utc::now(),
|
||||
amount: 1.32,
|
||||
currency: "EUR".to_string(),
|
||||
interval_amount: 1,
|
||||
interval_type: VmCostPlanIntervalType::Month,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_template() -> VmTemplate {
|
||||
VmTemplate {
|
||||
id: 1,
|
||||
name: "mock".to_string(),
|
||||
enabled: true,
|
||||
created: Utc::now(),
|
||||
expires: None,
|
||||
cpu: 2,
|
||||
memory: Self::GB * 2,
|
||||
disk_size: Self::GB * 64,
|
||||
disk_type: DiskType::SSD,
|
||||
disk_interface: DiskInterface::PCIe,
|
||||
cost_plan_id: 1,
|
||||
region_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_vm() ->Vm {
|
||||
let template = Self::mock_template();
|
||||
Vm {
|
||||
id: 1,
|
||||
host_id: 1,
|
||||
user_id: 1,
|
||||
image_id: 1,
|
||||
template_id: Some(template.id),
|
||||
custom_template_id: None,
|
||||
ssh_key_id: 1,
|
||||
created: Utc::now(),
|
||||
expires: Default::default(),
|
||||
disk_id: 1,
|
||||
mac_address: "ff:ff:ff:ff:ff:ff".to_string(),
|
||||
deleted: false,
|
||||
ref_code: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MockDb {
|
||||
fn default() -> Self {
|
||||
const GB: u64 = 1024 * 1024 * 1024;
|
||||
const TB: u64 = GB * 1024;
|
||||
|
||||
let mut regions = HashMap::new();
|
||||
regions.insert(
|
||||
1,
|
||||
@ -76,7 +132,7 @@ impl Default for MockDb {
|
||||
name: "mock-host".to_string(),
|
||||
ip: "https://localhost".to_string(),
|
||||
cpu: 4,
|
||||
memory: 8 * GB,
|
||||
memory: 8 * Self::GB,
|
||||
enabled: true,
|
||||
api_token: "".to_string(),
|
||||
load_factor: 1.5,
|
||||
@ -89,43 +145,16 @@ impl Default for MockDb {
|
||||
id: 1,
|
||||
host_id: 1,
|
||||
name: "mock-disk".to_string(),
|
||||
size: TB * 10,
|
||||
size: Self::TB * 10,
|
||||
kind: DiskType::SSD,
|
||||
interface: DiskInterface::PCIe,
|
||||
enabled: true,
|
||||
},
|
||||
);
|
||||
let mut cost_plans = HashMap::new();
|
||||
cost_plans.insert(
|
||||
1,
|
||||
VmCostPlan {
|
||||
id: 1,
|
||||
name: "mock".to_string(),
|
||||
created: Utc::now(),
|
||||
amount: 1f32,
|
||||
currency: "EUR".to_string(),
|
||||
interval_amount: 1,
|
||||
interval_type: VmCostPlanIntervalType::Month,
|
||||
},
|
||||
);
|
||||
cost_plans.insert(1, Self::mock_cost_plan());
|
||||
let mut templates = HashMap::new();
|
||||
templates.insert(
|
||||
1,
|
||||
VmTemplate {
|
||||
id: 1,
|
||||
name: "mock".to_string(),
|
||||
enabled: true,
|
||||
created: Utc::now(),
|
||||
expires: None,
|
||||
cpu: 2,
|
||||
memory: GB * 2,
|
||||
disk_size: GB * 64,
|
||||
disk_type: DiskType::SSD,
|
||||
disk_interface: DiskInterface::PCIe,
|
||||
cost_plan_id: 1,
|
||||
region_id: 1,
|
||||
},
|
||||
);
|
||||
templates.insert(1, Self::mock_template());
|
||||
let mut os_images = HashMap::new();
|
||||
os_images.insert(
|
||||
1,
|
||||
@ -150,7 +179,11 @@ impl Default for MockDb {
|
||||
users: Arc::new(Default::default()),
|
||||
vms: Arc::new(Default::default()),
|
||||
ip_assignments: Arc::new(Default::default()),
|
||||
custom_pricing: Arc::new(Default::default()),
|
||||
custom_pricing_disk: Arc::new(Default::default()),
|
||||
user_ssh_keys: Arc::new(Mutex::new(Default::default())),
|
||||
custom_template: Arc::new(Default::default()),
|
||||
payments: Arc::new(Default::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -377,7 +410,12 @@ impl LNVpsDb for MockDb {
|
||||
self.get_host(vm.host_id).await?;
|
||||
self.get_user(vm.user_id).await?;
|
||||
self.get_os_image(vm.image_id).await?;
|
||||
self.get_vm_template(vm.template_id).await?;
|
||||
if let Some(t) = vm.template_id {
|
||||
self.get_vm_template(t).await?;
|
||||
}
|
||||
if let Some(t) = vm.custom_template_id {
|
||||
self.get_custom_vm_template(t).await?;
|
||||
}
|
||||
self.get_user_ssh_key(vm.ssh_key_id).await?;
|
||||
self.get_host_disk(vm.disk_id).await?;
|
||||
|
||||
@ -462,27 +500,86 @@ impl LNVpsDb for MockDb {
|
||||
}
|
||||
|
||||
async fn list_vm_payment(&self, vm_id: u64) -> anyhow::Result<Vec<VmPayment>> {
|
||||
todo!()
|
||||
let p = self.payments.lock().await;
|
||||
Ok(p.iter().filter(|p| p.vm_id == vm_id).cloned().collect())
|
||||
}
|
||||
|
||||
async fn insert_vm_payment(&self, vm_payment: &VmPayment) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
let mut p = self.payments.lock().await;
|
||||
p.push(vm_payment.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_vm_payment(&self, id: &Vec<u8>) -> anyhow::Result<VmPayment> {
|
||||
todo!()
|
||||
let p = self.payments.lock().await;
|
||||
Ok(p.iter()
|
||||
.find(|p| p.id == *id)
|
||||
.context("no vm_payment")?
|
||||
.clone())
|
||||
}
|
||||
|
||||
async fn update_vm_payment(&self, vm_payment: &VmPayment) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
let mut p = self.payments.lock().await;
|
||||
if let Some(p) = p.iter_mut().find(|p| p.id == *vm_payment.id) {
|
||||
p.is_paid = vm_payment.is_paid.clone();
|
||||
p.settle_index = vm_payment.settle_index.clone();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn vm_payment_paid(&self, id: &VmPayment) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
async fn vm_payment_paid(&self, p: &VmPayment) -> anyhow::Result<()> {
|
||||
let mut v = self.vms.lock().await;
|
||||
self.update_vm_payment(p).await?;
|
||||
if let Some(v) = v.get_mut(&p.vm_id) {
|
||||
v.expires = v.expires.add(TimeDelta::seconds(p.time_value as i64));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn last_paid_invoice(&self) -> anyhow::Result<Option<VmPayment>> {
|
||||
todo!()
|
||||
let p = self.payments.lock().await;
|
||||
Ok(p.iter()
|
||||
.max_by(|a, b| a.settle_index.cmp(&b.settle_index))
|
||||
.map(|v| v.clone()))
|
||||
}
|
||||
|
||||
async fn list_custom_pricing(&self, region_id: u64) -> anyhow::Result<Vec<VmCustomPricing>> {
|
||||
let p = self.custom_pricing.lock().await;
|
||||
Ok(p.values().filter(|p| p.enabled).cloned().collect())
|
||||
}
|
||||
|
||||
async fn get_custom_pricing(&self, id: u64) -> anyhow::Result<VmCustomPricing> {
|
||||
let p = self.custom_pricing.lock().await;
|
||||
Ok(p.get(&id).cloned().context("no custom pricing")?)
|
||||
}
|
||||
|
||||
async fn get_custom_vm_template(&self, id: u64) -> anyhow::Result<VmCustomTemplate> {
|
||||
let t = self.custom_template.lock().await;
|
||||
Ok(t.get(&id).cloned().context("no custom template")?)
|
||||
}
|
||||
|
||||
async fn insert_custom_vm_template(&self, template: &VmCustomTemplate) -> anyhow::Result<u64> {
|
||||
let mut t = self.custom_template.lock().await;
|
||||
let max_id = *t.keys().max().unwrap_or(&0);
|
||||
t.insert(
|
||||
max_id + 1,
|
||||
VmCustomTemplate {
|
||||
id: max_id + 1,
|
||||
..template.clone()
|
||||
},
|
||||
);
|
||||
Ok(max_id + 1)
|
||||
}
|
||||
|
||||
async fn list_custom_pricing_disk(
|
||||
&self,
|
||||
pricing_id: u64,
|
||||
) -> anyhow::Result<Vec<VmCustomPricingDisk>> {
|
||||
let d = self.custom_pricing_disk.lock().await;
|
||||
Ok(d.values()
|
||||
.filter(|d| d.pricing_id == pricing_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@ -673,7 +770,11 @@ impl VmHostClient for MockVmHost {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_time_series_data(&self, vm: &Vm, series: TimeSeries) -> anyhow::Result<Vec<TimeSeriesData>> {
|
||||
async fn get_time_series_data(
|
||||
&self,
|
||||
vm: &Vm,
|
||||
series: TimeSeries,
|
||||
) -> anyhow::Result<Vec<TimeSeriesData>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
@ -758,3 +859,33 @@ impl DnsServer for MockDnsServer {
|
||||
Ok(record.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockExchangeRate {
|
||||
pub rate: Arc<Mutex<f32>>,
|
||||
}
|
||||
|
||||
impl MockExchangeRate {
|
||||
pub fn new(rate: f32) -> Self {
|
||||
Self {
|
||||
rate: Arc::new(Mutex::new(rate)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExchangeRateService for MockExchangeRate {
|
||||
async fn fetch_rates(&self) -> anyhow::Result<Vec<TickerRate>> {
|
||||
let r = self.rate.lock().await;
|
||||
Ok(vec![TickerRate(Ticker::btc_rate("EUR")?, *r)])
|
||||
}
|
||||
|
||||
async fn set_rate(&self, ticker: Ticker, amount: f32) {
|
||||
let mut r = self.rate.lock().await;
|
||||
*r = amount;
|
||||
}
|
||||
|
||||
async fn get_rate(&self, ticker: Ticker) -> Option<f32> {
|
||||
let r = self.rate.lock().await;
|
||||
Some(*r)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
use crate::provisioner::Template;
|
||||
use anyhow::{bail, Result};
|
||||
use chrono::Utc;
|
||||
use futures::future::join_all;
|
||||
use lnvps_db::{DiskType, LNVpsDb, VmHost, VmHostDisk, VmTemplate};
|
||||
use lnvps_db::{
|
||||
DiskInterface, DiskType, LNVpsDb, VmCustomTemplate, VmHost, VmHostDisk, VmTemplate,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -24,7 +28,7 @@ impl HostCapacityService {
|
||||
// use all hosts since we dont expect there to be many
|
||||
let hosts = self.db.list_hosts().await?;
|
||||
let caps: Vec<Result<HostCapacity>> =
|
||||
join_all(hosts.iter().map(|h| self.get_host_capacity(h, None))).await;
|
||||
join_all(hosts.iter().map(|h| self.get_host_capacity(h, None, None))).await;
|
||||
let caps: Vec<HostCapacity> = caps.into_iter().filter_map(Result::ok).collect();
|
||||
|
||||
Ok(templates
|
||||
@ -38,15 +42,20 @@ impl HostCapacityService {
|
||||
}
|
||||
|
||||
/// Pick a host for the purposes of provisioning a new VM
|
||||
pub async fn get_host_for_template(&self, template: &VmTemplate) -> Result<HostCapacity> {
|
||||
pub async fn get_host_for_template(
|
||||
&self,
|
||||
region_id: u64,
|
||||
template: &impl Template,
|
||||
) -> Result<HostCapacity> {
|
||||
let hosts = self.db.list_hosts().await?;
|
||||
let caps: Vec<Result<HostCapacity>> = join_all(
|
||||
hosts
|
||||
.iter()
|
||||
.filter(|h| h.region_id == template.region_id)
|
||||
// TODO: filter disk interface?
|
||||
.map(|h| self.get_host_capacity(h, Some(template.disk_type.clone()))),
|
||||
let caps: Vec<Result<HostCapacity>> =
|
||||
join_all(hosts.iter().filter(|h| h.region_id == region_id).map(|h| {
|
||||
self.get_host_capacity(
|
||||
h,
|
||||
Some(template.disk_type()),
|
||||
Some(template.disk_interface()),
|
||||
)
|
||||
}))
|
||||
.await;
|
||||
let mut host_cap: Vec<HostCapacity> = caps
|
||||
.into_iter()
|
||||
@ -68,31 +77,76 @@ impl HostCapacityService {
|
||||
&self,
|
||||
host: &VmHost,
|
||||
disk_type: Option<DiskType>,
|
||||
disk_interface: Option<DiskInterface>,
|
||||
) -> Result<HostCapacity> {
|
||||
let vms = self.db.list_vms_on_host(host.id).await?;
|
||||
// TODO: filter disks from DB? Should be very few disks anyway
|
||||
let storage = self.db.list_host_disks(host.id).await?;
|
||||
let templates = self.db.list_vm_templates().await?;
|
||||
let custom_templates: Vec<Result<VmCustomTemplate>> = join_all(
|
||||
vms.iter()
|
||||
.filter(|v| v.custom_template_id.is_some() && v.expires > Utc::now())
|
||||
.map(|v| {
|
||||
self.db
|
||||
.get_custom_vm_template(v.custom_template_id.unwrap())
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let custom_templates: HashMap<u64, VmCustomTemplate> = custom_templates
|
||||
.into_iter()
|
||||
.filter_map(|r| r.ok())
|
||||
.map(|v| (v.id, v))
|
||||
.collect();
|
||||
|
||||
// a mapping between vm_id and template
|
||||
let vm_template: HashMap<u64, &VmTemplate> = vms
|
||||
struct VmResources {
|
||||
vm_id: u64,
|
||||
cpu: u16,
|
||||
memory: u64,
|
||||
disk: u64,
|
||||
disk_id: u64,
|
||||
}
|
||||
// a mapping between vm_id and resources
|
||||
let vm_resources: HashMap<u64, VmResources> = vms
|
||||
.iter()
|
||||
.filter(|v| v.expires > Utc::now())
|
||||
.filter_map(|v| {
|
||||
templates
|
||||
.iter()
|
||||
.find(|t| t.id == v.template_id)
|
||||
.map(|t| (v.id, t))
|
||||
if let Some(x) = v.template_id {
|
||||
templates.iter().find(|t| t.id == x).map(|t| VmResources {
|
||||
vm_id: v.id,
|
||||
cpu: t.cpu,
|
||||
memory: t.memory,
|
||||
disk: t.disk_size,
|
||||
disk_id: v.disk_id,
|
||||
})
|
||||
} else if let Some(x) = v.custom_template_id {
|
||||
custom_templates.get(&x).map(|t| VmResources {
|
||||
vm_id: v.id,
|
||||
cpu: t.cpu,
|
||||
memory: t.memory,
|
||||
disk: t.disk_size,
|
||||
disk_id: v.disk_id,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|m| (m.vm_id, m))
|
||||
.collect();
|
||||
|
||||
let mut storage_disks: Vec<DiskCapacity> = storage
|
||||
.iter()
|
||||
.filter(|d| disk_type.as_ref().map(|t| d.kind == *t).unwrap_or(true))
|
||||
.filter(|d| {
|
||||
disk_type.as_ref().map(|t| d.kind == *t).unwrap_or(true)
|
||||
&& disk_interface
|
||||
.as_ref()
|
||||
.map(|i| d.interface == *i)
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.map(|s| {
|
||||
let usage = vm_template
|
||||
let usage = vm_resources
|
||||
.iter()
|
||||
.filter(|(k, v)| v.id == s.id)
|
||||
.fold(0, |acc, (k, v)| acc + v.disk_size);
|
||||
.filter(|(k, v)| s.id == v.disk_id)
|
||||
.fold(0, |acc, (k, v)| acc + v.disk);
|
||||
DiskCapacity {
|
||||
load_factor: host.load_factor,
|
||||
disk: s.clone(),
|
||||
@ -103,8 +157,8 @@ impl HostCapacityService {
|
||||
|
||||
storage_disks.sort_by(|a, b| a.load_factor.partial_cmp(&b.load_factor).unwrap());
|
||||
|
||||
let cpu_consumed = vm_template.values().fold(0, |acc, vm| acc + vm.cpu);
|
||||
let memory_consumed = vm_template.values().fold(0, |acc, vm| acc + vm.memory);
|
||||
let cpu_consumed = vm_resources.values().fold(0, |acc, vm| acc + vm.cpu);
|
||||
let memory_consumed = vm_resources.values().fold(0, |acc, vm| acc + vm.memory);
|
||||
|
||||
Ok(HostCapacity {
|
||||
load_factor: host.load_factor,
|
||||
@ -164,13 +218,13 @@ impl HostCapacity {
|
||||
}
|
||||
|
||||
/// Can this host and its available capacity accommodate the given template
|
||||
pub fn can_accommodate(&self, template: &VmTemplate) -> bool {
|
||||
self.available_cpu() >= template.cpu
|
||||
&& self.available_memory() >= template.memory
|
||||
pub fn can_accommodate(&self, template: &impl Template) -> bool {
|
||||
self.available_cpu() >= template.cpu()
|
||||
&& self.available_memory() >= template.memory()
|
||||
&& self
|
||||
.disks
|
||||
.iter()
|
||||
.any(|d| d.available_capacity() >= template.disk_size)
|
||||
.any(|d| d.available_capacity() >= template.disk_size())
|
||||
}
|
||||
}
|
||||
|
||||
@ -239,7 +293,7 @@ mod tests {
|
||||
|
||||
let hc = HostCapacityService::new(db.clone());
|
||||
let host = db.get_host(1).await?;
|
||||
let cap = hc.get_host_capacity(&host, None).await?;
|
||||
let cap = hc.get_host_capacity(&host, None, None).await?;
|
||||
let disks = db.list_host_disks(1).await?;
|
||||
/// check all resources are available
|
||||
assert_eq!(cap.cpu, 0);
|
||||
@ -252,7 +306,9 @@ mod tests {
|
||||
}
|
||||
|
||||
let template = db.get_vm_template(1).await?;
|
||||
let host = hc.get_host_for_template(&template).await?;
|
||||
let host = hc
|
||||
.get_host_for_template(template.region_id, &template)
|
||||
.await?;
|
||||
assert_eq!(host.host.id, 1);
|
||||
|
||||
// all templates should be available
|
||||
@ -261,4 +317,26 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn expired_doesnt_count() -> Result<()> {
|
||||
let db = MockDb::default();
|
||||
{
|
||||
let mut v = db.vms.lock().await;
|
||||
v.insert(1, MockDb::mock_vm());
|
||||
}
|
||||
|
||||
let db: Arc<dyn LNVpsDb> = Arc::new(db);
|
||||
let hc = HostCapacityService::new(db.clone());
|
||||
let host = db.get_host(1).await?;
|
||||
let cap = hc.get_host_capacity(&host, None, None).await?;
|
||||
|
||||
assert_eq!(cap.load(), 0.0);
|
||||
assert_eq!(cap.cpu, 0);
|
||||
assert_eq!(cap.memory, 0);
|
||||
for disk in cap.disks {
|
||||
assert_eq!(0, disk.usage);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,14 @@ use crate::dns::{BasicRecord, DnsServer};
|
||||
use crate::exchange::{ExchangeRateService, Ticker};
|
||||
use crate::host::{get_host_client, FullVmInfo};
|
||||
use crate::lightning::{AddInvoiceRequest, LightningNode};
|
||||
use crate::provisioner::{HostCapacityService, NetworkProvisioner, ProvisionerMethod};
|
||||
use crate::provisioner::{
|
||||
CostResult, HostCapacityService, NetworkProvisioner, PricingEngine, ProvisionerMethod,
|
||||
};
|
||||
use crate::router::{ArpEntry, Router};
|
||||
use crate::settings::{NetworkAccessPolicy, NetworkPolicy, ProvisionerConfig, Settings};
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use chrono::{Days, Months, Utc};
|
||||
use lnvps_db::{LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
|
||||
use lnvps_db::{DiskType, LNVpsDb, Vm, VmCostPlanIntervalType, VmCustomTemplate, VmIpAssignment, VmPayment};
|
||||
use log::{info, warn};
|
||||
use nostr::util::hex;
|
||||
use std::ops::Add;
|
||||
@ -196,8 +198,8 @@ impl LNVpsProvisioner {
|
||||
// Use random network provisioner
|
||||
let network = NetworkProvisioner::new(ProvisionerMethod::Random, self.db.clone());
|
||||
|
||||
let template = self.db.get_vm_template(vm.template_id).await?;
|
||||
let ip = network.pick_ip_for_region(template.region_id).await?;
|
||||
let host = self.db.get_host(vm.host_id).await?;
|
||||
let ip = network.pick_ip_for_region(host.region_id).await?;
|
||||
let mut assignment = VmIpAssignment {
|
||||
id: 0,
|
||||
vm_id,
|
||||
@ -253,7 +255,7 @@ impl LNVpsProvisioner {
|
||||
|
||||
// TODO: cache capacity somewhere
|
||||
let cap = HostCapacityService::new(self.db.clone());
|
||||
let host = cap.get_host_for_template(&template).await?;
|
||||
let host = cap.get_host_for_template(template.region_id, &template).await?;
|
||||
|
||||
let pick_disk = if let Some(hd) = host.disks.first() {
|
||||
hd
|
||||
@ -267,7 +269,64 @@ impl LNVpsProvisioner {
|
||||
host_id: host.host.id,
|
||||
user_id: user.id,
|
||||
image_id: image.id,
|
||||
template_id: template.id,
|
||||
template_id: Some(template.id),
|
||||
custom_template_id: None,
|
||||
ssh_key_id: ssh_key.id,
|
||||
created: Utc::now(),
|
||||
expires: Utc::now(),
|
||||
disk_id: pick_disk.disk.id,
|
||||
mac_address: "NOT FILLED YET".to_string(),
|
||||
deleted: false,
|
||||
ref_code,
|
||||
};
|
||||
|
||||
// ask host client to generate the mac address
|
||||
new_vm.mac_address = client.generate_mac(&new_vm).await?;
|
||||
|
||||
let new_id = self.db.insert_vm(&new_vm).await?;
|
||||
new_vm.id = new_id;
|
||||
Ok(new_vm)
|
||||
}
|
||||
|
||||
/// Provision a new VM for a user on the database
|
||||
///
|
||||
/// Note:
|
||||
/// 1. Does not create a VM on the host machine
|
||||
/// 2. Does not assign any IP resources
|
||||
pub async fn provision_custom(
|
||||
&self,
|
||||
user_id: u64,
|
||||
template: VmCustomTemplate,
|
||||
image_id: u64,
|
||||
ssh_key_id: u64,
|
||||
ref_code: Option<String>,
|
||||
) -> Result<Vm> {
|
||||
let user = self.db.get_user(user_id).await?;
|
||||
let pricing = self.db.get_vm_template(template.pricing_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?;
|
||||
|
||||
// TODO: cache capacity somewhere
|
||||
let cap = HostCapacityService::new(self.db.clone());
|
||||
let host = cap.get_host_for_template(pricing.region_id, &template).await?;
|
||||
|
||||
let pick_disk = if let Some(hd) = host.disks.first() {
|
||||
hd
|
||||
} else {
|
||||
bail!("No host disk found")
|
||||
};
|
||||
|
||||
// insert custom templates
|
||||
let template_id = self.db.insert_custom_vm_template(&template).await?;
|
||||
|
||||
let client = get_host_client(&host.host, &self.provisioner_config)?;
|
||||
let mut new_vm = Vm {
|
||||
id: 0,
|
||||
host_id: host.host.id,
|
||||
user_id: user.id,
|
||||
image_id: image.id,
|
||||
template_id: None,
|
||||
custom_template_id: Some(template_id),
|
||||
ssh_key_id: ssh_key.id,
|
||||
created: Utc::now(),
|
||||
expires: Utc::now(),
|
||||
@ -287,67 +346,45 @@ impl LNVpsProvisioner {
|
||||
|
||||
/// Create a renewal payment
|
||||
pub 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?;
|
||||
let pe = PricingEngine::new(self.db.clone(), self.rates.clone());
|
||||
|
||||
// Reuse existing payment until expired
|
||||
let payments = self.db.list_vm_payment(vm.id).await?;
|
||||
if let Some(px) = payments
|
||||
.into_iter()
|
||||
.find(|p| p.expires > Utc::now() && !p.is_paid)
|
||||
{
|
||||
return Ok(px);
|
||||
}
|
||||
|
||||
// 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_SATS: f64 = 100_000_000.0;
|
||||
const INVOICE_EXPIRE: u32 = 3600;
|
||||
|
||||
let ticker = Ticker::btc_rate(cost_plan.currency.as_str())?;
|
||||
let rate = if let Some(r) = self.rates.get_rate(ticker).await {
|
||||
r
|
||||
} else {
|
||||
bail!("No exchange rate found")
|
||||
};
|
||||
|
||||
let cost_btc = cost_plan.amount / rate;
|
||||
let cost_msat = (cost_btc as f64 * BTC_SATS) as u64 * 1000;
|
||||
info!("Creating invoice for {vm_id} for {} sats", cost_msat / 1000);
|
||||
let price = pe.get_vm_cost(vm_id).await?;
|
||||
match price {
|
||||
CostResult::Existing(p) => Ok(p),
|
||||
CostResult::New {
|
||||
msats,
|
||||
time_value,
|
||||
new_expiry,
|
||||
rate,
|
||||
} => {
|
||||
const INVOICE_EXPIRE: u64 = 600;
|
||||
info!("Creating invoice for {vm_id} for {} sats", msats / 1000);
|
||||
let invoice = self
|
||||
.node
|
||||
.add_invoice(AddInvoiceRequest {
|
||||
memo: Some(format!("VM renewal {vm_id} to {new_expire}")),
|
||||
amount: cost_msat,
|
||||
expire: Some(INVOICE_EXPIRE),
|
||||
memo: Some(format!("VM renewal {vm_id} to {new_expiry}")),
|
||||
amount: msats,
|
||||
expire: Some(INVOICE_EXPIRE as u32),
|
||||
})
|
||||
.await?;
|
||||
let vm_payment = VmPayment {
|
||||
id: hex::decode(invoice.payment_hash)?,
|
||||
vm_id,
|
||||
created: Utc::now(),
|
||||
expires: Utc::now().add(Duration::from_secs(INVOICE_EXPIRE as u64)),
|
||||
amount: cost_msat,
|
||||
expires: Utc::now().add(Duration::from_secs(INVOICE_EXPIRE)),
|
||||
amount: msats,
|
||||
invoice: invoice.pr,
|
||||
time_value: (new_expire - vm.expires).num_seconds() as u64,
|
||||
time_value,
|
||||
is_paid: false,
|
||||
rate,
|
||||
..Default::default()
|
||||
settle_index: None,
|
||||
};
|
||||
self.db.insert_vm_payment(&vm_payment).await?;
|
||||
|
||||
Ok(vm_payment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a vm on the host as configured by the template
|
||||
pub async fn spawn_vm(&self, vm_id: u64) -> Result<()> {
|
||||
@ -401,8 +438,6 @@ mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
const ROUTER_BRIDGE: &str = "bridge1";
|
||||
const GB: u64 = 1024 * 1024 * 1024;
|
||||
const TB: u64 = GB * 1024;
|
||||
|
||||
fn settings() -> Settings {
|
||||
Settings {
|
||||
@ -546,8 +581,8 @@ mod tests {
|
||||
created: Default::default(),
|
||||
expires: None,
|
||||
cpu: 64,
|
||||
memory: 512 * GB,
|
||||
disk_size: 20 * TB,
|
||||
memory: 512 * MockDb::GB,
|
||||
disk_size: 20 * MockDb::TB,
|
||||
disk_type: DiskType::SSD,
|
||||
disk_interface: DiskInterface::PCIe,
|
||||
cost_plan_id: 1,
|
||||
|
@ -1,7 +1,62 @@
|
||||
mod capacity;
|
||||
mod lnvps;
|
||||
mod network;
|
||||
mod pricing;
|
||||
|
||||
pub use capacity::*;
|
||||
pub use lnvps::*;
|
||||
use lnvps_db::{DiskInterface, DiskType, VmCustomTemplate, VmTemplate};
|
||||
pub use network::*;
|
||||
pub use pricing::*;
|
||||
|
||||
pub trait Template {
|
||||
fn cpu(&self) -> u16;
|
||||
fn memory(&self) -> u64;
|
||||
fn disk_size(&self) -> u64;
|
||||
fn disk_type(&self) -> DiskType;
|
||||
fn disk_interface(&self) -> DiskInterface;
|
||||
}
|
||||
|
||||
impl Template for VmTemplate {
|
||||
fn cpu(&self) -> u16 {
|
||||
self.cpu
|
||||
}
|
||||
|
||||
fn memory(&self) -> u64 {
|
||||
self.memory
|
||||
}
|
||||
|
||||
fn disk_size(&self) -> u64 {
|
||||
self.disk_size
|
||||
}
|
||||
|
||||
fn disk_type(&self) -> DiskType {
|
||||
self.disk_type
|
||||
}
|
||||
|
||||
fn disk_interface(&self) -> DiskInterface {
|
||||
self.disk_interface
|
||||
}
|
||||
}
|
||||
|
||||
impl Template for VmCustomTemplate {
|
||||
fn cpu(&self) -> u16 {
|
||||
self.cpu
|
||||
}
|
||||
|
||||
fn memory(&self) -> u64 {
|
||||
self.memory
|
||||
}
|
||||
|
||||
fn disk_size(&self) -> u64 {
|
||||
self.disk_size
|
||||
}
|
||||
|
||||
fn disk_type(&self) -> DiskType {
|
||||
self.disk_type
|
||||
}
|
||||
|
||||
fn disk_interface(&self) -> DiskInterface {
|
||||
self.disk_interface
|
||||
}
|
||||
}
|
299
src/provisioner/pricing.rs
Normal file
299
src/provisioner/pricing.rs
Normal file
@ -0,0 +1,299 @@
|
||||
use crate::exchange::{ExchangeRateService, Ticker};
|
||||
use anyhow::{bail, Result};
|
||||
use chrono::{DateTime, Days, Months, TimeDelta, Utc};
|
||||
use ipnetwork::IpNetwork;
|
||||
use lnvps_db::{LNVpsDb, Vm, VmCostPlan, VmCostPlanIntervalType, VmCustomTemplate, VmPayment};
|
||||
use log::info;
|
||||
use std::ops::Add;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Pricing engine is used to calculate billing amounts for
|
||||
/// different resource allocations
|
||||
#[derive(Clone)]
|
||||
pub struct PricingEngine {
|
||||
db: Arc<dyn LNVpsDb>,
|
||||
rates: Arc<dyn ExchangeRateService>,
|
||||
}
|
||||
|
||||
impl PricingEngine {
|
||||
/// SATS per BTC
|
||||
const BTC_SATS: f64 = 100_000_000.0;
|
||||
const KB: u64 = 1024;
|
||||
const MB: u64 = Self::KB * 1024;
|
||||
const GB: u64 = Self::MB * 1024;
|
||||
|
||||
pub fn new(db: Arc<dyn LNVpsDb>, rates: Arc<dyn ExchangeRateService>) -> Self {
|
||||
Self { db, rates }
|
||||
}
|
||||
|
||||
/// Get VM cost (for renewal)
|
||||
pub async fn get_vm_cost(&self, vm_id: u64) -> Result<CostResult> {
|
||||
let vm = self.db.get_vm(vm_id).await?;
|
||||
|
||||
// Reuse existing payment until expired
|
||||
let payments = self.db.list_vm_payment(vm.id).await?;
|
||||
if let Some(px) = payments
|
||||
.into_iter()
|
||||
.find(|p| p.expires > Utc::now() && !p.is_paid)
|
||||
{
|
||||
return Ok(CostResult::Existing(px));
|
||||
}
|
||||
|
||||
if vm.template_id.is_some() {
|
||||
Ok(self.get_template_vm_cost(&vm).await?)
|
||||
} else {
|
||||
Ok(self.get_custom_vm_cost(&vm).await?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the cost amount as (Currency,amount)
|
||||
pub async fn get_custom_vm_cost_amount(
|
||||
db: &Arc<dyn LNVpsDb>,
|
||||
vm_id: u64,
|
||||
template: &VmCustomTemplate,
|
||||
) -> Result<PricingData> {
|
||||
let pricing = db.get_custom_pricing(template.pricing_id).await?;
|
||||
let pricing_disk = db.list_custom_pricing_disk(pricing.id).await?;
|
||||
let ips = db.list_vm_ip_assignments(vm_id).await?;
|
||||
let v4s = ips
|
||||
.iter()
|
||||
.filter(|i| {
|
||||
IpNetwork::from_str(&i.ip)
|
||||
.map(|i| i.is_ipv4())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.count()
|
||||
.max(1); // must have at least 1
|
||||
let v6s = ips
|
||||
.iter()
|
||||
.filter(|i| {
|
||||
IpNetwork::from_str(&i.ip)
|
||||
.map(|i| i.is_ipv6())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.count()
|
||||
.max(1); // must have at least 1
|
||||
let disk_pricing =
|
||||
if let Some(p) = pricing_disk.iter().find(|p| p.kind == template.disk_type) {
|
||||
p
|
||||
} else {
|
||||
bail!("No disk price found")
|
||||
};
|
||||
let disk_cost = (template.disk_size / Self::GB) as f32 * disk_pricing.cost;
|
||||
let cpu_cost = pricing.cpu_cost * template.cpu as f32;
|
||||
let memory_cost = pricing.memory_cost * (template.memory / Self::GB) as f32;
|
||||
let ip4_cost = pricing.ip4_cost * v4s as f32;
|
||||
let ip6_cost = pricing.ip6_cost * v6s as f32;
|
||||
|
||||
Ok(PricingData {
|
||||
currency: pricing.currency,
|
||||
cpu_cost,
|
||||
memory_cost,
|
||||
ip6_cost,
|
||||
ip4_cost,
|
||||
disk_cost,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_custom_vm_cost(&self, vm: &Vm) -> Result<CostResult> {
|
||||
let template_id = if let Some(i) = vm.custom_template_id {
|
||||
i
|
||||
} else {
|
||||
bail!("Not a custom template vm")
|
||||
};
|
||||
|
||||
let template = self.db.get_custom_vm_template(template_id).await?;
|
||||
let price = Self::get_custom_vm_cost_amount(&self.db, vm.id, &template).await?;
|
||||
info!("Custom pricing for {} = {:?}", vm.id, price);
|
||||
|
||||
// custom templates are always 1-month intervals
|
||||
let time_value = (vm.expires.add(Months::new(1)) - vm.expires).num_seconds() as u64;
|
||||
let (cost_msats, rate) = self
|
||||
.get_msats_amount(&price.currency, price.total())
|
||||
.await?;
|
||||
Ok(CostResult::New {
|
||||
msats: cost_msats,
|
||||
rate,
|
||||
time_value,
|
||||
new_expiry: vm.expires.add(TimeDelta::seconds(time_value as i64)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_msats_amount(&self, currency: &str, amount: f32) -> Result<(u64, f32)> {
|
||||
let ticker = Ticker::btc_rate(¤cy)?;
|
||||
let rate = if let Some(r) = self.rates.get_rate(ticker).await {
|
||||
r
|
||||
} else {
|
||||
bail!("No exchange rate found")
|
||||
};
|
||||
|
||||
let cost_btc = amount / rate;
|
||||
let cost_msats = (cost_btc as f64 * Self::BTC_SATS) as u64 * 1000;
|
||||
Ok((cost_msats, rate))
|
||||
}
|
||||
|
||||
fn next_template_expire(vm: &Vm, cost_plan: &VmCostPlan) -> u64 {
|
||||
let next_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)),
|
||||
};
|
||||
|
||||
(next_expire - vm.expires).num_seconds() as u64
|
||||
}
|
||||
|
||||
async fn get_template_vm_cost(&self, vm: &Vm) -> Result<CostResult> {
|
||||
let template_id = if let Some(i) = vm.template_id {
|
||||
i
|
||||
} else {
|
||||
bail!("Not a standard template vm");
|
||||
};
|
||||
let template = self.db.get_vm_template(template_id).await?;
|
||||
let cost_plan = self.db.get_cost_plan(template.cost_plan_id).await?;
|
||||
|
||||
let (cost_msats, rate) = self
|
||||
.get_msats_amount(cost_plan.currency.as_str(), cost_plan.amount)
|
||||
.await?;
|
||||
let time_value = Self::next_template_expire(&vm, &cost_plan);
|
||||
Ok(CostResult::New {
|
||||
msats: cost_msats,
|
||||
rate,
|
||||
time_value,
|
||||
new_expiry: vm.expires.add(TimeDelta::seconds(time_value as i64)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CostResult {
|
||||
/// An existing payment already exists and should be used
|
||||
Existing(VmPayment),
|
||||
/// A new payment can be created with the specified amount
|
||||
New {
|
||||
/// The cost in milli-sats
|
||||
msats: u64,
|
||||
/// The exchange rate used to calculate the price
|
||||
rate: f32,
|
||||
/// The time to extend the vm expiry in seconds
|
||||
time_value: u64,
|
||||
/// The absolute expiry time of the vm if renewed
|
||||
new_expiry: DateTime<Utc>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PricingData {
|
||||
pub currency: String,
|
||||
pub cpu_cost: f32,
|
||||
pub memory_cost: f32,
|
||||
pub ip4_cost: f32,
|
||||
pub ip6_cost: f32,
|
||||
pub disk_cost: f32,
|
||||
}
|
||||
|
||||
impl PricingData {
|
||||
pub fn total(&self) -> f32 {
|
||||
self.cpu_cost + self.memory_cost + self.ip4_cost + self.ip6_cost + self.disk_cost
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mocks::{MockDb, MockExchangeRate};
|
||||
use lnvps_db::{DiskType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate};
|
||||
const GB: u64 = 1024 * 1024 * 1024;
|
||||
const MOCK_RATE: f32 = 100_000.0;
|
||||
|
||||
async fn add_custom_pricing(db: &MockDb) {
|
||||
let mut p = db.custom_pricing.lock().await;
|
||||
p.insert(
|
||||
1,
|
||||
VmCustomPricing {
|
||||
id: 1,
|
||||
name: "mock-custom".to_string(),
|
||||
enabled: true,
|
||||
created: Utc::now(),
|
||||
expires: None,
|
||||
region_id: 1,
|
||||
currency: "EUR".to_string(),
|
||||
cpu_cost: 1.5,
|
||||
memory_cost: 0.5,
|
||||
ip4_cost: 0.5,
|
||||
ip6_cost: 0.05,
|
||||
},
|
||||
);
|
||||
let mut p = db.custom_template.lock().await;
|
||||
p.insert(
|
||||
1,
|
||||
VmCustomTemplate {
|
||||
id: 1,
|
||||
cpu: 2,
|
||||
memory: 2 * GB,
|
||||
disk_size: 80 * GB,
|
||||
disk_type: DiskType::SSD,
|
||||
disk_interface: Default::default(),
|
||||
pricing_id: 1,
|
||||
},
|
||||
);
|
||||
let mut d = db.custom_pricing_disk.lock().await;
|
||||
d.insert(
|
||||
1,
|
||||
VmCustomPricingDisk {
|
||||
id: 1,
|
||||
pricing_id: 1,
|
||||
kind: DiskType::SSD,
|
||||
interface: Default::default(),
|
||||
cost: 0.05,
|
||||
},
|
||||
);
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn custom_pricing() -> Result<()> {
|
||||
let db = MockDb::default();
|
||||
add_custom_pricing(&db).await;
|
||||
let db: Arc<dyn LNVpsDb> = Arc::new(db);
|
||||
|
||||
let template = db.get_custom_vm_template(1).await?;
|
||||
let price = PricingEngine::get_custom_vm_cost_amount(&db, 1, &template).await?;
|
||||
assert_eq!(3.0, price.cpu_cost);
|
||||
assert_eq!(1.0, price.memory_cost);
|
||||
assert_eq!(0.5, price.ip4_cost);
|
||||
assert_eq!(0.05, price.ip6_cost);
|
||||
assert_eq!(4.0, price.disk_cost);
|
||||
assert_eq!(8.55, price.total());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standard_pricing() -> Result<()> {
|
||||
let db = MockDb::default();
|
||||
let rates = Arc::new(MockExchangeRate::new(MOCK_RATE));
|
||||
// add basic vm
|
||||
{
|
||||
let mut v = db.vms.lock().await;
|
||||
v.insert(1, MockDb::mock_vm());
|
||||
}
|
||||
|
||||
let db: Arc<dyn LNVpsDb> = Arc::new(db);
|
||||
|
||||
let pe = PricingEngine::new(db.clone(), rates);
|
||||
let price = pe.get_vm_cost(1).await?;
|
||||
let plan = MockDb::mock_cost_plan();
|
||||
match price {
|
||||
CostResult::Existing(_) => bail!("??"),
|
||||
CostResult::New { msats, .. } => {
|
||||
let expect_price = (plan.amount / MOCK_RATE * 1.0e11) as u64;
|
||||
assert_eq!(expect_price, msats);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user