add xml parse

This commit is contained in:
2026-03-19 23:49:16 +08:00
parent 8b279df333
commit 230d3d4204
38 changed files with 2736 additions and 9 deletions

851
Cargo.lock generated
View File

@@ -2,6 +2,24 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.21" version = "0.6.21"
@@ -58,6 +76,64 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[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 = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[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",
"num-traits",
"serde",
"windows-link",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.60" version = "4.5.60"
@@ -104,24 +180,301 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "darling"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
"serde_core",
]
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[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 = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.2" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[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 = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[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 = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "num-conv"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "once_cell_polyfill" name = "once_cell_polyfill"
version = "1.70.2" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "path-slash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@@ -140,6 +493,243 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "ref-cast"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "roxmltree"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb"
dependencies = [
"memchr",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "schemars"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_with"
version = "3.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f"
dependencies = [
"base64",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.13.0",
"schemars 0.9.0",
"schemars 1.2.1",
"serde_core",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.13.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@@ -152,6 +742,18 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"lazy_static",
"path-slash",
"regex",
"roxmltree",
"serde",
"serde_json",
"serde_with",
"serde_yaml",
"thiserror",
"tokio",
"toml",
"yaml-rust",
] ]
[[package]] [[package]]
@@ -165,24 +767,252 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[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 = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tokio"
version = "1.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "1.0.7+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96"
dependencies = [
"indexmap 2.13.0",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.0.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.7+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[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]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@@ -191,3 +1021,24 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "winnow"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -5,4 +5,16 @@ edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.102" anyhow = "1.0.102"
clap = { version = "4.5.60", features = ["derive"] } clap = { version = "4.5.60", features = ["derive", "env"] }
lazy_static = "1.5.0"
path-slash = "0.2.1"
regex = "1.12.3"
roxmltree = "0.21.1"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
serde_yaml = "0.9.34"
serde_with = "*"
thiserror = "2.0.18"
tokio = { version = "1.50.0", features = ["full"] }
toml = "1.0.7"
yaml-rust = "0.4.5"

View File

View File

@@ -0,0 +1,11 @@
# 计算节点专属配置
linux_distro = "centos"
linux_version = "8"
[nodes.compute]
variables = {"cpu_cores" = "48","memory" = "192G","swap_size" = "32G"}
# 追加XML路径
xml_paths = [
"xml/compute"
]

5
config/groups/gpu.toml Normal file
View File

@@ -0,0 +1,5 @@
extends = "compute"
[vars]
os = "rocky9"
has_gpu = true

View File

38
config/main.toml Normal file
View File

@@ -0,0 +1,38 @@
# 全局配置
linux_distro = "centos"
linux_version = "8"
# XML配置路径相对路径基于main.toml所在目录
xml_paths = [
"xml/kickstart",
"xml/packages"
]
# 全局变量
[variables]
domain = "sunhpc.local"
ntp_server = "ntp.sunhpc.local"
dns_server = "192.168.1.1"
root_password = "111111"
# 节点配置
[nodes]
[nodes.common]
arch = "x86_64"
linux_distro = "centos"
linux_version = "8"
[nodes.common.variables]
network_interface = "eth0"
ip_prefix = "192.168.1."
[nodes.compute]
inherit = "common"
[nodes.compute.variables]
ip_suffix = "100"
hostname = "compute-01"
[nodes.storage]
inherit = "common"
[nodes.storage.variables]
ip_suffix = "200"
hostname = "storage-01"

0
output/compute-0-0.ks Normal file
View File

0
output/compute-0-1.ks Normal file
View File

0
output/frontend-0-0.ks Normal file
View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1,133 @@
//! sunhpc config check 命令实现
//! 功能:加载配置文件 → 解析XML → 生成Linux Kickstart脚本
use anyhow::{Context, Result};
use clap::Args;
use std::env;
use std::path::PathBuf;
// 导入内部模块
use crate::internal::config::ksgen::KickstartGenerator;
use crate::internal::config::loader::ConfigLoader;
/// 配置检查命令参数
#[derive(Args, Debug)]
#[command(about = "检查配置文件并生成Kickstart脚本", long_about = None)]
pub struct CheckArgs {
/// 配置文件路径默认config/main.toml
#[arg(
short = 'c',
long = "config",
default_value = "config/main.toml",
help = "指定主配置文件路径TOML/YAML格式"
)]
pub config: PathBuf,
/// 输出格式text/json默认text
#[arg(
short = 'f',
long = "format",
default_value = "text",
help = "Kickstart输出格式text纯文本/json调试用"
)]
pub format: String,
/// 详细输出模式
#[arg(
long = "verbose",
help = "显示更多调试信息(配置加载过程、节点变量等)"
)]
pub verbose: bool,
/// 目标节点名称默认compute
#[arg(
short = 'n',
long = "node",
default_value = "compute",
help = "指定要生成Kickstart的节点名称对应配置中的nodes字段"
)]
pub node: String,
/// Linux发行版过滤可选覆盖配置中的值
#[arg(
long = "distro",
help = "指定Linux发行版如centos/ubuntu/rhel优先级高于配置文件"
)]
pub distro: Option<String>,
/// Linux版本过滤可选覆盖配置中的值
#[arg(
long = "version",
help = "指定Linux版本如8/9/22.04),优先级高于配置文件"
)]
pub version: Option<String>,
}
/// 执行check命令的核心逻辑
pub fn run(args: CheckArgs) -> Result<()> {
let cwd = get_current_working_dir()?;
// 1. 打印基础信息
println!("========================================");
println!("🔍 SUNHPC 配置检查工具Linux专属");
println!("========================================");
println!("配置文件路径: {}", args.config.display());
println!("当前路径: {}", cwd.display());
println!("目标节点: {}", args.node);
println!("输出格式: {}", args.format);
println!("详细模式: {}", if args.verbose { "开启" } else { "关闭" });
if let Some(distro) = &args.distro {
println!("指定发行版: {}", distro);
}
if let Some(version) = &args.version {
println!("指定版本: {}", version);
}
println!("----------------------------------------");
// 2. 加载配置文件TOML/YAML
let mut config_loader = ConfigLoader::new(&args.config)
.with_context(|| format!("加载配置文件失败: {}", args.config.display()))?;
// 覆盖发行版/版本配置(如果命令行指定)
if let Some(distro) = &args.distro {
config_loader.global.linux_distro = distro.clone();
}
if let Some(version) = &args.version {
config_loader.global.linux_version = version.clone();
}
// 详细模式:打印加载的配置信息
if args.verbose {
println!("📄 配置加载成功!");
println!(" - 全局变量数: {}", config_loader.global.variables.len());
println!(" - 节点数: {}", config_loader.global.nodes.len());
println!(" - XML路径数: {}", config_loader.global.xml_paths.len());
println!(" - Linux发行版: {}", config_loader.global.linux_distro);
println!(" - Linux版本: {}", config_loader.global.linux_version);
println!("----------------------------------------");
}
// 3. 初始化Kickstart生成器
let mut generator = KickstartGenerator::new(config_loader, &args.node)
.with_context(|| format!("初始化Kickstart生成器失败节点: {}", args.node))?;
// 4. 解析XML配置并构建Kickstart上下文
generator.parse()
.with_context(|| "解析XML配置文件失败")?;
// 5. 生成Kickstart脚本内容
let kickstart_content = generator.generate(&args.format)
.with_context(|| format!("生成Kickstart失败格式: {}", args.format))?;
// 6. 输出结果
println!("✅ Kickstart脚本生成成功");
println!("----------------------------------------");
println!("{}", kickstart_content);
Ok(())
}
/// 获取当前路径
fn get_current_working_dir() -> Result<PathBuf, std::io::Error> {
env::current_dir()
}

View File

@@ -0,0 +1,25 @@
use clap::Subcommand;
use anyhow::Result;
// 引入具体的命令逻辑文件
mod check;
mod sync;
/// Server 子命令集的具体命令
#[derive(Subcommand)]
pub enum ConfigCommands {
/// 检查配置文件语法正确性
Check(check::CheckArgs),
/// 分发配置文件到所有节点
Sync(sync::StopArgs),
}
/// 执行 server 命令集的入口函数
pub fn execute(cmd: ConfigCommands) -> Result<(), anyhow::Error> {
match cmd {
ConfigCommands::Check(args) => check::run(args),
ConfigCommands::Sync(args) => sync::run(args),
}
}

View File

@@ -0,0 +1,13 @@
use anyhow::Result;
#[derive(clap::Args)]
pub struct StopArgs {
/// 强制停止
#[arg(short, long)]
pub force: bool,
}
pub fn run(args: StopArgs) -> Result<()> {
println!("🛑 正在停止服务器... (force: {})", args.force);
Ok(())
}

49
src/commands/db/init.rs Normal file
View File

@@ -0,0 +1,49 @@
use clap::Args;
use anyhow::Result;
// 引入核心逻辑
use crate::internal::database::DbConfig;
use crate::internal::database::init_db;
#[derive(Args)]
pub struct InitArgs {
/// 数据库连接 URL
#[arg(short, long, env = "DATABASE_URL", default_value = "postgres://localhost/mydb")]
pub url: String,
/// 最大连接池大小
#[arg(long, default_value_t = 10)]
pub max_connections: u32,
/// 是否跳过确认提示
#[arg(long, short)]
pub yes: bool,
}
/// CLI 层的执行函数
pub async fn run(args: InitArgs) -> Result<()> {
if !args.yes {
print!("⚠️ 即将初始化数据库 '{}',数据可能会被清空。继续吗?(y/N): ", args.url);
use std::io::{self, Write};
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("操作已取消。");
return Ok(());
}
}
// 1. 将 CLI 参数转换为 核心逻辑需要的结构体
let config = DbConfig {
url: args.url,
max_connections: args.max_connections,
};
// 2. 调用 internal 层的纯逻辑
// 注意:这里调用的是 async 函数
init_db(&config).await?;
Ok(())
}

View File

@@ -2,15 +2,20 @@ use clap::Subcommand;
use anyhow::Result; use anyhow::Result;
mod migrate; mod migrate;
mod init;
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum DbCommands { pub enum DbCommands {
/// 运行数据库迁移 /// 运行数据库迁移
Migrate(migrate::MigrateArgs), Migrate(migrate::MigrateArgs),
/// 数据库初始化
Init(init::InitArgs),
} }
pub fn execute(cmd: DbCommands) -> Result<()> { pub async fn execute(cmd: DbCommands) -> Result<()> {
match cmd { match cmd {
DbCommands::Migrate(args) => migrate::run(args), DbCommands::Migrate(args) => migrate::run(args),
DbCommands::Init(args) => init::run(args).await,
} }
} }

27
src/commands/db/status.rs Normal file
View File

@@ -0,0 +1,27 @@
// src/commands/db/status.rs
use clap::Args;
use anyhow::Result;
use crate::internal::database::check_connection;
#[derive(Args)]
pub struct StatusArgs {
/// 数据库 URL (默认从环境变量读取)
#[arg(short, long, env = "DATABASE_URL", default_value = "postgres://localhost/mydb")]
pub url: String,
}
pub async fn run(args: StatusArgs) -> Result<()> {
println!("🏓 正在检查数据库状态...");
match check_connection(&args.url).await? {
true => {
println!("✅ 数据库连接正常: {}", args.url);
Ok(())
}
false => {
eprintln!("❌ 无法连接到数据库: {}", args.url);
// 返回错误码
std::process::exit(1);
}
}
}

View File

@@ -1,6 +1,8 @@
use clap::Subcommand; use clap::Subcommand;
// 声明子模块,对应 src/commands/ 下的目录 // 声明子模块,对应 src/commands/ 下的目录
pub mod config;
pub mod report;
pub mod server; pub mod server;
pub mod db; pub mod db;
// 未来扩展pub mod new_feature; // 未来扩展pub mod new_feature;
@@ -13,7 +15,15 @@ pub enum CliCommands {
#[command(subcommand)] #[command(subcommand)]
Server(server::ServerCommands), Server(server::ServerCommands),
/// 数据库管理相关命令 (db migrate, db seed) /// 服务器配置文件解析相关命令
#[command(subcommand)]
Config(config::ConfigCommands),
/// 打印集群配置信息
#[command(subcommand)]
Report(report::ReportCommands),
/// 数据库管理相关命令 (db init, db migrate)
#[command(subcommand)] #[command(subcommand)]
Db(db::DbCommands), Db(db::DbCommands),

View File

@@ -0,0 +1,21 @@
use clap::Subcommand;
use anyhow::Result;
// 引入具体的命令逻辑文件
mod nextip;
/// Server 子命令集的具体命令
#[derive(Subcommand)]
pub enum ReportCommands {
/// 生成下一个可用 IP 地址
#[command(name = "nextip")] // 强制子命令名为 nextip
NextIP(nextip::NextIPArgs), // 默认字母有大小写自动转换成驼峰命令法next-ip.
}
/// 执行 report nextip 命令集的入口函数
pub fn execute(cmd: ReportCommands) -> Result<(), anyhow::Error> {
match cmd {
ReportCommands::NextIP(args) => nextip::run(args),
}
}

View File

@@ -0,0 +1,71 @@
#[warn(unused_imports)]
use crate::internal::network::IPGenerator;
use anyhow::Result;
#[derive(clap::Args, Debug)]
pub struct NextIPArgs {
/// 网络地址(如 10.1.1.0
#[arg(short = 'n', long = "network", default_value = "10.1.1.0")]
network: String,
/// 子网掩码(如 255.255.255.128,不指定则自动推断)
#[arg(short = 'm', long = "netmask", default_value = "255.255.255.0")]
netmask: Option<String>,
/// IP 偏移步长(仅允许正数,从第一个可以用 IP 向后偏移)
#[arg(short = 's', long = "step", default_value = "1")]
step: u32,
}
pub fn run(args: NextIPArgs) -> Result<()> {
// 调用 IP 生成逻辑
generate_next_ip(args)?;
Ok(())
}
/// 核心逻辑:生成指定偏移的 IP 地址
fn generate_next_ip(opts: NextIPArgs) -> Result<()> {
// 创建 IP 生成器实例(用 anyhow 包装错误)
let mut generator = IPGenerator::new(&opts.network, opts.netmask.as_deref())
.map_err(|e| anyhow::anyhow!("Failed to create IP generator: {}", e))?;
// 获取网段最大可用步长,提前校验
let max_step = generator.get_max_available_step();
if opts.step > max_step {
return Err(anyhow::anyhow!(
"Step {} exceeds maximum available step {} for this network (mask: {})",
opts.step,
max_step,
generator.get_netmask_str()
));
}
// 获取初始 IP、新 IP、网络地址等信息
let initial_ip = generator.curr()
.map_err(|e| anyhow::anyhow!("Failed to get initial IP: {}", e))?;
let new_ip = generator.next(opts.step as i32)
.map_err(|e| anyhow::anyhow!("Failed to move IP address: {}", e))?;
let network_addr = generator.get_network();
let netmask_str = generator.get_netmask_str();
// ========== 核心:格式化对齐输出 ==========
// 定义左侧字段的宽度(根据最长字段调整,这里选 25 足够覆盖所有字段名)
const FIELD_WIDTH: usize = 30;
println!("{:>FIELD_WIDTH$}: {}", "Initial IP (first available)", initial_ip);
println!("{:>FIELD_WIDTH$}: {}", "IP after step", format!("{} (step: {})", new_ip, opts.step));
println!("{:>FIELD_WIDTH$}: {}", "Network address", network_addr);
println!("{:>FIELD_WIDTH$}: {}", "Subnet mask", netmask_str);
println!("{:>FIELD_WIDTH$}: {}", "Max available step", max_step);
println!("\nIP generation completed successfully");
// 扩展点:写入配置/同步到集群的业务逻辑
// 示例write_ip_to_config(&new_ip)?;
Ok(())
}

View File

@@ -0,0 +1,595 @@
//! Kickstart生成器模块
//! 功能:
//! 1. 基于配置和XML解析结果生成Linux Kickstart脚本
//! 2. 支持不同Linux发行版/版本的适配
//! 3. 输出纯文本或JSON格式的Kickstart内容
use anyhow::{Context, Result};
use serde::Serialize;
use std::collections::{HashMap, HashSet};
use std::collections::hash_map::RandomState;
use std::path::PathBuf;
use roxmltree::Node;
// 导入内部模块
use super::loader::ConfigLoader;
use super::xml_parser::XmlLoader;
/// Kickstart上下文存储生成脚本所需的所有数据
/// 对应gen.py的Generator_linux的ks字段
#[derive(Debug, Default, Serialize)]
pub struct KickstartContext {
/// 节点遍历顺序file, roll
pub order: Vec<(String, String)>,
/// debug信息列表
pub debug: Vec<String>,
/// main部分内容Kickstart核心配置
pub main: Vec<String>,
/// 启用的RPM包列表
pub rpms_on: Vec<String>,
/// 禁用的RPM包列表
pub rpms_off: Vec<String>,
/// pre脚本[参数, 内容]
pub pre: Vec<Vec<String>>,
/// post脚本[参数, 解释器, 内容]
pub post: Vec<Vec<String>>,
/// boot pre脚本
pub boot_pre: Vec<String>,
/// boot post脚本
pub boot_post: Vec<String>,
/// RCS文件配置路径: (所有者, 权限)
pub rcs_files: HashMap<PathBuf, (String, String)>,
}
/// 节点数据结构(用于避免借用冲突)
#[derive(Debug)]
struct NodeData {
/// 节点名称
pub node_name: String,
/// 文件属性
pub file: String,
/// roll属性
pub roll: String,
/// 节点文本内容
pub text: String,
/// 是否禁用package节点
pub disable: bool,
/// 是否元包package节点
pub meta_type: bool,
/// 解释器pre/post节点
pub interpreter: String,
/// 参数pre/post节点
pub arg: String,
/// 顺序boot节点
pub order: String,
/// 文件名file节点
pub file_name: String,
/// 所有者file节点
pub owner: String,
/// 权限file节点
pub perms: String,
/// main节点的子节点数据
pub children_data: Vec<MainChildData>,
}
/// main节点的子节点数据
#[derive(Debug)]
struct MainChildData {
/// 子节点名称
pub name: String,
/// 子节点文本
pub text: String,
/// partition属性clearpart节点
pub partition: String,
}
/// Linux Kickstart生成器
pub struct KickstartGenerator {
/// 配置加载器
config_loader: ConfigLoader,
/// XML加载器
xml_loader: XmlLoader,
/// Kickstart上下文
context: KickstartContext,
/// 目标节点名称
node_name: String,
}
impl KickstartGenerator {
/// 创建Kickstart生成器
/// 参数:
/// - config_loader: 配置加载器实例
/// - node_name: 目标节点名称
pub fn new(config_loader: ConfigLoader, node_name: &str) -> Result<Self> {
// 1. 获取节点变量用于XML过滤
let node_vars = config_loader.get_node_vars(node_name)
.with_context(|| format!("获取节点{}的变量失败", node_name))?;
// 2. 创建XML加载器使用节点变量作为过滤条件
let xml_loader = XmlLoader::new(node_vars, &config_loader.global.xml_paths)
.with_context(|| "初始化XML加载器失败")?;
// 3. 初始化上下文
let context = KickstartContext::default();
Ok(Self {
config_loader,
xml_loader,
context,
node_name: node_name.to_string(),
})
}
/// 解析XML并构建Kickstart上下文
pub fn parse(&mut self) -> Result<()> {
println!("📝 开始解析XML配置生成Kickstart...");
// 提取节点数据(避免借用冲突)
let node_data: Vec<NodeData> = self.xml_loader.iter_linux_nodes()
.map(|node| {
let node_name = node.tag_name().name().to_string();
let file = node.attribute("file").unwrap_or_default().to_string();
let roll = node.attribute("roll").unwrap_or_default().to_string();
let text = self.xml_loader.get_node_text(&node);
// 提取常用属性
let disable = node.attribute("disable").is_some();
let meta_type = node.attribute("type").map(|s| s == "meta").unwrap_or(false);
let interpreter = node.attribute("interpreter").unwrap_or("/bin/bash").to_string();
let arg = node.attribute("arg").unwrap_or_default().to_string();
let order = node.attribute("order").unwrap_or("pre").to_string();
let file_name = node.attribute("name").unwrap_or_default().to_string();
let owner = node.attribute("owner").unwrap_or("root").to_string();
let perms = node.attribute("perms").unwrap_or("0644").to_string();
// 对于main节点提取子节点数据
let children_data = if node_name == "main" {
Self::extract_main_children(&node, &self.xml_loader)
} else {
Vec::new()
};
NodeData {
node_name,
file,
roll,
text,
disable,
meta_type,
interpreter,
arg,
order,
file_name,
owner,
perms,
children_data,
}
})
.collect();
// 遍历节点数据此时不再持有xml_loader的借用
for data in node_data {
// 记录遍历顺序
self.context.order.push((data.file.clone(), data.roll.clone()));
// 根据节点类型处理
match data.node_name.as_str() {
"main" => self.handle_main_node_data(&data)?,
"package" => self.handle_package_node_data(&data)?,
"pre" => self.handle_pre_node_data(&data)?,
"post" => self.handle_post_node_data(&data)?,
"configure" => self.handle_configure_node_data(&data)?,
"boot" => self.handle_boot_node_data(&data)?,
"debug" => self.context.debug.push(data.text),
"file" => self.handle_file_node_data(&data)?,
_ => {
// 其他节点记录到debug
self.context.debug.push(format!(
"未处理的节点类型: {} (文本: {})",
data.node_name, data.text
));
}
}
}
println!("✅ XML解析完成共处理{}个节点", self.context.order.len());
Ok(())
}
/// 提取main节点的子节点数据
fn extract_main_children<'a, 'b>(node: &Node<'a, 'b>, xml_loader: &XmlLoader) -> Vec<MainChildData> {
let mut children_data = Vec::new();
for child in node.children() {
if child.node_type() != roxmltree::NodeType::Element {
continue;
}
let child_name = child.tag_name().name().to_string();
let child_text = xml_loader.get_node_text(&child);
let partition = child.attribute("partition").unwrap_or_default().to_string();
children_data.push(MainChildData {
name: child_name,
text: child_text,
partition,
});
}
children_data
}
/// 生成Kickstart脚本内容
/// 参数format - 输出格式text/json
pub fn generate(&self, format: &str) -> Result<String> {
match format.to_lowercase().as_str() {
"text" => self.generate_text(),
"json" => self.generate_json(),
_ => Err(anyhow::anyhow!("不支持的输出格式: {}仅支持text/json", format)),
}
}
/// 生成纯文本格式的Kickstart脚本
fn generate_text(&self) -> Result<String> {
let mut content = String::new();
// 1. 生成头部注释
content.push_str(&format!(
"# SUNHPC Kickstart脚本自动生成
# 节点: {}
# Linux发行版: {}
# Linux版本: {}
# 生成时间: autoGen
\n",
self.node_name,
self.config_loader.global.linux_distro,
self.config_loader.global.linux_version
));
// 2. 生成main部分Kickstart核心配置
content.push_str("# ========== 核心配置 ==========\n");
for line in &self.context.main {
content.push_str(line);
content.push('\n');
}
content.push('\n');
// 3. 生成包管理部分
content.push_str("# ========== 包配置 ==========\n");
if !self.context.rpms_on.is_empty() || !self.context.rpms_off.is_empty() {
content.push_str("%packages --ignoremissing\n");
// 启用的包
for rpm in &self.context.rpms_on {
content.push_str(rpm);
content.push('\n');
}
// 禁用的包(前缀-
for rpm in &self.context.rpms_off {
content.push_str(&format!("-{}\n", rpm));
}
content.push_str("%end\n\n");
}
// 4. 生成pre脚本部分
content.push_str("# ========== Pre安装脚本 ==========\n");
for pre in &self.context.pre {
content.push_str(&format!("%pre --log=/tmp/ks-pre.log {}\n", pre[0]));
content.push_str(&pre[1]);
content.push_str("\n%end\n\n");
}
// 5. 生成post脚本部分
content.push_str("# ========== Post安装脚本 ==========\n");
let log_path = "/mnt/sysimage/var/log/sunhpc-install.log";
for post in &self.context.post {
content.push_str(&format!("%post --log={} {}\n", log_path, post[0]));
content.push_str(&post[1]); // 解释器(#!/bin/bash
content.push_str("\n");
content.push_str(&post[2]); // 脚本内容
content.push_str("\n%end\n\n");
}
// 6. 生成boot脚本部分
content.push_str("# ========== Boot配置脚本 ==========\n");
content.push_str(&format!("%post --log={}\n", log_path));
content.push_str("cat >> /etc/sysconfig/sunhpc-pre << EOF\n");
for line in &self.context.boot_pre {
content.push_str(line);
content.push('\n');
}
content.push_str("EOF\n\n");
content.push_str("cat >> /etc/sysconfig/sunhpc-post << EOF\n");
for line in &self.context.boot_post {
content.push_str(line);
content.push('\n');
}
content.push_str("EOF\n%end\n");
// 7. 生成debug信息如果有
if !self.context.debug.is_empty() {
content.push_str("\n# ========== Debug信息 ==========\n");
content.push_str("# 以下为调试信息,实际部署时可删除\n");
for debug in &self.context.debug {
content.push_str(&format!("# {}\n", debug));
}
}
Ok(content)
}
/// 生成JSON格式的Kickstart上下文调试用
fn generate_json(&self) -> Result<String> {
let json = serde_json::to_string_pretty(&self.context)
.with_context(|| "将Kickstart上下文序列化为JSON失败")?;
Ok(json)
}
// ------------------------------
// 以下为节点处理方法按XML节点类型
// ------------------------------
/// 处理<main>节点Kickstart核心配置
fn handle_main_node_data(&mut self, data: &NodeData) -> Result<()> {
for child in &data.children_data {
// 根据不同子节点类型生成配置
match child.name.as_str() {
"clearpart" => {
// clearpart配置示例clearpart --all --initlabel
let mut clearpart_line = "clearpart".to_string();
if !child.partition.is_empty() {
clearpart_line.push_str(&format!(" --partition={}", child.partition));
}
if !child.text.is_empty() {
clearpart_line.push_str(&format!(" {}", child.text));
}
self.context.main.push(clearpart_line);
}
"bootloader" | "lilo" => {
// 引导加载器配置
self.context.main.push(format!("bootloader {}", child.text));
}
"lang" => {
// 语言配置
self.context.main.push(format!("lang {}", child.text));
}
"keyboard" => {
// 键盘配置
self.context.main.push(format!("keyboard {}", child.text));
}
"timezone" => {
// 时区配置
self.context.main.push(format!("timezone {}", child.text));
}
"rootpw" => {
// root密码配置
self.context.main.push(format!("rootpw {}", child.text));
}
"network" => {
// 网络配置
self.context.main.push(format!("network {}", child.text));
}
"part" | "volgroup" | "logvol" | "raid" => {
// 分区配置
self.context.main.push(format!("{} {}", child.name, child.text));
}
"reboot" | "poweroff" => {
// 安装后动作
self.context.main.push(child.name.clone());
}
_ => {
// 其他主节点:直接拼接
self.context.main.push(format!("{} {}", child.name, child.text));
}
}
}
Ok(())
}
/// 处理<package>节点RPM包配置
fn handle_package_node_data(&mut self, data: &NodeData) -> Result<()> {
let package_name = data.text.trim();
if package_name.is_empty() {
return Ok(());
}
// 构建包名(元包前缀@
let rpm_name = if data.meta_type {
format!("@{}", package_name)
} else {
package_name.to_string()
};
// 去重集合(避免重复添加)
let mut rpms_on_set: HashSet<String, RandomState> = self.context.rpms_on.clone()
.into_iter()
.collect();
let mut rpms_off_set: HashSet<String, RandomState> = self.context.rpms_off.clone()
.into_iter()
.collect();
if data.disable {
// 禁用的包:不在启用列表才添加到禁用列表
if !rpms_on_set.contains(&rpm_name) {
rpms_off_set.insert(rpm_name.clone());
}
rpms_on_set.remove(&rpm_name);
} else {
// 启用的包:添加到启用列表,移除禁用列表
rpms_on_set.insert(rpm_name.clone());
rpms_off_set.remove(&rpm_name);
}
// 将临时HashSet转回Vec覆盖原数据可变借用
self.context.rpms_on = rpms_on_set.into_iter().collect();
self.context.rpms_off = rpms_off_set.into_iter().collect();
Ok(())
}
/// 处理<pre>节点(安装前脚本)
fn handle_pre_node_data(&mut self, data: &NodeData) -> Result<()> {
// 构建pre脚本参数
let pre_args = format!(
"--interpreter {} {}",
data.interpreter,
data.arg
).trim().to_string();
// 添加到pre列表
self.context.pre.push(vec![pre_args, data.text.clone()]);
Ok(())
}
/// 处理<post>节点(安装后脚本)
fn handle_post_node_data(&mut self, data: &NodeData) -> Result<()> {
// 构建post脚本项参数、解释器、内容
let post_item = vec![
data.arg.clone(),
format!("#!{}", data.interpreter),
data.text.clone(),
];
// 添加到post列表
self.context.post.push(post_item);
Ok(())
}
/// 处理<configure>节点配置脚本转为post脚本
fn handle_configure_node_data(&mut self, data: &NodeData) -> Result<()> {
// configure节点等价于post节点
self.handle_post_node_data(data)
}
/// 处理<configure>节点配置脚本转为post脚本- 保留以兼容
#[allow(dead_code)]
fn handle_configure_node<'a, 'b>(&mut self, node: &Node<'a, 'b>) -> Result<()> {
// configure节点等价于post节点保留这个方法以兼容
let interpreter = node.attribute("interpreter").unwrap_or("/bin/bash");
let arg = node.attribute("arg").unwrap_or_default();
let content = self.xml_loader.get_node_text(node);
// 构建post脚本项参数、解释器、内容
let post_item = vec![
arg.to_string(),
format!("#!{}", interpreter),
content,
];
// 添加到post列表
self.context.post.push(post_item);
Ok(())
}
/// 处理<boot>节点(启动脚本)
fn handle_boot_node_data(&mut self, data: &NodeData) -> Result<()> {
match data.order.as_str() {
"pre" => self.context.boot_pre.push(data.text.clone()),
"post" => self.context.boot_post.push(data.text.clone()),
_ => {
// 未知顺序同时添加到pre和post
self.context.boot_pre.push(data.text.clone());
self.context.boot_post.push(data.text.clone());
}
}
Ok(())
}
/// 处理<debug>节点(调试信息)
#[allow(dead_code)]
fn handle_debug_node<'a, 'b>(&mut self, node: &Node<'a, 'b>) -> Result<()> {
// 保留这个方法以兼容
let content = self.xml_loader.get_node_text(node);
self.context.debug.push(content);
Ok(())
}
/// 处理<file>节点(文件配置)
fn handle_file_node_data(&mut self, data: &NodeData) -> Result<()> {
if data.file_name.is_empty() {
return Ok(());
}
// 记录RCS文件配置
let file_path = PathBuf::from(&data.file_name);
self.context.rcs_files.insert(file_path, (data.owner.clone(), data.perms.clone()));
// 生成文件创建脚本添加到post
let post_content = format!(
r#"
# 创建文件: {}
mkdir -p $(dirname {})
cat > {} << EOF
->{}
EOF
chown {} {}
chmod {} {}
"#,
data.file_name, data.file_name, data.file_name, data.text, data.owner, data.file_name, data.perms, data.file_name
);
// 添加到post脚本
self.context.post.push(vec![
"".to_string(),
"#!/bin/bash".to_string(),
post_content,
]);
Ok(())
}
/// 处理<file>节点(文件配置)
#[allow(dead_code)]
fn handle_file_node<'a, 'b>(&mut self, node: &Node<'a, 'b>) -> Result<()> {
// 提取文件属性
let file_name = node.attribute("name").unwrap_or_default();
let owner = node.attribute("owner").unwrap_or("root");
let perms = node.attribute("perms").unwrap_or("0644");
let content = self.xml_loader.get_node_text(node);
if file_name.is_empty() {
return Ok(());
}
// 记录RCS文件配置
let file_path = PathBuf::from(file_name);
self.context.rcs_files.insert(file_path, (owner.to_string(), perms.to_string()));
// 生成文件创建脚本添加到post
let post_content = format!(
r#"
# 创建文件: {}
mkdir -p $(dirname {})
cat > {} << EOF
{}
EOF
chown {} {}
chmod {} {}
"#,
file_name, file_name, file_name, content, owner, file_name, perms, file_name
);
// 添加到post脚本
self.context.post.push(vec![
"".to_string(),
"#!/bin/bash".to_string(),
post_content,
]);
Ok(())
}
}

View File

@@ -0,0 +1,322 @@
//! 配置加载器模块
//! 功能:
//! 1. 解析TOML/YAML格式的主配置+分组配置
//! 2. 处理节点继承关系(子节点合并父节点变量)
//! 3. 管理Linux发行版/版本配置
//! 4. 收集XML文件路径转为绝对路径
use anyhow::{Context, Result};
use serde::{Deserialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use yaml_rust::{Yaml, YamlLoader};
/// 全局配置结构体对应main.toml的根节点
#[derive(Debug, Deserialize, Clone)]
pub struct GlobalConfig {
/// 全局通用变量(所有节点共享)
#[serde(default)]
pub variables: HashMap<String, String>,
/// 节点配置key=节点名value=节点具体配置)
#[serde(default)]
pub nodes: HashMap<String, NodeConfig>,
/// XML配置文件/目录路径列表
#[serde(default)]
pub xml_paths: Vec<PathBuf>,
/// Linux发行版如centos/rhel/ubuntu
#[serde(default = "default_linux_distro")]
pub linux_distro: String,
/// Linux版本如8/9/22.04
#[serde(default = "default_linux_version")]
pub linux_version: String,
}
/// 节点配置结构体(支持继承)
#[derive(Debug, Deserialize, Clone)]
pub struct NodeConfig {
/// 继承的父节点名称(可选)
#[serde(default)]
pub inherit: Option<String>,
/// 节点架构x86_64/aarch64等
#[serde(default = "default_arch")]
pub arch: String,
/// Linux发行版覆盖全局配置
#[serde(default)]
pub linux_distro: String,
/// Linux版本覆盖全局配置
#[serde(default)]
pub linux_version: String,
/// 节点私有变量(覆盖全局变量)
#[serde(default)]
pub variables: HashMap<String, String>,
}
/// 配置加载器核心结构体
pub struct ConfigLoader {
/// 加载完成的全局配置
pub global: GlobalConfig,
/// 原始配置文件路径
#[allow(dead_code)]
config_path: PathBuf,
}
// 默认值函数
fn default_arch() -> String {
"x86_64".to_string()
}
fn default_linux_distro() -> String {
"centos".to_string()
}
fn default_linux_version() -> String {
"8".to_string()
}
impl ConfigLoader {
/// 创建配置加载器并加载配置文件
/// 参数config_path - 主配置文件路径TOML/YAML
pub fn new(config_path: &Path) -> Result<Self> {
// 1. 读取配置文件内容
let config_str = fs::read_to_string(config_path)
.with_context(|| format!("无法读取配置文件: {}", config_path.display()))?;
// 2. 解析配置优先TOML兼容YAML
let mut global: GlobalConfig = match toml::from_str(&config_str) {
Ok(cfg) => cfg,
Err(toml_err) => {
eprintln!("TOML解析失败: {}", toml_err);
// TOML解析失败尝试YAML
let yaml_docs = YamlLoader::load_from_str(&config_str)
.with_context(|| "配置文件不是有效的TOML或YAML格式")?;
if yaml_docs.is_empty() {
return Err(anyhow::anyhow!("YAML配置文件为空"))
.context("解析Yaml后未获取到任何信息");
}
Self::yaml_to_global(&yaml_docs[0])?
}
};
// 3. 合并分组配置groups目录下的compute/storage等
Self::merge_group_configs(&mut global, config_path.parent().unwrap())?;
// 4. 处理节点继承关系(子节点合并父节点变量)
Self::process_node_inheritance(&mut global)?;
// 5. 标准化XML路径转为绝对路径
global.xml_paths = global.xml_paths
.iter()
.map(|p| {
if p.is_absolute() {
p.clone()
} else {
config_path.parent().unwrap().join(p)
}
})
.collect();
Ok(Self {
global,
config_path: config_path.to_path_buf(),
})
}
/// 将YAML对象转换为GlobalConfig兼容逻辑
fn yaml_to_global(yaml: &Yaml) -> Result<GlobalConfig> {
let mut global = GlobalConfig {
variables: HashMap::new(),
nodes: HashMap::new(),
xml_paths: Vec::new(),
linux_distro: default_linux_distro(),
linux_version: default_linux_version(),
};
// 解析全局变量
if let Yaml::Hash(vars_hash) = &yaml["variables"] {
for (k, v) in vars_hash {
if let (Yaml::String(k_str), Yaml::String(v_str)) = (k, v) {
global.variables.insert(k_str.clone(), v_str.clone());
}
}
}
// 解析节点配置
if let Yaml::Hash(nodes_hash) = &yaml["nodes"] {
for (node_name, node_yaml) in nodes_hash {
let node_name_str = node_name
.as_str()
.with_context(|| "节点名称必须是字符串")?
.to_string();
// 解析单个节点配置
let inherit = node_yaml["inherit"].as_str().map(|s| s.to_string());
let arch = node_yaml["arch"].as_str().unwrap_or(&default_arch()).to_string();
let linux_distro = node_yaml["linux_distro"]
.as_str()
.unwrap_or(&default_linux_distro())
.to_string();
let linux_version = node_yaml["linux_version"]
.as_str()
.unwrap_or(&default_linux_version())
.to_string();
// 解析节点私有变量
let mut node_vars = HashMap::new();
if let Yaml::Hash(vars_hash) = &node_yaml["variables"] {
for (k, v) in vars_hash {
if let (Yaml::String(k_str), Yaml::String(v_str)) = (k, v) {
node_vars.insert(k_str.clone(), v_str.clone());
}
}
}
// 添加到节点列表
global.nodes.insert(
node_name_str,
NodeConfig {
inherit,
arch,
linux_distro,
linux_version,
variables: node_vars,
},
);
}
}
// 解析XML路径
if let Yaml::Array(xml_paths_arr) = &yaml["xml_paths"] {
for p in xml_paths_arr {
if let Yaml::String(p_str) = p {
global.xml_paths.push(PathBuf::from(p_str));
}
}
}
// 解析Linux发行版/版本
if let Yaml::String(distro) = &yaml["linux_distro"] {
global.linux_distro = distro.clone();
}
if let Yaml::String(version) = &yaml["linux_version"] {
global.linux_version = version.clone();
}
Ok(global)
}
/// 合并groups目录下的分组配置compute/storage/gpu/common
fn merge_group_configs(global: &mut GlobalConfig, parent_dir: &Path) -> Result<()> {
let groups_dir = parent_dir.join("groups");
if !groups_dir.exists() || !groups_dir.is_dir() {
// 没有groups目录直接返回
return Ok(());
}
// 遍历groups目录下的所有TOML/YAML文件
for entry in fs::read_dir(groups_dir)? {
let entry = entry?;
let path = entry.path();
// 只处理文件且后缀为toml/yaml/yml
if !path.is_file() {
continue;
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if !["toml", "yaml", "yml"].contains(&ext) {
continue;
}
// 读取并解析分组配置
let group_str = fs::read_to_string(&path)?;
let group_config: GlobalConfig = if ext == "toml" {
toml::from_str(&group_str)?
} else {
let yaml_docs = YamlLoader::load_from_str(&group_str)?;
Self::yaml_to_global(&yaml_docs[0])?
};
// 合并配置(分组配置覆盖全局配置)
global.variables.extend(group_config.variables);
global.nodes.extend(group_config.nodes);
global.xml_paths.extend(group_config.xml_paths);
// 合并Linux发行版/版本(如果分组配置有值)
if !group_config.linux_distro.is_empty() {
global.linux_distro = group_config.linux_distro;
}
if !group_config.linux_version.is_empty() {
global.linux_version = group_config.linux_version;
}
}
Ok(())
}
/// 处理节点继承关系(子节点合并父节点的变量/配置)
fn process_node_inheritance(global: &mut GlobalConfig) -> Result<()> {
let nodes_clone = global.nodes.clone(); // 克隆一份用于读取父节点
for (node_name, node) in global.nodes.iter_mut() {
// 如果节点有继承的父节点
if let Some(parent_name) = &node.inherit {
let parent_node = nodes_clone
.get(parent_name)
.with_context(|| format!("节点{}继承的父节点{}不存在", node_name, parent_name))?;
// 1. 合并变量:父节点变量 → 子节点变量(子节点覆盖父节点)
let mut parent_vars = parent_node.variables.clone();
parent_vars.extend(node.variables.clone());
node.variables = parent_vars;
// 2. 继承架构(子节点未设置则使用父节点)
if node.arch == default_arch() && parent_node.arch != default_arch() {
node.arch = parent_node.arch.clone();
}
// 3. 继承Linux发行版子节点未设置则使用父节点
if node.linux_distro == default_linux_distro() && parent_node.linux_distro != default_linux_distro() {
node.linux_distro = parent_node.linux_distro.clone();
}
// 4. 继承Linux版本子节点未设置则使用父节点
if node.linux_version == default_linux_version() && parent_node.linux_version != default_linux_version() {
node.linux_version = parent_node.linux_version.clone();
}
}
}
Ok(())
}
/// 获取指定节点的完整变量(全局变量 + 节点私有变量)
/// 参数node_name - 节点名称
pub fn get_node_vars(&self, node_name: &str) -> Result<HashMap<String, String>> {
let node = self.global.nodes.get(node_name)
.with_context(|| format!("节点{}不存在于配置文件中", node_name))?;
// 1. 复制全局变量
let mut vars = self.global.variables.clone();
// 2. 合并节点私有变量(覆盖全局变量)
vars.extend(node.variables.clone());
// 3. 注入节点内置变量
vars.insert("arch".to_string(), node.arch.clone());
vars.insert("linux_distro".to_string(), node.linux_distro.clone());
vars.insert("linux_version".to_string(), node.linux_version.clone());
vars.insert("node_name".to_string(), node_name.to_string());
Ok(vars)
}
}

View File

@@ -0,0 +1,5 @@
// 配置相关模块:加载、解析、合并 TOML 配置文件
pub mod loader;
pub mod xml_parser;
pub mod ksgen;

View File

@@ -0,0 +1,226 @@
//! XML解析器模块
//! 功能:
//! 1. 加载指定目录/文件下的所有XML文件
//! 2. 实现NodeFilter逻辑过滤符合Linux发行版/版本/架构的节点)
//! 3. 提供XML节点遍历和文本提取功能
use anyhow::{Context, Result};
use roxmltree::{Document, Node, NodeType};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
/// XML节点过滤器复刻gen.py的NodeFilter
/// 用于过滤符合条件的XML节点架构/发行版/版本/阶段)
#[derive(Debug, Clone)]
pub struct XmlNodeFilter {
/// 过滤条件arch/linux_distro/linux_version等
filter_attrs: HashMap<String, String>,
/// 阶段过滤pre/post默认全部
phases: HashSet<String>,
}
impl XmlNodeFilter {
/// 创建过滤器
/// 参数filter_attrs - 过滤属性arch/linux_distro/linux_version
pub fn new(filter_attrs: HashMap<String, String>) -> Self {
// 默认包含pre和post阶段
let mut phases = HashSet::new();
phases.insert("pre".to_string());
phases.insert("post".to_string());
Self {
filter_attrs,
phases,
}
}
/// 设置阶段过滤(仅保留指定阶段)
/// 参数phases - 阶段列表(如["pre"]或["post"]
#[allow(dead_code)]
pub fn set_phases(&mut self, phases: &[&str]) {
self.phases = phases.iter().map(|s| s.to_string()).collect();
}
/// 检查节点是否符合过滤条件(核心逻辑)
/// 对应gen.py的isCorrectCond
pub fn is_node_match(&self, node: &Node) -> bool {
// 非元素节点直接过滤
if node.node_type() != NodeType::Element {
return false;
}
// 1. 提取节点属性
let node_arch = node.attribute("arch").unwrap_or_default();
let node_distro = node.attribute("linux_distro").unwrap_or_default();
let node_version = node.attribute("linux_version").unwrap_or_default();
let node_phase = node.attribute("phase").unwrap_or_default();
// 2. 阶段过滤(支持逗号分隔的多个阶段)
let node_phases: HashSet<String> = node_phase
.split(',')
.map(|s| s.trim().to_lowercase())
.filter(|s| !s.is_empty())
.collect();
// 如果节点指定了阶段,但与过滤器阶段无交集 → 过滤
if !node_phases.is_empty() && node_phases.intersection(&self.phases).next().is_none() {
return false;
}
// 3. 架构过滤节点指定了arch但不匹配 → 过滤)
if !node_arch.is_empty() && node_arch != self.filter_attrs.get("arch").map(|s| s.as_str()).unwrap_or("") {
return false;
}
// 4. 发行版过滤节点指定了linux_distro但不匹配 → 过滤)
if !node_distro.is_empty() && node_distro != self.filter_attrs.get("linux_distro").map(|s|s.as_str()).unwrap_or("") {
return false;
}
// 5. 版本过滤节点指定了linux_version但不匹配 → 过滤)
if !node_version.is_empty() && node_version != self.filter_attrs.get("linux_version").map(|s|s.as_str()).unwrap_or("") {
return false;
}
// 所有条件都满足
true
}
/// 检查节点是否是Linux Kickstart允许的主节点
/// 对应gen.py的MainNodeFilter_linux
pub fn is_linux_main_node(&self, node: &Node) -> bool {
if !self.is_node_match(node) {
return false;
}
// 允许的主节点列表Kickstart核心节点
let allowed_main_nodes = [
"kickstart", "include", "main", "auth", "clearpart", "device", "driverdisk",
"install", "nfs", "cdrom", "interactive", "harddrive", "url", "keyboard",
"lang", "langsupport", "lilo", "bootloader", "mouse", "network", "part",
"volgroup", "logvol", "raid", "reboot", "rootpw", "skipx", "text",
"timezone", "upgrade", "xconfig", "zerombr"
];
allowed_main_nodes.contains(&node.tag_name().name())
}
/// 检查节点是否是Linux Kickstart允许的其他节点
/// 对应gen.py的OtherNodeFilter_linux
pub fn is_linux_other_node(&self, node: &Node) -> bool {
if !self.is_node_match(node) {
return false;
}
// 允许的其他节点列表(包/脚本/配置相关)
let allowed_other_nodes = [
"attributes", "debug", "description", "package", "pre", "post",
"boot", "configure", "file"
];
allowed_other_nodes.contains(&node.tag_name().name())
}
}
/// XML加载器加载所有XML文件并提供遍历接口
#[derive(Debug)]
pub struct XmlLoader{
/// 加载的所有XML文档
pub docs: Vec<Document<'static>>,
/// 节点过滤器
pub filter: XmlNodeFilter,
}
impl XmlLoader {
/// 创建XML加载器
/// 参数:
/// - filter_attrs: 过滤属性arch/linux_distro/linux_version
/// - xml_paths: XML文件/目录路径列表
pub fn new(filter_attrs: HashMap<String, String>, xml_paths: &[PathBuf]) -> Result<Self> {
let mut docs = Vec::new();
// 遍历所有XML路径文件/目录)
for path in xml_paths {
if path.is_dir() {
// 目录:遍历所有.xml文件
docs.extend(Self::load_xml_dir(path)?);
} else if path.is_file() && path.extension().and_then(|e| e.to_str()) == Some("xml") {
// 文件:直接加载
let doc = Self::load_xml_file(path)?;
docs.push(doc);
}
}
// 创建过滤器
let filter = XmlNodeFilter::new(filter_attrs);
Ok(Self { docs, filter})
}
/// 加载目录下的所有XML文件
fn load_xml_dir(dir: &Path) -> Result<Vec<Document<'static>>> {
let mut docs = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
// 只处理.xml文件
if path.is_file() && path.extension().and_then(|e| e.to_str()) == Some("xml") {
let doc = Self::load_xml_file(&path)?;
docs.push(doc);
}
}
Ok(docs)
}
/// 加载单个XML文件
fn load_xml_file(path: &Path) -> Result<Document<'static>> {
// 读取文件内容
let xml_str = fs::read_to_string(path)
.with_context(|| format!("读取XML文件失败: {}", path.display()))?;
// 解析XML为DOM文档
let xml_static: &'static str = Box::leak(xml_str.into_boxed_str());
let doc = Document::parse(xml_static)
.with_context(|| format!("解析XML文件失败: {}", path.display()))?;
Ok(doc)
}
/// 遍历所有符合条件的Linux节点
pub fn iter_linux_nodes(&self) -> impl Iterator<Item = Node<'_, '_>> {
self.docs.iter()
.flat_map(|doc| doc.descendants()) // 遍历所有后代节点
.filter(|node| {
// 过滤出主节点或其他节点
self.filter.is_linux_main_node(node) || self.filter.is_linux_other_node(node)
})
}
/// 获取节点的所有文本内容(包括子节点)
pub fn get_node_text<'a, 'b>(&self, node: &Node<'a, 'b>) -> String {
let mut text = String::new();
for child in node.children() {
match child.node_type() {
// 文本节点:直接追加
NodeType::Text => {
if let Some(t) = child.text() {
text.push_str(t);
}
}
// 元素节点:递归获取文本
NodeType::Element => {
text.push_str(&self.get_node_text(&child));
}
// 其他节点:忽略
_ => {}
}
}
// 去除首尾空白,替换多空格为单空格
text.trim().replace(&['\n', '\r', '\t'][..], " ").replace(" ", " ")
}
}

View File

@@ -0,0 +1,52 @@
use anyhow::{Result};
use std::time::Duration;
/// 模拟数据库配置结构
#[derive(Debug, Clone)]
pub struct DbConfig {
pub url: String,
pub max_connections: u32,
}
/// 核心逻辑:初始化数据库
/// 这个函数不关心参数从哪里来CLI、配置文件、环境变量
pub async fn init_db(config: &DbConfig) -> Result<()> {
println!("🔌 正在连接数据库: {}", config.url);
// 模拟网络延迟
tokio::time::sleep(Duration::from_millis(500)).await;
// 模拟连接逻辑
if config.url.contains("error") {
anyhow::bail!("无法连接到数据库URL 无效");
}
println!("✅ 连接成功!最大连接数设置为: {}", config.max_connections);
// 模拟执行初始化脚本
run_migrations().await?;
println!("🎉 数据库初始化完成!");
Ok(())
}
/// 核心逻辑:检查连接状态
#[allow(dead_code)]
pub async fn check_connection(url: &str) -> Result<bool> {
println!("🏓 正在 Ping 数据库: {}", url);
tokio::time::sleep(Duration::from_millis(200)).await;
if url.contains("error") {
return Ok(false);
}
Ok(true)
}
/// 内部辅助函数:运行迁移
async fn run_migrations() -> Result<()> {
println!("🚀 正在运行数据迁移脚本...");
tokio::time::sleep(Duration::from_millis(300)).await;
println!(" - 创建表 users");
println!(" - 创建表 logs");
Ok(())
}

View File

@@ -0,0 +1,8 @@
// 公开具体的实现文件
pub mod database;
// 重新导出常用函数,方便 commands 层调用
// 例如use crate::internal::database::init_db;
pub use database::init_db;
//pub use database::check_connection;
pub use database::DbConfig;

5
src/internal/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
// 公开 database 模块供外部commands使用
pub mod database;
pub mod config;
pub mod network;

238
src/internal/network/ip.rs Normal file
View File

@@ -0,0 +1,238 @@
use std::fmt;
use std::error::Error;
// 自定义错误类型(实现 Error trait 方便上层处理)
#[derive(Debug)]
pub enum IPError {
InvalidAddressFormat,
OutOfRange(String),
NonUnicastAddress,
ParseError(String),
}
impl fmt::Display for IPError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IPError::InvalidAddressFormat => write!(f, "Invalid IP address format (must be x.x.x.x)"),
IPError::OutOfRange(msg) => write!(f, "IP address out of range: {}", msg),
IPError::NonUnicastAddress => write!(f, "Not a unicast IP address (invalid network type)"),
IPError::ParseError(msg) => write!(f, "IP parse error: {}", msg),
}
}
}
impl Error for IPError {}
// IP 地址核心结构体IPv4
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IPAddr(u32);
impl IPAddr {
/// 从字符串创建 IP 地址(如 "192.168.1.1"
pub fn from_str(s: &str) -> Result<Self, IPError> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 4 {
return Err(IPError::InvalidAddressFormat);
}
let mut octets = [0u8; 4];
for (i, part) in parts.iter().enumerate() {
octets[i] = part.parse().map_err(|e| {
IPError::ParseError(format!("Failed to parse octet {}: {}", part, e))
})?;
}
// 转换为 u32大端序第一个 octet 是最高位)
let addr = ((octets[0] as u32) << 24)
| ((octets[1] as u32) << 16)
| ((octets[2] as u32) << 8)
| (octets[3] as u32);
Ok(Self(addr))
}
/// 从 u32 数值创建 IP 地址
#[allow(dead_code)]
pub fn from_u32(addr: u32) -> Self {
Self(addr)
}
/// 获取 u32 格式的地址
pub fn to_u32(&self) -> u32 {
self.0
}
/// 位运算:与
pub fn bitwise_and(&self, other: &Self) -> Self {
Self(self.0 & other.0)
}
/// 位运算:或
#[allow(dead_code)]
pub fn bitwise_or(&self, other: &Self) -> Self {
Self(self.0 | other.0)
}
/// 位运算:非
pub fn bitwise_not(&self) -> Self {
Self(!self.0)
}
/// 地址加法(支持负数)
pub fn add(&self, n: i32) -> Self {
if n >= 0 {
Self(self.0 + n as u32)
} else {
Self(self.0 - n.abs() as u32)
}
}
}
// 实现 Display 以便格式化输出
impl fmt::Display for IPAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}.{}.{}.{}",
(self.0 >> 24) & 0xFF,
(self.0 >> 16) & 0xFF,
(self.0 >> 8) & 0xFF,
self.0 & 0xFF
)
}
}
// IP 生成器核心结构体
#[derive(Debug)]
pub struct IPGenerator {
network: IPAddr,
netmask: IPAddr,
addr: IPAddr,
}
impl IPGenerator {
/// 创建 IP 生成器实例
/// - network: 网络地址(如 "10.1.1.0"
/// - netmask: 子网掩码None 则自动推断 A/B/C 类)
pub fn new(network: &str, netmask: Option<&str>) -> Result<Self, IPError> {
let network_addr = IPAddr::from_str(network)?;
let netmask_addr = match netmask {
Some(addr_str) => IPAddr::from_str(addr_str)?,
None => Self::infer_netmask(&network_addr)?,
};
// 初始IP = 网络地址 + 1(第一个可用地址)
let initial_addr = network_addr.add(1);
// 效验初始 IP 是否在合法范围
Self::validate_ip_in_range(&initial_addr, &network_addr, &netmask_addr)?;
Ok(Self {
network: network_addr,
netmask: netmask_addr,
addr: initial_addr,
})
}
/// 自动推断子网掩码A/B/C 类)
fn infer_netmask(addr: &IPAddr) -> Result<IPAddr, IPError> {
let first_octet = (addr.0 >> 24) & 0xFF;
match first_octet {
// A 类: 0-127
0..=127 => Ok(IPAddr::from_str("255.0.0.0")?),
// B 类: 128-191
128..=191 => Ok(IPAddr::from_str("255.255.0.0")?),
// C 类: 192-223
192..=223 => Ok(IPAddr::from_str("255.255.255.0")?),
// 非单播地址
_ => Err(IPError::NonUnicastAddress),
}
}
// 校验 IP 是否在网段合法范围(非网络/广播地址)
fn validate_ip_in_range(ip: &IPAddr, network: &IPAddr, netmask: &IPAddr) -> Result<(), IPError> {
let inverted_netmask = netmask.bitwise_not();
let net_addr = network.bitwise_and(netmask).to_u32();
let ip_addr = ip.to_u32();
let broadcast_addr = net_addr | inverted_netmask.to_u32();
// 检查是否是网络地址
if ip_addr == net_addr {
return Err(IPError::OutOfRange("IP is network address".to_string()));
}
// 检查是否是广播地址
if ip_addr == broadcast_addr {
return Err(IPError::OutOfRange("IP is broadcast address".to_string()));
}
// 检查是否在网段内
if (ip_addr & netmask.to_u32()) != net_addr {
return Err(IPError::OutOfRange("IP is not in network range".to_string()));
}
Ok(())
}
// 获取当前 IP带校验
pub fn curr(&self) -> Result<IPAddr, IPError> {
Self::validate_ip_in_range(&self.addr, &self.network, &self.netmask)?;
Ok(self.addr)
}
// 动态计算最大可用步长(根据子网掩码)
pub fn get_max_available_step(&self) -> u32 {
let inverted_netmask = self.netmask.bitwise_not().to_u32();
// 可用 IP 数量 = 主机位总数 - 2排除网络/广播地址)
let total_available = inverted_netmask - 1; // 减 1 是因为初始 IP 已经是 +1 了
total_available as u32
}
// 获取子网掩码的字符串形式
pub fn get_netmask_str(&self) -> String {
format!("{}", self.netmask)
}
/// 获取网络地址(如 10.1.1.0/25 对应 10.1.1.0
pub fn get_network(&self) -> String {
format!("{}", self.addr.bitwise_and(&self.netmask))
}
// 偏移 IP仅支持正数步长
pub fn next(&mut self, n: i32) -> Result<IPAddr, IPError> {
// 强制转为正数(禁用负数偏移)
let step = if n < 0 { 0 } else { n as u32 };
let new_addr = self.addr.add(step as i32);
// 校验新 IP 是否合法
Self::validate_ip_in_range(&new_addr, &self.network, &self.netmask)?;
self.addr = new_addr;
Ok(self.addr)
}
/// 地址递减(等价于 next(-1)
#[allow(dead_code)]
pub fn dec(&mut self) -> Result<IPAddr, IPError> {
self.next(-1)
}
}
// 测试用例(可选)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ip_from_str() {
let ip = IPAddr::from_str("10.1.1.0").unwrap();
assert_eq!(ip.to_u32(), 0x0a010100);
}
#[test]
fn test_ip_generator() {
let mut generator = IPGenerator::new("10.1.1.0", Some("255.255.255.128")).unwrap();
assert_eq!(generator.curr().unwrap().to_string(), "10.1.1.127");
generator.next(-126).unwrap();
assert_eq!(generator.curr().unwrap().to_string(), "10.1.1.1");
}
}

View File

@@ -0,0 +1,3 @@
pub mod ip;
pub use ip::IPGenerator;

View File

@@ -1,13 +1,12 @@
use clap::Parser; use clap::Parser;
use anyhow::Result; use anyhow::Result;
// 引入 commands 模块 mod commands; // 引入 commands 模块
mod commands; mod internal; // 引入 internal 模块
mod utils; // 假设有通用工具 mod utils; // 引入 utils 通用工具模块
use commands::CliCommands; use commands::CliCommands;
/// 我的超级 CLI 工具
#[derive(Parser)] #[derive(Parser)]
#[command(name = "sunhpc")] #[command(name = "sunhpc")]
#[command(author = "Qichao.Sun")] #[command(author = "Qichao.Sun")]
@@ -23,7 +22,8 @@ struct Cli {
command: CliCommands, command: CliCommands,
} }
fn main() -> Result<()> { #[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
if cli.debug { if cli.debug {
@@ -33,7 +33,9 @@ fn main() -> Result<()> {
// 根据子命令分发逻辑 // 根据子命令分发逻辑
match cli.command { match cli.command {
CliCommands::Server(args) => commands::server::execute(args)?, CliCommands::Server(args) => commands::server::execute(args)?,
CliCommands::Db(args) => commands::db::execute(args)?, CliCommands::Db(args) => commands::db::execute(args).await?,
CliCommands::Config(args) => commands::config::execute(args)?,
CliCommands::Report(args) => commands::report::execute(args)?,
// 未来扩展新命令时,只需在这里添加新的匹配臂 // 未来扩展新命令时,只需在这里添加新的匹配臂
// CliCommands::NewFeature(args) => commands::new_feature::execute(args)?, // CliCommands::NewFeature(args) => commands::new_feature::execute(args)?,
} }