Initial commit

This commit is contained in:
snow flurry 2022-12-29 22:06:14 -08:00
commit 4c02261015
35 changed files with 7969 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
.DS_Store
.vscode

2332
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

6
Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[workspace]
members = [
"nzrd",
"api",
"client",
]

14
api/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
pub mod cidr;
pub mod mac;

15
client/Cargo.toml Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

44
nzrd/Cargo.toml Normal file
View 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
View 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
View 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
View 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