fix: impl configure_vm

This commit is contained in:
2025-03-04 11:03:15 +00:00
parent 4aa96020a6
commit 8ec143bd6b
17 changed files with 179 additions and 155 deletions

View File

@ -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"]

View File

@ -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!()
} }

View File

@ -1,4 +1,4 @@
use rocket::{routes, Route}; use rocket::Route;
mod model; mod model;
mod routes; mod routes;

View File

@ -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")
}

View File

@ -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;

View File

@ -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,
}) })
} }

View File

@ -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,
} }

View File

@ -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!()
} }

View File

@ -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,
})
}
}

View File

@ -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(())
} }
} }

View File

@ -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 {

View File

@ -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!"),
} }
} }

View File

@ -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,
}) })
} }

View File

@ -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?;

View File

@ -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,
}
}
}

View File

@ -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>,
} }

View File

@ -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 {