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"
[features]
default = ["mikrotik", "nostr-dm", "proxmox", "lnd", "bitvora", "cloudflare"]
default = ["mikrotik", "nostr-dm", "proxmox", "lnd", "cloudflare"]
mikrotik = ["dep:reqwest"]
nostr-dm = ["dep:nostr-sdk"]
proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"]

View File

@ -68,7 +68,7 @@ impl LNVpsDb for LNVpsDbMysql {
Ok(())
}
async fn delete_user(&self, id: u64) -> Result<()> {
async fn delete_user(&self, _id: u64) -> Result<()> {
todo!()
}
@ -93,7 +93,7 @@ impl LNVpsDb for LNVpsDbMysql {
.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!()
}

View File

@ -1,4 +1,4 @@
use rocket::{routes, Route};
use rocket::Route;
mod model;
mod routes;
@ -10,4 +10,4 @@ pub fn routes() -> Vec<Route> {
r
}
pub use webhook::WEBHOOK_BRIDGE;
pub use webhook::WEBHOOK_BRIDGE;

View File

@ -2,18 +2,17 @@ use crate::api::model::{
AccountPatchRequest, ApiUserSshKey, ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus,
ApiVmTemplate, CreateSshKey, CreateVmRequest, VMPatchRequest,
};
use crate::host::get_host_client;
use crate::host::{get_host_client, FullVmInfo};
use crate::nip98::Nip98Auth;
use crate::provisioner::LNVpsProvisioner;
use crate::settings::Settings;
use crate::status::{VmState, VmStateCache};
use crate::worker::WorkJob;
use anyhow::{bail, Result};
use anyhow::Result;
use futures::future::join_all;
use lnvps_db::{IpRange, LNVpsDb};
use log::{debug, error};
use nostr::util::hex;
use rocket::futures::{Sink, SinkExt, StreamExt};
use rocket::futures::{SinkExt, StreamExt};
use rocket::serde::json::Json;
use rocket::{get, patch, post, Responder, Route, State};
use rocket_okapi::gen::OpenApiGenerator;
@ -24,10 +23,8 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ssh_key::PublicKey;
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::sync::Arc;
use tokio::sync::mpsc::UnboundedSender;
use ws::Message;
pub fn routes() -> Vec<Route> {
openapi_get_routes![
@ -229,9 +226,10 @@ async fn v1_patch_vm(
db.update_vm(&vm).await?;
let info = FullVmInfo::load(vm.id, (*db).clone()).await?;
let host = db.get_host(vm.host_id).await?;
let client = get_host_client(&host, &settings.provisioner)?;
client.configure_vm(&vm).await?;
client.configure_vm(&info).await?;
ApiData::ok(())
}
@ -480,25 +478,3 @@ async fn v1_get_payment(
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 log::warn;
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 lnvps_db::async_trait;
use serde::{Deserialize, Serialize};
@ -39,7 +39,8 @@ impl DnsServer for Cloudflare {
Ok(BasicRecord {
name: id_response.result.name,
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 {
name: id_response.result.name,
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 lnvps_db::async_trait;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
#[cfg(feature = "cloudflare")]
@ -22,9 +23,17 @@ pub trait DnsServer: Send + Sync {
async fn delete_a_record(&self, name: &str) -> Result<()>;
}
#[derive(Clone, Debug)]
pub enum RecordType {
A,
AAAA,
PTR,
}
#[derive(Debug, Clone)]
pub struct BasicRecord {
pub name: 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 lnvps_db::{async_trait, Vm, VmOsImage};
@ -26,7 +26,7 @@ impl VmHostClient for LibVirt {
todo!()
}
async fn create_vm(&self, cfg: &CreateVmRequest) -> anyhow::Result<()> {
async fn create_vm(&self, cfg: &FullVmInfo) -> anyhow::Result<()> {
todo!()
}

View File

@ -1,10 +1,12 @@
use crate::settings::ProvisionerConfig;
use crate::status::VmState;
use anyhow::{bail, Result};
use futures::future::join_all;
use lnvps_db::{
async_trait, IpRange, UserSshKey, Vm, VmHost, VmHostDisk, VmHostKind, VmIpAssignment,
async_trait, IpRange, LNVpsDb, UserSshKey, Vm, VmHost, VmHostDisk, VmHostKind, VmIpAssignment,
VmOsImage, VmTemplate,
};
use std::collections::HashSet;
use std::sync::Arc;
#[cfg(feature = "libvirt")]
@ -31,13 +33,13 @@ pub trait VmHostClient: Send + Sync {
async fn reset_vm(&self, vm: &Vm) -> Result<()>;
/// 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
async fn get_vm_state(&self, vm: &Vm) -> Result<VmState>;
/// Apply vm configuration (update)
async fn configure_vm(&self, vm: &Vm) -> Result<()>;
/// Apply vm configuration (patch)
async fn configure_vm(&self, cfg: &FullVmInfo) -> Result<()>;
}
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
/// based on app settings
pub struct CreateVmRequest {
/// All VM info necessary to provision a VM and its associated resources
pub struct FullVmInfo {
/// Instance to create
pub vm: Vm,
/// Disk where this VM will be saved on the host
@ -87,3 +88,33 @@ pub struct CreateVmRequest {
/// SSH key to access the VM
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::settings::{QemuConfig, SshConfig};
use crate::ssh_client::SshClient;
@ -8,23 +8,17 @@ use chrono::Utc;
use futures::future::join_all;
use ipnetwork::IpNetwork;
use lnvps_db::{async_trait, DiskType, IpRange, LNVpsDb, Vm, VmIpAssignment, VmOsImage};
use log::{debug, info};
use log::{info, warn};
use rand::random;
use reqwest::header::{HeaderMap, AUTHORIZATION};
use reqwest::{ClientBuilder, Method, Url};
use serde::de::value::I32Deserializer;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::fmt::{Debug, Display, Formatter};
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::time::sleep;
use tokio_tungstenite::tungstenite::handshake::client::{generate_key, Request};
use tokio_tungstenite::{Connector, MaybeTlsStream, WebSocketStream};
pub struct ProxmoxClient {
api: JsonApi,
@ -359,7 +353,7 @@ 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
.ips
.iter()
@ -474,7 +468,7 @@ impl VmHostClient for ProxmoxClient {
Ok(())
}
async fn create_vm(&self, req: &CreateVmRequest) -> Result<()> {
async fn create_vm(&self, req: &FullVmInfo) -> Result<()> {
let config = self.make_config(&req)?;
let vm_id = req.vm.id.into();
let t_create = self
@ -511,11 +505,14 @@ impl VmHostClient for ProxmoxClient {
size: req.template.disk_size.to_string(),
})
.await?;
// TODO: rollback
self.wait_for_task(&j_resize).await?;
// try start, otherwise ignore error (maybe its already running)
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(())
@ -539,8 +536,23 @@ impl VmHostClient for ProxmoxClient {
})
}
async fn configure_vm(&self, vm: &Vm) -> Result<()> {
todo!()
async fn configure_vm(&self, cfg: &FullVmInfo) -> Result<()> {
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 anyhow::Result;
use fedimint_tonic_lnd::invoicesrpc::lookup_invoice_msg::InvoiceRef;
@ -9,6 +8,7 @@ use fedimint_tonic_lnd::{connect, Client};
use futures::StreamExt;
use lnvps_db::async_trait;
use nostr_sdk::async_utility::futures_util::Stream;
use std::path::Path;
use std::pin::Pin;
pub struct LndNode {

View File

@ -53,7 +53,10 @@ pub async fn get_node(settings: &Settings) -> Result<Arc<dyn LightningNode>> {
macaroon,
} => Ok(Arc::new(lnd::LndNode::new(url, cert, macaroon).await?)),
#[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!"),
}
}

View File

@ -1,5 +1,6 @@
use crate::dns::{BasicRecord, DnsServer};
use crate::host::{CreateVmRequest, VmHostClient};
#![allow(unused)]
use crate::dns::{BasicRecord, DnsServer, RecordType};
use crate::host::{FullVmInfo, VmHostClient};
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
use crate::router::{ArpEntry, Router};
use crate::settings::NetworkPolicy;
@ -515,23 +516,21 @@ impl Router for MockRouter {
mac: &str,
interface: &str,
comment: Option<&str>,
) -> anyhow::Result<()> {
) -> anyhow::Result<ArpEntry> {
let mut arp = self.arp.lock().await;
if arp.iter().any(|(k, v)| v.address == ip.to_string()) {
bail!("Address is already in use");
}
let max_id = *arp.keys().max().unwrap_or(&0);
arp.insert(
max_id + 1,
ArpEntry {
id: Some((max_id + 1).to_string()),
address: ip.to_string(),
mac_address: Some(mac.to_string()),
interface: interface.to_string(),
comment: comment.map(|s| s.to_string()),
},
);
Ok(())
let e = ArpEntry {
id: (max_id + 1).to_string(),
address: ip.to_string(),
mac_address: mac.to_string(),
interface: Some(interface.to_string()),
comment: comment.map(|s| s.to_string()),
};
arp.insert(max_id + 1, e.clone());
Ok(e)
}
async fn remove_arp_entry(&self, id: &str) -> anyhow::Result<()> {
@ -636,7 +635,7 @@ impl VmHostClient for MockVmHost {
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 max_id = *vms.keys().max().unwrap_or(&0);
vms.insert(
@ -717,7 +716,8 @@ impl DnsServer for MockDnsServer {
Ok(BasicRecord {
name: format!("{}.X.Y.Z.in-addr.arpa", key),
value: value.to_string(),
id,
id: Some(id),
kind: RecordType::PTR,
})
}
@ -746,7 +746,8 @@ impl DnsServer for MockDnsServer {
Ok(BasicRecord {
name: fqdn,
value: ip.to_string(),
id,
id: Some(id),
kind: RecordType::A,
})
}

View File

@ -1,6 +1,6 @@
use crate::dns::DnsServer;
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::provisioner::{NetworkProvisioner, ProvisionerMethod};
use crate::router::Router;
@ -8,19 +8,16 @@ use crate::settings::{NetworkAccessPolicy, NetworkPolicy, ProvisionerConfig, Set
use anyhow::{bail, Result};
use chrono::{Days, Months, Utc};
use futures::future::join_all;
use lnvps_db::{DiskType, IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
use log::{debug, info, warn};
use lnvps_db::{IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
use log::{info, warn};
use nostr::util::hex;
use rand::random;
use rocket::futures::{SinkExt, StreamExt};
use std::collections::{HashMap, HashSet};
use std::fmt::format;
use std::collections::HashSet;
use std::net::IpAddr;
use std::ops::Add;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpStream;
/// Main provisioner class for LNVPS
///
@ -62,14 +59,11 @@ impl LNVpsProvisioner {
if let NetworkAccessPolicy::StaticArp { .. } = &self.network_policy.access {
if let Some(r) = self.router.as_ref() {
let ent = r.list_arp_entry().await?;
if let Some(ent) = ent.iter().find(|e| {
e.mac_address
.as_ref()
.map(|m| m.eq_ignore_ascii_case(&vm.mac_address))
.unwrap_or(false)
}) {
r.remove_arp_entry(ent.id.as_ref().unwrap().as_str())
.await?;
if let Some(ent) = ent
.iter()
.find(|e| e.mac_address.eq_ignore_ascii_case(&vm.mac_address))
{
r.remove_arp_entry(&ent.id).await?;
} else {
warn!("ARP entry not found, skipping")
}
@ -101,14 +95,14 @@ impl LNVpsProvisioner {
let sub_name = format!("vm-{}", vm.id);
let fwd = dns.add_a_record(&sub_name, ip.clone()).await?;
assignment.dns_forward = Some(fwd.name.clone());
assignment.dns_forward_ref = Some(fwd.id);
assignment.dns_forward_ref = fwd.id;
match ip {
IpAddr::V4(ip) => {
let last_octet = ip.octets()[3].to_string();
let rev = dns.add_ptr_record(&last_octet, &fwd.name).await?;
assignment.dns_reverse = Some(fwd.name.clone());
assignment.dns_reverse_ref = Some(rev.id);
assignment.dns_reverse_ref = rev.id;
}
IpAddr::V6(_) => {
warn!("IPv6 forward DNS not supported yet")
@ -290,40 +284,16 @@ impl LNVpsProvisioner {
if self.read_only {
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
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();
let ip_ranges: Vec<_> = ip_range_ids
.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();
// load full info
let info = FullVmInfo::load(vm_id, self.db.clone()).await?;
// create VM
let req = CreateVmRequest {
vm,
template,
image,
ips,
disk,
ranges,
ssh_key,
};
client.create_vm(&req).await?;
// load host client
let host = self.db.get_host(info.vm.host_id).await?;
let client = get_host_client(&host, &self.provisioner_config)?;
client.create_vm(&info).await?;
Ok(())
}
@ -410,7 +380,6 @@ mod tests {
let node = Arc::new(MockNode::default());
let rates = Arc::new(DefaultRateCache::default());
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 pubkey: [u8; 32] = random();
@ -433,8 +402,8 @@ mod tests {
let arp = router.list_arp_entry().await?;
assert_eq!(1, arp.len());
let arp = arp.first().unwrap();
assert_eq!(&vm.mac_address, arp.mac_address.as_ref().unwrap());
assert_eq!(ROUTER_BRIDGE, &arp.interface);
assert_eq!(&vm.mac_address, &arp.mac_address);
assert_eq!(ROUTER_BRIDGE, arp.interface.as_ref().unwrap());
println!("{:?}", arp);
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 base64::engine::general_purpose::STANDARD;
use base64::Engine;
use log::debug;
use reqwest::Method;
use rocket::async_trait;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
pub struct MikrotikRouter {
@ -26,8 +28,8 @@ impl MikrotikRouter {
#[async_trait]
impl Router for MikrotikRouter {
async fn list_arp_entry(&self) -> Result<Vec<ArpEntry>> {
let rsp: Vec<ArpEntry> = self.api.req(Method::GET, "/rest/ip/arp", ()).await?;
Ok(rsp)
let rsp: Vec<MikrotikArpEntry> = self.api.req(Method::GET, "/rest/ip/arp", ()).await?;
Ok(rsp.into_iter().map(|e| e.into()).collect())
}
async fn add_arp_entry(
@ -36,31 +38,57 @@ impl Router for MikrotikRouter {
mac: &str,
arp_interface: &str,
comment: Option<&str>,
) -> Result<()> {
let _rsp: ArpEntry = self
) -> Result<ArpEntry> {
let rsp: MikrotikArpEntry = self
.api
.req(
Method::PUT,
"/rest/ip/arp",
ArpEntry {
MikrotikArpEntry {
id: None,
address: ip.to_string(),
mac_address: Some(mac.to_string()),
interface: arp_interface.to_string(),
comment: comment.map(|c| c.to_string()),
..Default::default()
},
)
.await?;
Ok(())
debug!("{:?}", rsp);
Ok(rsp.into())
}
async fn remove_arp_entry(&self, id: &str) -> Result<()> {
let _rsp: ArpEntry = self
let rsp: MikrotikArpEntry = self
.api
.req(Method::DELETE, &format!("/rest/ip/arp/{id}"), ())
.await?;
debug!("{:?}", rsp);
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 rocket::async_trait;
use rocket::serde::{Deserialize, Serialize};
use std::net::IpAddr;
/// Router defines a network device used to access the hosts
@ -19,21 +18,16 @@ pub trait Router: Send + Sync {
mac: &str,
interface: &str,
comment: Option<&str>,
) -> Result<()>;
) -> Result<ArpEntry>;
async fn remove_arp_entry(&self, id: &str) -> Result<()>;
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone)]
pub struct ArpEntry {
#[serde(rename = ".id")]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub id: 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 mac_address: String,
pub interface: Option<String>,
pub comment: Option<String>,
}

View File

@ -181,7 +181,7 @@ impl Settings {
pub fn get_router(&self) -> Result<Option<Arc<dyn Router>>> {
#[cfg(test)]
{
if let Some(router) = &self.router {
if let Some(_router) = &self.router {
let router = crate::mocks::MockRouter::new(self.network_policy.clone());
Ok(Some(Arc::new(router)))
} else {