From 6da77159b1599ef8b3ed00c0563ef346e241ba39 Mon Sep 17 00:00:00 2001 From: snow flurry Date: Sat, 10 Aug 2024 00:58:20 -0700 Subject: [PATCH] Complete rewrite time Main changes: * Use diesel instead of sled * Split libvirt components into new crate, nzr-virt * Start moving toward network-based cloud-init To facilitate the latter, nzrdhcp is an added unicast-only DHCP server, intended to be used behind a DHCP relay. --- Cargo.lock | 1504 ++++++++++++++--- Cargo.toml | 2 +- client/Cargo.toml | 2 +- client/src/main.rs | 19 +- client/src/table.rs | 5 +- {api => nzr-api}/Cargo.toml | 7 +- {api => nzr-api}/src/args.rs | 0 {api => nzr-api}/src/config.rs | 19 +- {api => nzr-api}/src/lib.rs | 0 {api => nzr-api}/src/model.rs | 12 +- {api => nzr-api}/src/net/cidr.rs | 47 +- {api => nzr-api}/src/net/mac.rs | 33 +- {api => nzr-api}/src/net/mod.rs | 0 nzr-virt/Cargo.toml | 20 + nzr-virt/src/dom.rs | 128 ++ nzr-virt/src/error.rs | 66 + {nzrd => nzr-virt}/src/img.rs | 30 +- nzr-virt/src/lib.rs | 61 + nzr-virt/src/vol.rs | 298 ++++ .../virtxml => nzr-virt/src/xml}/build.rs | 10 +- .../ctrl/virtxml => nzr-virt/src/xml}/mod.rs | 0 .../ctrl/virtxml => nzr-virt/src/xml}/test.rs | 6 +- nzrd/Cargo.toml | 51 +- nzrd/migrations/2024080901_initial.sql | 24 + nzrd/src/cloud.rs | 43 - nzrd/src/cmd/net.rs | 45 +- nzrd/src/cmd/vm.rs | 315 ++-- nzrd/src/ctrl/mod.rs | 290 ---- nzrd/src/ctrl/net.rs | 176 -- nzrd/src/ctrl/vm.rs | 326 ---- nzrd/src/ctx.rs | 130 +- nzrd/src/dns.rs | 15 +- nzrd/src/main.rs | 74 +- nzrd/src/model/mod.rs | 437 +++++ nzrd/src/model/tx.rs | 56 + nzrd/src/prelude.rs | 10 - nzrd/src/rpc.rs | 183 +- nzrd/src/virt.rs | 265 --- nzrdhcp/Cargo.toml | 21 + nzrdhcp/src/ctx.rs | 124 ++ nzrdhcp/src/hack.rs | 0 nzrdhcp/src/main.rs | 184 ++ 42 files changed, 3237 insertions(+), 1801 deletions(-) rename {api => nzr-api}/Cargo.toml (72%) rename {api => nzr-api}/src/args.rs (100%) rename {api => nzr-api}/src/config.rs (88%) rename {api => nzr-api}/src/lib.rs (100%) rename {api => nzr-api}/src/model.rs (93%) rename {api => nzr-api}/src/net/cidr.rs (83%) rename {api => nzr-api}/src/net/mac.rs (71%) rename {api => nzr-api}/src/net/mod.rs (100%) create mode 100644 nzr-virt/Cargo.toml create mode 100644 nzr-virt/src/dom.rs create mode 100644 nzr-virt/src/error.rs rename {nzrd => nzr-virt}/src/img.rs (81%) create mode 100644 nzr-virt/src/lib.rs create mode 100644 nzr-virt/src/vol.rs rename {nzrd/src/ctrl/virtxml => nzr-virt/src/xml}/build.rs (96%) rename {nzrd/src/ctrl/virtxml => nzr-virt/src/xml}/mod.rs (100%) rename {nzrd/src/ctrl/virtxml => nzr-virt/src/xml}/test.rs (96%) create mode 100644 nzrd/migrations/2024080901_initial.sql create mode 100644 nzrd/src/model/mod.rs create mode 100644 nzrd/src/model/tx.rs delete mode 100644 nzrd/src/prelude.rs delete mode 100644 nzrd/src/virt.rs create mode 100644 nzrdhcp/Cargo.toml create mode 100644 nzrdhcp/src/ctx.rs create mode 100644 nzrdhcp/src/hack.rs create mode 100644 nzrdhcp/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 8d89acf..dd64b9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -26,6 +38,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -86,9 +104,20 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.67" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7724808837b77f4b4de9d283820f9d98bcf496d5692934b857a2399d31ff22e6" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] [[package]] name = "async-trait" @@ -101,6 +130,15 @@ dependencies = [ "syn 1.0.106", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.1" @@ -137,6 +175,18 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bincode" version = "1.3.3" @@ -157,6 +207,18 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "bumpalo" @@ -207,42 +269,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", - "js-sys", "num-integer", "num-traits", "serde", - "time 0.1.45", - "wasm-bindgen", "winapi", ] -[[package]] -name = "ciborium" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" - -[[package]] -name = "ciborium-ll" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "clap" version = "4.0.29" @@ -254,7 +286,7 @@ dependencies = [ "clap_lex", "is-terminal", "once_cell", - "strsim", + "strsim 0.10.0", "termcolor", ] @@ -264,7 +296,7 @@ version = "4.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro-error", "proc-macro2", "quote", @@ -296,6 +328,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -303,14 +350,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] -name = "crc32fast" -version = "1.3.2" +name = "cpufeatures" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ - "cfg-if", + "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crossbeam-channel" version = "0.5.6" @@ -323,24 +385,36 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.13" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "cfg-if", + "generic-array", + "typenum", ] [[package]] @@ -393,8 +467,18 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.2", + "darling_macro 0.14.2", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core 0.20.10", + "darling_macro 0.20.10", ] [[package]] @@ -407,27 +491,162 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.106", ] +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.72", +] + [[package]] name = "darling_macro" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" dependencies = [ - "darling_core", + "darling_core 0.14.2", "quote", "syn 1.0.106", ] +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core 0.20.10", + "quote", + "syn 2.0.72", +] + [[package]] name = "data-encoding" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "dhcproto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6794294f2c4665aae452e950c2803a1e487c5672dc8448f0bfa3f52ff67e270" +dependencies = [ + "dhcproto-macros", + "hex", + "ipnet", + "rand", + "serde", + "thiserror", + "trust-dns-proto", + "url", +] + +[[package]] +name = "dhcproto-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7993efb860416547839c115490d4951c6d0f8ec04a3594d9dd99d50ed7ec170" + +[[package]] +name = "diesel" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf97ee7261bb708fa3402fa9c17a54b70e90e3cb98afb3dc8999d5512cb03f94" +dependencies = [ + "diesel_derives", + "libsqlite3-sys", + "r2d2", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ff2be1e7312c858b2ef974f5c7089833ae57b5311b334b30923af58e5718d8" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "diesel_migrations" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +dependencies = [ + "syn 2.0.72", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dsl_auto_type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" +dependencies = [ + "darling 0.20.10", + "either", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "educe" version = "0.4.20" @@ -452,13 +671,34 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck 0.4.0", + "proc-macro2", + "quote", + "syn 1.0.106", +] + [[package]] name = "enum-as-inner" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro2", "quote", "syn 2.0.72", @@ -521,6 +761,12 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.2.8" @@ -552,24 +798,44 @@ dependencies = [ "libc", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" -[[package]] -name = "fatfs" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e18f80a87439240dac45d927fd8f8081b6f1e34c03e97271189fa8a8c2e96c8f" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "chrono", - "log", -] - [[package]] name = "figment" version = "0.10.8" @@ -580,11 +846,22 @@ dependencies = [ "pear", "serde", "serde_json", - "toml", + "toml 0.5.10", "uncased", "version_check", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -600,16 +877,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "futures" version = "0.3.25" @@ -652,6 +919,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.25" @@ -700,12 +978,13 @@ dependencies = [ ] [[package]] -name = "fxhash" -version = "0.2.1" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "byteorder", + "typenum", + "version_check", ] [[package]] @@ -716,7 +995,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -725,24 +1004,43 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.2.6" @@ -773,7 +1071,7 @@ dependencies = [ "async-trait", "cfg-if", "data-encoding", - "enum-as-inner", + "enum-as-inner 0.6.0", "futures-channel", "futures-io", "futures-util", @@ -798,24 +1096,42 @@ dependencies = [ "async-trait", "bytes", "cfg-if", - "enum-as-inner", + "enum-as-inner 0.6.0", "futures-util", "hickory-proto", "serde", "thiserror", - "time 0.3.17", + "time", "tokio", "tokio-util", "tracing", ] [[package]] -name = "home" -version = "0.5.4" +name = "hkdf" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "winapi", + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", ] [[package]] @@ -865,6 +1181,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.4.0" @@ -892,25 +1219,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "serde", ] +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + [[package]] name = "inlinable_string" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "io-lifetimes" version = "1.0.3" @@ -926,6 +1254,9 @@ name = "ipnet" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" +dependencies = [ + "serde", +] [[package]] name = "is-terminal" @@ -965,6 +1296,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "libc" @@ -972,6 +1306,23 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "link-cplusplus" version = "1.0.8" @@ -1005,11 +1356,24 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", + "digest", ] [[package]] @@ -1019,14 +1383,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] -name = "memoffset" -version = "0.7.1" +name = "migrations_internals" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff" dependencies = [ - "autocfg", + "serde", + "toml 0.8.19", ] +[[package]] +name = "migrations_macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -1044,10 +1426,34 @@ checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ "hermit-abi 0.3.9", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.52.0", ] +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +dependencies = [ + "async-lock", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "event-listener", + "futures-util", + "once_cell", + "parking_lot", + "quanta", + "rustc_version", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1060,6 +1466,26 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -1071,6 +1497,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1081,6 +1524,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1088,6 +1542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1119,24 +1574,41 @@ dependencies = [ name = "nzr-api" version = "0.1.0" dependencies = [ + "diesel", "figment", "hickory-proto", "log", "serde", + "sqlx", "tarpc", "tokio", "uuid", ] [[package]] -name = "nzrd" +name = "nzr-virt" version = "0.1.0" +dependencies = [ + "nzr-api", + "quick-xml", + "serde", + "serde_with", + "tempfile", + "thiserror", + "tokio", + "tracing", + "uuid", + "virt", +] + +[[package]] +name = "nzrd" +version = "1.0.0" dependencies = [ "async-trait", - "ciborium", - "ciborium-io", "clap", - "fatfs", + "diesel", + "diesel_migrations", "futures", "hickory-proto", "hickory-server", @@ -1145,24 +1617,41 @@ dependencies = [ "log", "nix", "nzr-api", + "nzr-virt", + "paste", "quick-xml", "rand", "regex", "serde", "serde_with", "serde_yaml", - "sled", "stdext", "syslog", "tarpc", "tempfile", + "thiserror", "tokio", "tokio-serde 0.9.0", + "trait-variant", "uuid", - "virt", "zerocopy", ] +[[package]] +name = "nzrdhcp" +version = "0.1.0" +dependencies = [ + "anyhow", + "dhcproto", + "moka", + "nzr-api", + "serde", + "tarpc", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "object" version = "0.36.2" @@ -1196,7 +1685,7 @@ checksum = "c24f96e21e7acc813c7a8394ee94978929db2bcc46cf6b5014fc612bf7760c22" dependencies = [ "futures-channel", "futures-util", - "indexmap", + "indexmap 1.9.2", "js-sys", "once_cell", "pin-project-lite", @@ -1227,6 +1716,12 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "papergrid" version = "0.11.0" @@ -1239,30 +1734,40 @@ dependencies = [ ] [[package]] -name = "parking_lot" -version = "0.11.2" +name = "parking" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ - "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.6" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", - "instant", "libc", - "redox_syscall", + "redox_syscall 0.5.3", "smallvec", - "winapi", + "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pear" version = "0.2.3" @@ -1286,6 +1791,15 @@ dependencies = [ "syn 1.0.106", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1324,6 +1838,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.26" @@ -1382,6 +1917,21 @@ dependencies = [ "yansi", ] +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quick-xml" version = "0.36.1" @@ -1401,6 +1951,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.8.5" @@ -1432,14 +1993,32 @@ dependencies = [ ] [[package]] -name = "redox_syscall" -version = "0.2.16" +name = "raw-cpuid" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "regex" version = "1.7.0" @@ -1457,6 +2036,26 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1505,6 +2104,15 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1525,22 +2133,22 @@ checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" [[package]] name = "serde" -version = "1.0.151" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.151" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 1.0.106", + "syn 2.0.72", ] [[package]] @@ -1554,20 +2162,41 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25bf4a5a814902cd1014dbccfa4d4560fb8432c779471e96e035602519f82eef" dependencies = [ - "base64", + "base64 0.13.1", "chrono", "hex", - "indexmap", + "indexmap 1.9.2", "serde", "serde_json", "serde_with_macros", - "time 0.3.17", + "time", ] [[package]] @@ -1576,7 +2205,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3452b4c0f6c1e357f73fdb87cd1efabaa12acf328c7a528e252893baeb3f4aa" dependencies = [ - "darling", + "darling 0.14.2", "proc-macro2", "quote", "syn 1.0.106", @@ -1588,13 +2217,35 @@ version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f33c5a526b140e830219698a1bafd340d3206cfa37371ed00442babaf53105" dependencies = [ - "indexmap", + "indexmap 1.9.2", "itoa", "ryu", "serde", "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1613,6 +2264,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "slab" version = "0.4.7" @@ -1622,27 +2283,14 @@ dependencies = [ "autocfg", ] -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot", -] - [[package]] name = "smallvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -1654,6 +2302,232 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27144619c6e5802f1380337a209d2ac1c431002dd74c6e60aebff3c506dc4f0c" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a999083c1af5b5d6c071d34a708a19ba3e02106ad82ef7bbd69f5e48266b613b" +dependencies = [ + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.14.5", + "hashlink", + "hex", + "indexmap 2.2.6", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23217eb7d86c584b8cbe0337b9eacf12ab76fe7673c513141ec42565698bb88" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.72", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.72", + "tempfile", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.6.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.6.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2cdd83c008a622d94499c0006d8ee5f821f36c89b7d625c900e5dc30b5c5ee" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "tracing", + "url", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1666,12 +2540,35 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f3b6b32ae82412fb897ef134867d53a294f57ba5b758f06d71e865352c3e207" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.106" @@ -1703,7 +2600,7 @@ dependencies = [ "hostname", "libc", "log", - "time 0.3.17", + "time", ] [[package]] @@ -1723,13 +2620,19 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c138f99377e5d653a371cdad263615634cfc8467685dfe8e73e2b8e98f44b17" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro-error", "proc-macro2", "quote", "syn 1.0.106", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tarpc" version = "0.34.0" @@ -1788,22 +2691,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 1.0.106", + "syn 2.0.72", ] [[package]] @@ -1815,17 +2718,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.3.17" @@ -1952,12 +2844,45 @@ dependencies = [ ] [[package]] -name = "tracing" -version = "0.1.37" +name = "toml" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -1966,25 +2891,36 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.106", + "syn 2.0.72", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-opentelemetry" version = "0.18.0" @@ -2000,15 +2936,66 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "nu-ansi-term", "sharded-slab", + "smallvec", "thread_local", "tracing-core", + "tracing-log", ] +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + +[[package]] +name = "trust-dns-proto" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner 0.5.1", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "rand", + "serde", + "smallvec", + "thiserror", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "uncased" version = "0.9.7" @@ -2039,12 +3026,24 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unsafe-libyaml" version = "0.2.5" @@ -2078,18 +3077,6 @@ dependencies = [ "getrandom", "rand", "serde", - "uuid-macro-internal", -] - -[[package]] -name = "uuid-macro-internal" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee1cd046f83ea2c4e920d6ee9f7c3537ef928d75dce5d84a87c2c5d6b3999a3a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", ] [[package]] @@ -2098,6 +3085,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -2125,18 +3118,18 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.83" @@ -2191,6 +3184,26 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2229,7 +3242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2238,7 +3251,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2256,13 +3269,37 @@ dependencies = [ "windows_x86_64_msvc 0.42.0", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -2287,6 +3324,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2299,6 +3342,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2311,6 +3360,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2329,6 +3384,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2341,6 +3402,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2353,6 +3420,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2365,12 +3438,27 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + [[package]] name = "yansi" version = "0.5.1" @@ -2397,3 +3485,9 @@ dependencies = [ "quote", "syn 2.0.72", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 22e866c..cf28bd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["nzrd", "api", "client"] +members = ["nzrd", "nzr-api", "client", "nzrdhcp", "nzr-virt"] resolver = "2" diff --git a/client/Cargo.toml b/client/Cargo.toml index 97f0310..733fa14 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -nzr-api = { path = "../api" } +nzr-api = { path = "../nzr-api" } clap = { version = "4.0.26", features = ["derive"] } home = "0.5.4" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } diff --git a/client/src/main.rs b/client/src/main.rs index b37afe6..836e35c 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -283,12 +283,10 @@ async fn handle_command() -> Result<(), Box> { match result { Ok(instance) => { println!("Instance {} created!", &instance.name); - if let Some(lease) = instance.lease { - println!( - "You should be able to reach it with: ssh root@{}", - lease.addr.addr, - ); - } + println!( + "You should be able to reach it with: ssh root@{}", + instance.lease.addr.addr, + ); } Err(err) => { log::error!("Error while creating instance: {}", err); @@ -340,12 +338,12 @@ async fn handle_command() -> Result<(), Box> { name: args.name, data: model::SubnetData { ifname: args.interface.clone(), - network: net_arg.clone(), + network: net_arg, 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.unwrap_or(net_arg.make_ip(1)?), + gateway4: Some(args.gateway.unwrap_or(net_arg.make_ip(1)?)), dns: args.dns_server.map_or(Vec::new(), |d| vec![d]), domain_name: args.domain_name, vlan_id: args.vlan_id, @@ -373,9 +371,8 @@ async fn handle_command() -> Result<(), Box> { })?; // merge in the new args - if let Some(gateway) = args.gateway { - net.data.gateway4 = gateway; - } + net.data.gateway4 = args.gateway; + if let Some(dns_server) = args.dns_server { net.data.dns = vec![dns_server] } diff --git a/client/src/table.rs b/client/src/table.rs index e127064..e2b56d0 100644 --- a/client/src/table.rs +++ b/client/src/table.rs @@ -15,10 +15,7 @@ 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()), + ip_addr: value.lease.addr.to_string(), state: value.state, } } diff --git a/api/Cargo.toml b/nzr-api/Cargo.toml similarity index 72% rename from api/Cargo.toml rename to nzr-api/Cargo.toml index 7939cd5..ba3c168 100644 --- a/api/Cargo.toml +++ b/nzr-api/Cargo.toml @@ -8,6 +8,11 @@ figment = { version = "0.10.8", features = ["json", "toml", "env"] } serde = { version = "1", features = ["derive"] } tarpc = { version = "0.34", features = ["tokio1", "unix"] } tokio = { version = "1.0", features = ["macros"] } -uuid = "1.2.2" +uuid = { version = "1.2.2", features = ["serde"] } hickory-proto = { version = "0.24", features = ["serde-config"] } log = "0.4.17" +sqlx = "0.8" +diesel = { version = "2.2", optional = true } + +[features] +diesel = ["dep:diesel"] diff --git a/api/src/args.rs b/nzr-api/src/args.rs similarity index 100% rename from api/src/args.rs rename to nzr-api/src/args.rs diff --git a/api/src/config.rs b/nzr-api/src/config.rs similarity index 88% rename from api/src/config.rs rename to nzr-api/src/config.rs index a51730a..1eade15 100644 --- a/api/src/config.rs +++ b/nzr-api/src/config.rs @@ -14,8 +14,6 @@ pub struct StorageConfig { pub primary_pool: String, /// The secondary storage pool, allocated to any VMs that require slower storage. pub secondary_pool: String, - #[deprecated(note = "FAT32 NoCloud support will be replaced with an HTTP endpoint")] - pub ci_image_pool: String, /// Pool containing cloud-init base images. pub base_image_pool: String, } @@ -38,6 +36,12 @@ pub struct DNSConfig { pub soa: SOAConfig, } +/// DHCP server configuration. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DHCPConfig { + pub listen_addr: String, +} + /// Server<->Client RPC configuration. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RPCConfig { @@ -51,12 +55,13 @@ pub struct Config { pub rpc: RPCConfig, pub log_level: String, /// Where database information should be stored. - pub db_path: PathBuf, + pub db_uri: String, pub qemu_img_path: Option, /// The libvirt URI to use for connections; e.g. `qemu:///system`. pub libvirt_uri: String, pub storage: StorageConfig, pub dns: DNSConfig, + pub dhcp: DHCPConfig, } impl Default for Config { @@ -68,7 +73,7 @@ impl Default for Config { socket_path: PathBuf::from("/var/run/nazrin/nzrd.sock"), admin_group: None, }, - db_path: PathBuf::from("/var/lib/nazrin/nzr.db"), + db_uri: "sqlite:/var/lib/nazrin/main_sql.db".to_owned(), libvirt_uri: match std::env::var("LIBVIRT_URI") { Ok(v) => v, Err(_) => String::from("qemu:///system"), @@ -76,12 +81,11 @@ impl Default for Config { 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(), + default_zone: Name::from_utf8("servers.locaddral").unwrap(), soa: SOAConfig { nzr_domain: Name::from_utf8("nzr.local").unwrap(), contact: Name::from_utf8("admin.nzr.local").unwrap(), @@ -90,6 +94,9 @@ impl Default for Config { expire: 3_600_000, }, }, + dhcp: DHCPConfig { + listen_addr: "127.0.0.1".to_owned(), + }, } } } diff --git a/api/src/lib.rs b/nzr-api/src/lib.rs similarity index 100% rename from api/src/lib.rs rename to nzr-api/src/lib.rs diff --git a/api/src/model.rs b/nzr-api/src/model.rs similarity index 93% rename from api/src/model.rs rename to nzr-api/src/model.rs index 14ddbcb..fee07f4 100644 --- a/api/src/model.rs +++ b/nzr-api/src/model.rs @@ -68,16 +68,16 @@ pub struct CreateStatus { } /// Struct representing a VM instance. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Instance { pub name: String, - pub uuid: uuid::Uuid, - pub lease: Option, + pub id: i32, + pub lease: Lease, pub state: DomainState, } /// Struct representing a logical "lease" held by a VM. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Lease { /// Subnet name corresponding to the lease pub subnet: String, @@ -108,8 +108,8 @@ pub struct SubnetData { /// The last host address that can be assigned dynamically /// on the subnet. pub end_host: Ipv4Addr, - /// The default gateway for the subnet. - pub gateway4: Ipv4Addr, + /// The default gateway for the subnet, if any. + pub gateway4: Option, /// The primary DNS server for the subnet. pub dns: Vec, /// The base domain used for DNS lookup. diff --git a/api/src/net/cidr.rs b/nzr-api/src/net/cidr.rs similarity index 83% rename from api/src/net/cidr.rs rename to nzr-api/src/net/cidr.rs index d6b71a6..71ef9d3 100644 --- a/api/src/net/cidr.rs +++ b/nzr-api/src/net/cidr.rs @@ -5,6 +5,9 @@ use std::str::FromStr; use serde::{de, Deserialize, Serialize}; +#[cfg(feature = "diesel")] +use diesel::{sql_types::Text, sqlite::Sqlite}; + #[derive(Debug)] pub enum Error { Malformed, @@ -31,13 +34,55 @@ impl fmt::Display for Error { impl std::error::Error for Error {} -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +/// Representation of a combined IPv4 network address and subnet mask, as used +/// in Classless Inter-Domain Routing (CIDR). +#[cfg_attr(feature = "diesel", derive(diesel::FromSqlRow, diesel::AsExpression))] +#[cfg_attr(feature = "diesel", diesel(sql_type = Text))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct CidrV4 { pub addr: Ipv4Addr, cidr: u8, netmask: u32, } +impl Default for CidrV4 { + /// Create a CidrV4 address corresponding to `0.0.0.0/0`. This is intended + /// to be used as a placeholder. + fn default() -> Self { + CidrV4 { + addr: Ipv4Addr::new(0, 0, 0, 0), + cidr: 0, + netmask: 0, + } + } +} + +#[cfg(feature = "diesel")] +impl diesel::serialize::ToSql for CidrV4 { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, Sqlite>, + ) -> diesel::serialize::Result { + use diesel::serialize::IsNull; + + let value = self.to_string(); + out.set_value(value); + Ok(IsNull::No) + } +} + +#[cfg(feature = "diesel")] +impl diesel::deserialize::FromSql for CidrV4 +where + DB: diesel::backend::Backend, + String: diesel::deserialize::FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result { + let str_val = String::from_sql(bytes)?; + Ok(Self::from_str(&str_val)?) + } +} + impl fmt::Display for CidrV4 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}/{}", self.addr, self.cidr) diff --git a/api/src/net/mac.rs b/nzr-api/src/net/mac.rs similarity index 71% rename from api/src/net/mac.rs rename to nzr-api/src/net/mac.rs index f8527bc..7daf6ff 100644 --- a/api/src/net/mac.rs +++ b/nzr-api/src/net/mac.rs @@ -2,11 +2,42 @@ use std::{fmt, str::FromStr}; use serde::{de, Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg(feature = "diesel")] +use diesel::{sql_types::Text, sqlite::Sqlite}; + +#[cfg_attr(feature = "diesel", derive(diesel::FromSqlRow, diesel::AsExpression))] +#[cfg_attr(feature = "diesel", diesel(sql_type = Text))] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub struct MacAddr { octets: [u8; 6], } +#[cfg(feature = "diesel")] +impl diesel::serialize::ToSql for MacAddr { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, Sqlite>, + ) -> diesel::serialize::Result { + use diesel::serialize::IsNull; + + let value = self.to_string(); + out.set_value(value); + Ok(IsNull::No) + } +} + +#[cfg(feature = "diesel")] +impl diesel::deserialize::FromSql for MacAddr +where + DB: diesel::backend::Backend, + String: diesel::deserialize::FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result { + let str_val = String::from_sql(bytes)?; + Ok(Self::from_str(&str_val)?) + } +} + impl Serialize for MacAddr { fn serialize(&self, serializer: S) -> Result where diff --git a/api/src/net/mod.rs b/nzr-api/src/net/mod.rs similarity index 100% rename from api/src/net/mod.rs rename to nzr-api/src/net/mod.rs diff --git a/nzr-virt/Cargo.toml b/nzr-virt/Cargo.toml new file mode 100644 index 0000000..653e95c --- /dev/null +++ b/nzr-virt/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "nzr-virt" +version = "0.1.0" +edition = "2021" + +[dependencies] +tracing = "0.1" +thiserror = "1" +tokio = { version = "1", features = ["process"] } + +serde = { version = "1", features = ["derive"] } +quick-xml = { version = "0.36", features = ["serialize"] } +serde_with = "2" +uuid = { version = "1.10", features = ["v4", "fast-rng"] } + +virt = "0.4" + +nzr-api = { path = "../nzr-api" } + +tempfile = "3" diff --git a/nzr-virt/src/dom.rs b/nzr-virt/src/dom.rs new file mode 100644 index 0000000..cb29efb --- /dev/null +++ b/nzr-virt/src/dom.rs @@ -0,0 +1,128 @@ +use std::sync::Arc; + +use crate::{ + error::{DomainError, VirtError}, + xml, Connection, +}; + +pub struct Domain { + inner: xml::Domain, + virt: Arc, + persist: bool, +} + +impl Domain { + pub(crate) async fn define(conn: &Connection, xml: xml::Domain) -> Result { + let conn = conn.virtconn.clone(); + tokio::task::spawn_blocking(move || { + let virt_domain = { + let inst_xml = quick_xml::se::to_string(&xml).map_err(DomainError::XmlError)?; + virt::domain::Domain::define_xml(&conn, &inst_xml) + .map_err(DomainError::VirtError)? + }; + + let built_xml = match virt_domain.get_xml_desc(0) { + Ok(xml) => { + quick_xml::de::from_str::(&xml).map_err(DomainError::XmlError) + } + Err(err) => { + if let Err(err) = virt_domain.undefine() { + tracing::warn!("Couldn't undefine domain after failure: {err}"); + } + Err(DomainError::VirtError(err)) + } + }?; + + Ok(Self { + inner: built_xml, + virt: Arc::new(virt_domain), + persist: false, + }) + }) + .await + .unwrap() + } + + #[inline] + // Convenience function so I can stop doing exactly this so much + async fn spawn_virt(&self, f: F) -> R + where + F: FnOnce(Arc) -> R + Send + 'static, + R: Send + 'static, + { + let virt = self.virt.clone(); + tokio::task::spawn_blocking(move || f(virt)).await.unwrap() + } + + pub(crate) async fn get(conn: &Connection, name: impl AsRef) -> Result { + let name = name.as_ref().to_owned(); + let virtconn = conn.virtconn.clone(); + + // Run libvirt calls in a blocking thread + tokio::task::spawn_blocking(move || { + let dom = match virt::domain::Domain::lookup_by_name(&virtconn, &name) { + Ok(inst) => Ok(inst), + Err(err) if err.code() == virt::error::ErrorNumber::NoDomain => { + Err(DomainError::DomainNotFound) + } + Err(err) => Err(DomainError::VirtError(err)), + }?; + let domain_xml: xml::Domain = { + let xml_str = dom.get_xml_desc(0).map_err(DomainError::VirtError)?; + quick_xml::de::from_str(&xml_str).map_err(DomainError::XmlError)? + }; + + Ok(Self { + inner: domain_xml, + virt: Arc::new(dom), + persist: true, + }) + }) + .await + .unwrap() + } + + /// Undefines the libvirt domain. + /// If `deep` is set to true, all connected volumes are deleted. + pub async fn undefine(&mut self, deep: bool) -> Result<(), VirtError> { + if deep { + let conn: Connection = self.virt.get_connect()?.into(); + for disk in self.inner.devices.disks() { + if let (Some(pool), Some(vol)) = (&disk.source.pool, &disk.source.volume) { + if let Ok(pool) = conn.get_pool(pool).await { + if let Ok(vol) = pool.volume(vol).await { + vol.delete().await?; + } + } + } + } + } + self.spawn_virt(|virt| virt.undefine()).await + } + + /// Gets a reference to the inner libvirt XML. + pub async fn xml(&self) -> &xml::Domain { + &self.inner + } + + pub async fn persist(&mut self) { + self.persist = true; + } + + /// Sets whether the domain is autostarted. The return value, if successful, + /// represents the previous state. + pub async fn autostart(&mut self, doit: bool) -> Result { + self.spawn_virt(move |virt| virt.set_autostart(doit)).await + } + + /// Starts the domain. + pub async fn start(&self) -> Result<(), VirtError> { + self.spawn_virt(|virt| virt.create()).await?; + Ok(()) + } + + /// Gets the current domain state. + pub async fn state(&self) -> Result { + self.spawn_virt(|virt| virt.get_state().map(|s| s.0)).await + } +} diff --git a/nzr-virt/src/error.rs b/nzr-virt/src/error.rs new file mode 100644 index 0000000..7647ab1 --- /dev/null +++ b/nzr-virt/src/error.rs @@ -0,0 +1,66 @@ +use std::mem::discriminant; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum PoolError { + #[error("libvirt error: {0}")] + VirtError(virt::error::Error), + #[error("error reading XML: {0}")] + XmlError(quick_xml::de::DeError), + #[error("Error getting source image: {0}")] + NoPath(virt::error::Error), + #[error("{0}")] + FileError(std::io::Error), + #[error("Unable to start upload: {0}")] + CantUpload(virt::error::Error), + #[error("Upload failed: {0}")] + UploadError(virt::error::Error), + #[error("{0}")] + QemuError(ImgError), +} + +#[derive(Debug)] +pub struct ImgError { + pub(crate) message: String, + pub(crate) command_output: Option, +} + +impl std::fmt::Display for ImgError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(output) = &self.command_output { + write!(f, "{}\n output from command: {}", self.message, output) + } else { + write!(f, "{}", self.message) + } + } +} + +impl From for ImgError { + fn from(value: std::io::Error) -> Self { + Self { + message: format!("IO Error: {}", value), + command_output: None, + } + } +} + +impl std::error::Error for ImgError {} + +#[derive(Debug, Error)] +pub enum DomainError { + #[error("libvirt error: {0}")] + VirtError(VirtError), + #[error("Error processing XML: {0}")] + XmlError(quick_xml::de::DeError), + #[error("Domain not found")] + DomainNotFound, +} + +impl PartialEq for DomainError { + fn eq(&self, other: &Self) -> bool { + discriminant(self) == discriminant(other) + } +} + +pub type VirtError = virt::error::Error; diff --git a/nzrd/src/img.rs b/nzr-virt/src/img.rs similarity index 81% rename from nzrd/src/img.rs rename to nzr-virt/src/img.rs index d4497df..0beea6b 100644 --- a/nzrd/src/img.rs +++ b/nzr-virt/src/img.rs @@ -8,34 +8,8 @@ use std::future::Future; use tempfile::TempDir; use tokio::process::Command; -use crate::ctrl::virtxml::SizeInfo; - -#[derive(Debug)] -pub struct ImgError { - message: String, - command_output: Option, -} - -impl std::fmt::Display for ImgError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(output) = &self.command_output { - write!(f, "{}\n output from command: {}", self.message, output) - } else { - write!(f, "{}", self.message) - } - } -} - -impl From for ImgError { - fn from(value: std::io::Error) -> Self { - Self { - message: format!("IO Error: {}", value), - command_output: None, - } - } -} - -impl std::error::Error for ImgError {} +use crate::error::ImgError; +use crate::xml::SizeInfo; impl ImgError { fn new(message: S) -> Self diff --git a/nzr-virt/src/lib.rs b/nzr-virt/src/lib.rs new file mode 100644 index 0000000..2b53bb9 --- /dev/null +++ b/nzr-virt/src/lib.rs @@ -0,0 +1,61 @@ +pub mod dom; +pub mod error; +pub(crate) mod img; +pub mod vol; +pub mod xml; + +use std::sync::Arc; + +use virt::connect::Connect; + +#[macro_export] +macro_rules! datasize { + ($amt:tt $unit:tt) => { + $crate::xml::SizeInfo { + amount: $amt as u64, + unit: $crate::xml::SizeUnit::$unit, + } + }; +} + +pub struct Connection { + virtconn: Arc, +} + +impl Connection { + /// Opens a connection to the libvirt host. + pub fn open(uri: impl AsRef) -> Result { + let virtconn = Connect::open(Some(uri.as_ref()))?; + virt::error::clear_error_callback(); + + Ok(Self { + virtconn: Arc::new(virtconn), + }) + } + + pub async fn get_pool(&self, name: impl AsRef) -> Result { + vol::Pool::get(self, name.as_ref()).await + } + + pub async fn get_instance( + &self, + name: impl AsRef, + ) -> Result { + dom::Domain::get(self, name.as_ref()).await + } + + pub async fn define_instance( + &self, + data: xml::Domain, + ) -> Result { + dom::Domain::define(self, data).await + } +} + +impl From for Connection { + fn from(value: Connect) -> Self { + Self { + virtconn: Arc::new(value), + } + } +} diff --git a/nzr-virt/src/vol.rs b/nzr-virt/src/vol.rs new file mode 100644 index 0000000..8f903a3 --- /dev/null +++ b/nzr-virt/src/vol.rs @@ -0,0 +1,298 @@ +use std::io::{prelude::*, BufReader}; +use std::sync::Arc; +use virt::{storage_pool::StoragePool, storage_vol::StorageVol, stream::Stream}; + +use crate::error::VirtError; +use crate::xml::SizeInfo; +use crate::{error::PoolError, xml}; +use crate::{img, Connection}; + +/// An abstracted representation of a libvirt volume. +pub struct Volume { + virt: Arc, + pub persist: bool, + pub name: String, +} + +impl Volume { + /// Upload a disk image from libvirt in a blocking task + async fn upload_img(from: impl Read + Send + 'static, to: Stream) -> Result<(), PoolError> { + let mut reader = BufReader::with_capacity(4294967296, from); + + tokio::task::spawn_blocking(move || { + loop { + // We can't borrow reader as mut twice. As such, most of the function is stored in this + let read_bytes = { + // Read from file + let data = match reader.fill_buf() { + Ok(buf) => buf, + Err(err) => { + if let Err(err) = to.abort() { + tracing::warn!("Failed to abort stream: {err}"); + } + + return Err(PoolError::FileError(err)); + } + }; + + if data.is_empty() { + break; + } + + tracing::trace!("read {} bytes", data.len()); + + // Send to libvirt + let mut send_idx = 0; + while send_idx < data.len() { + tracing::trace!("sending {} bytes", data.len() - send_idx); + match to.send(&data[send_idx..]) { + Ok(len) => { + send_idx += len; + } + Err(err) => { + if let Err(err) = to.abort() { + tracing::warn!("Stream abort failed: {err}"); + } + + return Err(PoolError::VirtError(err)); + } + } + } + data.len() + }; + + reader.consume(read_bytes); + } + + Ok(()) + }) + .await + .unwrap() + } + + /// Creates a [VirtVolume] from the given [Volume](crate::xml::Volume) XML data. + pub async fn create(pool: &Pool, xml: xml::Volume, flags: u32) -> Result { + let virt_pool = pool.virt.clone(); + let xml_str = quick_xml::se::to_string(&xml).map_err(PoolError::XmlError)?; + let vol = { + let xml_str = xml_str.clone(); + let vol = tokio::task::spawn_blocking(move || { + StorageVol::create_xml(&virt_pool, &xml_str, flags).map_err(PoolError::VirtError) + }) + .await + .unwrap()?; + Arc::new(vol) + }; + + if xml.vol_type() == Some(xml::VolType::Qcow2) { + let size = xml.capacity.unwrap(); + let src_img = img::create_qcow2(size) + .await + .map_err(PoolError::QemuError)?; + let stream_vol = vol.clone(); + + let stream = tokio::task::spawn_blocking(move || { + match Stream::new(&stream_vol.get_connect().map_err(PoolError::VirtError)?, 0) { + Ok(s) => Ok(s), + Err(err) => { + stream_vol.delete(0).ok(); + Err(PoolError::VirtError(err)) + } + } + }) + .await + .unwrap()?; + + let img_size = src_img.metadata().unwrap().len(); + + if let Err(err) = vol.upload(&stream, 0, img_size, 0) { + vol.delete(0).ok(); + return Err(PoolError::CantUpload(err)); + } + + let upload_fh = src_img.try_clone().map_err(PoolError::FileError)?; + + Self::upload_img(upload_fh, stream).await?; + } + + let name = xml.name.clone(); + + Ok(Self { + virt: vol, + persist: false, + name, + }) + } + + /// Finds a volume by the given pool and name. + async fn get(pool: &Pool, name: &str) -> Result { + let pool = pool.virt.clone(); + let name = name.to_owned(); + tokio::task::spawn_blocking(move || { + let vol = StorageVol::lookup_by_name(&pool, &name).map_err(PoolError::VirtError)?; + + Ok(Self { + virt: Arc::new(vol), + // default to persisting when looking up by name + persist: true, + name, + }) + }) + .await + .unwrap() + } + + /// Permanently deletes the volume. + pub async fn delete(&self) -> Result<(), VirtError> { + let virt = self.virt.clone(); + tokio::task::spawn_blocking(move || virt.delete(0)) + .await + .unwrap() + } + + /// Clones the data to a new libvirt volume. + pub async fn clone_vol( + &mut self, + pool: &Pool, + vol_name: impl AsRef, + size: SizeInfo, + ) -> Result { + let vol_name = vol_name.as_ref(); + tracing::debug!("Cloning volume to {vol_name} ({size})"); + + let virt = self.virt.clone(); + let src_path = + tokio::task::spawn_blocking(move || virt.get_path().map_err(PoolError::NoPath)) + .await + .unwrap()?; + + let src_img = img::clone_qcow2(src_path, size) + .await + .map_err(PoolError::QemuError)?; + + let newvol = xml::Volume::new(vol_name, pool.xml.vol_type(), size); + let newxml_str = quick_xml::se::to_string(&newvol).map_err(PoolError::XmlError)?; + tracing::debug!("Creating new vol..."); + let pool_virt = pool.virt.clone(); + let cloned = tokio::task::spawn_blocking(move || { + StorageVol::create_xml(&pool_virt, &newxml_str, 0).map_err(PoolError::VirtError) + }) + .await + .unwrap()?; + + match cloned.get_info() { + Ok(info) => { + if info.capacity != u64::from(size) { + tracing::debug!( + "libvirt set wrong size {}, trying this again...", + info.capacity + ); + if let Err(er) = cloned.resize(size.into(), 0) { + if let Err(er) = cloned.delete(0) { + tracing::warn!("Resizing disk failed, and couldn't clean up: {}", er); + } + return Err(PoolError::VirtError(er)); + } + } else { + tracing::debug!( + "capacity is correct ({} bytes), allocation = {} bytes", + info.capacity, + info.allocation, + ); + } + } + Err(er) => { + if let Err(er) = cloned.delete(0) { + tracing::warn!("Couldn't clean up destination volume: {}", er); + } + return Err(PoolError::VirtError(er)); + } + } + + let stream = { + let virt_conn = cloned.get_connect().map_err(PoolError::VirtError)?; + let cloned = cloned.clone(); + tokio::task::spawn_blocking(move || match Stream::new(&virt_conn, 0) { + Ok(s) => Ok(s), + Err(er) => { + cloned.delete(0).ok(); + Err(PoolError::VirtError(er)) + } + }) + .await + .unwrap() + }?; + + let img_size = src_img.metadata().unwrap().len(); + + { + let stream = stream.clone(); + let cloned = cloned.clone(); + tokio::task::spawn_blocking(move || { + if let Err(er) = cloned.upload(&stream, 0, img_size, 0) { + cloned.delete(0).ok(); + Err(PoolError::CantUpload(er)) + } else { + Ok(()) + } + }) + .await + .unwrap()?; + } + + let stream_fh = src_img.try_clone().map_err(PoolError::FileError)?; + + Self::upload_img(stream_fh, stream).await?; + + Ok(Self { + virt: Arc::new(cloned), + persist: false, + name: vol_name.to_owned(), + }) + } +} + +impl Drop for Volume { + fn drop(&mut self) { + if !self.persist { + tracing::debug!("Deleting volume {}", &self.name); + self.virt.delete(0).ok(); + } + } +} + +pub struct Pool { + virt: Arc, + xml: xml::Pool, +} + +impl AsRef for Pool { + fn as_ref(&self) -> &StoragePool { + &self.virt + } +} + +impl Pool { + pub(crate) async fn get(conn: &Connection, id: impl AsRef) -> Result { + let conn = conn.virtconn.clone(); + let id = id.as_ref().to_owned(); + tokio::task::spawn_blocking(move || { + let inner = StoragePool::lookup_by_name(&conn, &id).map_err(PoolError::VirtError)?; + if !inner.is_active().map_err(PoolError::VirtError)? { + inner.create(0).map_err(PoolError::VirtError)?; + } + let xml_str = inner.get_xml_desc(0).map_err(PoolError::VirtError)?; + let xml = quick_xml::de::from_str(&xml_str).map_err(PoolError::XmlError)?; + Ok(Self { + virt: Arc::new(inner), + xml, + }) + }) + .await + .unwrap() + } + + pub async fn volume(&self, name: impl AsRef) -> Result { + Volume::get(self, name.as_ref()).await + } +} diff --git a/nzrd/src/ctrl/virtxml/build.rs b/nzr-virt/src/xml/build.rs similarity index 96% rename from nzrd/src/ctrl/virtxml/build.rs rename to nzr-virt/src/xml/build.rs index 368622f..c20b11b 100644 --- a/nzrd/src/ctrl/virtxml/build.rs +++ b/nzr-virt/src/xml/build.rs @@ -1,4 +1,4 @@ -use log::*; +use nzr_api::net::mac::MacAddr; use super::*; @@ -126,7 +126,7 @@ impl DomainBuilder { pub fn build(mut self) -> Domain { if self.domain.devices.disk.iter().any(|d| d.boot.is_some()) { - debug!("Disk has boot order, removing style boot..."); + tracing::debug!("Disk has boot order, removing style boot..."); self.domain.os.boot = None; } self.domain @@ -159,10 +159,8 @@ impl IfaceBuilder { } /// Defines the MAC address the interface should use. - pub fn mac_addr(mut self, addr: &MacAddr) -> Self { - self.iface.mac = Some(NetMac { - address: addr.clone(), - }); + pub fn mac_addr(mut self, address: MacAddr) -> Self { + self.iface.mac = Some(NetMac { address }); self } diff --git a/nzrd/src/ctrl/virtxml/mod.rs b/nzr-virt/src/xml/mod.rs similarity index 100% rename from nzrd/src/ctrl/virtxml/mod.rs rename to nzr-virt/src/xml/mod.rs diff --git a/nzrd/src/ctrl/virtxml/test.rs b/nzr-virt/src/xml/test.rs similarity index 96% rename from nzrd/src/ctrl/virtxml/test.rs rename to nzr-virt/src/xml/test.rs index ef2556f..26fc248 100644 --- a/nzrd/src/ctrl/virtxml/test.rs +++ b/nzr-virt/src/xml/test.rs @@ -1,8 +1,8 @@ use uuid::uuid; +use super::build::DomainBuilder; use super::*; -use crate::ctrl::virtxml::build::DomainBuilder; -use crate::prelude::*; +use crate::datasize; trait Unprettify { fn unprettify(&self) -> String; @@ -61,7 +61,7 @@ fn domain_serde() { dsk.volume_source("tank", "test-vm-root") .target("sda", "virtio") }) - .net_device(|net| net.with_bridge("virbr0").mac_addr(&mac)) + .net_device(|net| net.with_bridge("virbr0").mac_addr(mac)) .build(); let dom_xml = quick_xml::se::to_string(&domain).unwrap(); println!("{}", dom_xml); diff --git a/nzrd/Cargo.toml b/nzrd/Cargo.toml index d71f882..22b3925 100644 --- a/nzrd/Cargo.toml +++ b/nzrd/Cargo.toml @@ -1,47 +1,56 @@ [package] name = "nzrd" -version = "0.1.0" +version = "1.0.0" edition = "2021" [dependencies] +# The usual +tokio = { version = "1", features = ["macros", "rt-multi-thread", "process"] } +tokio-serde = { version = "0.9", features = ["bincode"] } +futures = "0.3" +serde = { version = "1", features = ["derive"] } +nzr-api = { path = "../nzr-api", features = ["diesel"] } +nzr-virt = { path = "../nzr-virt" } +async-trait = "0.1" +tempfile = "3" +thiserror = "1.0.63" +uuid = { version = "1.2.2", features = ["serde"] } +trait-variant = "0.1" + +# RPC tarpc = { version = "0.34", features = [ "tokio1", "unix", "serde-transport", "serde-transport-bincode", ] } -tokio = { version = "1", features = ["macros", "rt-multi-thread", "process"] } -tokio-serde = { version = "0.9", features = ["bincode"] } -sled = "0.34.7" -virt = "0.4" -fatfs = "0.3" -uuid = { version = "1.2.2", features = [ - "v4", - "fast-rng", - "serde", - "macro-diagnostics", + +# Logging +# TODO: switch to tracing? +log = "0.4.17" +syslog = "7" + +# Database +diesel = { version = "2.2", features = [ + "r2d2", + "sqlite", + "returning_clauses_for_sqlite_3_35", ] } +diesel_migrations = "2.2" + clap = { version = "4.0.26", features = ["derive"] } -serde = { version = "1", features = ["derive"] } quick-xml = { version = "0.36", features = ["serialize"] } serde_with = "2" serde_yaml = "0.9.14" rand = "0.8.5" libc = "0.2.137" +nix = { version = "0.29", features = ["user", "fs"] } home = "0.5.4" stdext = "0.3.1" zerocopy = "0.7" -nzr-api = { path = "../api" } -futures = "0.3" -ciborium = "0.2.0" -ciborium-io = "0.2.0" hickory-server = "0.24" hickory-proto = { version = "0.24", features = ["serde-config"] } -async-trait = "0.1" -log = "0.4.17" -syslog = "7" -nix = { version = "0.29", features = ["user", "fs"] } -tempfile = "3" +paste = "1.0.15" [dev-dependencies] regex = "1" diff --git a/nzrd/migrations/2024080901_initial.sql b/nzrd/migrations/2024080901_initial.sql new file mode 100644 index 0000000..a677d73 --- /dev/null +++ b/nzrd/migrations/2024080901_initial.sql @@ -0,0 +1,24 @@ +CREATE TABLE subnets ( + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + ifname TEXT NOT NULL, + network TEXT NOT NULL, + start_host INTEGER NOT NULL, + end_host INTEGER NOT NULL, + gateway4 INTEGER, + dns TEXT, + domain_name TEXT, + vlan_id INTEGER +); + +CREATE TABLE instances ( + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + mac_addr TEXT NOT NULL, + subnet_id INTEGER NOT NULL, + host_num INTEGER NOT NULL, + ci_metadata TEXT NOT NULL, + ci_userdata BINARY, + UNIQUE(subnet_id, host_num), + FOREIGN KEY(subnet_id) REFERENCES subnet(id) +); \ No newline at end of file diff --git a/nzrd/src/cloud.rs b/nzrd/src/cloud.rs index 51b6f09..e0dc299 100644 --- a/nzrd/src/cloud.rs +++ b/nzrd/src/cloud.rs @@ -1,11 +1,9 @@ use std::net::Ipv4Addr; -use fatfs::FsOptions; use hickory_server::proto::rr::Name; use serde::Serialize; use serde_with::skip_serializing_none; use std::collections::HashMap; -use std::io::{prelude::*, Cursor}; use nzr_api::net::{cidr::CidrV4, mac::MacAddr}; @@ -149,44 +147,3 @@ impl<'a> DNSMeta<'a> { } } } - -pub fn create_image( - metadata: &Metadata, - netconfig: &NetworkMeta, - user_data: Option<&B>, -) -> Result>, Box> -where - B: AsRef<[u8]>, -{ - let mut image: Cursor> = 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) -} diff --git a/nzrd/src/cmd/net.rs b/nzrd/src/cmd/net.rs index 688135e..986796b 100644 --- a/nzrd/src/cmd/net.rs +++ b/nzrd/src/cmd/net.rs @@ -1,37 +1,46 @@ use super::*; -use crate::ctrl::net::Subnet; -use crate::ctrl::Entity; -use crate::ctrl::Storable; use crate::ctx::Context; +use crate::model::tx::Transaction; +use crate::model::Subnet; use nzr_api::model; pub async fn add_subnet( ctx: &Context, args: model::Subnet, -) -> Result, Box> { - let subnet = Subnet::from_model(&args.data) - .map_err(|er| cmd_error!("Couldn't generate subnet: {}", er))?; - - let mut ent = Subnet::insert(ctx.db.clone(), subnet.clone(), args.name.as_bytes())?; - - ent.transient = true; +) -> Result> { + let subnet = { + let s = Subnet::insert(ctx, args.name, args.data) + .await + .map_err(|er| cmd_error!("Couldn't generate subnet: {}", er))?; + Transaction::begin(ctx, s) + }; 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(ent) + Ok(subnet.take()) } } -pub fn delete_subnet(ctx: &Context, interface: &str) -> Result<(), Box> { - match Subnet::get_by_key(ctx.db.clone(), interface.as_bytes()) +pub async fn delete_subnet( + ctx: &Context, + name: impl AsRef, +) -> Result<(), Box> { + match Subnet::get_by_name(ctx, name.as_ref()) + .await .map_err(|er| cmd_error!("Couldn't find subnet: {}", er))? { - Some(subnet) => subnet - .delete() - .map_err(|er| cmd_error!("Couldn't fully delete subnet entry: {}", er)), - None => Err(cmd_error!("No subnet object found for {}", interface)), + Some(subnet) => { + if let Some(domain_name) = &subnet.domain_name { + ctx.zones.delete_zone(domain_name).await; + } + + subnet + .delete(ctx) + .await + .map_err(|er| cmd_error!("Couldn't fully delete subnet entry: {}", er)) + } + None => Err(cmd_error!("Subnet not found")), }?; Ok(()) diff --git a/nzrd/src/cmd/vm.rs b/nzrd/src/cmd/vm.rs index caafd18..07f3789 100644 --- a/nzrd/src/cmd/vm.rs +++ b/nzrd/src/cmd/vm.rs @@ -1,18 +1,16 @@ +use nzr_api::net::cidr::CidrV4; +use nzr_virt::error::DomainError; +use nzr_virt::xml::build::DomainBuilder; +use nzr_virt::xml::{self, SerialType}; +use nzr_virt::{datasize, dom, vol}; use tokio::sync::RwLock; -use virt::stream::Stream; use super::*; -use crate::cloud::{DNSMeta, EtherMatch, Metadata, NetworkMeta}; -use crate::ctrl::net::Subnet; -use crate::ctrl::virtxml::build::DomainBuilder; -use crate::ctrl::virtxml::{DiskDeviceType, SerialType, VolType, Volume}; -use crate::ctrl::vm::{InstDb, Instance, InstanceError, Progress}; -use crate::ctrl::Storable; +use crate::cloud::Metadata; +use crate::ctrl::vm::Progress; use crate::ctx::Context; -use crate::prelude::*; -use crate::virt::VirtVolume; -use hickory_server::proto::rr::Name; -use log::*; +use crate::model::{Instance, Subnet}; +use log::{debug, info, warn}; use nzr_api::args; use nzr_api::net::mac::MacAddr; use std::sync::Arc; @@ -32,10 +30,11 @@ pub async fn new_instance( ctx: Context, prog_task: Arc>, args: &args::NewInstance, -) -> Result> { +) -> Result<(Instance, dom::Domain), Box> { progress!(prog_task, 0.0, "Starting..."); // find the subnet corresponding to the interface - let subnet = Subnet::get_by_key(ctx.db.clone(), args.subnet.as_bytes()) + let subnet = Subnet::get_by_name(&ctx, &args.subnet) + .await .map_err(|er| cmd_error!("Unable to get interface: {}", er))? .ok_or(cmd_error!( "Subnet {} wasn't found in database", @@ -43,14 +42,19 @@ pub async fn new_instance( ))?; // bail if a domain already exists - if let Ok(dom) = virt::domain::Domain::lookup_by_name(&ctx.virt.conn, &args.name) { + if let Ok(dom) = ctx.virt.conn.get_instance(&args.name).await { Err(cmd_error!( "Domain with name already exists (uuid {})", - dom.get_uuid_string().unwrap_or("unknown".to_owned()) + dom.xml().await.uuid, )) } else { // make sure the base image exists - let mut base_image = VirtVolume::lookup_by_name(&ctx.virt.pools.baseimg, &args.base_image) + let mut base_image = ctx + .virt + .pools + .baseimg + .volume(&args.base_image) + .await .map_err(|er| cmd_error!("Couldn't find base image: {}", er))?; progress!(prog_task, 10.0, "Generating metadata..."); @@ -60,183 +64,150 @@ pub async fn new_instance( MacAddr::from_bytes(bytes) } .map_err(|er| cmd_error!("Unable to create a new MAC address: {}", er))?; - let lease = subnet - .new_lease(&mac_addr, &args.name) - .map_err(|er| cmd_error!("Failed to generate a new lease: {}", er))?; + + // Get highest host addr + 1 for our new addr + let addr = { + let addr_num = Instance::all_in_subnet(&ctx, &subnet) + .await? + .into_iter() + .max_by(|a, b| a.host_num.cmp(&b.host_num)) + .map_or(subnet.start_host, |i| i.host_num + 1); + if addr_num > subnet.end_host || addr_num < subnet.start_host { + Err(cmd_error!("Got invalid lease address for instance"))?; + } + let addr = subnet.network.make_ip(addr_num as u32)?; + CidrV4::new(addr, subnet.network.cidr()) + }; + + let lease = nzr_api::model::Lease { + subnet: subnet.name.clone(), + addr, + mac_addr, + }; // generate cloud-init data - let meta = Metadata::new(&args.name).ssh_pubkeys(&args.ssh_keys); - let netconfig = NetworkMeta::new().static_nic( - EtherMatch::mac_addr(&mac_addr), - &lease.ipv4_addr, - &subnet.gateway4, - DNSMeta::with_addrs( - { - let mut search: Vec = vec![ctx.config.dns.default_zone.clone()]; - if let Some(zone) = &subnet.domain_name { - search.push(zone.clone()); - } - Some(search) - }, - &subnet.dns, - ), - ); - let ci_data = crate::cloud::create_image(&meta, &netconfig, None as Option<&Vec>) - .map_err(|er| cmd_error!("Unable to create initial cloud-init image: {}", er))? - .into_inner(); + let ci_meta = { + let m = Metadata::new(&args.name).ssh_pubkeys(&args.ssh_keys); + serde_yaml::to_string(&m) + .map_err(|err| cmd_error!("Couldn't generate cloud-init metadata: {err}")) + }?; - // and upload it to a vol - let vol_data = Volume::new(&args.name, VolType::Raw, datasize!(1440 KiB)); - let mut cidata_vol = VirtVolume::create_xml(&ctx.virt.pools.cidata, vol_data, 0).await?; + let db_inst = + Instance::insert(&ctx, &args.name, &subnet, lease.clone(), ci_meta, None).await?; - let cistream = Stream::new(&cidata_vol.get_connect()?, 0)?; - if let Err(er) = cidata_vol.upload(&cistream, 0, datasize!(1440 KiB).into(), 0) { - cistream.abort().ok(); - cidata_vol.delete(0)?; - Err(cmd_error!("Failed to create cloud-init volume: {}", er)) - } else { - let mut idx: usize = 0; - while idx < ci_data.len() { - match cistream.send(&ci_data[idx..ci_data.len()]) { - Ok(sz) => idx += sz, - Err(er) => { - cistream.abort().ok(); - cidata_vol.delete(0)?; - return Err(cmd_error!("Failed uploading to cloud-init image: {}", er)); - } - } + progress!(prog_task, 30.0, "Creating instance images..."); + // create primary volume from base image + let mut pri_vol = base_image + .clone_vol( + &ctx.virt.pools.primary, + &args.name, + datasize!((args.disk_sizes.0) GiB), + ) + .await + .map_err(|er| cmd_error!("Failed to clone base image: {}", er))?; + + // and, if it exists: the second volume + let sec_vol = match args.disk_sizes.1 { + Some(sec_size) => { + let voldata = + // TODO: Fix VolType + xml::Volume::new(&args.name, xml::VolType::Qcow2, datasize!(sec_size GiB)); + Some(vol::Volume::create(&ctx.virt.pools.secondary, voldata, 0).await?) } - // mark the stream as finished - cistream.finish()?; + None => None, + }; - progress!(prog_task, 30.0, "Creating instance images..."); - // create primary volume from base image - let mut pri_vol = base_image - .clone_vol( - &ctx.virt.pools.primary, - &args.name, - datasize!((args.disk_sizes.0) GiB), - ) - .await - .map_err(|er| cmd_error!("Failed to clone base image: {}", er))?; + // build domain xml + let ifname = subnet.ifname.clone(); + let devname = format!( + "veth-{:02x}{:02x}{:02x}", + mac_addr[3], mac_addr[4], mac_addr[5] + ); + progress!(prog_task, 60.0, "Initializing instance..."); - // and, if it exists: the second volume - let sec_vol = match args.disk_sizes.1 { - Some(sec_size) => { - let voldata = Volume::new( - &args.name, - ctx.virt.pools.secondary.xml.vol_type(), - datasize!(sec_size GiB), - ); - Some(VirtVolume::create_xml(&ctx.virt.pools.secondary, voldata, 0).await?) - } - None => None, + let dom_xml = { + let pri_name = &ctx.config.storage.primary_pool; + let sec_name = &ctx.config.storage.secondary_pool; + + let mut instdata = DomainBuilder::default() + .name(&args.name) + .memory(datasize!((args.memory) MiB)) + .cpu_topology(1, 1, args.cores, 1) + .net_device(|nd| { + nd.mac_addr(mac_addr) + .with_bridge(&ifname) + .target_dev(&devname) + }) + .disk_device(|dsk| { + dsk.volume_source(pri_name, &pri_vol.name) + .target("vda", "virtio") + .qcow2() + .boot_order(1) + }) + .serial_device(SerialType::Pty); + + // add desription, if provided + instdata = match &args.description { + Some(desc) => instdata.description(desc), + None => instdata, }; - // build domain xml - let ifname = subnet.ifname.clone(); - let devname = format!( - "veth-{:02x}{:02x}{:02x}", - mac_addr[3], mac_addr[4], mac_addr[5] - ); - progress!(prog_task, 60.0, "Initializing instance..."); - let (mut inst, conn) = Instance::new(ctx.clone(), subnet, lease, { - let pri_name = &ctx.virt.pools.primary.xml.name; - let sec_name = &ctx.virt.pools.secondary.xml.name; - let cidata_name = &ctx.virt.pools.cidata.xml.name; - - let mut instdata = DomainBuilder::default() - .name(&args.name) - .memory(datasize!((args.memory) MiB)) - .cpu_topology(1, 1, args.cores, 1) - .net_device(|nd| { - nd.mac_addr(&mac_addr) - .with_bridge(&ifname) - .target_dev(&devname) - }) - .disk_device(|dsk| { - dsk.volume_source(pri_name, &pri_vol.name) - .target("vda", "virtio") - .qcow2() - .boot_order(1) - }) - .disk_device(|fda| { - fda.volume_source(cidata_name, &cidata_vol.name) - .device_type(DiskDeviceType::Disk) - .target("hda", "ide") - }) - .serial_device(SerialType::Pty); - - // add desription, if provided - instdata = match &args.description { - Some(desc) => instdata.description(desc), - None => instdata, - }; - - // add second volume, if provided - match &sec_vol { - Some(vol) => instdata.disk_device(|dsk| { - dsk.volume_source(sec_name, &vol.name) - .target("vdb", "virtio") - .qcow2() - }), - None => instdata, - } - }) - .await?; - - // not a fatal error, we can set autostart afterward - if let Err(er) = conn.set_autostart(true) { - warn!("Couldn't set autostart for domain: {}", er); + // add second volume, if provided + match &sec_vol { + Some(vol) => instdata.disk_device(|dsk| { + dsk.volume_source(sec_name, &vol.name) + .target("vdb", "virtio") + .qcow2() + }), + None => instdata, } + .build() + }; - tokio::task::spawn_blocking(move || { - if let Err(er) = conn.create() { - warn!("Domain defined, but couldn't be started! Error: {}", er); - } - }) - .await?; + let mut virt_dom = ctx.virt.conn.define_instance(dom_xml).await?; - // set all volumes to persistent to avoid deletion - pri_vol.persist = true; - if let Some(mut sec_vol) = sec_vol { - sec_vol.persist = true; - } - cidata_vol.persist = true; - inst.persist(); - - progress!(prog_task, 80.0, "Domain created!"); - debug!("Domain {} created!", inst.xml().name.as_str()); - Ok(inst) + // not a fatal error, we can set autostart afterward + if let Err(er) = virt_dom.autostart(true).await { + warn!("Couldn't set autostart for domain: {}", er); } + + if let Err(er) = virt_dom.start().await { + warn!("Domain defined, but couldn't be started! Error: {}", er); + } + + // set all volumes to persistent to avoid deletion + pri_vol.persist = true; + if let Some(mut sec_vol) = sec_vol { + sec_vol.persist = true; + } + virt_dom.persist().await; + + progress!(prog_task, 80.0, "Domain created!"); + debug!("Domain {} created!", virt_dom.xml().await.name.as_str()); + Ok((db_inst, virt_dom)) } } pub async fn delete_instance(ctx: Context, name: String) -> Result<(), Box> { - let mut inst = Instance::lookup_by_name(ctx.clone(), &name) - .await? - .ok_or(cmd_error!("No such domain!"))?; - - let conn = inst.virt()?; - if conn.is_active()? { - conn.destroy() - .map_err(|er| cmd_error!("Failed to destroy domain: {}", er))?; - } - - inst.undefine().await?; + let Some(inst_db) = Instance::get_by_name(&ctx, &name).await? else { + return Err(cmd_error!("Instance {name} not found")); + }; + let mut inst = ctx.virt.conn.get_instance(name.clone()).await?; + inst.undefine(true).await?; + inst_db.delete(&ctx).await?; Ok(()) } -pub fn prune_instances(ctx: &Context) -> Result<(), Box> { - for entity in InstDb::all(ctx.db.clone())? { - let entity = entity?; - if let Err(InstanceError::DomainNotFound(name)) = - Instance::from_entity(ctx.clone(), entity.clone()) - { - info!("Instance {} was invalid, deleting", name); - if let Err(err) = entity.delete() { - warn!("Couldn't delete {}: {}", name, err); +pub async fn prune_instances(ctx: &Context) -> Result<(), Box> { + for entity in Instance::all(ctx).await? { + if let Err(err) = ctx.virt.conn.get_instance(&entity.name).await { + if err == DomainError::DomainNotFound { + info!("Invalid domain {}, deleting", &entity.name); + let name = entity.name.clone(); + if let Err(err) = entity.delete(ctx).await { + warn!("Couldn't delete {}: {}", name, err); + } } } } diff --git a/nzrd/src/ctrl/mod.rs b/nzrd/src/ctrl/mod.rs index 296b763..601eb02 100644 --- a/nzrd/src/ctrl/mod.rs +++ b/nzrd/src/ctrl/mod.rs @@ -1,292 +1,2 @@ -use std::{ - marker::PhantomData, - ops::{Deref, DerefMut}, -}; - -use serde::{Deserialize, Serialize}; - -use log::*; -use std::fmt; - pub mod net; -pub mod virtxml; pub mod vm; - -#[derive(Clone)] -pub struct Entity -where - T: Storable + Serialize, -{ - inner: T, - key: Vec, - tree: sled::Tree, - db: sled::Db, - pub transient: bool, -} - -impl Deref for Entity -where - T: Storable, -{ - type Target = T; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl DerefMut for Entity -where - T: Storable, -{ - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -impl Drop for Entity -where - T: Storable, -{ - fn drop(&mut self) { - if self.transient { - let key_str = String::from_utf8_lossy(&self.key); - debug!("Transient flag enabled for {}, dropping!", &key_str); - if let Err(err) = self.delete() { - warn!("Couldn't delete {} from database: {}", &key_str, err); - } - } - } -} - -impl Entity -where - T: Storable, -{ - pub fn transient(inner: T, key: V, tree: sled::Tree, db: sled::Db) -> Self - where - V: AsRef<[u8]>, - { - Entity { - inner, - key: key.as_ref().to_owned(), - tree, - db, - transient: true, - } - } - - pub fn key(&self) -> &[u8] { - &self.key - } -} - -impl Entity -where - T: Storable + Serialize, -{ - pub fn update(&self) -> Result<(), StorableError> { - let mut bytes: Vec = Vec::new(); - ciborium::ser::into_writer(&self.inner, &mut bytes) - .map_err(|e| StorableError::new(ErrType::SerializeFailed, e))?; - self.tree - .insert(&self.key, bytes.as_slice()) - .map_err(|e| StorableError::new(ErrType::DbError, e))?; - Ok(()) - } - - pub fn replace(&mut self, other: T) -> Result<(), StorableError> { - self.inner = other; - self.update() - } - - pub fn delete(&self) -> Result<(), StorableError> { - self.on_delete(&self.db)?; - self.tree - .remove(&self.key) - .map_err(|e| StorableError::new(ErrType::DbError, e))?; - Ok(()) - } -} - -#[derive(Debug)] -pub enum ErrType { - DbError, - DeserializeFailed, - SerializeFailed, -} - -#[derive(Debug)] -pub struct StorableError { - err_type: ErrType, - inner: Option>, -} - -impl StorableError { - fn new(err_type: ErrType, inner: E) -> Self - where - E: std::error::Error + 'static, - { - Self { - err_type, - inner: Some(Box::new(inner)), - } - } -} - -impl fmt::Display for ErrType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::DbError => write!(f, "Database error"), - Self::DeserializeFailed => write!(f, "Deserialize failed"), - Self::SerializeFailed => write!(f, "Serialize failed"), - } - } -} - -impl fmt::Display for StorableError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.err_type.fmt(f)?; - if let Some(inner) = &self.inner { - write!(f, ": {}", inner)?; - } - Ok(()) - } -} - -impl std::error::Error for StorableError {} - -pub trait Storable -where - for<'de> Self: Deserialize<'de> + Serialize, -{ - fn tree_name() -> Option<&'static [u8]>; - - fn get_by_key(db: sled::Db, key: &[u8]) -> Result>, StorableError> { - let tree_name = match Self::tree_name() { - Some(tn) => tn, - None => unimplemented!(), - }; - let tree = db - .open_tree(tree_name) - .map_err(|e| StorableError::new(ErrType::DbError, e))?; - match tree - .get(key) - .map_err(|e| StorableError::new(ErrType::DbError, e))? - { - Some(vec) => { - let deserialized: Self = ciborium::de::from_reader(&*vec) - .map_err(|e| StorableError::new(ErrType::DeserializeFailed, e))?; - Ok(Some(Entity { - inner: deserialized, - key: key.to_owned(), - tree, - db, - transient: false, - })) - } - None => Ok(None), - } - } - - fn insert(db: sled::Db, item: Self, key: &[u8]) -> Result, StorableError> { - let tree_name = match Self::tree_name() { - Some(tn) => tn, - None => unimplemented!(), - }; - let tree = db - .open_tree(tree_name) - .map_err(|e| StorableError::new(ErrType::DbError, e))?; - let ent = Entity { - inner: item, - key: key.to_owned(), - tree, - db, - transient: false, - }; - ent.update()?; - Ok(ent) - } - - /// Requests all items from the database, as a [`StorIter`]. - fn all(db: sled::Db) -> Result, StorableError> { - let tree_name = match Self::tree_name() { - Some(tn) => tn, - None => unimplemented!(), - }; - let tree = db - .open_tree(tree_name) - .map_err(|e| StorableError::new(ErrType::DbError, e))?; - Ok(StorIter::new(db, tree)) - } - - /// Function to allow storable objects to perform actions on deletion. - fn on_delete(&self, _db: &sled::Db) -> Result<(), StorableError> { - // No-op - debug!("deleting; Storable no-op!"); - Ok(()) - } -} - -/// Iterator of [`Storable`]s in the running database. -pub struct StorIter -where - T: Storable, -{ - db: sled::Db, - tree: sled::Tree, - iter: sled::Iter, - phantom: PhantomData, -} - -impl StorIter -where - T: Storable, -{ - /// Creates a new iterator of [`Storable`]s using a [`sled::Db`] and - /// [`sled::Tree`]. - fn new(db: sled::Db, tree: sled::Tree) -> Self { - Self { - db, - tree: tree.clone(), - iter: tree.iter(), - phantom: PhantomData, - } - } -} - -impl Iterator for StorIter -where - T: Storable, -{ - type Item = Result, StorableError>; - - fn next(&mut self) -> Option { - if let Some(next) = self.iter.next() { - match next { - Ok((key, val)) => { - let inner = { - let vec = val.to_vec(); - let inner = ciborium::de::from_reader(vec.as_slice()) - .map_err(|e| StorableError::new(ErrType::DeserializeFailed, e)); - match inner { - Ok(inner) => inner, - Err(err) => { - return Some(Err(err)); - } - } - }; - Some(Ok(Entity { - inner, - key: key.to_vec(), - tree: self.tree.clone(), - db: self.db.clone(), - transient: false, - })) - } - Err(err) => Some(Err(StorableError::new(ErrType::DbError, err))), - } - } else { - None - } - } -} diff --git a/nzrd/src/ctrl/net.rs b/nzrd/src/ctrl/net.rs index 57eeab6..e69de29 100644 --- a/nzrd/src/ctrl/net.rs +++ b/nzrd/src/ctrl/net.rs @@ -1,176 +0,0 @@ -use super::{Entity, StorIter}; -use nzr_api::model::SubnetData; -use nzr_api::net::cidr::CidrV4; -use nzr_api::net::mac::MacAddr; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -use std::fmt; -use std::ops::Deref; - -use super::Storable; - -#[skip_serializing_none] -#[derive(Clone, Serialize, Deserialize)] -pub struct Subnet { - pub model: SubnetData, -} - -impl Deref for Subnet { - type Target = SubnetData; - - fn deref(&self) -> &Self::Target { - &self.model - } -} - -impl From<&Subnet> for SubnetData { - fn from(value: &Subnet) -> Self { - value.model.clone() - } -} - -impl From<&SubnetData> for Subnet { - fn from(value: &SubnetData) -> Self { - Self { - model: value.clone(), - } - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct Lease { - pub subnet: String, - pub ipv4_addr: CidrV4, - pub mac_addr: MacAddr, - pub inst_name: String, -} - -#[derive(Debug)] -pub enum SubnetError { - DbError(sled::Error), - SubnetExists, - BadNetwork(nzr_api::net::cidr::Error), - BadData, - BadStartHost, - BadEndHost, - BadRange, - HostOutsideRange, - BadHost(nzr_api::net::cidr::Error), - CantDelete(sled::Error), - SubnetFull, - BadDomainName, -} - -impl fmt::Display for SubnetError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::DbError(er) => write!(f, "Database error: {}", er), - Self::SubnetExists => write!(f, "Subnet already exists"), - Self::BadNetwork(er) => write!(f, "Error deserializing network from database: {}", er), - Self::BadData => write!(f, "Malformed data in database"), - Self::BadStartHost => write!(f, "Starting host is not in provided subnet"), - Self::BadEndHost => write!(f, "Ending host is not in provided subnet"), - Self::BadRange => write!(f, "Ending host is before starting host"), - Self::HostOutsideRange => write!(f, "Available host is outside defined host range"), - Self::BadHost(er) => write!( - f, - "Host is within range but couldn't be converted to IP: {}", - er - ), - Self::CantDelete(de) => write!(f, "Error when trying to delete: {}", de), - Self::SubnetFull => write!(f, "No addresses are left to assign in subnet"), - Self::BadDomainName => { - write!(f, "Invalid domain name. Must be in the format xx.yy.tld") - } - } - } -} - -impl std::error::Error for SubnetError {} - -impl Storable for Subnet { - fn tree_name() -> Option<&'static [u8]> { - Some(b"nets") - } - - fn on_delete(&self, db: &sled::Db) -> Result<(), super::StorableError> { - db.drop_tree(self.lease_tree()) - .map_err(|e| super::StorableError::new(super::ErrType::DbError, e))?; - Ok(()) - } -} - -impl Subnet { - pub fn from_model(data: &nzr_api::model::SubnetData) -> Result { - // validate start and end addresses - if data.end_host < data.start_host { - Err(SubnetError::BadRange) - } else if !data.network.contains(&data.start_host) { - Err(SubnetError::BadStartHost) - } else if !data.network.contains(&data.end_host) { - Err(SubnetError::BadEndHost) - } else { - let subnet = Subnet { - model: data.clone(), - }; - Ok(subnet) - } - } - - /// Gets the lease tree from sled. - pub fn lease_tree(&self) -> Vec { - let mut lt_name: Vec = vec![b'L']; - lt_name.extend_from_slice(&self.model.network.octets()); - lt_name - } -} - -impl Storable for Lease { - fn tree_name() -> Option<&'static [u8]> { - None - } -} - -impl Entity { - /// Create a new lease associated with the subnet. - pub fn new_lease( - &self, - mac_addr: &MacAddr, - inst_name: &str, - ) -> Result, Box> { - let tree = self.db.open_tree(self.lease_tree())?; - let max_lease = match tree.last()? { - Some(lease) => { - // XXX: this is overkill, but a lazy hack for now - u32::from_be_bytes(lease.0[..4].try_into().unwrap()) - & !u32::from(self.model.network.netmask()) - } - None => self.model.start_bytes(), - }; - let new_ip = self - .model - .network - .make_ip(max_lease + 1) - .map_err(SubnetError::BadHost)?; - let lease_data = Lease { - subnet: String::from_utf8_lossy(&self.key).to_string(), - ipv4_addr: CidrV4::new(new_ip, self.model.network.cidr()), - mac_addr: mac_addr.clone(), - inst_name: inst_name.to_owned(), - }; - let lease_tree = self - .db - .open_tree(self.lease_tree()) - .map_err(SubnetError::DbError)?; - let octets = lease_data.ipv4_addr.addr.octets(); - let ent = Entity::transient(lease_data, octets, lease_tree, self.db.clone()); - ent.update()?; - Ok(ent) - } - - /// Get an iterator over all leases in the subnet. - pub fn leases(&self) -> Result, sled::Error> { - let lease_tree = self.db.open_tree(self.lease_tree())?; - Ok(StorIter::new(self.db.clone(), lease_tree)) - } -} diff --git a/nzrd/src/ctrl/vm.rs b/nzrd/src/ctrl/vm.rs index 579d825..1929bd9 100644 --- a/nzrd/src/ctrl/vm.rs +++ b/nzrd/src/ctrl/vm.rs @@ -1,331 +1,5 @@ -use crate::ctrl::net::Lease; -use crate::ctx::Context; -use log::*; -use nzr_api::net::cidr::CidrV4; -use nzr_api::net::mac::MacAddr; -use std::net::Ipv4Addr; -use std::str::{self, Utf8Error}; - -use super::virtxml::{build::DomainBuilder, Domain}; -use super::Storable; -use super::{net::Subnet, Entity}; -use crate::virt::*; -use serde::{Deserialize, Serialize}; - #[derive(Clone)] pub struct Progress { pub status_text: String, pub percentage: f32, } - -#[derive(Clone, Serialize, Deserialize)] -pub struct InstDb { - uuid: uuid::Uuid, - lease_subnet: Vec, - lease_addr: CidrV4, -} - -impl InstDb { - pub fn addr(&self) -> Ipv4Addr { - self.lease_addr.addr - } -} - -impl Storable for InstDb { - fn tree_name() -> Option<&'static [u8]> { - Some(b"instances") - } -} - -impl From> for nzr_api::model::Instance { - fn from(value: Entity) -> Self { - nzr_api::model::Instance { - name: String::from_utf8_lossy(&value.key).to_string(), - uuid: value.uuid, - lease: Some(nzr_api::model::Lease { - subnet: String::from_utf8_lossy(&value.lease_subnet).to_string(), - addr: value.lease_addr.clone(), - mac_addr: MacAddr::invalid(), - }), - state: nzr_api::model::DomainState::NoState, - } - } -} - -pub struct Instance { - db_data: Entity, - lease: Option>, - ctx: Context, - domain_xml: Domain, -} - -impl Instance { - pub async fn new( - ctx: Context, - subnet: Entity, - lease: Entity, - builder: DomainBuilder, - ) -> Result<(Self, virt::domain::Domain), InstanceError> { - let domain_xml = builder.build(); - - let virt_domain = { - let inst_xml = - quick_xml::se::to_string(&domain_xml).map_err(InstanceError::CantSerialize)?; - virt::domain::Domain::define_xml(&ctx.virt.conn, &inst_xml) - .map_err(InstanceError::CreationFailed)? - }; - - // Get the final XML data back from libvirt; this will contain the UUID and - // other auto-filled stuff - let real_xml = match virt_domain.get_xml_desc(0) { - Ok(xml_data) => match quick_xml::de::from_str::(&xml_data) { - Ok(xml_obj) => xml_obj, - Err(err) => { - error!("Failed to deserialize XML from libvirt: {}", err); - if let Err(err) = virt_domain.undefine() { - warn!("Couldn't undefine domain after failure: {}", err); - } - return Err(InstanceError::CantDeserialize(err)); - } - }, - Err(err) => { - error!("Failed to get XML data from libvirt: {}", err); - if let Err(err) = virt_domain.undefine() { - warn!("Couldn't undefine domain after failure: {}", err); - } - return Err(InstanceError::VirtError(err)); - } - }; - - debug!( - "Adding {} (interface: {}) to the instance tree...", - &lease.ipv4_addr, &subnet.ifname, - ); - - let db_data = InstDb { - uuid: real_xml.uuid, - lease_subnet: subnet.key().to_vec(), - lease_addr: lease.ipv4_addr.clone(), - }; - - let db_data = InstDb::insert(ctx.db.clone(), db_data, real_xml.name.as_bytes()) - .map_err(InstanceError::other)?; - - let inst_obj = Instance { - db_data, - lease: Some(lease), - ctx, - domain_xml, - }; - - Ok((inst_obj, virt_domain)) - } - - pub fn uuid(&self) -> uuid::Uuid { - self.db_data.uuid - } - - pub fn persist(&mut self) { - if let Some(lease) = &mut self.lease { - lease.transient = false; - } - - self.db_data.transient = false; - } - - pub async fn undefine(&mut self) -> Result<(), InstanceError> { - let virt_domain = self.virt()?; - let connect = virt_domain - .get_connect() - .map_err(InstanceError::VirtError)?; - - // delete volumes - for disk in self.domain_xml.devices.disks() { - if let (Some(pool), Some(vol)) = (&disk.source.pool, &disk.source.volume) { - if let Ok(vpool) = VirtPool::lookup_by_name(&connect, pool) { - match VirtVolume::lookup_by_name(vpool, vol) { - Ok(virt_vol) => { - if let Err(er) = virt_vol.delete(0) { - warn!("Can't delete {}/{}: {}", pool, vol, er); - } - } - Err(er) => { - warn!("Can't acquire handle to {}/{}: {}", pool, vol, er); - } - } - } - } - } - - // undefine IP lease - if let Some(lease) = &mut self.lease { - lease.delete().map_err(InstanceError::other)?; - } - - // delete instance - virt_domain - .undefine() - .map_err(InstanceError::DomainDelete)?; - self.db_data.delete().map_err(InstanceError::other)?; - Ok(()) - } - - /// Create an Instance from a given InstDb entity. - pub fn from_entity(ctx: Context, db_data: Entity) -> Result { - let name = String::from_utf8_lossy(&db_data.key).into_owned(); - let virt_domain = match virt::domain::Domain::lookup_by_name(&ctx.virt.conn, &name) { - Ok(inst) => Ok(inst), - Err(err) => { - if err.code() == virt::error::ErrorNumber::NoDomain { - // domain not found - Err(InstanceError::DomainNotFound(name.to_owned())) - } else { - Err(InstanceError::VirtError(err)) - } - } - }?; - let domain_xml: Domain = { - let xml_str = virt_domain - .get_xml_desc(0) - .map_err(InstanceError::VirtError)?; - quick_xml::de::from_str(&xml_str).map_err(InstanceError::CantDeserialize)? - }; - - let lease = match Subnet::get_by_key(ctx.db.clone(), &db_data.lease_subnet) - .map_err(InstanceError::other)? - { - Some(subnet) => subnet - .leases() - .map_err(InstanceError::other)? - .find(|l| { - if let Ok(lease) = l { - lease.ipv4_addr == db_data.lease_addr - } else { - false - } - }) - .map(|o| o.unwrap()), - None => None, - }; - - Ok(Self { - ctx, - domain_xml, - db_data, - lease, - }) - } - - pub async fn lookup_by_name(ctx: Context, name: &str) -> Result, InstanceError> { - let db_data = match InstDb::get_by_key(ctx.db.clone(), name.as_bytes()) - .map_err(InstanceError::other)? - { - Some(data) => data, - None => { - return Ok(None); - } - }; - - // TODO: handle from_instdb having None? - Self::from_entity(ctx, db_data).map(Some) - } - - pub fn virt(&self) -> Result { - let name = self.domain_xml.name.as_str(); - match virt::domain::Domain::lookup_by_name(&self.ctx.virt.conn, name) { - Ok(inst) => Ok(inst), - Err(err) => { - if err.code() == virt::error::ErrorNumber::NoDomain { - // domain not found - Err(InstanceError::DomainNotFound(name.to_owned())) - } else { - Err(InstanceError::VirtError(err)) - } - } - } - } - - pub fn xml(&self) -> &Domain { - &self.domain_xml - } - - pub fn ip_lease(&self) -> Option<&Lease> { - self.lease.as_deref() - } -} - -impl From<&Instance> for nzr_api::model::Instance { - fn from(value: &Instance) -> Self { - nzr_api::model::Instance { - name: value.domain_xml.name.clone(), - uuid: value.domain_xml.uuid, - lease: value.lease.as_ref().map(|l| nzr_api::model::Lease { - subnet: l.subnet.clone(), - addr: l.ipv4_addr.clone(), - mac_addr: l.mac_addr.clone(), - }), - state: value.virt().map_or(Default::default(), |domain| { - domain - .get_state() - .map_or(Default::default(), |(code, _reason)| code.into()) - }), - } - } -} - -#[derive(Debug)] -pub enum InstanceError { - VirtError(virt::error::Error), - NotInDb, - CantDeserialize(quick_xml::de::DeError), - CantSerialize(quick_xml::de::DeError), - DbError(sled::Error), - MalformedData, - DomainNotFound(String), - CreationFailed(virt::error::Error), - BadInterface(Utf8Error), - NoSubnetForInterface, - Other(Box), - LeaseNotInDb, - DomainDelete(virt::error::Error), - LeaseUndefined, -} - -impl std::fmt::Display for InstanceError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::VirtError(er) => er.fmt(f), - Self::NotInDb => write!(f, "Domain exists in libvirt but is not in database"), - Self::CantDeserialize(er) => write!(f, "Deserializing domain XML failed: {}", er), - Self::CantSerialize(er) => write!(f, "Serializing domain XML failed: {}", er), - Self::DbError(er) => write!(f, "Database error: {}", er), - Self::DomainNotFound(name) => write!(f, "No domain {} found in libvirt", name), - Self::MalformedData => write!(f, "Entry has malformed data in database"), - Self::CreationFailed(er) => write!(f, "Error while creating domain: {}", er), - Self::BadInterface(er) => { - write!(f, "Couldn't get interface name from database: {}", er) - } - Self::NoSubnetForInterface => { - write!(f, "Interface associated with instance isn't in database!") - } - Self::LeaseNotInDb => write!( - f, - "Found IP address, but it doesn't correspond to a lease in the database" - ), - Self::DomainDelete(ve) => write!(f, "Couldn't delete libvirt domain: {}", ve), - Self::LeaseUndefined => write!(f, "Lease has been undefined by another function"), - Self::Other(er) => er.fmt(f), - } - } -} - -impl InstanceError { - fn other(err: E) -> Self - where - E: std::error::Error + 'static, - { - Self::Other(Box::new(err)) - } -} - -impl std::error::Error for InstanceError {} diff --git a/nzrd/src/ctx.rs b/nzrd/src/ctx.rs index c85909d..17cb3d3 100644 --- a/nzrd/src/ctx.rs +++ b/nzrd/src/ctx.rs @@ -1,32 +1,26 @@ -use std::{fmt, ops::Deref}; -use virt::connect::Connect; +use diesel::{ + r2d2::{ConnectionManager, Pool, PooledConnection}, + SqliteConnection, +}; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use nzr_virt::{vol, Connection}; +use std::ops::Deref; +use thiserror::Error; -use crate::{dns::ZoneData, virt::VirtPool}; +use crate::dns::ZoneData; use nzr_api::config::Config; use std::sync::Arc; -pub struct PoolRefs { - pub primary: VirtPool, - pub secondary: VirtPool, - pub cidata: VirtPool, - pub baseimg: VirtPool, -} +const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); -impl PoolRefs { - pub fn find_pool(&self, name: &str) -> Option<&VirtPool> { - for pool in [&self.primary, &self.secondary, &self.baseimg, &self.cidata] { - if let Ok(pool_name) = pool.get_name() { - if pool_name == name { - return Some(pool); - } - } - } - None - } +pub struct PoolRefs { + pub primary: vol::Pool, + pub secondary: vol::Pool, + pub baseimg: vol::Pool, } pub struct VirtCtx { - pub conn: virt::connect::Connect, + pub conn: nzr_virt::Connection, pub pools: PoolRefs, } @@ -41,50 +35,56 @@ impl Deref for Context { } pub struct InnerCtx { - pub db: sled::Db, + pub sqldb: diesel::r2d2::Pool>, pub config: Config, pub zones: crate::dns::ZoneData, pub virt: VirtCtx, } -#[derive(Debug)] +#[derive(Debug, Error)] pub enum ContextError { - Virt(virt::error::Error), - Db(sled::Error), - Pool(crate::virt::PoolError), + #[error("libvirt error: {0}")] + Virt(#[from] nzr_virt::error::VirtError), + #[error("Database error: {0}")] + Sql(#[from] diesel::r2d2::PoolError), + #[error("Unable to apply database migrations: {0}")] + DbMigrate(String), + #[error("Error opening libvirt pool: {0}")] + Pool(#[from] nzr_virt::error::PoolError), } -impl fmt::Display for ContextError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Virt(ve) => write!(f, "Error connecting to libvirt: {}", ve), - Self::Db(de) => write!(f, "Error opening database: {}", de), - Self::Pool(pe) => write!(f, "Error opening pool: {}", pe), - } - } -} - -impl std::error::Error for ContextError {} - impl InnerCtx { - fn new(config: Config) -> Result { + async fn new(config: Config) -> Result { let zones = ZoneData::new(&config.dns); - let conn = Connect::open(Some(&config.libvirt_uri)).map_err(ContextError::Virt)?; - virt::error::clear_error_callback(); + let conn = Connection::open(&config.libvirt_uri)?; let pools = PoolRefs { - primary: VirtPool::lookup_by_name(&conn, &config.storage.primary_pool) - .map_err(ContextError::Pool)?, - secondary: VirtPool::lookup_by_name(&conn, &config.storage.secondary_pool) - .map_err(ContextError::Pool)?, - cidata: VirtPool::lookup_by_name(&conn, &config.storage.ci_image_pool) - .map_err(ContextError::Pool)?, - baseimg: VirtPool::lookup_by_name(&conn, &config.storage.base_image_pool) - .map_err(ContextError::Pool)?, + primary: conn.get_pool(&config.storage.primary_pool).await?, + secondary: conn.get_pool(&config.storage.secondary_pool).await?, + baseimg: conn.get_pool(&config.storage.base_image_pool).await?, }; + let db_uri = config.db_uri.clone(); + let sqldb = tokio::task::spawn_blocking(|| { + let manager = ConnectionManager::::new(db_uri); + + Pool::builder().test_on_check_out(true).build(manager) + }) + .await + .unwrap()?; + + { + let mut conn = sqldb.get()?; + tokio::task::spawn_blocking(move || { + conn.run_pending_migrations(MIGRATIONS) + .map_or_else(|e| Err(ContextError::DbMigrate(e.to_string())), |_| Ok(())) + }) + .await + .unwrap()?; + } + Ok(Self { - db: sled::open(&config.db_path).map_err(ContextError::Db)?, + sqldb, config, zones, virt: VirtCtx { conn, pools }, @@ -92,9 +92,35 @@ impl InnerCtx { } } +pub type DbConn = PooledConnection>; + impl Context { - pub fn new(config: Config) -> Result { - let inner = InnerCtx::new(config)?; + pub async fn new(config: Config) -> Result { + let inner = InnerCtx::new(config).await?; Ok(Self(Arc::new(inner))) } + + /// Gets a connection to the database from the pool. + pub async fn db( + &self, + ) -> Result>, diesel::r2d2::PoolError> + { + let pool = self.sqldb.clone(); + tokio::task::spawn_blocking(move || pool.get()) + .await + .unwrap() + } + + pub async fn spawn_db( + &self, + f: impl FnOnce(DbConn) -> R + Send + 'static, + ) -> Result + where + R: Send + 'static, + { + let pool = self.sqldb.clone(); + tokio::task::spawn_blocking(move || pool.get().map(f)) + .await + .unwrap() + } } diff --git a/nzrd/src/dns.rs b/nzrd/src/dns.rs index 5ddf271..81eddee 100644 --- a/nzrd/src/dns.rs +++ b/nzrd/src/dns.rs @@ -1,4 +1,4 @@ -use crate::ctrl::net::Subnet; +use crate::model::Subnet; use log::*; use nzr_api::config::DNSConfig; use std::borrow::Borrow; @@ -118,11 +118,14 @@ impl InnerZD { } } + /// Creates a new DNS zone for the given subnet. pub async fn new_zone(&self, subnet: &Subnet) -> Result<(), Box> { if let Some(name) = &subnet.domain_name { + let name: Name = name.parse()?; + let rectree = make_rectree_with_soa(&name, &self.config); let auth = InMemoryAuthority::new( - name.clone(), - make_rectree_with_soa(name, &self.config), + name, + rectree, hickory_server::authority::ZoneType::Primary, false, )?; @@ -150,10 +153,12 @@ impl InnerZD { .upsert(auth_arc.origin().clone(), Box::new(auth_arc.clone())); } - pub async fn delete_zone(&self, interface: &str) -> bool { - self.map.lock().await.remove(interface).is_some() + /// Deletes the DNS zone. + pub async fn delete_zone(&self, domain_name: &str) -> bool { + self.map.lock().await.remove(domain_name).is_some() } + /// Adds a new host record in the DNS zone. pub async fn new_record( &self, interface: &str, diff --git a/nzrd/src/main.rs b/nzrd/src/main.rs index 5deafc9..6a5cef7 100644 --- a/nzrd/src/main.rs +++ b/nzrd/src/main.rs @@ -3,17 +3,15 @@ mod cmd; mod ctrl; mod ctx; mod dns; -mod img; -mod prelude; +mod model; mod rpc; #[cfg(test)] mod test; -mod virt; -use crate::ctrl::{net::Subnet, Storable}; use hickory_server::ServerFuture; use log::LevelFilter; use log::*; +use model::{Instance, Subnet}; use nzr_api::config; use std::str::FromStr; use tokio::net::UdpSocket; @@ -21,7 +19,7 @@ use tokio::net::UdpSocket; #[tokio::main(flavor = "multi_thread")] async fn main() -> Result<(), Box> { let cfg: config::Config = config::Config::figment().extract()?; - let ctx = ctx::Context::new(cfg)?; + let ctx = ctx::Context::new(cfg).await?; syslog::init_unix( syslog::Facility::LOG_DAEMON, @@ -29,51 +27,35 @@ async fn main() -> Result<(), Box> { )?; info!("Hydrating initial zones..."); - for subnet in Subnet::all(ctx.db.clone())? { - match subnet { - Ok(subnet) => { - // A records - if let Err(err) = ctx.zones.new_zone(&subnet).await { - error!("Couldn't create zone for {}: {}", &subnet.ifname, err); - continue; - } - match subnet.leases() { - Ok(leases) => { - for lease in leases { - match lease { - Ok(lease) => { - if let Err(err) = ctx - .zones - .new_record( - &subnet.ifname.to_string(), - &lease.inst_name, - lease.ipv4_addr.addr, - ) - .await - { - error!( - "Failed to set up lease for {} in {}: {}", - &lease.inst_name, &subnet.ifname, err - ); - } - } - Err(err) => { - warn!( - "Lease iterator error while hydrating {}: {}", - &subnet.ifname, err - ); - } - } - } - } - Err(err) => { - error!("Couldn't get leases for {}: {}", &subnet.ifname, err); + for subnet in Subnet::all(&ctx).await? { + // A records + if let Err(err) = ctx.zones.new_zone(&subnet).await { + error!("Couldn't create zone for {}: {}", &subnet.ifname, err); + continue; + } + match Instance::all_in_subnet(&ctx, &subnet).await { + Ok(leases) => { + for lease in leases { + let Ok(lease_addr) = subnet.network.make_ip(lease.host_num as u32) else { + warn!("Ignoring {} due to lease address issue", &lease.name); continue; + }; + + if let Err(err) = ctx + .zones + .new_record(&subnet.ifname.to_string(), &lease.name, lease_addr) + .await + { + error!( + "Failed to set up lease for {} in {}: {}", + &lease.name, &subnet.ifname, err + ); } } } Err(err) => { - warn!("Error while iterating subnets: {}", err); + error!("Couldn't get leases for {}: {}", &subnet.ifname, err); + continue; } } } @@ -84,7 +66,7 @@ async fn main() -> Result<(), Box> { dns_listener.register_socket(dns_socket); tokio::select! { - res = rpc::serve(ctx.clone(), ctx.zones.clone()) => { + res = rpc::serve(ctx.clone()) => { if let Err(err) = res { error!("Error from RPC: {}", err); } diff --git a/nzrd/src/model/mod.rs b/nzrd/src/model/mod.rs new file mode 100644 index 0000000..e134da5 --- /dev/null +++ b/nzrd/src/model/mod.rs @@ -0,0 +1,437 @@ +use std::{net::Ipv4Addr, str::FromStr}; + +pub mod tx; + +use diesel::{associations::HasTable, prelude::*}; +use hickory_proto::rr::Name; +use nzr_api::{ + model::SubnetData, + net::{ + cidr::{self, CidrV4}, + mac::MacAddr, + }, +}; +use thiserror::Error; + +use crate::ctx::Context; +use tx::Transactable; + +#[derive(Debug, Error)] +pub enum ModelError { + #[error("Database error occured: {0}")] + Db(#[from] diesel::result::Error), + #[error("Unable to get database handle: {0}")] + Pool(#[from] diesel::r2d2::PoolError), + #[error("{0}")] + Cidr(#[from] cidr::Error), +} + +diesel::table! { + instances { + id -> Integer, + name -> Text, + mac_addr -> Text, + subnet_id -> Integer, + host_num -> Integer, + ci_metadata -> Text, + ci_userdata -> Nullable, + } +} + +diesel::table! { + subnets { + id -> Integer, + name -> Text, + ifname -> Text, + network -> Text, + start_host -> Integer, + end_host -> Integer, + gateway4 -> Nullable, + dns -> Nullable, + domain_name -> Nullable, + vlan_id -> Nullable, + } +} + +#[derive( + AsChangeset, + Clone, + Insertable, + Identifiable, + Selectable, + Queryable, + Associations, + PartialEq, + Debug, +)] +#[diesel(table_name = instances, treat_none_as_default_value = false, belongs_to(Subnet))] +pub struct Instance { + pub id: i32, + pub name: String, + pub mac_addr: MacAddr, + pub subnet_id: i32, + pub host_num: i32, + pub ci_metadata: String, + pub ci_userdata: Option>, +} + +impl Instance { + /// Gets all instances. + pub async fn all(ctx: &Context) -> Result, ModelError> { + use self::instances::dsl::instances; + + let res = ctx + .spawn_db(move |mut db| { + instances + .select(Instance::as_select()) + .load::(&mut db) + }) + .await??; + + Ok(res) + } + + pub async fn all_in_subnet(ctx: &Context, net: &Subnet) -> Result, ModelError> { + let subnet = net.clone(); + + let res = ctx + .spawn_db(move |mut db| Instance::belonging_to(&subnet).load(&mut db)) + .await??; + + Ok(res) + } + + /// Gets an instance by its name. + pub async fn get_by_name( + ctx: &Context, + inst_name: impl Into, + ) -> Result, ModelError> { + use self::instances::dsl::{instances, name}; + + let inst_name = inst_name.into(); + + let res: Vec = ctx + .spawn_db(move |mut db| { + instances + .filter(name.eq(inst_name)) + .select(Instance::as_select()) + .load::(&mut db) + }) + .await??; + + Ok(res.into_iter().next()) + } + + /// Gets an Instance model by the IPv4 address that has been assigned to it. + pub async fn get_by_ip4(ctx: &Context, ip_addr: Ipv4Addr) -> Result, ModelError> { + use self::instances::dsl::host_num; + + let Some(net) = Subnet::all(ctx) + .await? + .into_iter() + .find(|net| net.network.contains(&ip_addr)) + else { + todo!("IP address not found"); + }; + + let num = net.network.host_bits(&ip_addr) as i32; + + let Some(inst) = ctx + .spawn_db(move |mut db| { + Instance::belonging_to(&net) + .filter(host_num.eq(num)) + .load(&mut db) + .map(|inst: Vec| inst.into_iter().next()) + }) + .await?? + else { + return Ok(None); + }; + + Ok(Some(inst)) + } + + /// Creates a new instance model. + pub async fn insert( + ctx: &Context, + name: impl AsRef, + subnet: &Subnet, + lease: nzr_api::model::Lease, + ci_meta: impl Into, + ci_user: Option>, + ) -> Result { + // Get highest host addr + 1 for our addr + let addr_num = Self::all_in_subnet(ctx, subnet) + .await? + .into_iter() + .max_by(|a, b| a.host_num.cmp(&b.host_num)) + .map_or(subnet.start_host, |i| i.host_num + 1); + + let wanted_name = name.as_ref().to_owned(); + let netid = subnet.id; + let ci_meta = ci_meta.into(); + + if addr_num > subnet.end_host { + Err(cidr::Error::HostBitsTooLarge)?; + } + + let ent = ctx + .spawn_db(move |mut db| { + use self::instances::dsl::*; + + let values = ( + name.eq(wanted_name), + mac_addr.eq(lease.mac_addr), + subnet_id.eq(netid), + host_num.eq(addr_num), + ci_metadata.eq(ci_meta), + ci_userdata.eq(ci_user), + ); + + diesel::insert_into(instances) + .values(values) + .returning(instances::all_columns()) + .get_result::(&mut db) + }) + .await??; + Ok(ent) + } + + /// Updates the instance model. + pub async fn update(&mut self, ctx: &Context) -> Result<(), ModelError> { + let self_2 = self.clone(); + + ctx.spawn_db(move |mut db| diesel::update(&self_2).set(&self_2).execute(&mut db)) + .await??; + + Ok(()) + } + + /// Deletes the instance model from the database. + pub async fn delete(self, ctx: &Context) -> Result<(), ModelError> { + ctx.spawn_db(move |mut db| diesel::delete(&self).execute(&mut db)) + .await??; + + Ok(()) + } + + /// Creates an [nzr_api::model::Instance] from the information available in + /// the database. + pub async fn api_model( + &self, + ctx: &Context, + ) -> Result> { + let netid = self.subnet_id; + let Some(subnet) = ctx + .spawn_db(move |mut db| Subnet::table().find(netid).load::(&mut db)) + .await?? + .into_iter() + .next() + else { + todo!("something went horribly wrong"); + }; + + Ok(nzr_api::model::Instance { + name: self.name.clone(), + id: self.id, + lease: nzr_api::model::Lease { + subnet: subnet.name.clone(), + addr: CidrV4::new( + subnet.network.make_ip(self.host_num as u32)?, + subnet.network.cidr(), + ), + mac_addr: self.mac_addr, + }, + state: Default::default(), + }) + } +} + +impl Transactable for Instance { + type Error = ModelError; + + async fn undo_tx(self, ctx: &Context) -> Result<(), Self::Error> { + self.delete(ctx).await + } +} + +// +// +// + +#[derive(AsChangeset, Clone, Insertable, Identifiable, Selectable, Queryable, PartialEq, Debug)] +pub struct Subnet { + pub id: i32, + pub name: String, + pub ifname: String, + pub network: CidrV4, + pub start_host: i32, + pub end_host: i32, + pub gateway4: Option, + pub dns: Option, + pub domain_name: Option, + pub vlan_id: Option, +} + +impl Subnet { + /// Gets all subnets. + pub async fn all(ctx: &Context) -> Result, ModelError> { + use self::subnets::dsl::subnets; + + let res = ctx + .spawn_db(move |mut db| subnets.select(Subnet::as_select()).load::(&mut db)) + .await??; + + Ok(res) + } + + /// Gets a list of DNS servers used by the subnet. + pub fn dns_servers(&self) -> Vec<&str> { + if let Some(ref dns) = self.dns { + dns.split(',').collect() + } else { + Vec::new() + } + } + + /// Gets a subnet model by its name. + pub async fn get_by_name( + ctx: &Context, + net_name: impl Into, + ) -> Result, ModelError> { + use self::subnets::dsl::{name, subnets}; + + let net_name = net_name.into(); + + let res: Vec = ctx + .spawn_db(move |mut db| { + subnets + .filter(name.eq(net_name)) + .select(Subnet::as_select()) + .load::(&mut db) + }) + .await??; + + Ok(res.into_iter().next()) + } + + /// Creates a new subnet model. + pub async fn insert( + ctx: &Context, + net_name: impl Into, + data: SubnetData, + ) -> Result { + let net_name = net_name.into(); + + let ent = ctx + .spawn_db(move |mut db| { + use self::subnets::columns::*; + let values = ( + name.eq(net_name), + ifname.eq(&data.ifname), + network.eq(data.network.network()), + start_host.eq(data.start_bytes() as i32), + end_host.eq(data.end_bytes() as i32), + gateway4.eq(data.gateway4.map(|g| data.network.host_bits(&g) as i32)), + dns.eq(data + .dns + .iter() + .map(|ip| ip.to_string()) + .collect::>() + .join(",")), + domain_name.eq(data.domain_name.map(|n| n.to_utf8())), + vlan_id.eq(data.vlan_id.map(|v| v as i32)), + ); + + diesel::insert_into(Subnet::table()) + .values(values) + .returning(self::subnets::all_columns) + .get_result::(&mut db) + }) + .await??; + Ok(ent) + } + + /// Generates an [nzr_api::model::Subnet]. + pub fn api_model(&self) -> Result { + Ok(nzr_api::model::Subnet { + name: self.name.clone(), + data: SubnetData { + ifname: self.ifname.clone(), + network: self.network, + start_host: self.start_ip()?, + end_host: self.end_ip()?, + gateway4: self.gateway_ip()?, + dns: self + .dns_servers() + .into_iter() + .filter_map(|s| match Ipv4Addr::from_str(s) { + // Instead of erroring when we get an unparseable DNS + // server, report it as an error and continue. This + // hopefully will avoid cases where a malformed DNS + // entry makes its way into the DB and wreaks havoc on + // the API. + Ok(addr) => Some(addr), + Err(err) => { + log::error!( + "Error parsing DNS server '{}' for {}: {}", + s, + &self.name, + err + ); + None + } + }) + .collect(), + domain_name: self.domain_name.as_ref().map(|s| { + Name::from_str(s).unwrap_or_else(|e| { + log::error!("Error parsing DNS name for {}: {}", &self.name, e); + Name::default() + }) + }), + vlan_id: self.vlan_id.map(|v| v as u32), + }, + }) + } + + /// Deletes the subnet model from the database. + pub async fn delete(self, ctx: &Context) -> Result<(), ModelError> { + ctx.spawn_db(move |mut db| diesel::delete(&self).execute(&mut db)) + .await??; + + Ok(()) + } + + /// Gets the first IPv4 address usable by hosts. + pub fn start_ip(&self) -> Result { + match self.start_host { + host if !host.is_negative() => self.network.make_ip(host as u32), + _ => Err(cidr::Error::Malformed), + } + } + + /// Gets the last IPv4 address usable by hosts. + pub fn end_ip(&self) -> Result { + match self.end_host { + host if !host.is_negative() => self.network.make_ip(host as u32), + _ => Err(cidr::Error::Malformed), + } + } + + /// Gets the default gateway IPv4 address, if defined. + pub fn gateway_ip(&self) -> Result, cidr::Error> { + match self.gateway4 { + Some(host) if !host.is_negative() => self.network.make_ip(host as u32).map(Some), + Some(_) => Err(cidr::Error::Malformed), + None => Ok(None), + } + } +} + +impl Transactable for Subnet { + type Error = ModelError; + + async fn undo_tx(self, ctx: &Context) -> Result<(), Self::Error> { + self.delete(ctx).await + } +} diff --git a/nzrd/src/model/tx.rs b/nzrd/src/model/tx.rs new file mode 100644 index 0000000..e7a5775 --- /dev/null +++ b/nzrd/src/model/tx.rs @@ -0,0 +1,56 @@ +use std::ops::Deref; + +use crate::ctx::Context; + +#[trait_variant::make(Transactable: Send)] +pub trait LocalTransactable { + type Error: std::error::Error + Send; + + // I'm guessing trait_variant makes it so this version isn't used? + #[allow(dead_code)] + async fn undo_tx(self, ctx: &Context) -> Result<(), Self::Error>; +} + +pub struct Transaction<'a, T: Transactable + 'static> { + inner: Option, + ctx: &'a Context, +} + +impl<'a, T: Transactable> Transaction<'a, T> { + /// Takes the value from the transaction. This is the equivalent of ensuring + /// the transaction is successful. + pub fn take(mut self) -> T { + // There should never be a situation where Transaction exists and + // inner is None, except for during .drop() + self.inner.take().unwrap() + } + + pub fn begin(ctx: &'a Context, inner: T) -> Self { + Self { + inner: Some(inner), + ctx, + } + } +} + +impl<'a, T: Transactable> Deref for Transaction<'a, T> { + type Target = T; + fn deref(&self) -> &Self::Target { + // As with take(), there should never be a situation where + // Transaction exists and inner is None + self.inner.as_ref().unwrap() + } +} + +impl<'a, T: Transactable> Drop for Transaction<'a, T> { + fn drop(&mut self) { + if let Some(inner) = self.inner.take() { + let ctx = self.ctx.clone(); + tokio::spawn(async move { + if let Err(err) = inner.undo_tx(&ctx).await { + log::error!("Error undoing transaction: {err}"); + } + }); + } + } +} diff --git a/nzrd/src/prelude.rs b/nzrd/src/prelude.rs deleted file mode 100644 index 7f1dcdd..0000000 --- a/nzrd/src/prelude.rs +++ /dev/null @@ -1,10 +0,0 @@ -macro_rules! datasize { - ($amt:tt $unit:tt) => { - $crate::ctrl::virtxml::SizeInfo { - amount: $amt as u64, - unit: $crate::ctrl::virtxml::SizeUnit::$unit, - } - }; -} - -pub(crate) use datasize; diff --git a/nzrd/src/rpc.rs b/nzrd/src/rpc.rs index 9201b2a..417516a 100644 --- a/nzrd/src/rpc.rs +++ b/nzrd/src/rpc.rs @@ -1,6 +1,5 @@ use futures::{future, StreamExt}; use nzr_api::{args, model, Nazrin}; -use std::borrow::Borrow; use std::sync::Arc; use tarpc::server::{BaseChannel, Channel}; use tarpc::tokio_serde::formats::Bincode; @@ -10,27 +9,22 @@ use tokio::sync::RwLock; use tokio::task::JoinHandle; use uuid::Uuid; -use crate::ctrl::vm::InstDb; -use crate::ctrl::{net::Subnet, Storable}; +use crate::cmd; use crate::ctx::Context; -use crate::dns::ZoneData; -use crate::{cmd, ctrl::vm::Instance}; +use crate::model::{Instance, Subnet}; use log::*; use std::collections::HashMap; -use std::ops::Deref; #[derive(Clone)] pub struct NzrServer { ctx: Context, - zones: ZoneData, create_tasks: Arc>>, } impl NzrServer { - pub fn new(ctx: Context, zones: ZoneData) -> Self { + pub fn new(ctx: Context) -> Self { Self { ctx, - zones, create_tasks: Arc::new(RwLock::new(HashMap::new())), } } @@ -48,26 +42,23 @@ impl Nazrin for NzrServer { })); let prog_task = progress.clone(); let build_task = tokio::spawn(async move { - let inst = cmd::vm::new_instance(self.ctx.clone(), prog_task.clone(), &build_args) - .await - .map_err(|e| format!("Instance creation failed: {}", e))?; - let addr = inst.ip_lease().map(|l| l.ipv4_addr.addr); - - { - let mut pt = prog_task.write().await; - "Starting instance...".clone_into(&mut pt.status_text); - pt.percentage = 90.0; - } - if let Some(addr) = addr { - if let Err(err) = self - .zones - .new_record(&build_args.subnet, &build_args.name, addr) + let (inst, dom) = + cmd::vm::new_instance(self.ctx.clone(), prog_task.clone(), &build_args) .await - { - warn!("Instance created, but no DNS record was made: {}", err); + .map_err(|e| format!("Instance creation failed: {}", e))?; + let mut api_model = inst + .api_model(&self.ctx) + .await + .map_err(|e| format!("Couldn't generate API response: {e}"))?; + match dom.state().await { + Ok(state) => { + api_model.state = state.into(); + } + Err(err) => { + warn!("Unable to get instance state: {err}"); } } - Ok((&inst).into()) + Ok(api_model) }); let task_id = uuid::Uuid::new_v4(); @@ -128,35 +119,40 @@ impl Nazrin for NzrServer { _: tarpc::context::Context, with_status: bool, ) -> Result, String> { - let insts: Vec = InstDb::all(self.ctx.db.clone()) - .map_err(|e| e.to_string())? - .filter_map(|i| match i { - Ok(entity) => { - if with_status { - match Instance::from_entity(self.ctx.clone(), entity.clone()) { - Ok(instance) => { - Some(<&Instance as Into>::into(&instance)) - } - Err(err) => { - let ent_name = { - let key = entity.key(); - String::from_utf8_lossy(key).to_string() - }; - warn!("Couldn't get instance for {}: {}", err, ent_name); - None - } + let db_models = Instance::all(&self.ctx) + .await + .map_err(|e| format!("Unable to get all instances: {e}"))?; + let mut models = Vec::new(); + for inst in db_models { + let mut api_model = match inst.api_model(&self.ctx).await { + Ok(model) => model, + Err(err) => { + warn!("Couldn't create API model for {}: {}", &inst.name, err); + continue; + } + }; + + // Try to get libvirt domain statuses, if requested + if with_status { + match self.ctx.virt.conn.get_instance(&inst.name).await { + Ok(dom) => match dom.state().await { + Ok(s) => { + api_model.state = s.into(); } - } else { - Some(entity.into()) + Err(err) => { + warn!("Couldn't get instance state for {}: {}", &inst.name, err); + } + }, + Err(err) => { + warn!("Couldn't get instance {}: {}", &inst.name, err); } } - Err(err) => { - warn!("Iterator error: {}", err); - None - } - }) - .collect(); - Ok(insts) + } + + models.push(api_model); + } + + Ok(models) } async fn new_subnet( @@ -164,17 +160,11 @@ impl Nazrin for NzrServer { _: tarpc::context::Context, build_args: model::Subnet, ) -> Result { - let subnet = cmd::net::add_subnet(&self.ctx, build_args) + cmd::net::add_subnet(&self.ctx, build_args) .await - .map_err(|e| e.to_string())?; - self.zones - .new_zone(&subnet) - .await - .map_err(|e| e.to_string())?; - Ok(model::Subnet { - name: String::from_utf8_lossy(subnet.key()).to_string(), - data: <&Subnet as Into>::into(&subnet), - }) + .map_err(|e| e.to_string())? + .api_model() + .map_err(|e| e.to_string()) } async fn modify_subnet( @@ -182,61 +172,48 @@ impl Nazrin for NzrServer { _: tarpc::context::Context, edit_args: model::Subnet, ) -> Result { - let subnet = Subnet::all(self.ctx.db.clone()) + if let Some(subnet) = Subnet::get_by_name(&self.ctx, &edit_args.name) + .await .map_err(|e| e.to_string())? - .find_map(|sub| { - if let Ok(sub) = sub { - if edit_args.name.as_str() == String::from_utf8_lossy(sub.key()) { - Some(sub) - } else { - None - } - } else { - None - } - }); - if let Some(mut subnet) = subnet { - subnet - .replace(edit_args.data.borrow().into()) - .map_err(|e| e.to_string())?; - Ok(model::Subnet { - name: edit_args.name, - data: subnet.deref().into(), - }) + { + todo!("support updating Subnets") } else { Err(format!("Subnet {} not found", &edit_args.name)) } } async fn get_subnets(self, _: tarpc::context::Context) -> Result, String> { - let subnets: Vec = Subnet::all(self.ctx.db.clone()) - .map_err(|e| e.to_string())? - .filter_map(|s| match s { - Ok(s) => Some(model::Subnet { - name: String::from_utf8(s.key().to_vec()).unwrap(), - data: <&Subnet as Into>::into(s.deref()), - }), - Err(err) => { - warn!("Iterator error: {}", err); - None - } - }) - .collect(); - Ok(subnets) + Subnet::all(&self.ctx).await.map_or_else( + |e| Err(e.to_string()), + |v| { + Ok(v.into_iter() + .filter_map(|s| match s.api_model() { + Ok(model) => Some(model), + Err(err) => { + error!("Couldn't parse subnet {}: {}", &s.name, err); + None + } + }) + .collect()) + }, + ) } async fn delete_subnet( self, _: tarpc::context::Context, - interface: String, + subnet_name: String, ) -> Result<(), String> { - cmd::net::delete_subnet(&self.ctx, &interface).map_err(|e| e.to_string())?; - self.zones.delete_zone(&interface).await; + cmd::net::delete_subnet(&self.ctx, &subnet_name) + .await + .map_err(|e| e.to_string())?; Ok(()) } async fn garbage_collect(self, _: tarpc::context::Context) -> Result<(), String> { - cmd::vm::prune_instances(&self.ctx).map_err(|e| e.to_string())?; + cmd::vm::prune_instances(&self.ctx) + .await + .map_err(|e| e.to_string())?; Ok(()) } } @@ -252,7 +229,7 @@ impl std::fmt::Display for GroupError { impl std::error::Error for GroupError {} -pub async fn serve(ctx: Context, zones: ZoneData) -> Result<(), Box> { +pub async fn serve(ctx: Context) -> Result<(), Box> { use std::os::unix::fs::PermissionsExt; if ctx.config.rpc.socket_path.exists() { @@ -274,13 +251,13 @@ pub async fn serve(ctx: Context, zones: ZoneData) -> Result<(), Box Result<(), PoolError> { - let buf_cap: u64 = datasize!(4 MiB).into(); - - let mut reader = BufReader::with_capacity(buf_cap as usize, from); - loop { - let read_bytes = { - // read from the source file... - let data = match reader.fill_buf() { - Ok(buf) => buf, - Err(er) => { - if let Err(er) = to.abort() { - warn!("Stream abort failed: {}", er); - } - return Err(PoolError::FileError(er)); - } - }; - - if data.is_empty() { - break; - } - - debug!("pulled {} bytes", data.len()); - - // ... and then send upstream - let mut send_idx = 0; - while send_idx < data.len() { - match to.send(&data[send_idx..]) { - Ok(sz) => { - send_idx += sz; - } - Err(er) => { - if let Err(er) = to.abort() { - warn!("Stream abort failed: {}", er); - } - return Err(PoolError::UploadError(er)); - } - } - } - data.len() - }; - - debug!("consuming {} bytes", read_bytes); - reader.consume(read_bytes); - } - Ok(()) - } - - /// Creates a [VirtVolume] from the given [Volume](crate::ctrl::virtxml::Volume) XML data. - pub async fn create_xml( - pool: &StoragePool, - xmldata: Volume, - flags: u32, - ) -> Result> { - let xml = quick_xml::se::to_string(&xmldata)?; - - let svol = StorageVol::create_xml(pool, &xml, flags)?; - - if xmldata.vol_type() == Some(VolType::Qcow2) { - let size = xmldata.capacity.unwrap(); - let src_img = img::create_qcow2(size).await?; - - let stream = match Stream::new(&svol.get_connect().map_err(PoolError::VirtError)?, 0) { - Ok(s) => s, - Err(er) => { - svol.delete(0).ok(); - return Err(Box::new(er)); - } - }; - - let img_size = src_img.metadata().unwrap().len(); - - if let Err(er) = svol.upload(&stream, 0, img_size, 0) { - svol.delete(0).ok(); - return Err(Box::new(PoolError::CantUpload(er))); - } - - Self::upload_img(&src_img, stream)?; - } - - Ok(Self { - inner: svol, - persist: false, - name: xmldata.name, - }) - } - - /// Finds a volume by the given pool and name. - pub fn lookup_by_name

(pool: P, name: &str) -> Result - where - P: AsRef, - { - Ok(Self { - inner: StorageVol::lookup_by_name(pool.as_ref(), name)?, - // default to persisting when looking up by name - persist: true, - name: name.to_owned(), - }) - } - - /// Clones the volume to the given pool. - pub async fn clone_vol( - &mut self, - pool: &VirtPool, - vol_name: &str, - size: SizeInfo, - ) -> Result { - debug!("Cloning volume to {} ({})", vol_name, &size); - - let src_path = self.get_path().map_err(PoolError::NoPath)?; - - let src_img = img::clone_qcow2(src_path, size) - .await - .map_err(PoolError::QemuError)?; - - let newvol = Volume::new(vol_name, pool.xml.vol_type(), size); - let newxml_str = quick_xml::se::to_string(&newvol).map_err(PoolError::SerdeError)?; - debug!("Creating new vol..."); - let cloned = StorageVol::create_xml(pool, &newxml_str, 0).map_err(PoolError::VirtError)?; - - match cloned.get_info() { - Ok(info) => { - if info.capacity != u64::from(size) { - debug!( - "libvirt set wrong size {}, trying this again...", - info.capacity - ); - if let Err(er) = cloned.resize(size.into(), 0) { - if let Err(er) = cloned.delete(0) { - warn!("Resizing disk failed, and couldn't clean up: {}", er); - } - return Err(PoolError::VirtError(er)); - } - } else { - debug!( - "capacity is correct ({} bytes), allocation = {} bytes", - info.capacity, info.allocation, - ); - } - } - Err(er) => { - if let Err(er) = cloned.delete(0) { - warn!("Couldn't clean up destination volume: {}", er); - } - return Err(PoolError::VirtError(er)); - } - } - - let stream = match Stream::new(&cloned.get_connect().map_err(PoolError::VirtError)?, 0) { - Ok(s) => s, - Err(er) => { - cloned.delete(0).ok(); - return Err(PoolError::VirtError(er)); - } - }; - - let img_size = src_img.metadata().unwrap().len(); - - if let Err(er) = cloned.upload(&stream, 0, img_size, 0) { - cloned.delete(0).ok(); - return Err(PoolError::CantUpload(er)); - } - - Self::upload_img(&src_img, stream)?; - - Ok(Self { - inner: cloned, - persist: false, - name: vol_name.to_owned(), - }) - } -} - -impl Deref for VirtVolume { - type Target = StorageVol; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl Drop for VirtVolume { - fn drop(&mut self) { - if !self.persist { - debug!("Deleting volume {}", &self.name); - self.inner.delete(0).ok(); - } - } -} - -#[derive(Debug)] -pub enum PoolError { - VirtError(virt::error::Error), - SerdeError(quick_xml::de::DeError), - NoPath(virt::error::Error), - FileError(std::io::Error), - CantUpload(virt::error::Error), - UploadError(virt::error::Error), - QemuError(img::ImgError), -} - -impl Display for PoolError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::VirtError(er) => er.fmt(f), - Self::SerdeError(er) => er.fmt(f), - Self::NoPath(er) => write!(f, "Couldn't get source image path: {}", er), - Self::FileError(er) => er.fmt(f), - Self::CantUpload(er) => write!(f, "Unable to start upload to image: {}", er), - Self::UploadError(er) => write!(f, "Failed to upload image: {}", er), - Self::QemuError(er) => er.fmt(f), - } - } -} - -impl std::error::Error for PoolError {} - -pub struct VirtPool { - inner: StoragePool, - pub xml: Pool, -} - -impl Deref for VirtPool { - type Target = StoragePool; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl AsRef for VirtPool { - fn as_ref(&self) -> &StoragePool { - &self.inner - } -} - -impl VirtPool { - pub fn lookup_by_name(conn: &virt::connect::Connect, id: &str) -> Result { - let inner = StoragePool::lookup_by_name(conn, id).map_err(PoolError::VirtError)?; - if !inner.is_active().map_err(PoolError::VirtError)? { - inner.create(0).map_err(PoolError::VirtError)?; - } - let xml_str = inner.get_xml_desc(0).map_err(PoolError::VirtError)?; - let xml = quick_xml::de::from_str(&xml_str).map_err(PoolError::SerdeError)?; - Ok(Self { inner, xml }) - } -} diff --git a/nzrdhcp/Cargo.toml b/nzrdhcp/Cargo.toml new file mode 100644 index 0000000..c59df1e --- /dev/null +++ b/nzrdhcp/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nzrdhcp" +description = "Unicast-only static DHCP server for nazrin" +version = "0.1.0" +edition = "2021" + +[dependencies] +dhcproto = { version = "0.12.0", features = ["serde"] } +serde = { version = "1.0.204", features = ["derive"] } +tokio = { version = "1.39.2", features = ["rt-multi-thread", "net", "macros"] } +nzr-api = { path = "../nzr-api" } +tracing = { version = "0.1.40", features = ["log"] } +tracing-subscriber = "0.3.18" +tarpc = { version = "0.34", features = [ + "tokio1", + "unix", + "serde-transport", + "serde-transport-bincode", +] } +moka = { version = "0.12.8", features = ["future"] } +anyhow = "1.0.86" diff --git a/nzrdhcp/src/ctx.rs b/nzrdhcp/src/ctx.rs new file mode 100644 index 0000000..e0a628f --- /dev/null +++ b/nzrdhcp/src/ctx.rs @@ -0,0 +1,124 @@ +use std::hash::RandomState; +use std::net::SocketAddr; + +use anyhow::Context as _; +use anyhow::Result; +use moka::future::Cache; +use nzr_api::{ + config::Config, + model::{Instance, SubnetData}, + net::mac::MacAddr, + NazrinClient, +}; +use tarpc::{tokio_serde::formats::Bincode, tokio_util::codec::LengthDelimitedCodec}; +use tokio::net::UdpSocket; +use tokio::net::UnixStream; + +pub struct Context { + subnet_cache: Cache, + server_sock: UdpSocket, + listen_addr: SocketAddr, + host_cache: Cache, + api_client: NazrinClient, +} + +impl Context { + async fn hydrate_hosts(&self) -> Result<()> { + let instances = self + .api_client + .get_instances(tarpc::context::current(), false) + .await? + .map_err(|e| anyhow::anyhow!("nzrd error: {e}"))?; + + for instance in instances { + if let Some(cached) = self.host_cache.get(&instance.lease.mac_addr).await { + if cached.lease.addr == instance.lease.addr { + // Already cached + continue; + } else { + // Same MAC address, but different IP? Invalidate + self.host_cache.remove(&cached.lease.mac_addr).await; + } + } + + self.host_cache + .insert(instance.lease.mac_addr, instance) + .await; + } + + Ok(()) + } + + async fn hydrate_nets(&self) -> Result<()> { + let subnets = self + .api_client + .get_subnets(tarpc::context::current()) + .await? + .map_err(|e| anyhow::anyhow!("nzrd error: {e}"))?; + + for net in subnets { + self.subnet_cache.insert(net.name, net.data).await; + } + + Ok(()) + } + + pub async fn new(cfg: &Config) -> Result { + let api_client = { + let sock = UnixStream::connect(&cfg.rpc.socket_path) + .await + .context("Connection to nzrd failed")?; + let framed_io = LengthDelimitedCodec::builder() + .length_field_type::() + .new_framed(sock); + let transport = tarpc::serde_transport::new(framed_io, Bincode::default()); + NazrinClient::new(Default::default(), transport) + } + .spawn(); + + let listen_addr: SocketAddr = cfg + .dhcp + .listen_addr + .parse() + .context("Malformed listen address")?; + + let server_sock = UdpSocket::bind(&listen_addr) + .await + .context("Unable to listen")?; + + Ok(Self { + subnet_cache: Cache::new(50), + host_cache: Cache::new(2000), + server_sock, + api_client, + listen_addr, + }) + } + + pub fn sock(&self) -> &UdpSocket { + &self.server_sock + } + + pub fn addr(&self) -> SocketAddr { + self.listen_addr + } + + pub async fn instance_by_mac(&self, addr: MacAddr) -> anyhow::Result> { + if let Some(inst) = self.host_cache.get(&addr).await { + Ok(Some(inst)) + } else { + self.hydrate_hosts().await?; + Ok(self.host_cache.get(&addr).await) + } + } + + pub async fn get_subnet(&self, name: impl AsRef) -> anyhow::Result> { + let name = name.as_ref(); + if let Some(net) = self.subnet_cache.get(name).await { + Ok(Some(net)) + } else { + self.hydrate_nets().await?; + Ok(self.subnet_cache.get(name).await) + } + } +} diff --git a/nzrdhcp/src/hack.rs b/nzrdhcp/src/hack.rs new file mode 100644 index 0000000..e69de29 diff --git a/nzrdhcp/src/main.rs b/nzrdhcp/src/main.rs new file mode 100644 index 0000000..44a012b --- /dev/null +++ b/nzrdhcp/src/main.rs @@ -0,0 +1,184 @@ +mod ctx; + +use std::{net::Ipv4Addr, process::ExitCode}; + +use ctx::Context; +use dhcproto::{ + v4::{DhcpOption, Message, MessageType, Opcode, OptionCode}, + Decodable, Decoder, +}; +use nzr_api::{config::Config, net::mac::MacAddr}; +use std::net::SocketAddr; +use tracing::instrument; + +const EMPTY_V4: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); +const DEFAULT_LEASE: u32 = 86400; + +fn make_reply(msg: &Message, msg_type: MessageType, lease_addr: Option) -> Message { + let mut resp = Message::new( + EMPTY_V4, + EMPTY_V4, + lease_addr.unwrap_or(EMPTY_V4), + msg.giaddr(), + msg.chaddr(), + ); + resp.set_opcode(Opcode::BootReply) + .set_xid(msg.xid()) + .set_htype(msg.htype()) + .set_flags(msg.flags()); + resp.opts_mut().insert(DhcpOption::MessageType(msg_type)); + resp +} + +#[instrument(skip(ctx, msg))] +async fn handle_message(ctx: &Context, from: SocketAddr, msg: &Message) { + if msg.opcode() != Opcode::BootRequest { + tracing::warn!("Invalid incoming opcode {:?}", msg.opcode()); + return; + } + + let Some(DhcpOption::MessageType(msg_type)) = msg.opts().get(OptionCode::MessageType) else { + tracing::warn!("Missing DHCP message type"); + return; + }; + + let Ok(client_mac) = MacAddr::from_bytes(msg.chaddr()) else { + tracing::info!("Received DHCP payload with invalid addr (different media type?)"); + return; + }; + + let instance = match ctx.instance_by_mac(client_mac).await { + Ok(Some(i)) => i, + Ok(None) => { + tracing::info!("{msg_type:?} from unknown host {client_mac}, ignoring"); + return; + } + Err(err) => { + tracing::error!("Error getting instance for {client_mac}: {err}"); + return; + } + }; + + let mut lease_time = None; + let mut nak = false; + + let mut response = match msg_type { + MessageType::Discover => { + lease_time = Some(DEFAULT_LEASE); + make_reply(msg, MessageType::Offer, Some(instance.lease.addr.addr)) + } + MessageType::Request => { + if msg.ciaddr() != instance.lease.addr.addr { + nak = true; + make_reply(msg, MessageType::Nak, None) + } else { + lease_time = Some(DEFAULT_LEASE); + make_reply(msg, MessageType::Ack, Some(instance.lease.addr.addr)) + } + } + MessageType::Decline => { + tracing::warn!( + "Client (assumed to be {}) informed us that {} is in use by another server", + &instance.name, + instance.lease.addr.addr + ); + return; + } + MessageType::Release => { + // We only provide static leases + tracing::trace!("Ignoring DHCPRELEASE"); + return; + } + MessageType::Inform => make_reply(msg, MessageType::Ack, None), + other => { + tracing::trace!("Received unhandled message {other:?}"); + return; + } + }; + + let opts = response.opts_mut(); + let giaddr = if msg.giaddr().is_unspecified() { + todo!("no relay??") + } else { + msg.giaddr() + }; + + opts.insert(DhcpOption::ServerIdentifier(giaddr)); + if let Some(time) = lease_time { + opts.insert(DhcpOption::AddressLeaseTime(time)); + } + + if !nak { + // Get general networking info + let subnet = match ctx.get_subnet(&instance.lease.subnet).await { + Ok(Some(net)) => net, + Ok(None) => { + tracing::error!("nzrd says '{}' isn't a subnet", &instance.lease.subnet); + return; + } + Err(err) => { + tracing::error!("Error getting subnet: {err}"); + return; + } + }; + + opts.insert(DhcpOption::Hostname(instance.name.clone())); + + if !subnet.dns.is_empty() { + opts.insert(DhcpOption::DomainNameServer(subnet.dns)); + } + + if let Some(name) = subnet.domain_name { + opts.insert(DhcpOption::DomainName(name.to_utf8())); + } + + if let Some(gw) = subnet.gateway4 { + opts.insert(DhcpOption::Router(Vec::from(&[gw]))); + } + + opts.insert(DhcpOption::SubnetMask(instance.lease.addr.netmask())); + } +} + +#[tokio::main] +async fn main() -> ExitCode { + tracing_subscriber::fmt().init(); + let cfg: Config = match Config::figment().extract() { + Ok(cfg) => cfg, + Err(err) => { + tracing::error!("Unable to get configuration: {err}"); + return ExitCode::FAILURE; + } + }; + + let ctx = match Context::new(&cfg).await { + Ok(ctx) => ctx, + Err(err) => { + tracing::error!("{err}"); + return ExitCode::FAILURE; + } + }; + + tracing::info!("nzrdhcp ready! Listening on {}", ctx.addr()); + + loop { + let mut buf = [0u8; 576]; + let (_, src) = match ctx.sock().recv_from(&mut buf).await { + Ok(x) => x, + Err(err) => { + tracing::error!("recv_from error: {err}"); + return ExitCode::FAILURE; + } + }; + + let msg = match Message::decode(&mut Decoder::new(&buf)) { + Ok(msg) => msg, + Err(err) => { + tracing::error!("Couldn't process message from {}: {}", src, err); + continue; + } + }; + + handle_message(&ctx, src, &msg).await; + } +}