Initial commit
This commit is contained in:
commit
4c02261015
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
.DS_Store
|
||||
.vscode
|
2332
Cargo.lock
generated
Normal file
2332
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
6
Cargo.toml
Normal file
6
Cargo.toml
Normal file
|
@ -0,0 +1,6 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"nzrd",
|
||||
"api",
|
||||
"client",
|
||||
]
|
14
api/Cargo.toml
Normal file
14
api/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "nzr-api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
figment = { version = "0.10.8", features = ["json", "toml", "env"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tarpc = { version = "0.31", features = ["tokio1", "unix"] }
|
||||
tokio = { version = "1.0", features = ["macros"] }
|
||||
uuid = "1.2.2"
|
||||
trust-dns-proto = { version = "0.22.0", features = ["serde-config"] }
|
27
api/src/args.rs
Normal file
27
api/src/args.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::net::cidr::CidrV4;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NewInstance {
|
||||
pub name: String,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub interface: String,
|
||||
pub base_image: String,
|
||||
pub cores: u8,
|
||||
pub memory: u32,
|
||||
pub disk_sizes: (u32, Option<u32>),
|
||||
pub ssh_keys: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NewSubnet {
|
||||
pub if_name: String,
|
||||
pub network: CidrV4,
|
||||
pub start_addr: Option<Ipv4Addr>,
|
||||
pub end_addr: Option<Ipv4Addr>,
|
||||
pub gateway: Option<Ipv4Addr>,
|
||||
pub dns: Vec<Ipv4Addr>,
|
||||
}
|
112
api/src/config.rs
Normal file
112
api/src/config.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use figment::{
|
||||
providers::{Env, Format, Json, Toml},
|
||||
Figment, Metadata, Provider,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use trust_dns_proto::rr::Name;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct StorageConfig {
|
||||
pub primary_pool: String,
|
||||
pub secondary_pool: String,
|
||||
pub ci_image_pool: String,
|
||||
pub base_image_pool: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SOAConfig {
|
||||
pub nzr_domain: Name,
|
||||
pub contact: Name,
|
||||
pub refresh: i32,
|
||||
pub retry: i32,
|
||||
pub expire: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DNSConfig {
|
||||
pub listen_addr: String,
|
||||
pub default_zone: Name,
|
||||
pub soa: SOAConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RPCConfig {
|
||||
pub socket_path: PathBuf,
|
||||
pub admin_group: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub rpc: RPCConfig,
|
||||
pub log_level: String,
|
||||
pub db_path: PathBuf,
|
||||
pub libvirt_uri: String,
|
||||
pub storage: StorageConfig,
|
||||
pub dns: DNSConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
log_level: "WARN".to_owned(),
|
||||
rpc: RPCConfig {
|
||||
socket_path: PathBuf::from("/var/run/nazrin/nzrd.sock"),
|
||||
admin_group: None,
|
||||
},
|
||||
db_path: PathBuf::from("/var/run/nazrin/nzr.db"),
|
||||
libvirt_uri: match std::env::var("LIBVIRT_URI") {
|
||||
Ok(v) => v,
|
||||
Err(_) => String::from("qemu:///system"),
|
||||
},
|
||||
storage: StorageConfig {
|
||||
primary_pool: "pri".to_owned(),
|
||||
secondary_pool: "data".to_owned(),
|
||||
ci_image_pool: "cidata".to_owned(),
|
||||
base_image_pool: "images".to_owned(),
|
||||
},
|
||||
dns: DNSConfig {
|
||||
listen_addr: "127.0.0.1:5353".to_owned(),
|
||||
default_zone: Name::from_utf8("servers.local").unwrap(),
|
||||
soa: SOAConfig {
|
||||
nzr_domain: Name::from_utf8("nzr.local").unwrap(),
|
||||
contact: Name::from_utf8("admin.nzr.local").unwrap(),
|
||||
refresh: 86400,
|
||||
retry: 7200,
|
||||
expire: 3_600_000,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for Config {
|
||||
fn metadata(&self) -> figment::Metadata {
|
||||
Metadata::named("Nazrin config")
|
||||
}
|
||||
|
||||
fn data(
|
||||
&self,
|
||||
) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
|
||||
figment::providers::Serialized::defaults(Config::default()).data()
|
||||
}
|
||||
|
||||
fn profile(&self) -> Option<figment::Profile> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn figment() -> Figment {
|
||||
let mut fig = Figment::from(Config::default()).merge(Toml::file("/etc/nazrin.conf"));
|
||||
|
||||
#[allow(deprecated)]
|
||||
if let Some(mut home) = std::env::home_dir() {
|
||||
home.push(".nazrin.conf");
|
||||
fig = fig.merge(Json::file(home));
|
||||
}
|
||||
|
||||
fig.merge(Env::prefixed("NZR_"))
|
||||
}
|
||||
}
|
33
api/src/lib.rs
Normal file
33
api/src/lib.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use model::{Instance, Subnet};
|
||||
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod model;
|
||||
pub mod net;
|
||||
|
||||
pub use trust_dns_proto;
|
||||
|
||||
#[tarpc::service]
|
||||
pub trait Nazrin {
|
||||
/// Creates a new instance.
|
||||
async fn new_instance(build_args: args::NewInstance) -> Result<Instance, String>;
|
||||
/// Deletes an existing instance.
|
||||
///
|
||||
/// This should involve deleting all related disks and clearing
|
||||
/// the lease information from the subnet data, if any.
|
||||
async fn delete_instance(name: String) -> Result<(), String>;
|
||||
/// Gets a list of existing instances.
|
||||
async fn get_instances() -> Result<Vec<Instance>, String>;
|
||||
/// Cleans up unusable entries in the database.
|
||||
async fn garbage_collect() -> Result<(), String>;
|
||||
/// Creates a new subnet.
|
||||
///
|
||||
/// Unlike instances, subnets shouldn't perform any changes to the
|
||||
/// interfaces they reference. This should be used primarily for
|
||||
/// ease-of-use and bookkeeping (e.g., assigning dynamic leases).
|
||||
async fn new_subnet(build_args: Subnet) -> Result<Subnet, String>;
|
||||
/// Gets a list of existing subnets.
|
||||
async fn get_subnets() -> Result<Vec<Subnet>, String>;
|
||||
/// Deletes an existing subnet.
|
||||
async fn delete_subnet(interface: String) -> Result<(), String>;
|
||||
}
|
162
api/src/model.rs
Normal file
162
api/src/model.rs
Normal file
|
@ -0,0 +1,162 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt, net::Ipv4Addr, str::FromStr};
|
||||
use trust_dns_proto::rr::Name;
|
||||
|
||||
use crate::net::{cidr::CidrV4, mac::MacAddr};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[repr(u32)]
|
||||
pub enum DomainState {
|
||||
NoState = 0u32,
|
||||
Running,
|
||||
Blocked,
|
||||
Paused,
|
||||
ShuttingDown,
|
||||
ShutOff,
|
||||
Crashed,
|
||||
Suspended,
|
||||
Unknown(u32),
|
||||
}
|
||||
|
||||
impl Default for DomainState {
|
||||
fn default() -> Self {
|
||||
Self::NoState
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for DomainState {
|
||||
fn from(value: u32) -> Self {
|
||||
match value {
|
||||
0 => Self::NoState,
|
||||
1 => Self::Running,
|
||||
2 => Self::Blocked,
|
||||
3 => Self::Paused,
|
||||
4 => Self::ShuttingDown,
|
||||
5 => Self::ShutOff,
|
||||
6 => Self::Crashed,
|
||||
7 => Self::Suspended,
|
||||
other => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DomainState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::NoState => "no state".to_owned(),
|
||||
Self::Running => "running".to_owned(),
|
||||
Self::Blocked => "blocked".to_owned(),
|
||||
Self::Paused => "paused".to_owned(),
|
||||
Self::ShuttingDown => "shutting down".to_owned(),
|
||||
Self::ShutOff => "shut off".to_owned(),
|
||||
Self::Crashed => "crashed".to_owned(),
|
||||
Self::Suspended => "suspended".to_owned(),
|
||||
Self::Unknown(code) => format!("unknown ({})", code),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a VM instance.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Instance {
|
||||
pub name: String,
|
||||
pub uuid: uuid::Uuid,
|
||||
pub lease: Option<Lease>,
|
||||
pub state: DomainState,
|
||||
}
|
||||
|
||||
/// Struct representing a logical "lease" held by a VM.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Lease {
|
||||
/// The IPv4 address held by the Lease
|
||||
pub addr: CidrV4,
|
||||
/// The MAC address associated by the Lease
|
||||
pub mac_addr: MacAddr,
|
||||
}
|
||||
|
||||
/// Struct representing a subnet used by the host for virtual
|
||||
/// networking.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Subnet {
|
||||
/// The name of the interface the subnet is accessible via.
|
||||
pub ifname: IfaceStr,
|
||||
pub data: SubnetData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SubnetData {
|
||||
/// The network information for the subnet.
|
||||
pub network: CidrV4,
|
||||
/// The first host address that can be assigned dynamically
|
||||
/// on the subnet.
|
||||
pub start_host: Ipv4Addr,
|
||||
/// The last host address that can be assigned dynamically
|
||||
/// on the subnet.
|
||||
pub end_host: Ipv4Addr,
|
||||
/// The default gateway for the subnet.
|
||||
pub gateway4: Option<Ipv4Addr>,
|
||||
/// The primary DNS server for the subnet.
|
||||
pub dns: Vec<Ipv4Addr>,
|
||||
/// The base domain used for DNS lookup.
|
||||
pub domain_name: Option<Name>,
|
||||
}
|
||||
|
||||
/// A wrapper struct for [u8; 16], representing the maximum length
|
||||
/// for an interface's name.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct IfaceStr {
|
||||
name: [u8; 16],
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for IfaceStr {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum ParseError {
|
||||
BadSize(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Interface name must be at most 15 characters")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseError {}
|
||||
|
||||
impl FromStr for IfaceStr {
|
||||
type Err = ParseError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.bytes().len() > 15 {
|
||||
Err(Self::Err::BadSize(s.to_owned()))
|
||||
} else {
|
||||
Ok(IfaceStr {
|
||||
name: {
|
||||
let mut ifstr = [0u8; 16];
|
||||
let bytes = s.as_bytes();
|
||||
ifstr[..bytes.len()].copy_from_slice(&bytes[..bytes.len()]);
|
||||
ifstr
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for IfaceStr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
std::str::from_utf8(&self.name)
|
||||
.map(|a| a.trim_end_matches(char::from(0)))
|
||||
.map_err(|_| fmt::Error)?
|
||||
)
|
||||
}
|
||||
}
|
257
api/src/net/cidr.rs
Normal file
257
api/src/net/cidr.rs
Normal file
|
@ -0,0 +1,257 @@
|
|||
use std::fmt;
|
||||
use std::net::{AddrParseError, Ipv4Addr};
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{de, Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Malformed,
|
||||
AddrParse(AddrParseError),
|
||||
SuffixParse(ParseIntError),
|
||||
InvalidSize,
|
||||
HostBitsTooLarge,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Malformed => write!(f, "Malformed address/cidr combination"),
|
||||
Self::AddrParse(er) => write!(f, "Couldn't parse address: {}", er),
|
||||
Self::SuffixParse(er) => write!(f, "Couldn't parse CIDR suffix: {}", er),
|
||||
Self::InvalidSize => write!(f, "Byte array needs to be at least 5 bytes long"),
|
||||
Self::HostBitsTooLarge => write!(
|
||||
f,
|
||||
"Provided host does not match bits allowed by subnet mask"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct CidrV4 {
|
||||
pub addr: Ipv4Addr,
|
||||
cidr: u8,
|
||||
netmask: u32,
|
||||
}
|
||||
|
||||
impl fmt::Display for CidrV4 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}/{}", self.addr, self.cidr)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CidrV4 {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let parts = s.split('/').collect::<Vec<&str>>();
|
||||
|
||||
match parts.len() {
|
||||
2 => {
|
||||
let addr = Ipv4Addr::from_str(parts[0]).map_err(Error::AddrParse)?;
|
||||
let cidr = u8::from_str(parts[1]).map_err(Error::SuffixParse)?;
|
||||
Ok(CidrV4::new(addr, cidr))
|
||||
}
|
||||
_ => Err(Error::Malformed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for CidrV4 {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CidrV4 {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s: String = Deserialize::deserialize(deserializer)?;
|
||||
CidrV4::from_str(s.as_str()).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 5]> for CidrV4 {
|
||||
fn from(octets: [u8; 5]) -> Self {
|
||||
let addr = Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]);
|
||||
let cidr = octets[4];
|
||||
Self::new(addr, cidr)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for CidrV4 {
|
||||
type Error = Error;
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
if value.len() < 5 {
|
||||
Err(Error::InvalidSize)
|
||||
} else {
|
||||
// unwrap should be fine here, since we already validate for size?
|
||||
let arr: [u8; 5] = value[0..5].try_into().unwrap();
|
||||
Ok(Self::from(arr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CidrV4 {
|
||||
pub fn new(addr: Ipv4Addr, cidr: u8) -> Self {
|
||||
let netmask = match cidr {
|
||||
0 => 0,
|
||||
32.. => u32::MAX,
|
||||
c => ((1u32 << c) - 1) << (32 - c),
|
||||
};
|
||||
|
||||
CidrV4 {
|
||||
addr,
|
||||
cidr,
|
||||
netmask,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the subnet mask as an Ipv4Addr.
|
||||
pub fn netmask(&self) -> Ipv4Addr {
|
||||
Ipv4Addr::from(self.netmask)
|
||||
}
|
||||
|
||||
/// Get the network address.
|
||||
pub fn network(&self) -> CidrV4 {
|
||||
CidrV4::new(
|
||||
Ipv4Addr::from(u32::from(self.addr) & self.netmask),
|
||||
self.cidr,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the network bit length in CIDR notation.
|
||||
pub fn cidr(&self) -> u8 {
|
||||
self.cidr
|
||||
}
|
||||
|
||||
/// Get the broadcast address for the network.
|
||||
pub fn broadcast(&self) -> Ipv4Addr {
|
||||
Ipv4Addr::from(u32::from(self.network().addr) | !self.netmask)
|
||||
}
|
||||
|
||||
/// Determine if a network contains a given address.
|
||||
///
|
||||
/// This method is not affected by the object's host address.
|
||||
pub fn contains(&self, addr: &Ipv4Addr) -> bool {
|
||||
if self.cidr == 32 {
|
||||
&self.addr == addr
|
||||
} else {
|
||||
&self.broadcast() > addr && addr > &self.network().addr
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets only the host bits for a given Ipv4 address.
|
||||
///
|
||||
/// This method is not affected by the object's host address.
|
||||
pub fn host_bits(&self, addr: &Ipv4Addr) -> u32 {
|
||||
let addr_bits = u32::from(*addr);
|
||||
addr_bits & !self.netmask
|
||||
}
|
||||
|
||||
/// Create an IP from a given u32 of host bits.
|
||||
///
|
||||
/// This method is not affected by the object's host address.
|
||||
pub fn make_ip(&self, host_bits: u32) -> Result<Ipv4Addr, Error> {
|
||||
if host_bits > !self.netmask {
|
||||
Err(Error::HostBitsTooLarge)
|
||||
} else {
|
||||
Ok((host_bits | u32::from(self.network().addr)).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the address and CIDR bit length as an array of bytes.
|
||||
///
|
||||
/// This is in big endian format, so e.g., 192.168.0.5/24 would
|
||||
/// be returned as `[192, 168, 0, 5, 24]`.
|
||||
pub fn octets(&self) -> [u8; 5] {
|
||||
[self.network().addr.octets().as_slice(), &[self.cidr]]
|
||||
.concat()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::net::cidr::CidrV4;
|
||||
use std::{net::Ipv4Addr, str::FromStr};
|
||||
|
||||
const ADDR_BYTES: u32 = 0xc0_a8_02_a9; // 192.168.2.169
|
||||
#[allow(clippy::unusual_byte_groupings)]
|
||||
const NETWORK_TRUTH_MAP: &[(u8, u32, u32, [u8; 5])] = &[
|
||||
(
|
||||
8,
|
||||
0b11111111_00000000_00000000_00000000,
|
||||
0xc0_00_00_00,
|
||||
[0xc0, 0, 0, 0, 8],
|
||||
),
|
||||
(
|
||||
16,
|
||||
0b11111111_11111111_00000000_00000000,
|
||||
0xc0_a8_00_00,
|
||||
[0xc0, 0xa8, 0, 0, 16],
|
||||
),
|
||||
(
|
||||
25,
|
||||
0b11111111_11111111_11111111_10000000,
|
||||
0xc0_a8_02_80,
|
||||
[0xc0, 0xa8, 0x02, 0x80, 25],
|
||||
),
|
||||
(0, 0, 0, [0u8; 5]),
|
||||
(
|
||||
32,
|
||||
0b11111111_11111111_11111111_11111111,
|
||||
0xc0_a8_02_a9,
|
||||
[0xc0, 0xa8, 0x02, 0xa9, 32],
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn v4_constructor() {
|
||||
for (cidr, netmask, net_addr, _) in NETWORK_TRUTH_MAP {
|
||||
let test = CidrV4::new(Ipv4Addr::from(ADDR_BYTES), *cidr);
|
||||
assert_eq!(u32::from(test.netmask()), *netmask);
|
||||
assert_eq!(test.cidr(), *cidr);
|
||||
assert_eq!(u32::from(test.network().addr), *net_addr);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v4_fromstr() {
|
||||
let addr = Ipv4Addr::from(ADDR_BYTES);
|
||||
for (cidr, _netmask, net_addr, _) in NETWORK_TRUTH_MAP {
|
||||
let addr_str = format!("{}/{}", addr, cidr);
|
||||
println!(">> {}", addr_str);
|
||||
let cidr = CidrV4::from_str(&addr_str).unwrap();
|
||||
assert_eq!(cidr.network().addr, Ipv4Addr::from(*net_addr));
|
||||
assert!(cidr.contains(&addr));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v4_contains() {
|
||||
let yes = Ipv4Addr::from(ADDR_BYTES);
|
||||
let no = Ipv4Addr::from(0x08_08_08_08);
|
||||
|
||||
let cidr = CidrV4::new(yes, 8);
|
||||
assert!(cidr.contains(&yes));
|
||||
assert!(!cidr.contains(&no));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v4_octets() {
|
||||
for (cidr, _, _, netbytes) in NETWORK_TRUTH_MAP {
|
||||
let test = CidrV4::new(Ipv4Addr::from(ADDR_BYTES), *cidr);
|
||||
assert_eq!(&test.octets(), netbytes);
|
||||
}
|
||||
}
|
||||
}
|
96
api/src/net/mac.rs
Normal file
96
api/src/net/mac.rs
Normal file
|
@ -0,0 +1,96 @@
|
|||
use std::{fmt, str::FromStr};
|
||||
|
||||
use serde::{de, Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MacAddr {
|
||||
octets: [u8; 6],
|
||||
}
|
||||
|
||||
impl Serialize for MacAddr {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MacAddr {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s: String = Deserialize::deserialize(deserializer)?;
|
||||
MacAddr::from_str(s.as_str()).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
ParseError(std::num::ParseIntError),
|
||||
SizeError(usize),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ParseError(er) => write!(f, "Couldn't parse octets: {}", er),
|
||||
Self::SizeError(sz) => write!(f, "Too many octets; expected 6, string had {}", sz),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl fmt::Display for MacAddr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
self.octets.map(|oct| format!("{:02x}", oct)).join(":")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MacAddr {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let octets = s
|
||||
.split(':')
|
||||
.map(|s| u8::from_str_radix(s, 16))
|
||||
.collect::<Result<Vec<u8>, std::num::ParseIntError>>()
|
||||
.map_err(Error::ParseError)?;
|
||||
Ok(MacAddr {
|
||||
octets: octets
|
||||
.try_into()
|
||||
.map_err(|v: Vec<u8>| Error::SizeError(v.len()))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MacAddr {
|
||||
pub fn new(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8) -> MacAddr {
|
||||
MacAddr {
|
||||
octets: [a, b, c, d, e, f],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes<T>(value: T) -> Result<MacAddr, Error>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
let slice = value.as_ref();
|
||||
if slice.len() < 6 {
|
||||
Err(Error::SizeError(slice.len()))
|
||||
} else {
|
||||
Ok(MacAddr {
|
||||
octets: slice[0..6].try_into().unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn octets(&self) -> [u8; 6] {
|
||||
self.octets
|
||||
}
|
||||
}
|
2
api/src/net/mod.rs
Normal file
2
api/src/net/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod cidr;
|
||||
pub mod mac;
|
15
client/Cargo.toml
Normal file
15
client/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "nzr"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
nzr-api = { path = "../api" }
|
||||
clap = { version = "4.0.26", features = ["derive"] }
|
||||
home = "0.5.4"
|
||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
||||
tokio-serde = { version = "0.8.0", features = ["bincode"] }
|
||||
tarpc = { version = "0.31", features = ["tokio1", "unix", "serde-transport", "serde-transport-bincode"] }
|
||||
tabled = "0.10.0"
|
290
client/src/main.rs
Normal file
290
client/src/main.rs
Normal file
|
@ -0,0 +1,290 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use nzr_api::model;
|
||||
use nzr_api::net::cidr::CidrV4;
|
||||
use nzr_api::trust_dns_proto::rr::Name;
|
||||
use nzr_api::{config, NazrinClient};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use tarpc::tokio_serde::formats::Bincode;
|
||||
use tarpc::tokio_util::codec::LengthDelimitedCodec;
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
mod table;
|
||||
|
||||
#[derive(Debug, clap::Args)]
|
||||
pub struct NewInstanceArgs {
|
||||
/// Name of the instance to be created
|
||||
name: String,
|
||||
/// Bridge the instance will initially run on
|
||||
#[arg(short, long)]
|
||||
interface: String,
|
||||
/// Long description of the instance
|
||||
#[arg(long)]
|
||||
description: Option<String>,
|
||||
/// Base image to use for the instance
|
||||
#[arg(short, long)]
|
||||
base: String,
|
||||
///How many cores to assign to the instance
|
||||
#[arg(short, long, default_value_t = 2)]
|
||||
cores: u8,
|
||||
/// Memory to assign, in MiB
|
||||
#[arg(short, long, default_value_t = 1024)]
|
||||
mem: u32,
|
||||
/// Primary HDD size, in MiB
|
||||
#[arg(short, long, default_value_t = 20)]
|
||||
primary_size: u32,
|
||||
/// Secndary HDD size, in MiB
|
||||
#[arg(short, long)]
|
||||
secondary_size: Option<u32>,
|
||||
/// File containing a list of SSH keys to use
|
||||
#[arg(long)]
|
||||
sshkey_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum InstanceCmd {
|
||||
/// Create a new instance
|
||||
New(NewInstanceArgs),
|
||||
/// Delete an existing instance
|
||||
Delete { name: String },
|
||||
/// List all instances
|
||||
List,
|
||||
/// Deletes all invalid instances from the database
|
||||
Prune,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Args)]
|
||||
pub struct AddNetArgs {
|
||||
/// Name of the bridge interface VMs will attach to
|
||||
pub interface: String,
|
||||
/// Subnet associated with the bridge interface, in CIDR notation (x.x.x.x/y)
|
||||
pub network: String,
|
||||
/// Default gateway for the subnet
|
||||
#[arg(long)]
|
||||
pub gateway: Option<std::net::Ipv4Addr>,
|
||||
/// Default DNS address for the subnet
|
||||
#[arg(long)]
|
||||
pub dns_server: Option<std::net::Ipv4Addr>,
|
||||
/// Start address for IP assignment
|
||||
#[arg(short, long)]
|
||||
pub start_addr: Option<std::net::Ipv4Addr>,
|
||||
/// End address for IP assignment
|
||||
#[arg(short, long)]
|
||||
pub end_addr: Option<std::net::Ipv4Addr>,
|
||||
#[arg(short, long)]
|
||||
pub domain_name: Option<Name>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum NetCmd {
|
||||
/// Add a new network to the database
|
||||
Add(AddNetArgs),
|
||||
/// List all networks in the database
|
||||
List,
|
||||
/// Delete a network from the database
|
||||
Delete {
|
||||
#[arg(short, long)]
|
||||
interface: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
/// Commands for managing instances
|
||||
Instance {
|
||||
#[command(subcommand)]
|
||||
command: InstanceCmd,
|
||||
},
|
||||
/// Commands for managing network assignments
|
||||
Net {
|
||||
#[command(subcommand)]
|
||||
command: NetCmd,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = "")]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CommandError {
|
||||
inner: Option<Box<dyn std::error::Error>>,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CommandError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.message)?;
|
||||
if let Some(inner) = &self.inner {
|
||||
write!(f, "\n inner: {:?}", &inner)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CommandError {}
|
||||
|
||||
impl From<String> for CommandError {
|
||||
fn from(value: String) -> Self {
|
||||
Self {
|
||||
inner: None,
|
||||
message: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for CommandError {
|
||||
fn from(value: &str) -> Self {
|
||||
Self {
|
||||
inner: None,
|
||||
message: value.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandError {
|
||||
fn new<S, E>(message: S, inner: E) -> Self
|
||||
where
|
||||
S: AsRef<str>,
|
||||
E: std::error::Error + 'static,
|
||||
{
|
||||
Self {
|
||||
message: message.as_ref().to_owned(),
|
||||
inner: Some(Box::new(inner)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_command() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Args::parse();
|
||||
let config: config::Config = nzr_api::config::Config::figment().extract()?;
|
||||
let conn = UnixStream::connect(&config.rpc.socket_path).await?;
|
||||
let codec_builder = LengthDelimitedCodec::builder();
|
||||
let transport = tarpc::serde_transport::new(codec_builder.new_framed(conn), Bincode::default());
|
||||
let client = NazrinClient::new(Default::default(), transport).spawn();
|
||||
|
||||
match cli.command {
|
||||
Commands::Instance { command } => {
|
||||
match command {
|
||||
InstanceCmd::New(args) => {
|
||||
let ssh_keys: Vec<String> = {
|
||||
let key_file =
|
||||
args.sshkey_file.map_or_else(
|
||||
|| {
|
||||
home::home_dir().map_or_else(|| {
|
||||
Err(CommandError::from("SSH keyfile not defined, and couldn't find home directory"))
|
||||
}, |hd| {
|
||||
Ok(hd.join(".ssh/authorized_keys"))
|
||||
})
|
||||
},
|
||||
Ok,
|
||||
)?;
|
||||
|
||||
if !key_file.exists() {
|
||||
Err("SSH keyfile doesn't exist".into())
|
||||
} else {
|
||||
match std::fs::read_to_string(&key_file) {
|
||||
Ok(data) => {
|
||||
let keys: Vec<String> =
|
||||
data.split('\n').map(|s| s.trim().to_owned()).collect();
|
||||
Ok(keys)
|
||||
}
|
||||
Err(err) => Err(CommandError::new(
|
||||
format!("Couldn't read {} for SSH keys", &key_file.display()),
|
||||
err,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}?;
|
||||
|
||||
let build_args = nzr_api::args::NewInstance {
|
||||
name: args.name,
|
||||
title: None,
|
||||
description: args.description,
|
||||
interface: args.interface,
|
||||
base_image: args.base,
|
||||
cores: args.cores,
|
||||
memory: args.mem,
|
||||
disk_sizes: (args.primary_size, args.secondary_size),
|
||||
ssh_keys,
|
||||
};
|
||||
let instance = (client
|
||||
.new_instance(tarpc::context::current(), build_args)
|
||||
.await?)?;
|
||||
println!("Instance {} created!", &instance.name);
|
||||
if let Some(lease) = instance.lease {
|
||||
println!(
|
||||
"You should be able to reach it at:\n\n ssh root@{}",
|
||||
lease.addr.addr,
|
||||
);
|
||||
}
|
||||
}
|
||||
InstanceCmd::Delete { name } => {
|
||||
(client
|
||||
.delete_instance(tarpc::context::current(), name)
|
||||
.await?)?;
|
||||
}
|
||||
InstanceCmd::List => {
|
||||
let instances = client.get_instances(tarpc::context::current()).await?;
|
||||
|
||||
let tabular: Vec<table::Instance> =
|
||||
instances?.iter().map(table::Instance::from).collect();
|
||||
let mut table = tabled::Table::new(&tabular);
|
||||
println!("{}", table.with(tabled::Style::psql()));
|
||||
}
|
||||
InstanceCmd::Prune => (client.garbage_collect(tarpc::context::current()).await?)?,
|
||||
}
|
||||
}
|
||||
Commands::Net { command } => match command {
|
||||
NetCmd::Add(args) => {
|
||||
let net_arg = CidrV4::from_str(&args.network)?;
|
||||
let build_args = model::Subnet {
|
||||
ifname: model::IfaceStr::from_str(&args.interface)?,
|
||||
data: model::SubnetData {
|
||||
network: net_arg.clone(),
|
||||
start_host: args.start_addr.unwrap_or(net_arg.make_ip(10)?),
|
||||
end_host: args
|
||||
.end_addr
|
||||
.unwrap_or((u32::from(net_arg.broadcast()) - 1u32).into()),
|
||||
gateway4: args.gateway,
|
||||
dns: args.dns_server.map_or(Vec::new(), |d| vec![d]),
|
||||
domain_name: args.domain_name,
|
||||
},
|
||||
};
|
||||
(client
|
||||
.new_subnet(tarpc::context::current(), build_args)
|
||||
.await?)?;
|
||||
}
|
||||
NetCmd::Delete { interface } => {
|
||||
(client
|
||||
.delete_subnet(tarpc::context::current(), interface)
|
||||
.await?)?;
|
||||
}
|
||||
NetCmd::List => {
|
||||
let subnets = client.get_subnets(tarpc::context::current()).await?;
|
||||
|
||||
let tabular: Vec<table::Subnet> =
|
||||
subnets?.iter().map(table::Subnet::from).collect();
|
||||
let mut table = tabled::Table::new(&tabular);
|
||||
println!("{}", table.with(tabled::Style::psql()));
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Err(err) = handle_command().await {
|
||||
if std::any::Any::type_id(&*err).type_id() == TypeId::of::<tarpc::client::RpcError>() {
|
||||
eprintln!("[err] Error communicating with server: {}", err);
|
||||
} else {
|
||||
eprintln!("[err] {}", err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
42
client/src/table.rs
Normal file
42
client/src/table.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use nzr_api::model;
|
||||
use tabled::Tabled;
|
||||
|
||||
#[derive(Tabled)]
|
||||
pub struct Instance {
|
||||
#[tabled(rename = "Hostname")]
|
||||
hostname: String,
|
||||
#[tabled(rename = "IP Address")]
|
||||
ip_addr: String,
|
||||
#[tabled(rename = "State")]
|
||||
state: model::DomainState,
|
||||
}
|
||||
|
||||
impl From<&model::Instance> for Instance {
|
||||
fn from(value: &model::Instance) -> Self {
|
||||
Self {
|
||||
hostname: value.name.to_owned(),
|
||||
ip_addr: value
|
||||
.lease
|
||||
.as_ref()
|
||||
.map_or("(none)".to_owned(), |lease| lease.addr.to_string()),
|
||||
state: value.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Tabled)]
|
||||
pub struct Subnet {
|
||||
#[tabled(rename = "Interface")]
|
||||
interface: String,
|
||||
#[tabled(rename = "Network")]
|
||||
network: String,
|
||||
}
|
||||
|
||||
impl From<&model::Subnet> for Subnet {
|
||||
fn from(value: &model::Subnet) -> Self {
|
||||
Self {
|
||||
interface: value.ifname.to_string(),
|
||||
network: value.data.network.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
11
deploy.sh
Executable file
11
deploy.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
RHOST="$1"
|
||||
|
||||
if [ -z "$RHOST" ] ; then
|
||||
echo "usage: $0 remote-host" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cargo build --profile $PROFILE && \
|
||||
scp "$PWD/target/${PROFILE:-debug}/nzr"{,d} "$RHOST:"
|
1353
nzrd/Cargo.lock
generated
Normal file
1353
nzrd/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
44
nzrd/Cargo.toml
Normal file
44
nzrd/Cargo.toml
Normal file
|
@ -0,0 +1,44 @@
|
|||
[package]
|
||||
name = "nzrd"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tarpc = { version = "0.31", features = ["tokio1", "unix", "serde-transport", "serde-transport-bincode"] }
|
||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
||||
tokio-serde = { version = "0.8.0", features = ["bincode"] }
|
||||
sled = "0.34.7"
|
||||
# virt = "0.2.11"
|
||||
virt = { path = "../../libvirt-rust" }
|
||||
fatfs = "0.3"
|
||||
uuid = { version = "1.2.2", features = ["v4", "fast-rng", "serde", "macro-diagnostics"] }
|
||||
clap = { version = "4.0.26", features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
# for new @attr support, awaiting 0.27.0
|
||||
quick-xml = { git = "https://github.com/tafia/quick-xml", rev = "fb079b6714d7238d5180aaa098c5f9b02dbcc7da", features = ["serialize"] }
|
||||
serde_with = "2.1.0"
|
||||
serde_yaml = "0.9.14"
|
||||
rand = "0.8.5"
|
||||
libc = "0.2.137"
|
||||
home = "0.5.4"
|
||||
stdext = "0.3.1"
|
||||
zerocopy = "0.6.1"
|
||||
nzr-api = { path = "../api" }
|
||||
futures = "0.3"
|
||||
async-trait = "0.1.60"
|
||||
ciborium = "0.2.0"
|
||||
ciborium-io = "0.2.0"
|
||||
trust-dns-server = "0.22.0"
|
||||
log = "0.4.17"
|
||||
syslog = "6.0.1"
|
||||
nix = "0.26.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3.7"
|
||||
regex = "1"
|
||||
|
||||
[[bin]]
|
||||
name = "nzrd"
|
||||
path = "src/main.rs"
|
192
nzrd/src/cloud.rs
Normal file
192
nzrd/src/cloud.rs
Normal file
|
@ -0,0 +1,192 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use fatfs::FsOptions;
|
||||
use serde::Serialize;
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{prelude::*, Cursor};
|
||||
use trust_dns_server::proto::rr::Name;
|
||||
|
||||
use nzr_api::net::{cidr::CidrV4, mac::MacAddr};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Metadata<'a> {
|
||||
instance_id: &'a str,
|
||||
local_hostname: &'a str,
|
||||
public_keys: Option<Vec<&'a String>>,
|
||||
}
|
||||
|
||||
impl<'a> Metadata<'a> {
|
||||
pub fn new(instance_id: &'a str) -> Self {
|
||||
Self {
|
||||
instance_id,
|
||||
local_hostname: instance_id,
|
||||
public_keys: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ssh_pubkeys(mut self, pubkeys: &'a [String]) -> Self {
|
||||
self.public_keys = Some(pubkeys.iter().filter(|i| !i.is_empty()).collect());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkMeta<'a> {
|
||||
version: u32,
|
||||
ethernets: HashMap<String, EtherNic<'a>>,
|
||||
#[serde(skip)]
|
||||
ethnum: u8,
|
||||
}
|
||||
|
||||
impl<'a> NetworkMeta<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
version: 2,
|
||||
ethernets: HashMap::new(),
|
||||
ethnum: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Define a NIC with a static address.
|
||||
pub fn static_nic(
|
||||
mut self,
|
||||
match_data: EtherMatch<'a>,
|
||||
cidr: &'a CidrV4,
|
||||
gateway: &'a Ipv4Addr,
|
||||
dns: DNSMeta<'a>,
|
||||
) -> Self {
|
||||
self.ethernets.insert(
|
||||
format!("eth{}", self.ethnum),
|
||||
EtherNic {
|
||||
r#match: match_data,
|
||||
addresses: Some(vec![cidr]),
|
||||
gateway4: Some(gateway),
|
||||
dhcp4: false,
|
||||
nameservers: Some(dns),
|
||||
},
|
||||
);
|
||||
self.ethnum += 1;
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn dhcp_nic(mut self, match_data: EtherMatch<'a>) -> Self {
|
||||
self.ethernets.insert(
|
||||
format!("eth{}", self.ethnum),
|
||||
EtherNic {
|
||||
r#match: match_data,
|
||||
addresses: None,
|
||||
gateway4: None,
|
||||
dhcp4: true,
|
||||
nameservers: None,
|
||||
},
|
||||
);
|
||||
self.ethnum += 1;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Ethernets<'a> {
|
||||
nics: Vec<EtherNic<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct EtherNic<'a> {
|
||||
r#match: EtherMatch<'a>,
|
||||
addresses: Option<Vec<&'a CidrV4>>,
|
||||
gateway4: Option<&'a Ipv4Addr>,
|
||||
dhcp4: bool,
|
||||
nameservers: Option<DNSMeta<'a>>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
pub struct EtherMatch<'a> {
|
||||
name: Option<&'a str>,
|
||||
macaddress: Option<&'a MacAddr>,
|
||||
driver: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> EtherMatch<'a> {
|
||||
#[allow(dead_code)]
|
||||
pub fn name(name: &'a str) -> Self {
|
||||
Self {
|
||||
name: Some(name),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mac_addr(addr: &'a MacAddr) -> Self {
|
||||
Self {
|
||||
macaddress: Some(addr),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn driver(driver: &'a str) -> Self {
|
||||
Self {
|
||||
driver: Some(driver),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DNSMeta<'a> {
|
||||
search: Vec<Name>,
|
||||
addresses: &'a Vec<Ipv4Addr>,
|
||||
}
|
||||
|
||||
impl<'a> DNSMeta<'a> {
|
||||
pub fn with_addrs(search: Option<Vec<Name>>, addrs: &'a Vec<Ipv4Addr>) -> Self {
|
||||
Self {
|
||||
addresses: addrs,
|
||||
search: search.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_image<B>(
|
||||
metadata: &Metadata,
|
||||
netconfig: &NetworkMeta,
|
||||
user_data: Option<&B>,
|
||||
) -> Result<Cursor<Vec<u8>>, Box<dyn std::error::Error>>
|
||||
where
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
let mut image: Cursor<Vec<u8>> = Cursor::new(Vec::new());
|
||||
|
||||
// format a:
|
||||
fatfs::format_volume(
|
||||
&mut image,
|
||||
fatfs::FormatVolumeOptions::new()
|
||||
.volume_label(*b"cidata ")
|
||||
.fat_type(fatfs::FatType::Fat12)
|
||||
.total_sectors(2880),
|
||||
)?;
|
||||
|
||||
{
|
||||
let fs = fatfs::FileSystem::new(&mut image, FsOptions::new())?;
|
||||
let rootdir = fs.root_dir();
|
||||
|
||||
let md_data = serde_yaml::to_string(&metadata)?;
|
||||
let mut md_fd = rootdir.create_file("meta-data")?;
|
||||
md_fd.write_all(md_data.as_bytes())?;
|
||||
|
||||
let net_data = serde_yaml::to_string(&netconfig)?;
|
||||
let mut net_fd = rootdir.create_file("network-config")?;
|
||||
net_fd.write_all(net_data.as_bytes())?;
|
||||
|
||||
// user-data MUST exist, even if there is no user-data
|
||||
let mut user_fd = rootdir.create_file("user-data")?;
|
||||
if let Some(user_data) = user_data {
|
||||
user_fd.write_all(user_data.as_ref())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(image)
|
||||
}
|
23
nzrd/src/cmd/mod.rs
Normal file
23
nzrd/src/cmd/mod.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
pub mod net;
|
||||
pub mod vm;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandError(String);
|
||||
|
||||
impl fmt::Display for CommandError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CommandError {}
|
||||
|
||||
macro_rules! cmd_error {
|
||||
($($arg:tt)*) => {
|
||||
Box::new(CommandError(format!($($arg)*)))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use cmd_error;
|
49
nzrd/src/cmd/net.rs
Normal file
49
nzrd/src/cmd/net.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use super::*;
|
||||
use crate::ctrl::net::Subnet;
|
||||
use crate::ctrl::Storable;
|
||||
use crate::ctx::Context;
|
||||
use nzr_api::model;
|
||||
|
||||
pub async fn add_subnet(
|
||||
ctx: &Context,
|
||||
args: model::Subnet,
|
||||
) -> Result<Subnet, Box<dyn std::error::Error>> {
|
||||
let subnet = Subnet::new(
|
||||
&args.ifname.to_string(),
|
||||
&args.data.network,
|
||||
&args.data.start_host,
|
||||
&args.data.end_host,
|
||||
args.data.gateway4.as_ref(),
|
||||
&args.data.dns,
|
||||
args.data.domain_name,
|
||||
)
|
||||
.map_err(|er| cmd_error!("Couldn't generate subnet: {}", er))?;
|
||||
|
||||
let mut ent = Subnet::insert(
|
||||
ctx.db.clone(),
|
||||
subnet.clone(),
|
||||
args.ifname.to_string().as_bytes(),
|
||||
)?;
|
||||
|
||||
ent.transient = true;
|
||||
|
||||
if let Err(err) = ctx.zones.new_zone(&subnet).await {
|
||||
Err(cmd_error!("Failed to create new DNS zone: {}", err))
|
||||
} else {
|
||||
ent.transient = false;
|
||||
Ok(subnet)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_subnet(ctx: &Context, interface: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match Subnet::get_by_key |