diff --git a/.gitignore b/.gitignore index 573792f..2390d92 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/target -.idea/ \ No newline at end of file +.idea/ +*.config.yaml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 13a3e0d..13bf2b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.91" @@ -474,6 +523,52 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clap" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1695,6 +1790,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -1781,6 +1882,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "chrono", + "clap", "config", "fedimint-tonic-lnd", "ipnetwork", @@ -4098,6 +4200,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 459d7f6..849d228 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ urlencoding = "2.1.3" fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] } ipnetwork = "0.20.0" rand = "0.8.5" +clap = { version = "4.5.21", features = ["derive"] } diff --git a/config.yaml b/config.yaml index 16d23bb..2d160f7 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,3 @@ -# MySQL database connection string db: "mysql://root:root@localhost:3376/lnvps" lnd: url: "https://127.0.0.1:10003" diff --git a/lnvps_db/migrations/20241127163556_networking.sql b/lnvps_db/migrations/20241127163556_networking.sql new file mode 100644 index 0000000..2d5a5af --- /dev/null +++ b/lnvps_db/migrations/20241127163556_networking.sql @@ -0,0 +1,4 @@ +alter table ip_range + add column gateway varchar(255) not null; +alter table vm + add column mac_address varchar(20) not null; diff --git a/lnvps_db/src/hydrate.rs b/lnvps_db/src/hydrate.rs index 37d574e..9b2278d 100644 --- a/lnvps_db/src/hydrate.rs +++ b/lnvps_db/src/hydrate.rs @@ -1,4 +1,4 @@ -use crate::{LNVpsDb, Vm, VmTemplate}; +use crate::{LNVpsDb, Vm, VmIpAssignment, VmTemplate}; use anyhow::Result; use async_trait::async_trait; use std::ops::Deref; @@ -49,3 +49,15 @@ impl + Sync> Hydrate for VmTemplate { todo!() } } + +#[async_trait] +impl + Sync> Hydrate for VmIpAssignment { + async fn hydrate_up(&mut self, db: &D) -> Result<()> { + self.ip_range = Some(db.get_ip_range(self.ip_range_id).await?); + Ok(()) + } + + async fn hydrate_down(&mut self, db: &D) -> Result<()> { + todo!() + } +} diff --git a/lnvps_db/src/lib.rs b/lnvps_db/src/lib.rs index d0ffb1e..408582d 100644 --- a/lnvps_db/src/lib.rs +++ b/lnvps_db/src/lib.rs @@ -60,6 +60,9 @@ pub trait LNVpsDb: Sync + Send { /// List available OS images async fn list_os_image(&self) -> Result>; + /// List available IP Ranges + async fn get_ip_range(&self, id: u64) -> Result; + /// List available IP Ranges async fn list_ip_range(&self) -> Result>; diff --git a/lnvps_db/src/model.rs b/lnvps_db/src/model.rs index 8861eb0..7774853 100644 --- a/lnvps_db/src/model.rs +++ b/lnvps_db/src/model.rs @@ -149,6 +149,7 @@ impl VmOsImage { pub struct IpRange { pub id: u64, pub cidr: String, + pub gateway: String, pub enabled: bool, pub region_id: u64, } @@ -225,6 +226,8 @@ pub struct Vm { pub disk_size: u64, /// The [VmHostDisk] this VM is on pub disk_id: u64, + /// Network MAC address + pub mac_address: String, #[sqlx(skip)] #[serde(skip_serializing_if = "Option::is_none")] @@ -244,12 +247,16 @@ pub struct Vm { pub ip_assignments: Option>, } -#[derive(Serialize, Deserialize, FromRow, Clone, Debug)] +#[derive(Serialize, Deserialize, FromRow, Clone, Debug, Default)] pub struct VmIpAssignment { pub id: u64, pub vm_id: u64, pub ip_range_id: u64, pub ip: String, + + #[sqlx(skip)] + #[serde(skip_serializing_if = "Option::is_none")] + pub ip_range: Option, } #[serde_as] diff --git a/lnvps_db/src/mysql.rs b/lnvps_db/src/mysql.rs index ab923d5..ee58b27 100644 --- a/lnvps_db/src/mysql.rs +++ b/lnvps_db/src/mysql.rs @@ -152,6 +152,14 @@ impl LNVpsDb for LNVpsDbMysql { .map_err(Error::new) } + async fn get_ip_range(&self, id: u64) -> Result { + sqlx::query_as("select * from ip_range where id=?") + .bind(id) + .fetch_one(&self.db) + .await + .map_err(Error::new) + } + async fn list_ip_range(&self) -> Result> { sqlx::query_as("select * from ip_range") .fetch_all(&self.db) @@ -206,7 +214,7 @@ impl LNVpsDb for LNVpsDbMysql { } async fn insert_vm(&self, vm: &Vm) -> Result { - Ok(sqlx::query("insert into vm(host_id,user_id,image_id,template_id,ssh_key_id,created,expires,cpu,memory,disk_size,disk_id) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) returning id") + Ok(sqlx::query("insert into vm(host_id,user_id,image_id,template_id,ssh_key_id,created,expires,cpu,memory,disk_size,disk_id,mac_address) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) returning id") .bind(vm.host_id) .bind(vm.user_id) .bind(vm.image_id) @@ -218,6 +226,7 @@ impl LNVpsDb for LNVpsDbMysql { .bind(vm.memory) .bind(vm.disk_size) .bind(vm.disk_id) + .bind(&vm.mac_address) .fetch_one(&self.db) .await .map_err(Error::new)? diff --git a/src/bin/api.rs b/src/bin/api.rs index 5a13940..0d53b4b 100644 --- a/src/bin/api.rs +++ b/src/bin/api.rs @@ -1,4 +1,5 @@ use anyhow::Error; +use clap::Parser; use config::{Config, File}; use fedimint_tonic_lnd::connect; use lnvps::api; @@ -14,12 +15,20 @@ use log::error; use std::net::{IpAddr, SocketAddr}; use std::time::Duration; +#[derive(Parser)] +#[clap(about, version, author)] +struct Args { + #[clap(short, long)] + config: Option, +} + #[rocket::main] async fn main() -> Result<(), Error> { pretty_env_logger::init(); + let args = Args::parse(); let settings: Settings = Config::builder() - .add_source(File::with_name("config.yaml")) + .add_source(File::with_name(&args.config.unwrap_or("config.yaml".to_string()))) .build()? .try_deserialize()?; diff --git a/src/lib.rs b/src/lib.rs index 5bdb162..afdd874 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,3 +8,4 @@ pub mod provisioner; pub mod settings; pub mod status; pub mod worker; +pub mod router; diff --git a/src/provisioner/lnvps.rs b/src/provisioner/lnvps.rs index 22315ba..aeb6f7d 100644 --- a/src/provisioner/lnvps.rs +++ b/src/provisioner/lnvps.rs @@ -17,6 +17,8 @@ use lnvps_db::{ VmPayment, }; use log::{error, info, warn}; +use nostr::util::hex; +use rand::random; use rand::seq::IteratorRandom; use reqwest::Url; use std::collections::HashSet; @@ -82,17 +84,22 @@ impl Provisioner for LNVpsProvisioner { for image in self.db.list_os_image().await? { info!("Downloading image {} on {}", image.url, host.name); let i_name = image.filename()?; - if files.iter().any(|v| v.vol_id.ends_with(&format!("iso/{i_name}"))) { + if files + .iter() + .any(|v| v.vol_id.ends_with(&format!("iso/{i_name}"))) + { info!("Already downloaded, skipping"); continue; } - let t_download = client.download_image(DownloadUrlRequest { - content: StorageContent::ISO, - node: host.name.clone(), - storage: iso_storage.clone(), - url: image.url.clone(), - filename: i_name, - }).await?; + let t_download = client + .download_image(DownloadUrlRequest { + content: StorageContent::ISO, + node: host.name.clone(), + storage: iso_storage.clone(), + url: image.url.clone(), + filename: i_name, + }) + .await?; client.wait_for_task(&t_download).await?; } } @@ -137,6 +144,12 @@ impl Provisioner for LNVpsProvisioner { memory: template.memory, disk_size: template.disk_size, disk_id: pick_disk.id, + mac_address: format!( + "bc:24:11:{}:{}:{}", + hex::encode(&[random::()]), + hex::encode(&[random::()]), + hex::encode(&[random::()]) + ), ..Default::default() }; @@ -254,6 +267,7 @@ impl Provisioner for LNVpsProvisioner { vm_id, ip_range_id: range.id, ip: IpNetwork::new(ip, range_cidr.prefix())?.to_string(), + ..Default::default() }; let id = self.db.insert_vm_ip_assignment(&assignment).await?; assignment.id = id; @@ -280,12 +294,23 @@ impl Provisioner for LNVpsProvisioner { ips = self.allocate_ips(vm.id).await?; } + // load ranges + for ip in &mut ips { + ip.hydrate_up(&self.db).await?; + } + let mut ip_config = ips .iter() .map_while(|ip| { if let Ok(net) = ip.ip.parse::() { Some(match net { - IpNetwork::V4(addr) => format!("ip={}", addr), + IpNetwork::V4(addr) => { + format!( + "ip={},gw={}", + addr, + ip.ip_range.as_ref().map(|r| &r.gateway).unwrap() + ) + } IpNetwork::V6(addr) => format!("ip6={}", addr), }) } else { @@ -305,7 +330,7 @@ impl Provisioner for LNVpsProvisioner { let ssh_key = self.db.get_user_ssh_key(vm.ssh_key_id).await?; let mut net = vec![ - "virtio".to_string(), + format!("virtio={}", vm.mac_address), format!("bridge={}", self.config.bridge), ]; if let Some(t) = self.config.vlan { diff --git a/src/router/mikrotik.rs b/src/router/mikrotik.rs new file mode 100644 index 0000000..3163389 --- /dev/null +++ b/src/router/mikrotik.rs @@ -0,0 +1,25 @@ +use std::net::IpAddr; +use lnvps_db::VmIpAssignment; +use rocket::async_trait; +use crate::router::Router; + +pub struct MikrotikRouter { + url: String, + token: String, +} + +impl MikrotikRouter { + pub fn new(url: &str, token: &str) -> Self { + Self { + url: url.to_string(), + token: token.to_string(), + } + } +} + +#[async_trait] +impl Router for MikrotikRouter { + async fn add_arp_entry(&self, ip: IpAddr, mac: &[u8; 6], comment: Option<&str>) -> anyhow::Result<()> { + todo!() + } +} diff --git a/src/router/mod.rs b/src/router/mod.rs new file mode 100644 index 0000000..15a8468 --- /dev/null +++ b/src/router/mod.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use rocket::async_trait; +use std::net::IpAddr; + +/// Router defines a network device used to access the hosts +/// +/// In our infrastructure we use this to add static ARP entries on the router +/// for every IP assignment, this way we don't need to have a ton of ARP requests on the +/// VM network because of people doing IP scanning +/// +/// It also prevents people from re-assigning their IP to another in the range, +#[async_trait] +pub trait Router { + async fn add_arp_entry(&self, ip: IpAddr, mac: &[u8; 6], comment: Option<&str>) -> Result<()>; +} + +mod mikrotik; +pub use mikrotik::*;