fix: impl configure_vm
This commit is contained in:
@ -7,7 +7,7 @@ edition = "2021"
|
|||||||
name = "api"
|
name = "api"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["mikrotik", "nostr-dm", "proxmox", "lnd", "bitvora", "cloudflare"]
|
default = ["mikrotik", "nostr-dm", "proxmox", "lnd", "cloudflare"]
|
||||||
mikrotik = ["dep:reqwest"]
|
mikrotik = ["dep:reqwest"]
|
||||||
nostr-dm = ["dep:nostr-sdk"]
|
nostr-dm = ["dep:nostr-sdk"]
|
||||||
proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"]
|
proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"]
|
||||||
|
@ -68,7 +68,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_user(&self, id: u64) -> Result<()> {
|
async fn delete_user(&self, _id: u64) -> Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
.map_err(Error::new)
|
.map_err(Error::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_user_ssh_key(&self, id: u64) -> Result<()> {
|
async fn delete_user_ssh_key(&self, _id: u64) -> Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use rocket::{routes, Route};
|
use rocket::Route;
|
||||||
|
|
||||||
mod model;
|
mod model;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
@ -2,18 +2,17 @@ use crate::api::model::{
|
|||||||
AccountPatchRequest, ApiUserSshKey, ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus,
|
AccountPatchRequest, ApiUserSshKey, ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus,
|
||||||
ApiVmTemplate, CreateSshKey, CreateVmRequest, VMPatchRequest,
|
ApiVmTemplate, CreateSshKey, CreateVmRequest, VMPatchRequest,
|
||||||
};
|
};
|
||||||
use crate::host::get_host_client;
|
use crate::host::{get_host_client, FullVmInfo};
|
||||||
use crate::nip98::Nip98Auth;
|
use crate::nip98::Nip98Auth;
|
||||||
use crate::provisioner::LNVpsProvisioner;
|
use crate::provisioner::LNVpsProvisioner;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::status::{VmState, VmStateCache};
|
use crate::status::{VmState, VmStateCache};
|
||||||
use crate::worker::WorkJob;
|
use crate::worker::WorkJob;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::Result;
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use lnvps_db::{IpRange, LNVpsDb};
|
use lnvps_db::{IpRange, LNVpsDb};
|
||||||
use log::{debug, error};
|
|
||||||
use nostr::util::hex;
|
use nostr::util::hex;
|
||||||
use rocket::futures::{Sink, SinkExt, StreamExt};
|
use rocket::futures::{SinkExt, StreamExt};
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::{get, patch, post, Responder, Route, State};
|
use rocket::{get, patch, post, Responder, Route, State};
|
||||||
use rocket_okapi::gen::OpenApiGenerator;
|
use rocket_okapi::gen::OpenApiGenerator;
|
||||||
@ -24,10 +23,8 @@ 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::collections::{HashMap, HashSet};
|
||||||
use std::fmt::Display;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
use ws::Message;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
openapi_get_routes![
|
openapi_get_routes![
|
||||||
@ -229,9 +226,10 @@ async fn v1_patch_vm(
|
|||||||
|
|
||||||
db.update_vm(&vm).await?;
|
db.update_vm(&vm).await?;
|
||||||
|
|
||||||
|
let info = FullVmInfo::load(vm.id, (*db).clone()).await?;
|
||||||
let host = db.get_host(vm.host_id).await?;
|
let host = db.get_host(vm.host_id).await?;
|
||||||
let client = get_host_client(&host, &settings.provisioner)?;
|
let client = get_host_client(&host, &settings.provisioner)?;
|
||||||
client.configure_vm(&vm).await?;
|
client.configure_vm(&info).await?;
|
||||||
|
|
||||||
ApiData::ok(())
|
ApiData::ok(())
|
||||||
}
|
}
|
||||||
@ -480,25 +478,3 @@ async fn v1_get_payment(
|
|||||||
|
|
||||||
ApiData::ok(payment.into())
|
ApiData::ok(payment.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/v1/console/<id>?<auth>")]
|
|
||||||
async fn v1_terminal_proxy(
|
|
||||||
auth: &str,
|
|
||||||
db: &State<Arc<dyn LNVpsDb>>,
|
|
||||||
_provisioner: &State<Arc<LNVpsProvisioner>>,
|
|
||||||
id: u64,
|
|
||||||
_ws: ws::WebSocket,
|
|
||||||
) -> Result<ws::Channel<'static>, &'static str> {
|
|
||||||
let auth = Nip98Auth::from_base64(auth).map_err(|_| "Missing or invalid auth param")?;
|
|
||||||
if auth.check(&format!("/api/v1/console/{id}"), "GET").is_err() {
|
|
||||||
return Err("Invalid auth event");
|
|
||||||
}
|
|
||||||
let pubkey = auth.event.pubkey.to_bytes();
|
|
||||||
let uid = db.upsert_user(&pubkey).await.map_err(|_| "Insert failed")?;
|
|
||||||
let vm = db.get_vm(id).await.map_err(|_| "VM not found")?;
|
|
||||||
if uid != vm.user_id {
|
|
||||||
return Err("VM does not belong to you");
|
|
||||||
}
|
|
||||||
|
|
||||||
Err("Not implemented")
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use anyhow::anyhow;
|
|
||||||
use lettre::message::header::Headers;
|
use lettre::message::header::Headers;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use reqwest::header::HeaderMap;
|
use reqwest::header::HeaderMap;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::dns::{BasicRecord, DnsServer};
|
use crate::dns::{BasicRecord, DnsServer, RecordType};
|
||||||
use crate::json_api::JsonApi;
|
use crate::json_api::JsonApi;
|
||||||
use lnvps_db::async_trait;
|
use lnvps_db::async_trait;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -39,7 +39,8 @@ impl DnsServer for Cloudflare {
|
|||||||
Ok(BasicRecord {
|
Ok(BasicRecord {
|
||||||
name: id_response.result.name,
|
name: id_response.result.name,
|
||||||
value: value.to_string(),
|
value: value.to_string(),
|
||||||
id: id_response.result.id.unwrap(),
|
id: id_response.result.id,
|
||||||
|
kind: RecordType::PTR,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +68,8 @@ impl DnsServer for Cloudflare {
|
|||||||
Ok(BasicRecord {
|
Ok(BasicRecord {
|
||||||
name: id_response.result.name,
|
name: id_response.result.name,
|
||||||
value: ip.to_string(),
|
value: ip.to_string(),
|
||||||
id: id_response.result.id.unwrap(),
|
id: id_response.result.id,
|
||||||
|
kind: RecordType::A,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use lnvps_db::async_trait;
|
use lnvps_db::async_trait;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
#[cfg(feature = "cloudflare")]
|
#[cfg(feature = "cloudflare")]
|
||||||
@ -22,9 +23,17 @@ pub trait DnsServer: Send + Sync {
|
|||||||
async fn delete_a_record(&self, name: &str) -> Result<()>;
|
async fn delete_a_record(&self, name: &str) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum RecordType {
|
||||||
|
A,
|
||||||
|
AAAA,
|
||||||
|
PTR,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BasicRecord {
|
pub struct BasicRecord {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
pub id: String,
|
pub id: Option<String>,
|
||||||
|
pub kind: RecordType,
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
use crate::host::{CreateVmRequest, VmHostClient};
|
use crate::host::{FullVmInfo, VmHostClient};
|
||||||
use crate::status::VmState;
|
use crate::status::VmState;
|
||||||
use lnvps_db::{async_trait, Vm, VmOsImage};
|
use lnvps_db::{async_trait, Vm, VmOsImage};
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ impl VmHostClient for LibVirt {
|
|||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_vm(&self, cfg: &CreateVmRequest) -> anyhow::Result<()> {
|
async fn create_vm(&self, cfg: &FullVmInfo) -> anyhow::Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
use crate::settings::ProvisionerConfig;
|
use crate::settings::ProvisionerConfig;
|
||||||
use crate::status::VmState;
|
use crate::status::VmState;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
use futures::future::join_all;
|
||||||
use lnvps_db::{
|
use lnvps_db::{
|
||||||
async_trait, IpRange, UserSshKey, Vm, VmHost, VmHostDisk, VmHostKind, VmIpAssignment,
|
async_trait, IpRange, LNVpsDb, UserSshKey, Vm, VmHost, VmHostDisk, VmHostKind, VmIpAssignment,
|
||||||
VmOsImage, VmTemplate,
|
VmOsImage, VmTemplate,
|
||||||
};
|
};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[cfg(feature = "libvirt")]
|
#[cfg(feature = "libvirt")]
|
||||||
@ -31,13 +33,13 @@ pub trait VmHostClient: Send + Sync {
|
|||||||
async fn reset_vm(&self, vm: &Vm) -> Result<()>;
|
async fn reset_vm(&self, vm: &Vm) -> Result<()>;
|
||||||
|
|
||||||
/// Spawn a VM
|
/// Spawn a VM
|
||||||
async fn create_vm(&self, cfg: &CreateVmRequest) -> Result<()>;
|
async fn create_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
||||||
|
|
||||||
/// Get the running status of a VM
|
/// Get the running status of a VM
|
||||||
async fn get_vm_state(&self, vm: &Vm) -> Result<VmState>;
|
async fn get_vm_state(&self, vm: &Vm) -> Result<VmState>;
|
||||||
|
|
||||||
/// Apply vm configuration (update)
|
/// Apply vm configuration (patch)
|
||||||
async fn configure_vm(&self, vm: &Vm) -> Result<()>;
|
async fn configure_vm(&self, cfg: &FullVmInfo) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_host_client(host: &VmHost, cfg: &ProvisionerConfig) -> Result<Arc<dyn VmHostClient>> {
|
pub fn get_host_client(host: &VmHost, cfg: &ProvisionerConfig) -> Result<Arc<dyn VmHostClient>> {
|
||||||
@ -69,9 +71,8 @@ pub fn get_host_client(host: &VmHost, cfg: &ProvisionerConfig) -> Result<Arc<dyn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic VM create request, host impl decides how VMs are created
|
/// All VM info necessary to provision a VM and its associated resources
|
||||||
/// based on app settings
|
pub struct FullVmInfo {
|
||||||
pub struct CreateVmRequest {
|
|
||||||
/// Instance to create
|
/// Instance to create
|
||||||
pub vm: Vm,
|
pub vm: Vm,
|
||||||
/// Disk where this VM will be saved on the host
|
/// Disk where this VM will be saved on the host
|
||||||
@ -87,3 +88,33 @@ pub struct CreateVmRequest {
|
|||||||
/// SSH key to access the VM
|
/// SSH key to access the VM
|
||||||
pub ssh_key: UserSshKey,
|
pub ssh_key: UserSshKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FullVmInfo {
|
||||||
|
pub async fn load(vm_id: u64, db: Arc<dyn LNVpsDb>) -> Result<Self> {
|
||||||
|
let vm = db.get_vm(vm_id).await?;
|
||||||
|
let template = db.get_vm_template(vm.template_id).await?;
|
||||||
|
let image = db.get_os_image(vm.image_id).await?;
|
||||||
|
let disk = db.get_host_disk(vm.disk_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 ranges: Vec<IpRange> = join_all(ip_ranges)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// create VM
|
||||||
|
Ok(FullVmInfo {
|
||||||
|
vm,
|
||||||
|
template,
|
||||||
|
image,
|
||||||
|
ips,
|
||||||
|
disk,
|
||||||
|
ranges,
|
||||||
|
ssh_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::host::{CreateVmRequest, VmHostClient};
|
use crate::host::{FullVmInfo, VmHostClient};
|
||||||
use crate::json_api::JsonApi;
|
use crate::json_api::JsonApi;
|
||||||
use crate::settings::{QemuConfig, SshConfig};
|
use crate::settings::{QemuConfig, SshConfig};
|
||||||
use crate::ssh_client::SshClient;
|
use crate::ssh_client::SshClient;
|
||||||
@ -8,23 +8,17 @@ use chrono::Utc;
|
|||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use lnvps_db::{async_trait, DiskType, IpRange, LNVpsDb, Vm, VmIpAssignment, VmOsImage};
|
use lnvps_db::{async_trait, DiskType, IpRange, LNVpsDb, Vm, VmIpAssignment, VmOsImage};
|
||||||
use log::{debug, info};
|
use log::{info, warn};
|
||||||
use rand::random;
|
use rand::random;
|
||||||
use reqwest::header::{HeaderMap, AUTHORIZATION};
|
use reqwest::header::{HeaderMap, AUTHORIZATION};
|
||||||
use reqwest::{ClientBuilder, Method, Url};
|
use reqwest::{ClientBuilder, Method, Url};
|
||||||
use serde::de::value::I32Deserializer;
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tokio_tungstenite::tungstenite::handshake::client::{generate_key, Request};
|
|
||||||
use tokio_tungstenite::{Connector, MaybeTlsStream, WebSocketStream};
|
|
||||||
|
|
||||||
pub struct ProxmoxClient {
|
pub struct ProxmoxClient {
|
||||||
api: JsonApi,
|
api: JsonApi,
|
||||||
@ -359,7 +353,7 @@ impl ProxmoxClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProxmoxClient {
|
impl ProxmoxClient {
|
||||||
fn make_config(&self, value: &CreateVmRequest) -> Result<VmConfig> {
|
fn make_config(&self, value: &FullVmInfo) -> Result<VmConfig> {
|
||||||
let mut ip_config = value
|
let mut ip_config = value
|
||||||
.ips
|
.ips
|
||||||
.iter()
|
.iter()
|
||||||
@ -474,7 +468,7 @@ impl VmHostClient for ProxmoxClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_vm(&self, req: &CreateVmRequest) -> Result<()> {
|
async fn create_vm(&self, req: &FullVmInfo) -> Result<()> {
|
||||||
let config = self.make_config(&req)?;
|
let config = self.make_config(&req)?;
|
||||||
let vm_id = req.vm.id.into();
|
let vm_id = req.vm.id.into();
|
||||||
let t_create = self
|
let t_create = self
|
||||||
@ -511,11 +505,14 @@ impl VmHostClient for ProxmoxClient {
|
|||||||
size: req.template.disk_size.to_string(),
|
size: req.template.disk_size.to_string(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
// TODO: rollback
|
||||||
self.wait_for_task(&j_resize).await?;
|
self.wait_for_task(&j_resize).await?;
|
||||||
|
|
||||||
// try start, otherwise ignore error (maybe its already running)
|
// try start, otherwise ignore error (maybe its already running)
|
||||||
if let Ok(j_start) = self.start_vm(&self.node, vm_id).await {
|
if let Ok(j_start) = self.start_vm(&self.node, vm_id).await {
|
||||||
self.wait_for_task(&j_start).await?;
|
if let Err(e) = self.wait_for_task(&j_start).await {
|
||||||
|
warn!("Failed to start vm: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -539,8 +536,23 @@ impl VmHostClient for ProxmoxClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn configure_vm(&self, vm: &Vm) -> Result<()> {
|
async fn configure_vm(&self, cfg: &FullVmInfo) -> Result<()> {
|
||||||
todo!()
|
let mut config = self.make_config(&cfg)?;
|
||||||
|
|
||||||
|
// dont re-create the disks
|
||||||
|
config.scsi_0 = None;
|
||||||
|
config.scsi_1 = None;
|
||||||
|
config.efi_disk_0 = None;
|
||||||
|
|
||||||
|
self.configure_vm(ConfigureVm {
|
||||||
|
node: self.node.clone(),
|
||||||
|
vm_id: cfg.vm.id.into(),
|
||||||
|
current: None,
|
||||||
|
snapshot: None,
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use std::path::Path;
|
|
||||||
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use fedimint_tonic_lnd::invoicesrpc::lookup_invoice_msg::InvoiceRef;
|
use fedimint_tonic_lnd::invoicesrpc::lookup_invoice_msg::InvoiceRef;
|
||||||
@ -9,6 +8,7 @@ use fedimint_tonic_lnd::{connect, Client};
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use lnvps_db::async_trait;
|
use lnvps_db::async_trait;
|
||||||
use nostr_sdk::async_utility::futures_util::Stream;
|
use nostr_sdk::async_utility::futures_util::Stream;
|
||||||
|
use std::path::Path;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
pub struct LndNode {
|
pub struct LndNode {
|
||||||
|
@ -53,7 +53,10 @@ pub async fn get_node(settings: &Settings) -> Result<Arc<dyn LightningNode>> {
|
|||||||
macaroon,
|
macaroon,
|
||||||
} => Ok(Arc::new(lnd::LndNode::new(url, cert, macaroon).await?)),
|
} => Ok(Arc::new(lnd::LndNode::new(url, cert, macaroon).await?)),
|
||||||
#[cfg(feature = "bitvora")]
|
#[cfg(feature = "bitvora")]
|
||||||
LightningConfig::Bitvora { token, webhook_secret } => Ok(Arc::new(bitvora::BitvoraNode::new(token, webhook_secret))),
|
LightningConfig::Bitvora {
|
||||||
|
token,
|
||||||
|
webhook_secret,
|
||||||
|
} => Ok(Arc::new(bitvora::BitvoraNode::new(token, webhook_secret))),
|
||||||
_ => anyhow::bail!("Unsupported lightning config!"),
|
_ => anyhow::bail!("Unsupported lightning config!"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
src/mocks.rs
31
src/mocks.rs
@ -1,5 +1,6 @@
|
|||||||
use crate::dns::{BasicRecord, DnsServer};
|
#![allow(unused)]
|
||||||
use crate::host::{CreateVmRequest, VmHostClient};
|
use crate::dns::{BasicRecord, DnsServer, RecordType};
|
||||||
|
use crate::host::{FullVmInfo, VmHostClient};
|
||||||
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
||||||
use crate::router::{ArpEntry, Router};
|
use crate::router::{ArpEntry, Router};
|
||||||
use crate::settings::NetworkPolicy;
|
use crate::settings::NetworkPolicy;
|
||||||
@ -515,23 +516,21 @@ impl Router for MockRouter {
|
|||||||
mac: &str,
|
mac: &str,
|
||||||
interface: &str,
|
interface: &str,
|
||||||
comment: Option<&str>,
|
comment: Option<&str>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<ArpEntry> {
|
||||||
let mut arp = self.arp.lock().await;
|
let mut arp = self.arp.lock().await;
|
||||||
if arp.iter().any(|(k, v)| v.address == ip.to_string()) {
|
if arp.iter().any(|(k, v)| v.address == ip.to_string()) {
|
||||||
bail!("Address is already in use");
|
bail!("Address is already in use");
|
||||||
}
|
}
|
||||||
let max_id = *arp.keys().max().unwrap_or(&0);
|
let max_id = *arp.keys().max().unwrap_or(&0);
|
||||||
arp.insert(
|
let e = ArpEntry {
|
||||||
max_id + 1,
|
id: (max_id + 1).to_string(),
|
||||||
ArpEntry {
|
|
||||||
id: Some((max_id + 1).to_string()),
|
|
||||||
address: ip.to_string(),
|
address: ip.to_string(),
|
||||||
mac_address: Some(mac.to_string()),
|
mac_address: mac.to_string(),
|
||||||
interface: interface.to_string(),
|
interface: Some(interface.to_string()),
|
||||||
comment: comment.map(|s| s.to_string()),
|
comment: comment.map(|s| s.to_string()),
|
||||||
},
|
};
|
||||||
);
|
arp.insert(max_id + 1, e.clone());
|
||||||
Ok(())
|
Ok(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_arp_entry(&self, id: &str) -> anyhow::Result<()> {
|
async fn remove_arp_entry(&self, id: &str) -> anyhow::Result<()> {
|
||||||
@ -636,7 +635,7 @@ impl VmHostClient for MockVmHost {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_vm(&self, cfg: &CreateVmRequest) -> anyhow::Result<()> {
|
async fn create_vm(&self, cfg: &FullVmInfo) -> anyhow::Result<()> {
|
||||||
let mut vms = self.vms.lock().await;
|
let mut vms = self.vms.lock().await;
|
||||||
let max_id = *vms.keys().max().unwrap_or(&0);
|
let max_id = *vms.keys().max().unwrap_or(&0);
|
||||||
vms.insert(
|
vms.insert(
|
||||||
@ -717,7 +716,8 @@ impl DnsServer for MockDnsServer {
|
|||||||
Ok(BasicRecord {
|
Ok(BasicRecord {
|
||||||
name: format!("{}.X.Y.Z.in-addr.arpa", key),
|
name: format!("{}.X.Y.Z.in-addr.arpa", key),
|
||||||
value: value.to_string(),
|
value: value.to_string(),
|
||||||
id,
|
id: Some(id),
|
||||||
|
kind: RecordType::PTR,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -746,7 +746,8 @@ impl DnsServer for MockDnsServer {
|
|||||||
Ok(BasicRecord {
|
Ok(BasicRecord {
|
||||||
name: fqdn,
|
name: fqdn,
|
||||||
value: ip.to_string(),
|
value: ip.to_string(),
|
||||||
id,
|
id: Some(id),
|
||||||
|
kind: RecordType::A,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::dns::DnsServer;
|
use crate::dns::DnsServer;
|
||||||
use crate::exchange::{ExchangeRateService, Ticker};
|
use crate::exchange::{ExchangeRateService, Ticker};
|
||||||
use crate::host::{get_host_client, CreateVmRequest, VmHostClient};
|
use crate::host::{get_host_client, FullVmInfo};
|
||||||
use crate::lightning::{AddInvoiceRequest, LightningNode};
|
use crate::lightning::{AddInvoiceRequest, LightningNode};
|
||||||
use crate::provisioner::{NetworkProvisioner, ProvisionerMethod};
|
use crate::provisioner::{NetworkProvisioner, ProvisionerMethod};
|
||||||
use crate::router::Router;
|
use crate::router::Router;
|
||||||
@ -8,19 +8,16 @@ use crate::settings::{NetworkAccessPolicy, NetworkPolicy, ProvisionerConfig, Set
|
|||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use chrono::{Days, Months, Utc};
|
use chrono::{Days, Months, Utc};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use lnvps_db::{DiskType, IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
|
use lnvps_db::{IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
|
||||||
use log::{debug, info, warn};
|
use log::{info, warn};
|
||||||
use nostr::util::hex;
|
use nostr::util::hex;
|
||||||
use rand::random;
|
use rand::random;
|
||||||
use rocket::futures::{SinkExt, StreamExt};
|
use std::collections::HashSet;
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::fmt::format;
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::net::TcpStream;
|
|
||||||
|
|
||||||
/// Main provisioner class for LNVPS
|
/// Main provisioner class for LNVPS
|
||||||
///
|
///
|
||||||
@ -62,14 +59,11 @@ impl LNVpsProvisioner {
|
|||||||
if let NetworkAccessPolicy::StaticArp { .. } = &self.network_policy.access {
|
if let NetworkAccessPolicy::StaticArp { .. } = &self.network_policy.access {
|
||||||
if let Some(r) = self.router.as_ref() {
|
if let Some(r) = self.router.as_ref() {
|
||||||
let ent = r.list_arp_entry().await?;
|
let ent = r.list_arp_entry().await?;
|
||||||
if let Some(ent) = ent.iter().find(|e| {
|
if let Some(ent) = ent
|
||||||
e.mac_address
|
.iter()
|
||||||
.as_ref()
|
.find(|e| e.mac_address.eq_ignore_ascii_case(&vm.mac_address))
|
||||||
.map(|m| m.eq_ignore_ascii_case(&vm.mac_address))
|
{
|
||||||
.unwrap_or(false)
|
r.remove_arp_entry(&ent.id).await?;
|
||||||
}) {
|
|
||||||
r.remove_arp_entry(ent.id.as_ref().unwrap().as_str())
|
|
||||||
.await?;
|
|
||||||
} else {
|
} else {
|
||||||
warn!("ARP entry not found, skipping")
|
warn!("ARP entry not found, skipping")
|
||||||
}
|
}
|
||||||
@ -101,14 +95,14 @@ impl LNVpsProvisioner {
|
|||||||
let sub_name = format!("vm-{}", vm.id);
|
let sub_name = format!("vm-{}", vm.id);
|
||||||
let fwd = dns.add_a_record(&sub_name, ip.clone()).await?;
|
let fwd = dns.add_a_record(&sub_name, ip.clone()).await?;
|
||||||
assignment.dns_forward = Some(fwd.name.clone());
|
assignment.dns_forward = Some(fwd.name.clone());
|
||||||
assignment.dns_forward_ref = Some(fwd.id);
|
assignment.dns_forward_ref = fwd.id;
|
||||||
|
|
||||||
match ip {
|
match ip {
|
||||||
IpAddr::V4(ip) => {
|
IpAddr::V4(ip) => {
|
||||||
let last_octet = ip.octets()[3].to_string();
|
let last_octet = ip.octets()[3].to_string();
|
||||||
let rev = dns.add_ptr_record(&last_octet, &fwd.name).await?;
|
let rev = dns.add_ptr_record(&last_octet, &fwd.name).await?;
|
||||||
assignment.dns_reverse = Some(fwd.name.clone());
|
assignment.dns_reverse = Some(fwd.name.clone());
|
||||||
assignment.dns_reverse_ref = Some(rev.id);
|
assignment.dns_reverse_ref = rev.id;
|
||||||
}
|
}
|
||||||
IpAddr::V6(_) => {
|
IpAddr::V6(_) => {
|
||||||
warn!("IPv6 forward DNS not supported yet")
|
warn!("IPv6 forward DNS not supported yet")
|
||||||
@ -290,40 +284,16 @@ impl LNVpsProvisioner {
|
|||||||
if self.read_only {
|
if self.read_only {
|
||||||
bail!("Cant spawn VM's in read-only mode")
|
bail!("Cant spawn VM's in read-only mode")
|
||||||
}
|
}
|
||||||
let vm = self.db.get_vm(vm_id).await?;
|
|
||||||
let template = self.db.get_vm_template(vm.template_id).await?;
|
|
||||||
let host = self.db.get_host(vm.host_id).await?;
|
|
||||||
let image = self.db.get_os_image(vm.image_id).await?;
|
|
||||||
let disk = self.db.get_host_disk(vm.disk_id).await?;
|
|
||||||
let ssh_key = self.db.get_user_ssh_key(vm.ssh_key_id).await?;
|
|
||||||
|
|
||||||
let client = get_host_client(&host, &self.provisioner_config)?;
|
|
||||||
|
|
||||||
// setup network by allocating some IP space
|
// setup network by allocating some IP space
|
||||||
let ips = self.allocate_ips(vm.id).await?;
|
self.allocate_ips(vm_id).await?;
|
||||||
|
|
||||||
let ip_range_ids: HashSet<u64> = ips.iter().map(|i| i.ip_range_id).collect();
|
// load full info
|
||||||
let ip_ranges: Vec<_> = ip_range_ids
|
let info = FullVmInfo::load(vm_id, self.db.clone()).await?;
|
||||||
.iter()
|
|
||||||
.map(|i| self.db.get_ip_range(*i))
|
|
||||||
.collect();
|
|
||||||
let ranges: Vec<IpRange> = join_all(ip_ranges)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// create VM
|
// load host client
|
||||||
let req = CreateVmRequest {
|
let host = self.db.get_host(info.vm.host_id).await?;
|
||||||
vm,
|
let client = get_host_client(&host, &self.provisioner_config)?;
|
||||||
template,
|
client.create_vm(&info).await?;
|
||||||
image,
|
|
||||||
ips,
|
|
||||||
disk,
|
|
||||||
ranges,
|
|
||||||
ssh_key,
|
|
||||||
};
|
|
||||||
client.create_vm(&req).await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -410,7 +380,6 @@ mod tests {
|
|||||||
let node = Arc::new(MockNode::default());
|
let node = Arc::new(MockNode::default());
|
||||||
let rates = Arc::new(DefaultRateCache::default());
|
let rates = Arc::new(DefaultRateCache::default());
|
||||||
let router = settings.get_router().expect("router").unwrap();
|
let router = settings.get_router().expect("router").unwrap();
|
||||||
let dns = settings.get_dns().expect("dns").unwrap();
|
|
||||||
let provisioner = LNVpsProvisioner::new(settings, db.clone(), node.clone(), rates.clone());
|
let provisioner = LNVpsProvisioner::new(settings, db.clone(), node.clone(), rates.clone());
|
||||||
|
|
||||||
let pubkey: [u8; 32] = random();
|
let pubkey: [u8; 32] = random();
|
||||||
@ -433,8 +402,8 @@ mod tests {
|
|||||||
let arp = router.list_arp_entry().await?;
|
let arp = router.list_arp_entry().await?;
|
||||||
assert_eq!(1, arp.len());
|
assert_eq!(1, arp.len());
|
||||||
let arp = arp.first().unwrap();
|
let arp = arp.first().unwrap();
|
||||||
assert_eq!(&vm.mac_address, arp.mac_address.as_ref().unwrap());
|
assert_eq!(&vm.mac_address, &arp.mac_address);
|
||||||
assert_eq!(ROUTER_BRIDGE, &arp.interface);
|
assert_eq!(ROUTER_BRIDGE, arp.interface.as_ref().unwrap());
|
||||||
println!("{:?}", arp);
|
println!("{:?}", arp);
|
||||||
|
|
||||||
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
||||||
|
@ -3,8 +3,10 @@ use crate::router::{ArpEntry, Router};
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use base64::engine::general_purpose::STANDARD;
|
use base64::engine::general_purpose::STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
use log::debug;
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use rocket::async_trait;
|
use rocket::async_trait;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
pub struct MikrotikRouter {
|
pub struct MikrotikRouter {
|
||||||
@ -26,8 +28,8 @@ impl MikrotikRouter {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Router for MikrotikRouter {
|
impl Router for MikrotikRouter {
|
||||||
async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>> {
|
async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>> {
|
||||||
let rsp: Vec<ArpEntry> = self.api.req(Method::GET, "/rest/ip/arp", ()).await?;
|
let rsp: Vec<MikrotikArpEntry> = self.api.req(Method::GET, "/rest/ip/arp", ()).await?;
|
||||||
Ok(rsp)
|
Ok(rsp.into_iter().map(|e| e.into()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_arp_entry(
|
async fn add_arp_entry(
|
||||||
@ -36,31 +38,57 @@ impl Router for MikrotikRouter {
|
|||||||
mac: &str,
|
mac: &str,
|
||||||
arp_interface: &str,
|
arp_interface: &str,
|
||||||
comment: Option<&str>,
|
comment: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<ArpEntry> {
|
||||||
let _rsp: ArpEntry = self
|
let rsp: MikrotikArpEntry = self
|
||||||
.api
|
.api
|
||||||
.req(
|
.req(
|
||||||
Method::PUT,
|
Method::PUT,
|
||||||
"/rest/ip/arp",
|
"/rest/ip/arp",
|
||||||
ArpEntry {
|
MikrotikArpEntry {
|
||||||
|
id: None,
|
||||||
address: ip.to_string(),
|
address: ip.to_string(),
|
||||||
mac_address: Some(mac.to_string()),
|
mac_address: Some(mac.to_string()),
|
||||||
interface: arp_interface.to_string(),
|
interface: arp_interface.to_string(),
|
||||||
comment: comment.map(|c| c.to_string()),
|
comment: comment.map(|c| c.to_string()),
|
||||||
..Default::default()
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
debug!("{:?}", rsp);
|
||||||
Ok(())
|
Ok(rsp.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_arp_entry(&self, id: &str) -> Result<()> {
|
async fn remove_arp_entry(&self, id: &str) -> Result<()> {
|
||||||
let _rsp: ArpEntry = self
|
let rsp: MikrotikArpEntry = self
|
||||||
.api
|
.api
|
||||||
.req(Method::DELETE, &format!("/rest/ip/arp/{id}"), ())
|
.req(Method::DELETE, &format!("/rest/ip/arp/{id}"), ())
|
||||||
.await?;
|
.await?;
|
||||||
|
debug!("{:?}", rsp);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MikrotikArpEntry {
|
||||||
|
#[serde(rename = ".id")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub address: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "mac-address")]
|
||||||
|
pub mac_address: Option<String>,
|
||||||
|
pub interface: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub comment: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<ArpEntry> for MikrotikArpEntry {
|
||||||
|
fn into(self) -> ArpEntry {
|
||||||
|
ArpEntry {
|
||||||
|
id: self.id.unwrap(),
|
||||||
|
address: self.address,
|
||||||
|
mac_address: self.mac_address.unwrap(),
|
||||||
|
interface: Some(self.interface),
|
||||||
|
comment: self.comment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use rocket::async_trait;
|
use rocket::async_trait;
|
||||||
use rocket::serde::{Deserialize, Serialize};
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
/// Router defines a network device used to access the hosts
|
/// Router defines a network device used to access the hosts
|
||||||
@ -19,21 +18,16 @@ pub trait Router: Send + Sync {
|
|||||||
mac: &str,
|
mac: &str,
|
||||||
interface: &str,
|
interface: &str,
|
||||||
comment: Option<&str>,
|
comment: Option<&str>,
|
||||||
) -> Result<()>;
|
) -> Result<ArpEntry>;
|
||||||
async fn remove_arp_entry(&self, id: &str) -> Result<()>;
|
async fn remove_arp_entry(&self, id: &str) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ArpEntry {
|
pub struct ArpEntry {
|
||||||
#[serde(rename = ".id")]
|
pub id: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub id: Option<String>,
|
|
||||||
pub address: String,
|
pub address: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
pub mac_address: String,
|
||||||
#[serde(rename = "mac-address")]
|
pub interface: Option<String>,
|
||||||
pub mac_address: Option<String>,
|
|
||||||
pub interface: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ impl Settings {
|
|||||||
pub fn get_router(&self) -> Result<Option<Arc<dyn Router>>> {
|
pub fn get_router(&self) -> Result<Option<Arc<dyn Router>>> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
{
|
{
|
||||||
if let Some(router) = &self.router {
|
if let Some(_router) = &self.router {
|
||||||
let router = crate::mocks::MockRouter::new(self.network_policy.clone());
|
let router = crate::mocks::MockRouter::new(self.network_policy.clone());
|
||||||
Ok(Some(Arc::new(router)))
|
Ok(Some(Arc::new(router)))
|
||||||
} else {
|
} else {
|
||||||
|
Reference in New Issue
Block a user