feat: assign mac / gateway

This commit is contained in:
kieran 2024-11-29 11:18:02 +00:00
parent 1cc4d40082
commit 06d8339653
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
14 changed files with 238 additions and 16 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
**/target
.idea/
.idea/
*.config.yaml

108
Cargo.lock generated
View File

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

View File

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

View File

@ -1,4 +1,3 @@
# MySQL database connection string
db: "mysql://root:root@localhost:3376/lnvps"
lnd:
url: "https://127.0.0.1:10003"

View File

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

View File

@ -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<D: Deref<Target = dyn LNVpsDb> + Sync> Hydrate<D> for VmTemplate {
todo!()
}
}
#[async_trait]
impl<D: Deref<Target = dyn LNVpsDb> + Sync> Hydrate<D> 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!()
}
}

View File

@ -60,6 +60,9 @@ pub trait LNVpsDb: Sync + Send {
/// List available OS images
async fn list_os_image(&self) -> Result<Vec<VmOsImage>>;
/// List available IP Ranges
async fn get_ip_range(&self, id: u64) -> Result<IpRange>;
/// List available IP Ranges
async fn list_ip_range(&self) -> Result<Vec<IpRange>>;

View File

@ -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<Vec<VmIpAssignment>>,
}
#[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<IpRange>,
}
#[serde_as]

View File

@ -152,6 +152,14 @@ impl LNVpsDb for LNVpsDbMysql {
.map_err(Error::new)
}
async fn get_ip_range(&self, id: u64) -> Result<IpRange> {
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<Vec<IpRange>> {
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<u64> {
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)?

View File

@ -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<String>,
}
#[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()?;

View File

@ -8,3 +8,4 @@ pub mod provisioner;
pub mod settings;
pub mod status;
pub mod worker;
pub mod router;

View File

@ -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::<u8>()]),
hex::encode(&[random::<u8>()]),
hex::encode(&[random::<u8>()])
),
..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::<IpNetwork>() {
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 {

25
src/router/mikrotik.rs Normal file
View File

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

18
src/router/mod.rs Normal file
View File

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