feat: separate db models from api models

feat: generate openapi docs
This commit is contained in:
2025-02-25 15:06:43 +00:00
parent c61cfde5c1
commit 870995e1fc
16 changed files with 1188 additions and 868 deletions

1242
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,20 +15,22 @@ nostr-dm = ["dep:nostr-sdk"]
lnvps_db = { path = "lnvps_db" } lnvps_db = { path = "lnvps_db" }
tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros"] }
anyhow = "1.0.83" anyhow = "1.0.83"
config = { version = "0.14.0", features = ["yaml"] } config = { version = "0.15.8", features = ["yaml"] }
log = "0.4.21" log = "0.4.21"
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
serde = { version = "1.0.213", features = ["derive"] } serde = { version = "1.0.213", features = ["derive"] }
reqwest = { version = "0.12.8" }
serde_json = "1.0.132" serde_json = "1.0.132"
reqwest = { version = "0.12.8" }
rocket = { version = "0.5.1", features = ["json"] } rocket = { version = "0.5.1", features = ["json"] }
rocket_okapi = { version = "0.9.0", features = ["swagger", "rapidoc"] }
schemars = { version = "0.8.22", features = ["chrono"] }
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
nostr = { version = "0.37.0", default-features = false, features = ["std"] } nostr = { version = "0.39.0", default-features = false, features = ["std"] }
base64 = { version = "0.22.1", features = ["alloc"] } base64 = { version = "0.22.1", features = ["alloc"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] } fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] }
ipnetwork = "0.20.0" ipnetwork = "0.21.1"
rand = "0.8.5" rand = "0.9.0"
clap = { version = "4.5.21", features = ["derive"] } clap = { version = "4.5.21", features = ["derive"] }
ssh2 = "0.9.4" ssh2 = "0.9.4"
ssh-key = "0.6.7" ssh-key = "0.6.7"
@ -38,5 +40,7 @@ tokio-tungstenite = { version = "^0.21", features = ["native-tls"] }
native-tls = "0.2.12" native-tls = "0.2.12"
#nostr-dm #nostr-dm
nostr-sdk = { version = "0.37.0", optional = true, default-features = false, features = ["nip44", "nip59"] } nostr-sdk = { version = "0.39.0", optional = true, default-features = false, features = ["nip44", "nip59"] }

View File

@ -42,7 +42,7 @@
<div class="page"> <div class="page">
<div class="header"> <div class="header">
LNVPS LNVPS
<img height="48" src="https://lnvps.net/logo.jpg" alt="logo"/> <img height="48" width="48" src="https://lnvps.net/logo.jpg" alt="logo"/>
</div> </div>
<hr/> <hr/>
<p>%%_MESSAGE_%%</p> <p>%%_MESSAGE_%%</p>

View File

@ -10,8 +10,6 @@ mysql = ["sqlx/mysql"]
[dependencies] [dependencies]
anyhow = "1.0.83" anyhow = "1.0.83"
sqlx = { version = "0.8.2", features = ["chrono", "migrate", "runtime-tokio"] } sqlx = { version = "0.8.2", features = ["chrono", "migrate", "runtime-tokio"] }
serde = { version = "1.0.213", features = ["derive"] }
serde_with = { version = "3.11.0", features = ["macros", "hex"] }
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
async-trait = "0.1.83" async-trait = "0.1.83"
url = "2.5.4" url = "2.5.4"

View File

@ -1,63 +0,0 @@
use crate::{LNVpsDb, Vm, VmIpAssignment, VmTemplate};
use anyhow::Result;
use async_trait::async_trait;
use std::ops::Deref;
#[async_trait]
pub trait Hydrate<D> {
/// Load parent resources
async fn hydrate_up(&mut self, db: &D) -> Result<()>;
/// Load child resources
async fn hydrate_down(&mut self, db: &D) -> Result<()>;
}
#[async_trait]
impl<D: Deref<Target = dyn LNVpsDb> + Sync> Hydrate<D> for Vm {
async fn hydrate_up(&mut self, db: &D) -> Result<()> {
let image = db.get_os_image(self.image_id).await?;
let template = db.get_vm_template(self.template_id).await?;
let ssh_key = db.get_user_ssh_key(self.ssh_key_id).await?;
self.image = Some(image);
self.template = Some(template);
self.ssh_key = Some(ssh_key);
Ok(())
}
async fn hydrate_down(&mut self, db: &D) -> Result<()> {
//let payments = db.list_vm_payment(self.id).await?;
let ips = db.list_vm_ip_assignments(self.id).await?;
//self.payments = Some(payments);
self.ip_assignments = Some(ips);
Ok(())
}
}
#[async_trait]
impl<D: Deref<Target = dyn LNVpsDb> + Sync> Hydrate<D> for VmTemplate {
async fn hydrate_up(&mut self, db: &D) -> Result<()> {
let cost_plan = db.get_cost_plan(self.cost_plan_id).await?;
let region = db.get_host_region(self.region_id).await?;
self.cost_plan = Some(cost_plan);
self.region = Some(region);
Ok(())
}
async fn hydrate_down(&mut self, db: &D) -> Result<()> {
todo!()
}
}
#[async_trait]
impl<D: Deref<Target = dyn LNVpsDb> + Sync> Hydrate<D> for VmIpAssignment {
async fn hydrate_up(&mut self, db: &D) -> Result<()> {
self.ip_range = Some(db.get_ip_range(self.ip_range_id).await?);
Ok(())
}
async fn hydrate_down(&mut self, db: &D) -> Result<()> {
todo!()
}
}

View File

@ -1,7 +1,6 @@
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
pub mod hydrate;
mod model; mod model;
#[cfg(feature = "mysql")] #[cfg(feature = "mysql")]
mod mysql; mod mysql;

View File

@ -1,61 +1,49 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use sqlx::FromRow; use sqlx::FromRow;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::path::PathBuf; use std::path::PathBuf;
use url::Url; use url::Url;
#[serde_as] #[derive(FromRow, Clone, Debug)]
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
/// Users who buy VM's /// Users who buy VM's
pub struct User { pub struct User {
/// Unique ID of this user (database generated) /// Unique ID of this user (database generated)
pub id: u64, pub id: u64,
/// The nostr public key for this user /// The nostr public key for this user
#[serde_as(as = "serde_with::hex::Hex")]
pub pubkey: Vec<u8>, pub pubkey: Vec<u8>,
/// When this user first started using the service (first login) /// When this user first started using the service (first login)
pub created: DateTime<Utc>, pub created: DateTime<Utc>,
pub email: Option<String>, pub email: Option<String>,
pub contact_nip4: bool, pub contact_nip4: bool,
pub contact_nip17: bool, pub contact_nip17: bool,
pub contact_email: bool, pub contact_email: bool,
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)] #[derive(FromRow, Clone, Debug, Default)]
pub struct UserSshKey { pub struct UserSshKey {
pub id: u64, pub id: u64,
pub name: String, pub name: String,
pub user_id: u64, pub user_id: u64,
pub created: DateTime<Utc>, pub created: DateTime<Utc>,
#[serde(skip_serializing)]
pub key_data: String, pub key_data: String,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub vms: Option<Vec<Vm>>,
} }
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)] #[derive(Clone, Debug, sqlx::Type)]
#[repr(u16)] #[repr(u16)]
/// The type of VM host /// The type of VM host
pub enum VmHostKind { pub enum VmHostKind {
Proxmox = 0, Proxmox = 0,
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(FromRow, Clone, Debug)]
pub struct VmHostRegion { pub struct VmHostRegion {
pub id: u64, pub id: u64,
pub name: String, pub name: String,
pub enabled: bool, pub enabled: bool,
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(FromRow, Clone, Debug)]
/// A VM host /// A VM host
pub struct VmHost { pub struct VmHost {
/// Unique id of this host /// Unique id of this host
@ -78,7 +66,7 @@ pub struct VmHost {
pub api_token: String, pub api_token: String,
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(FromRow, Clone, Debug)]
pub struct VmHostDisk { pub struct VmHostDisk {
pub id: u64, pub id: u64,
pub host_id: u64, pub host_id: u64,
@ -89,8 +77,7 @@ pub struct VmHostDisk {
pub enabled: bool, pub enabled: bool,
} }
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)] #[derive(Clone, Debug, sqlx::Type, Default)]
#[serde(rename_all = "lowercase")]
#[repr(u16)] #[repr(u16)]
pub enum DiskType { pub enum DiskType {
#[default] #[default]
@ -98,8 +85,7 @@ pub enum DiskType {
SSD = 1, SSD = 1,
} }
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)] #[derive(Clone, Debug, sqlx::Type, Default)]
#[serde(rename_all = "lowercase")]
#[repr(u16)] #[repr(u16)]
pub enum DiskInterface { pub enum DiskInterface {
#[default] #[default]
@ -108,7 +94,7 @@ pub enum DiskInterface {
PCIe = 2, PCIe = 2,
} }
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)] #[derive(Clone, Debug, sqlx::Type, Default)]
#[repr(u16)] #[repr(u16)]
pub enum OsDistribution { pub enum OsDistribution {
#[default] #[default]
@ -124,7 +110,7 @@ pub enum OsDistribution {
/// OS Images are templates which are used as a basis for /// OS Images are templates which are used as a basis for
/// provisioning new vms /// provisioning new vms
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(FromRow, Clone, Debug)]
pub struct VmOsImage { pub struct VmOsImage {
pub id: u64, pub id: u64,
pub distribution: OsDistribution, pub distribution: OsDistribution,
@ -132,8 +118,6 @@ pub struct VmOsImage {
pub version: String, pub version: String,
pub enabled: bool, pub enabled: bool,
pub release_date: DateTime<Utc>, pub release_date: DateTime<Utc>,
#[serde(skip_serializing)]
/// URL location of cloud image /// URL location of cloud image
pub url: String, pub url: String,
} }
@ -158,7 +142,7 @@ impl Display for VmOsImage {
} }
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(FromRow, Clone, Debug)]
pub struct IpRange { pub struct IpRange {
pub id: u64, pub id: u64,
pub cidr: String, pub cidr: String,
@ -167,8 +151,7 @@ pub struct IpRange {
pub region_id: u64, pub region_id: u64,
} }
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)] #[derive(Clone, Debug, sqlx::Type)]
#[serde(rename_all = "lowercase")]
#[repr(u16)] #[repr(u16)]
pub enum VmCostPlanIntervalType { pub enum VmCostPlanIntervalType {
Day = 0, Day = 0,
@ -176,7 +159,7 @@ pub enum VmCostPlanIntervalType {
Year = 2, Year = 2,
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] #[derive(FromRow, Clone, Debug)]
pub struct VmCostPlan { pub struct VmCostPlan {
pub id: u64, pub id: u64,
pub name: String, pub name: String,
@ -189,13 +172,12 @@ pub struct VmCostPlan {
/// Offers. /// Offers.
/// These are the same as the offers visible to customers /// These are the same as the offers visible to customers
#[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)] #[derive(FromRow, Clone, Debug, Default)]
pub struct VmTemplate { pub struct VmTemplate {
pub id: u64, pub id: u64,
pub name: String, pub name: String,
pub enabled: bool, pub enabled: bool,
pub created: DateTime<Utc>, pub created: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires: Option<DateTime<Utc>>, pub expires: Option<DateTime<Utc>>,
pub cpu: u16, pub cpu: u16,
pub memory: u64, pub memory: u64,
@ -204,16 +186,9 @@ pub struct VmTemplate {
pub disk_interface: DiskInterface, pub disk_interface: DiskInterface,
pub cost_plan_id: u64, pub cost_plan_id: u64,
pub region_id: u64, pub region_id: u64,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub cost_plan: Option<VmCostPlan>,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub region: Option<VmHostRegion>,
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)] #[derive(FromRow, Clone, Debug, Default)]
pub struct Vm { pub struct Vm {
/// Unique VM ID (Same in proxmox) /// Unique VM ID (Same in proxmox)
pub id: u64, pub id: u64,
@ -237,35 +212,14 @@ pub struct Vm {
pub mac_address: String, pub mac_address: String,
/// Is the VM deleted /// Is the VM deleted
pub deleted: bool, pub deleted: bool,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<VmOsImage>,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<VmTemplate>,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_key: Option<UserSshKey>,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub payments: Option<Vec<VmPayment>>,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_assignments: Option<Vec<VmIpAssignment>>,
} }
#[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)] #[derive(FromRow, Clone, Debug, Default)]
pub struct VmIpAssignment { pub struct VmIpAssignment {
pub id: u64, pub id: u64,
pub vm_id: u64, pub vm_id: u64,
pub ip_range_id: u64, pub ip_range_id: u64,
pub ip: String, pub ip: String,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_range: Option<IpRange>,
} }
impl Display for VmIpAssignment { impl Display for VmIpAssignment {
@ -274,11 +228,9 @@ impl Display for VmIpAssignment {
} }
} }
#[serde_as] #[derive(FromRow, Clone, Debug, Default)]
#[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)]
pub struct VmPayment { pub struct VmPayment {
/// Payment hash /// Payment hash
#[serde_as(as = "serde_with::hex::Hex")]
pub id: Vec<u8>, pub id: Vec<u8>,
pub vm_id: u64, pub vm_id: u64,
pub created: DateTime<Utc>, pub created: DateTime<Utc>,
@ -288,11 +240,7 @@ pub struct VmPayment {
pub is_paid: bool, pub is_paid: bool,
/// Exchange rate /// Exchange rate
pub rate: f32, pub rate: f32,
/// Number of seconds this payment will add to vm expiry /// Number of seconds this payment will add to vm expiry
#[serde(skip_serializing)]
pub time_value: u64, pub time_value: u64,
#[serde(skip_serializing)]
pub settle_index: Option<u64>, pub settle_index: Option<u64>,
} }

4
src/api/mod.rs Normal file
View File

@ -0,0 +1,4 @@
mod routes;
mod model;
pub use routes::routes;

277
src/api/model.rs Normal file
View File

@ -0,0 +1,277 @@
use nostr::util::hex;
use crate::status::VmState;
use chrono::{DateTime, Utc};
use lnvps_db::VmHostRegion;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ApiVmStatus {
/// Unique VM ID (Same in proxmox)
pub id: u64,
/// When the VM was created
pub created: DateTime<Utc>,
/// When the VM expires
pub expires: DateTime<Utc>,
/// Network MAC address
pub mac_address: String,
/// OS Image in use
pub image: ApiVmOsImage,
/// VM template
pub template: ApiVmTemplate,
/// SSH key attached to this VM
pub ssh_key: ApiUserSshKey,
/// IPs assigned to this VM
pub ip_assignments: Vec<ApiVmIpAssignment>,
/// Current running state of the VM
pub status: VmState,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ApiUserSshKey {
pub id: u64,
pub name: String,
pub created: DateTime<Utc>,
}
impl From<lnvps_db::UserSshKey> for ApiUserSshKey {
fn from(ssh_key: lnvps_db::UserSshKey) -> Self {
ApiUserSshKey {
id: ssh_key.id,
name: ssh_key.name,
created: ssh_key.created,
}
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ApiVmIpAssignment {
pub id: u64,
pub ip: String,
pub range: String,
}
impl ApiVmIpAssignment {
pub fn from(ip: lnvps_db::VmIpAssignment, range: &lnvps_db::IpRange) -> Self {
ApiVmIpAssignment {
id: ip.id,
ip: ip.ip,
range: range.cidr.clone(),
}
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum DiskType {
HDD = 0,
SSD = 1,
}
impl From<lnvps_db::DiskType> for DiskType {
fn from(value: lnvps_db::DiskType) -> Self {
match value {
lnvps_db::DiskType::HDD => Self::HDD,
lnvps_db::DiskType::SSD => Self::SSD,
}
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum DiskInterface {
SATA = 0,
SCSI = 1,
PCIe = 2,
}
impl From<lnvps_db::DiskInterface> for DiskInterface {
fn from(value: lnvps_db::DiskInterface) -> Self {
match value {
lnvps_db::DiskInterface::SATA => Self::SATA,
lnvps_db::DiskInterface::SCSI => Self::SCSI,
lnvps_db::DiskInterface::PCIe => Self::PCIe,
}
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ApiVmTemplate {
pub id: u64,
pub name: String,
pub created: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires: Option<DateTime<Utc>>,
pub cpu: u16,
pub memory: u64,
pub disk_size: u64,
pub disk_type: DiskType,
pub disk_interface: DiskInterface,
pub cost_plan: ApiVmCostPlan,
pub region: ApiVmHostRegion,
}
impl ApiVmTemplate {
pub fn from(
template: lnvps_db::VmTemplate,
cost_plan: lnvps_db::VmCostPlan,
region: VmHostRegion,
) -> Self {
Self {
id: template.id,
name: template.name,
created: template.created,
expires: template.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(),
},
region: ApiVmHostRegion {
id: region.id,
name: region.name,
},
}
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ApiVmCostPlanIntervalType {
Day = 0,
Month = 1,
Year = 2,
}
impl From<lnvps_db::VmCostPlanIntervalType> for ApiVmCostPlanIntervalType {
fn from(value: lnvps_db::VmCostPlanIntervalType) -> Self {
match value {
lnvps_db::VmCostPlanIntervalType::Day => Self::Day,
lnvps_db::VmCostPlanIntervalType::Month => Self::Month,
lnvps_db::VmCostPlanIntervalType::Year => Self::Year,
}
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ApiVmCostPlan {
pub id: u64,
pub name: String,
pub amount: u64,
pub currency: String,
pub interval_amount: u64,
pub interval_type: ApiVmCostPlanIntervalType,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ApiVmHostRegion {
pub id: u64,
pub name: String,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct VMPatchRequest {
/// SSH key assigned to vm
pub ssh_key_id: Option<u64>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct AccountPatchRequest {
pub email: Option<String>,
pub contact_nip17: bool,
pub contact_email: bool,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct CreateVmRequest {
pub template_id: u64,
pub image_id: u64,
pub ssh_key_id: u64,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct CreateSshKey {
pub name: String,
pub key_data: String,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum ApiOsDistribution {
Ubuntu = 0,
Debian = 1,
CentOS = 2,
Fedora = 3,
FreeBSD = 4,
OpenSUSE = 5,
ArchLinux = 6,
RedHatEnterprise = 7,
}
impl From<lnvps_db::OsDistribution> for ApiOsDistribution {
fn from(value: lnvps_db::OsDistribution) -> Self {
match value {
lnvps_db::OsDistribution::Ubuntu => Self::Ubuntu,
lnvps_db::OsDistribution::Debian => Self::Debian,
lnvps_db::OsDistribution::CentOS => Self::CentOS,
lnvps_db::OsDistribution::Fedora => Self::Fedora,
lnvps_db::OsDistribution::FreeBSD => Self::FreeBSD,
lnvps_db::OsDistribution::OpenSUSE => Self::OpenSUSE,
lnvps_db::OsDistribution::ArchLinux => Self::ArchLinux,
lnvps_db::OsDistribution::RedHatEnterprise => Self::RedHatEnterprise,
}
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ApiVmOsImage {
pub id: u64,
pub distribution: ApiOsDistribution,
pub flavour: String,
pub version: String,
pub release_date: DateTime<Utc>,
}
impl From<lnvps_db::VmOsImage> for ApiVmOsImage {
fn from(image: lnvps_db::VmOsImage) -> Self {
ApiVmOsImage {
id: image.id,
distribution: image.distribution.into(),
flavour: image.flavour,
version: image.version,
release_date: image.release_date,
}
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ApiVmPayment {
/// Payment hash hex
pub id: String,
pub vm_id: u64,
pub created: DateTime<Utc>,
pub expires: DateTime<Utc>,
pub amount: u64,
pub invoice: String,
pub is_paid: bool,
}
impl From<lnvps_db::VmPayment> for ApiVmPayment {
fn from(value: lnvps_db::VmPayment) -> Self {
Self {
id: hex::encode(&value.id),
vm_id: value.vm_id,
created: value.created,
expires: value.expires,
amount: value.amount,
invoice: value.invoice,
is_paid: value.is_paid,
}
}
}

View File

@ -1,23 +1,33 @@
use crate::api::model::{
AccountPatchRequest, ApiUserSshKey, ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus,
ApiVmTemplate, CreateSshKey, CreateVmRequest, VMPatchRequest,
};
use crate::nip98::Nip98Auth; use crate::nip98::Nip98Auth;
use crate::provisioner::Provisioner; use crate::provisioner::Provisioner;
use crate::status::{VmState, VmStateCache}; use crate::status::{VmState, VmStateCache};
use crate::worker::WorkJob; use crate::worker::WorkJob;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use lnvps_db::hydrate::Hydrate; use lnvps_db::{IpRange, LNVpsDb};
use lnvps_db::{LNVpsDb, UserSshKey, Vm, VmOsImage, VmPayment, VmTemplate};
use log::{debug, error}; use log::{debug, error};
use nostr::util::hex; use nostr::util::hex;
use nostr_sdk::async_utility::futures_util::future::join_all;
use rocket::futures::{Sink, SinkExt, StreamExt}; use rocket::futures::{Sink, SinkExt, StreamExt};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::{get, patch, post, routes, Responder, Route, State}; use rocket::{get, patch, post, Responder, Route, State};
use rocket_okapi::gen::OpenApiGenerator;
use rocket_okapi::okapi::openapi3::Responses;
use rocket_okapi::response::OpenApiResponderInner;
use rocket_okapi::{openapi, openapi_get_routes};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ssh_key::PublicKey; use ssh_key::PublicKey;
use std::collections::{HashMap, HashSet};
use std::fmt::Display; use std::fmt::Display;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use ws::Message; use ws::Message;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ openapi_get_routes![
v1_get_account, v1_get_account,
v1_patch_account, v1_patch_account,
v1_list_vms, v1_list_vms,
@ -32,14 +42,13 @@ pub fn routes() -> Vec<Route> {
v1_start_vm, v1_start_vm,
v1_stop_vm, v1_stop_vm,
v1_restart_vm, v1_restart_vm,
v1_terminal_proxy,
v1_patch_vm v1_patch_vm
] ]
} }
type ApiResult<T> = Result<Json<ApiData<T>>, ApiError>; type ApiResult<T> = Result<Json<ApiData<T>>, ApiError>;
#[derive(Serialize)] #[derive(Serialize, Deserialize, JsonSchema)]
struct ApiData<T: Serialize> { struct ApiData<T: Serialize> {
pub data: T, pub data: T,
} }
@ -53,7 +62,7 @@ impl<T: Serialize> ApiData<T> {
} }
} }
#[derive(Responder)] #[derive(Serialize, Deserialize, JsonSchema, Responder)]
#[response(status = 500)] #[response(status = 500)]
struct ApiError { struct ApiError {
pub error: String, pub error: String,
@ -67,25 +76,14 @@ impl<T: ToString> From<T> for ApiError {
} }
} }
#[derive(Serialize)] impl OpenApiResponderInner for ApiError {
struct ApiVmStatus { fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
#[serde(flatten)] Ok(Responses::default())
pub vm: Vm, }
pub status: VmState,
}
#[derive(Serialize, Deserialize)]
struct VMPatchRequest {
pub ssh_key_id: Option<u64>,
}
#[derive(Serialize, Deserialize)]
struct AccountPatchRequest {
pub email: Option<String>,
pub contact_nip17: bool,
pub contact_email: bool,
} }
/// Update user account
#[openapi(tag = "Account")]
#[patch("/api/v1/account", format = "json", data = "<req>")] #[patch("/api/v1/account", format = "json", data = "<req>")]
async fn v1_patch_account( async fn v1_patch_account(
auth: Nip98Auth, auth: Nip98Auth,
@ -104,6 +102,8 @@ async fn v1_patch_account(
ApiData::ok(()) ApiData::ok(())
} }
/// Get user account detail
#[openapi(tag = "Account")]
#[get("/api/v1/account")] #[get("/api/v1/account")]
async fn v1_get_account( async fn v1_get_account(
auth: Nip98Auth, auth: Nip98Auth,
@ -111,7 +111,7 @@ async fn v1_get_account(
) -> ApiResult<AccountPatchRequest> { ) -> ApiResult<AccountPatchRequest> {
let pubkey = auth.event.pubkey.to_bytes(); let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?; let uid = db.upsert_user(&pubkey).await?;
let mut user = db.get_user(uid).await?; let user = db.get_user(uid).await?;
ApiData::ok(AccountPatchRequest { ApiData::ok(AccountPatchRequest {
email: user.email, email: user.email,
@ -120,6 +120,49 @@ async fn v1_get_account(
}) })
} }
async fn vm_to_status(
db: &Box<dyn LNVpsDb>,
vm: lnvps_db::Vm,
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();
let ip_ranges: Vec<_> = ip_range_ids.iter().map(|i| db.get_ip_range(*i)).collect();
let ip_ranges: HashMap<u64, IpRange> = join_all(ip_ranges)
.await
.into_iter()
.filter_map(Result::ok)
.map(|i| (i.id, i))
.collect();
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),
ssh_key: ssh_key.into(),
status: state.unwrap_or_default(),
ip_assignments: ips
.into_iter()
.map(|i| {
let range = ip_ranges
.get(&i.ip_range_id)
.expect("ip range id not found");
ApiVmIpAssignment::from(i, range)
})
.collect(),
})
}
/// List VMs belonging to user
#[openapi(tag = "VM")]
#[get("/api/v1/vm")] #[get("/api/v1/vm")]
async fn v1_list_vms( async fn v1_list_vms(
auth: Nip98Auth, auth: Nip98Auth,
@ -130,23 +173,16 @@ async fn v1_list_vms(
let uid = db.upsert_user(&pubkey).await?; let uid = db.upsert_user(&pubkey).await?;
let vms = db.list_user_vms(uid).await?; let vms = db.list_user_vms(uid).await?;
let mut ret = vec![]; let mut ret = vec![];
for mut vm in vms { for vm in vms {
vm.hydrate_up(db.inner()).await?; let vm_id = vm.id;
vm.hydrate_down(db.inner()).await?; ret.push(vm_to_status(db, vm, vm_state.get_state(vm_id).await).await?);
if let Some(t) = &mut vm.template {
t.hydrate_up(db.inner()).await?;
}
let state = vm_state.get_state(vm.id).await;
ret.push(ApiVmStatus {
vm,
status: state.unwrap_or_default(),
});
} }
ApiData::ok(ret) ApiData::ok(ret)
} }
/// Get status of a VM
#[openapi(tag = "VM")]
#[get("/api/v1/vm/<id>")] #[get("/api/v1/vm/<id>")]
async fn v1_get_vm( async fn v1_get_vm(
auth: Nip98Auth, auth: Nip98Auth,
@ -156,22 +192,15 @@ async fn v1_get_vm(
) -> ApiResult<ApiVmStatus> { ) -> ApiResult<ApiVmStatus> {
let pubkey = auth.event.pubkey.to_bytes(); let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?; let uid = db.upsert_user(&pubkey).await?;
let mut vm = db.get_vm(id).await?; let vm = db.get_vm(id).await?;
if vm.user_id != uid { if vm.user_id != uid {
return ApiData::err("VM doesnt belong to you"); return ApiData::err("VM doesnt belong to you");
} }
vm.hydrate_up(db.inner()).await?; ApiData::ok(vm_to_status(db, vm, vm_state.get_state(id).await).await?)
vm.hydrate_down(db.inner()).await?;
if let Some(t) = &mut vm.template {
t.hydrate_up(db.inner()).await?;
}
let state = vm_state.get_state(vm.id).await;
ApiData::ok(ApiVmStatus {
vm,
status: state.unwrap_or_default(),
})
} }
/// Update a VM config
#[openapi(tag = "VM")]
#[patch("/api/v1/vm/<id>", data = "<data>", format = "json")] #[patch("/api/v1/vm/<id>", data = "<data>", format = "json")]
async fn v1_patch_vm( async fn v1_patch_vm(
auth: Nip98Auth, auth: Nip98Auth,
@ -201,39 +230,88 @@ async fn v1_patch_vm(
ApiData::ok(()) ApiData::ok(())
} }
/// List available VM OS images
#[openapi(tag = "Image")]
#[get("/api/v1/image")] #[get("/api/v1/image")]
async fn v1_list_vm_images(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmOsImage>> { async fn v1_list_vm_images(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<ApiVmOsImage>> {
let vms = db.list_os_image().await?; let images = db.list_os_image().await?;
let vms: Vec<VmOsImage> = vms.into_iter().filter(|i| i.enabled).collect(); let ret = images
ApiData::ok(vms) .into_iter()
} .filter(|i| i.enabled)
.map(|i| i.into())
#[get("/api/v1/vm/templates")] .collect();
async fn v1_list_vm_templates(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmTemplate>> {
let mut vms = db.list_vm_templates().await?;
for vm in &mut vms {
vm.hydrate_up(db.inner()).await?;
}
let ret: Vec<VmTemplate> = vms.into_iter().filter(|v| v.enabled).collect();
ApiData::ok(ret) ApiData::ok(ret)
} }
/// List available VM templates (Offers)
#[openapi(tag = "Template")]
#[get("/api/v1/vm/templates")]
async fn v1_list_vm_templates(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<ApiVmTemplate>> {
let templates = db.list_vm_templates().await?;
let cost_plans: HashSet<u64> = templates.iter().map(|t| t.cost_plan_id).collect();
let regions: HashSet<u64> = templates.iter().map(|t| t.region_id).collect();
let cost_plans: Vec<_> = cost_plans
.into_iter()
.map(|i| db.get_cost_plan(i))
.collect();
let regions: Vec<_> = regions.into_iter().map(|r| db.get_host_region(r)).collect();
let cost_plans: HashMap<u64, lnvps_db::VmCostPlan> = join_all(cost_plans)
.await
.into_iter()
.filter_map(|c| {
let c = c.ok()?;
Some((c.id, c))
})
.collect();
let regions: HashMap<u64, lnvps_db::VmHostRegion> = join_all(regions)
.await
.into_iter()
.filter_map(|c| {
let c = c.ok()?;
Some((c.id, c))
})
.collect();
let ret = templates
.into_iter()
.filter(|v| v.enabled)
.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()))
})
.collect();
ApiData::ok(ret)
}
/// List user SSH keys
#[openapi(tag = "Account")]
#[get("/api/v1/ssh-key")] #[get("/api/v1/ssh-key")]
async fn v1_list_ssh_keys( async fn v1_list_ssh_keys(
auth: Nip98Auth, auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>, db: &State<Box<dyn LNVpsDb>>,
) -> ApiResult<Vec<UserSshKey>> { ) -> ApiResult<Vec<ApiUserSshKey>> {
let uid = db.upsert_user(&auth.event.pubkey.to_bytes()).await?; let uid = db.upsert_user(&auth.event.pubkey.to_bytes()).await?;
let keys = db.list_user_ssh_key(uid).await?; let ret = db
ApiData::ok(keys) .list_user_ssh_key(uid)
.await?
.into_iter()
.map(|i| i.into())
.collect();
ApiData::ok(ret)
} }
/// Add new SSH key to account
#[openapi(tag = "Account")]
#[post("/api/v1/ssh-key", data = "<req>", format = "json")] #[post("/api/v1/ssh-key", data = "<req>", format = "json")]
async fn v1_add_ssh_key( async fn v1_add_ssh_key(
auth: Nip98Auth, auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>, db: &State<Box<dyn LNVpsDb>>,
req: Json<CreateSshKey>, req: Json<CreateSshKey>,
) -> ApiResult<UserSshKey> { ) -> ApiResult<ApiUserSshKey> {
let uid = db.upsert_user(&auth.event.pubkey.to_bytes()).await?; let uid = db.upsert_user(&auth.event.pubkey.to_bytes()).await?;
let pk: PublicKey = req.key_data.parse()?; let pk: PublicKey = req.key_data.parse()?;
@ -242,7 +320,7 @@ async fn v1_add_ssh_key(
} else { } else {
pk.comment() pk.comment()
}; };
let mut new_key = UserSshKey { let mut new_key = lnvps_db::UserSshKey {
name: key_name.to_string(), name: key_name.to_string(),
user_id: uid, user_id: uid,
key_data: pk.to_openssh()?, key_data: pk.to_openssh()?,
@ -251,35 +329,42 @@ async fn v1_add_ssh_key(
let key_id = db.insert_user_ssh_key(&new_key).await?; let key_id = db.insert_user_ssh_key(&new_key).await?;
new_key.id = key_id; new_key.id = key_id;
ApiData::ok(new_key) ApiData::ok(new_key.into())
} }
/// 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", data = "<req>", format = "json")] #[post("/api/v1/vm", data = "<req>", format = "json")]
async fn v1_create_vm_order( async fn v1_create_vm_order(
auth: Nip98Auth, auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>, db: &State<Box<dyn LNVpsDb>>,
provisioner: &State<Box<dyn Provisioner>>, provisioner: &State<Box<dyn Provisioner>>,
req: Json<CreateVmRequest>, req: Json<CreateVmRequest>,
) -> ApiResult<Vm> { ) -> ApiResult<ApiVmStatus> {
let pubkey = auth.event.pubkey.to_bytes(); let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?; let uid = db.upsert_user(&pubkey).await?;
let req = req.0; let req = req.0;
let mut rsp = provisioner let rsp = provisioner
.provision(uid, req.template_id, req.image_id, req.ssh_key_id) .provision(uid, req.template_id, req.image_id, req.ssh_key_id)
.await?; .await?;
rsp.hydrate_up(db.inner()).await?; ApiData::ok(vm_to_status(db, rsp, None).await?)
ApiData::ok(rsp)
} }
/// Renew(Extend) a VM
#[openapi(tag = "VM")]
#[get("/api/v1/vm/<id>/renew")] #[get("/api/v1/vm/<id>/renew")]
async fn v1_renew_vm( async fn v1_renew_vm(
auth: Nip98Auth, auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>, db: &State<Box<dyn LNVpsDb>>,
provisioner: &State<Box<dyn Provisioner>>, provisioner: &State<Box<dyn Provisioner>>,
id: u64, id: u64,
) -> ApiResult<VmPayment> { ) -> ApiResult<ApiVmPayment> {
let pubkey = auth.event.pubkey.to_bytes(); let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?; let uid = db.upsert_user(&pubkey).await?;
let vm = db.get_vm(id).await?; let vm = db.get_vm(id).await?;
@ -288,9 +373,11 @@ async fn v1_renew_vm(
} }
let rsp = provisioner.renew(id).await?; let rsp = provisioner.renew(id).await?;
ApiData::ok(rsp) ApiData::ok(rsp.into())
} }
/// Start a VM
#[openapi(tag = "VM")]
#[patch("/api/v1/vm/<id>/start")] #[patch("/api/v1/vm/<id>/start")]
async fn v1_start_vm( async fn v1_start_vm(
auth: Nip98Auth, auth: Nip98Auth,
@ -311,6 +398,8 @@ async fn v1_start_vm(
ApiData::ok(()) ApiData::ok(())
} }
/// Stop a VM
#[openapi(tag = "VM")]
#[patch("/api/v1/vm/<id>/stop")] #[patch("/api/v1/vm/<id>/stop")]
async fn v1_stop_vm( async fn v1_stop_vm(
auth: Nip98Auth, auth: Nip98Auth,
@ -331,6 +420,8 @@ async fn v1_stop_vm(
ApiData::ok(()) ApiData::ok(())
} }
/// Restart a VM
#[openapi(tag = "VM")]
#[patch("/api/v1/vm/<id>/restart")] #[patch("/api/v1/vm/<id>/restart")]
async fn v1_restart_vm( async fn v1_restart_vm(
auth: Nip98Auth, auth: Nip98Auth,
@ -351,12 +442,14 @@ async fn v1_restart_vm(
ApiData::ok(()) ApiData::ok(())
} }
/// Get payment status (for polling)
#[openapi(tag = "Payment")]
#[get("/api/v1/payment/<id>")] #[get("/api/v1/payment/<id>")]
async fn v1_get_payment( async fn v1_get_payment(
auth: Nip98Auth, auth: Nip98Auth,
db: &State<Box<dyn LNVpsDb>>, db: &State<Box<dyn LNVpsDb>>,
id: &str, id: &str,
) -> ApiResult<VmPayment> { ) -> ApiResult<ApiVmPayment> {
let pubkey = auth.event.pubkey.to_bytes(); let pubkey = auth.event.pubkey.to_bytes();
let uid = db.upsert_user(&pubkey).await?; let uid = db.upsert_user(&pubkey).await?;
let id = if let Ok(i) = hex::decode(id) { let id = if let Ok(i) = hex::decode(id) {
@ -370,7 +463,7 @@ async fn v1_get_payment(
return ApiData::err("VM does not belong to you"); return ApiData::err("VM does not belong to you");
} }
ApiData::ok(payment) ApiData::ok(payment.into())
} }
#[get("/api/v1/console/<id>?<auth>")] #[get("/api/v1/console/<id>?<auth>")]
@ -478,25 +571,3 @@ async fn v1_terminal_proxy(
}) })
})) }))
} }
#[derive(Deserialize)]
struct CreateVmRequest {
template_id: u64,
image_id: u64,
ssh_key_id: u64,
}
impl From<CreateVmRequest> for VmTemplate {
fn from(val: CreateVmRequest) -> Self {
VmTemplate {
id: val.template_id,
..Default::default()
}
}
}
#[derive(Deserialize)]
struct CreateSshKey {
name: String,
key_data: String,
}

View File

@ -14,6 +14,7 @@ use lnvps_db::{LNVpsDb, LNVpsDbMysql};
use log::error; use log::error;
use nostr::Keys; use nostr::Keys;
use nostr_sdk::Client; use nostr_sdk::Client;
use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig};
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::time::Duration; use std::time::Duration;
use tokio::time::sleep; use tokio::time::sleep;
@ -156,6 +157,13 @@ async fn main() -> Result<(), Error> {
.manage(exchange) .manage(exchange)
.manage(sender) .manage(sender)
.mount("/", api::routes()) .mount("/", api::routes())
.mount(
"/swagger",
make_swagger_ui(&SwaggerUIConfig {
url: "../openapi.json".to_owned(),
..Default::default()
}),
)
.launch() .launch()
.await .await
{ {

View File

@ -7,6 +7,9 @@ use rocket::http::uri::{Absolute, Uri};
use rocket::http::Status; use rocket::http::Status;
use rocket::request::{FromRequest, Outcome}; use rocket::request::{FromRequest, Outcome};
use rocket::{async_trait, Request}; use rocket::{async_trait, Request};
use rocket_okapi::gen::OpenApiGenerator;
use rocket_okapi::okapi::openapi3::{SecurityRequirement, SecurityScheme, SecuritySchemeData};
use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
pub struct Nip98Auth { pub struct Nip98Auth {
pub event: Event, pub event: Event,
@ -106,3 +109,27 @@ impl<'r> FromRequest<'r> for Nip98Auth {
} }
} }
} }
impl OpenApiFromRequest<'_> for Nip98Auth {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> rocket_okapi::Result<RequestHeaderInput> {
let security_scheme = SecurityScheme {
description: Some("Requires an Bearer token to access".to_owned()),
data: SecuritySchemeData::Http {
scheme: "Nostr".to_owned(),
bearer_format: Some("base64-encoded-auth-event".to_owned()),
},
extensions: Default::default(),
};
let mut security_req = SecurityRequirement::new();
security_req.insert("NostrAuth".to_owned(), Vec::new());
Ok(RequestHeaderInput::Security(
"NostrAuth".to_owned(),
security_scheme,
security_req,
))
}
}

View File

@ -14,13 +14,13 @@ use fedimint_tonic_lnd::lnrpc::Invoice;
use fedimint_tonic_lnd::tonic::async_trait; use fedimint_tonic_lnd::tonic::async_trait;
use fedimint_tonic_lnd::Client; use fedimint_tonic_lnd::Client;
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use lnvps_db::hydrate::Hydrate;
use lnvps_db::{DiskType, IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment}; use lnvps_db::{DiskType, IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
use log::{debug, info, warn}; use log::{debug, info, warn};
use nostr::util::hex; use nostr::util::hex;
use rand::random; use rand::random;
use rand::seq::IteratorRandom; use rand::seq::IteratorRandom;
use reqwest::Url; use reqwest::Url;
use rocket::futures::future::join_all;
use rocket::futures::{SinkExt, StreamExt}; use rocket::futures::{SinkExt, StreamExt};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::net::IpAddr; use std::net::IpAddr;
@ -81,10 +81,17 @@ impl LNVpsProvisioner {
ips = self.allocate_ips(vm.id).await?; ips = self.allocate_ips(vm.id).await?;
} }
// load ranges let ip_range_ids: HashSet<u64> = ips.iter().map(|i| i.ip_range_id).collect();
for ip in &mut ips { let ip_ranges: Vec<_> = ip_range_ids
ip.hydrate_up(&self.db).await?; .iter()
} .map(|i| self.db.get_ip_range(*i))
.collect();
let ip_ranges: HashMap<u64, IpRange> = join_all(ip_ranges)
.await
.into_iter()
.filter_map(Result::ok)
.map(|i| (i.id, i))
.collect();
let mut ip_config = ips let mut ip_config = ips
.iter() .iter()
@ -92,11 +99,8 @@ impl LNVpsProvisioner {
if let Ok(net) = ip.ip.parse::<IpNetwork>() { if let Ok(net) = ip.ip.parse::<IpNetwork>() {
Some(match net { Some(match net {
IpNetwork::V4(addr) => { IpNetwork::V4(addr) => {
format!( let range = ip_ranges.get(&ip.ip_range_id)?;
"ip={},gw={}", format!("ip={},gw={}", addr, range.gateway)
addr,
ip.ip_range.as_ref().map(|r| &r.gateway).unwrap()
)
} }
IpNetwork::V6(addr) => format!("ip6={}", addr), IpNetwork::V6(addr) => format!("ip6={}", addr),
}) })
@ -122,11 +126,7 @@ impl LNVpsProvisioner {
bail!("No host drive found!") bail!("No host drive found!")
}; };
let template = if let Some(t) = &vm.template { let template = self.db.get_vm_template(vm.template_id).await?;
t
} else {
&self.db.get_vm_template(vm.template_id).await?
};
Ok(VmConfig { Ok(VmConfig {
cpu: Some(self.config.cpu.clone()), cpu: Some(self.config.cpu.clone()),
@ -302,18 +302,18 @@ impl Provisioner for LNVpsProvisioner {
} }
async fn allocate_ips(&self, vm_id: u64) -> Result<Vec<VmIpAssignment>> { async fn allocate_ips(&self, vm_id: u64) -> Result<Vec<VmIpAssignment>> {
let mut vm = self.db.get_vm(vm_id).await?; let vm = self.db.get_vm(vm_id).await?;
let template = self.db.get_vm_template(vm.template_id).await?;
let ips = self.db.list_vm_ip_assignments(vm.id).await?; let ips = self.db.list_vm_ip_assignments(vm.id).await?;
if !ips.is_empty() { if !ips.is_empty() {
bail!("IP resources are already assigned"); bail!("IP resources are already assigned");
} }
vm.hydrate_up(&self.db).await?;
let ip_ranges = self.db.list_ip_range().await?; let ip_ranges = self.db.list_ip_range().await?;
let ip_ranges: Vec<IpRange> = ip_ranges let ip_ranges: Vec<IpRange> = ip_ranges
.into_iter() .into_iter()
.filter(|i| i.region_id == vm.template.as_ref().unwrap().region_id && i.enabled) .filter(|i| i.region_id == template.region_id && i.enabled)
.collect(); .collect();
if ip_ranges.is_empty() { if ip_ranges.is_empty() {

View File

@ -73,6 +73,7 @@ pub struct SmtpConfig {
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ProvisionerConfig { pub enum ProvisionerConfig {
Proxmox { Proxmox {
/// Readonly mode, don't spawn any VM's /// Readonly mode, don't spawn any VM's
@ -147,7 +148,7 @@ impl RouterConfig {
username, username,
password, password,
arp_interface, arp_interface,
} => MikrotikRouter::new(&url, &username, &password, &arp_interface), } => MikrotikRouter::new(url, username, password, arp_interface),
} }
} }
} }

View File

@ -2,9 +2,11 @@ use anyhow::Result;
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use rocket::serde::Deserialize;
use schemars::JsonSchema;
use tokio::sync::RwLock; use tokio::sync::RwLock;
#[derive(Clone, Serialize, Default)] #[derive(Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum VmRunningState { pub enum VmRunningState {
Running, Running,
@ -14,7 +16,7 @@ pub enum VmRunningState {
Deleting, Deleting,
} }
#[derive(Clone, Serialize, Default)] #[derive(Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct VmState { pub struct VmState {
pub timestamp: u64, pub timestamp: u64,
pub state: VmRunningState, pub state: VmRunningState,

View File

@ -48,11 +48,11 @@ pub struct WorkerSettings {
pub smtp: Option<SmtpConfig>, pub smtp: Option<SmtpConfig>,
} }
impl Into<WorkerSettings> for &Settings { impl From<&Settings> for WorkerSettings {
fn into(self) -> WorkerSettings { fn from(val: &Settings) -> Self {
WorkerSettings { WorkerSettings {
delete_after: self.delete_after, delete_after: val.delete_after,
smtp: self.smtp.clone(), smtp: val.smtp.clone(),
} }
} }
} }
@ -206,7 +206,7 @@ impl Worker {
if let Err(e) = self.handle_vm_info(vm).await { if let Err(e) = self.handle_vm_info(vm).await {
error!("{}", e); error!("{}", e);
self.queue_admin_notification( self.queue_admin_notification(
format!("Failed to check VM {}:\n{}", vm_id, e.to_string()), format!("Failed to check VM {}:\n{}", vm_id, e),
Some("Job Failed".to_string()), Some("Job Failed".to_string()),
)? )?
} }
@ -331,7 +331,7 @@ impl Worker {
"Failed to check VM {}:\n{:?}\n{}", "Failed to check VM {}:\n{:?}\n{}",
vm_id, vm_id,
&job, &job,
e.to_string() e
), ),
Some("Job Failed".to_string()), Some("Job Failed".to_string()),
)? )?
@ -351,7 +351,7 @@ impl Worker {
format!( format!(
"Failed to send notification:\n{:?}\n{}", "Failed to send notification:\n{:?}\n{}",
&job, &job,
e.to_string() e
), ),
Some("Job Failed".to_string()), Some("Job Failed".to_string()),
)? )?
@ -361,7 +361,7 @@ impl Worker {
if let Err(e) = self.check_vms().await { if let Err(e) = self.check_vms().await {
error!("Failed to check VMs: {}", e); error!("Failed to check VMs: {}", e);
self.queue_admin_notification( self.queue_admin_notification(
format!("Failed to check VM's:\n{:?}\n{}", &job, e.to_string()), format!("Failed to check VM's:\n{:?}\n{}", &job, e),
Some("Job Failed".to_string()), Some("Job Failed".to_string()),
)? )?
} }