feat: custom pricing

closes #3
This commit is contained in:
2025-03-06 21:42:27 +00:00
parent 36ba1f836a
commit 8c3756e3e8
15 changed files with 1242 additions and 359 deletions

160
lnvps_db/Cargo.lock generated
View File

@ -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"

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

View File

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

View File

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

View File

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