mirror of
https://github.com/CloudNebulaProject/zmgr.git
synced 2026-04-10 13:10:42 +00:00
Add zmgr: illumos zone manager with IPAM and flat-file registry
Rust CLI that creates/destroys/imports illumos zones from KDL template configs with automatic IP allocation from named pools. Registry lives under /etc/zmgr as flat KDL files — zone entries double as the IPAM ledger. Includes default templates for ipkg (OI) and nlipkg (OFL) brands, matching the existing shell scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
abdce9c927
22 changed files with 2864 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
819
Cargo.lock
generated
Normal file
819
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,819 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace-ext"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "is_ci"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kdl"
|
||||
version = "6.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e"
|
||||
dependencies = [
|
||||
"miette",
|
||||
"num",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "miette"
|
||||
version = "7.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"backtrace-ext",
|
||||
"cfg-if",
|
||||
"miette-derive",
|
||||
"owo-colors",
|
||||
"supports-color",
|
||||
"supports-hyperlinks",
|
||||
"supports-unicode",
|
||||
"terminal_size",
|
||||
"textwrap",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miette-derive"
|
||||
version = "7.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "supports-color"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
|
||||
dependencies = [
|
||||
"is_ci",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supports-hyperlinks"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91"
|
||||
|
||||
[[package]]
|
||||
name = "supports-unicode"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"unicode-linebreak",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmgr"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"ipnet",
|
||||
"kdl",
|
||||
"miette",
|
||||
"thiserror",
|
||||
]
|
||||
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "zmgr"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.44"
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
ipnet = "2.12.0"
|
||||
kdl = "6.5.0"
|
||||
miette = { version = "7.6.0", features = ["fancy"] }
|
||||
thiserror = "2.0.18"
|
||||
66
create_ipxe_vm.sh
Executable file
66
create_ipxe_vm.sh
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/bash
|
||||
set -ex
|
||||
|
||||
ZONE=$1
|
||||
IP_ADDRESS_CIDR="${2}/16"
|
||||
NICNAME="${ZONE}0"
|
||||
INTERNALSTUB="oinetint0"
|
||||
GATEWAY="10.1.0.1"
|
||||
RAM="16G"
|
||||
VCPUS="8"
|
||||
DISK="60G"
|
||||
ISO="/vm/iso/ipxe-oi-internal.iso"
|
||||
|
||||
if ! dladm show-vnic $NICNAME > /dev/null; then
|
||||
dladm create-vnic -l $INTERNALSTUB $NICNAME
|
||||
fi
|
||||
|
||||
if ! zfs get name rpool/vm/${ZONE}d0 > /dev/null;then
|
||||
zfs create -V ${DISK} rpool/vm/${ZONE}d0
|
||||
fi
|
||||
|
||||
cat <<EOF | zonecfg -z $ZONE
|
||||
create -b
|
||||
set zonepath=/zones/$ZONE
|
||||
set brand=bhyve
|
||||
set autoboot=true
|
||||
set ip-type=exclusive
|
||||
add fs
|
||||
set dir="/vm/iso"
|
||||
set special="/vm/iso"
|
||||
set type="lofs"
|
||||
add options ro
|
||||
add options nodevices
|
||||
end
|
||||
add net
|
||||
set physical="$NICNAME"
|
||||
end
|
||||
add device
|
||||
set match="/dev/zvol/rdsk/rpool/vm/${ZONE}d0"
|
||||
end
|
||||
add attr
|
||||
set name="bootdisk"
|
||||
set type="string"
|
||||
set value="rpool/vm/${ZONE}d0"
|
||||
end
|
||||
add attr
|
||||
set name="ram"
|
||||
set type="string"
|
||||
set value="${RAM}"
|
||||
end
|
||||
add attr
|
||||
set name="vcpus"
|
||||
set type="string"
|
||||
set value="${VCPUS}"
|
||||
end
|
||||
add attr
|
||||
set name="cdrom"
|
||||
set type="string"
|
||||
set value="${ISO}"
|
||||
end
|
||||
EOF
|
||||
|
||||
zoneadm -z $ZONE install
|
||||
zoneadm -z $ZONE boot
|
||||
|
||||
zlogin -C $ZONE
|
||||
29
create_ofl_zone.sh
Executable file
29
create_ofl_zone.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/bash
|
||||
set -ex
|
||||
|
||||
ZONE=$1
|
||||
ADDRESS="${2}/24"
|
||||
NICNAME="${ZONE}0"
|
||||
INTERNALSTUB="oflint0"
|
||||
GATEWAY="192.168.10.1"
|
||||
|
||||
if ! dladm show-vnic $NICNAME > /dev/null; then
|
||||
dladm create-vnic -l $INTERNALSTUB $NICNAME
|
||||
fi
|
||||
|
||||
cat <<EOF | zonecfg -z $ZONE
|
||||
create -b
|
||||
set zonepath=/zones/$ZONE
|
||||
set brand=nlipkg
|
||||
set autoboot=true
|
||||
set ip-type=exclusive
|
||||
add net
|
||||
set physical=$NICNAME
|
||||
set allowed-address=$ADDRESS
|
||||
set defrouter=$GATEWAY
|
||||
end
|
||||
verify
|
||||
commit
|
||||
EOF
|
||||
|
||||
zoneadm -z $ZONE install
|
||||
121
create_oi_vm.sh
Executable file
121
create_oi_vm.sh
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/bash
|
||||
set -ex
|
||||
|
||||
ZONE=$1
|
||||
IP_ADDRESS_CIDR="${2}/16"
|
||||
NICNAME="${ZONE}0"
|
||||
INTERNALSTUB="oinetint0"
|
||||
GATEWAY="10.1.0.1"
|
||||
RAM="16G"
|
||||
VCPUS="8"
|
||||
DISK="60G"
|
||||
CIDATA_DIR="/vm/iso/${ZONE}-cidata"
|
||||
ISO="${CIDATA_DIR}.iso"
|
||||
|
||||
if ! dladm show-vnic $NICNAME > /dev/null; then
|
||||
dladm create-vnic -l $INTERNALSTUB $NICNAME
|
||||
fi
|
||||
|
||||
if ! zfs get name rpool/vm/${ZONE}d0 > /dev/null;then
|
||||
zfs create -V ${DISK} rpool/vm/${ZONE}d0
|
||||
fi
|
||||
|
||||
dd if=/zones/oinetentry/root/var/www/dlc/hipster/cloudimage-ttya-openindiana-hipster.raw of=/dev/zvol/rdsk/rpool/vm/${ZONE}d0 bs=1M status=progress
|
||||
|
||||
cat <<EOF | zonecfg -z $ZONE
|
||||
create -b
|
||||
set zonepath=/zones/$ZONE
|
||||
set brand=bhyve
|
||||
set autoboot=true
|
||||
set ip-type=exclusive
|
||||
add fs
|
||||
set dir="/vm/iso"
|
||||
set special="/vm/iso"
|
||||
set type="lofs"
|
||||
add options ro
|
||||
add options nodevices
|
||||
end
|
||||
add net
|
||||
set physical="$NICNAME"
|
||||
end
|
||||
add device
|
||||
set match="/dev/zvol/rdsk/rpool/vm/${ZONE}d0"
|
||||
end
|
||||
add attr
|
||||
set name="bootdisk"
|
||||
set type="string"
|
||||
set value="rpool/vm/${ZONE}d0"
|
||||
end
|
||||
add attr
|
||||
set name="acpi"
|
||||
set type="string"
|
||||
set value="off"
|
||||
end
|
||||
add attr
|
||||
set name="ram"
|
||||
set type="string"
|
||||
set value="${RAM}"
|
||||
end
|
||||
add attr
|
||||
set name="vcpus"
|
||||
set type="string"
|
||||
set value="${VCPUS}"
|
||||
end
|
||||
add attr
|
||||
set name="cdrom"
|
||||
set type="string"
|
||||
set value="${ISO}"
|
||||
end
|
||||
EOF
|
||||
|
||||
mkdir -p $CIDATA_DIR
|
||||
|
||||
cat<<EOF > $CIDATA_DIR/user-data
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
- name: root
|
||||
ssh_authorized_keys:
|
||||
- "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQBmEXyfBHRiQogrAEgFp8Lr1TDEKdRRcpwqHOiujzTAFqszxutMPzuFlyU0Ljh28VqeDD9cIyS6VGfKG3iASQbIZd61yE0NHPNC6IPTFTggGK+3PzKt4T+t1ZdIhdrCGcsrq8c+44kSyIutvbruB0UmRjq4XqqkCDFEZMzBF/KUcu0M7oSEawFuzbtl7xoaDY83F7+HXTxbBKuRwSJw58d8MqsI7KlW6zUKJi0bgmYJOFjjDLPVTo7lqdNYfTRAHZYaCUADfrUqBCOLHSDET1Zrs3ibr6l8oMqOvctfwvuGpNT8vs05Z0P/ETUiZ/SzTyvoRyxXG0JTKlNSRmxcYA2J toasty_rsa"
|
||||
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILcp4Wgea5V8N750xcRGil1rrdd+mCpwYBspNwwb45SN toasty_ed25519"
|
||||
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBrXXxL7WUfh5UjSGV2uANb9E4LP9jl/cEt3NpC87g6h a.wacknitz@gmx.de"
|
||||
EOF
|
||||
|
||||
ZONE_UUID=$(zoneadm -z ${ZONE} list -p | cut -d ":" -f 5)
|
||||
|
||||
cat<<EOF > $CIDATA_DIR/meta-data
|
||||
instance-id: "${ZONE_UUID}"
|
||||
hostname: "${ZONE}"
|
||||
EOF
|
||||
|
||||
MAC_ADDRESS=$(dladm show-vnic ${NICNAME} -p -o macaddress)
|
||||
|
||||
|
||||
cat<<EOF > $CIDATA_DIR/network-config
|
||||
network:
|
||||
version: 1
|
||||
config:
|
||||
- type: physical
|
||||
name: ${NICNAME}
|
||||
mac_address: '${MAC_ADDRESS}'
|
||||
subnets:
|
||||
- type: static
|
||||
address: ${IP_ADDRESS_CIDR}
|
||||
gateway: ${GATEWAY}
|
||||
dns_nameservers:
|
||||
- 9.9.9.9
|
||||
- 149.112.112.112
|
||||
- 2620:fe::fe
|
||||
- 2620:fe::9
|
||||
dns_search:
|
||||
- openindiana.org
|
||||
EOF
|
||||
|
||||
vim $CIDATA_DIR/network-config
|
||||
|
||||
(cd ${CIDATA_DIR}; mkisofs -graft-points -dlrDJN -relaxed-filenames -o ${ISO} -V cidata user-data meta-data network-config)
|
||||
|
||||
zoneadm -z $ZONE install
|
||||
zoneadm -z $ZONE boot
|
||||
|
||||
zlogin -C $ZONE
|
||||
29
create_oi_zone.sh
Executable file
29
create_oi_zone.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/bash
|
||||
set -ex
|
||||
|
||||
ZONE=$1
|
||||
ADDRESS="${2}/24"
|
||||
NICNAME="${ZONE}0"
|
||||
INTERNALSTUB="oinetint0"
|
||||
GATEWAY="10.1.0.1"
|
||||
|
||||
if ! dladm show-vnic $NICNAME > /dev/null; then
|
||||
dladm create-vnic -l $INTERNALSTUB $NICNAME
|
||||
fi
|
||||
|
||||
cat <<EOF | zonecfg -z $ZONE
|
||||
create -b
|
||||
set zonepath=/zones/$ZONE
|
||||
set brand=ipkg
|
||||
set autoboot=false
|
||||
set ip-type=exclusive
|
||||
add net
|
||||
set physical=$NICNAME
|
||||
set allowed-address=$ADDRESS
|
||||
set defrouter=$GATEWAY
|
||||
end
|
||||
verify
|
||||
commit
|
||||
EOF
|
||||
|
||||
zoneadm -z $ZONE install
|
||||
63
docs/ai/architecture.md
Normal file
63
docs/ai/architecture.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
zmgr is a single Rust binary that manages illumos zones via flat KDL config files under `/etc/zmgr`. No daemon, no database — just a CLI that reads configs, calls system commands (`zonecfg`, `zoneadm`, `dladm`), and writes registry entries.
|
||||
|
||||
## Module Layout
|
||||
|
||||
```
|
||||
src/
|
||||
main.rs CLI entry point (clap), command dispatch
|
||||
config.rs Global config (/etc/zmgr/config.kdl)
|
||||
template.rs Zone templates (/etc/zmgr/templates/*.kdl)
|
||||
pool.rs IPAM pools (/etc/zmgr/pools/*.kdl)
|
||||
zone.rs Zone registry (/etc/zmgr/zones/*.kdl)
|
||||
publisher.rs IPS publishers (/etc/zmgr/publishers/*.kdl)
|
||||
exec.rs System command wrappers (zonecfg, zoneadm, dladm)
|
||||
import.rs Import existing zones into registry
|
||||
kdl_util.rs KDL document parsing helpers
|
||||
error.rs Error types (miette diagnostics)
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Zone Creation (`zmgr create <name>`)
|
||||
|
||||
1. Load global config → get default template name
|
||||
2. Load template → get brand, autoboot, pool reference
|
||||
3. Load pool → get network, gateway, stub, IP range
|
||||
4. Scan zone registry → find allocated IPs
|
||||
5. Allocate next free IP from pool range
|
||||
6. `dladm create-vnic` → create VNIC on stub
|
||||
7. `zonecfg -z <name>` → pipe zone configuration
|
||||
8. `zoneadm -z <name> install` → install zone
|
||||
9. Write zone registry entry (KDL file)
|
||||
10. If autoboot, `zoneadm boot`
|
||||
|
||||
### Zone Import (`zmgr import`)
|
||||
|
||||
1. `zoneadm list -cp` → get system zones
|
||||
2. Filter out already-registered and bhyve zones
|
||||
3. `zonecfg -z <name> info` → extract config
|
||||
4. Match brand → template, IP → pool
|
||||
5. Write zone registry entry
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
main.rs
|
||||
├── config.rs ← kdl_util.rs, error.rs
|
||||
├── template.rs ← kdl_util.rs, error.rs
|
||||
├── pool.rs ← kdl_util.rs, error.rs, zone.rs
|
||||
├── zone.rs ← kdl_util.rs, error.rs
|
||||
├── publisher.rs ← kdl_util.rs, error.rs
|
||||
├── exec.rs ← error.rs
|
||||
└── import.rs ← exec.rs, pool.rs, template.rs, zone.rs
|
||||
```
|
||||
|
||||
## Boundary with vm-manager
|
||||
|
||||
zmgr manages **zone brands** (ipkg, nlipkg, etc.) — native illumos zones.
|
||||
vm-manager manages **bhyve** brand zones — virtual machines.
|
||||
Import logic skips bhyve zones to avoid overlap.
|
||||
49
docs/ai/decisions.md
Normal file
49
docs/ai/decisions.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Design Decisions
|
||||
|
||||
## KDL over TOML/JSON
|
||||
|
||||
KDL (v2) was chosen for all config files because:
|
||||
- Node-based structure maps naturally to zone/pool/template semantics
|
||||
- More readable than TOML for nested structures
|
||||
- `kdl` crate v6.5 supports KDL v2 spec
|
||||
- Note: KDL v2 uses `#true`/`#false` for booleans, not bare `true`/`false`
|
||||
|
||||
`knuffel` (typed derive) was evaluated but targets KDL v1, incompatible with `kdl` v6. Manual extraction via `kdl_util.rs` helpers is sufficient for our simple schemas.
|
||||
|
||||
## Flat Files, No Database
|
||||
|
||||
All state lives in `/etc/zmgr/` as individual KDL files:
|
||||
- One file per zone, pool, template, publisher
|
||||
- Human-editable with any text editor
|
||||
- Works on minimal illumos installs (no SQLite, no sled)
|
||||
- Easy to back up, version control, or inspect
|
||||
|
||||
## IPAM: Zone Files Are the Ledger
|
||||
|
||||
No separate allocation table. To find allocated IPs:
|
||||
1. Scan `/etc/zmgr/zones/*.kdl`
|
||||
2. Parse each zone's `address` field
|
||||
3. Check which IPs fall within a pool's network
|
||||
|
||||
This avoids consistency issues between a ledger and registry.
|
||||
|
||||
## Global Pools, Template References
|
||||
|
||||
Pools are defined independently (not inside templates) so multiple templates can share a pool. Templates reference pools by name. This matches the user's preference for a "global pool config" pattern.
|
||||
|
||||
## bhyve Exclusion
|
||||
|
||||
Import logic explicitly skips `brand=bhyve` zones. These are vm-manager's responsibility. This boundary is enforced at the import level, not at the config level.
|
||||
|
||||
## Error Handling: miette Diagnostics
|
||||
|
||||
Every error variant includes:
|
||||
- A descriptive message
|
||||
- A diagnostic code (e.g., `zmgr::pool::exhausted`)
|
||||
- A help message telling the user what to do next
|
||||
|
||||
This follows the miette diagnostic pattern per project conventions.
|
||||
|
||||
## No Daemon, No State Machine
|
||||
|
||||
zmgr is a one-shot CLI. Each invocation reads config, acts, writes results. No background process, no lock files, no PID management. Zone lifecycle is delegated to the OS (`zoneadm`).
|
||||
40
docs/ai/gap-analysis.md
Normal file
40
docs/ai/gap-analysis.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Gap Analysis
|
||||
|
||||
## Current Gaps
|
||||
|
||||
### Publishers Not Applied During Install
|
||||
Publishers are stored and listed but not yet passed to `zonecfg`/`zoneadm` during zone creation. Future: configure IPS publishers inside the zone after install via `zlogin` or `sysding`.
|
||||
|
||||
### No Zone Boot/Halt Commands
|
||||
`zmgr` doesn't expose boot/halt/reboot as subcommands. Users must use `zoneadm -z <name> boot` directly. Could add `zmgr boot <name>` / `zmgr halt <name>` as thin wrappers.
|
||||
|
||||
### No Template Create/Edit via CLI
|
||||
Templates must be edited as KDL files directly. Could add `zmgr template create` / `zmgr template edit` commands.
|
||||
|
||||
### No Pool Create/Edit via CLI
|
||||
Same as templates — pools are managed by editing files. Could add CLI commands.
|
||||
|
||||
### No ZFS Dataset Management
|
||||
The original VM scripts create ZFS volumes (`zfs create -V`). zmgr doesn't manage ZFS datasets. For zone brands, `zoneadm install` handles the zonepath ZFS dataset automatically.
|
||||
|
||||
### No Cloud-Init / Sysding Integration
|
||||
The VM scripts generate cloud-init configs (user-data, meta-data, network-config). Zones don't use cloud-init but could benefit from sysding config generation for first-boot setup (hostname, SSH keys, networking).
|
||||
|
||||
### No Dry-Run Mode
|
||||
`zmgr create --dry-run` could show what would happen without executing system commands. Useful for validation.
|
||||
|
||||
### No VNIC Naming Customization
|
||||
VNICs are always `<zonename>0`. Could support custom VNIC naming patterns.
|
||||
|
||||
### Import Matching is Best-Effort
|
||||
Import matches zones to templates by brand and IPs to pools by network containment. Zones with unusual configs may get poor matches. Manual editing of the resulting KDL files may be needed.
|
||||
|
||||
### No IPv6 Support
|
||||
IPAM only handles IPv4 pools. Could extend to dual-stack.
|
||||
|
||||
## Future Considerations
|
||||
|
||||
- **Zone ordering**: Dependencies between zones (e.g., start DNS zone before app zones)
|
||||
- **Snapshots**: ZFS snapshot management for zone rollback
|
||||
- **Migration**: Move zones between hosts
|
||||
- **Monitoring**: Health checks, resource usage
|
||||
37
docs/ai/implementation-state.md
Normal file
37
docs/ai/implementation-state.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Implementation State
|
||||
|
||||
## Completed
|
||||
|
||||
- [x] Project scaffold (Cargo.toml, module structure)
|
||||
- [x] KDL parsing helpers (`kdl_util.rs`)
|
||||
- [x] Error types with miette diagnostics (`error.rs`)
|
||||
- [x] Global config loading (`config.rs`)
|
||||
- [x] Template loading + defaults (`template.rs`)
|
||||
- [x] IPAM pool loading, allocation, defaults (`pool.rs`)
|
||||
- [x] Zone registry CRUD (`zone.rs`)
|
||||
- [x] Publisher management (`publisher.rs`)
|
||||
- [x] Exec layer for system commands (`exec.rs`)
|
||||
- [x] Import logic from existing zones (`import.rs`)
|
||||
- [x] CLI commands: init, create, destroy, list, status, import
|
||||
- [x] CLI commands: template list/show, pool list/show
|
||||
- [x] CLI commands: publisher list/add/remove
|
||||
- [x] Clean build (zero warnings)
|
||||
- [x] Tested: init, template list, pool list/show, publisher list, list
|
||||
|
||||
## Not Yet Tested on illumos
|
||||
|
||||
- [ ] `zmgr create` (requires illumos with zonecfg/zoneadm/dladm)
|
||||
- [ ] `zmgr destroy`
|
||||
- [ ] `zmgr import`
|
||||
- [ ] `zmgr status` (requires zoneadm)
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Crate | Version | Purpose |
|
||||
|---|---|---|
|
||||
| clap | 4.6.0 | CLI parsing (derive) |
|
||||
| kdl | 6.5.0 | KDL v2 document parsing |
|
||||
| miette | 7.6.0 | Diagnostic error reporting |
|
||||
| thiserror | 2.0.18 | Error type derives |
|
||||
| ipnet | 2.12.0 | IP network arithmetic |
|
||||
| chrono | 0.4.44 | Date formatting |
|
||||
78
docs/ai/registry-format.md
Normal file
78
docs/ai/registry-format.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Registry Format
|
||||
|
||||
All files use KDL v2 syntax. The registry root defaults to `/etc/zmgr/` but can be overridden with `--registry`.
|
||||
|
||||
## config.kdl
|
||||
|
||||
```kdl
|
||||
zonepath-prefix "/zones"
|
||||
default-template "oi"
|
||||
```
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `zonepath-prefix` | string | `/zones` | Parent directory for zone roots |
|
||||
| `default-template` | string | `oi` | Template used when `--template` is omitted |
|
||||
|
||||
## templates/*.kdl
|
||||
|
||||
```kdl
|
||||
template "oi" {
|
||||
brand "ipkg"
|
||||
autoboot #false
|
||||
ip-type "exclusive"
|
||||
pool "internal"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `brand` | string | yes | Zone brand (ipkg, nlipkg, etc.) |
|
||||
| `autoboot` | bool | no | Boot zone after install (default: `#false`) |
|
||||
| `ip-type` | string | no | IP type (default: `exclusive`) |
|
||||
| `pool` | string | yes | IPAM pool name to allocate from |
|
||||
|
||||
## pools/*.kdl
|
||||
|
||||
```kdl
|
||||
pool "internal" {
|
||||
network "10.1.0.0/24"
|
||||
gateway "10.1.0.1"
|
||||
stub "oinetint0"
|
||||
range-start "10.1.0.10"
|
||||
range-end "10.1.0.250"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `network` | CIDR | yes | Network in CIDR notation |
|
||||
| `gateway` | IPv4 | yes | Default router for zones in this pool |
|
||||
| `stub` | string | yes | Etherstub/VNIC parent for zone VNICs |
|
||||
| `range-start` | IPv4 | yes | First allocatable address |
|
||||
| `range-end` | IPv4 | yes | Last allocatable address |
|
||||
|
||||
## zones/*.kdl
|
||||
|
||||
```kdl
|
||||
zone "myzone" {
|
||||
template "oi"
|
||||
address "10.1.0.10/24"
|
||||
gateway "10.1.0.1"
|
||||
vnic "myzone0"
|
||||
stub "oinetint0"
|
||||
created "2026-03-22"
|
||||
}
|
||||
```
|
||||
|
||||
These files are created by `zmgr create` and `zmgr import`. They serve as the IPAM allocation ledger — scanning all zone files determines which IPs are in use.
|
||||
|
||||
## publishers/*.kdl
|
||||
|
||||
```kdl
|
||||
publisher "openindiana.org" {
|
||||
origin "https://pkg.openindiana.org/hipster"
|
||||
}
|
||||
```
|
||||
|
||||
IPS publisher configurations. Currently informational — future work could apply these during zone install.
|
||||
46
src/config.rs
Normal file
46
src/config.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use std::path::Path;
|
||||
|
||||
use crate::error::{Result, ZmgrError};
|
||||
use crate::kdl_util;
|
||||
|
||||
/// Global zmgr configuration.
|
||||
pub struct Config {
|
||||
pub zonepath_prefix: String,
|
||||
pub default_template: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(registry: &Path) -> Result<Self> {
|
||||
let path = registry.join("config.kdl");
|
||||
let doc = kdl_util::read_document(&path)?;
|
||||
|
||||
let zonepath_prefix = kdl_util::get_string(&doc, "zonepath-prefix")
|
||||
.unwrap_or_else(|| "/zones".to_string());
|
||||
let default_template = kdl_util::get_string(&doc, "default-template")
|
||||
.unwrap_or_else(|| "oi".to_string());
|
||||
|
||||
Ok(Config {
|
||||
zonepath_prefix,
|
||||
default_template,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_default(registry: &Path) -> Result<()> {
|
||||
let content = r#"zonepath-prefix "/zones"
|
||||
default-template "oi"
|
||||
"#;
|
||||
let path = registry.join("config.kdl");
|
||||
std::fs::write(&path, content).map_err(ZmgrError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_initialized(registry: &Path) -> Result<()> {
|
||||
if !registry.join("config.kdl").exists() {
|
||||
return Err(ZmgrError::NotInitialized {
|
||||
path: registry.display().to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
90
src/error.rs
Normal file
90
src/error.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[allow(unused_assignments, dead_code)]
|
||||
pub enum ZmgrError {
|
||||
#[error("Registry not initialized at {path}")]
|
||||
#[diagnostic(
|
||||
code(zmgr::not_initialized),
|
||||
help("Run `zmgr init` to create the registry structure at {path}")
|
||||
)]
|
||||
NotInitialized { path: String },
|
||||
|
||||
#[error("Registry already exists at {path}")]
|
||||
#[diagnostic(
|
||||
code(zmgr::already_initialized),
|
||||
help("The registry is already set up. Use `zmgr init --force` to reinitialize.")
|
||||
)]
|
||||
AlreadyInitialized { path: String },
|
||||
|
||||
#[error("Zone '{name}' already exists in registry")]
|
||||
#[diagnostic(
|
||||
code(zmgr::zone::exists),
|
||||
help("Use `zmgr destroy {name}` first, or choose a different name.")
|
||||
)]
|
||||
ZoneExists { name: String },
|
||||
|
||||
#[error("Zone '{name}' not found in registry")]
|
||||
#[diagnostic(
|
||||
code(zmgr::zone::not_found),
|
||||
help("Run `zmgr list` to see managed zones, or `zmgr import` to import existing zones.")
|
||||
)]
|
||||
ZoneNotFound { name: String },
|
||||
|
||||
#[error("Template '{name}' not found")]
|
||||
#[diagnostic(
|
||||
code(zmgr::template::not_found),
|
||||
help("Run `zmgr template list` to see available templates.")
|
||||
)]
|
||||
TemplateNotFound { name: String },
|
||||
|
||||
#[error("Pool '{name}' not found")]
|
||||
#[diagnostic(
|
||||
code(zmgr::pool::not_found),
|
||||
help("Run `zmgr pool list` to see available pools.")
|
||||
)]
|
||||
PoolNotFound { name: String },
|
||||
|
||||
#[error("No free addresses in pool '{pool}'")]
|
||||
#[diagnostic(
|
||||
code(zmgr::ipam::exhausted),
|
||||
help("All addresses in pool '{pool}' ({range_start} - {range_end}) are allocated. \
|
||||
Destroy unused zones or expand the pool range.")
|
||||
)]
|
||||
PoolExhausted {
|
||||
pool: String,
|
||||
range_start: String,
|
||||
range_end: String,
|
||||
},
|
||||
|
||||
#[error("Failed to parse KDL file: {path}")]
|
||||
#[diagnostic(code(zmgr::kdl::parse))]
|
||||
KdlParse {
|
||||
path: String,
|
||||
#[source]
|
||||
source: kdl::KdlError,
|
||||
},
|
||||
|
||||
#[error("Missing required field '{field}' in {context}")]
|
||||
#[diagnostic(
|
||||
code(zmgr::kdl::missing_field),
|
||||
help("Add `{field} \"<value>\"` to {context}")
|
||||
)]
|
||||
MissingField { field: String, context: String },
|
||||
|
||||
#[error("Command failed: {command}")]
|
||||
#[diagnostic(code(zmgr::exec::failed))]
|
||||
CommandFailed {
|
||||
command: String,
|
||||
stderr: String,
|
||||
#[help]
|
||||
help_msg: String,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(zmgr::io))]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = miette::Result<T>;
|
||||
158
src/exec.rs
Normal file
158
src/exec.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
use std::process::Command;
|
||||
|
||||
use crate::error::{Result, ZmgrError};
|
||||
|
||||
/// Run a command and return stdout, or a diagnostic error.
|
||||
fn run(cmd: &str, args: &[&str]) -> Result<String> {
|
||||
let output = Command::new(cmd)
|
||||
.args(args)
|
||||
.output()
|
||||
.map_err(ZmgrError::Io)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
Err(ZmgrError::CommandFailed {
|
||||
command: format!("{cmd} {}", args.join(" ")),
|
||||
stderr: stderr.clone(),
|
||||
help_msg: format!("Command exited with {}. stderr: {stderr}", output.status),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Pipe input to a command's stdin.
|
||||
fn run_with_stdin(cmd: &str, args: &[&str], input: &str) -> Result<String> {
|
||||
use std::io::Write;
|
||||
use std::process::Stdio;
|
||||
|
||||
let mut child = Command::new(cmd)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(ZmgrError::Io)?;
|
||||
|
||||
if let Some(ref mut stdin) = child.stdin {
|
||||
stdin.write_all(input.as_bytes()).map_err(ZmgrError::Io)?;
|
||||
}
|
||||
|
||||
let output = child.wait_with_output().map_err(ZmgrError::Io)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
Err(ZmgrError::CommandFailed {
|
||||
command: format!("{cmd} {}", args.join(" ")),
|
||||
stderr: stderr.clone(),
|
||||
help_msg: format!("Command exited with {}. stderr: {stderr}", output.status),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
// --- dladm ---
|
||||
|
||||
pub fn dladm_create_vnic(name: &str, stub: &str) -> Result<()> {
|
||||
// Check if VNIC already exists
|
||||
if dladm_vnic_exists(name)? {
|
||||
return Ok(());
|
||||
}
|
||||
run("dladm", &["create-vnic", "-l", stub, name])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn dladm_delete_vnic(name: &str) -> Result<()> {
|
||||
if !dladm_vnic_exists(name)? {
|
||||
return Ok(());
|
||||
}
|
||||
run("dladm", &["delete-vnic", name])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dladm_vnic_exists(name: &str) -> Result<bool> {
|
||||
let result = Command::new("dladm")
|
||||
.args(["show-vnic", name])
|
||||
.output()
|
||||
.map_err(ZmgrError::Io)?;
|
||||
Ok(result.status.success())
|
||||
}
|
||||
|
||||
// --- zonecfg ---
|
||||
|
||||
/// Configure a zone using zonecfg commands piped to stdin.
|
||||
pub fn zonecfg_create(zone_name: &str, zonecfg_commands: &str) -> Result<()> {
|
||||
run_with_stdin("zonecfg", &["-z", zone_name], zonecfg_commands)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get zone configuration info.
|
||||
pub fn zonecfg_info(zone_name: &str) -> Result<String> {
|
||||
run("zonecfg", &["-z", zone_name, "info"])
|
||||
}
|
||||
|
||||
/// Delete zone configuration.
|
||||
pub fn zonecfg_delete(zone_name: &str) -> Result<()> {
|
||||
run_with_stdin("zonecfg", &["-z", zone_name], "delete -F\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- zoneadm ---
|
||||
|
||||
pub fn zoneadm_install(zone_name: &str) -> Result<()> {
|
||||
run("zoneadm", &["-z", zone_name, "install"])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn zoneadm_uninstall(zone_name: &str) -> Result<()> {
|
||||
run("zoneadm", &["-z", zone_name, "uninstall", "-F"])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn zoneadm_boot(zone_name: &str) -> Result<()> {
|
||||
run("zoneadm", &["-z", zone_name, "boot"])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn zoneadm_halt(zone_name: &str) -> Result<()> {
|
||||
run("zoneadm", &["-z", zone_name, "halt"])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parsed entry from `zoneadm list -cp`.
|
||||
#[allow(dead_code)]
|
||||
pub struct ZoneadmEntry {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub state: String,
|
||||
pub path: String,
|
||||
pub uuid: String,
|
||||
pub brand: String,
|
||||
pub ip_type: String,
|
||||
}
|
||||
|
||||
/// List all zones via `zoneadm list -cp`.
|
||||
pub fn zoneadm_list() -> Result<Vec<ZoneadmEntry>> {
|
||||
let output = run("zoneadm", &["list", "-cp"])?;
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for line in output.lines() {
|
||||
let fields: Vec<&str> = line.split(':').collect();
|
||||
if fields.len() >= 7 && fields[1] != "global" {
|
||||
entries.push(ZoneadmEntry {
|
||||
id: fields[0].to_string(),
|
||||
name: fields[1].to_string(),
|
||||
state: fields[2].to_string(),
|
||||
path: fields[3].to_string(),
|
||||
uuid: fields[4].to_string(),
|
||||
brand: fields[5].to_string(),
|
||||
ip_type: fields[6].to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
119
src/import.rs
Normal file
119
src/import.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
use std::path::Path;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::exec;
|
||||
use crate::pool::Pool;
|
||||
use crate::template::Template;
|
||||
use crate::zone::Zone;
|
||||
|
||||
/// Import existing zones from the system into the registry.
|
||||
///
|
||||
/// For each zone reported by `zoneadm list -cp` that is not already in the
|
||||
/// registry, we parse its `zonecfg info` output to extract configuration and
|
||||
/// create a registry entry.
|
||||
pub fn import_zones(registry: &Path, filter_name: Option<&str>) -> Result<Vec<String>> {
|
||||
let system_zones = exec::zoneadm_list()?;
|
||||
let templates = Template::list(registry)?;
|
||||
let pools = Pool::list(registry)?;
|
||||
|
||||
let mut imported = Vec::new();
|
||||
|
||||
for entry in &system_zones {
|
||||
// Skip if already in registry
|
||||
if Zone::exists(registry, &entry.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If user specified a filter, only import that zone
|
||||
if let Some(filter) = filter_name {
|
||||
if entry.name != filter {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip bhyve zones — those belong to vm-manager
|
||||
if entry.brand == "bhyve" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get zonecfg info for details
|
||||
let info = match exec::zonecfg_info(&entry.name) {
|
||||
Ok(info) => info,
|
||||
Err(e) => {
|
||||
eprintln!("warning: cannot read config for zone '{}': {e}", entry.name);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse zonecfg info output
|
||||
let address = parse_zonecfg_field(&info, "allowed-address:")
|
||||
.unwrap_or_default();
|
||||
let vnic = parse_zonecfg_field(&info, "physical:")
|
||||
.unwrap_or_else(|| format!("{}0", entry.name));
|
||||
let gateway = parse_zonecfg_field(&info, "defrouter:")
|
||||
.unwrap_or_default();
|
||||
|
||||
// Match to a template by brand
|
||||
let template = match_template(&entry.brand, &templates, registry);
|
||||
|
||||
// Match to a pool by finding which pool's network contains the address
|
||||
let stub = match_pool_stub(&address, &pools, registry);
|
||||
|
||||
let zone = Zone {
|
||||
name: entry.name.clone(),
|
||||
template: template.unwrap_or_else(|| entry.brand.clone()),
|
||||
address,
|
||||
gateway,
|
||||
vnic,
|
||||
stub: stub.unwrap_or_default(),
|
||||
created: String::new(), // unknown for imported zones
|
||||
};
|
||||
|
||||
zone.write(registry)?;
|
||||
imported.push(entry.name.clone());
|
||||
}
|
||||
|
||||
Ok(imported)
|
||||
}
|
||||
|
||||
/// Parse a field from `zonecfg info` output.
|
||||
/// Lines look like: `\tset allowed-address=10.1.0.5/24` or `\tallowed-address: 10.1.0.5/24`
|
||||
fn parse_zonecfg_field(info: &str, field: &str) -> Option<String> {
|
||||
for line in info.lines() {
|
||||
let trimmed = line.trim();
|
||||
if let Some(rest) = trimmed.strip_prefix(field) {
|
||||
let value = rest.trim().trim_matches('"');
|
||||
if !value.is_empty() {
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Match a brand to a template name.
|
||||
fn match_template(brand: &str, template_names: &[String], registry: &Path) -> Option<String> {
|
||||
for name in template_names {
|
||||
if let Ok(t) = Template::load(registry, name) {
|
||||
if t.brand == brand {
|
||||
return Some(name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Find which pool contains the given address and return its stub.
|
||||
fn match_pool_stub(address: &str, pool_names: &[String], registry: &Path) -> Option<String> {
|
||||
let ip_str = address.split('/').next()?;
|
||||
let ip: std::net::Ipv4Addr = ip_str.parse().ok()?;
|
||||
|
||||
for name in pool_names {
|
||||
if let Ok(pool) = Pool::load(registry, name) {
|
||||
if pool.network.contains(&ip) {
|
||||
return Some(pool.stub.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
30
src/kdl_util.rs
Normal file
30
src/kdl_util.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use std::path::Path;
|
||||
|
||||
use kdl::KdlDocument;
|
||||
|
||||
use crate::error::{Result, ZmgrError};
|
||||
|
||||
/// Read and parse a KDL file.
|
||||
pub fn read_document(path: &Path) -> Result<KdlDocument> {
|
||||
let content = std::fs::read_to_string(path).map_err(ZmgrError::Io)?;
|
||||
let doc: KdlDocument = content.parse().map_err(|e| ZmgrError::KdlParse {
|
||||
path: path.display().to_string(),
|
||||
source: e,
|
||||
})?;
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
/// Get a string value from a top-level node: `name "value"`
|
||||
pub fn get_string(doc: &KdlDocument, name: &str) -> Option<String> {
|
||||
doc.get(name)
|
||||
.and_then(|node| node.entries().first())
|
||||
.and_then(|entry| entry.value().as_string())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
/// Get a bool value from a top-level node: `name true`
|
||||
pub fn get_bool(doc: &KdlDocument, name: &str) -> Option<bool> {
|
||||
doc.get(name)
|
||||
.and_then(|node| node.entries().first())
|
||||
.and_then(|entry| entry.value().as_bool())
|
||||
}
|
||||
522
src/main.rs
Normal file
522
src/main.rs
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
// thiserror derive generates false-positive unused_assignments warnings
|
||||
// for fields used in #[error("...{field}...")] format strings
|
||||
#![allow(unused_assignments)]
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
mod exec;
|
||||
mod import;
|
||||
mod kdl_util;
|
||||
mod pool;
|
||||
mod publisher;
|
||||
mod template;
|
||||
mod zone;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::Path;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::{Result, ZmgrError};
|
||||
use crate::pool::Pool;
|
||||
use crate::publisher::Publisher;
|
||||
use crate::template::Template;
|
||||
use crate::zone::Zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "zmgr", about = "illumos zone manager")]
|
||||
struct Cli {
|
||||
/// Registry path (default: /etc/zmgr)
|
||||
#[arg(long, default_value = "/etc/zmgr")]
|
||||
registry: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Initialize the registry with default templates and pools
|
||||
Init {
|
||||
/// Overwrite existing registry
|
||||
#[arg(long)]
|
||||
force: bool,
|
||||
},
|
||||
/// Create a new zone
|
||||
Create {
|
||||
/// Zone name
|
||||
name: String,
|
||||
/// Template to use (default from config)
|
||||
#[arg(short, long)]
|
||||
template: Option<String>,
|
||||
},
|
||||
/// Destroy a managed zone
|
||||
Destroy {
|
||||
/// Zone name
|
||||
name: String,
|
||||
},
|
||||
/// List managed zones
|
||||
List,
|
||||
/// Show zone status (registry + system state)
|
||||
Status {
|
||||
/// Zone name (omit for all)
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Import existing zones into the registry
|
||||
Import {
|
||||
/// Only import this specific zone
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Manage templates
|
||||
Template {
|
||||
#[command(subcommand)]
|
||||
action: TemplateAction,
|
||||
},
|
||||
/// Manage IPAM pools
|
||||
Pool {
|
||||
#[command(subcommand)]
|
||||
action: PoolAction,
|
||||
},
|
||||
/// Manage IPS publishers
|
||||
Publisher {
|
||||
#[command(subcommand)]
|
||||
action: PublisherAction,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum TemplateAction {
|
||||
/// List available templates
|
||||
List,
|
||||
/// Show template details
|
||||
Show { name: String },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum PoolAction {
|
||||
/// List available pools
|
||||
List,
|
||||
/// Show pool details and allocations
|
||||
Show { name: String },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum PublisherAction {
|
||||
/// List configured publishers
|
||||
List,
|
||||
/// Add a publisher
|
||||
Add {
|
||||
/// Publisher name (e.g., openindiana.org)
|
||||
name: String,
|
||||
/// Origin URL
|
||||
origin: String,
|
||||
},
|
||||
/// Remove a publisher
|
||||
Remove {
|
||||
/// Publisher filename stem
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> miette::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let registry = std::path::PathBuf::from(&cli.registry);
|
||||
|
||||
match cli.command {
|
||||
Commands::Init { force } => cmd_init(®istry, force),
|
||||
Commands::Create { name, template } => cmd_create(®istry, &name, template.as_deref()),
|
||||
Commands::Destroy { name } => cmd_destroy(®istry, &name),
|
||||
Commands::List => cmd_list(®istry),
|
||||
Commands::Status { name } => cmd_status(®istry, name.as_deref()),
|
||||
Commands::Import { name } => cmd_import(®istry, name.as_deref()),
|
||||
Commands::Template { action } => match action {
|
||||
TemplateAction::List => cmd_template_list(®istry),
|
||||
TemplateAction::Show { name } => cmd_template_show(®istry, &name),
|
||||
},
|
||||
Commands::Pool { action } => match action {
|
||||
PoolAction::List => cmd_pool_list(®istry),
|
||||
PoolAction::Show { name } => cmd_pool_show(®istry, &name),
|
||||
},
|
||||
Commands::Publisher { action } => match action {
|
||||
PublisherAction::List => cmd_publisher_list(®istry),
|
||||
PublisherAction::Add { name, origin } => cmd_publisher_add(®istry, &name, &origin),
|
||||
PublisherAction::Remove { name } => cmd_publisher_remove(®istry, &name),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_init(registry: &Path, force: bool) -> Result<()> {
|
||||
if registry.join("config.kdl").exists() && !force {
|
||||
return Err(ZmgrError::AlreadyInitialized {
|
||||
path: registry.display().to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(registry).map_err(ZmgrError::Io)?;
|
||||
std::fs::create_dir_all(registry.join("zones")).map_err(ZmgrError::Io)?;
|
||||
|
||||
Config::write_default(registry)?;
|
||||
Template::write_defaults(registry)?;
|
||||
Pool::write_defaults(registry)?;
|
||||
Publisher::write_defaults(registry)?;
|
||||
|
||||
println!("Initialized zmgr registry at {}", registry.display());
|
||||
println!(" config.kdl — global settings");
|
||||
println!(" templates/ — zone templates (oi, ofl)");
|
||||
println!(" pools/ — IPAM pools (internal, ofl)");
|
||||
println!(" publishers/ — IPS publishers (openindiana)");
|
||||
println!(" zones/ — zone registry (empty)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_create(registry: &Path, name: &str, template_name: Option<&str>) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
|
||||
if Zone::exists(registry, name) {
|
||||
return Err(ZmgrError::ZoneExists {
|
||||
name: name.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
let cfg = Config::load(registry)?;
|
||||
let tmpl_name = template_name.unwrap_or(&cfg.default_template);
|
||||
let tmpl = Template::load(registry, tmpl_name)?;
|
||||
let pool = Pool::load(registry, &tmpl.pool)?;
|
||||
|
||||
// Allocate next free IP
|
||||
let zones = Zone::list(registry)?;
|
||||
let ip = pool.allocate(&zones)?;
|
||||
let prefix_len = pool.network.prefix_len();
|
||||
let address = format!("{ip}/{prefix_len}");
|
||||
let vnic = format!("{name}0");
|
||||
|
||||
// Create VNIC
|
||||
exec::dladm_create_vnic(&vnic, &pool.stub)?;
|
||||
|
||||
// Build zonecfg commands
|
||||
let autoboot = if tmpl.autoboot { "true" } else { "false" };
|
||||
let zonecfg_cmds = format!(
|
||||
"create -b\n\
|
||||
set zonepath={}/{name}\n\
|
||||
set brand={}\n\
|
||||
set autoboot={autoboot}\n\
|
||||
set ip-type={}\n\
|
||||
add net\n\
|
||||
set physical={vnic}\n\
|
||||
set allowed-address={address}\n\
|
||||
set defrouter={}\n\
|
||||
end\n\
|
||||
verify\n\
|
||||
commit\n",
|
||||
cfg.zonepath_prefix, tmpl.brand, tmpl.ip_type, pool.gateway
|
||||
);
|
||||
|
||||
exec::zonecfg_create(name, &zonecfg_cmds)?;
|
||||
exec::zoneadm_install(name)?;
|
||||
|
||||
// Write registry entry
|
||||
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||
let zone = Zone {
|
||||
name: name.to_string(),
|
||||
template: tmpl_name.to_string(),
|
||||
address: address.clone(),
|
||||
gateway: pool.gateway.to_string(),
|
||||
vnic: vnic.clone(),
|
||||
stub: pool.stub.clone(),
|
||||
created: today,
|
||||
};
|
||||
zone.write(registry)?;
|
||||
|
||||
println!("Created zone '{name}'");
|
||||
println!(" template: {tmpl_name}");
|
||||
println!(" brand: {}", tmpl.brand);
|
||||
println!(" address: {address}");
|
||||
println!(" vnic: {vnic}");
|
||||
println!(" gateway: {}", pool.gateway);
|
||||
|
||||
if tmpl.autoboot {
|
||||
exec::zoneadm_boot(name)?;
|
||||
println!(" status: booted");
|
||||
} else {
|
||||
println!(" status: installed (autoboot=false)");
|
||||
println!(" hint: run `zoneadm -z {name} boot` to start");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_destroy(registry: &Path, name: &str) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
|
||||
let zone = Zone::load(registry, name)?;
|
||||
|
||||
// Halt if running (ignore errors — zone may already be halted)
|
||||
let _ = exec::zoneadm_halt(name);
|
||||
|
||||
// Uninstall
|
||||
if let Err(e) = exec::zoneadm_uninstall(name) {
|
||||
eprintln!("warning: zoneadm uninstall: {e}");
|
||||
}
|
||||
|
||||
// Delete zonecfg
|
||||
if let Err(e) = exec::zonecfg_delete(name) {
|
||||
eprintln!("warning: zonecfg delete: {e}");
|
||||
}
|
||||
|
||||
// Delete VNIC
|
||||
if let Err(e) = exec::dladm_delete_vnic(&zone.vnic) {
|
||||
eprintln!("warning: dladm delete-vnic: {e}");
|
||||
}
|
||||
|
||||
// Remove from registry
|
||||
Zone::delete(registry, name)?;
|
||||
|
||||
println!("Destroyed zone '{name}'");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_list(registry: &Path) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
|
||||
let zones = Zone::list(registry)?;
|
||||
if zones.is_empty() {
|
||||
println!("No managed zones. Use `zmgr create <name>` or `zmgr import`.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!(
|
||||
"{:<20} {:<10} {:<20} {:<15}",
|
||||
"NAME", "TEMPLATE", "ADDRESS", "VNIC"
|
||||
);
|
||||
for z in &zones {
|
||||
println!(
|
||||
"{:<20} {:<10} {:<20} {:<15}",
|
||||
z.name, z.template, z.address, z.vnic
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
|
||||
let system_zones = exec::zoneadm_list()?;
|
||||
let registry_zones = Zone::list(registry)?;
|
||||
|
||||
if let Some(name) = name {
|
||||
let reg = registry_zones.iter().find(|z| z.name == name);
|
||||
let sys = system_zones.iter().find(|z| z.name == name);
|
||||
|
||||
match (reg, sys) {
|
||||
(Some(r), Some(s)) => {
|
||||
println!("Zone: {name}");
|
||||
println!(" template: {}", r.template);
|
||||
println!(" brand: {}", s.brand);
|
||||
println!(" state: {}", s.state);
|
||||
println!(" address: {}", r.address);
|
||||
println!(" vnic: {}", r.vnic);
|
||||
println!(" path: {}", s.path);
|
||||
println!(" uuid: {}", s.uuid);
|
||||
println!(" created: {}", r.created);
|
||||
}
|
||||
(Some(r), None) => {
|
||||
println!("Zone: {name} (in registry but not on system)");
|
||||
println!(" template: {}", r.template);
|
||||
println!(" address: {}", r.address);
|
||||
}
|
||||
(None, Some(s)) => {
|
||||
println!("Zone: {name} (on system but not in registry)");
|
||||
println!(" brand: {}", s.brand);
|
||||
println!(" state: {}", s.state);
|
||||
println!(" hint: run `zmgr import {name}` to add to registry");
|
||||
}
|
||||
(None, None) => {
|
||||
return Err(ZmgrError::ZoneNotFound {
|
||||
name: name.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{:<20} {:<10} {:<12} {:<20} {:<10}",
|
||||
"NAME", "TEMPLATE", "STATE", "ADDRESS", "REGISTRY"
|
||||
);
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for r in ®istry_zones {
|
||||
let state = system_zones
|
||||
.iter()
|
||||
.find(|s| s.name == r.name)
|
||||
.map(|s| s.state.as_str())
|
||||
.unwrap_or("absent");
|
||||
println!(
|
||||
"{:<20} {:<10} {:<12} {:<20} {:<10}",
|
||||
r.name, r.template, state, r.address, "yes"
|
||||
);
|
||||
seen.insert(r.name.clone());
|
||||
}
|
||||
|
||||
for s in &system_zones {
|
||||
if !seen.contains(&s.name) && s.brand != "bhyve" {
|
||||
println!(
|
||||
"{:<20} {:<10} {:<12} {:<20} {:<10}",
|
||||
s.name, s.brand, s.state, "-", "no"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_import(registry: &Path, name: Option<&str>) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
|
||||
let imported = import::import_zones(registry, name)?;
|
||||
|
||||
if imported.is_empty() {
|
||||
if let Some(name) = name {
|
||||
println!("Zone '{name}' not found on system or already in registry.");
|
||||
} else {
|
||||
println!("No new zones to import.");
|
||||
}
|
||||
} else {
|
||||
for z in &imported {
|
||||
println!("Imported zone '{z}'");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_template_list(registry: &Path) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
|
||||
let names = Template::list(registry)?;
|
||||
if names.is_empty() {
|
||||
println!("No templates defined.");
|
||||
return Ok(());
|
||||
}
|
||||
for name in &names {
|
||||
let tmpl = Template::load(registry, name)?;
|
||||
println!(
|
||||
"{:<10} brand={:<10} pool={:<10} autoboot={}",
|
||||
name, tmpl.brand, tmpl.pool, tmpl.autoboot
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_template_show(registry: &Path, name: &str) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
|
||||
let tmpl = Template::load(registry, name)?;
|
||||
println!("Template: {name}");
|
||||
println!(" brand: {}", tmpl.brand);
|
||||
println!(" autoboot: {}", tmpl.autoboot);
|
||||
println!(" ip-type: {}", tmpl.ip_type);
|
||||
println!(" pool: {}", tmpl.pool);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_pool_list(registry: &Path) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
|
||||
let names = Pool::list(registry)?;
|
||||
if names.is_empty() {
|
||||
println!("No pools defined.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let zones = Zone::list(registry)?;
|
||||
|
||||
for name in &names {
|
||||
let pool = Pool::load(registry, name)?;
|
||||
let allocated: usize = zones
|
||||
.iter()
|
||||
.filter(|z| {
|
||||
z.address
|
||||
.split('/')
|
||||
.next()
|
||||
.and_then(|ip| ip.parse::<Ipv4Addr>().ok())
|
||||
.is_some_and(|ip| pool.network.contains(&ip))
|
||||
})
|
||||
.count();
|
||||
let total = u32::from(pool.range_end) - u32::from(pool.range_start) + 1;
|
||||
println!(
|
||||
"{:<10} network={:<18} stub={:<12} used={}/{}",
|
||||
name, pool.network, pool.stub, allocated, total
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_pool_show(registry: &Path, name: &str) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
|
||||
let pool = Pool::load(registry, name)?;
|
||||
let zones = Zone::list(registry)?;
|
||||
|
||||
println!("Pool: {name}");
|
||||
println!(" network: {}", pool.network);
|
||||
println!(" gateway: {}", pool.gateway);
|
||||
println!(" stub: {}", pool.stub);
|
||||
println!(" range: {} - {}", pool.range_start, pool.range_end);
|
||||
|
||||
let allocated: Vec<&Zone> = zones
|
||||
.iter()
|
||||
.filter(|z| {
|
||||
z.address
|
||||
.split('/')
|
||||
.next()
|
||||
.and_then(|ip| ip.parse::<Ipv4Addr>().ok())
|
||||
.is_some_and(|ip| pool.network.contains(&ip))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if allocated.is_empty() {
|
||||
println!(" allocations: none");
|
||||
} else {
|
||||
println!(" allocations:");
|
||||
for z in &allocated {
|
||||
println!(" {} -> {}", z.address, z.name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_publisher_list(registry: &Path) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
|
||||
let names = Publisher::list(registry)?;
|
||||
if names.is_empty() {
|
||||
println!("No publishers configured.");
|
||||
return Ok(());
|
||||
}
|
||||
for name in &names {
|
||||
let pub_ = Publisher::load(registry, name)?;
|
||||
println!("{:<30} {}", pub_.name, pub_.origin);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_publisher_add(registry: &Path, name: &str, origin: &str) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
Publisher::add(registry, name, origin)?;
|
||||
println!("Added publisher '{name}' -> {origin}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_publisher_remove(registry: &Path, name: &str) -> Result<()> {
|
||||
config::ensure_initialized(registry)?;
|
||||
Publisher::remove(registry, name)?;
|
||||
println!("Removed publisher '{name}'");
|
||||
Ok(())
|
||||
}
|
||||
189
src/pool.rs
Normal file
189
src/pool.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
use std::net::Ipv4Addr;
|
||||
use std::path::Path;
|
||||
|
||||
use ipnet::Ipv4Net;
|
||||
|
||||
use crate::error::{Result, ZmgrError};
|
||||
use crate::kdl_util;
|
||||
use crate::zone::Zone;
|
||||
|
||||
/// An IPAM address pool.
|
||||
pub struct Pool {
|
||||
pub name: String,
|
||||
pub network: Ipv4Net,
|
||||
pub gateway: Ipv4Addr,
|
||||
pub stub: String,
|
||||
pub range_start: Ipv4Addr,
|
||||
pub range_end: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
/// Load a pool from `/etc/zmgr/pools/<name>.kdl`.
|
||||
pub fn load(registry: &Path, name: &str) -> Result<Self> {
|
||||
let path = registry.join("pools").join(format!("{name}.kdl"));
|
||||
if !path.exists() {
|
||||
return Err(ZmgrError::PoolNotFound {
|
||||
name: name.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let doc = kdl_util::read_document(&path)?;
|
||||
let ctx = format!("pool '{name}'");
|
||||
|
||||
let pool_node = doc.get("pool").ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "pool".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
let children = pool_node.children().ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "pool children".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
let network_str = kdl_util::get_string(children, "network")
|
||||
.ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "network".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
let network: Ipv4Net = network_str.parse().map_err(|_| ZmgrError::MissingField {
|
||||
field: format!("network (invalid CIDR: {network_str})"),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
let gateway_str = kdl_util::get_string(children, "gateway")
|
||||
.ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "gateway".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
let gateway: Ipv4Addr = gateway_str.parse().map_err(|_| ZmgrError::MissingField {
|
||||
field: format!("gateway (invalid IP: {gateway_str})"),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
let stub = kdl_util::get_string(children, "stub")
|
||||
.ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "stub".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
let range_start_str = kdl_util::get_string(children, "range-start")
|
||||
.ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "range-start".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
let range_start: Ipv4Addr =
|
||||
range_start_str
|
||||
.parse()
|
||||
.map_err(|_| ZmgrError::MissingField {
|
||||
field: format!("range-start (invalid IP: {range_start_str})"),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
let range_end_str = kdl_util::get_string(children, "range-end")
|
||||
.ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "range-end".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
let range_end: Ipv4Addr =
|
||||
range_end_str
|
||||
.parse()
|
||||
.map_err(|_| ZmgrError::MissingField {
|
||||
field: format!("range-end (invalid IP: {range_end_str})"),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
Ok(Pool {
|
||||
name: name.to_string(),
|
||||
network,
|
||||
gateway,
|
||||
stub,
|
||||
range_start,
|
||||
range_end,
|
||||
})
|
||||
}
|
||||
|
||||
/// Allocate the next free IP from this pool, given existing zones.
|
||||
pub fn allocate(&self, zones: &[Zone]) -> Result<Ipv4Addr> {
|
||||
let allocated: Vec<Ipv4Addr> = zones
|
||||
.iter()
|
||||
.filter_map(|z| {
|
||||
// address is stored as "x.x.x.x/prefix"
|
||||
z.address
|
||||
.split('/')
|
||||
.next()
|
||||
.and_then(|ip| ip.parse::<Ipv4Addr>().ok())
|
||||
})
|
||||
.filter(|ip| self.network.contains(ip))
|
||||
.collect();
|
||||
|
||||
let start = u32::from(self.range_start);
|
||||
let end = u32::from(self.range_end);
|
||||
|
||||
for candidate in start..=end {
|
||||
let ip = Ipv4Addr::from(candidate);
|
||||
if !allocated.contains(&ip) {
|
||||
return Ok(ip);
|
||||
}
|
||||
}
|
||||
|
||||
Err(ZmgrError::PoolExhausted {
|
||||
pool: self.name.clone(),
|
||||
range_start: self.range_start.to_string(),
|
||||
range_end: self.range_end.to_string(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// List all pool names.
|
||||
pub fn list(registry: &Path) -> Result<Vec<String>> {
|
||||
let dir = registry.join("pools");
|
||||
if !dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut names = Vec::new();
|
||||
for entry in std::fs::read_dir(&dir).map_err(ZmgrError::Io)? {
|
||||
let entry = entry.map_err(ZmgrError::Io)?;
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "kdl") {
|
||||
if let Some(stem) = path.file_stem() {
|
||||
names.push(stem.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
names.sort();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// Write default pools derived from the existing scripts.
|
||||
pub fn write_defaults(registry: &Path) -> Result<()> {
|
||||
let dir = registry.join("pools");
|
||||
std::fs::create_dir_all(&dir).map_err(ZmgrError::Io)?;
|
||||
|
||||
std::fs::write(
|
||||
dir.join("internal.kdl"),
|
||||
r#"pool "internal" {
|
||||
network "10.1.0.0/24"
|
||||
gateway "10.1.0.1"
|
||||
stub "oinetint0"
|
||||
range-start "10.1.0.10"
|
||||
range-end "10.1.0.250"
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.map_err(ZmgrError::Io)?;
|
||||
|
||||
std::fs::write(
|
||||
dir.join("ofl.kdl"),
|
||||
r#"pool "ofl" {
|
||||
network "192.168.10.0/24"
|
||||
gateway "192.168.10.1"
|
||||
stub "oflint0"
|
||||
range-start "192.168.10.10"
|
||||
range-end "192.168.10.250"
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.map_err(ZmgrError::Io)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
119
src/publisher.rs
Normal file
119
src/publisher.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
use std::path::Path;
|
||||
|
||||
use crate::error::{Result, ZmgrError};
|
||||
use crate::kdl_util;
|
||||
|
||||
/// An IPS publisher configuration.
|
||||
pub struct Publisher {
|
||||
pub name: String,
|
||||
pub origin: String,
|
||||
}
|
||||
|
||||
impl Publisher {
|
||||
/// Load a publisher from `/etc/zmgr/publishers/<name>.kdl`.
|
||||
pub fn load(registry: &Path, name: &str) -> Result<Self> {
|
||||
let path = registry.join("publishers").join(format!("{name}.kdl"));
|
||||
if !path.exists() {
|
||||
return Err(ZmgrError::MissingField {
|
||||
field: "publisher".to_string(),
|
||||
context: format!("publishers/{name}.kdl"),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let doc = kdl_util::read_document(&path)?;
|
||||
let ctx = format!("publisher '{name}'");
|
||||
|
||||
let pub_node = doc
|
||||
.get("publisher")
|
||||
.ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "publisher".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
let pub_name = pub_node
|
||||
.entries()
|
||||
.first()
|
||||
.and_then(|e| e.value().as_string())
|
||||
.unwrap_or(name)
|
||||
.to_string();
|
||||
|
||||
let children = pub_node.children().ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "publisher children".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
let origin = kdl_util::get_string(children, "origin").ok_or_else(|| {
|
||||
ZmgrError::MissingField {
|
||||
field: "origin".to_string(),
|
||||
context: ctx.clone(),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Publisher {
|
||||
name: pub_name,
|
||||
origin,
|
||||
})
|
||||
}
|
||||
|
||||
/// List all publisher names.
|
||||
pub fn list(registry: &Path) -> Result<Vec<String>> {
|
||||
let dir = registry.join("publishers");
|
||||
if !dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut names = Vec::new();
|
||||
for entry in std::fs::read_dir(&dir).map_err(ZmgrError::Io)? {
|
||||
let entry = entry.map_err(ZmgrError::Io)?;
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "kdl") {
|
||||
if let Some(stem) = path.file_stem() {
|
||||
names.push(stem.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
names.sort();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// Add a new publisher.
|
||||
pub fn add(registry: &Path, name: &str, origin: &str) -> Result<()> {
|
||||
let dir = registry.join("publishers");
|
||||
std::fs::create_dir_all(&dir).map_err(ZmgrError::Io)?;
|
||||
// Use the last segment of the publisher name as the filename
|
||||
let filename = name.replace('.', "_");
|
||||
let path = dir.join(format!("{filename}.kdl"));
|
||||
let content = format!(
|
||||
r#"publisher "{name}" {{
|
||||
origin "{origin}"
|
||||
}}
|
||||
"#
|
||||
);
|
||||
std::fs::write(&path, content).map_err(ZmgrError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a publisher by filename stem.
|
||||
pub fn remove(registry: &Path, name: &str) -> Result<()> {
|
||||
let dir = registry.join("publishers");
|
||||
let path = dir.join(format!("{name}.kdl"));
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path).map_err(ZmgrError::Io)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write default publishers.
|
||||
pub fn write_defaults(registry: &Path) -> Result<()> {
|
||||
let dir = registry.join("publishers");
|
||||
std::fs::create_dir_all(&dir).map_err(ZmgrError::Io)?;
|
||||
std::fs::write(
|
||||
dir.join("openindiana.kdl"),
|
||||
r#"publisher "openindiana.org" {
|
||||
origin "https://pkg.openindiana.org/hipster"
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.map_err(ZmgrError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
116
src/template.rs
Normal file
116
src/template.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use std::path::Path;
|
||||
|
||||
use crate::error::{Result, ZmgrError};
|
||||
use crate::kdl_util;
|
||||
|
||||
/// A zone creation template.
|
||||
#[allow(dead_code)]
|
||||
pub struct Template {
|
||||
pub name: String,
|
||||
pub brand: String,
|
||||
pub autoboot: bool,
|
||||
pub ip_type: String,
|
||||
pub pool: String,
|
||||
}
|
||||
|
||||
impl Template {
|
||||
/// Load a template from `/etc/zmgr/templates/<name>.kdl`.
|
||||
pub fn load(registry: &Path, name: &str) -> Result<Self> {
|
||||
let path = registry.join("templates").join(format!("{name}.kdl"));
|
||||
if !path.exists() {
|
||||
return Err(ZmgrError::TemplateNotFound {
|
||||
name: name.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let doc = kdl_util::read_document(&path)?;
|
||||
let ctx = format!("template '{name}'");
|
||||
|
||||
// The template node wraps the children
|
||||
let template_node = doc.get("template").ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "template".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
let children = template_node.children().ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "template children".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
let brand = kdl_util::get_string(children, "brand")
|
||||
.ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "brand".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
let autoboot = kdl_util::get_bool(children, "autoboot").unwrap_or(false);
|
||||
|
||||
let ip_type = kdl_util::get_string(children, "ip-type")
|
||||
.unwrap_or_else(|| "exclusive".to_string());
|
||||
|
||||
let pool = kdl_util::get_string(children, "pool")
|
||||
.ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "pool".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
Ok(Template {
|
||||
name: name.to_string(),
|
||||
brand,
|
||||
autoboot,
|
||||
ip_type,
|
||||
pool,
|
||||
})
|
||||
}
|
||||
|
||||
/// List all template names.
|
||||
pub fn list(registry: &Path) -> Result<Vec<String>> {
|
||||
let dir = registry.join("templates");
|
||||
if !dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut names = Vec::new();
|
||||
for entry in std::fs::read_dir(&dir).map_err(ZmgrError::Io)? {
|
||||
let entry = entry.map_err(ZmgrError::Io)?;
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "kdl") {
|
||||
if let Some(stem) = path.file_stem() {
|
||||
names.push(stem.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
names.sort();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// Write default templates derived from the existing scripts.
|
||||
pub fn write_defaults(registry: &Path) -> Result<()> {
|
||||
let dir = registry.join("templates");
|
||||
std::fs::create_dir_all(&dir).map_err(ZmgrError::Io)?;
|
||||
|
||||
std::fs::write(
|
||||
dir.join("oi.kdl"),
|
||||
r#"template "oi" {
|
||||
brand "ipkg"
|
||||
autoboot #false
|
||||
ip-type "exclusive"
|
||||
pool "internal"
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.map_err(ZmgrError::Io)?;
|
||||
|
||||
std::fs::write(
|
||||
dir.join("ofl.kdl"),
|
||||
r#"template "ofl" {
|
||||
brand "nlipkg"
|
||||
autoboot #true
|
||||
ip-type "exclusive"
|
||||
pool "ofl"
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.map_err(ZmgrError::Io)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
131
src/zone.rs
Normal file
131
src/zone.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
use std::path::Path;
|
||||
|
||||
use crate::error::{Result, ZmgrError};
|
||||
use crate::kdl_util;
|
||||
|
||||
/// A managed zone registry entry.
|
||||
pub struct Zone {
|
||||
pub name: String,
|
||||
pub template: String,
|
||||
pub address: String,
|
||||
pub gateway: String,
|
||||
pub vnic: String,
|
||||
pub stub: String,
|
||||
pub created: String,
|
||||
}
|
||||
|
||||
impl Zone {
|
||||
/// Load a zone from `/etc/zmgr/zones/<name>.kdl`.
|
||||
pub fn load(registry: &Path, name: &str) -> Result<Self> {
|
||||
let path = registry.join("zones").join(format!("{name}.kdl"));
|
||||
if !path.exists() {
|
||||
return Err(ZmgrError::ZoneNotFound {
|
||||
name: name.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
Self::load_from_path(&path)
|
||||
}
|
||||
|
||||
fn load_from_path(path: &Path) -> Result<Self> {
|
||||
let doc = kdl_util::read_document(path)?;
|
||||
let ctx = path.display().to_string();
|
||||
|
||||
let zone_node = doc.get("zone").ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "zone".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
let name = zone_node
|
||||
.entries()
|
||||
.first()
|
||||
.and_then(|e| e.value().as_string())
|
||||
.ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "zone name argument".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
let children = zone_node.children().ok_or_else(|| ZmgrError::MissingField {
|
||||
field: "zone children".to_string(),
|
||||
context: ctx.clone(),
|
||||
})?;
|
||||
|
||||
Ok(Zone {
|
||||
name: name.clone(),
|
||||
template: kdl_util::get_string(children, "template")
|
||||
.unwrap_or_default(),
|
||||
address: kdl_util::get_string(children, "address")
|
||||
.unwrap_or_default(),
|
||||
gateway: kdl_util::get_string(children, "gateway")
|
||||
.unwrap_or_default(),
|
||||
vnic: kdl_util::get_string(children, "vnic")
|
||||
.unwrap_or_default(),
|
||||
stub: kdl_util::get_string(children, "stub")
|
||||
.unwrap_or_default(),
|
||||
created: kdl_util::get_string(children, "created")
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Write this zone to the registry.
|
||||
pub fn write(&self, registry: &Path) -> Result<()> {
|
||||
let dir = registry.join("zones");
|
||||
std::fs::create_dir_all(&dir).map_err(ZmgrError::Io)?;
|
||||
let path = dir.join(format!("{}.kdl", self.name));
|
||||
let content = format!(
|
||||
r#"zone "{}" {{
|
||||
template "{}"
|
||||
address "{}"
|
||||
gateway "{}"
|
||||
vnic "{}"
|
||||
stub "{}"
|
||||
created "{}"
|
||||
}}
|
||||
"#,
|
||||
self.name, self.template, self.address, self.gateway, self.vnic, self.stub, self.created
|
||||
);
|
||||
std::fs::write(&path, content).map_err(ZmgrError::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete this zone's registry entry.
|
||||
pub fn delete(registry: &Path, name: &str) -> Result<()> {
|
||||
let path = registry.join("zones").join(format!("{name}.kdl"));
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path).map_err(ZmgrError::Io)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all zone registry entries.
|
||||
pub fn list(registry: &Path) -> Result<Vec<Zone>> {
|
||||
let dir = registry.join("zones");
|
||||
if !dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut zones = Vec::new();
|
||||
for entry in std::fs::read_dir(&dir).map_err(ZmgrError::Io)? {
|
||||
let entry = entry.map_err(ZmgrError::Io)?;
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "kdl") {
|
||||
match Zone::load_from_path(&path) {
|
||||
Ok(z) => zones.push(z),
|
||||
Err(e) => {
|
||||
eprintln!("warning: skipping {}: {e}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
zones.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(zones)
|
||||
}
|
||||
|
||||
/// Check if a zone exists in the registry.
|
||||
pub fn exists(registry: &Path, name: &str) -> bool {
|
||||
registry
|
||||
.join("zones")
|
||||
.join(format!("{name}.kdl"))
|
||||
.exists()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue