feat: alt prices
This commit is contained in:
@ -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,18 +144,14 @@ 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),
|
|
||||||
) {
|
|
||||||
template.cost_plan.price.push(ApiPrice {
|
|
||||||
currency: alt_price.0,
|
currency: alt_price.0,
|
||||||
amount: alt_price.1,
|
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,
|
||||||
|
@ -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;
|
||||||
Some(
|
let max_cpu = templates.iter().map(|t| t.cpu).max().unwrap_or(8);
|
||||||
custom_templates
|
let max_memory = templates.iter().map(|t| t.memory).max().unwrap_or(GB * 2);
|
||||||
.into_iter()
|
let max_disk = templates
|
||||||
.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()
|
.iter()
|
||||||
.map(|t| t.disk_size)
|
.map(|t| t.disk_size)
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(GB * 5),
|
.unwrap_or(GB * 5);
|
||||||
disks: custom_template_disks
|
Some(
|
||||||
.iter()
|
custom_templates
|
||||||
.filter(|d| d.pricing_id == t.id)
|
.into_iter()
|
||||||
.map(|d| ApiCustomTemplateDiskParam {
|
.filter_map(|t| {
|
||||||
disk_type: d.kind.into(),
|
let region = regions.get(&t.region_id)?;
|
||||||
disk_interface: d.interface.into(),
|
Some(
|
||||||
})
|
ApiCustomTemplateParams::from(
|
||||||
.collect(),
|
&t,
|
||||||
|
&custom_template_disks,
|
||||||
|
region,
|
||||||
|
max_cpu,
|
||||||
|
max_memory,
|
||||||
|
max_disk,
|
||||||
|
)
|
||||||
|
.ok()?,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
};
|
||||||
|
rsp.expand_pricing(rates).await?;
|
||||||
|
ApiData::ok(rsp)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a price for a custom order
|
/// Get a price for a custom order
|
||||||
|
@ -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)]
|
||||||
|
Reference in New Issue
Block a user