feat: DNS A/PTR with Cloudflare
This commit is contained in:
@ -7,13 +7,14 @@ edition = "2021"
|
|||||||
name = "api"
|
name = "api"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["mikrotik", "nostr-dm", "proxmox", "lnd", "bitvora"]
|
default = ["mikrotik", "nostr-dm", "proxmox", "lnd", "bitvora", "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"]
|
||||||
libvirt = ["dep:virt"]
|
libvirt = ["dep:virt"]
|
||||||
lnd = ["dep:fedimint-tonic-lnd"]
|
lnd = ["dep:fedimint-tonic-lnd"]
|
||||||
bitvora = ["dep:reqwest", "dep:tokio-stream"]
|
bitvora = ["dep:reqwest", "dep:tokio-stream"]
|
||||||
|
cloudflare = ["dep:reqwest"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lnvps_db = { path = "lnvps_db" }
|
lnvps_db = { path = "lnvps_db" }
|
||||||
@ -25,7 +26,7 @@ pretty_env_logger = "0.5.0"
|
|||||||
serde = { version = "1.0.213", features = ["derive"] }
|
serde = { version = "1.0.213", features = ["derive"] }
|
||||||
serde_json = "1.0.132"
|
serde_json = "1.0.132"
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
rocket_okapi = { version = "0.9.0", features = ["swagger", "rapidoc"] }
|
rocket_okapi = { version = "0.9.0", features = ["swagger"] }
|
||||||
schemars = { version = "0.8.22", features = ["chrono"] }
|
schemars = { version = "0.8.22", features = ["chrono"] }
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
base64 = { version = "0.22.1", features = ["alloc"] }
|
base64 = { version = "0.22.1", features = ["alloc"] }
|
||||||
|
15
README.md
15
README.md
@ -112,4 +112,19 @@ network-policy:
|
|||||||
static-arp:
|
static-arp:
|
||||||
# Interface where the static ARP entry is added
|
# Interface where the static ARP entry is added
|
||||||
interface: "bridge1"
|
interface: "bridge1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### DNS (PTR/A/AAAA)
|
||||||
|
|
||||||
|
To create PTR records automatically use the following config:
|
||||||
|
```yaml
|
||||||
|
dns:
|
||||||
|
cloudflare:
|
||||||
|
# The zone containing the reverse domain (eg. X.Y.Z.in-addr.arpa)
|
||||||
|
reverse-zone-id: "my-reverse-zone-id"
|
||||||
|
# The zone where forward (A/AAAA) entries are added (eg. lnvps.cloud zone)
|
||||||
|
# We create forward entries with the format vm-<vmid>.lnvps.cloud
|
||||||
|
forward-zone-id: "my-forward-zone-id"
|
||||||
|
# API token to add/remove DNS records to this zone
|
||||||
|
token: "my-api-token"
|
||||||
```
|
```
|
7
lnvps_db/migrations/20250303152800_ip_refs.sql
Normal file
7
lnvps_db/migrations/20250303152800_ip_refs.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
alter table vm_ip_assignment
|
||||||
|
add column arp_ref varchar(50),
|
||||||
|
add column dns_reverse varchar(255),
|
||||||
|
add column dns_reverse_ref varchar(50),
|
||||||
|
add column dns_forward varchar(255),
|
||||||
|
add column dns_forward_ref varchar(50);
|
@ -104,6 +104,9 @@ pub trait LNVpsDb: Sync + Send {
|
|||||||
/// List VM ip assignments
|
/// List VM ip assignments
|
||||||
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64>;
|
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64>;
|
||||||
|
|
||||||
|
/// Update VM ip assignments
|
||||||
|
async fn update_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<()>;
|
||||||
|
|
||||||
/// List VM ip assignments
|
/// List VM ip assignments
|
||||||
async fn list_vm_ip_assignments(&self, vm_id: u64) -> Result<Vec<VmIpAssignment>>;
|
async fn list_vm_ip_assignments(&self, vm_id: u64) -> Result<Vec<VmIpAssignment>>;
|
||||||
|
|
||||||
|
@ -226,11 +226,26 @@ pub struct Vm {
|
|||||||
|
|
||||||
#[derive(FromRow, Clone, Debug, Default)]
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
pub struct VmIpAssignment {
|
pub struct VmIpAssignment {
|
||||||
|
/// Unique id of this assignment
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
/// VM id this IP is assigned to
|
||||||
pub vm_id: u64,
|
pub vm_id: u64,
|
||||||
|
/// IP range id
|
||||||
pub ip_range_id: u64,
|
pub ip_range_id: u64,
|
||||||
|
/// The IP address (v4/v6)
|
||||||
pub ip: String,
|
pub ip: String,
|
||||||
|
/// If this record was freed
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
|
/// External ID pointing to a static arp entry on the router
|
||||||
|
pub arp_ref: Option<String>,
|
||||||
|
/// Forward DNS FQDN
|
||||||
|
pub dns_forward: Option<String>,
|
||||||
|
/// External ID pointing to the forward DNS entry for this IP
|
||||||
|
pub dns_forward_ref: Option<String>,
|
||||||
|
/// Reverse DNS FQDN
|
||||||
|
pub dns_reverse: Option<String>,
|
||||||
|
/// External ID pointing to the reverse DNS entry for this IP
|
||||||
|
pub dns_reverse_ref: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for VmIpAssignment {
|
impl Display for VmIpAssignment {
|
||||||
|
@ -290,17 +290,38 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
|
|
||||||
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64> {
|
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64> {
|
||||||
Ok(sqlx::query(
|
Ok(sqlx::query(
|
||||||
"insert into vm_ip_assignment(vm_id,ip_range_id,ip) values(?, ?, ?) returning id",
|
"insert into vm_ip_assignment(vm_id,ip_range_id,ip,arp_ref,dns_forward,dns_forward_ref,dns_reverse,dns_reverse_ref) values(?,?,?,?,?,?,?,?) returning id",
|
||||||
)
|
)
|
||||||
.bind(ip_assignment.vm_id)
|
.bind(ip_assignment.vm_id)
|
||||||
.bind(ip_assignment.ip_range_id)
|
.bind(ip_assignment.ip_range_id)
|
||||||
.bind(&ip_assignment.ip)
|
.bind(&ip_assignment.ip)
|
||||||
|
.bind(&ip_assignment.arp_ref)
|
||||||
|
.bind(&ip_assignment.dns_forward)
|
||||||
|
.bind(&ip_assignment.dns_forward_ref)
|
||||||
|
.bind(&ip_assignment.dns_reverse)
|
||||||
|
.bind(&ip_assignment.dns_reverse_ref)
|
||||||
.fetch_one(&self.db)
|
.fetch_one(&self.db)
|
||||||
.await
|
.await
|
||||||
.map_err(Error::new)?
|
.map_err(Error::new)?
|
||||||
.try_get(0)?)
|
.try_get(0)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<()> {
|
||||||
|
sqlx::query(
|
||||||
|
"update vm_ip_assignment set arp_ref = ?, dns_forward = ?, dns_forward_ref = ?, dns_reverse = ?, dns_reverse_ref = ? where id = ?",
|
||||||
|
)
|
||||||
|
.bind(&ip_assignment.arp_ref)
|
||||||
|
.bind(&ip_assignment.dns_forward)
|
||||||
|
.bind(&ip_assignment.dns_forward_ref)
|
||||||
|
.bind(&ip_assignment.dns_reverse)
|
||||||
|
.bind(&ip_assignment.dns_reverse_ref)
|
||||||
|
.bind(&ip_assignment.id)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_vm_ip_assignments(&self, vm_id: u64) -> Result<Vec<VmIpAssignment>> {
|
async fn list_vm_ip_assignments(&self, vm_id: u64) -> Result<Vec<VmIpAssignment>> {
|
||||||
sqlx::query_as("select * from vm_ip_assignment where vm_id = ? and deleted = 0")
|
sqlx::query_as("select * from vm_ip_assignment where vm_id = ? and deleted = 0")
|
||||||
.bind(vm_id)
|
.bind(vm_id)
|
||||||
|
@ -51,6 +51,8 @@ pub struct ApiVmIpAssignment {
|
|||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub ip: String,
|
pub ip: String,
|
||||||
pub gateway: String,
|
pub gateway: String,
|
||||||
|
pub forward_dns: Option<String>,
|
||||||
|
pub reverse_dns: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiVmIpAssignment {
|
impl ApiVmIpAssignment {
|
||||||
@ -64,6 +66,8 @@ impl ApiVmIpAssignment {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
gateway: range.gateway.to_string(),
|
gateway: range.gateway.to_string(),
|
||||||
|
forward_dns: ip.dns_forward.clone(),
|
||||||
|
reverse_dns: ip.dns_reverse.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,16 +74,6 @@ async fn main() -> Result<(), Error> {
|
|||||||
nostr_client.clone(),
|
nostr_client.clone(),
|
||||||
);
|
);
|
||||||
let sender = worker.sender();
|
let sender = worker.sender();
|
||||||
|
|
||||||
// send a startup notification
|
|
||||||
if let Some(admin) = settings.smtp.as_ref().and_then(|s| s.admin) {
|
|
||||||
sender.send(WorkJob::SendNotification {
|
|
||||||
title: Some("Startup".to_string()),
|
|
||||||
message: "System is starting!".to_string(),
|
|
||||||
user_id: admin,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
if let Err(e) = worker.handle().await {
|
if let Err(e) = worker.handle().await {
|
||||||
|
92
src/dns/cloudflare.rs
Normal file
92
src/dns/cloudflare.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use crate::dns::{BasicRecord, DnsServer};
|
||||||
|
use crate::json_api::JsonApi;
|
||||||
|
use lnvps_db::async_trait;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
pub struct Cloudflare {
|
||||||
|
api: JsonApi,
|
||||||
|
reverse_zone_id: String,
|
||||||
|
forward_zone_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cloudflare {
|
||||||
|
pub fn new(token: &str, reverse_zone_id: &str, forward_zone_id: &str) -> Cloudflare {
|
||||||
|
Self {
|
||||||
|
api: JsonApi::token("https://api.cloudflare.com", &format!("Bearer {}", token))
|
||||||
|
.unwrap(),
|
||||||
|
reverse_zone_id: reverse_zone_id.to_owned(),
|
||||||
|
forward_zone_id: forward_zone_id.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl DnsServer for Cloudflare {
|
||||||
|
async fn add_ptr_record(&self, key: &str, value: &str) -> anyhow::Result<BasicRecord> {
|
||||||
|
let id_response: CfResult<CfRecord> = self
|
||||||
|
.api
|
||||||
|
.post(
|
||||||
|
&format!("/client/v4/zones/{}/dns_records", self.reverse_zone_id),
|
||||||
|
CfRecord {
|
||||||
|
content: value.to_string(),
|
||||||
|
name: key.to_string(),
|
||||||
|
r_type: "PTR".to_string(),
|
||||||
|
id: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(BasicRecord {
|
||||||
|
name: id_response.result.name,
|
||||||
|
value: value.to_string(),
|
||||||
|
id: id_response.result.id.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_ptr_record(&self, key: &str) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_a_record(&self, name: &str, ip: IpAddr) -> anyhow::Result<BasicRecord> {
|
||||||
|
let id_response: CfResult<CfRecord> = self
|
||||||
|
.api
|
||||||
|
.post(
|
||||||
|
&format!("/client/v4/zones/{}/dns_records", self.forward_zone_id),
|
||||||
|
CfRecord {
|
||||||
|
content: ip.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
r_type: if ip.is_ipv4() {
|
||||||
|
"A".to_string()
|
||||||
|
} else {
|
||||||
|
"AAAA".to_string()
|
||||||
|
},
|
||||||
|
id: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(BasicRecord {
|
||||||
|
name: id_response.result.name,
|
||||||
|
value: ip.to_string(),
|
||||||
|
id: id_response.result.id.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_a_record(&self, name: &str) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct CfRecord {
|
||||||
|
pub content: String,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub r_type: String,
|
||||||
|
pub id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct CfResult<T> {
|
||||||
|
pub success: bool,
|
||||||
|
pub result: T,
|
||||||
|
}
|
30
src/dns/mod.rs
Normal file
30
src/dns/mod.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use lnvps_db::async_trait;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
#[cfg(feature = "cloudflare")]
|
||||||
|
mod cloudflare;
|
||||||
|
#[cfg(feature = "cloudflare")]
|
||||||
|
pub use cloudflare::*;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait DnsServer: Send + Sync {
|
||||||
|
/// Add PTR record to the reverse zone
|
||||||
|
async fn add_ptr_record(&self, key: &str, value: &str) -> Result<BasicRecord>;
|
||||||
|
|
||||||
|
/// Delete PTR record from the reverse zone
|
||||||
|
async fn delete_ptr_record(&self, key: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// Add A/AAAA record onto the forward zone
|
||||||
|
async fn add_a_record(&self, name: &str, ip: IpAddr) -> Result<BasicRecord>;
|
||||||
|
|
||||||
|
/// Delete A/AAAA record from the forward zone
|
||||||
|
async fn delete_a_record(&self, name: &str) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BasicRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
|
pub id: String,
|
||||||
|
}
|
@ -43,7 +43,7 @@ pub trait VmHostClient: Send + Sync {
|
|||||||
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>> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
{
|
{
|
||||||
Ok(Arc::new(crate::mocks::MockVmHost::default()))
|
Ok(Arc::new(crate::mocks::MockVmHost::new()))
|
||||||
}
|
}
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
{
|
{
|
||||||
@ -56,16 +56,14 @@ pub fn get_host_client(host: &VmHost, cfg: &ProvisionerConfig) -> Result<Arc<dyn
|
|||||||
ssh,
|
ssh,
|
||||||
mac_prefix,
|
mac_prefix,
|
||||||
},
|
},
|
||||||
) => Arc::new(
|
) => Arc::new(proxmox::ProxmoxClient::new(
|
||||||
proxmox::ProxmoxClient::new(
|
host.ip.parse()?,
|
||||||
host.ip.parse()?,
|
&host.name,
|
||||||
&host.name,
|
&host.api_token,
|
||||||
mac_prefix.clone(),
|
mac_prefix.clone(),
|
||||||
qemu.clone(),
|
qemu.clone(),
|
||||||
ssh.clone(),
|
ssh.clone(),
|
||||||
)
|
)),
|
||||||
.with_api_token(&host.api_token),
|
|
||||||
),
|
|
||||||
_ => bail!("Unknown host config: {}", host.kind),
|
_ => bail!("Unknown host config: {}", host.kind),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::host::{CreateVmRequest, VmHostClient};
|
use crate::host::{CreateVmRequest, VmHostClient};
|
||||||
|
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;
|
||||||
use crate::status::{VmRunningState, VmState};
|
use crate::status::{VmRunningState, VmState};
|
||||||
@ -9,6 +10,7 @@ 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::{debug, info};
|
||||||
use rand::random;
|
use rand::random;
|
||||||
|
use reqwest::header::{HeaderMap, AUTHORIZATION};
|
||||||
use reqwest::{ClientBuilder, Method, Url};
|
use reqwest::{ClientBuilder, Method, Url};
|
||||||
use serde::de::value::I32Deserializer;
|
use serde::de::value::I32Deserializer;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
@ -25,9 +27,7 @@ use tokio_tungstenite::tungstenite::handshake::client::{generate_key, Request};
|
|||||||
use tokio_tungstenite::{Connector, MaybeTlsStream, WebSocketStream};
|
use tokio_tungstenite::{Connector, MaybeTlsStream, WebSocketStream};
|
||||||
|
|
||||||
pub struct ProxmoxClient {
|
pub struct ProxmoxClient {
|
||||||
base: Url,
|
api: JsonApi,
|
||||||
token: String,
|
|
||||||
client: reqwest::Client,
|
|
||||||
config: QemuConfig,
|
config: QemuConfig,
|
||||||
ssh: Option<SshConfig>,
|
ssh: Option<SshConfig>,
|
||||||
mac_prefix: String,
|
mac_prefix: String,
|
||||||
@ -38,19 +38,24 @@ impl ProxmoxClient {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
base: Url,
|
base: Url,
|
||||||
node: &str,
|
node: &str,
|
||||||
|
token: &str,
|
||||||
mac_prefix: Option<String>,
|
mac_prefix: Option<String>,
|
||||||
config: QemuConfig,
|
config: QemuConfig,
|
||||||
ssh: Option<SshConfig>,
|
ssh: Option<SshConfig>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
AUTHORIZATION,
|
||||||
|
format!("PVEAPIToken={}", token).parse().unwrap(),
|
||||||
|
);
|
||||||
let client = ClientBuilder::new()
|
let client = ClientBuilder::new()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
|
.default_headers(headers)
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to build client");
|
.expect("Failed to build client");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
base,
|
api: JsonApi { base, client },
|
||||||
token: String::new(),
|
|
||||||
client,
|
|
||||||
config,
|
config,
|
||||||
ssh,
|
ssh,
|
||||||
node: node.to_string(),
|
node: node.to_string(),
|
||||||
@ -58,25 +63,21 @@ impl ProxmoxClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_api_token(mut self, token: &str) -> Self {
|
|
||||||
self.token = token.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get version info
|
/// Get version info
|
||||||
pub async fn version(&self) -> Result<VersionResponse> {
|
pub async fn version(&self) -> Result<VersionResponse> {
|
||||||
let rsp: ResponseBase<VersionResponse> = self.get("/api2/json/version").await?;
|
let rsp: ResponseBase<VersionResponse> = self.api.get("/api2/json/version").await?;
|
||||||
Ok(rsp.data)
|
Ok(rsp.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List nodes
|
/// List nodes
|
||||||
pub async fn list_nodes(&self) -> Result<Vec<NodeResponse>> {
|
pub async fn list_nodes(&self) -> Result<Vec<NodeResponse>> {
|
||||||
let rsp: ResponseBase<Vec<NodeResponse>> = self.get("/api2/json/nodes").await?;
|
let rsp: ResponseBase<Vec<NodeResponse>> = self.api.get("/api2/json/nodes").await?;
|
||||||
Ok(rsp.data)
|
Ok(rsp.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_vm_status(&self, node: &str, vm_id: ProxmoxVmId) -> Result<VmInfo> {
|
pub async fn get_vm_status(&self, node: &str, vm_id: ProxmoxVmId) -> Result<VmInfo> {
|
||||||
let rsp: ResponseBase<VmInfo> = self
|
let rsp: ResponseBase<VmInfo> = self
|
||||||
|
.api
|
||||||
.get(&format!(
|
.get(&format!(
|
||||||
"/api2/json/nodes/{node}/qemu/{vm_id}/status/current"
|
"/api2/json/nodes/{node}/qemu/{vm_id}/status/current"
|
||||||
))
|
))
|
||||||
@ -85,13 +86,16 @@ impl ProxmoxClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_vms(&self, node: &str) -> Result<Vec<VmInfo>> {
|
pub async fn list_vms(&self, node: &str) -> Result<Vec<VmInfo>> {
|
||||||
let rsp: ResponseBase<Vec<VmInfo>> =
|
let rsp: ResponseBase<Vec<VmInfo>> = self
|
||||||
self.get(&format!("/api2/json/nodes/{node}/qemu")).await?;
|
.api
|
||||||
|
.get(&format!("/api2/json/nodes/{node}/qemu"))
|
||||||
|
.await?;
|
||||||
Ok(rsp.data)
|
Ok(rsp.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_storage(&self, node: &str) -> Result<Vec<NodeStorage>> {
|
pub async fn list_storage(&self, node: &str) -> Result<Vec<NodeStorage>> {
|
||||||
let rsp: ResponseBase<Vec<NodeStorage>> = self
|
let rsp: ResponseBase<Vec<NodeStorage>> = self
|
||||||
|
.api
|
||||||
.get(&format!("/api2/json/nodes/{node}/storage"))
|
.get(&format!("/api2/json/nodes/{node}/storage"))
|
||||||
.await?;
|
.await?;
|
||||||
Ok(rsp.data)
|
Ok(rsp.data)
|
||||||
@ -104,6 +108,7 @@ impl ProxmoxClient {
|
|||||||
storage: &str,
|
storage: &str,
|
||||||
) -> Result<Vec<StorageContentEntry>> {
|
) -> Result<Vec<StorageContentEntry>> {
|
||||||
let rsp: ResponseBase<Vec<StorageContentEntry>> = self
|
let rsp: ResponseBase<Vec<StorageContentEntry>> = self
|
||||||
|
.api
|
||||||
.get(&format!(
|
.get(&format!(
|
||||||
"/api2/json/nodes/{node}/storage/{storage}/content"
|
"/api2/json/nodes/{node}/storage/{storage}/content"
|
||||||
))
|
))
|
||||||
@ -116,6 +121,7 @@ impl ProxmoxClient {
|
|||||||
/// https://pve.proxmox.com/pve-docs/api-viewer/?ref=public_apis#/nodes/{node}/qemu
|
/// https://pve.proxmox.com/pve-docs/api-viewer/?ref=public_apis#/nodes/{node}/qemu
|
||||||
pub async fn create_vm(&self, req: CreateVm) -> Result<TaskId> {
|
pub async fn create_vm(&self, req: CreateVm) -> Result<TaskId> {
|
||||||
let rsp: ResponseBase<Option<String>> = self
|
let rsp: ResponseBase<Option<String>> = self
|
||||||
|
.api
|
||||||
.post(&format!("/api2/json/nodes/{}/qemu", req.node), &req)
|
.post(&format!("/api2/json/nodes/{}/qemu", req.node), &req)
|
||||||
.await?;
|
.await?;
|
||||||
if let Some(id) = rsp.data {
|
if let Some(id) = rsp.data {
|
||||||
@ -130,6 +136,7 @@ impl ProxmoxClient {
|
|||||||
/// https://pve.proxmox.com/pve-docs/api-viewer/?ref=public_apis#/nodes/{node}/qemu/{vmid}/config
|
/// https://pve.proxmox.com/pve-docs/api-viewer/?ref=public_apis#/nodes/{node}/qemu/{vmid}/config
|
||||||
pub async fn configure_vm(&self, req: ConfigureVm) -> Result<TaskId> {
|
pub async fn configure_vm(&self, req: ConfigureVm) -> Result<TaskId> {
|
||||||
let rsp: ResponseBase<Option<String>> = self
|
let rsp: ResponseBase<Option<String>> = self
|
||||||
|
.api
|
||||||
.post(
|
.post(
|
||||||
&format!("/api2/json/nodes/{}/qemu/{}/config", req.node, req.vm_id),
|
&format!("/api2/json/nodes/{}/qemu/{}/config", req.node, req.vm_id),
|
||||||
&req,
|
&req,
|
||||||
@ -147,6 +154,7 @@ impl ProxmoxClient {
|
|||||||
/// https://pve.proxmox.com/pve-docs/api-viewer/?ref=public_apis#/nodes/{node}/qemu
|
/// https://pve.proxmox.com/pve-docs/api-viewer/?ref=public_apis#/nodes/{node}/qemu
|
||||||
pub async fn delete_vm(&self, node: &str, vm: ProxmoxVmId) -> Result<TaskId> {
|
pub async fn delete_vm(&self, node: &str, vm: ProxmoxVmId) -> Result<TaskId> {
|
||||||
let rsp: ResponseBase<Option<String>> = self
|
let rsp: ResponseBase<Option<String>> = self
|
||||||
|
.api
|
||||||
.req(
|
.req(
|
||||||
Method::DELETE,
|
Method::DELETE,
|
||||||
&format!("/api2/json/nodes/{node}/qemu/{vm}"),
|
&format!("/api2/json/nodes/{node}/qemu/{vm}"),
|
||||||
@ -168,6 +176,7 @@ impl ProxmoxClient {
|
|||||||
/// https://pve.proxmox.com/pve-docs/api-viewer/?ref=public_apis#/nodes/{node}/tasks/{upid}/status
|
/// https://pve.proxmox.com/pve-docs/api-viewer/?ref=public_apis#/nodes/{node}/tasks/{upid}/status
|
||||||
pub async fn get_task_status(&self, task: &TaskId) -> Result<TaskStatus> {
|
pub async fn get_task_status(&self, task: &TaskId) -> Result<TaskStatus> {
|
||||||
let rsp: ResponseBase<TaskStatus> = self
|
let rsp: ResponseBase<TaskStatus> = self
|
||||||
|
.api
|
||||||
.get(&format!(
|
.get(&format!(
|
||||||
"/api2/json/nodes/{}/tasks/{}/status",
|
"/api2/json/nodes/{}/tasks/{}/status",
|
||||||
task.node, task.id
|
task.node, task.id
|
||||||
@ -209,6 +218,7 @@ impl ProxmoxClient {
|
|||||||
/// Download an image to the host disk
|
/// Download an image to the host disk
|
||||||
pub async fn download_image(&self, req: DownloadUrlRequest) -> Result<TaskId> {
|
pub async fn download_image(&self, req: DownloadUrlRequest) -> Result<TaskId> {
|
||||||
let rsp: ResponseBase<String> = self
|
let rsp: ResponseBase<String> = self
|
||||||
|
.api
|
||||||
.post(
|
.post(
|
||||||
&format!(
|
&format!(
|
||||||
"/api2/json/nodes/{}/storage/{}/download-url",
|
"/api2/json/nodes/{}/storage/{}/download-url",
|
||||||
@ -229,7 +239,7 @@ impl ProxmoxClient {
|
|||||||
if let Some(ssh_config) = &self.ssh {
|
if let Some(ssh_config) = &self.ssh {
|
||||||
let mut ses = SshClient::new()?;
|
let mut ses = SshClient::new()?;
|
||||||
ses.connect(
|
ses.connect(
|
||||||
(self.base.host().unwrap().to_string(), 22),
|
(self.api.base.host().unwrap().to_string(), 22),
|
||||||
&ssh_config.user,
|
&ssh_config.user,
|
||||||
&ssh_config.key,
|
&ssh_config.key,
|
||||||
)
|
)
|
||||||
@ -274,6 +284,7 @@ impl ProxmoxClient {
|
|||||||
/// Resize a disk on a VM
|
/// Resize a disk on a VM
|
||||||
pub async fn resize_disk(&self, req: ResizeDiskRequest) -> Result<TaskId> {
|
pub async fn resize_disk(&self, req: ResizeDiskRequest) -> Result<TaskId> {
|
||||||
let rsp: ResponseBase<String> = self
|
let rsp: ResponseBase<String> = self
|
||||||
|
.api
|
||||||
.req(
|
.req(
|
||||||
Method::PUT,
|
Method::PUT,
|
||||||
&format!("/api2/json/nodes/{}/qemu/{}/resize", &req.node, &req.vm_id),
|
&format!("/api2/json/nodes/{}/qemu/{}/resize", &req.node, &req.vm_id),
|
||||||
@ -289,6 +300,7 @@ impl ProxmoxClient {
|
|||||||
/// Start a VM
|
/// Start a VM
|
||||||
pub async fn start_vm(&self, node: &str, vm: ProxmoxVmId) -> Result<TaskId> {
|
pub async fn start_vm(&self, node: &str, vm: ProxmoxVmId) -> Result<TaskId> {
|
||||||
let rsp: ResponseBase<String> = self
|
let rsp: ResponseBase<String> = self
|
||||||
|
.api
|
||||||
.post(
|
.post(
|
||||||
&format!("/api2/json/nodes/{}/qemu/{}/status/start", node, vm),
|
&format!("/api2/json/nodes/{}/qemu/{}/status/start", node, vm),
|
||||||
(),
|
(),
|
||||||
@ -303,6 +315,7 @@ impl ProxmoxClient {
|
|||||||
/// Stop a VM
|
/// Stop a VM
|
||||||
pub async fn stop_vm(&self, node: &str, vm: ProxmoxVmId) -> Result<TaskId> {
|
pub async fn stop_vm(&self, node: &str, vm: ProxmoxVmId) -> Result<TaskId> {
|
||||||
let rsp: ResponseBase<String> = self
|
let rsp: ResponseBase<String> = self
|
||||||
|
.api
|
||||||
.post(
|
.post(
|
||||||
&format!("/api2/json/nodes/{}/qemu/{}/status/stop", node, vm),
|
&format!("/api2/json/nodes/{}/qemu/{}/status/stop", node, vm),
|
||||||
(),
|
(),
|
||||||
@ -317,6 +330,7 @@ impl ProxmoxClient {
|
|||||||
/// Stop a VM
|
/// Stop a VM
|
||||||
pub async fn shutdown_vm(&self, node: &str, vm: ProxmoxVmId) -> Result<TaskId> {
|
pub async fn shutdown_vm(&self, node: &str, vm: ProxmoxVmId) -> Result<TaskId> {
|
||||||
let rsp: ResponseBase<String> = self
|
let rsp: ResponseBase<String> = self
|
||||||
|
.api
|
||||||
.post(
|
.post(
|
||||||
&format!("/api2/json/nodes/{}/qemu/{}/status/shutdown", node, vm),
|
&format!("/api2/json/nodes/{}/qemu/{}/status/shutdown", node, vm),
|
||||||
(),
|
(),
|
||||||
@ -331,6 +345,7 @@ impl ProxmoxClient {
|
|||||||
/// Stop a VM
|
/// Stop a VM
|
||||||
pub async fn reset_vm(&self, node: &str, vm: ProxmoxVmId) -> Result<TaskId> {
|
pub async fn reset_vm(&self, node: &str, vm: ProxmoxVmId) -> Result<TaskId> {
|
||||||
let rsp: ResponseBase<String> = self
|
let rsp: ResponseBase<String> = self
|
||||||
|
.api
|
||||||
.post(
|
.post(
|
||||||
&format!("/api2/json/nodes/{}/qemu/{}/status/reset", node, vm),
|
&format!("/api2/json/nodes/{}/qemu/{}/status/reset", node, vm),
|
||||||
(),
|
(),
|
||||||
@ -341,117 +356,6 @@ impl ProxmoxClient {
|
|||||||
node: node.to_string(),
|
node: node.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create terminal proxy session
|
|
||||||
pub async fn terminal_proxy(&self, node: &str, vm: ProxmoxVmId) -> Result<TerminalProxyTicket> {
|
|
||||||
let rsp: ResponseBase<TerminalProxyTicket> = self
|
|
||||||
.post(
|
|
||||||
&format!("/api2/json/nodes/{}/qemu/{}/termproxy", node, vm),
|
|
||||||
(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(rsp.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open websocket connection to terminal proxy
|
|
||||||
pub async fn open_terminal_proxy(
|
|
||||||
&self,
|
|
||||||
node: &str,
|
|
||||||
vm: ProxmoxVmId,
|
|
||||||
req: TerminalProxyTicket,
|
|
||||||
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
|
|
||||||
self.get_task_status(&TaskId {
|
|
||||||
id: req.upid,
|
|
||||||
node: node.to_string(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut url: Url = self.base.join(&format!(
|
|
||||||
"/api2/json/nodes/{}/qemu/{}/vncwebsocket",
|
|
||||||
node, vm
|
|
||||||
))?;
|
|
||||||
url.set_scheme("wss").unwrap();
|
|
||||||
url.query_pairs_mut().append_pair("port", &req.port);
|
|
||||||
url.query_pairs_mut().append_pair("vncticket", &req.ticket);
|
|
||||||
|
|
||||||
let r = Request::builder()
|
|
||||||
.method("GET")
|
|
||||||
.header("Host", url.host().unwrap().to_string())
|
|
||||||
.header("Connection", "Upgrade")
|
|
||||||
.header("Upgrade", "websocket")
|
|
||||||
.header("Sec-WebSocket-Version", "13")
|
|
||||||
.header("Sec-WebSocket-Key", generate_key())
|
|
||||||
.header("Sec-WebSocket-Protocol", "binary")
|
|
||||||
.header("Authorization", format!("PVEAPIToken={}", self.token))
|
|
||||||
.uri(url.as_str())
|
|
||||||
.body(())?;
|
|
||||||
|
|
||||||
debug!("Connecting terminal proxy: {:?}", &r);
|
|
||||||
let (ws, _rsp) = tokio_tungstenite::connect_async_tls_with_config(
|
|
||||||
r,
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
Some(Connector::NativeTls(
|
|
||||||
native_tls::TlsConnector::builder()
|
|
||||||
.danger_accept_invalid_certs(true)
|
|
||||||
.build()?,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(ws)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
|
|
||||||
debug!(">> GET {}", path);
|
|
||||||
let rsp = self
|
|
||||||
.client
|
|
||||||
.get(self.base.join(path)?)
|
|
||||||
.header("Authorization", format!("PVEAPIToken={}", self.token))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let status = rsp.status();
|
|
||||||
let text = rsp.text().await?;
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
debug!("<< {}", text);
|
|
||||||
if status.is_success() {
|
|
||||||
Ok(serde_json::from_str(&text)?)
|
|
||||||
} else {
|
|
||||||
bail!("{}", status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn post<T: DeserializeOwned, R: Serialize>(&self, path: &str, body: R) -> Result<T> {
|
|
||||||
self.req(Method::POST, path, body).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn req<T: DeserializeOwned, R: Serialize>(
|
|
||||||
&self,
|
|
||||||
method: Method,
|
|
||||||
path: &str,
|
|
||||||
body: R,
|
|
||||||
) -> Result<T> {
|
|
||||||
let body = serde_json::to_string(&body)?;
|
|
||||||
debug!(">> {} {}: {}", method.clone(), path, &body);
|
|
||||||
let rsp = self
|
|
||||||
.client
|
|
||||||
.request(method.clone(), self.base.join(path)?)
|
|
||||||
.header("Authorization", format!("PVEAPIToken={}", self.token))
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.body(body)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let status = rsp.status();
|
|
||||||
let text = rsp.text().await?;
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
debug!("<< {}", text);
|
|
||||||
if status.is_success() {
|
|
||||||
Ok(serde_json::from_str(&text)?)
|
|
||||||
} else {
|
|
||||||
bail!("{} {}: {}: {}", method, path, status, &text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProxmoxClient {
|
impl ProxmoxClient {
|
||||||
|
73
src/json_api.rs
Normal file
73
src/json_api.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
use anyhow::bail;
|
||||||
|
use log::debug;
|
||||||
|
use reqwest::header::{HeaderMap, AUTHORIZATION};
|
||||||
|
use reqwest::{Client, Method, Url};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub struct JsonApi {
|
||||||
|
pub client: Client,
|
||||||
|
pub base: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonApi {
|
||||||
|
pub fn token(base: &str, token: &str) -> anyhow::Result<Self> {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(AUTHORIZATION, token.parse()?);
|
||||||
|
|
||||||
|
let client = Client::builder().default_headers(headers).build()?;
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
base: base.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get<T: DeserializeOwned>(&self, path: &str) -> anyhow::Result<T> {
|
||||||
|
debug!(">> GET {}", path);
|
||||||
|
let rsp = self.client.get(self.base.join(path)?).send().await?;
|
||||||
|
let status = rsp.status();
|
||||||
|
let text = rsp.text().await?;
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
debug!("<< {}", text);
|
||||||
|
if status.is_success() {
|
||||||
|
Ok(serde_json::from_str(&text)?)
|
||||||
|
} else {
|
||||||
|
bail!("{}", status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post<T: DeserializeOwned, R: Serialize>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
body: R,
|
||||||
|
) -> anyhow::Result<T> {
|
||||||
|
self.req(Method::POST, path, body).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn req<T: DeserializeOwned, R: Serialize>(
|
||||||
|
&self,
|
||||||
|
method: Method,
|
||||||
|
path: &str,
|
||||||
|
body: R,
|
||||||
|
) -> anyhow::Result<T> {
|
||||||
|
let body = serde_json::to_string(&body)?;
|
||||||
|
debug!(">> {} {}: {}", method.clone(), path, &body);
|
||||||
|
let rsp = self
|
||||||
|
.client
|
||||||
|
.request(method.clone(), self.base.join(path)?)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.body(body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let status = rsp.status();
|
||||||
|
let text = rsp.text().await?;
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
debug!("<< {}", text);
|
||||||
|
if status.is_success() {
|
||||||
|
Ok(serde_json::from_str(&text)?)
|
||||||
|
} else {
|
||||||
|
bail!("{} {}: {}: {}", method, path, status, &text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod cors;
|
pub mod cors;
|
||||||
|
pub mod dns;
|
||||||
pub mod exchange;
|
pub mod exchange;
|
||||||
pub mod host;
|
pub mod host;
|
||||||
pub mod invoice;
|
pub mod invoice;
|
||||||
|
pub mod json_api;
|
||||||
pub mod lightning;
|
pub mod lightning;
|
||||||
pub mod nip98;
|
pub mod nip98;
|
||||||
pub mod provisioner;
|
pub mod provisioner;
|
||||||
|
@ -1,91 +1,26 @@
|
|||||||
use crate::api::WEBHOOK_BRIDGE;
|
use crate::api::WEBHOOK_BRIDGE;
|
||||||
|
use crate::json_api::JsonApi;
|
||||||
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
use lnvps_db::async_trait;
|
use lnvps_db::async_trait;
|
||||||
use log::debug;
|
|
||||||
use reqwest::header::HeaderMap;
|
|
||||||
use reqwest::{Method, Url};
|
|
||||||
use rocket::http::ext::IntoCollection;
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use tokio_stream::wrappers::BroadcastStream;
|
use tokio_stream::wrappers::BroadcastStream;
|
||||||
|
|
||||||
pub struct BitvoraNode {
|
pub struct BitvoraNode {
|
||||||
base: Url,
|
api: JsonApi,
|
||||||
client: reqwest::Client,
|
|
||||||
webhook_secret: String,
|
webhook_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BitvoraNode {
|
impl BitvoraNode {
|
||||||
pub fn new(api_token: &str, webhook_secret: &str) -> Self {
|
pub fn new(api_token: &str, webhook_secret: &str) -> Self {
|
||||||
let mut headers = HeaderMap::new();
|
let auth = format!("Bearer {}", api_token);
|
||||||
headers.insert(
|
|
||||||
"Authorization",
|
|
||||||
format!("Bearer {}", api_token).parse().unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.default_headers(headers)
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
base: Url::parse("https://api.bitvora.com/").unwrap(),
|
api: JsonApi::token("https://api.bitvora.com/", &auth).unwrap(),
|
||||||
client,
|
|
||||||
webhook_secret: webhook_secret.to_string(),
|
webhook_secret: webhook_secret.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get<T: DeserializeOwned>(&self, path: &str) -> anyhow::Result<T> {
|
|
||||||
debug!(">> GET {}", path);
|
|
||||||
let rsp = self.client.get(self.base.join(path)?).send().await?;
|
|
||||||
let status = rsp.status();
|
|
||||||
let text = rsp.text().await?;
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
debug!("<< {}", text);
|
|
||||||
if status.is_success() {
|
|
||||||
Ok(serde_json::from_str(&text)?)
|
|
||||||
} else {
|
|
||||||
bail!("{}", status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn post<T: DeserializeOwned, R: Serialize>(
|
|
||||||
&self,
|
|
||||||
path: &str,
|
|
||||||
body: R,
|
|
||||||
) -> anyhow::Result<T> {
|
|
||||||
self.req(Method::POST, path, body).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn req<T: DeserializeOwned, R: Serialize>(
|
|
||||||
&self,
|
|
||||||
method: Method,
|
|
||||||
path: &str,
|
|
||||||
body: R,
|
|
||||||
) -> anyhow::Result<T> {
|
|
||||||
let body = serde_json::to_string(&body)?;
|
|
||||||
debug!(">> {} {}: {}", method.clone(), path, &body);
|
|
||||||
let rsp = self
|
|
||||||
.client
|
|
||||||
.request(method.clone(), self.base.join(path)?)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.body(body)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let status = rsp.status();
|
|
||||||
let text = rsp.text().await?;
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
debug!("<< {}", text);
|
|
||||||
if status.is_success() {
|
|
||||||
Ok(serde_json::from_str(&text)?)
|
|
||||||
} else {
|
|
||||||
bail!("{} {}: {}: {}", method, path, status, &text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -98,7 +33,8 @@ impl LightningNode for BitvoraNode {
|
|||||||
expiry_seconds: req.expire.unwrap_or(3600) as u64,
|
expiry_seconds: req.expire.unwrap_or(3600) as u64,
|
||||||
};
|
};
|
||||||
let rsp: BitvoraResponse<CreateInvoiceResponse> = self
|
let rsp: BitvoraResponse<CreateInvoiceResponse> = self
|
||||||
.req(Method::POST, "/v1/bitcoin/deposit/lightning-invoice", req)
|
.api
|
||||||
|
.post("/v1/bitcoin/deposit/lightning-invoice", req)
|
||||||
.await?;
|
.await?;
|
||||||
if rsp.status >= 400 {
|
if rsp.status >= 400 {
|
||||||
bail!(
|
bail!(
|
||||||
|
127
src/mocks.rs
127
src/mocks.rs
@ -1,3 +1,4 @@
|
|||||||
|
use crate::dns::{BasicRecord, DnsServer};
|
||||||
use crate::host::{CreateVmRequest, VmHostClient};
|
use crate::host::{CreateVmRequest, 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};
|
||||||
@ -406,11 +407,28 @@ impl LNVpsDb for MockDb {
|
|||||||
ip_range_id: ip_assignment.ip_range_id,
|
ip_range_id: ip_assignment.ip_range_id,
|
||||||
ip: ip_assignment.ip.clone(),
|
ip: ip_assignment.ip.clone(),
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
arp_ref: ip_assignment.arp_ref.clone(),
|
||||||
|
dns_forward: ip_assignment.dns_forward.clone(),
|
||||||
|
dns_forward_ref: ip_assignment.dns_forward_ref.clone(),
|
||||||
|
dns_reverse: ip_assignment.dns_reverse.clone(),
|
||||||
|
dns_reverse_ref: ip_assignment.dns_reverse_ref.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(max + 1)
|
Ok(max + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> anyhow::Result<()> {
|
||||||
|
let mut ip_assignments = self.ip_assignments.lock().await;
|
||||||
|
if let Some(i) = ip_assignments.get_mut(&ip_assignment.vm_id) {
|
||||||
|
i.arp_ref = ip_assignment.arp_ref.clone();
|
||||||
|
i.dns_forward = ip_assignment.dns_forward.clone();
|
||||||
|
i.dns_reverse = ip_assignment.dns_reverse.clone();
|
||||||
|
i.dns_reverse_ref = ip_assignment.dns_reverse_ref.clone();
|
||||||
|
i.dns_forward_ref = ip_assignment.dns_forward_ref.clone();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_vm_ip_assignments(&self, vm_id: u64) -> anyhow::Result<Vec<VmIpAssignment>> {
|
async fn list_vm_ip_assignments(&self, vm_id: u64) -> anyhow::Result<Vec<VmIpAssignment>> {
|
||||||
let ip_assignments = self.ip_assignments.lock().await;
|
let ip_assignments = self.ip_assignments.lock().await;
|
||||||
Ok(ip_assignments
|
Ok(ip_assignments
|
||||||
@ -475,12 +493,12 @@ pub struct MockRouter {
|
|||||||
|
|
||||||
impl MockRouter {
|
impl MockRouter {
|
||||||
pub fn new(policy: NetworkPolicy) -> Self {
|
pub fn new(policy: NetworkPolicy) -> Self {
|
||||||
static ARP: LazyLock<Arc<Mutex<HashMap<u64, ArpEntry>>>> =
|
static LAZY_ARP: LazyLock<Arc<Mutex<HashMap<u64, ArpEntry>>>> =
|
||||||
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
|
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
policy,
|
policy,
|
||||||
arp: ARP.clone(),
|
arp: LAZY_ARP.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -535,6 +553,16 @@ struct MockInvoice {
|
|||||||
settle_index: u64,
|
settle_index: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MockNode {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
static LAZY_INVOICES: LazyLock<Arc<Mutex<HashMap<String, MockInvoice>>>> =
|
||||||
|
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
|
||||||
|
Self {
|
||||||
|
invoices: LAZY_INVOICES.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl LightningNode for MockNode {
|
impl LightningNode for MockNode {
|
||||||
async fn add_invoice(&self, req: AddInvoiceRequest) -> anyhow::Result<AddInvoiceResult> {
|
async fn add_invoice(&self, req: AddInvoiceRequest) -> anyhow::Result<AddInvoiceResult> {
|
||||||
@ -549,7 +577,7 @@ impl LightningNode for MockNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MockVmHost {
|
pub struct MockVmHost {
|
||||||
vms: Arc<Mutex<HashMap<u64, MockVm>>>,
|
vms: Arc<Mutex<HashMap<u64, MockVm>>>,
|
||||||
}
|
}
|
||||||
@ -559,6 +587,16 @@ struct MockVm {
|
|||||||
pub state: VmRunningState,
|
pub state: VmRunningState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MockVmHost {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
static LAZY_VMS: LazyLock<Arc<Mutex<HashMap<u64, MockVm>>>> =
|
||||||
|
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
|
||||||
|
Self {
|
||||||
|
vms: LAZY_VMS.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl VmHostClient for MockVmHost {
|
impl VmHostClient for MockVmHost {
|
||||||
async fn download_os_image(&self, image: &VmOsImage) -> anyhow::Result<()> {
|
async fn download_os_image(&self, image: &VmOsImage) -> anyhow::Result<()> {
|
||||||
@ -633,3 +671,86 @@ impl VmHostClient for MockVmHost {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct MockDnsServer {
|
||||||
|
pub forward: Arc<Mutex<HashMap<String, MockDnsEntry>>>,
|
||||||
|
pub reverse: Arc<Mutex<HashMap<String, MockDnsEntry>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MockDnsEntry {
|
||||||
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
|
pub kind: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockDnsServer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
static LAZY_FWD: LazyLock<Arc<Mutex<HashMap<String, MockDnsEntry>>>> =
|
||||||
|
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
|
||||||
|
static LAZY_REV: LazyLock<Arc<Mutex<HashMap<String, MockDnsEntry>>>> =
|
||||||
|
LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
|
||||||
|
Self {
|
||||||
|
forward: LAZY_FWD.clone(),
|
||||||
|
reverse: LAZY_REV.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[async_trait]
|
||||||
|
impl DnsServer for MockDnsServer {
|
||||||
|
async fn add_ptr_record(&self, key: &str, value: &str) -> anyhow::Result<BasicRecord> {
|
||||||
|
let mut rev = self.reverse.lock().await;
|
||||||
|
|
||||||
|
if rev.values().any(|v| v.name == key) {
|
||||||
|
bail!("Duplicate record with name {}", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rnd_id: [u8; 12] = rand::random();
|
||||||
|
let id = hex::encode(rnd_id);
|
||||||
|
rev.insert(
|
||||||
|
id.clone(),
|
||||||
|
MockDnsEntry {
|
||||||
|
name: key.to_string(),
|
||||||
|
value: value.to_string(),
|
||||||
|
kind: "PTR".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(BasicRecord {
|
||||||
|
name: format!("{}.X.Y.Z.in-addr.arpa", key),
|
||||||
|
value: value.to_string(),
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_ptr_record(&self, key: &str) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_a_record(&self, name: &str, ip: IpAddr) -> anyhow::Result<BasicRecord> {
|
||||||
|
let mut rev = self.forward.lock().await;
|
||||||
|
|
||||||
|
if rev.values().any(|v| v.name == name) {
|
||||||
|
bail!("Duplicate record with name {}", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fqdn = format!("{}.lnvps.mock", name);
|
||||||
|
let rnd_id: [u8; 12] = rand::random();
|
||||||
|
let id = hex::encode(rnd_id);
|
||||||
|
rev.insert(
|
||||||
|
id.clone(),
|
||||||
|
MockDnsEntry {
|
||||||
|
name: fqdn.clone(),
|
||||||
|
value: ip.to_string(),
|
||||||
|
kind: "A".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(BasicRecord {
|
||||||
|
name: fqdn,
|
||||||
|
value: ip.to_string(),
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_a_record(&self, name: &str) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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, CreateVmRequest, VmHostClient};
|
||||||
use crate::lightning::{AddInvoiceRequest, LightningNode};
|
use crate::lightning::{AddInvoiceRequest, LightningNode};
|
||||||
@ -13,6 +14,7 @@ use nostr::util::hex;
|
|||||||
use rand::random;
|
use rand::random;
|
||||||
use rocket::futures::{SinkExt, StreamExt};
|
use rocket::futures::{SinkExt, StreamExt};
|
||||||
use std::collections::{HashMap, 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;
|
||||||
@ -31,6 +33,8 @@ pub struct LNVpsProvisioner {
|
|||||||
rates: Arc<dyn ExchangeRateService>,
|
rates: Arc<dyn ExchangeRateService>,
|
||||||
|
|
||||||
router: Option<Arc<dyn Router>>,
|
router: Option<Arc<dyn Router>>,
|
||||||
|
dns: Option<Arc<dyn DnsServer>>,
|
||||||
|
|
||||||
network_policy: NetworkPolicy,
|
network_policy: NetworkPolicy,
|
||||||
provisioner_config: ProvisionerConfig,
|
provisioner_config: ProvisionerConfig,
|
||||||
}
|
}
|
||||||
@ -47,6 +51,7 @@ impl LNVpsProvisioner {
|
|||||||
node,
|
node,
|
||||||
rates,
|
rates,
|
||||||
router: settings.get_router().expect("router config"),
|
router: settings.get_router().expect("router config"),
|
||||||
|
dns: settings.get_dns().expect("dns config"),
|
||||||
network_policy: settings.network_policy,
|
network_policy: settings.network_policy,
|
||||||
provisioner_config: settings.provisioner,
|
provisioner_config: settings.provisioner,
|
||||||
read_only: settings.read_only,
|
read_only: settings.read_only,
|
||||||
@ -73,12 +78,14 @@ impl LNVpsProvisioner {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_ip_assignment(&self, vm: &Vm, assignment: &VmIpAssignment) -> Result<()> {
|
async fn save_ip_assignment(&self, vm: &Vm, assignment: &mut VmIpAssignment) -> Result<()> {
|
||||||
|
let ip = IpAddr::from_str(&assignment.ip)?;
|
||||||
|
|
||||||
// apply network policy
|
// apply network policy
|
||||||
if let NetworkAccessPolicy::StaticArp { interface } = &self.network_policy.access {
|
if let NetworkAccessPolicy::StaticArp { interface } = &self.network_policy.access {
|
||||||
if let Some(r) = self.router.as_ref() {
|
if let Some(r) = self.router.as_ref() {
|
||||||
r.add_arp_entry(
|
r.add_arp_entry(
|
||||||
IpAddr::from_str(&assignment.ip)?,
|
ip.clone(),
|
||||||
&vm.mac_address,
|
&vm.mac_address,
|
||||||
interface,
|
interface,
|
||||||
Some(&format!("VM{}", vm.id)),
|
Some(&format!("VM{}", vm.id)),
|
||||||
@ -89,8 +96,28 @@ impl LNVpsProvisioner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add DNS records
|
||||||
|
if let Some(dns) = &self.dns {
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
IpAddr::V6(_) => {
|
||||||
|
warn!("IPv6 forward DNS not supported yet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// save to db
|
// save to db
|
||||||
self.db.insert_vm_ip_assignment(assignment).await?;
|
self.db.insert_vm_ip_assignment(&assignment).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,15 +133,20 @@ impl LNVpsProvisioner {
|
|||||||
|
|
||||||
let template = self.db.get_vm_template(vm.template_id).await?;
|
let template = self.db.get_vm_template(vm.template_id).await?;
|
||||||
let ip = network.pick_ip_for_region(template.region_id).await?;
|
let ip = network.pick_ip_for_region(template.region_id).await?;
|
||||||
let assignment = VmIpAssignment {
|
let mut assignment = VmIpAssignment {
|
||||||
id: 0,
|
id: 0,
|
||||||
vm_id,
|
vm_id,
|
||||||
ip_range_id: ip.range_id,
|
ip_range_id: ip.range_id,
|
||||||
ip: ip.ip.to_string(),
|
ip: ip.ip.to_string(),
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
arp_ref: None,
|
||||||
|
dns_forward: None,
|
||||||
|
dns_forward_ref: None,
|
||||||
|
dns_reverse: None,
|
||||||
|
dns_reverse_ref: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.save_ip_assignment(&vm, &assignment).await?;
|
self.save_ip_assignment(&vm, &mut assignment).await?;
|
||||||
Ok(vec![assignment])
|
Ok(vec![assignment])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,9 +358,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::exchange::DefaultRateCache;
|
use crate::exchange::DefaultRateCache;
|
||||||
use crate::mocks::{MockDb, MockNode};
|
use crate::mocks::{MockDb, MockNode};
|
||||||
use crate::settings::{
|
use crate::settings::{DnsServerConfig, LightningConfig, QemuConfig, RouterConfig};
|
||||||
ApiConfig, Credentials, LndConfig, ProvisionerConfig, QemuConfig, RouterConfig,
|
|
||||||
};
|
|
||||||
use lnvps_db::UserSshKey;
|
use lnvps_db::UserSshKey;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@ -338,7 +368,7 @@ mod tests {
|
|||||||
let settings = Settings {
|
let settings = Settings {
|
||||||
listen: None,
|
listen: None,
|
||||||
db: "".to_string(),
|
db: "".to_string(),
|
||||||
lnd: LndConfig {
|
lightning: LightningConfig::LND {
|
||||||
url: "".to_string(),
|
url: "".to_string(),
|
||||||
cert: Default::default(),
|
cert: Default::default(),
|
||||||
macaroon: Default::default(),
|
macaroon: Default::default(),
|
||||||
@ -364,21 +394,23 @@ mod tests {
|
|||||||
},
|
},
|
||||||
delete_after: 0,
|
delete_after: 0,
|
||||||
smtp: None,
|
smtp: None,
|
||||||
router: Some(RouterConfig::Mikrotik(ApiConfig {
|
router: Some(RouterConfig::Mikrotik {
|
||||||
id: "mock-router".to_string(),
|
|
||||||
url: "https://localhost".to_string(),
|
url: "https://localhost".to_string(),
|
||||||
credentials: Credentials::UsernamePassword {
|
username: "admin".to_string(),
|
||||||
username: "admin".to_string(),
|
password: "password123".to_string(),
|
||||||
password: "password123".to_string(),
|
}),
|
||||||
},
|
dns: Some(DnsServerConfig::Cloudflare {
|
||||||
})),
|
token: "abc".to_string(),
|
||||||
dns: None,
|
forward_zone_id: "123".to_string(),
|
||||||
|
reverse_zone_id: "456".to_string(),
|
||||||
|
}),
|
||||||
nostr: None,
|
nostr: None,
|
||||||
};
|
};
|
||||||
let db = Arc::new(MockDb::default());
|
let db = Arc::new(MockDb::default());
|
||||||
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();
|
||||||
@ -408,9 +440,15 @@ mod tests {
|
|||||||
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
||||||
assert_eq!(1, ips.len());
|
assert_eq!(1, ips.len());
|
||||||
let ip = ips.first().unwrap();
|
let ip = ips.first().unwrap();
|
||||||
|
println!("{:?}", ip);
|
||||||
assert_eq!(ip.ip, arp.address);
|
assert_eq!(ip.ip, arp.address);
|
||||||
assert_eq!(ip.ip_range_id, 1);
|
assert_eq!(ip.ip_range_id, 1);
|
||||||
assert_eq!(ip.vm_id, vm.id);
|
assert_eq!(ip.vm_id, vm.id);
|
||||||
|
assert!(ip.dns_forward.is_some());
|
||||||
|
assert!(ip.dns_reverse.is_some());
|
||||||
|
assert!(ip.dns_reverse_ref.is_some());
|
||||||
|
assert!(ip.dns_forward.is_some());
|
||||||
|
assert_eq!(ip.dns_reverse, ip.dns_forward);
|
||||||
|
|
||||||
// assert IP address is not CIDR
|
// assert IP address is not CIDR
|
||||||
assert!(IpAddr::from_str(&ip.ip).is_ok());
|
assert!(IpAddr::from_str(&ip.ip).is_ok());
|
||||||
|
@ -116,7 +116,7 @@ mod tests {
|
|||||||
vm_id: 0,
|
vm_id: 0,
|
||||||
ip_range_id: ip.range_id,
|
ip_range_id: ip.range_id,
|
||||||
ip: ip.ip.to_string(),
|
ip: ip.ip.to_string(),
|
||||||
deleted: false,
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("Could not insert vm ip");
|
.expect("Could not insert vm ip");
|
||||||
|
@ -1,65 +1,24 @@
|
|||||||
|
use crate::json_api::JsonApi;
|
||||||
use crate::router::{ArpEntry, Router};
|
use crate::router::{ArpEntry, Router};
|
||||||
use anyhow::{bail, 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::{Client, Method, Url};
|
|
||||||
use rocket::async_trait;
|
use rocket::async_trait;
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
pub struct MikrotikRouter {
|
pub struct MikrotikRouter {
|
||||||
url: Url,
|
api: JsonApi,
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
client: Client,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MikrotikRouter {
|
impl MikrotikRouter {
|
||||||
pub fn new(url: &str, username: &str, password: &str) -> Self {
|
pub fn new(url: &str, username: &str, password: &str) -> Self {
|
||||||
|
let auth = format!(
|
||||||
|
"Basic {}",
|
||||||
|
STANDARD.encode(format!("{}:{}", username, password))
|
||||||
|
);
|
||||||
Self {
|
Self {
|
||||||
url: url.parse().unwrap(),
|
api: JsonApi::token(url, &auth).unwrap(),
|
||||||
username: username.to_string(),
|
|
||||||
password: password.to_string(),
|
|
||||||
client: Client::builder()
|
|
||||||
.danger_accept_invalid_certs(true)
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn req<T: DeserializeOwned, R: Serialize>(
|
|
||||||
&self,
|
|
||||||
method: Method,
|
|
||||||
path: &str,
|
|
||||||
body: R,
|
|
||||||
) -> Result<T> {
|
|
||||||
let body = serde_json::to_string(&body)?;
|
|
||||||
debug!(">> {} {}: {}", method.clone(), path, &body);
|
|
||||||
let rsp = self
|
|
||||||
.client
|
|
||||||
.request(method.clone(), self.url.join(path)?)
|
|
||||||
.header(
|
|
||||||
"Authorization",
|
|
||||||
format!(
|
|
||||||
"Basic {}",
|
|
||||||
STANDARD.encode(format!("{}:{}", self.username, self.password))
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.body(body)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let status = rsp.status();
|
|
||||||
let text = rsp.text().await?;
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
debug!("<< {}", text);
|
|
||||||
if status.is_success() {
|
|
||||||
Ok(serde_json::from_str(&text)?)
|
|
||||||
} else {
|
|
||||||
bail!("{} {}: {}", method, path, status);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,7 +26,7 @@ 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.req(Method::GET, "/rest/ip/arp", ()).await?;
|
let rsp: Vec<ArpEntry> = self.api.req(Method::GET, "/rest/ip/arp", ()).await?;
|
||||||
Ok(rsp)
|
Ok(rsp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +38,7 @@ impl Router for MikrotikRouter {
|
|||||||
comment: Option<&str>,
|
comment: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let _rsp: ArpEntry = self
|
let _rsp: ArpEntry = self
|
||||||
|
.api
|
||||||
.req(
|
.req(
|
||||||
Method::PUT,
|
Method::PUT,
|
||||||
"/rest/ip/arp",
|
"/rest/ip/arp",
|
||||||
@ -97,6 +57,7 @@ impl Router for MikrotikRouter {
|
|||||||
|
|
||||||
async fn remove_arp_entry(&self, id: &str) -> Result<()> {
|
async fn remove_arp_entry(&self, id: &str) -> Result<()> {
|
||||||
let _rsp: ArpEntry = self
|
let _rsp: ArpEntry = self
|
||||||
|
.api
|
||||||
.req(Method::DELETE, &format!("/rest/ip/arp/{id}"), ())
|
.req(Method::DELETE, &format!("/rest/ip/arp/{id}"), ())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
use crate::dns::DnsServer;
|
||||||
use crate::exchange::ExchangeRateService;
|
use crate::exchange::ExchangeRateService;
|
||||||
use crate::lightning::LightningNode;
|
use crate::lightning::LightningNode;
|
||||||
use crate::provisioner::LNVpsProvisioner;
|
use crate::provisioner::LNVpsProvisioner;
|
||||||
use crate::router::{MikrotikRouter, Router};
|
use crate::router::Router;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::Result;
|
||||||
use lnvps_db::LNVpsDb;
|
use lnvps_db::LNVpsDb;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@ -56,7 +57,7 @@ pub enum LightningConfig {
|
|||||||
Bitvora {
|
Bitvora {
|
||||||
token: String,
|
token: String,
|
||||||
webhook_secret: String,
|
webhook_secret: String,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
@ -68,32 +69,22 @@ pub struct NostrConfig {
|
|||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum RouterConfig {
|
pub enum RouterConfig {
|
||||||
Mikrotik(ApiConfig),
|
Mikrotik {
|
||||||
|
url: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum DnsServerConfig {
|
pub enum DnsServerConfig {
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
Cloudflare { api: ApiConfig, zone_id: String },
|
Cloudflare {
|
||||||
}
|
token: String,
|
||||||
|
forward_zone_id: String,
|
||||||
/// Generic remote API credentials
|
reverse_zone_id: String,
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
},
|
||||||
pub struct ApiConfig {
|
|
||||||
/// unique ID of this router, used in references
|
|
||||||
pub id: String,
|
|
||||||
/// http://<my-router>
|
|
||||||
pub url: String,
|
|
||||||
/// Login credentials used for this router
|
|
||||||
pub credentials: Credentials,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub enum Credentials {
|
|
||||||
UsernamePassword { username: String, password: String },
|
|
||||||
ApiToken { token: String },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Policy that determines how packets arrive at the VM
|
/// Policy that determines how packets arrive at the VM
|
||||||
@ -187,26 +178,52 @@ impl Settings {
|
|||||||
Arc::new(LNVpsProvisioner::new(self.clone(), db, node, exchange))
|
Arc::new(LNVpsProvisioner::new(self.clone(), db, node, exchange))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
pub fn get_router(&self) -> Result<Option<Arc<dyn Router>>> {
|
pub fn get_router(&self) -> Result<Option<Arc<dyn Router>>> {
|
||||||
match &self.router {
|
#[cfg(test)]
|
||||||
Some(RouterConfig::Mikrotik(api)) => match &api.credentials {
|
{
|
||||||
Credentials::UsernamePassword { username, password } => Ok(Some(Arc::new(
|
if let Some(router) = &self.router {
|
||||||
MikrotikRouter::new(&api.url, username, password),
|
let router = crate::mocks::MockRouter::new(self.network_policy.clone());
|
||||||
))),
|
Ok(Some(Arc::new(router)))
|
||||||
_ => bail!("Only username/password is supported for Mikrotik routers"),
|
} else {
|
||||||
},
|
Ok(None)
|
||||||
_ => Ok(None),
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(test))]
|
||||||
|
{
|
||||||
|
match &self.router {
|
||||||
|
#[cfg(feature = "mikrotik")]
|
||||||
|
Some(RouterConfig::Mikrotik {
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}) => Ok(Some(Arc::new(crate::router::MikrotikRouter::new(
|
||||||
|
url, username, password,
|
||||||
|
)))),
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
pub fn get_dns(&self) -> Result<Option<Arc<dyn DnsServer>>> {
|
||||||
pub fn get_router(&self) -> Result<Option<Arc<dyn Router>>> {
|
#[cfg(test)]
|
||||||
if self.router.is_some() {
|
{
|
||||||
let router = crate::mocks::MockRouter::new(self.network_policy.clone());
|
Ok(Some(Arc::new(crate::mocks::MockDnsServer::new())))
|
||||||
Ok(Some(Arc::new(router)))
|
}
|
||||||
} else {
|
#[cfg(not(test))]
|
||||||
Ok(None)
|
{
|
||||||
|
match &self.dns {
|
||||||
|
None => Ok(None),
|
||||||
|
#[cfg(feature = "cloudflare")]
|
||||||
|
Some(DnsServerConfig::Cloudflare {
|
||||||
|
token,
|
||||||
|
forward_zone_id,
|
||||||
|
reverse_zone_id,
|
||||||
|
}) => Ok(Some(Arc::new(crate::dns::Cloudflare::new(
|
||||||
|
token,
|
||||||
|
reverse_zone_id,
|
||||||
|
forward_zone_id,
|
||||||
|
)))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user