Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F73680
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
224 KB
Subscribers
None
View Options
diff --git a/.gitignore b/.gitignore
index 686ddf2..5fb7f9e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
/target
/libcollar_store
**/libcollar_store
+crates/api/test_api*
diff --git a/Cargo.lock b/Cargo.lock
index a6a646b..75b0884 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,2088 +1,2730 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.59.0",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "api"
version = "0.1.0"
dependencies = [
"axum",
"axum-test",
"libcollar",
"log",
"serde",
"serde_json",
"store",
"tempfile",
"thiserror",
"tokio",
"tokio-test",
"tower-http",
"tracing",
]
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
[[package]]
name = "auto-future"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
dependencies = [
"axum-core",
"bytes",
"form_urlencoded",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-test"
version = "17.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac"
dependencies = [
"anyhow",
"assert-json-diff",
"auto-future",
"axum",
"bytes",
"bytesize",
"cookie",
"http",
"http-body-util",
"hyper",
"hyper-util",
"mime",
"pretty_assertions",
"reserve-port",
"rust-multipart-rfc7578_2",
"serde",
"serde_json",
"serde_urlencoded",
"smallvec",
"tokio",
"tower",
"url",
]
[[package]]
name = "backtrace"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
[[package]]
name = "bindgen"
version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
"bitflags 2.9.1",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "bytesize"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba"
+[[package]]
+name = "cc"
+version = "1.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
+dependencies = [
+ "shlex",
+]
+
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
+[[package]]
+name = "clap"
+version = "4.5.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
[[package]]
name = "collar"
version = "0.1.0"
dependencies = [
"api",
"env_logger",
"libcollar",
"log",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
]
+[[package]]
+name = "collar-cli"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tokio",
+ "url",
+]
+
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"time",
"version_check",
]
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "deranged"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
[[package]]
name = "errno"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
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-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-io",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+[[package]]
+name = "h2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
[[package]]
name = "http"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
+ "h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
+[[package]]
+name = "hyper-rustls"
+version = "0.27.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
[[package]]
name = "hyper-util"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
dependencies = [
+ "base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
+ "ipnet",
"libc",
+ "percent-encoding",
"pin-project-lite",
"socket2",
+ "system-configuration",
"tokio",
"tower-service",
"tracing",
+ "windows-registry",
]
[[package]]
name = "icu_collections"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
dependencies = [
"displaydoc",
"potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"potential_utf",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
[[package]]
name = "icu_provider"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
dependencies = [
"displaydoc",
"icu_locale_core",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "ifconfig"
version = "0.1.0"
dependencies = [
"errno",
"libc",
"log",
"nix",
"thiserror",
]
+[[package]]
+name = "indexmap"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libcollar"
version = "0.1.0"
dependencies = [
"ifconfig",
"log",
"ng",
"serde",
"store",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "libloading"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.53.0",
]
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litemap"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "lock_api"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[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.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
[[package]]
name = "ng"
version = "0.1.0"
dependencies = [
"bindgen",
"errno",
"libc",
"log",
"socket2",
"thiserror",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[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-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.6",
]
[[package]]
name = "parking_lot"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
"parking_lot_core 0.9.11",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall 0.2.16",
"smallvec",
"winapi",
]
[[package]]
name = "parking_lot_core"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.12",
"smallvec",
"windows-targets 0.52.6",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "prettyplease"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
- "getrandom",
+ "getrandom 0.3.3",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
"bitflags 2.9.1",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+[[package]]
+name = "reqwest"
+version = "0.12.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
[[package]]
name = "reserve-port"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356"
dependencies = [
"thiserror",
]
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "rust-multipart-rfc7578_2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41"
dependencies = [
"bytes",
"futures-core",
"futures-util",
"http",
"mime",
"rand",
"thiserror",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
+[[package]]
+name = "rustls"
+version = "0.23.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
dependencies = [
"itoa",
"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 = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
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 0.11.2",
]
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "store"
version = "0.1.0"
dependencies = [
"log",
"serde",
"serde_json",
"sled",
"tempfile",
"thiserror",
]
+[[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 = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+[[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "tempfile"
version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"fastrand",
- "getrandom",
+ "getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "time"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tokio"
version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"parking_lot 0.12.4",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
[[package]]
name = "tokio-stream"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-test"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
dependencies = [
"async-stream",
"bytes",
"futures-core",
"tokio",
"tokio-stream",
]
+[[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
dependencies = [
"bitflags 2.9.1",
"bytes",
+ "futures-util",
"http",
"http-body",
+ "iri-string",
"pin-project-lite",
+ "tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
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-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
[[package]]
name = "url"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+[[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.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+[[package]]
+name = "windows-link"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+
+[[package]]
+name = "windows-registry"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
+dependencies = [
+ "windows-result",
+ "windows-strings",
+ "windows-targets 0.53.0",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
+dependencies = [
+ "windows-link",
+]
+
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.1",
]
[[package]]
name = "writeable"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
[[package]]
name = "zerotrie"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
new file mode 100644
index 0000000..53fc84e
--- /dev/null
+++ b/crates/cli/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "collar-cli"
+version = "0.1.0"
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+
+[[bin]]
+name = "collar"
+path = "src/main.rs"
+
+[dependencies]
+clap = { version = "4.5", features = ["derive"] }
+reqwest = { version = "0.12", features = ["json"] }
+tokio = { version = "1.45.1", features = ["full"] }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+anyhow = "1.0"
+url = "2.5"
\ No newline at end of file
diff --git a/crates/cli/README.md b/crates/cli/README.md
new file mode 100644
index 0000000..5cc6c68
--- /dev/null
+++ b/crates/cli/README.md
@@ -0,0 +1,259 @@
+# Collar CLI
+
+Command-line interface for the Collar API server. Provides commands for managing networks and instances through the HTTP API.
+
+## Installation
+
+From the project root directory:
+
+```bash
+cargo build --release
+```
+
+The binary will be available at `target/release/collar`.
+
+## Usage
+
+```bash
+collar [OPTIONS] <COMMAND>
+```
+
+### Options
+
+- `--url <URL>` - Base URL of the Collar API server (default: http://localhost:3000)
+
+### Commands
+
+- `network` - Network management commands
+- `instance` - Instance management commands
+
+## Network Management
+
+### List Networks
+
+```bash
+collar network list
+```
+
+### Create Network
+
+Basic network:
+```bash
+collar network create my-network 192.168.1.0/24
+```
+
+Network with basic assigner:
+```bash
+collar network create my-network 192.168.1.0/24 \
+ --assigner-type basic \
+ --assigner-config '{"strategy": "sequential"}'
+```
+
+Network with range assigner:
+```bash
+collar network create my-network 192.168.1.0/24 \
+ --assigner-type range \
+ --assigner-config '{"range_name": "my-range"}'
+```
+
+### Get Network Details
+
+```bash
+collar network get my-network
+```
+
+### Delete Network
+
+```bash
+collar network delete my-network
+```
+
+### Show Examples
+
+```bash
+collar network create --examples
+```
+
+## Instance Management
+
+### List Instances
+
+```bash
+collar instance list
+```
+
+### Create Instance
+
+Jail instance:
+```bash
+collar instance create my-jail \
+ --provider-type jail \
+ --provider-config '{"jail_conf": "/etc/jail.conf", "dataset": "zroot/jails"}'
+```
+
+Container instance:
+```bash
+collar instance create my-container \
+ --provider-type container \
+ --provider-config '{"runtime": "docker", "image": "alpine:latest"}'
+```
+
+Instance with network interfaces:
+```bash
+collar instance create networked-instance \
+ --provider-type jail \
+ --provider-config '{}' \
+ --network-interfaces '[{"network_name": "test-network", "interface_name": "eth0"}]'
+```
+
+Instance with metadata:
+```bash
+collar instance create my-instance \
+ --provider-type jail \
+ --provider-config '{}' \
+ --metadata '{"env": "production", "version": "1.0"}'
+```
+
+### Get Instance Details
+
+```bash
+collar instance get my-instance
+```
+
+### Delete Instance
+
+```bash
+collar instance delete my-instance
+```
+
+### Show Examples
+
+```bash
+collar instance create --examples
+```
+
+## Configuration Examples
+
+### Network Provider Types
+
+Currently, the API supports basic network providers. Provider configuration is optional and can be passed as JSON.
+
+### Network Assigner Types
+
+- **basic**: Sequential IP assignment
+ ```json
+ {"strategy": "sequential"}
+ ```
+
+- **range**: Range-based IP assignment
+ ```json
+ {"range_name": "my-range"}
+ ```
+
+### Instance Provider Types
+
+- **jail**: FreeBSD jail instances
+ ```json
+ {
+ "jail_conf": "/etc/jail.conf",
+ "dataset": "zroot/jails"
+ }
+ ```
+
+- **container**: Container instances
+ ```json
+ {
+ "runtime": "docker",
+ "image": "alpine:latest"
+ }
+ ```
+
+### Network Interfaces
+
+Network interfaces can be specified as a JSON array:
+```json
+[
+ {
+ "network_name": "test-network",
+ "interface_name": "eth0"
+ }
+]
+```
+
+### Metadata
+
+Instance metadata can be specified as a JSON object:
+```json
+{
+ "env": "production",
+ "version": "1.0",
+ "owner": "team-infrastructure"
+}
+```
+
+## Examples
+
+### Complete Workflow
+
+1. Create a network:
+```bash
+collar network create app-network 10.0.1.0/24 \
+ --assigner-type basic \
+ --assigner-config '{"strategy": "sequential"}'
+```
+
+2. Create an instance connected to the network:
+```bash
+collar instance create web-server \
+ --provider-type container \
+ --provider-config '{"runtime": "docker", "image": "nginx:latest"}' \
+ --network-interfaces '[{"network_name": "app-network", "interface_name": "eth0"}]' \
+ --metadata '{"role": "web-server", "env": "production"}'
+```
+
+3. List resources:
+```bash
+collar network list
+collar instance list
+```
+
+4. Get details:
+```bash
+collar network get app-network
+collar instance get web-server
+```
+
+5. Clean up:
+```bash
+collar instance delete web-server
+collar network delete app-network
+```
+
+## Error Handling
+
+The CLI provides descriptive error messages for common issues:
+
+- Invalid JSON configuration
+- Network/instance not found
+- Server connection errors
+- Invalid provider or assigner types
+
+## Development
+
+### Running Tests
+
+```bash
+cargo test -p collar-cli
+```
+
+### Building
+
+```bash
+cargo build -p collar-cli
+```
+
+### Running from Source
+
+```bash
+cargo run -p collar-cli -- network list
+```
\ No newline at end of file
diff --git a/crates/cli/examples/demo.sh b/crates/cli/examples/demo.sh
new file mode 100755
index 0000000..c1d321c
--- /dev/null
+++ b/crates/cli/examples/demo.sh
@@ -0,0 +1,95 @@
+#!/bin/bash
+
+# Collar CLI Demo Script
+# Demonstrates various CLI operations for network and instance management
+
+set -e
+
+# Configuration
+API_URL="http://localhost:3000"
+COLLAR_CLI="cargo run -p collar-cli --"
+
+echo "=== Collar CLI Demo ==="
+echo "API URL: $API_URL"
+echo
+
+# Function to run CLI command with proper formatting
+run_cli() {
+ echo "$ collar $*"
+ $COLLAR_CLI --url "$API_URL" "$@"
+ echo
+}
+
+# Function to handle errors gracefully
+run_cli_safe() {
+ echo "$ collar $*"
+ if ! $COLLAR_CLI --url "$API_URL" "$@"; then
+ echo "Command failed (this might be expected if server is not running)"
+ fi
+ echo
+}
+
+echo "=== CLI Help ==="
+run_cli --help
+
+echo "=== Network Management ==="
+
+echo "--- Show network examples ---"
+run_cli network create --examples
+
+echo "--- List networks (may fail if server not running) ---"
+run_cli_safe network list
+
+echo "--- Create a basic network (may fail if server not running) ---"
+run_cli_safe network create demo-network 192.168.100.0/24
+
+echo "--- Create network with basic assigner (may fail if server not running) ---"
+run_cli_safe network create demo-basic-network 192.168.101.0/24 \
+ --assigner-type basic \
+ --assigner-config '{"strategy": "sequential"}'
+
+echo "--- Get network details (may fail if server not running) ---"
+run_cli_safe network get demo-network
+
+echo "=== Instance Management ==="
+
+echo "--- Show instance examples ---"
+run_cli instance create --examples
+
+echo "--- List instances (may fail if server not running) ---"
+run_cli_safe instance list
+
+echo "--- Create jail instance (may fail if server not running) ---"
+run_cli_safe instance create demo-jail \
+ --provider-type jail \
+ --provider-config '{"jail_conf": "/etc/jail.conf", "dataset": "zroot/jails"}'
+
+echo "--- Create container instance (may fail if server not running) ---"
+run_cli_safe instance create demo-container \
+ --provider-type container \
+ --provider-config '{"runtime": "docker", "image": "alpine:latest"}'
+
+echo "--- Create instance with network interface (may fail if server not running) ---"
+run_cli_safe instance create demo-networked \
+ --provider-type jail \
+ --provider-config '{}' \
+ --network-interfaces '[{"network_name": "demo-network", "interface_name": "eth0"}]' \
+ --metadata '{"env": "demo", "purpose": "testing"}'
+
+echo "--- Get instance details (may fail if server not running) ---"
+run_cli_safe instance get demo-jail
+
+echo "=== Cleanup ==="
+
+echo "--- Delete instances (may fail if server not running) ---"
+run_cli_safe instance delete demo-jail
+run_cli_safe instance delete demo-container
+run_cli_safe instance delete demo-networked
+
+echo "--- Delete networks (may fail if server not running) ---"
+run_cli_safe network delete demo-network
+run_cli_safe network delete demo-basic-network
+
+echo "=== Demo Complete ==="
+echo "Note: Some commands may have failed if the Collar API server is not running."
+echo "To start the server, run the appropriate collar daemon command first."
\ No newline at end of file
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
new file mode 100644
index 0000000..339946c
--- /dev/null
+++ b/crates/cli/src/main.rs
@@ -0,0 +1,416 @@
+//! # Collar CLI
+//!
+//! Command-line interface for the Collar API server.
+//!
+//! Provides commands for managing networks and instances through the HTTP API.
+
+use std::collections::HashMap;
+
+use anyhow::Result;
+use clap::{Parser, Subcommand};
+use reqwest::Client;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use url::Url;
+
+mod utils;
+use utils::*;
+
+#[derive(Parser)]
+#[command(name = "collar")]
+#[command(about = "CLI client for Collar API server")]
+#[command(version)]
+struct Cli {
+ #[arg(long, default_value = "http://localhost:3000")]
+ /// Base URL of the Collar API server
+ url: String,
+
+ #[command(subcommand)]
+ command: Commands,
+}
+
+#[derive(Subcommand)]
+enum Commands {
+ /// Network management commands
+ Network {
+ #[command(subcommand)]
+ command: NetworkCommands,
+ },
+ /// Instance management commands
+ Instance {
+ #[command(subcommand)]
+ command: InstanceCommands,
+ },
+}
+
+#[derive(Subcommand)]
+enum NetworkCommands {
+ /// List all networks
+ List,
+ /// Create a new network
+ Create {
+ /// Network name
+ name: Option<String>,
+ /// Network CIDR (e.g., 192.168.1.0/24)
+ cidr: Option<String>,
+ /// Provider type (optional)
+ #[arg(long)]
+ provider_type: Option<String>,
+ /// Provider config as JSON string (optional)
+ #[arg(long)]
+ provider_config: Option<String>,
+ /// Assigner type (basic, range)
+ #[arg(long)]
+ assigner_type: Option<String>,
+ /// Assigner config as JSON string (optional)
+ #[arg(long)]
+ assigner_config: Option<String>,
+ /// Show example configurations
+ #[arg(long)]
+ examples: bool,
+ },
+ /// Get network details
+ Get {
+ /// Network name
+ name: String,
+ },
+ /// Delete a network
+ Delete {
+ /// Network name
+ name: String,
+ },
+}
+
+#[derive(Subcommand)]
+enum InstanceCommands {
+ /// List all instances
+ List,
+ /// Create a new instance
+ Create {
+ /// Instance name
+ name: Option<String>,
+ /// Provider type (jail, container)
+ #[arg(long)]
+ provider_type: Option<String>,
+ /// Provider config as JSON string
+ #[arg(long)]
+ provider_config: Option<String>,
+ /// Network interfaces as JSON array string (optional)
+ #[arg(long)]
+ network_interfaces: Option<String>,
+ /// Metadata as JSON object string (optional)
+ #[arg(long)]
+ metadata: Option<String>,
+ /// Show example configurations
+ #[arg(long)]
+ examples: bool,
+ },
+ /// Get instance details
+ Get {
+ /// Instance name
+ name: String,
+ },
+ /// Delete an instance
+ Delete {
+ /// Instance name
+ name: String,
+ },
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct CreateNetworkRequest {
+ name: String,
+ cidr: String,
+ provider_type: Option<String>,
+ provider_config: Option<Value>,
+ assigner_type: Option<String>,
+ assigner_config: Option<Value>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct CreateInstanceRequest {
+ name: String,
+ provider_type: String,
+ provider_config: Value,
+ network_interfaces: Option<Vec<NetworkInterfaceRequest>>,
+ metadata: Option<HashMap<String, String>>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct NetworkInterfaceRequest {
+ network_name: String,
+ interface_name: String,
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ let cli = Cli::parse();
+ let client = Client::new();
+ let base_url = Url::parse(&cli.url)?;
+
+ match cli.command {
+ Commands::Network { command } => handle_network_command(&client, &base_url, command).await?,
+ Commands::Instance { command } => handle_instance_command(&client, &base_url, command).await?,
+ }
+
+ Ok(())
+}
+
+async fn handle_network_command(client: &Client, base_url: &Url, command: NetworkCommands) -> Result<()> {
+ match command {
+ NetworkCommands::List => {
+ let url = base_url.join("/networks")?;
+ let response = client.get(url).send().await?;
+
+ if response.status().is_success() {
+ let networks: Value = response.json().await?;
+ println!("{}", format_response(&networks)?);
+ } else {
+ let error = response.text().await?;
+ eprintln!("Error: {}", error);
+ }
+ }
+ NetworkCommands::Create {
+ name,
+ cidr,
+ provider_type,
+ provider_config,
+ assigner_type,
+ assigner_config,
+ examples,
+ } => {
+ if examples {
+ println!("Example network configurations:");
+ println!("\nBasic network:");
+ println!("collar --url http://localhost:3000 network create my-network 192.168.1.0/24");
+
+ println!("\nNetwork with basic assigner:");
+ println!("collar network create my-network 192.168.1.0/24 \\");
+ println!(" --assigner-type basic \\");
+ println!(" --assigner-config '{}'", serde_json::to_string(&example_basic_assigner_config())?);
+
+ println!("\nNetwork with range assigner:");
+ println!("collar network create my-network 192.168.1.0/24 \\");
+ println!(" --assigner-type range \\");
+ println!(" --assigner-config '{}'", serde_json::to_string(&example_range_assigner_config())?);
+
+ return Ok(());
+ }
+
+ // Require name and cidr if not showing examples
+ let name = name.ok_or_else(|| anyhow::anyhow!("Network name is required"))?;
+ let cidr = cidr.ok_or_else(|| anyhow::anyhow!("Network CIDR is required"))?;
+
+ // Validate assigner type if provided
+ if let Some(ref assigner) = assigner_type {
+ validate_assigner_type(assigner)?;
+ }
+ let provider_config = if let Some(config) = provider_config {
+ Some(parse_json_string(&config)?)
+ } else {
+ None
+ };
+
+ let assigner_config = if let Some(config) = assigner_config {
+ Some(parse_json_string(&config)?)
+ } else {
+ None
+ };
+
+ let request = CreateNetworkRequest {
+ name: name.clone(),
+ cidr: cidr.clone(),
+ provider_type,
+ provider_config,
+ assigner_type,
+ assigner_config,
+ };
+
+ let url = base_url.join("/networks")?;
+ let response = client.post(url).json(&request).send().await?;
+
+ if response.status().is_success() {
+ println!("Network '{}' created successfully", name);
+ } else {
+ let error = response.text().await?;
+ eprintln!("Error: {}", error);
+ }
+ }
+ NetworkCommands::Get { name } => {
+ let url = base_url.join(&format!("/networks/{}", name))?;
+ let response = client.get(url).send().await?;
+
+ if response.status().is_success() {
+ let network: Value = response.json().await?;
+ println!("{}", format_response(&network)?);
+ } else {
+ let error = response.text().await?;
+ eprintln!("Error: {}", error);
+ }
+ }
+ NetworkCommands::Delete { name } => {
+ let url = base_url.join(&format!("/networks/{}", name))?;
+ let response = client.delete(url).send().await?;
+
+ if response.status().is_success() {
+ println!("Network '{}' deleted successfully", name);
+ } else {
+ let error = response.text().await?;
+ eprintln!("Error: {}", error);
+ }
+ }
+ }
+
+ Ok(())
+}
+
+async fn handle_instance_command(client: &Client, base_url: &Url, command: InstanceCommands) -> Result<()> {
+ match command {
+ InstanceCommands::List => {
+ let url = base_url.join("/instances")?;
+ let response = client.get(url).send().await?;
+
+ if response.status().is_success() {
+ let instances: Value = response.json().await?;
+ println!("{}", format_response(&instances)?);
+ } else {
+ let error = response.text().await?;
+ eprintln!("Error: {}", error);
+ }
+ }
+ InstanceCommands::Create {
+ name,
+ provider_type,
+ provider_config,
+ network_interfaces,
+ metadata,
+ examples,
+ } => {
+ if examples {
+ println!("Example instance configurations:");
+ println!("\nJail instance:");
+ println!("collar instance create my-jail \\");
+ println!(" --provider-type jail \\");
+ println!(" --provider-config '{}'", serde_json::to_string(&example_jail_config())?);
+
+ println!("\nContainer instance:");
+ println!("collar instance create my-container \\");
+ println!(" --provider-type container \\");
+ println!(" --provider-config '{}'", serde_json::to_string(&example_container_config())?);
+
+ println!("\nInstance with network interfaces:");
+ println!("collar instance create networked-instance \\");
+ println!(" --provider-type jail \\");
+ println!(" --provider-config '{{}}' \\");
+ println!(" --network-interfaces '{}'", serde_json::to_string(&example_network_interfaces())?);
+
+ return Ok(());
+ }
+
+ // Require name and provider details if not showing examples
+ let name = name.ok_or_else(|| anyhow::anyhow!("Instance name is required"))?;
+ let provider_type = provider_type.ok_or_else(|| anyhow::anyhow!("Provider type is required"))?;
+ let provider_config = provider_config.ok_or_else(|| anyhow::anyhow!("Provider config is required"))?;
+
+ // Validate provider type
+ validate_provider_type(&provider_type)?;
+ let provider_config: Value = parse_json_string(&provider_config)?;
+
+ let network_interfaces = if let Some(interfaces) = network_interfaces {
+ Some(serde_json::from_str(&interfaces)?)
+ } else {
+ None
+ };
+
+ let metadata = if let Some(meta) = metadata {
+ Some(parse_metadata(&meta)?)
+ } else {
+ None
+ };
+
+ let request = CreateInstanceRequest {
+ name: name.clone(),
+ provider_type,
+ provider_config,
+ network_interfaces,
+ metadata,
+ };
+
+ let url = base_url.join("/instances")?;
+ let response = client.post(url).json(&request).send().await?;
+
+ if response.status().is_success() {
+ println!("Instance '{}' created successfully", name);
+ } else {
+ let error = response.text().await?;
+ eprintln!("Error: {}", error);
+ }
+ }
+ InstanceCommands::Get { name } => {
+ let url = base_url.join(&format!("/instances/{}", name))?;
+ let response = client.get(url).send().await?;
+
+ if response.status().is_success() {
+ let instance: Value = response.json().await?;
+ println!("{}", format_response(&instance)?);
+ } else {
+ let error = response.text().await?;
+ eprintln!("Error: {}", error);
+ }
+ }
+ InstanceCommands::Delete { name } => {
+ let url = base_url.join(&format!("/instances/{}", name))?;
+ let response = client.delete(url).send().await?;
+
+ if response.status().is_success() {
+ println!("Instance '{}' deleted successfully", name);
+ } else {
+ let error = response.text().await?;
+ eprintln!("Error: {}", error);
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use std::process::Command;
+
+ #[test]
+ fn test_cli_help() {
+ let output = Command::new("cargo")
+ .args(&["run", "--bin", "collar", "--", "--help"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ assert!(stdout.contains("CLI client for Collar API server"));
+ }
+
+ #[test]
+ fn test_network_help() {
+ let output = Command::new("cargo")
+ .args(&["run", "--bin", "collar", "--", "network", "--help"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ assert!(stdout.contains("Network management commands"));
+ }
+
+ #[test]
+ fn test_instance_help() {
+ let output = Command::new("cargo")
+ .args(&["run", "--bin", "collar", "--", "instance", "--help"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ assert!(stdout.contains("Instance management commands"));
+ }
+}
\ No newline at end of file
diff --git a/crates/cli/src/utils.rs b/crates/cli/src/utils.rs
new file mode 100644
index 0000000..9a5cb43
--- /dev/null
+++ b/crates/cli/src/utils.rs
@@ -0,0 +1,120 @@
+//! Utility functions for the CLI client
+
+use anyhow::{Context, Result};
+use serde_json::Value;
+use std::collections::HashMap;
+
+/// Parse a JSON string into a serde_json::Value
+pub fn parse_json_string(json_str: &str) -> Result<Value> {
+ serde_json::from_str(json_str).context("Failed to parse JSON string")
+}
+
+/// Parse a JSON string into a HashMap for metadata
+pub fn parse_metadata(json_str: &str) -> Result<HashMap<String, String>> {
+ serde_json::from_str(json_str).context("Failed to parse metadata JSON")
+}
+
+/// Format response output with proper indentation
+pub fn format_response(value: &Value) -> Result<String> {
+ serde_json::to_string_pretty(value).context("Failed to format response")
+}
+
+/// Validate provider type for instances
+pub fn validate_provider_type(provider_type: &str) -> Result<()> {
+ match provider_type {
+ "jail" | "container" => Ok(()),
+ _ => Err(anyhow::anyhow!(
+ "Invalid provider type '{}'. Must be 'jail' or 'container'",
+ provider_type
+ )),
+ }
+}
+
+/// Validate assigner type for networks
+pub fn validate_assigner_type(assigner_type: &str) -> Result<()> {
+ match assigner_type {
+ "basic" | "range" => Ok(()),
+ _ => Err(anyhow::anyhow!(
+ "Invalid assigner type '{}'. Must be 'basic' or 'range'",
+ assigner_type
+ )),
+ }
+}
+
+/// Build example configurations for different provider types
+pub fn example_jail_config() -> Value {
+ serde_json::json!({
+ "jail_conf": "/etc/jail.conf",
+ "dataset": "zroot/jails"
+ })
+}
+
+pub fn example_container_config() -> Value {
+ serde_json::json!({
+ "runtime": "docker",
+ "image": "alpine:latest"
+ })
+}
+
+pub fn example_basic_assigner_config() -> Value {
+ serde_json::json!({
+ "strategy": "sequential"
+ })
+}
+
+pub fn example_range_assigner_config() -> Value {
+ serde_json::json!({
+ "range_name": "my-range"
+ })
+}
+
+pub fn example_network_interfaces() -> Value {
+ serde_json::json!([
+ {
+ "network_name": "test-network",
+ "interface_name": "eth0"
+ }
+ ])
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_json_string() {
+ let json_str = r#"{"key": "value"}"#;
+ let result = parse_json_string(json_str).unwrap();
+ assert_eq!(result["key"], "value");
+ }
+
+ #[test]
+ fn test_parse_metadata() {
+ let json_str = r#"{"env": "production", "version": "1.0"}"#;
+ let result = parse_metadata(json_str).unwrap();
+ assert_eq!(result.get("env"), Some(&"production".to_string()));
+ assert_eq!(result.get("version"), Some(&"1.0".to_string()));
+ }
+
+ #[test]
+ fn test_validate_provider_type() {
+ assert!(validate_provider_type("jail").is_ok());
+ assert!(validate_provider_type("container").is_ok());
+ assert!(validate_provider_type("invalid").is_err());
+ }
+
+ #[test]
+ fn test_validate_assigner_type() {
+ assert!(validate_assigner_type("basic").is_ok());
+ assert!(validate_assigner_type("range").is_ok());
+ assert!(validate_assigner_type("invalid").is_err());
+ }
+
+ #[test]
+ fn test_format_response() {
+ let value = serde_json::json!({"name": "test", "value": 42});
+ let formatted = format_response(&value).unwrap();
+ assert!(formatted.contains("\"name\": \"test\""));
+ assert!(formatted.contains("\"value\": 42"));
+ }
+}
\ No newline at end of file
diff --git a/crates/cli/tests/integration_tests.rs b/crates/cli/tests/integration_tests.rs
new file mode 100644
index 0000000..864637d
--- /dev/null
+++ b/crates/cli/tests/integration_tests.rs
@@ -0,0 +1,177 @@
+//! Integration tests for the Collar CLI
+//!
+//! These tests verify CLI functionality by spawning the CLI process
+//! and checking its output and behavior.
+
+use std::process::Command;
+
+#[tokio::test]
+async fn test_cli_help() {
+ let output = Command::new("cargo")
+ .args(&["run", "-p", "collar-cli", "--", "--help"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ assert!(stdout.contains("CLI client for Collar API server"));
+ assert!(stdout.contains("network"));
+ assert!(stdout.contains("instance"));
+}
+
+#[tokio::test]
+async fn test_network_help() {
+ let output = Command::new("cargo")
+ .args(&["run", "-p", "collar-cli", "--", "network", "--help"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ assert!(stdout.contains("Network management commands"));
+ assert!(stdout.contains("list"));
+ assert!(stdout.contains("create"));
+ assert!(stdout.contains("get"));
+ assert!(stdout.contains("delete"));
+}
+
+#[tokio::test]
+async fn test_instance_help() {
+ let output = Command::new("cargo")
+ .args(&["run", "-p", "collar-cli", "--", "instance", "--help"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ assert!(stdout.contains("Instance management commands"));
+ assert!(stdout.contains("list"));
+ assert!(stdout.contains("create"));
+ assert!(stdout.contains("get"));
+ assert!(stdout.contains("delete"));
+}
+
+#[tokio::test]
+async fn test_network_create_examples() {
+ let output = Command::new("cargo")
+ .args(&["run", "-p", "collar-cli", "--", "network", "create", "--examples"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ assert!(stdout.contains("Example network configurations"));
+ assert!(stdout.contains("Basic network"));
+ assert!(stdout.contains("basic assigner"));
+ assert!(stdout.contains("range assigner"));
+}
+
+#[tokio::test]
+async fn test_instance_create_examples() {
+ let output = Command::new("cargo")
+ .args(&["run", "-p", "collar-cli", "--", "instance", "create", "--examples"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ assert!(stdout.contains("Example instance configurations"));
+ assert!(stdout.contains("Jail instance"));
+ assert!(stdout.contains("Container instance"));
+ assert!(stdout.contains("network interfaces"));
+}
+
+#[tokio::test]
+async fn test_network_list_connection_error() {
+ let output = Command::new("cargo")
+ .args(&[
+ "run", "-p", "collar-cli", "--",
+ "--url", "http://localhost:9999",
+ "network", "list"
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ // Should fail with connection error since server isn't running on port 9999
+ assert!(!output.status.success() || !String::from_utf8(output.stderr).unwrap().is_empty());
+}
+
+#[tokio::test]
+async fn test_instance_list_connection_error() {
+ let output = Command::new("cargo")
+ .args(&[
+ "run", "-p", "collar-cli", "--",
+ "--url", "http://localhost:9999",
+ "instance", "list"
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ // Should fail with connection error since server isn't running on port 9999
+ assert!(!output.status.success() || !String::from_utf8(output.stderr).unwrap().is_empty());
+}
+
+#[tokio::test]
+async fn test_invalid_provider_type_validation() {
+ let output = Command::new("cargo")
+ .args(&[
+ "run", "-p", "collar-cli", "--",
+ "--url", "http://localhost:9999",
+ "instance", "create", "test-instance",
+ "--provider-type", "invalid",
+ "--provider-config", "{}"
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(!output.status.success());
+ let stderr = String::from_utf8(output.stderr).unwrap();
+ assert!(stderr.contains("Invalid provider type"));
+}
+
+#[tokio::test]
+async fn test_invalid_assigner_type_validation() {
+ let output = Command::new("cargo")
+ .args(&[
+ "run", "-p", "collar-cli", "--",
+ "--url", "http://localhost:9999",
+ "network", "create", "test-network", "192.168.1.0/24",
+ "--assigner-type", "invalid"
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(!output.status.success());
+ let stderr = String::from_utf8(output.stderr).unwrap();
+ assert!(stderr.contains("Invalid assigner type"));
+}
+
+#[tokio::test]
+async fn test_invalid_json_config() {
+ let output = Command::new("cargo")
+ .args(&[
+ "run", "-p", "collar-cli", "--",
+ "--url", "http://localhost:9999",
+ "instance", "create", "test-instance",
+ "--provider-type", "jail",
+ "--provider-config", "invalid-json"
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(!output.status.success());
+ let stderr = String::from_utf8(output.stderr).unwrap();
+ assert!(stderr.contains("Failed to parse JSON"));
+}
+
+#[tokio::test]
+async fn test_version_flag() {
+ let output = Command::new("cargo")
+ .args(&["run", "-p", "collar-cli", "--", "--version"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ assert!(stdout.contains("collar"));
+}
\ No newline at end of file
diff --git a/crates/store/examples/jail_auto_ip_demo.rs b/crates/store/examples/jail_auto_ip_demo.rs
new file mode 100644
index 0000000..68dc122
--- /dev/null
+++ b/crates/store/examples/jail_auto_ip_demo.rs
@@ -0,0 +1,229 @@
+use store::{
+ InstanceStore, Instance, InstanceConfig, NetworkInterface,
+ NamespaceStore, NetworkStore, RangeStore,
+ JailProvider, Provider,
+ network::{NetRange, BasicProvider, RangeAssigner},
+};
+use std::collections::HashMap;
+
+fn main() -> store::Result<()> {
+ println!("=== Jail Automatic IP Assignment Demo ===\n");
+
+ // Open the store
+ let db = sled::open("jail_auto_ip_demo.db")?;
+
+ // Set up stores
+ let namespaces = NamespaceStore::open(&db)?;
+ let ranges = RangeStore::open(&db)?;
+ let networks = NetworkStore::open_with_ranges(&db, ranges)?;
+ let instances = InstanceStore::open(&db, namespaces, networks)?;
+
+ println!("1. Creating network with RangeAssigner...");
+
+ // Create a network with automatic IP assignment
+ let provider = BasicProvider::new()
+ .with_config("type", "bridge")
+ .with_config("driver", "epair");
+
+ let assigner = RangeAssigner::with_range_name("jail_ips")
+ .with_config("strategy", "sequential")
+ .with_config("reserve_gateway", "true");
+
+ let netrange = NetRange::from_cidr("10.0.20.0/24")?;
+
+ instances.networks().create(
+ "jail_network",
+ netrange,
+ provider,
+ Some(assigner),
+ )?;
+
+ println!(" ✓ Created network 'jail_network' with range 10.0.20.0/24");
+
+ println!("\n2. Creating jails with automatic IP assignment...");
+
+ // Create multiple jail instances - they should automatically get IP assignments
+ let jail_names = ["web-jail", "db-jail", "cache-jail"];
+
+ for jail_name in &jail_names {
+ let jail_provider = JailProvider::new()
+ .with_dataset("tank/jails".to_string());
+
+ let jail_config = InstanceConfig {
+ provider_config: jail_provider.to_json()?,
+ network_interfaces: vec![
+ NetworkInterface {
+ network_name: "jail_network".to_string(),
+ interface_name: "em0".to_string(),
+ assignment: None, // Will be automatically assigned
+ }
+ ],
+ metadata: {
+ let mut meta = HashMap::new();
+ meta.insert("role".to_string(), jail_name.replace("-jail", ""));
+ meta
+ },
+ };
+
+ let jail_instance = Instance {
+ name: jail_name.to_string(),
+ provider_type: "jail".to_string(),
+ config: jail_config,
+ created_at: std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ updated_at: std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ };
+
+ let created = instances.create(jail_instance)?;
+
+ if let Some(ref assignment) = created.config.network_interfaces[0].assignment {
+ println!(" ✓ Created jail '{}' with IP: {}", jail_name, assignment);
+ } else {
+ println!(" ✗ Failed to assign IP to jail '{}'", jail_name);
+ }
+ }
+
+ println!("\n3. Verifying all jails have unique IP assignments...");
+
+ let mut assigned_ips = std::collections::HashSet::new();
+
+ for jail_name in &jail_names {
+ let instance = instances.get(jail_name)?;
+ let interface = &instance.config.network_interfaces[0];
+
+ if let Some(ref assignment) = interface.assignment {
+ if assigned_ips.insert(assignment.clone()) {
+ println!(" ✓ {} -> {}", jail_name, assignment);
+ } else {
+ println!(" ✗ Duplicate IP assignment for {}: {}", jail_name, assignment);
+ return Err(store::Error::StoreError(sled::Error::Unsupported(
+ "Duplicate IP assignment detected".to_string()
+ )));
+ }
+ } else {
+ println!(" ✗ No IP assignment found for {}", jail_name);
+ }
+ }
+
+ println!("\n4. Creating jail with multiple network interfaces...");
+
+ // Create another network for demonstration
+ let mgmt_provider = BasicProvider::new();
+ let mgmt_assigner = RangeAssigner::with_range_name("mgmt_ips");
+ instances.networks().create(
+ "management",
+ NetRange::from_cidr("192.168.1.0/24")?,
+ mgmt_provider,
+ Some(mgmt_assigner),
+ )?;
+
+ // Create a jail with multiple interfaces
+ let multi_jail_provider = JailProvider::new();
+ let multi_jail_config = InstanceConfig {
+ provider_config: multi_jail_provider.to_json()?,
+ network_interfaces: vec![
+ NetworkInterface {
+ network_name: "jail_network".to_string(),
+ interface_name: "em0".to_string(),
+ assignment: None,
+ },
+ NetworkInterface {
+ network_name: "management".to_string(),
+ interface_name: "em1".to_string(),
+ assignment: None,
+ },
+ ],
+ metadata: HashMap::new(),
+ };
+
+ let multi_jail = Instance {
+ name: "multi-interface-jail".to_string(),
+ provider_type: "jail".to_string(),
+ config: multi_jail_config,
+ created_at: std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ updated_at: std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ };
+
+ let created_multi = instances.create(multi_jail)?;
+
+ println!(" ✓ Created multi-interface jail:");
+ for (i, interface) in created_multi.config.network_interfaces.iter().enumerate() {
+ if let Some(ref assignment) = interface.assignment {
+ println!(" Interface {}: {} -> {}", i, interface.network_name, assignment);
+ }
+ }
+
+ println!("\n5. Testing deletion and IP cleanup...");
+
+ // Delete the first jail
+ instances.delete("web-jail")?;
+ println!(" ✓ Deleted jail 'web-jail'");
+
+ // Verify the IP is cleaned up
+ let network = instances.networks().get("jail_network")?.unwrap();
+ let assignment_after_delete = network.get_assignment("web-jail", instances.networks().ranges())?;
+ if assignment_after_delete.is_none() {
+ println!(" ✓ IP assignment cleaned up after deletion");
+ } else {
+ println!(" ✗ IP assignment still exists after deletion");
+ }
+
+ // Create a new jail to demonstrate IP reuse
+ let reuse_jail_provider = JailProvider::new();
+ let reuse_jail_config = InstanceConfig {
+ provider_config: reuse_jail_provider.to_json()?,
+ network_interfaces: vec![
+ NetworkInterface {
+ network_name: "jail_network".to_string(),
+ interface_name: "em0".to_string(),
+ assignment: None,
+ }
+ ],
+ metadata: HashMap::new(),
+ };
+
+ let reuse_jail = Instance {
+ name: "replacement-jail".to_string(),
+ provider_type: "jail".to_string(),
+ config: reuse_jail_config,
+ created_at: std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ updated_at: std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ };
+
+ let created_reuse = instances.create(reuse_jail)?;
+
+ if let Some(ref new_assignment) = created_reuse.config.network_interfaces[0].assignment {
+ println!(" ✓ New jail 'replacement-jail' reused IP: {}", new_assignment);
+ }
+
+ println!("\n✅ Demonstration completed successfully!");
+ println!("\nKey points:");
+ println!(" • Jails automatically receive IP assignments when created");
+ println!(" • Each jail gets a unique IP from the network's range");
+ println!(" • Multiple network interfaces are supported");
+ println!(" • RangeAssigner handles sequential IP allocation");
+ println!(" • IP assignments are automatically cleaned up on deletion");
+ println!(" • Freed IPs can be reused by new instances");
+
+ // Clean up
+ std::fs::remove_dir_all("jail_auto_ip_demo.db").ok();
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/crates/store/src/instance.rs b/crates/store/src/instance.rs
index 3cf1d85..5d03794 100644
--- a/crates/store/src/instance.rs
+++ b/crates/store/src/instance.rs
@@ -1,946 +1,1022 @@
use crate::{Result, Error, NamespaceStore, NetworkStore};
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
/// Generic provider trait for different virtualization backends
pub trait Provider: Send + Sync {
/// Get the provider type identifier
fn provider_type(&self) -> &'static str;
/// Serialize provider configuration to JSON
fn to_json(&self) -> Result<String>;
/// Deserialize provider configuration from JSON
fn from_json(json: &str) -> Result<Self> where Self: Sized;
/// Start the instance with the given configuration
fn start(&self, name: &str, config: &InstanceConfig) -> Result<()>;
/// Stop the instance
fn stop(&self, name: &str) -> Result<()>;
/// Check if instance is running
fn is_running(&self, name: &str) -> Result<bool>;
/// Get instance status information
fn status(&self, name: &str) -> Result<ProviderStatus>;
}
/// Status information from a provider
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderStatus {
pub running: bool,
pub pid: Option<u32>,
pub uptime: Option<u64>,
pub memory_usage: Option<u64>,
pub cpu_usage: Option<f64>,
}
/// Network interface assignment for an instance
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkInterface {
pub network_name: String,
pub interface_name: String,
pub assignment: Option<String>, // IP assignment from network
}
/// Instance configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceConfig {
pub provider_config: String, // JSON config specific to provider
pub network_interfaces: Vec<NetworkInterface>,
pub metadata: HashMap<String, String>,
}
/// Instance representation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Instance {
pub name: String,
pub provider_type: String,
pub config: InstanceConfig,
pub created_at: u64,
pub updated_at: u64,
}
impl Instance {
/// Create a new instance
pub fn new(name: String, provider_type: String, config: InstanceConfig) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
Self {
name,
provider_type,
config,
created_at: now,
updated_at: now,
}
}
/// Update instance configuration
pub fn update_config(&mut self, config: InstanceConfig) {
self.config = config;
self.updated_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
}
}
/// Store for managing instances
#[derive(Debug, Clone)]
pub struct InstanceStore {
pub(crate) instances: sled::Tree,
pub(crate) namespaces: NamespaceStore,
pub(crate) networks: NetworkStore,
}
impl InstanceStore {
const INSTANCE_NAMESPACE: &'static str = "instances";
/// Open instance store with existing namespace and network stores
pub fn open(
db: &sled::Db,
namespaces: NamespaceStore,
networks: NetworkStore,
) -> Result<Self> {
let instances = db.open_tree("instances")?;
let store = Self {
instances,
namespaces,
networks,
};
// Ensure instance namespace exists
store.namespaces.define(Self::INSTANCE_NAMESPACE)?;
Ok(store)
}
- /// Create a new instance with network assignments
+ /// Get reference to the network store
+ pub fn networks(&self) -> &NetworkStore {
+ &self.networks
+ }
+
+ /// Create a new instance with automatic network IP assignments
+ ///
+ /// This method creates a new instance and automatically assigns IP addresses
+ /// to any network interfaces that are configured but don't have existing assignments.
+ /// The automatic assignment works with networks that have RangeAssigners configured.
+ ///
+ /// # Automatic IP Assignment Process
+ ///
+ /// For each network interface:
+ /// 1. Check if an IP assignment already exists
+ /// 2. If not, and the network has a RangeAssigner, automatically assign an IP
+ /// 3. Update the interface with the assigned IP address
+ ///
+ /// This enables seamless provisioning of jails, containers, and other instances
+ /// with proper network connectivity without manual IP management.
pub fn create(&self, mut instance: Instance) -> Result<Instance> {
// Reserve name in namespace
self.namespaces.reserve(Self::INSTANCE_NAMESPACE, &instance.name, &instance.name)?;
// Get network assignments for interfaces
for interface in &mut instance.config.network_interfaces {
if let Ok(Some(network)) = self.networks.get(&interface.network_name) {
match network.get_assignment(&instance.name, self.networks.ranges.as_ref()) {
Ok(Some(assignment)) => {
interface.assignment = Some(assignment.to_string());
}
Ok(None) => {
- // No assignment available
+ // No existing assignment, try to auto-assign if network has RangeAssigner
+ // This enables automatic IP allocation for instances with network interfaces
+ if let Some(ref ranges) = self.networks.ranges {
+ // Attempt automatic IP assignment using the network's RangeAssigner
+ if let Err(e) = network.auto_assign_ip(&instance.name, ranges) {
+ // Clean up reserved name on failure
+ let _ = self.namespaces.remove(Self::INSTANCE_NAMESPACE, &instance.name);
+ return Err(e);
+ }
+
+ // Retrieve the newly assigned IP address
+ match network.get_assignment(&instance.name, self.networks.ranges.as_ref()) {
+ Ok(Some(assignment)) => {
+ interface.assignment = Some(assignment.to_string());
+ }
+ Ok(None) => {
+ // Auto-assignment failed silently or range is full
+ // This is not necessarily an error - some networks may not support auto-assignment
+ }
+ Err(e) => {
+ // Clean up reserved name on failure
+ let _ = self.namespaces.remove(Self::INSTANCE_NAMESPACE, &instance.name);
+ return Err(e);
+ }
+ }
+ }
}
Err(e) => {
// Clean up reserved name on failure
let _ = self.namespaces.remove(Self::INSTANCE_NAMESPACE, &instance.name);
return Err(e);
}
}
}
}
// Store instance
let instance_json = serde_json::to_string(&instance)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))))?;
match self.instances.insert(&instance.name, instance_json.as_bytes()) {
Ok(_) => Ok(instance),
Err(e) => {
// Clean up on failure
let _ = self.namespaces.remove(Self::INSTANCE_NAMESPACE, &instance.name);
Err(Error::StoreError(e))
}
}
}
/// Create instance within a transaction
pub fn create_in_transaction(
&self,
names_tree: &sled::transaction::TransactionalTree,
spaces_tree: &sled::transaction::TransactionalTree,
instances_tree: &sled::transaction::TransactionalTree,
mut instance: Instance,
) -> std::result::Result<Instance, sled::transaction::ConflictableTransactionError<Error>> {
// Reserve name in namespace within transaction
match self.namespaces.reserve_in_transaction(names_tree, spaces_tree, &instance.name, Self::INSTANCE_NAMESPACE, &instance.name) {
Ok(_) => {},
Err(e) => return Err(sled::transaction::ConflictableTransactionError::Abort(
Error::StoreError(sled::Error::Unsupported(format!("Namespace reservation error: {:?}", e)))
)),
}
// Get network assignments for interfaces
for interface in &mut instance.config.network_interfaces {
if let Ok(Some(network)) = self.networks.get(&interface.network_name) {
match network.get_assignment(&instance.name, self.networks.ranges.as_ref()) {
Ok(Some(assignment)) => {
interface.assignment = Some(assignment.to_string());
}
Ok(None) => {
- // No assignment available
+ // No existing assignment, try to auto-assign if network has RangeAssigner
+ // This enables automatic IP allocation for instances with network interfaces
+ if let Some(ref ranges) = self.networks.ranges {
+ // Attempt automatic IP assignment using the network's RangeAssigner
+ if let Err(e) = network.auto_assign_ip(&instance.name, ranges) {
+ return Err(sled::transaction::ConflictableTransactionError::Abort(e));
+ }
+
+ // Retrieve the newly assigned IP address
+ match network.get_assignment(&instance.name, self.networks.ranges.as_ref()) {
+ Ok(Some(assignment)) => {
+ interface.assignment = Some(assignment.to_string());
+ }
+ Ok(None) => {
+ // Auto-assignment failed silently or range is full
+ // This is not necessarily an error - some networks may not support auto-assignment
+ }
+ Err(e) => return Err(sled::transaction::ConflictableTransactionError::Abort(e)),
+ }
+ }
}
Err(e) => return Err(sled::transaction::ConflictableTransactionError::Abort(e)),
}
}
}
// Store instance in transaction
let instance_json = serde_json::to_string(&instance)
.map_err(|e| sled::transaction::ConflictableTransactionError::Abort(
Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))
))?;
instances_tree.insert(instance.name.as_bytes(), instance_json.as_bytes())
.map_err(|e| sled::transaction::ConflictableTransactionError::Abort(
Error::StoreError(sled::Error::Unsupported(format!("Transaction insert error: {}", e)))
))?;
Ok(instance)
}
/// Get an instance by name
pub fn get(&self, name: &str) -> Result<Instance> {
match self.instances.get(name)? {
Some(data) => {
let json = String::from_utf8(data.to_vec())?;
serde_json::from_str(&json)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))))
}
None => Err(Error::StoreError(sled::Error::Unsupported(format!("Instance '{}' not found", name)))),
}
}
/// Update an existing instance
pub fn update(&self, instance: Instance) -> Result<Instance> {
// Check if instance exists
if !self.exists(&instance.name)? {
return Err(Error::StoreError(sled::Error::Unsupported(format!("Instance '{}' not found", instance.name))));
}
let instance_json = serde_json::to_string(&instance)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))))?;
self.instances.insert(&instance.name, instance_json.as_bytes())?;
Ok(instance)
}
- /// Delete an instance
+ /// Delete an instance and clean up IP assignments
+ ///
+ /// This method removes the instance and automatically unassigns any IP addresses
+ /// that were allocated to the instance's network interfaces. This ensures proper
+ /// cleanup of network resources and allows IPs to be reused by new instances.
pub fn delete(&self, name: &str) -> Result<()> {
// Get instance to clean up network assignments
if let Ok(instance) = self.get(name) {
- // TODO: Clean up network assignments when unassign functionality is available
- for _interface in &instance.config.network_interfaces {
- // network.unassign_ip(&name)?;
+ // Clean up IP assignments for each network interface
+ for interface in &instance.config.network_interfaces {
+ if let Ok(Some(network)) = self.networks.get(&interface.network_name) {
+ // Only clean up if there's a range store and the network has a RangeAssigner
+ if let Some(ref ranges) = self.networks.ranges {
+ if let Err(e) = network.auto_unassign_ip(name, ranges) {
+ // Log the error but don't fail the deletion
+ eprintln!("Warning: Failed to clean up IP assignment for {} on network {}: {:?}",
+ name, interface.network_name, e);
+ }
+ }
+ }
}
}
// Remove from instance store
self.instances.remove(name)?;
// Remove from namespace
self.namespaces.remove(Self::INSTANCE_NAMESPACE, name)?;
Ok(())
}
/// Check if instance exists
pub fn exists(&self, name: &str) -> Result<bool> {
Ok(self.instances.contains_key(name)?)
}
/// List all instances
pub fn list(&self) -> Result<Vec<Instance>> {
let mut instances = Vec::new();
for item in self.instances.iter() {
let (_, value_bytes) = item?;
let json = String::from_utf8(value_bytes.to_vec())?;
let instance: Instance = serde_json::from_str(&json)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))))?;
instances.push(instance);
}
instances.sort_by(|a, b| a.name.cmp(&b.name));
Ok(instances)
}
/// List instances by provider type
pub fn list_by_provider(&self, provider_type: &str) -> Result<Vec<Instance>> {
Ok(self.list()?
.into_iter()
.filter(|instance| instance.provider_type == provider_type)
.collect())
}
/// Check if an instance name is reserved in the namespace
pub fn is_name_reserved(&self, name: &str) -> Result<bool> {
self.namespaces.key_exists(Self::INSTANCE_NAMESPACE, name)
}
}
// Example provider implementations
/// Jail provider for FreeBSD jails
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JailProvider {
pub jail_conf: String,
pub dataset: Option<String>,
}
impl JailProvider {
pub fn new() -> Self {
Self {
jail_conf: String::new(),
dataset: None,
}
}
pub fn with_dataset(mut self, dataset: String) -> Self {
self.dataset = Some(dataset);
self
}
}
impl Provider for JailProvider {
fn provider_type(&self) -> &'static str {
"jail"
}
fn to_json(&self) -> Result<String> {
serde_json::to_string(self)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))))
}
fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))))
}
fn start(&self, name: &str, _config: &InstanceConfig) -> Result<()> {
// TODO: Implement jail start logic
println!("Starting jail: {}", name);
Ok(())
}
fn stop(&self, name: &str) -> Result<()> {
// TODO: Implement jail stop logic
println!("Stopping jail: {}", name);
Ok(())
}
fn is_running(&self, _name: &str) -> Result<bool> {
// TODO: Implement jail status check
Ok(false)
}
fn status(&self, _name: &str) -> Result<ProviderStatus> {
// TODO: Implement jail status collection
Ok(ProviderStatus {
running: false,
pid: None,
uptime: None,
memory_usage: None,
cpu_usage: None,
})
}
}
/// Container provider for Docker/Podman containers
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerProvider {
pub runtime: String, // "docker" or "podman"
pub image: String,
pub registry: Option<String>,
}
impl ContainerProvider {
pub fn docker(image: String) -> Self {
Self {
runtime: "docker".to_string(),
image,
registry: None,
}
}
pub fn podman(image: String) -> Self {
Self {
runtime: "podman".to_string(),
image,
registry: None,
}
}
}
impl Provider for ContainerProvider {
fn provider_type(&self) -> &'static str {
"container"
}
fn to_json(&self) -> Result<String> {
serde_json::to_string(self)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))))
}
fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))))
}
fn start(&self, name: &str, _config: &InstanceConfig) -> Result<()> {
// TODO: Implement container start logic
println!("Starting {} container: {}", self.runtime, name);
Ok(())
}
fn stop(&self, name: &str) -> Result<()> {
// TODO: Implement container stop logic
println!("Stopping {} container: {}", self.runtime, name);
Ok(())
}
fn is_running(&self, _name: &str) -> Result<bool> {
// TODO: Implement container status check
Ok(false)
}
fn status(&self, _name: &str) -> Result<ProviderStatus> {
// TODO: Implement container status collection
Ok(ProviderStatus {
running: false,
pid: None,
uptime: None,
memory_usage: None,
cpu_usage: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{NamespaceStore, NetworkStore, RangeStore};
use tempfile::TempDir;
fn create_test_stores() -> Result<(TempDir, InstanceStore)> {
let temp_dir = TempDir::new().unwrap();
let db = sled::open(temp_dir.path())?;
let namespaces = NamespaceStore::open(&db)?;
let ranges = RangeStore::open(&db)?;
let networks = NetworkStore::open_with_ranges(&db, ranges)?;
let instances = InstanceStore::open(&db, namespaces, networks)?;
Ok((temp_dir, instances))
}
#[test]
fn test_create_instance() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
let config = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![],
metadata: HashMap::new(),
};
let instance = Instance::new(
"test-instance".to_string(),
"jail".to_string(),
config,
);
let created = store.create(instance.clone())?;
assert_eq!(created.name, instance.name);
assert_eq!(created.provider_type, instance.provider_type);
Ok(())
}
#[test]
fn test_get_instance() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
let config = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![],
metadata: HashMap::new(),
};
let instance = Instance::new(
"test-instance".to_string(),
"jail".to_string(),
config,
);
store.create(instance.clone())?;
let retrieved = store.get(&instance.name)?;
assert_eq!(retrieved.name, instance.name);
assert_eq!(retrieved.provider_type, instance.provider_type);
Ok(())
}
#[test]
fn test_duplicate_instance_name() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
let config = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![],
metadata: HashMap::new(),
};
let instance1 = Instance::new(
"test-instance".to_string(),
"jail".to_string(),
config.clone(),
);
let instance2 = Instance::new(
"test-instance".to_string(),
"container".to_string(),
config,
);
store.create(instance1)?;
// Should fail due to duplicate name
assert!(store.create(instance2).is_err());
Ok(())
}
#[test]
fn test_list_instances() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
let config = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![],
metadata: HashMap::new(),
};
let instance1 = Instance::new(
"instance1".to_string(),
"jail".to_string(),
config.clone(),
);
let instance2 = Instance::new(
"instance2".to_string(),
"container".to_string(),
config,
);
store.create(instance1)?;
store.create(instance2)?;
let instances = store.list()?;
assert_eq!(instances.len(), 2);
let jail_instances = store.list_by_provider("jail")?;
assert_eq!(jail_instances.len(), 1);
assert_eq!(jail_instances[0].name, "instance1");
Ok(())
}
#[test]
fn test_delete_instance() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
let config = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![],
metadata: HashMap::new(),
};
let instance = Instance::new(
"test-instance".to_string(),
"jail".to_string(),
config,
);
store.create(instance.clone())?;
assert!(store.exists(&instance.name)?);
store.delete(&instance.name)?;
assert!(!store.exists(&instance.name)?);
Ok(())
}
#[test]
fn test_jail_provider() -> Result<()> {
let provider = JailProvider::new().with_dataset("tank/jails".to_string());
let json = provider.to_json()?;
let deserialized = JailProvider::from_json(&json)?;
assert_eq!(provider.dataset, deserialized.dataset);
assert_eq!(provider.provider_type(), "jail");
Ok(())
}
#[test]
fn test_container_provider() -> Result<()> {
let provider = ContainerProvider::docker("nginx:latest".to_string());
let json = provider.to_json()?;
let deserialized = ContainerProvider::from_json(&json)?;
assert_eq!(provider.runtime, deserialized.runtime);
assert_eq!(provider.image, deserialized.image);
assert_eq!(provider.provider_type(), "container");
Ok(())
}
#[test]
fn test_instance_with_range_assigner_network() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
// Set up a network with RangeAssigner
use crate::network::{Network, NetRange, BasicProvider, RangeAssigner};
let provider = BasicProvider::new();
let assigner = RangeAssigner::with_range_name("test_range");
store.networks.create(
"test_network",
NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?,
provider,
Some(assigner),
)?;
// Create instance with network interface
let config = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![
NetworkInterface {
network_name: "test_network".to_string(),
interface_name: "eth0".to_string(),
assignment: None,
},
],
metadata: HashMap::new(),
};
let instance = Instance::new(
"test-with-network".to_string(),
"jail".to_string(),
config,
);
let created = store.create(instance)?;
// Verify instance was created with network assignment
assert_eq!(created.name, "test-with-network");
assert_eq!(created.config.network_interfaces.len(), 1);
let interface = &created.config.network_interfaces[0];
assert_eq!(interface.network_name, "test_network");
assert_eq!(interface.interface_name, "eth0");
// Note: IP assignment would be available if range was properly initialized
// For this test, we verify the structure is correct
Ok(())
}
#[test]
fn test_multiple_instances_different_ip_assignments() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
// Set up a network with RangeAssigner
use crate::network::{Network, NetRange, BasicProvider, RangeAssigner};
let provider = BasicProvider::new();
let assigner = RangeAssigner::with_range_name("multi_test_range");
store.networks.create(
"multi_network",
NetRange::ipv4("10.0.0.0".parse().unwrap(), 24)?,
provider,
Some(assigner),
)?;
// Create first instance
let config1 = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![
NetworkInterface {
network_name: "multi_network".to_string(),
interface_name: "eth0".to_string(),
assignment: None,
},
],
metadata: HashMap::new(),
};
let instance1 = Instance::new(
"instance1".to_string(),
"jail".to_string(),
config1,
);
// Create second instance
let config2 = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![
NetworkInterface {
network_name: "multi_network".to_string(),
interface_name: "eth0".to_string(),
assignment: None,
},
],
metadata: HashMap::new(),
};
let instance2 = Instance::new(
"instance2".to_string(),
"container".to_string(),
config2,
);
let created1 = store.create(instance1)?;
let created2 = store.create(instance2)?;
// Verify both instances have network interfaces
assert_eq!(created1.config.network_interfaces.len(), 1);
assert_eq!(created2.config.network_interfaces.len(), 1);
// Verify they're connected to the same network
assert_eq!(created1.config.network_interfaces[0].network_name, "multi_network");
assert_eq!(created2.config.network_interfaces[0].network_name, "multi_network");
// Both instances should exist in the store
assert!(store.exists("instance1")?);
assert!(store.exists("instance2")?);
Ok(())
}
#[test]
fn test_instance_with_multiple_networks() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
// Set up multiple networks
use crate::network::{Network, NetRange, BasicProvider, RangeAssigner};
// Management network
let mgmt_provider = BasicProvider::new();
let mgmt_assigner = RangeAssigner::with_range_name("mgmt_range");
store.networks.create(
"management",
NetRange::ipv4("10.0.1.0".parse().unwrap(), 24)?,
mgmt_provider,
Some(mgmt_assigner),
)?;
// Public network
let public_provider = BasicProvider::new();
let public_assigner = RangeAssigner::with_range_name("public_range");
store.networks.create(
"public",
NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?,
public_provider,
Some(public_assigner),
)?;
// Create instance with multiple network interfaces
let config = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![
NetworkInterface {
network_name: "management".to_string(),
interface_name: "eth0".to_string(),
assignment: None,
},
NetworkInterface {
network_name: "public".to_string(),
interface_name: "eth1".to_string(),
assignment: None,
},
],
metadata: HashMap::new(),
};
let instance = Instance::new(
"multi-network-instance".to_string(),
"container".to_string(),
config,
);
let created = store.create(instance)?;
// Verify instance has both network interfaces
assert_eq!(created.config.network_interfaces.len(), 2);
let interfaces: Vec<&str> = created.config.network_interfaces
.iter()
.map(|i| i.network_name.as_str())
.collect();
assert!(interfaces.contains(&"management"));
assert!(interfaces.contains(&"public"));
Ok(())
}
#[test]
fn test_instance_network_assignment_cleanup_on_delete() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
// Set up a network
use crate::network::{Network, NetRange, BasicProvider, RangeAssigner};
let provider = BasicProvider::new();
let assigner = RangeAssigner::with_range_name("cleanup_range");
store.networks.create(
"cleanup_network",
NetRange::ipv4("172.16.0.0".parse().unwrap(), 24)?,
provider,
Some(assigner),
)?;
// Create instance
let config = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![
NetworkInterface {
network_name: "cleanup_network".to_string(),
interface_name: "eth0".to_string(),
assignment: None,
},
],
metadata: HashMap::new(),
};
let instance = Instance::new(
"cleanup-test".to_string(),
"jail".to_string(),
config,
);
let created = store.create(instance)?;
assert!(store.exists("cleanup-test")?);
// Delete the instance
store.delete("cleanup-test")?;
assert!(!store.exists("cleanup-test")?);
// Verify instance is removed from namespace
assert!(!store.namespaces.key_exists("instances", "cleanup-test")?);
Ok(())
}
#[test]
fn test_instance_creation_fails_with_nonexistent_network() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
// Try to create instance with non-existent network
let config = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![
NetworkInterface {
network_name: "nonexistent_network".to_string(),
interface_name: "eth0".to_string(),
assignment: None,
},
],
metadata: HashMap::new(),
};
let instance = Instance::new(
"test-nonexistent-network".to_string(),
"jail".to_string(),
config,
);
// This should succeed because network assignment is not enforced during creation
// The instance will be created but without IP assignments
let created = store.create(instance)?;
assert_eq!(created.config.network_interfaces[0].assignment, None);
Ok(())
}
#[test]
fn test_instance_transaction_rollback_preserves_state() -> Result<()> {
let (_temp_dir, store) = create_test_stores()?;
// Set up a network
use crate::network::{Network, NetRange, BasicProvider, RangeAssigner};
let provider = BasicProvider::new();
let assigner = RangeAssigner::with_range_name("tx_test_range");
store.networks.create(
"tx_network",
NetRange::ipv4("172.20.0.0".parse().unwrap(), 24)?,
provider,
Some(assigner),
)?;
// Create first instance successfully
let config1 = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![
NetworkInterface {
network_name: "tx_network".to_string(),
interface_name: "eth0".to_string(),
assignment: None,
},
],
metadata: HashMap::new(),
};
let instance1 = Instance::new(
"tx-test-1".to_string(),
"jail".to_string(),
config1,
);
store.create(instance1)?;
assert!(store.exists("tx-test-1")?);
// Verify namespace contains the instance
assert!(store.namespaces.key_exists("instances", "tx-test-1")?);
// Try to create duplicate instance (should fail)
let config2 = InstanceConfig {
provider_config: "{}".to_string(),
network_interfaces: vec![],
metadata: HashMap::new(),
};
let instance2 = Instance::new(
"tx-test-1".to_string(), // Same name - should fail
"container".to_string(),
config2,
);
// This should fail due to duplicate name
assert!(store.create(instance2).is_err());
// Original instance should still exist
assert!(store.exists("tx-test-1")?);
assert!(store.namespaces.key_exists("instances", "tx-test-1")?);
Ok(())
}
}
\ No newline at end of file
diff --git a/crates/store/src/network.rs b/crates/store/src/network.rs
index 2421c27..6c8a4f3 100644
--- a/crates/store/src/network.rs
+++ b/crates/store/src/network.rs
@@ -1,1535 +1,1664 @@
//! Network management module for storing and managing network configurations.
//!
//! This module provides functionality to create, read, update, and delete network
//! configurations. Each network is defined by a name, network range (IPv4 or IPv6),
//! a provider implementation, and an optional assigner implementation.
//!
//! # Example
//!
//! ```
//! use store::network::*;
//! # use tempfile::TempDir;
//! # fn example() -> store::Result<()> {
//! # let temp_dir = tempfile::tempdir().unwrap();
//! # let db = sled::open(temp_dir.path())?;
//! let store = NetworkStore::open(&db)?;
//!
//! // Create a network with IPv4 range
//! let netrange = NetRange::from_cidr("192.168.1.0/24")?;
//! let provider = BasicProvider::new()
//! .with_config("type", "aws")
//! .with_config("region", "us-west-2");
//! let assigner = Some(BasicAssigner::new("dhcp"));
//!
//! store.create("production", netrange, provider, assigner)?;
//!
//! // Retrieve the network
//! let network = store.get("production")?.unwrap();
//! assert_eq!(network.name, "production");
//!
//! // List all networks
//! let networks = store.list()?;
//! assert!(networks.contains(&"production".to_string()));
//! # Ok(())
//! # }
//! ```
use crate::{Result, Error, RangeStore};
use sled::Transactional;
use std::collections::HashMap;
use std::net::{Ipv4Addr, Ipv6Addr, IpAddr};
/// Represents an IPv4 or IPv6 network range
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum NetRange {
/// IPv4 network with address and prefix length
V4 { addr: Ipv4Addr, prefix: u8 },
/// IPv6 network with address and prefix length
V6 { addr: Ipv6Addr, prefix: u8 },
}
impl NetRange {
/// Create a new IPv4 network range
pub fn ipv4(addr: Ipv4Addr, prefix: u8) -> Result<Self> {
if prefix > 32 {
return Err(Error::StoreError(sled::Error::Unsupported(
format!("Invalid IPv4 prefix length: {}", prefix)
)));
}
Ok(NetRange::V4 { addr, prefix })
}
/// Create a new IPv6 network range
pub fn ipv6(addr: Ipv6Addr, prefix: u8) -> Result<Self> {
if prefix > 128 {
return Err(Error::StoreError(sled::Error::Unsupported(
format!("Invalid IPv6 prefix length: {}", prefix)
)));
}
Ok(NetRange::V6 { addr, prefix })
}
/// Parse a network range from CIDR notation
pub fn from_cidr(cidr: &str) -> Result<Self> {
let parts: Vec<&str> = cidr.split('/').collect();
if parts.len() != 2 {
return Err(Error::StoreError(sled::Error::Unsupported(
"Invalid CIDR format".to_string()
)));
}
let prefix: u8 = parts[1].parse().map_err(|_| {
Error::StoreError(sled::Error::Unsupported("Invalid prefix length".to_string()))
})?;
if let Ok(ipv4) = parts[0].parse::<Ipv4Addr>() {
Self::ipv4(ipv4, prefix)
} else if let Ok(ipv6) = parts[0].parse::<Ipv6Addr>() {
Self::ipv6(ipv6, prefix)
} else {
Err(Error::StoreError(sled::Error::Unsupported(
"Invalid IP address format".to_string()
)))
}
}
/// Convert to CIDR notation string
pub fn to_cidr(&self) -> String {
match self {
NetRange::V4 { addr, prefix } => format!("{}/{}", addr, prefix),
NetRange::V6 { addr, prefix } => format!("{}/{}", addr, prefix),
}
}
}
/// Trait for network providers
pub trait NetworkProvider: std::fmt::Debug {
/// Get the provider type identifier
fn provider_type(&self) -> &'static str;
/// Serialize provider configuration to JSON
fn to_json(&self) -> Result<String>;
/// Create provider from JSON
fn from_json(json: &str) -> Result<Self> where Self: Sized;
}
/// Trait for network assigners
pub trait NetworkAssigner: std::fmt::Debug {
/// Get the assigner type identifier
fn assigner_type(&self) -> &'static str;
/// Serialize assigner configuration to JSON
fn to_json(&self) -> Result<String>;
/// Create assigner from JSON
fn from_json(json: &str) -> Result<Self> where Self: Sized;
/// Initialize the assigner for a network during creation
fn initialize_for_network(&self, _network_name: &str, _netrange: &NetRange, _range_store: Option<&RangeStore>) -> Result<()> {
// Default implementation does nothing
Ok(())
}
/// Initialize the assigner for a network within a transaction
fn initialize_for_network_in_transaction(
&self,
_network_name: &str,
_netrange: &NetRange,
_range_trees: Option<(&sled::transaction::TransactionalTree, &sled::transaction::TransactionalTree)>
) -> Result<(), sled::transaction::ConflictableTransactionError<()>> {
// Default implementation does nothing
Ok(())
}
/// Cleanup the assigner for a network during deletion
fn cleanup_for_network(&self, _network_name: &str, _range_store: Option<&RangeStore>) -> Result<()> {
// Default implementation does nothing
Ok(())
}
/// Cleanup the assigner for a network within a transaction
fn cleanup_for_network_in_transaction(
&self,
_network_name: &str,
_range_trees: Option<(&sled::transaction::TransactionalTree, &sled::transaction::TransactionalTree)>
) -> Result<(), sled::transaction::ConflictableTransactionError<()>> {
// Default implementation does nothing
Ok(())
}
/// Get an IP assignment for the given identifier
fn get_assignment(&self, _network: &Network, _identifier: &str, _range_store: Option<&RangeStore>) -> Result<Option<IpAddr>> {
// Default implementation returns None (no assignment tracking)
Ok(None)
}
}
/// Basic network provider implementation
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BasicProvider {
pub config: HashMap<String, String>,
}
impl BasicProvider {
pub fn new() -> Self {
Self {
config: HashMap::new(),
}
}
pub fn with_config(mut self, key: &str, value: &str) -> Self {
self.config.insert(key.to_string(), value.to_string());
self
}
}
impl NetworkProvider for BasicProvider {
fn provider_type(&self) -> &'static str {
"basic"
}
fn to_json(&self) -> Result<String> {
serde_json::to_string(self).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))
})
}
fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e)))
})
}
}
/// Basic network assigner implementation
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BasicAssigner {
pub strategy: String,
pub config: HashMap<String, String>,
}
impl BasicAssigner {
pub fn new(strategy: &str) -> Self {
Self {
strategy: strategy.to_string(),
config: HashMap::new(),
}
}
pub fn with_config(mut self, key: &str, value: &str) -> Self {
self.config.insert(key.to_string(), value.to_string());
self
}
}
impl NetworkAssigner for BasicAssigner {
fn assigner_type(&self) -> &'static str {
"basic"
}
fn to_json(&self) -> Result<String> {
serde_json::to_string(self).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))
})
}
fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e)))
})
}
}
/// Range-based network assigner that uses RangeStore for IP assignment
///
/// The `RangeAssigner` provides IP address assignment functionality by integrating
/// with the `RangeStore`. It automatically manages IP address allocation within
/// a network range using a bitmap-based approach for efficient tracking.
///
/// # Features
///
/// - Automatic range initialization when creating networks
/// - Sequential IP assignment within the network range
/// - Assignment tracking with custom identifiers
/// - IP unassignment and reuse
/// - Integration with the NetworkStore lifecycle
///
/// # Example
///
/// ```
/// use store::network::{RangeAssigner, NetRange, BasicProvider, NetworkStore};
/// # use tempfile::TempDir;
/// # fn example() -> store::Result<()> {
/// # let temp_dir = tempfile::tempdir().unwrap();
/// # let db = sled::open(temp_dir.path())?;
/// # let ranges = store::range::RangeStore::open(&db)?;
/// # let store = NetworkStore::open_with_ranges(&db, ranges.clone())?;
///
/// // Create a RangeAssigner for a network (uses network name as range name)
/// let assigner = RangeAssigner::new()
/// .with_config("strategy", "sequential");
///
/// // Create a network with the range assigner
/// let netrange = NetRange::from_cidr("192.168.1.0/24")?;
/// let provider = BasicProvider::new();
/// store.create("my-network", netrange, provider, Some(assigner))?;
///
/// // Use the assigner to allocate IPs
/// let range_assigner = RangeAssigner::new();
/// let ip_bit = range_assigner.assign_ip("my-network", &ranges, "device1")?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RangeAssigner {
pub range_name: Option<String>,
pub config: HashMap<String, String>,
}
impl RangeAssigner {
/// Create a new RangeAssigner with default settings
///
/// The range name will default to the network name when the network is created.
/// Use `with_range_name()` to specify a custom range name if needed.
pub fn new() -> Self {
Self {
range_name: None,
config: HashMap::new(),
}
}
/// Create a new RangeAssigner with a specific range name
///
/// # Arguments
///
/// * `range_name` - Custom identifier for the IP range
pub fn with_range_name(range_name: &str) -> Self {
Self {
range_name: Some(range_name.to_string()),
config: HashMap::new(),
}
}
/// Add configuration key-value pairs to the assigner
///
/// Configuration options can be used to customize the behavior of the
/// assigner. Common configuration keys might include:
/// - `strategy`: Assignment strategy (e.g., "sequential", "random")
/// - `reserve_gateway`: Whether to reserve the first IP for gateway
/// - `pool_size`: Maximum number of IPs to manage
///
/// # Arguments
///
/// * `key` - Configuration key
/// * `value` - Configuration value
pub fn with_config(mut self, key: &str, value: &str) -> Self {
self.config.insert(key.to_string(), value.to_string());
self
}
/// Initialize the range for this assigner with the given network size
///
/// This method calculates the number of available IP addresses in the network
/// range and creates a corresponding range in the RangeStore. It's automatically
/// called when creating a network with a RangeAssigner.
///
/// # Arguments
///
/// * `range_name` - Name to use for the range
/// * `range_store` - Reference to the RangeStore
/// * `network` - Network range (IPv4 or IPv6) to initialize
///
/// # Returns
///
/// Returns `Ok(())` if the range was successfully initialized, or an error
/// if the network parameters are invalid or the range already exists.
pub fn initialize_range(&self, range_name: &str, range_store: &RangeStore, network: &NetRange) -> Result<()> {
let size = match network {
NetRange::V4 { prefix, .. } => {
if *prefix >= 32 {
return Err(Error::StoreError(sled::Error::Unsupported(
"IPv4 prefix must be less than 32".to_string()
)));
}
1u64 << (32 - prefix)
}
NetRange::V6 { prefix, .. } => {
if *prefix >= 128 {
return Err(Error::StoreError(sled::Error::Unsupported(
"IPv6 prefix must be less than 128".to_string()
)));
}
// For IPv6, we'll limit to a reasonable size to avoid huge ranges
let host_bits = 128 - prefix;
if host_bits > 32 {
1u64 << 32 // Cap at 2^32 addresses
} else {
1u64 << host_bits
}
}
};
range_store.define(range_name, size)
}
/// Assign an IP address from the range
///
/// Assigns the next available IP address in the range to the specified identifier.
/// The assignment uses a sequential allocation strategy, finding the first
/// available bit position in the range bitmap.
///
/// # Arguments
///
/// * `range_name` - Name of the range to assign from
/// * `range_store` - Reference to the RangeStore
/// * `identifier` - Unique identifier for the device/entity receiving the IP
///
/// # Returns
///
/// Returns the bit position of the assigned IP address, which can be used
/// to calculate the actual IP address within the network range.
///
/// # Errors
///
/// Returns an error if the range is full or doesn't exist.
pub fn assign_ip(&self, range_name: &str, range_store: &RangeStore, identifier: &str) -> Result<u64> {
range_store.assign(range_name, identifier)
}
/// Get assigned IP information
///
/// Retrieves the identifier associated with a specific bit position in the range.
/// This is useful for determining which device or entity is assigned to a
/// particular IP address.
///
/// # Arguments
///
/// * `range_name` - Name of the range to query
/// * `range_store` - Reference to the RangeStore
/// * `bit_position` - The bit position to query
///
/// # Returns
///
/// Returns `Some(identifier)` if the bit position is assigned, or `None`
/// if it's free or out of range.
pub fn get_assignment_by_bit(&self, range_name: &str, range_store: &RangeStore, bit_position: u64) -> Result<Option<String>> {
range_store.get(range_name, bit_position)
}
/// Unassign an IP address
///
/// Releases an IP address assignment, making it available for future assignments.
/// This is typically called when a device is removed or no longer needs its
/// assigned IP address.
///
/// # Arguments
///
/// * `range_name` - Name of the range to unassign from
/// * `range_store` - Reference to the RangeStore
/// * `bit_position` - The bit position to unassign
///
/// # Returns
///
/// Returns `true` if the IP was successfully unassigned, or `false` if it
/// was already free or the bit position is out of range.
pub fn unassign_ip(&self, range_name: &str, range_store: &RangeStore, bit_position: u64) -> Result<bool> {
range_store.unassign_bit(range_name, bit_position)
}
/// Get the range name for this assigner, using the network name as default
///
/// # Arguments
///
/// * `network_name` - Network name to use as default if no custom range name is set
///
/// # Returns
///
/// Returns the range name to use for operations
pub fn get_range_name(&self, network_name: &str) -> String {
self.range_name.clone().unwrap_or_else(|| network_name.to_string())
}
/// Convert a bit position to an IP address within the network range
///
/// # Arguments
///
/// * `bit_position` - The bit position within the range
/// * `netrange` - The network range to calculate the IP within
///
/// # Returns
///
/// Returns the IP address corresponding to the bit position
pub fn bit_position_to_ip(&self, bit_position: u64, netrange: &NetRange) -> Result<IpAddr> {
match netrange {
NetRange::V4 { addr, prefix: _ } => {
let network_addr = u32::from(*addr);
let host_addr = network_addr + bit_position as u32;
Ok(IpAddr::V4(Ipv4Addr::from(host_addr)))
}
NetRange::V6 { addr, prefix: _ } => {
let network_addr = u128::from(*addr);
let host_addr = network_addr + bit_position as u128;
Ok(IpAddr::V6(Ipv6Addr::from(host_addr)))
}
}
}
/// Find the bit position for a given identifier in the range
///
/// # Arguments
///
/// * `range_name` - Name of the range to search
/// * `range_store` - Reference to the RangeStore
/// * `identifier` - The identifier to search for
///
/// # Returns
///
/// Returns the bit position if found, None otherwise
pub fn find_identifier_bit_position(&self, range_name: &str, range_store: &RangeStore, identifier: &str) -> Result<Option<u64>> {
// Get range ID first
let range_id = match range_store.names.get(range_name)? {
Some(data) => {
if data.len() != 16 {
return Ok(None);
}
let id_bytes: [u8; 8] = data[0..8].try_into()
.map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid range data".to_string())))?;
u64::from_be_bytes(id_bytes)
}
None => return Ok(None),
};
// Search for the assignment with this identifier
let prefix = range_id.to_be_bytes();
for result in range_store.assign.scan_prefix(&prefix) {
let (key, stored_value) = result?;
if key.len() == 16 {
let stored_value_str = String::from_utf8(stored_value.to_vec())?;
if stored_value_str == identifier {
let bit_position_bytes: [u8; 8] = key[8..16].try_into()
.map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid key format".to_string())))?;
return Ok(Some(u64::from_be_bytes(bit_position_bytes)));
}
}
}
Ok(None)
}
}
impl NetworkAssigner for RangeAssigner {
fn assigner_type(&self) -> &'static str {
"range"
}
fn to_json(&self) -> Result<String> {
serde_json::to_string(self).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))
})
}
fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e)))
})
}
fn initialize_for_network(&self, network_name: &str, netrange: &NetRange, range_store: Option<&RangeStore>) -> Result<()> {
if let Some(store) = range_store {
let range_name = self.get_range_name(network_name);
self.initialize_range(&range_name, store, netrange)?;
}
Ok(())
}
fn initialize_for_network_in_transaction(
&self,
network_name: &str,
netrange: &NetRange,
range_trees: Option<(&sled::transaction::TransactionalTree, &sled::transaction::TransactionalTree)>
) -> Result<(), sled::transaction::ConflictableTransactionError<()>> {
if let Some((range_names, range_map)) = range_trees {
let range_name = self.get_range_name(network_name);
let size = match netrange {
NetRange::V4 { prefix, .. } => {
if *prefix >= 32 {
return Err(sled::transaction::ConflictableTransactionError::Abort(()));
}
1u64 << (32 - prefix)
}
NetRange::V6 { prefix, .. } => {
if *prefix >= 128 {
return Err(sled::transaction::ConflictableTransactionError::Abort(()));
}
// For IPv6, we'll limit to a reasonable size to avoid huge ranges
let host_bits = 128 - prefix;
if host_bits > 32 {
1u64 << 32 // Cap at 2^32 addresses
} else {
1u64 << host_bits
}
}
};
// Define the range within the transaction
match range_names.get(range_name.as_bytes())? {
Some(_) => Ok(()), // Range already exists
None => {
let id = range_names.generate_id()?;
// Store name -> (id, size)
let mut value = Vec::new();
value.extend_from_slice(&id.to_be_bytes());
value.extend_from_slice(&size.to_be_bytes());
range_names.insert(range_name.as_bytes(), value)?;
// Initialize bitmap for the range (all zeros)
let bitmap_size = (size + 7) / 8; // Round up to nearest byte
let bitmap = vec![0u8; bitmap_size as usize];
range_map.insert(&id.to_be_bytes(), bitmap)?;
Ok(())
}
}
} else {
Ok(())
}
}
fn get_assignment(&self, network: &Network, identifier: &str, range_store: Option<&RangeStore>) -> Result<Option<IpAddr>> {
if let Some(store) = range_store {
let range_name = self.get_range_name(&network.name);
// Find the bit position for this identifier
if let Some(bit_position) = self.find_identifier_bit_position(&range_name, store, identifier)? {
// Convert bit position to IP address
let ip = self.bit_position_to_ip(bit_position, &network.netrange)?;
Ok(Some(ip))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
fn cleanup_for_network(&self, network_name: &str, range_store: Option<&RangeStore>) -> Result<()> {
if let Some(store) = range_store {
let range_name = self.get_range_name(network_name);
store.delete_range(&range_name)?;
}
Ok(())
}
fn cleanup_for_network_in_transaction(
&self,
network_name: &str,
range_trees: Option<(&sled::transaction::TransactionalTree, &sled::transaction::TransactionalTree)>
) -> Result<(), sled::transaction::ConflictableTransactionError<()>> {
if let Some((range_names, range_map)) = range_trees {
let range_name = self.get_range_name(network_name);
// Get the range ID from the name
if let Some(value) = range_names.get(range_name.as_bytes())? {
let id = u64::from_be_bytes([
value[0], value[1], value[2], value[3],
value[4], value[5], value[6], value[7],
]);
// Remove the range data
range_map.remove(&id.to_be_bytes())?;
}
// Remove the range name
range_names.remove(range_name.as_bytes())?;
}
Ok(())
}
}
/// Network configuration
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Network {
pub name: String,
pub netrange: NetRange,
pub provider_type: String,
pub provider_config: String,
pub assigner_type: Option<String>,
pub assigner_config: Option<String>,
}
impl Network {
/// Get an IP assignment for the given identifier
///
/// This method calls the appropriate assigner implementation to retrieve
/// the assigned IP address for a given identifier. Different assigners
/// may return the IP in different formats (bit position or full IP).
///
/// # Arguments
///
/// * `identifier` - The identifier to look up (e.g., device name, container ID)
/// * `range_store` - Optional reference to RangeStore (required for RangeAssigner)
///
/// # Returns
///
/// Returns the assigned IP address if found, None if no assignment exists
///
/// # Example
///
/// ```
/// # use store::network::{Network, NetRange, RangeAssigner};
/// # use tempfile::TempDir;
/// # fn example() -> store::Result<()> {
/// # let temp_dir = tempfile::tempdir().unwrap();
/// # let db = sled::open(temp_dir.path())?;
/// # let ranges = store::range::RangeStore::open(&db)?;
/// # let store = store::network::NetworkStore::open_with_ranges(&db, ranges.clone())?;
/// # let netrange = NetRange::from_cidr("192.168.1.0/24")?;
/// # let provider = store::network::BasicProvider::new();
/// # let assigner = RangeAssigner::new();
/// # store.create("test", netrange, provider, Some(assigner))?;
/// # let network = store.get("test")?.unwrap();
///
/// let ip = network.get_assignment("device1", Some(&ranges))?;
/// if let Some(addr) = ip {
/// println!("Device1 is assigned IP: {}", addr);
/// }
/// # Ok(())
/// # }
/// ```
/// Create an assigner instance from the network configuration
pub fn create_assigner(&self) -> Result<Box<dyn NetworkAssigner>> {
if let (Some(assigner_type), Some(assigner_config)) = (&self.assigner_type, &self.assigner_config) {
match assigner_type.as_str() {
"range" => {
let range_assigner: RangeAssigner = serde_json::from_str(assigner_config)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(
format!("Failed to deserialize RangeAssigner: {}", e)
)))?;
Ok(Box::new(range_assigner))
}
"basic" => {
let basic_assigner: BasicAssigner = serde_json::from_str(assigner_config)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(
format!("Failed to deserialize BasicAssigner: {}", e)
)))?;
Ok(Box::new(basic_assigner))
}
_ => {
Err(Error::StoreError(sled::Error::Unsupported(
format!("Unknown assigner type: {}", assigner_type)
)))
}
}
} else {
Err(Error::StoreError(sled::Error::Unsupported(
"No assigner configured".to_string()
)))
}
}
pub fn get_assignment(&self, identifier: &str, range_store: Option<&RangeStore>) -> Result<Option<IpAddr>> {
if let (Some(assigner_type), Some(assigner_config)) = (&self.assigner_type, &self.assigner_config) {
match assigner_type.as_str() {
"range" => {
let range_assigner: RangeAssigner = serde_json::from_str(assigner_config)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(
format!("Failed to deserialize RangeAssigner: {}", e)
)))?;
range_assigner.get_assignment(self, identifier, range_store)
}
"basic" => {
let basic_assigner: BasicAssigner = serde_json::from_str(assigner_config)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(
format!("Failed to deserialize BasicAssigner: {}", e)
)))?;
basic_assigner.get_assignment(self, identifier, range_store)
}
_ => {
// Unknown assigner type
Ok(None)
}
}
} else {
// No assigner configured
Ok(None)
}
}
+ /// Automatically assign an IP address for the given identifier
+ ///
+ /// This method will assign an IP address from the network's range if it has
+ /// a RangeAssigner configured. This is typically called during instance creation
+ /// to automatically provision network connectivity.
+ ///
+ /// The method performs the following steps:
+ /// 1. Checks if the network has a RangeAssigner configured
+ /// 2. If so, calls the assigner's assign_ip method to allocate an IP
+ /// 3. The assigned IP can then be retrieved using get_assignment()
+ ///
+ /// # Arguments
+ ///
+ /// * `identifier` - The identifier to assign an IP to (e.g., instance name, container ID)
+ /// * `range_store` - Reference to the RangeStore for managing IP assignments
+ ///
+ /// # Returns
+ ///
+ /// * `Ok(())` - Assignment was successful or no assigner is configured
+ /// * `Err(Error)` - Assignment failed (e.g., range is full, network error)
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use store::network::{Network, NetRange, BasicProvider, RangeAssigner};
+ /// # use store::RangeStore;
+ /// # use tempfile::TempDir;
+ /// # fn example() -> store::Result<()> {
+ /// # let temp_dir = TempDir::new().unwrap();
+ /// # let db = sled::open(temp_dir.path())?;
+ /// # let ranges = RangeStore::open(&db)?;
+ /// # let networks = store::NetworkStore::open_with_ranges(&db, ranges.clone())?;
+ /// # let netrange = NetRange::from_cidr("10.0.1.0/24")?;
+ /// # let provider = BasicProvider::new();
+ /// # let assigner = RangeAssigner::with_range_name("test_range");
+ /// # networks.create("test_net", netrange, provider, Some(assigner))?;
+ /// # let network = networks.get("test_net")?.unwrap();
+ /// # let ranges_ref = networks.ranges().unwrap();
+ ///
+ /// // Automatically assign an IP to a jail instance
+ /// network.auto_assign_ip("my-jail-01", ranges_ref)?;
+ ///
+ /// // Retrieve the assigned IP
+ /// let assigned_ip = network.get_assignment("my-jail-01", Some(ranges_ref))?;
+ /// println!("Assigned IP: {:?}", assigned_ip);
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// # Note
+ ///
+ /// This method is automatically called during instance creation when the instance
+ /// has network interfaces configured for networks with RangeAssigners. Manual calls
+ /// are typically not needed unless implementing custom provisioning logic.
+ pub fn auto_assign_ip(&self, identifier: &str, range_store: &RangeStore) -> Result<()> {
+ if let (Some(assigner_type), Some(assigner_config)) = (&self.assigner_type, &self.assigner_config) {
+ match assigner_type.as_str() {
+ "range" => {
+ let range_assigner: RangeAssigner = serde_json::from_str(assigner_config)
+ .map_err(|e| Error::StoreError(sled::Error::Unsupported(
+ format!("Failed to deserialize RangeAssigner: {}", e)
+ )))?;
+
+ let range_name = range_assigner.get_range_name(&self.name);
+
+ // Try to assign an IP - ignore the result bit position
+ range_assigner.assign_ip(&range_name, range_store, identifier)
+ .map(|_| ())
+ }
+ _ => {
+ // Other assigner types don't support auto-assignment
+ Ok(())
+ }
+ }
+ } else {
+ // No assigner configured, nothing to do
+ Ok(())
+ }
+ }
+
+ /// Automatically unassign an IP address for the given identifier
+ ///
+ /// This method will unassign an IP address from the network's range if it has
+ /// a RangeAssigner configured and the identifier has an assignment. This is
+ /// typically called during instance deletion to clean up network resources.
+ ///
+ /// # Arguments
+ ///
+ /// * `identifier` - The identifier to unassign an IP from (e.g., instance name)
+ /// * `range_store` - Reference to the RangeStore for managing IP assignments
+ ///
+ /// # Returns
+ ///
+ /// * `Ok(())` - Unassignment was successful or no assigner is configured
+ /// * `Err(Error)` - Unassignment failed due to network error
+ pub fn auto_unassign_ip(&self, identifier: &str, range_store: &RangeStore) -> Result<()> {
+ if let (Some(assigner_type), Some(assigner_config)) = (&self.assigner_type, &self.assigner_config) {
+ match assigner_type.as_str() {
+ "range" => {
+ let range_assigner: RangeAssigner = serde_json::from_str(assigner_config)
+ .map_err(|e| Error::StoreError(sled::Error::Unsupported(
+ format!("Failed to deserialize RangeAssigner: {}", e)
+ )))?;
+
+ let range_name = range_assigner.get_range_name(&self.name);
+
+ // Find the bit position for this identifier
+ if let Some(bit_position) = range_assigner.find_identifier_bit_position(&range_name, range_store, identifier)? {
+ // Unassign the IP
+ range_assigner.unassign_ip(&range_name, range_store, bit_position)?;
+ }
+ Ok(())
+ }
+ _ => {
+ // Other assigner types don't support auto-unassignment
+ Ok(())
+ }
+ }
+ } else {
+ // No assigner configured, nothing to do
+ Ok(())
+ }
+ }
+
/// Get an IP address from a bit position in the network range
///
/// This method converts a bit position to the corresponding IP address
/// within this network's range. Useful when you already know the bit
/// position from range operations.
///
/// # Arguments
///
/// * `bit_position` - The bit position within the network range
///
/// # Returns
///
/// Returns the IP address corresponding to the bit position
///
/// # Example
///
/// ```
/// # use store::network::{Network, NetRange};
/// # fn example() -> store::Result<()> {
/// # let netrange = NetRange::from_cidr("192.168.1.0/24")?;
/// let network = Network {
/// name: "test".to_string(),
/// netrange: netrange,
/// provider_type: "basic".to_string(),
/// provider_config: "{}".to_string(),
/// assigner_type: None,
/// assigner_config: None,
/// };
///
/// let ip = network.get_assignment_by_bit_position(5)?;
/// println!("Bit position 5 corresponds to IP: {}", ip);
/// # Ok(())
/// # }
/// ```
pub fn get_assignment_by_bit_position(&self, bit_position: u64) -> Result<IpAddr> {
match &self.netrange {
NetRange::V4 { addr, prefix: _ } => {
let network_addr = u32::from(*addr);
let host_addr = network_addr + bit_position as u32;
Ok(IpAddr::V4(Ipv4Addr::from(host_addr)))
}
NetRange::V6 { addr, prefix: _ } => {
let network_addr = u128::from(*addr);
let host_addr = network_addr + bit_position as u128;
Ok(IpAddr::V6(Ipv6Addr::from(host_addr)))
}
}
}
}
/// Network store for managing network configurations
#[derive(Debug, Clone)]
pub struct NetworkStore {
pub(crate) namespaces: crate::namespace::NamespaceStore,
pub(crate) networks: sled::Tree,
pub(crate) ranges: Option<crate::range::RangeStore>,
}
impl NetworkStore {
/// Open a new network store
pub fn open(db: &sled::Db) -> Result<Self> {
Ok(NetworkStore {
namespaces: crate::namespace::NamespaceStore::open(db)?,
networks: db.open_tree("networks/1/data")?,
ranges: None,
})
}
/// Open a new network store with range store
pub fn open_with_ranges(db: &sled::Db, ranges: crate::range::RangeStore) -> Result<Self> {
Ok(NetworkStore {
namespaces: crate::namespace::NamespaceStore::open(db)?,
networks: db.open_tree("networks/1/data")?,
ranges: Some(ranges),
})
}
+ /// Get reference to the range store
+ pub fn ranges(&self) -> Option<&crate::range::RangeStore> {
+ self.ranges.as_ref()
+ }
+
/// Create a new network with provider and optional assigner
pub fn create<P, A>(&self, name: &str, netrange: NetRange, provider: P, assigner: Option<A>) -> Result<()>
where
P: NetworkProvider,
A: NetworkAssigner,
{
// Ensure "networks" namespace exists
if !self.namespaces.namespace_exists("networks")? {
self.namespaces.define("networks")?;
}
let network = Network {
name: name.to_string(),
netrange: netrange.clone(),
provider_type: provider.provider_type().to_string(),
provider_config: provider.to_json()?,
assigner_type: assigner.as_ref().map(|a| a.assigner_type().to_string()),
assigner_config: assigner.as_ref().map(|a| a.to_json()).transpose()?,
};
let network_json = serde_json::to_string(&network).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))
})?;
// Include range store trees in transaction if needed
if let (Some(assigner_ref), Some(range_store)) = (&assigner, &self.ranges) {
if assigner_ref.assigner_type() == "range" {
// Transaction with both network and range stores
(&self.namespaces.names, &self.namespaces.spaces, &self.networks, &range_store.names, &range_store.map).transaction(
|(names, spaces, networks, range_names, range_map)| {
// Reserve the network name in the "networks" namespace
if !self.namespaces.reserve_in_transaction(names, spaces, "networks", name, name)? {
return Err(sled::transaction::ConflictableTransactionError::Abort(()));
}
// Initialize the assigner within the transaction
assigner_ref.initialize_for_network_in_transaction(name, &netrange, Some((range_names, range_map)))?;
// Store the network configuration
networks.insert(name.as_bytes(), network_json.as_bytes())?;
Ok(())
}
).map_err(|e| match e {
sled::transaction::TransactionError::Abort(()) => {
Error::NamespaceKeyReserved("networks".to_string(), name.to_string())
}
sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
})?;
} else {
// Transaction without range store
(&self.namespaces.names, &self.namespaces.spaces, &self.networks).transaction(
|(names, spaces, networks)| {
// Reserve the network name in the "networks" namespace
if !self.namespaces.reserve_in_transaction(names, spaces, "networks", name, name)? {
return Err(sled::transaction::ConflictableTransactionError::Abort(()));
}
// Store the network configuration
networks.insert(name.as_bytes(), network_json.as_bytes())?;
Ok(())
}
).map_err(|e| match e {
sled::transaction::TransactionError::Abort(()) => {
Error::NamespaceKeyReserved("networks".to_string(), name.to_string())
}
sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
})?;
}
} else {
// Transaction without assigner or range store
(&self.namespaces.names, &self.namespaces.spaces, &self.networks).transaction(
|(names, spaces, networks)| {
// Reserve the network name in the "networks" namespace
if !self.namespaces.reserve_in_transaction(names, spaces, "networks", name, name)? {
return Err(sled::transaction::ConflictableTransactionError::Abort(()));
}
// Store the network configuration
networks.insert(name.as_bytes(), network_json.as_bytes())?;
Ok(())
}
).map_err(|e| match e {
sled::transaction::TransactionError::Abort(()) => {
Error::NamespaceKeyReserved("networks".to_string(), name.to_string())
}
sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
})?;
}
Ok(())
}
/// Get a network by name
pub fn get(&self, name: &str) -> Result<Option<Network>> {
if let Some(data) = self.networks.get(name.as_bytes())? {
let network_json = String::from_utf8(data.to_vec())?;
let network: Network = serde_json::from_str(&network_json).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e)))
})?;
Ok(Some(network))
} else {
Ok(None)
}
}
/// Update an existing network
pub fn update<P, A>(&self, name: &str, netrange: NetRange, provider: P, assigner: Option<A>) -> Result<()>
where
P: NetworkProvider,
A: NetworkAssigner,
{
if !self.namespaces.key_exists("networks", name)? {
return Err(Error::StoreError(sled::Error::Unsupported(
format!("Network '{}' does not exist", name)
)));
}
let network = Network {
name: name.to_string(),
netrange,
provider_type: provider.provider_type().to_string(),
provider_config: provider.to_json()?,
assigner_type: assigner.as_ref().map(|a| a.assigner_type().to_string()),
assigner_config: assigner.as_ref().map(|a| a.to_json()).transpose()?,
};
let network_json = serde_json::to_string(&network).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))
})?;
self.networks.insert(name.as_bytes(), network_json.as_bytes())?;
Ok(())
}
/// Delete a network by name
pub fn delete(&self, name: &str) -> Result<bool> {
let result = if let Some(ranges) = &self.ranges {
// Transaction with range cleanup
(&self.namespaces.names, &self.namespaces.spaces, &self.networks, &ranges.names, &ranges.map).transaction(
|(names, spaces, networks, range_names, range_map)| {
// Get network data before deletion to access assigner
if let Some(network_data) = networks.get(name.as_bytes())? {
let network: Network = serde_json::from_slice(network_data.as_ref())
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
// Clean up assigner if it exists
if network.assigner_type.is_some() {
let assigner = network.create_assigner()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
assigner.cleanup_for_network_in_transaction(name, Some((range_names, range_map)))?;
}
}
// Remove from namespace
let removed = self.namespaces.remove_in_transaction(names, spaces, "networks", name)?;
if removed {
// Remove network data
networks.remove(name.as_bytes())?;
}
Ok(removed)
}
)
} else {
// Transaction without range cleanup
(&self.namespaces.names, &self.namespaces.spaces, &self.networks).transaction(
|(names, spaces, networks)| {
// Remove from namespace
let removed = self.namespaces.remove_in_transaction(names, spaces, "networks", name)?;
if removed {
// Remove network data
networks.remove(name.as_bytes())?;
}
Ok(removed)
}
)
};
let result = result.map_err(|e| match e {
sled::transaction::TransactionError::Abort(()) => {
Error::StoreError(sled::Error::Unsupported("Transaction aborted".to_string()))
}
sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
})?;
Ok(result)
}
/// List all network names
pub fn list(&self) -> Result<Vec<String>> {
self.namespaces.list_keys("networks")
}
/// Check if a network exists
pub fn exists(&self, name: &str) -> Result<bool> {
self.namespaces.key_exists("networks", name)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_store() -> Result<(NetworkStore, TempDir)> {
let temp_dir = tempfile::tempdir().unwrap();
let db = sled::open(temp_dir.path())?;
let ranges = crate::range::RangeStore::open(&db)?;
let store = NetworkStore::open_with_ranges(&db, ranges)?;
Ok((store, temp_dir))
}
#[test]
fn test_netrange_ipv4() -> Result<()> {
let netrange = NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?;
assert_eq!(netrange.to_cidr(), "192.168.1.0/24");
Ok(())
}
#[test]
fn test_netrange_ipv6() -> Result<()> {
let netrange = NetRange::ipv6("2001:db8::".parse().unwrap(), 64)?;
assert_eq!(netrange.to_cidr(), "2001:db8::/64");
Ok(())
}
#[test]
fn test_netrange_from_cidr() -> Result<()> {
let ipv4_range = NetRange::from_cidr("10.0.0.0/8")?;
assert_eq!(ipv4_range.to_cidr(), "10.0.0.0/8");
let ipv6_range = NetRange::from_cidr("fe80::/10")?;
assert_eq!(ipv6_range.to_cidr(), "fe80::/10");
Ok(())
}
#[test]
fn test_basic_provider() -> Result<()> {
let provider = BasicProvider::new()
.with_config("endpoint", "https://api.example.com")
.with_config("timeout", "30");
let json = provider.to_json()?;
let restored = BasicProvider::from_json(&json)?;
assert_eq!(provider.config, restored.config);
assert_eq!(provider.provider_type(), "basic");
Ok(())
}
#[test]
fn test_basic_assigner() -> Result<()> {
let assigner = BasicAssigner::new("round_robin")
.with_config("pool_size", "100");
let json = assigner.to_json()?;
let restored = BasicAssigner::from_json(&json)?;
assert_eq!(assigner.strategy, restored.strategy);
assert_eq!(assigner.config, restored.config);
assert_eq!(assigner.assigner_type(), "basic");
Ok(())
}
#[test]
fn test_create_and_get_network() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
let netrange = NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?;
let provider = BasicProvider::new().with_config("type", "test");
let assigner = Some(BasicAssigner::new("sequential"));
store.create("test_network", netrange.clone(), provider, assigner)?;
let network = store.get("test_network")?.unwrap();
assert_eq!(network.name, "test_network");
assert_eq!(network.netrange, netrange);
assert_eq!(network.provider_type, "basic");
assert!(network.assigner_type.is_some());
Ok(())
}
#[test]
fn test_create_network_without_assigner() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
let netrange = NetRange::ipv6("2001:db8::".parse().unwrap(), 64)?;
let provider = BasicProvider::new();
let assigner: Option<BasicAssigner> = None;
store.create("ipv6_network", netrange.clone(), provider, assigner)?;
let network = store.get("ipv6_network")?.unwrap();
assert_eq!(network.name, "ipv6_network");
assert_eq!(network.netrange, netrange);
assert!(network.assigner_type.is_none());
assert!(network.assigner_config.is_none());
Ok(())
}
#[test]
fn test_create_duplicate_network() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
let netrange = NetRange::ipv4("10.0.0.0".parse().unwrap(), 8)?;
let provider = BasicProvider::new();
let assigner: Option<BasicAssigner> = None;
// First creation should succeed
store.create("duplicate_test", netrange.clone(), provider.clone(), assigner.clone())?;
// Second creation should fail
let result = store.create("duplicate_test", netrange, provider, assigner);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_update_network() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
// Create initial network
let netrange = NetRange::ipv4("172.16.0.0".parse().unwrap(), 12)?;
let provider = BasicProvider::new().with_config("version", "1");
let assigner: Option<BasicAssigner> = None;
store.create("update_test", netrange, provider, assigner)?;
// Update the network
let new_netrange = NetRange::ipv4("172.16.0.0".parse().unwrap(), 16)?;
let new_provider = BasicProvider::new().with_config("version", "2");
let new_assigner = Some(BasicAssigner::new("random"));
store.update("update_test", new_netrange.clone(), new_provider, new_assigner)?;
// Verify the update
let network = store.get("update_test")?.unwrap();
assert_eq!(network.netrange, new_netrange);
assert!(network.assigner_type.is_some());
Ok(())
}
#[test]
fn test_delete_network() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
let netrange = NetRange::ipv4("203.0.113.0".parse().unwrap(), 24)?;
let provider = BasicProvider::new();
let assigner: Option<BasicAssigner> = None;
store.create("delete_test", netrange, provider, assigner)?;
assert!(store.exists("delete_test")?);
let deleted = store.delete("delete_test")?;
assert!(deleted);
assert!(!store.exists("delete_test")?);
// Try to delete non-existent network
let deleted_again = store.delete("delete_test")?;
assert!(!deleted_again);
Ok(())
}
#[test]
fn test_list_networks() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
// Create multiple networks
let networks = vec![
("net1", "10.1.0.0/16"),
("net2", "10.2.0.0/16"),
("net3", "2001:db8:1::/48"),
];
for (name, cidr) in &networks {
let netrange = NetRange::from_cidr(cidr)?;
let provider = BasicProvider::new();
let assigner: Option<BasicAssigner> = None;
store.create(name, netrange, provider, assigner)?;
}
let mut network_names = store.list()?;
network_names.sort();
let mut expected: Vec<String> = networks.iter().map(|(name, _)| name.to_string()).collect();
expected.sort();
assert_eq!(network_names, expected);
Ok(())
}
#[test]
fn test_update_nonexistent_network() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
let netrange = NetRange::ipv4("198.51.100.0".parse().unwrap(), 24)?;
let provider = BasicProvider::new();
let assigner: Option<BasicAssigner> = None;
let result = store.update("nonexistent", netrange, provider, assigner);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_invalid_cidr() {
let result = NetRange::from_cidr("invalid");
assert!(result.is_err());
let result = NetRange::from_cidr("192.168.1.0");
assert!(result.is_err());
let result = NetRange::from_cidr("192.168.1.0/33");
assert!(result.is_err());
}
#[test]
fn test_range_assigner() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
// Create a network with RangeAssigner
let netrange = NetRange::from_cidr("192.168.1.0/24")?;
let provider = BasicProvider::new()
.with_config("type", "test");
let assigner = RangeAssigner::new()
.with_config("strategy", "sequential");
store.create("test-network", netrange, provider, Some(assigner))?;
// Verify the network was created
let network = store.get("test-network")?.unwrap();
assert_eq!(network.name, "test-network");
assert_eq!(network.assigner_type, Some("range".to_string()));
// Test range assignment functionality
if let Some(ref range_store) = store.ranges {
let range_assigner = RangeAssigner::new();
let range_name = "test-network"; // Uses network name as range name
// Assign some IPs
let bit1 = range_assigner.assign_ip(range_name, range_store, "device1")?;
let bit2 = range_assigner.assign_ip(range_name, range_store, "device2")?;
// Verify assignments
assert_eq!(bit1, 0);
assert_eq!(bit2, 1);
// Get assignment info
let assignment1 = range_assigner.get_assignment_by_bit(range_name, range_store, bit1)?.unwrap();
let assignment2 = range_assigner.get_assignment_by_bit(range_name, range_store, bit2)?.unwrap();
assert_eq!(assignment1, "device1");
assert_eq!(assignment2, "device2");
// Test unassignment
let unassigned = range_assigner.unassign_ip(range_name, range_store, bit1)?;
assert!(unassigned);
let assignment1_after = range_assigner.get_assignment_by_bit(range_name, range_store, bit1)?;
assert!(assignment1_after.is_none());
}
Ok(())
}
#[test]
fn test_range_assigner_serialization() -> Result<()> {
let assigner = RangeAssigner::with_range_name("test-range")
.with_config("strategy", "random")
.with_config("pool_size", "100");
// Test serialization
let json = assigner.to_json()?;
assert!(json.contains("test-range"));
assert!(json.contains("random"));
assert!(json.contains("100"));
// Test deserialization
let deserialized: RangeAssigner = RangeAssigner::from_json(&json)?;
assert_eq!(deserialized.range_name, Some("test-range".to_string()));
assert_eq!(deserialized.config.get("strategy"), Some(&"random".to_string()));
assert_eq!(deserialized.config.get("pool_size"), Some(&"100".to_string()));
Ok(())
}
#[test]
fn test_network_get_assignment() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
// Create a network with RangeAssigner
let netrange = NetRange::from_cidr("192.168.1.0/24")?;
let provider = BasicProvider::new()
.with_config("type", "test");
let assigner = RangeAssigner::new()
.with_config("strategy", "sequential");
store.create("test-network", netrange, provider, Some(assigner))?;
// Get the network
let network = store.get("test-network")?.unwrap();
// Test that no assignment exists initially
let ip = network.get_assignment("device1", store.ranges.as_ref())?;
assert!(ip.is_none());
// Assign an IP using the range assigner directly
if let Some(ref range_store) = store.ranges {
let range_assigner = RangeAssigner::new();
let range_name = "test-network"; // Uses network name as range name
// Assign an IP to device1
let bit_position = range_assigner.assign_ip(range_name, range_store, "device1")?;
assert_eq!(bit_position, 0);
// Now test get_assignment on the network
let ip = network.get_assignment("device1", store.ranges.as_ref())?;
assert!(ip.is_some());
let assigned_ip = ip.unwrap();
match assigned_ip {
std::net::IpAddr::V4(ipv4) => {
// Should be 192.168.1.0 + bit_position
let expected = std::net::Ipv4Addr::new(192, 168, 1, bit_position as u8);
assert_eq!(ipv4, expected);
}
_ => panic!("Expected IPv4 address"),
}
// Test assignment for non-existent device
let no_ip = network.get_assignment("nonexistent", store.ranges.as_ref())?;
assert!(no_ip.is_none());
// Assign another device
let bit_position2 = range_assigner.assign_ip(range_name, range_store, "device2")?;
assert_eq!(bit_position2, 1);
let ip2 = network.get_assignment("device2", store.ranges.as_ref())?;
assert!(ip2.is_some());
let assigned_ip2 = ip2.unwrap();
match assigned_ip2 {
std::net::IpAddr::V4(ipv4) => {
let expected = std::net::Ipv4Addr::new(192, 168, 1, bit_position2 as u8);
assert_eq!(ipv4, expected);
}
_ => panic!("Expected IPv4 address"),
}
}
Ok(())
}
#[test]
fn test_network_get_assignment_no_assigner() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
// Create a network without an assigner
let netrange = NetRange::from_cidr("192.168.1.0/24")?;
let provider = BasicProvider::new()
.with_config("type", "test");
store.create("no-assigner-network", netrange, provider, None::<RangeAssigner>)?;
// Get the network
let network = store.get("no-assigner-network")?.unwrap();
// Test that get_assignment returns None when no assigner is configured
let ip = network.get_assignment("device1", store.ranges.as_ref())?;
assert!(ip.is_none());
Ok(())
}
#[test]
fn test_delete_network_with_range_assigner() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
// Create a network with a range assigner
let netrange = NetRange::from_cidr("192.168.100.0/24")?;
let provider = BasicProvider::new()
.with_config("type", "test");
let assigner = RangeAssigner::new()
.with_config("strategy", "sequential");
store.create("test-range-network", netrange, provider, Some(assigner))?;
// Verify the network was created
assert!(store.exists("test-range-network")?);
let network = store.get("test-range-network")?.unwrap();
// Verify the range was initialized by checking if the range exists
if let Some(ranges) = &store.ranges {
let range_name = "test-range-network";
// Check if range exists by trying to get range info
let range_exists = ranges.names.get(range_name.as_bytes())?.is_some();
assert!(range_exists, "Range should be initialized after network creation");
// Make an assignment using the range assigner directly
let range_assigner = RangeAssigner::new();
let _bit_position = range_assigner.assign_ip(range_name, ranges, "device1")?;
// Assignment should succeed (bit_position is always >= 0 since it's u64)
// Now verify the assignment exists
let ip = network.get_assignment("device1", store.ranges.as_ref())?;
assert!(ip.is_some(), "Assignment should be retrievable");
// Verify assignment data exists
let range_info = ranges.list_range(range_name)?;
assert!(!range_info.is_empty(), "Range should have assignment data");
}
// Delete the network
let deleted = store.delete("test-range-network")?;
assert!(deleted);
// Verify the network no longer exists
assert!(!store.exists("test-range-network")?);
assert!(store.get("test-range-network")?.is_none());
// Verify range data was cleaned up
if let Some(ranges) = &store.ranges {
let range_name = "test-range-network";
let range_exists = ranges.names.get(range_name.as_bytes())?.is_some();
assert!(!range_exists, "Range should be deleted");
let range_info = ranges.list_range(range_name)?;
assert!(range_info.is_empty(), "Range assignments should be cleaned up");
}
Ok(())
}
#[test]
fn test_delete_network_without_assigner() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
// Create a network without an assigner
let netrange = NetRange::from_cidr("10.10.0.0/16")?;
let provider = BasicProvider::new()
.with_config("type", "test");
store.create("test-no-assigner", netrange, provider, None::<RangeAssigner>)?;
// Verify the network was created
assert!(store.exists("test-no-assigner")?);
let network = store.get("test-no-assigner")?.unwrap();
assert!(network.assigner_type.is_none());
// Delete the network
let deleted = store.delete("test-no-assigner")?;
assert!(deleted);
// Verify the network no longer exists
assert!(!store.exists("test-no-assigner")?);
assert!(store.get("test-no-assigner")?.is_none());
Ok(())
}
#[test]
fn test_network_get_assignment_ipv6() -> Result<()> {
let (store, _temp_dir) = create_test_store()?;
// Create an IPv6 network with RangeAssigner
let netrange = NetRange::from_cidr("2001:db8::/64")?;
let provider = BasicProvider::new()
.with_config("type", "test");
let assigner = RangeAssigner::new()
.with_config("strategy", "sequential");
store.create("ipv6-network", netrange, provider, Some(assigner))?;
// Get the network
let network = store.get("ipv6-network")?.unwrap();
// Assign an IP using the range assigner directly
if let Some(ref range_store) = store.ranges {
let range_assigner = RangeAssigner::new();
let range_name = "ipv6-network";
// Assign an IP to device1
let bit_position = range_assigner.assign_ip(range_name, range_store, "ipv6-device1")?;
assert_eq!(bit_position, 0);
// Test get_assignment on the network
let ip = network.get_assignment("ipv6-device1", store.ranges.as_ref())?;
assert!(ip.is_some());
let assigned_ip = ip.unwrap();
match assigned_ip {
std::net::IpAddr::V6(ipv6) => {
// Should be 2001:db8:: + bit_position
let expected = std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, bit_position as u16);
assert_eq!(ipv6, expected);
}
_ => panic!("Expected IPv6 address"),
}
}
Ok(())
}
}
\ No newline at end of file
diff --git a/crates/store/tests/jail_range_assignment_test.rs b/crates/store/tests/jail_range_assignment_test.rs
new file mode 100644
index 0000000..a88b5f3
--- /dev/null
+++ b/crates/store/tests/jail_range_assignment_test.rs
@@ -0,0 +1,330 @@
+use store::{
+ InstanceStore, Instance, InstanceConfig, NetworkInterface,
+ NamespaceStore, NetworkStore, RangeStore,
+ JailProvider, Provider,
+ network::{NetRange, BasicProvider, RangeAssigner},
+};
+use std::collections::HashMap;
+use tempfile::TempDir;
+
+fn create_test_stores() -> store::Result<(TempDir, sled::Db, InstanceStore)> {
+ let temp_dir = TempDir::new().unwrap();
+ let db = sled::open(temp_dir.path().join("test.db"))?;
+
+ let namespaces = NamespaceStore::open(&db)?;
+ let ranges = RangeStore::open(&db)?;
+ let networks = NetworkStore::open_with_ranges(&db, ranges)?;
+ let instances = InstanceStore::open(&db, namespaces, networks)?;
+
+ Ok((temp_dir, db, instances))
+}
+
+#[test]
+fn test_jail_with_range_assigner_should_get_automatic_ip_assignment() -> store::Result<()> {
+ let (_temp_dir, _db, instances) = create_test_stores()?;
+
+ // Create a network with RangeAssigner
+ let provider = BasicProvider::new()
+ .with_config("type", "bridge")
+ .with_config("driver", "epair");
+
+ let assigner = RangeAssigner::with_range_name("jail_network_ips")
+ .with_config("strategy", "sequential")
+ .with_config("reserve_gateway", "true");
+
+ let netrange = NetRange::from_cidr("10.0.10.0/24")?;
+
+ instances.networks().create(
+ "jail_network",
+ netrange,
+ provider,
+ Some(assigner),
+ )?;
+
+ // Create a jail instance with network interface
+ let jail_provider = JailProvider::new()
+ .with_dataset("tank/jails".to_string());
+
+ let jail_config = InstanceConfig {
+ provider_config: jail_provider.to_json()?,
+ network_interfaces: vec![
+ NetworkInterface {
+ network_name: "jail_network".to_string(),
+ interface_name: "em0".to_string(),
+ assignment: None, // Should be automatically assigned
+ }
+ ],
+ metadata: HashMap::new(),
+ };
+
+ let jail_instance = Instance {
+ name: "test-jail".to_string(),
+ provider_type: "jail".to_string(),
+ config: jail_config,
+ created_at: 0,
+ updated_at: 0,
+ };
+
+ // Create the instance - this should automatically assign an IP
+ let created_instance = instances.create(jail_instance)?;
+
+ // Verify the instance was created with an IP assignment
+ assert_eq!(created_instance.name, "test-jail");
+ assert_eq!(created_instance.config.network_interfaces.len(), 1);
+
+ let interface = &created_instance.config.network_interfaces[0];
+ assert_eq!(interface.network_name, "jail_network");
+ assert_eq!(interface.interface_name, "em0");
+
+ // This should pass once the automatic assignment is implemented
+ assert!(interface.assignment.is_some(), "IP should be automatically assigned");
+
+ // Verify the assigned IP is valid
+ if let Some(ref assignment) = interface.assignment {
+ let ip: std::net::IpAddr = assignment.parse()
+ .expect("Assignment should be a valid IP address");
+
+ // Should be in the 10.0.10.0/24 range
+ match ip {
+ std::net::IpAddr::V4(ipv4) => {
+ let octets = ipv4.octets();
+ assert_eq!(octets[0], 10);
+ assert_eq!(octets[1], 0);
+ assert_eq!(octets[2], 10);
+ // RangeAssigner starts from bit position 0, which is the network address
+ // This is actually correct behavior - bit 0 = .0, bit 1 = .1, etc.
+ assert!(octets[3] < 255, "IP should not be broadcast address");
+ }
+ _ => panic!("Expected IPv4 address"),
+ }
+ }
+
+ // Test that we can retrieve the instance and it still has the assignment
+ let retrieved_instance = instances.get("test-jail")?;
+ assert!(retrieved_instance.config.network_interfaces[0].assignment.is_some());
+
+ Ok(())
+}
+
+#[test]
+fn test_jail_deletion_cleans_up_ip_assignments() -> store::Result<()> {
+ let (_temp_dir, _db, instances) = create_test_stores()?;
+
+ // Create a network with RangeAssigner
+ let provider = BasicProvider::new();
+ let assigner = RangeAssigner::with_range_name("deletion_test_ips");
+ let netrange = NetRange::from_cidr("10.0.30.0/24")?;
+
+ instances.networks().create("deletion_test_network", netrange, provider, Some(assigner))?;
+
+ // Create a jail instance
+ let jail_provider = JailProvider::new();
+ let jail_config = InstanceConfig {
+ provider_config: jail_provider.to_json()?,
+ network_interfaces: vec![
+ NetworkInterface {
+ network_name: "deletion_test_network".to_string(),
+ interface_name: "em0".to_string(),
+ assignment: None,
+ }
+ ],
+ metadata: HashMap::new(),
+ };
+
+ let jail_instance = Instance {
+ name: "deletion-test-jail".to_string(),
+ provider_type: "jail".to_string(),
+ config: jail_config,
+ created_at: 0,
+ updated_at: 0,
+ };
+
+ // Create the instance - should get IP assignment
+ let created = instances.create(jail_instance)?;
+ assert!(created.config.network_interfaces[0].assignment.is_some());
+ let assigned_ip = created.config.network_interfaces[0].assignment.as_ref().unwrap().clone();
+
+ // Verify the IP is assigned in the range
+ let network = instances.networks().get("deletion_test_network")?.unwrap();
+ let assignment = network.get_assignment("deletion-test-jail", instances.networks().ranges())?;
+ assert!(assignment.is_some());
+ assert_eq!(assignment.unwrap().to_string(), assigned_ip);
+
+ // Delete the instance
+ instances.delete("deletion-test-jail")?;
+
+ // Verify instance is deleted
+ assert!(instances.get("deletion-test-jail").is_err());
+
+ // Verify IP assignment is cleaned up
+ let assignment_after = network.get_assignment("deletion-test-jail", instances.networks().ranges())?;
+ assert!(assignment_after.is_none(), "IP assignment should be cleaned up after deletion");
+
+ // Create a new instance to verify the IP can be reused
+ let new_jail_config = InstanceConfig {
+ provider_config: JailProvider::new().to_json()?,
+ network_interfaces: vec![
+ NetworkInterface {
+ network_name: "deletion_test_network".to_string(),
+ interface_name: "em0".to_string(),
+ assignment: None,
+ }
+ ],
+ metadata: HashMap::new(),
+ };
+
+ let new_jail = Instance {
+ name: "new-test-jail".to_string(),
+ provider_type: "jail".to_string(),
+ config: new_jail_config,
+ created_at: 0,
+ updated_at: 0,
+ };
+
+ let new_created = instances.create(new_jail)?;
+
+ // Should get the same IP that was freed
+ assert!(new_created.config.network_interfaces[0].assignment.is_some());
+ let new_assigned_ip = new_created.config.network_interfaces[0].assignment.as_ref().unwrap();
+ assert_eq!(*new_assigned_ip, assigned_ip, "IP should be reused after cleanup");
+
+ Ok(())
+}
+
+#[test]
+fn test_multiple_jails_get_different_ip_assignments() -> store::Result<()> {
+ let (_temp_dir, _db, instances) = create_test_stores()?;
+
+ // Create a network with RangeAssigner
+ let provider = BasicProvider::new();
+ let assigner = RangeAssigner::with_range_name("multi_jail_ips");
+ let netrange = NetRange::from_cidr("192.168.100.0/24")?;
+
+ instances.networks().create("multi_jail_network", netrange, provider, Some(assigner))?;
+
+ // Create multiple jail instances
+ for i in 1..=3 {
+ let jail_provider = JailProvider::new();
+ let jail_config = InstanceConfig {
+ provider_config: jail_provider.to_json()?,
+ network_interfaces: vec![
+ NetworkInterface {
+ network_name: "multi_jail_network".to_string(),
+ interface_name: "em0".to_string(),
+ assignment: None,
+ }
+ ],
+ metadata: HashMap::new(),
+ };
+
+ let jail_instance = Instance {
+ name: format!("jail-{}", i),
+ provider_type: "jail".to_string(),
+ config: jail_config,
+ created_at: 0,
+ updated_at: 0,
+ };
+
+ instances.create(jail_instance)?;
+ }
+
+ // Verify all instances have different IP assignments
+ let mut assigned_ips = std::collections::HashSet::new();
+
+ for i in 1..=3 {
+ let instance = instances.get(&format!("jail-{}", i))?;
+ let interface = &instance.config.network_interfaces[0];
+
+ assert!(interface.assignment.is_some(), "Instance jail-{} should have IP assignment", i);
+ let ip = interface.assignment.as_ref().unwrap();
+
+ assert!(assigned_ips.insert(ip.clone()),
+ "Instance jail-{} got duplicate IP: {}", i, ip);
+ }
+
+ // Should have 3 unique IP assignments
+ assert_eq!(assigned_ips.len(), 3);
+
+ Ok(())
+}
+
+#[test]
+fn test_jail_with_multiple_network_interfaces() -> store::Result<()> {
+ let (_temp_dir, _db, instances) = create_test_stores()?;
+
+ // Create two networks with RangeAssigners
+ let mgmt_provider = BasicProvider::new();
+ let mgmt_assigner = RangeAssigner::with_range_name("mgmt_ips");
+ instances.networks().create(
+ "management",
+ NetRange::from_cidr("10.0.1.0/24")?,
+ mgmt_provider,
+ Some(mgmt_assigner),
+ )?;
+
+ let data_provider = BasicProvider::new();
+ let data_assigner = RangeAssigner::with_range_name("data_ips");
+ instances.networks().create(
+ "data",
+ NetRange::from_cidr("10.0.2.0/24")?,
+ data_provider,
+ Some(data_assigner),
+ )?;
+
+ // Create jail with multiple interfaces
+ let jail_provider = JailProvider::new();
+ let jail_config = InstanceConfig {
+ provider_config: jail_provider.to_json()?,
+ network_interfaces: vec![
+ NetworkInterface {
+ network_name: "management".to_string(),
+ interface_name: "em0".to_string(),
+ assignment: None,
+ },
+ NetworkInterface {
+ network_name: "data".to_string(),
+ interface_name: "em1".to_string(),
+ assignment: None,
+ }
+ ],
+ metadata: HashMap::new(),
+ };
+
+ let jail_instance = Instance {
+ name: "multi-interface-jail".to_string(),
+ provider_type: "jail".to_string(),
+ config: jail_config,
+ created_at: 0,
+ updated_at: 0,
+ };
+
+ let created_instance = instances.create(jail_instance)?;
+
+ // Verify both interfaces got IP assignments
+ assert_eq!(created_instance.config.network_interfaces.len(), 2);
+
+ for (i, interface) in created_instance.config.network_interfaces.iter().enumerate() {
+ assert!(interface.assignment.is_some(),
+ "Interface {} should have IP assignment", i);
+
+ let ip: std::net::IpAddr = interface.assignment.as_ref().unwrap().parse()
+ .expect("Assignment should be valid IP");
+
+ match ip {
+ std::net::IpAddr::V4(ipv4) => {
+ let octets = ipv4.octets();
+ assert_eq!(octets[0], 10);
+ assert_eq!(octets[1], 0);
+
+ if interface.network_name == "management" {
+ assert_eq!(octets[2], 1);
+ } else if interface.network_name == "data" {
+ assert_eq!(octets[2], 2);
+ }
+ }
+ _ => panic!("Expected IPv4 address"),
+ }
+ }
+
+ Ok(())
+}
\ No newline at end of file
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jun 8, 9:36 AM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
47591
Default Alt Text
(224 KB)
Attached To
rCOLLAR collar
Event Timeline
Log In to Comment