feat: alt prices

This commit is contained in:
2025-03-10 15:09:50 +00:00
parent 3e9a850486
commit ddaed046dc
3 changed files with 100 additions and 49 deletions

View File

@ -4,7 +4,10 @@ use crate::status::VmState;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use lnvps_db::{LNVpsDb, Vm, VmCostPlan, VmCustomTemplate, VmHost, VmHostRegion, VmTemplate}; use lnvps_db::{
LNVpsDb, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost,
VmHostRegion, VmTemplate,
};
use nostr::util::hex; use nostr::util::hex;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -141,16 +144,12 @@ impl ApiTemplatesResponse {
let rates = rates.list_rates().await?; let rates = rates.list_rates().await?;
for mut template in &mut self.templates { for mut template in &mut self.templates {
if let Some(list_price) = template.cost_plan.price.first() { let list_price = CurrencyAmount(template.cost_plan.currency, template.cost_plan.amount);
for alt_price in alt_prices( for alt_price in alt_prices(&rates, list_price) {
&rates, template.cost_plan.other_price.push(ApiPrice {
CurrencyAmount(list_price.currency, list_price.amount), currency: alt_price.0,
) { amount: alt_price.1,
template.cost_plan.price.push(ApiPrice { });
currency: alt_price.0,
amount: alt_price.1,
});
}
} }
} }
Ok(()) Ok(())
@ -170,6 +169,40 @@ pub struct ApiCustomTemplateParams {
pub disks: Vec<ApiCustomTemplateDiskParam>, pub disks: Vec<ApiCustomTemplateDiskParam>,
} }
impl ApiCustomTemplateParams {
pub fn from(
pricing: &VmCustomPricing,
disks: &Vec<VmCustomPricingDisk>,
region: &VmHostRegion,
max_cpu: u16,
max_memory: u64,
max_disk: u64,
) -> Result<Self> {
const GB: u64 = 1024 * 1024 * 1024;
Ok(ApiCustomTemplateParams {
id: pricing.id,
name: pricing.name.clone(),
region: ApiVmHostRegion {
id: region.id,
name: region.name.clone(),
},
max_cpu,
min_cpu: 1,
min_memory: GB,
max_memory,
min_disk: GB * 5,
max_disk,
disks: disks
.iter()
.filter(|d| d.pricing_id == pricing.id)
.map(|d| ApiCustomTemplateDiskParam {
disk_type: d.kind.into(),
disk_interface: d.interface.into(),
})
.collect(),
})
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub struct ApiCustomTemplateDiskParam { pub struct ApiCustomTemplateDiskParam {
pub disk_type: DiskType, pub disk_type: DiskType,
@ -215,6 +248,15 @@ pub struct ApiPrice {
pub amount: f32, pub amount: f32,
} }
impl From<CurrencyAmount> for ApiPrice {
fn from(value: CurrencyAmount) -> Self {
Self {
currency: value.0,
amount: value.1,
}
}
}
#[derive(Serialize, Deserialize, JsonSchema)] #[derive(Serialize, Deserialize, JsonSchema)]
pub struct ApiVmTemplate { pub struct ApiVmTemplate {
pub id: u64, pub id: u64,

View File

@ -1,9 +1,10 @@
use crate::api::model::{ use crate::api::model::{
AccountPatchRequest, ApiPrice, ApiCustomTemplateDiskParam, ApiCustomTemplateParams, AccountPatchRequest, ApiCustomTemplateDiskParam, ApiCustomTemplateParams, ApiCustomVmOrder,
ApiCustomVmOrder, ApiCustomVmRequest, ApiTemplatesResponse, ApiUserSshKey, ApiVmHostRegion, ApiCustomVmRequest, ApiPrice, ApiTemplatesResponse, ApiUserSshKey, ApiVmHostRegion,
ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus, ApiVmTemplate, CreateSshKey, ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus, ApiVmTemplate, CreateSshKey,
CreateVmRequest, VMPatchRequest, CreateVmRequest, VMPatchRequest,
}; };
use crate::exchange::ExchangeRateService;
use crate::host::{get_host_client, FullVmInfo, TimeSeries, TimeSeriesData}; use crate::host::{get_host_client, FullVmInfo, TimeSeries, TimeSeriesData};
use crate::nip98::Nip98Auth; use crate::nip98::Nip98Auth;
use crate::provisioner::{HostCapacityService, LNVpsProvisioner, PricingEngine}; use crate::provisioner::{HostCapacityService, LNVpsProvisioner, PricingEngine};
@ -266,7 +267,10 @@ async fn v1_list_vm_images(db: &State<Arc<dyn LNVpsDb>>) -> ApiResult<Vec<ApiVmO
/// List available VM templates (Offers) /// List available VM templates (Offers)
#[openapi(tag = "VM")] #[openapi(tag = "VM")]
#[get("/api/v1/vm/templates")] #[get("/api/v1/vm/templates")]
async fn v1_list_vm_templates(db: &State<Arc<dyn LNVpsDb>>) -> ApiResult<ApiTemplatesResponse> { async fn v1_list_vm_templates(
db: &State<Arc<dyn LNVpsDb>>,
rates: &State<Arc<dyn ExchangeRateService>>,
) -> ApiResult<ApiTemplatesResponse> {
let hc = HostCapacityService::new((*db).clone()); let hc = HostCapacityService::new((*db).clone());
let templates = hc.list_available_vm_templates().await?; let templates = hc.list_available_vm_templates().await?;
@ -322,49 +326,42 @@ async fn v1_list_vm_templates(db: &State<Arc<dyn LNVpsDb>>) -> ApiResult<ApiTemp
.flatten() .flatten()
.collect(); .collect();
ApiData::ok(ApiTemplatesResponse { let mut rsp = ApiTemplatesResponse {
templates: ret, templates: ret,
custom_template: if custom_templates.is_empty() { custom_template: if custom_templates.is_empty() {
None None
} else { } else {
const GB: u64 = 1024 * 1024 * 1024; const GB: u64 = 1024 * 1024 * 1024;
let max_cpu = templates.iter().map(|t| t.cpu).max().unwrap_or(8);
let max_memory = templates.iter().map(|t| t.memory).max().unwrap_or(GB * 2);
let max_disk = templates
.iter()
.map(|t| t.disk_size)
.max()
.unwrap_or(GB * 5);
Some( Some(
custom_templates custom_templates
.into_iter() .into_iter()
.map(|t| ApiCustomTemplateParams { .filter_map(|t| {
id: t.id, let region = regions.get(&t.region_id)?;
name: t.name, Some(
region: regions ApiCustomTemplateParams::from(
.get(&t.region_id) &t,
.map(|r| ApiVmHostRegion { &custom_template_disks,
id: r.id, region,
name: r.name.clone(), max_cpu,
}) max_memory,
.context("Region information missing in custom template") max_disk,
.unwrap(), )
max_cpu: templates.iter().map(|t| t.cpu).max().unwrap_or(8), .ok()?,
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(), .collect(),
) )
}, },
}) };
rsp.expand_pricing(rates).await?;
ApiData::ok(rsp)
} }
/// Get a price for a custom order /// Get a price for a custom order

View File

@ -4,7 +4,7 @@ use log::info;
use rocket::serde::Deserialize; use rocket::serde::Deserialize;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
@ -91,11 +91,23 @@ pub trait ExchangeRateService: Send + Sync {
/// Get alternative prices based on a source price /// Get alternative prices based on a source price
pub fn alt_prices(rates: &Vec<TickerRate>, source: CurrencyAmount) -> Vec<CurrencyAmount> { pub fn alt_prices(rates: &Vec<TickerRate>, source: CurrencyAmount) -> Vec<CurrencyAmount> {
// TODO: return all alt prices by cross-converting all currencies let mut ret: Vec<CurrencyAmount> = rates
rates
.iter() .iter()
.filter_map(|r| r.convert(source).ok()) .filter_map(|r| r.convert(source).ok())
.collect() .collect();
let mut ret2 = vec![];
for y in rates.iter() {
for x in ret.iter() {
if let Ok(r1) = y.convert(x.clone()) {
if r1.0 != source.0 {
ret2.push(r1);
}
}
}
}
ret.append(&mut ret2);
ret
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]