feat: separate db models from api models
feat: generate openapi docs
This commit is contained in:
1242
Cargo.lock
generated
1242
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
@ -15,20 +15,22 @@ nostr-dm = ["dep:nostr-sdk"]
|
||||
lnvps_db = { path = "lnvps_db" }
|
||||
tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
anyhow = "1.0.83"
|
||||
config = { version = "0.14.0", features = ["yaml"] }
|
||||
config = { version = "0.15.8", features = ["yaml"] }
|
||||
log = "0.4.21"
|
||||
pretty_env_logger = "0.5.0"
|
||||
serde = { version = "1.0.213", features = ["derive"] }
|
||||
reqwest = { version = "0.12.8" }
|
||||
serde_json = "1.0.132"
|
||||
reqwest = { version = "0.12.8" }
|
||||
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"] }
|
||||
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"] }
|
||||
urlencoding = "2.1.3"
|
||||
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] }
|
||||
ipnetwork = "0.20.0"
|
||||
rand = "0.8.5"
|
||||
ipnetwork = "0.21.1"
|
||||
rand = "0.9.0"
|
||||
clap = { version = "4.5.21", features = ["derive"] }
|
||||
ssh2 = "0.9.4"
|
||||
ssh-key = "0.6.7"
|
||||
@ -38,5 +40,7 @@ tokio-tungstenite = { version = "^0.21", features = ["native-tls"] }
|
||||
native-tls = "0.2.12"
|
||||
|
||||
#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"] }
|
||||
|
||||
|
||||
|
||||
|
@ -42,7 +42,7 @@
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
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>
|
||||
<hr/>
|
||||
<p>%%_MESSAGE_%%</p>
|
||||
|
@ -10,8 +10,6 @@ mysql = ["sqlx/mysql"]
|
||||
[dependencies]
|
||||
anyhow = "1.0.83"
|
||||
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"] }
|
||||
async-trait = "0.1.83"
|
||||
url = "2.5.4"
|
@ -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!()
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub mod hydrate;
|
||||
mod model;
|
||||
#[cfg(feature = "mysql")]
|
||||
mod mysql;
|
||||
|
@ -1,61 +1,49 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
use sqlx::FromRow;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::path::PathBuf;
|
||||
use url::Url;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
|
||||
#[derive(FromRow, Clone, Debug)]
|
||||
/// Users who buy VM's
|
||||
pub struct User {
|
||||
/// Unique ID of this user (database generated)
|
||||
pub id: u64,
|
||||
|
||||
/// The nostr public key for this user
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
pub pubkey: Vec<u8>,
|
||||
|
||||
/// When this user first started using the service (first login)
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
pub email: Option<String>,
|
||||
pub contact_nip4: bool,
|
||||
pub contact_nip17: bool,
|
||||
pub contact_email: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)]
|
||||
#[derive(FromRow, Clone, Debug, Default)]
|
||||
pub struct UserSshKey {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub user_id: u64,
|
||||
pub created: DateTime<Utc>,
|
||||
#[serde(skip_serializing)]
|
||||
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)]
|
||||
/// The type of VM host
|
||||
pub enum VmHostKind {
|
||||
Proxmox = 0,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
|
||||
#[derive(FromRow, Clone, Debug)]
|
||||
pub struct VmHostRegion {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
|
||||
#[derive(FromRow, Clone, Debug)]
|
||||
/// A VM host
|
||||
pub struct VmHost {
|
||||
/// Unique id of this host
|
||||
@ -78,7 +66,7 @@ pub struct VmHost {
|
||||
pub api_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
|
||||
#[derive(FromRow, Clone, Debug)]
|
||||
pub struct VmHostDisk {
|
||||
pub id: u64,
|
||||
pub host_id: u64,
|
||||
@ -89,8 +77,7 @@ pub struct VmHostDisk {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Clone, Debug, sqlx::Type, Default)]
|
||||
#[repr(u16)]
|
||||
pub enum DiskType {
|
||||
#[default]
|
||||
@ -98,8 +85,7 @@ pub enum DiskType {
|
||||
SSD = 1,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Clone, Debug, sqlx::Type, Default)]
|
||||
#[repr(u16)]
|
||||
pub enum DiskInterface {
|
||||
#[default]
|
||||
@ -108,7 +94,7 @@ pub enum DiskInterface {
|
||||
PCIe = 2,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type, Default)]
|
||||
#[derive(Clone, Debug, sqlx::Type, Default)]
|
||||
#[repr(u16)]
|
||||
pub enum OsDistribution {
|
||||
#[default]
|
||||
@ -124,7 +110,7 @@ pub enum OsDistribution {
|
||||
|
||||
/// OS Images are templates which are used as a basis for
|
||||
/// provisioning new vms
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
|
||||
#[derive(FromRow, Clone, Debug)]
|
||||
pub struct VmOsImage {
|
||||
pub id: u64,
|
||||
pub distribution: OsDistribution,
|
||||
@ -132,8 +118,6 @@ pub struct VmOsImage {
|
||||
pub version: String,
|
||||
pub enabled: bool,
|
||||
pub release_date: DateTime<Utc>,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
/// URL location of cloud image
|
||||
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 id: u64,
|
||||
pub cidr: String,
|
||||
@ -167,8 +151,7 @@ pub struct IpRange {
|
||||
pub region_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, sqlx::Type)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Clone, Debug, sqlx::Type)]
|
||||
#[repr(u16)]
|
||||
pub enum VmCostPlanIntervalType {
|
||||
Day = 0,
|
||||
@ -176,7 +159,7 @@ pub enum VmCostPlanIntervalType {
|
||||
Year = 2,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug)]
|
||||
#[derive(FromRow, Clone, Debug)]
|
||||
pub struct VmCostPlan {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
@ -189,13 +172,12 @@ pub struct VmCostPlan {
|
||||
|
||||
/// Offers.
|
||||
/// 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 id: u64,
|
||||
pub name: String,
|
||||
pub enabled: bool,
|
||||
pub created: DateTime<Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expires: Option<DateTime<Utc>>,
|
||||
pub cpu: u16,
|
||||
pub memory: u64,
|
||||
@ -204,16 +186,9 @@ pub struct VmTemplate {
|
||||
pub disk_interface: DiskInterface,
|
||||
pub cost_plan_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 {
|
||||
/// Unique VM ID (Same in proxmox)
|
||||
pub id: u64,
|
||||
@ -237,35 +212,14 @@ pub struct Vm {
|
||||
pub mac_address: String,
|
||||
/// Is the VM deleted
|
||||
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 id: u64,
|
||||
pub vm_id: u64,
|
||||
pub ip_range_id: u64,
|
||||
pub ip: String,
|
||||
|
||||
#[sqlx(skip)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ip_range: Option<IpRange>,
|
||||
}
|
||||
|
||||
impl Display for VmIpAssignment {
|
||||
@ -274,11 +228,9 @@ impl Display for VmIpAssignment {
|
||||
}
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)]
|
||||
#[derive(FromRow, Clone, Debug, Default)]
|
||||
pub struct VmPayment {
|
||||
/// Payment hash
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
pub id: Vec<u8>,
|
||||
pub vm_id: u64,
|
||||
pub created: DateTime<Utc>,
|
||||
@ -288,11 +240,7 @@ pub struct VmPayment {
|
||||
pub is_paid: bool,
|
||||
/// Exchange rate
|
||||
pub rate: f32,
|
||||
|
||||
/// Number of seconds this payment will add to vm expiry
|
||||
#[serde(skip_serializing)]
|
||||
pub time_value: u64,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub settle_index: Option<u64>,
|
||||
}
|
||||
|
4
src/api/mod.rs
Normal file
4
src/api/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod routes;
|
||||
mod model;
|
||||
|
||||
pub use routes::routes;
|
277
src/api/model.rs
Normal file
277
src/api/model.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +1,33 @@
|
||||
use crate::api::model::{
|
||||
AccountPatchRequest, ApiUserSshKey, ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus,
|
||||
ApiVmTemplate, CreateSshKey, CreateVmRequest, VMPatchRequest,
|
||||
};
|
||||
use crate::nip98::Nip98Auth;
|
||||
use crate::provisioner::Provisioner;
|
||||
use crate::status::{VmState, VmStateCache};
|
||||
use crate::worker::WorkJob;
|
||||
use anyhow::{bail, Result};
|
||||
use lnvps_db::hydrate::Hydrate;
|
||||
use lnvps_db::{LNVpsDb, UserSshKey, Vm, VmOsImage, VmPayment, VmTemplate};
|
||||
use lnvps_db::{IpRange, LNVpsDb};
|
||||
use log::{debug, error};
|
||||
use nostr::util::hex;
|
||||
use nostr_sdk::async_utility::futures_util::future::join_all;
|
||||
use rocket::futures::{Sink, SinkExt, StreamExt};
|
||||
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 ssh_key::PublicKey;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Display;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use ws::Message;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
openapi_get_routes![
|
||||
v1_get_account,
|
||||
v1_patch_account,
|
||||
v1_list_vms,
|
||||
@ -32,14 +42,13 @@ pub fn routes() -> Vec<Route> {
|
||||
v1_start_vm,
|
||||
v1_stop_vm,
|
||||
v1_restart_vm,
|
||||
v1_terminal_proxy,
|
||||
v1_patch_vm
|
||||
]
|
||||
}
|
||||
|
||||
type ApiResult<T> = Result<Json<ApiData<T>>, ApiError>;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
struct ApiData<T: Serialize> {
|
||||
pub data: T,
|
||||
}
|
||||
@ -53,7 +62,7 @@ impl<T: Serialize> ApiData<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Responder)]
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Responder)]
|
||||
#[response(status = 500)]
|
||||
struct ApiError {
|
||||
pub error: String,
|
||||
@ -67,25 +76,14 @@ impl<T: ToString> From<T> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ApiVmStatus {
|
||||
#[serde(flatten)]
|
||||
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,
|
||||
impl OpenApiResponderInner for ApiError {
|
||||
fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
|
||||
Ok(Responses::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Update user account
|
||||
#[openapi(tag = "Account")]
|
||||
#[patch("/api/v1/account", format = "json", data = "<req>")]
|
||||
async fn v1_patch_account(
|
||||
auth: Nip98Auth,
|
||||
@ -104,6 +102,8 @@ async fn v1_patch_account(
|
||||
ApiData::ok(())
|
||||
}
|
||||
|
||||
/// Get user account detail
|
||||
#[openapi(tag = "Account")]
|
||||
#[get("/api/v1/account")]
|
||||
async fn v1_get_account(
|
||||
auth: Nip98Auth,
|
||||
@ -111,7 +111,7 @@ async fn v1_get_account(
|
||||
) -> ApiResult<AccountPatchRequest> {
|
||||
let pubkey = auth.event.pubkey.to_bytes();
|
||||
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 {
|
||||
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")]
|
||||
async fn v1_list_vms(
|
||||
auth: Nip98Auth,
|
||||
@ -130,23 +173,16 @@ async fn v1_list_vms(
|
||||
let uid = db.upsert_user(&pubkey).await?;
|
||||
let vms = db.list_user_vms(uid).await?;
|
||||
let mut ret = vec![];
|
||||
for mut vm in vms {
|
||||
vm.hydrate_up(db.inner()).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;
|
||||
ret.push(ApiVmStatus {
|
||||
vm,
|
||||
status: state.unwrap_or_default(),
|
||||
});
|
||||
for vm in vms {
|
||||
let vm_id = vm.id;
|
||||
ret.push(vm_to_status(db, vm, vm_state.get_state(vm_id).await).await?);
|
||||
}
|
||||
|
||||
ApiData::ok(ret)
|
||||
}
|
||||
|
||||
/// Get status of a VM
|
||||
#[openapi(tag = "VM")]
|
||||
#[get("/api/v1/vm/<id>")]
|
||||
async fn v1_get_vm(
|
||||
auth: Nip98Auth,
|
||||
@ -156,22 +192,15 @@ async fn v1_get_vm(
|
||||
) -> ApiResult<ApiVmStatus> {
|
||||
let pubkey = auth.event.pubkey.to_bytes();
|
||||
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 {
|
||||
return ApiData::err("VM doesnt belong to you");
|
||||
}
|
||||
vm.hydrate_up(db.inner()).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(),
|
||||
})
|
||||
ApiData::ok(vm_to_status(db, vm, vm_state.get_state(id).await).await?)
|
||||
}
|
||||
|
||||
/// Update a VM config
|
||||
#[openapi(tag = "VM")]
|
||||
#[patch("/api/v1/vm/<id>", data = "<data>", format = "json")]
|
||||
async fn v1_patch_vm(
|
||||
auth: Nip98Auth,
|
||||
@ -201,39 +230,88 @@ async fn v1_patch_vm(
|
||||
ApiData::ok(())
|
||||
}
|
||||
|
||||
/// List available VM OS images
|
||||
#[openapi(tag = "Image")]
|
||||
#[get("/api/v1/image")]
|
||||
async fn v1_list_vm_images(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<VmOsImage>> {
|
||||
let vms = db.list_os_image().await?;
|
||||
let vms: Vec<VmOsImage> = vms.into_iter().filter(|i| i.enabled).collect();
|
||||
ApiData::ok(vms)
|
||||
}
|
||||
|
||||
#[get("/api/v1/vm/templates")]
|
||||
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();
|
||||
async fn v1_list_vm_images(db: &State<Box<dyn LNVpsDb>>) -> ApiResult<Vec<ApiVmOsImage>> {
|
||||
let images = db.list_os_image().await?;
|
||||
let ret = images
|
||||
.into_iter()
|
||||
.filter(|i| i.enabled)
|
||||
.map(|i| i.into())
|
||||
.collect();
|
||||
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")]
|
||||
async fn v1_list_ssh_keys(
|
||||
auth: Nip98Auth,
|
||||
db: &State<Box<dyn LNVpsDb>>,
|
||||
) -> ApiResult<Vec<UserSshKey>> {
|
||||
) -> ApiResult<Vec<ApiUserSshKey>> {
|
||||
let uid = db.upsert_user(&auth.event.pubkey.to_bytes()).await?;
|
||||
let keys = db.list_user_ssh_key(uid).await?;
|
||||
ApiData::ok(keys)
|
||||
let ret = db
|
||||
.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")]
|
||||
async fn v1_add_ssh_key(
|
||||
auth: Nip98Auth,
|
||||
db: &State<Box<dyn LNVpsDb>>,
|
||||
req: Json<CreateSshKey>,
|
||||
) -> ApiResult<UserSshKey> {
|
||||
) -> ApiResult<ApiUserSshKey> {
|
||||
let uid = db.upsert_user(&auth.event.pubkey.to_bytes()).await?;
|
||||
|
||||
let pk: PublicKey = req.key_data.parse()?;
|
||||
@ -242,7 +320,7 @@ async fn v1_add_ssh_key(
|
||||
} else {
|
||||
pk.comment()
|
||||
};
|
||||
let mut new_key = UserSshKey {
|
||||
let mut new_key = lnvps_db::UserSshKey {
|
||||
name: key_name.to_string(),
|
||||
user_id: uid,
|
||||
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?;
|
||||
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")]
|
||||
async fn v1_create_vm_order(
|
||||
auth: Nip98Auth,
|
||||
db: &State<Box<dyn LNVpsDb>>,
|
||||
provisioner: &State<Box<dyn Provisioner>>,
|
||||
req: Json<CreateVmRequest>,
|
||||
) -> ApiResult<Vm> {
|
||||
) -> ApiResult<ApiVmStatus> {
|
||||
let pubkey = auth.event.pubkey.to_bytes();
|
||||
let uid = db.upsert_user(&pubkey).await?;
|
||||
|
||||
let req = req.0;
|
||||
let mut rsp = provisioner
|
||||
let rsp = provisioner
|
||||
.provision(uid, req.template_id, req.image_id, req.ssh_key_id)
|
||||
.await?;
|
||||
rsp.hydrate_up(db.inner()).await?;
|
||||
|
||||
ApiData::ok(rsp)
|
||||
ApiData::ok(vm_to_status(db, rsp, None).await?)
|
||||
}
|
||||
|
||||
/// Renew(Extend) a VM
|
||||
#[openapi(tag = "VM")]
|
||||
#[get("/api/v1/vm/<id>/renew")]
|
||||
async fn v1_renew_vm(
|
||||
auth: Nip98Auth,
|
||||
db: &State<Box<dyn LNVpsDb>>,
|
||||
provisioner: &State<Box<dyn Provisioner>>,
|
||||
id: u64,
|
||||
) -> ApiResult<VmPayment> {
|
||||
) -> ApiResult<ApiVmPayment> {
|
||||
let pubkey = auth.event.pubkey.to_bytes();
|
||||
let uid = db.upsert_user(&pubkey).await?;
|
||||
let vm = db.get_vm(id).await?;
|
||||
@ -288,9 +373,11 @@ async fn v1_renew_vm(
|
||||
}
|
||||
|
||||
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")]
|
||||
async fn v1_start_vm(
|
||||
auth: Nip98Auth,
|
||||
@ -311,6 +398,8 @@ async fn v1_start_vm(
|
||||
ApiData::ok(())
|
||||
}
|
||||
|
||||
/// Stop a VM
|
||||
#[openapi(tag = "VM")]
|
||||
#[patch("/api/v1/vm/<id>/stop")]
|
||||
async fn v1_stop_vm(
|
||||
auth: Nip98Auth,
|
||||
@ -331,6 +420,8 @@ async fn v1_stop_vm(
|
||||
ApiData::ok(())
|
||||
}
|
||||
|
||||
/// Restart a VM
|
||||
#[openapi(tag = "VM")]
|
||||
#[patch("/api/v1/vm/<id>/restart")]
|
||||
async fn v1_restart_vm(
|
||||
auth: Nip98Auth,
|
||||
@ -351,12 +442,14 @@ async fn v1_restart_vm(
|
||||
ApiData::ok(())
|
||||
}
|
||||
|
||||
/// Get payment status (for polling)
|
||||
#[openapi(tag = "Payment")]
|
||||
#[get("/api/v1/payment/<id>")]
|
||||
async fn v1_get_payment(
|
||||
auth: Nip98Auth,
|
||||
db: &State<Box<dyn LNVpsDb>>,
|
||||
id: &str,
|
||||
) -> ApiResult<VmPayment> {
|
||||
) -> ApiResult<ApiVmPayment> {
|
||||
let pubkey = auth.event.pubkey.to_bytes();
|
||||
let uid = db.upsert_user(&pubkey).await?;
|
||||
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");
|
||||
}
|
||||
|
||||
ApiData::ok(payment)
|
||||
ApiData::ok(payment.into())
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
@ -14,6 +14,7 @@ use lnvps_db::{LNVpsDb, LNVpsDbMysql};
|
||||
use log::error;
|
||||
use nostr::Keys;
|
||||
use nostr_sdk::Client;
|
||||
use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
@ -156,6 +157,13 @@ async fn main() -> Result<(), Error> {
|
||||
.manage(exchange)
|
||||
.manage(sender)
|
||||
.mount("/", api::routes())
|
||||
.mount(
|
||||
"/swagger",
|
||||
make_swagger_ui(&SwaggerUIConfig {
|
||||
url: "../openapi.json".to_owned(),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.launch()
|
||||
.await
|
||||
{
|
||||
|
27
src/nip98.rs
27
src/nip98.rs
@ -7,6 +7,9 @@ use rocket::http::uri::{Absolute, Uri};
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
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 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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -14,13 +14,13 @@ use fedimint_tonic_lnd::lnrpc::Invoice;
|
||||
use fedimint_tonic_lnd::tonic::async_trait;
|
||||
use fedimint_tonic_lnd::Client;
|
||||
use ipnetwork::IpNetwork;
|
||||
use lnvps_db::hydrate::Hydrate;
|
||||
use lnvps_db::{DiskType, IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
|
||||
use log::{debug, info, warn};
|
||||
use nostr::util::hex;
|
||||
use rand::random;
|
||||
use rand::seq::IteratorRandom;
|
||||
use reqwest::Url;
|
||||
use rocket::futures::future::join_all;
|
||||
use rocket::futures::{SinkExt, StreamExt};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::net::IpAddr;
|
||||
@ -81,10 +81,17 @@ impl LNVpsProvisioner {
|
||||
ips = self.allocate_ips(vm.id).await?;
|
||||
}
|
||||
|
||||
// load ranges
|
||||
for ip in &mut ips {
|
||||
ip.hydrate_up(&self.db).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| 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
|
||||
.iter()
|
||||
@ -92,11 +99,8 @@ impl LNVpsProvisioner {
|
||||
if let Ok(net) = ip.ip.parse::<IpNetwork>() {
|
||||
Some(match net {
|
||||
IpNetwork::V4(addr) => {
|
||||
format!(
|
||||
"ip={},gw={}",
|
||||
addr,
|
||||
ip.ip_range.as_ref().map(|r| &r.gateway).unwrap()
|
||||
)
|
||||
let range = ip_ranges.get(&ip.ip_range_id)?;
|
||||
format!("ip={},gw={}", addr, range.gateway)
|
||||
}
|
||||
IpNetwork::V6(addr) => format!("ip6={}", addr),
|
||||
})
|
||||
@ -122,11 +126,7 @@ impl LNVpsProvisioner {
|
||||
bail!("No host drive found!")
|
||||
};
|
||||
|
||||
let template = if let Some(t) = &vm.template {
|
||||
t
|
||||
} else {
|
||||
&self.db.get_vm_template(vm.template_id).await?
|
||||
};
|
||||
let template = self.db.get_vm_template(vm.template_id).await?;
|
||||
|
||||
Ok(VmConfig {
|
||||
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>> {
|
||||
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?;
|
||||
|
||||
if !ips.is_empty() {
|
||||
bail!("IP resources are already assigned");
|
||||
}
|
||||
|
||||
vm.hydrate_up(&self.db).await?;
|
||||
let ip_ranges = self.db.list_ip_range().await?;
|
||||
let ip_ranges: Vec<IpRange> = ip_ranges
|
||||
.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();
|
||||
|
||||
if ip_ranges.is_empty() {
|
||||
|
@ -73,6 +73,7 @@ pub struct SmtpConfig {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProvisionerConfig {
|
||||
Proxmox {
|
||||
/// Readonly mode, don't spawn any VM's
|
||||
@ -147,7 +148,7 @@ impl RouterConfig {
|
||||
username,
|
||||
password,
|
||||
arp_interface,
|
||||
} => MikrotikRouter::new(&url, &username, &password, &arp_interface),
|
||||
} => MikrotikRouter::new(url, username, password, arp_interface),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,11 @@ use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use rocket::serde::Deserialize;
|
||||
use schemars::JsonSchema;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone, Serialize, Default)]
|
||||
#[derive(Clone, Serialize, Deserialize, Default, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VmRunningState {
|
||||
Running,
|
||||
@ -14,7 +16,7 @@ pub enum VmRunningState {
|
||||
Deleting,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Default)]
|
||||
#[derive(Clone, Serialize, Deserialize, Default, JsonSchema)]
|
||||
pub struct VmState {
|
||||
pub timestamp: u64,
|
||||
pub state: VmRunningState,
|
||||
|
@ -48,11 +48,11 @@ pub struct WorkerSettings {
|
||||
pub smtp: Option<SmtpConfig>,
|
||||
}
|
||||
|
||||
impl Into<WorkerSettings> for &Settings {
|
||||
fn into(self) -> WorkerSettings {
|
||||
impl From<&Settings> for WorkerSettings {
|
||||
fn from(val: &Settings) -> Self {
|
||||
WorkerSettings {
|
||||
delete_after: self.delete_after,
|
||||
smtp: self.smtp.clone(),
|
||||
delete_after: val.delete_after,
|
||||
smtp: val.smtp.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -206,7 +206,7 @@ impl Worker {
|
||||
if let Err(e) = self.handle_vm_info(vm).await {
|
||||
error!("{}", e);
|
||||
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()),
|
||||
)?
|
||||
}
|
||||
@ -331,7 +331,7 @@ impl Worker {
|
||||
"Failed to check VM {}:\n{:?}\n{}",
|
||||
vm_id,
|
||||
&job,
|
||||
e.to_string()
|
||||
e
|
||||
),
|
||||
Some("Job Failed".to_string()),
|
||||
)?
|
||||
@ -351,7 +351,7 @@ impl Worker {
|
||||
format!(
|
||||
"Failed to send notification:\n{:?}\n{}",
|
||||
&job,
|
||||
e.to_string()
|
||||
e
|
||||
),
|
||||
Some("Job Failed".to_string()),
|
||||
)?
|
||||
@ -361,7 +361,7 @@ impl Worker {
|
||||
if let Err(e) = self.check_vms().await {
|
||||
error!("Failed to check VMs: {}", e);
|
||||
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()),
|
||||
)?
|
||||
}
|
||||
|
Reference in New Issue
Block a user