Compare commits

..
21 Commits
Author SHA1 Message Date
Vincent Stuyck 1a4e35a60e make ApiClient<Rule>::read consistend with other read ops 2026-03-01 16:42:42 +01:00
Vincent Stuyck f8a336429a cargo fmt 2026-03-01 16:39:54 +01:00
Vincent Stuyck 357b81db80 add crud operations for Rule 2026-03-01 16:37:33 +01:00
Vincent Stuyck a3b450736e add constructors and builder style methods (ai generated) 2026-03-01 16:13:35 +01:00
Vincent Stuyck cb93615a98 add rules and (de)/serialization, wip 2026-03-01 15:51:17 +01:00
Vincent Stuyck 804faa6273 add rulesets api 2026-02-28 19:22:06 +01:00
Vincent Stuyck 6de616e8ab cargo fmt 2026-02-28 17:56:26 +01:00
Vincent Stuyck 71b1d3411d add activation api 2026-02-28 16:58:43 +01:00
Vincent Stuyck e9e67358c4 groups: user domain_client marco instead of doing it manually 2026-02-28 16:34:00 +01:00
Vincent Stuyck f08b37cb7d go voer some ai feedback 2026-02-28 01:21:34 +01:00
Vincent Stuyck 7014b83ba2 update main for new api 2026-02-28 01:10:29 +01:00
Vincent Stuyck b1d696de84 update how the client is created with marcos instead of relying on
traits. this makes a cleaner api
2026-02-28 01:07:48 +01:00
Vincent Stuyck ed787b2099 add group api 2026-02-27 23:38:05 +01:00
Vincent Stuyck a72fc0a09f add hosttags 2026-02-27 23:09:39 +01:00
Vincent Stuyck c39315b2b7 rework client a little to make more low level methods available 2026-02-27 23:09:30 +01:00
Vincent Stuyck 96402420c1 add bones for change activations 2026-01-11 22:33:03 +01:00
Vincent Stuyck ae7643193a add tags to api 2026-01-11 17:15:20 +01:00
Vincent Stuyck 8413131ed4 create a marco to add groups 2026-01-10 18:47:23 +01:00
Vincent Stuyck 361fe1d2b5 add schemars feature 2025-12-20 23:25:48 +01:00
Vincent Stuyck 20d0c76e7a test and troubleshoot hostconfig 2025-12-20 16:37:48 +01:00
Vincent Stuyck 8e34f31810 fix typos 2025-12-20 13:22:00 +01:00
16 changed files with 2567 additions and 331 deletions
Generated
+385
View File
@@ -2,6 +2,35 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom 0.3.4",
"once_cell",
"serde",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@@ -40,18 +69,45 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "borrow-or-share"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.1" version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "bytecount"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.0" version = "1.11.0"
@@ -86,14 +142,17 @@ version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
"jsonschema",
"log", "log",
"reqwest", "reqwest",
"schemars",
"secrecy", "secrecy",
"serde", "serde",
"serde_json", "serde_json",
"simplelog", "simplelog",
"thiserror", "thiserror",
"tokio", "tokio",
"uuid",
] ]
[[package]] [[package]]
@@ -126,6 +185,12 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.5.5" version = "0.5.5"
@@ -146,6 +211,21 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "email_address"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -171,6 +251,17 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "fancy-regex"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
dependencies = [
"bit-set",
"regex-automata",
"regex-syntax",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@@ -183,12 +274,29 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "fluent-uri"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
dependencies = [
"borrow-or-share",
"ref-cast",
"serde",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@@ -213,6 +321,16 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fraction"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7"
dependencies = [
"lazy_static",
"num",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@@ -220,6 +338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@@ -228,6 +347,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.31" version = "0.3.31"
@@ -247,9 +372,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-io",
"futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab",
] ]
[[package]] [[package]]
@@ -303,6 +432,11 @@ name = "hashbrown"
version = "0.16.1" version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]] [[package]]
name = "http" name = "http"
@@ -592,6 +726,40 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "jsonschema"
version = "0.37.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73c9ffb2b5c56d58030e1b532d8e8389da94590515f118cf35b5cb68e4764a7e"
dependencies = [
"ahash",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"num-cmp",
"num-traits",
"percent-encoding",
"referencing",
"regex",
"regex-syntax",
"reqwest",
"serde",
"serde_json",
"unicode-general-category",
"uuid-simd",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.178" version = "0.2.178"
@@ -671,12 +839,82 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[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-cmp"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -745,6 +983,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "outref"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@@ -933,6 +1177,70 @@ dependencies = [
"bitflags", "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 = "referencing"
version = "0.37.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4283168a506f0dcbdce31c9f9cce3129c924da4c6bca46e46707fcb746d2d70c"
dependencies = [
"ahash",
"fluent-uri",
"getrandom 0.3.4",
"hashbrown",
"parking_lot",
"percent-encoding",
"serde_json",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.26" version = "0.12.26"
@@ -942,7 +1250,9 @@ dependencies = [
"base64", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-channel",
"futures-core", "futures-core",
"futures-util",
"h2", "h2",
"http", "http",
"http-body", "http-body",
@@ -1066,6 +1376,31 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "schemars"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
dependencies = [
"dyn-clone",
"ref-cast",
"schemars_derive",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301858a4023d78debd2353c7426dc486001bddc91ae31a76fb1f55132f7e2633"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -1135,6 +1470,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.145" version = "1.0.145"
@@ -1503,6 +1849,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.22" version = "1.0.22"
@@ -1533,12 +1885,45 @@ 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 = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
dependencies = [
"js-sys",
"serde_core",
"wasm-bindgen",
]
[[package]]
name = "uuid-simd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"vsimd",
]
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vsimd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
+6
View File
@@ -9,11 +9,17 @@ authors = [
[dependencies] [dependencies]
async-trait = "0.1.89" async-trait = "0.1.89"
chrono = { version = "0.4.42", features = ["serde"] } chrono = { version = "0.4.42", features = ["serde"] }
jsonschema = { version = "0.37.4", optional = true }
log = "0.4.29" log = "0.4.29"
reqwest = { version = "0.12.26", features = ["json", "rustls-tls"] } reqwest = { version = "0.12.26", features = ["json", "rustls-tls"] }
schemars = { version = "1.1.0", optional = true }
secrecy = { version = "0.10.3", features = ["serde"] } secrecy = { version = "0.10.3", features = ["serde"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145" serde_json = "1.0.145"
simplelog = "0.12.2" simplelog = "0.12.2"
thiserror = "2.0.17" thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.48.0", features = ["full"] }
uuid = { version = "1.21.0", features = ["serde"] }
[features]
schemars = ["dep:schemars"]
+188
View File
@@ -0,0 +1,188 @@
use std::fmt::Display;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::api::{DomainCollection, DomainExtension, DomainObject, domain_client, domain_read};
use crate::{ApiClient, Client, Result};
domain_client!(ActivationStatus, activation_api);
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ActivationStatus {
sites: Vec<String>,
is_running: bool,
force_foreign_changes: bool,
time_started: DateTime<Utc>,
changes: Vec<PendingChange>,
#[serde(default)]
status_per_site: Vec<SiteStatus>,
}
impl DomainExtension for ActivationStatus {
const DOMAIN_TYPE: super::DomainType = super::DomainType::ActivationRun;
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct PendingChange {
id: Uuid,
user_id: Option<String>,
action_name: String,
text: String,
time: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct SiteStatus {
site: String,
phase: ChangePhase,
state: ChangeState,
status_text: String,
status_details: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ChangePhase {
Initialized,
Queued,
Started,
Sync,
Activate,
Finishing,
Done,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ChangeState {
Success,
Error,
Warning,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ActivateChangeRequest {
redirect: bool,
sites: Vec<String>,
force_foreign_changes: bool,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ActivationOptions {
redirect: bool,
sites: Vec<String>,
force_foreign_changes: bool,
}
impl ActivationOptions {
pub fn set_redirect(&mut self, redirect: bool) {
self.redirect = redirect
}
pub fn with_redirect(mut self, redirect: bool) -> Self {
self.set_redirect(redirect);
self
}
pub fn set_force_foreign_changes(&mut self, force: bool) {
self.force_foreign_changes = force;
}
pub fn with_force_foreign_changes(mut self, force: bool) -> Self {
self.set_force_foreign_changes(force);
self
}
pub fn set_sites(&mut self, sites: Vec<String>) {
self.sites = sites;
}
pub fn with_sites(mut self, sites: Vec<String>) -> Self {
self.set_sites(sites);
self
}
pub fn set_site(&mut self, site: String) {
self.sites.push(site);
}
pub fn with_site(mut self, site: String) -> Self {
self.set_site(site);
self
}
}
domain_read!(ActivationStatus);
impl ApiClient<ActivationStatus> {
pub async fn activate_pending_changes(&self, options: &ActivationOptions) -> Result<String> {
let etag = {
let url = format!("{}/pending_changes", self.collection_root());
self.inner.get_etag(&url).await
}?;
let url = format!(
"{}/domain-types/{}/actions/activate-changes/invoke",
self.inner.url,
ActivationStatus::DOMAIN_TYPE
);
let request = self
.inner
.http
.post(url)
.json(&options)
.header(reqwest::header::IF_MATCH, etag)
.build()
.unwrap();
let response: DomainObject<ActivationStatus> = self.inner.query_api(request).await?;
Ok(response.id)
}
pub async fn await_completion(&self, activation_id: impl Display) -> Result<()> {
let url = format!(
"{}/objects/{}/{}/actions/wait-for-completion/invoke",
self.inner.url,
ActivationStatus::DOMAIN_TYPE,
activation_id
);
let request = self.inner.http.get(url).build().unwrap();
self.inner.invoke_api(request).await
}
#[inline]
fn collection_root(&self) -> String {
format!(
"{}/domain-types/{}/collections",
self.inner.url,
ActivationStatus::DOMAIN_TYPE
)
}
pub async fn read_running_activations(&self) -> Result<Vec<ActivationStatus>> {
let url = format!("{}/running", self.collection_root());
let request = self.inner.http.get(url).build().unwrap();
let collection: DomainCollection<_> = self.inner.query_api(request).await?;
Ok(collection.into())
}
pub async fn read_pending_changes(&self) -> Result<Vec<PendingChange>> {
let url = format!("{}/pending_changes", self.collection_root());
let request = self.inner.http.get(url).build().unwrap();
#[derive(Deserialize)]
struct ChangeCollection {
value: Vec<PendingChange>,
}
let collection: ChangeCollection = self.inner.query_api(request).await?;
Ok(collection.value)
}
}
+144 -48
View File
@@ -3,27 +3,31 @@ use std::fmt::Display;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::api::rules::contactgroups::HostContactGroups; #[cfg(feature = "schemars")]
use crate::api::rules::snmp::SnmpCommunity; use schemars::JsonSchema;
use crate::api::{BulkReadDomainExtention, BulkUpdateDomainExtention, DomainExtention};
use crate::api::{
DomainExtension, domain_bulk_read, domain_bulk_update, domain_client, domain_create,
domain_delete, domain_read, domain_update,
};
use crate::{ApiClient, Client, Result}; use crate::{ApiClient, Client, Result};
pub const ROOT_FOLDER: &str = "/"; pub const ROOT_FOLDER: &str = "/";
impl Client { domain_client!(FolderConfig, folder_api);
pub fn folder_api(&self) -> ApiClient<FolderConfig> {
self.into()
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct FolderConfig { pub struct FolderConfig {
pub path: String, pub path: String,
pub attributes: FolderAttributes, pub attributes: FolderAttributes,
} }
#[allow(dead_code)]
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct FolderAttributes { pub struct FolderAttributes {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub site: Option<String>, pub site: Option<String>,
@@ -39,15 +43,32 @@ pub struct FolderAttributes {
/// this attribute is not optional when queried from the api. /// this attribute is not optional when queried from the api.
/// It cannot be filled in when creating a new folder. /// It cannot be filled in when creating a new folder.
pub meta_data: Option<MetaData>, pub meta_data: Option<MetaData>,
#[serde(flatten, skip_serializing_if = "HashMap::is_empty")] // Additional attributes that can be used to categorize hosts. every tag is part of a tag_group
/// Additional attributes that can be used to categorize hosts. every tag is part of a tag_group // NOTE that checkmk always returns taggroups prefixed with "tag_"
/// NOTE that checkmk always returns taggroups prefixed with "tag_" #[serde(default, flatten, skip_serializing_if = "HashMap::is_empty")]
pub tags: HashMap<String, String>, pub tags: HashMap<String, String>,
// TODO: to be implemented
// #[serde(flatten, default)] // these fields are used to prevent tags from taking them up
// pub networkscan: NetworkScan, #[serde(default)]
// #[serde(flatten, default)] management_snmp_community: Option<SnmpCommunity>,
// pub management: ManagementProtocol, #[serde(default, skip_serializing)]
management_address: Value,
#[serde(default, skip_serializing)]
management_protocol: Value,
#[serde(default, skip_serializing)]
management_ipmi_credentials: Value,
#[serde(default, skip_serializing)]
network_scan: Value,
#[serde(default, skip_serializing)]
network_scan_result: Value,
#[serde(default, skip_serializing)]
waiting_for_discovery: Value,
#[serde(default, skip_serializing)]
inventory_failed: Value,
#[serde(default, skip_serializing)]
locked_attributes: Value,
#[serde(default, skip_serializing)]
locked_by: Value,
} }
impl FolderAttributes { impl FolderAttributes {
@@ -92,8 +113,7 @@ impl FolderAttributes {
} }
pub fn add_tag(&mut self, taggroup: impl Display, tagvalue: String) { pub fn add_tag(&mut self, taggroup: impl Display, tagvalue: String) {
self.tags self.tags.insert(format!("tag_{taggroup}"), tagvalue);
.insert(format!("tag_{taggroup}"), tagvalue.to_string());
} }
pub fn with_tag(mut self, taggroup: impl Display, tagvalue: String) -> Self { pub fn with_tag(mut self, taggroup: impl Display, tagvalue: String) -> Self {
self.add_tag(taggroup, tagvalue); self.add_tag(taggroup, tagvalue);
@@ -102,6 +122,7 @@ impl FolderAttributes {
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct MetaData { pub struct MetaData {
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
@@ -109,17 +130,95 @@ pub struct MetaData {
pub created_by: Option<String>, pub created_by: Option<String>,
} }
impl DomainExtention for FolderConfig { #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
const DOMAIN_TYPE: super::DomainType = super::DomainType::FolderConfig; #[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum SnmpV3AuthProtocol {
#[serde(rename = "MD5-96")]
Md5,
#[serde(rename = "SHA-1-96")]
Sha96,
#[serde(rename = "SHA-2-224")]
Sha224,
#[serde(rename = "SHA-2-256")]
Sha256,
#[serde(rename = "SHA-2-384")]
Sha384,
#[serde(rename = "SHA-2-512")]
Sha512,
}
type CreationRequest = FolderCreationRequest; #[derive(Debug, Clone, Serialize, Deserialize)]
type CreationQuery = (); #[cfg_attr(feature = "schemars", derive(JsonSchema))]
type ReadQuery = (); pub struct SnmpV3Auth {
type UpdateRequest = FolderUpdateRequest; pub auth_protocol: SnmpV3AuthProtocol,
type DeleteQuery = FolderDeleteQuery; pub security_name: String,
pub auth_password: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum SnmpV3PrivProtocol {
#[serde(rename = "CBC-DES")]
Des,
#[serde(rename = "3DES-EDE")]
Des3,
#[serde(rename = "AES-128")]
Aes128,
#[serde(rename = "AES-192")]
Aes192,
#[serde(rename = "AES-256")]
Aes256,
#[serde(rename = "AES-192-Blumenthal")]
Aes192Blumenthal,
#[serde(rename = "AES-256-Blumenthal")]
Aes256Blumenthal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct SnmpV3Priv {
pub privacy_protocol: SnmpV3PrivProtocol,
pub privacy_password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(tag = "type")]
pub enum SnmpCommunity {
V1V2Community {
community: String,
},
NoAuthNoPriv {
security_name: String,
},
AuthNoPriv {
#[serde(flatten)]
auth: SnmpV3Auth,
},
AuthPriv {
#[serde(flatten)]
authentication: SnmpV3Auth,
#[serde(flatten)]
privacy: SnmpV3Priv,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostContactGroups {
pub groups: Vec<String>,
pub r#use: bool,
pub use_for_services: bool,
pub recurse_use: bool,
pub recurse_perms: bool,
}
impl DomainExtension for FolderConfig {
const DOMAIN_TYPE: super::DomainType = super::DomainType::FolderConfig;
} }
#[derive(Debug, Default, Clone, Serialize)] #[derive(Debug, Default, Clone, Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct FolderCreationRequest { pub struct FolderCreationRequest {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
@@ -157,6 +256,7 @@ impl FolderCreationRequest {
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct FolderUpdateRequest { pub struct FolderUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>, pub title: Option<String>,
@@ -212,6 +312,7 @@ impl FolderUpdateRequest {
} }
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct FolderDeleteQuery { pub struct FolderDeleteQuery {
pub delete_mode: FolderDeleteMode, pub delete_mode: FolderDeleteMode,
} }
@@ -223,18 +324,16 @@ impl FolderDeleteQuery {
} }
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum FolderDeleteMode { pub enum FolderDeleteMode {
#[default] #[default]
Recursive, Recursive,
AbotyOnNonEmpty, AbortOnNonEmpty,
}
impl BulkReadDomainExtention for FolderConfig {
type BulkReadQuery = FolderBulkReadQuery;
} }
#[derive(Serialize)] #[derive(Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct FolderBulkReadQuery { pub struct FolderBulkReadQuery {
pub parent: String, pub parent: String,
pub recursive: bool, pub recursive: bool,
@@ -246,11 +345,8 @@ impl FolderBulkReadQuery {
} }
} }
impl BulkUpdateDomainExtention for FolderConfig {
type BulkUpdateRequest = FolderBulkUpdateRequest;
}
#[derive(Serialize)] #[derive(Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct FolderBulkUpdateRequest { pub struct FolderBulkUpdateRequest {
folder: String, folder: String,
#[serde(flatten)] #[serde(flatten)]
@@ -266,27 +362,27 @@ impl FolderBulkUpdateRequest {
} }
} }
#[derive(Serialize)]
pub struct MoveFolderRequest {
destination: String,
}
impl MoveFolderRequest {
pub fn new(destination: String) -> Self {
Self { destination }
}
}
impl ApiClient<FolderConfig> { impl ApiClient<FolderConfig> {
pub async fn move_folder(&self, id: impl Display, request: &MoveFolderRequest) -> Result<()> { pub async fn move_folder(&self, id: impl Display, destination: &str) -> Result<()> {
#[derive(Serialize)]
struct MoveFolderRequest<'a> {
destination: &'a str,
}
let request = MoveFolderRequest { destination };
let url = format!( let url = format!(
"{}/actions/move/invoke", "{}/actions/move/invoke",
self.inner.object_url(FolderConfig::DOMAIN_TYPE, id) self.inner.object_url(FolderConfig::DOMAIN_TYPE, id)
); );
let request = self.inner.http.post(url).json(request).build().unwrap(); let request = self.inner.http.post(url).json(&request).build().unwrap();
self.inner.invoke_api(request).await self.inner.invoke_api(request).await
} }
} }
// TODO: add show hosts api call domain_create!(FolderConfig, FolderCreationRequest);
domain_read!(FolderConfig);
domain_update!(FolderConfig, FolderUpdateRequest);
domain_delete!(FolderConfig, FolderDeleteQuery);
domain_bulk_read!(FolderConfig, FolderBulkReadQuery);
domain_bulk_update!(FolderConfig, FolderBulkUpdateRequest);
+126
View File
@@ -0,0 +1,126 @@
use core::ops::Deref;
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::api::{DomainCollection, DomainObject, DomainType, domain_client};
use crate::{ApiClient, Client, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct Group {
pub name: String,
pub alias: String,
}
impl Group {
pub fn new(name: String, alias: String) -> Self {
Self { name, alias }
}
}
impl From<DomainObject<serde_json::Value>> for Group {
fn from(value: DomainObject<serde_json::Value>) -> Self {
Self::new(value.id, value.title.unwrap_or_default())
}
}
macro_rules! domain_group {
($id:ident, $typ:expr, $func:ident) => {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct $id(Group);
domain_client!($id, $func);
impl Deref for $id {
type Target = Group;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl $id {
pub fn new(name: String, alias: String) -> Self {
Self(Group::new(name, alias))
}
}
impl From<DomainObject<serde_json::Value>> for $id {
fn from(value: DomainObject<serde_json::Value>) -> Self {
Self(Group::from(value))
}
}
impl ApiClient<$id> {
pub async fn create(&self, group: &$id) -> Result<()> {
self.inner.create($typ, group, &()).await
}
pub async fn read(&self, id: &str) -> Result<$id> {
let obj: DomainObject<serde_json::Value> = self.inner.read($typ, id, &()).await?;
Ok($id::from(obj))
}
pub async fn update(&self, id: &str, alias: &str) -> Result<()> {
#[derive(Serialize)]
struct Body<'a> {
alias: &'a str,
}
self.inner.update($typ, id, &Body { alias }).await
}
pub async fn delete(&self, id: &str) -> Result<()> {
self.inner.delete($typ, id, &()).await
}
pub async fn bulk_create(&self, entries: &[$id]) -> Result<()> {
self.inner.bulk_create($typ, entries, &()).await
}
pub async fn bulk_read(&self) -> Result<Vec<$id>> {
let collection: DomainCollection<serde_json::Value> =
self.inner.bulk_read($typ, &()).await?;
let groups = collection.value.into_iter().map($id::from).collect();
Ok(groups)
}
pub async fn bulk_update(&self, entries: &[Group]) -> Result<()> {
#[derive(Serialize)]
struct UpdateAttributes<'a> {
alias: &'a str,
}
#[derive(Serialize)]
struct UpdateRequest<'a> {
name: &'a str,
attributes: UpdateAttributes<'a>,
}
impl<'a> UpdateRequest<'a> {
fn from(value: &'a Group) -> Self {
Self {
name: value.name.as_str(),
attributes: UpdateAttributes {
alias: value.alias.as_str(),
},
}
}
}
let entries = entries.iter().map(UpdateRequest::from).collect::<Vec<_>>();
self.inner.bulk_update($typ, &entries).await
}
pub async fn bulk_delete(&self, entries: &[String]) -> Result<()> {
self.inner.bulk_delete($typ, entries).await
}
}
};
}
domain_group!(HostGroup, DomainType::HostGroupConfig, host_group_api);
domain_group!(
ServiceGroup,
DomainType::ServiceGroupConfig,
service_group_api
);
domain_group!(
ContactGroup,
DomainType::ContactGroupConfig,
contact_group_api
);
+48 -28
View File
@@ -1,53 +1,71 @@
use std::fmt::Display;
use std::net::{Ipv4Addr, Ipv6Addr};
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::api::folders::FolderAttributes; use crate::api::folders::FolderAttributes;
use crate::api::{ use crate::api::{
BulkCreateDomainExtention, BulkDeleteDomainExtention, BulkReadDomainExtention, DomainExtension, domain_bulk_create, domain_bulk_delete, domain_bulk_read, domain_bulk_update,
BulkUpdateDomainExtention, DomainExtention, domain_client, domain_create, domain_delete, domain_read, domain_update,
}; };
use crate::{ApiClient, Client}; use crate::{ApiClient, Client, Result};
impl Client { domain_client!(HostConfig, host_api);
pub fn host_api(&self) -> ApiClient<HostConfig> {
self.into()
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostConfig { pub struct HostConfig {
pub folder: String, pub folder: String,
pub attributes: FolderAttributes, pub attributes: HostAttributes,
pub effective_attributes: FolderAttributes, pub effective_attributes: HostAttributes,
pub is_cluster: bool, pub is_cluster: bool,
pub is_offline: bool, pub is_offline: bool,
pub cluster_nodes: Option<Vec<String>>, pub cluster_nodes: Option<Vec<String>>,
} }
impl DomainExtention for HostConfig { #[derive(Debug, Default, Clone, Serialize, Deserialize)]
const DOMAIN_TYPE: super::DomainType = super::DomainType::HostConfig; #[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostAttributes {
#[serde(default)]
alias: Option<String>,
#[serde(default)]
ipaddress: Option<String>,
#[serde(default)]
ipv6address: Option<String>,
#[serde(default)]
additional_ipv4addresses: Vec<Ipv4Addr>,
#[serde(default)]
additional_ipv6addresses: Vec<Ipv6Addr>,
type CreationRequest = HostCreationRequest; #[serde(flatten, default)]
type CreationQuery = HostCreationQuery; folder_attributes: FolderAttributes,
type ReadQuery = HostReadQuery; }
type UpdateRequest = HostUpdateRequest;
type DeleteQuery = (); impl DomainExtension for HostConfig {
const DOMAIN_TYPE: super::DomainType = super::DomainType::HostConfig;
} }
#[derive(Serialize)] #[derive(Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostCreationRequest { pub struct HostCreationRequest {
pub hostname: String, pub host_name: String,
pub folder: String, pub folder: String,
pub attributes: FolderAttributes, pub attributes: FolderAttributes,
} }
#[derive(Serialize)] #[derive(Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostCreationQuery { pub struct HostCreationQuery {
pub bake_agent: bool, pub bake_agent: bool,
} }
#[derive(Serialize)] #[derive(Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostReadQuery { pub struct HostReadQuery {
pub effective_attributes: bool, pub effective_attributes: bool,
} }
#[derive(Serialize)] #[derive(Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostUpdateRequest { pub struct HostUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<FolderAttributes>, pub attributes: Option<FolderAttributes>,
@@ -60,7 +78,7 @@ pub struct HostUpdateRequest {
impl HostCreationRequest { impl HostCreationRequest {
pub fn new(hostname: String, folder: String, attributes: FolderAttributes) -> Self { pub fn new(hostname: String, folder: String, attributes: FolderAttributes) -> Self {
HostCreationRequest { HostCreationRequest {
hostname, host_name: hostname,
folder, folder,
attributes, attributes,
} }
@@ -105,16 +123,8 @@ impl HostUpdateRequest {
} }
} }
impl BulkCreateDomainExtention for HostConfig {}
impl BulkReadDomainExtention for HostConfig {
type BulkReadQuery = HostBulkReadQuery;
}
impl BulkUpdateDomainExtention for HostConfig {
type BulkUpdateRequest = HostBulkUpdateRequest;
}
impl BulkDeleteDomainExtention for HostConfig {}
#[derive(Debug, Default, Clone, Serialize)] #[derive(Debug, Default, Clone, Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostBulkReadQuery { pub struct HostBulkReadQuery {
effective_attributes: bool, effective_attributes: bool,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -172,6 +182,7 @@ impl HostBulkReadQuery {
} }
#[derive(Serialize)] #[derive(Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostBulkUpdateRequest { pub struct HostBulkUpdateRequest {
host_name: String, host_name: String,
#[serde(flatten)] #[serde(flatten)]
@@ -186,3 +197,12 @@ impl HostBulkUpdateRequest {
} }
} }
} }
domain_create!(HostConfig, HostCreationRequest, HostCreationQuery);
domain_read!(HostConfig, HostReadQuery);
domain_update!(HostConfig, HostUpdateRequest);
domain_delete!(HostConfig);
domain_bulk_create!(HostConfig, HostCreationRequest, HostCreationQuery);
domain_bulk_read!(HostConfig, HostBulkReadQuery);
domain_bulk_update!(HostConfig, HostBulkUpdateRequest);
domain_bulk_delete!(HostConfig);
+261 -18
View File
@@ -1,11 +1,17 @@
pub mod activations;
pub mod folders; pub mod folders;
pub mod groups;
pub mod hosts; pub mod hosts;
pub(crate) mod rules; pub mod rules;
pub mod tags;
use std::fmt; use std::fmt::{self, Display};
use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::Result;
use crate::client::InnerClient;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum DomainType { pub enum DomainType {
@@ -15,6 +21,11 @@ pub enum DomainType {
ContactGroupConfig, ContactGroupConfig,
HostGroupConfig, HostGroupConfig,
ServiceGroupConfig, ServiceGroupConfig,
HostTagGroup,
ActivationRun,
Ruleset,
Rule,
Dict,
} }
impl fmt::Display for DomainType { impl fmt::Display for DomainType {
@@ -26,6 +37,11 @@ impl fmt::Display for DomainType {
DomainType::ContactGroupConfig => write!(f, "contact_group_config"), DomainType::ContactGroupConfig => write!(f, "contact_group_config"),
DomainType::HostGroupConfig => write!(f, "host_group_config"), DomainType::HostGroupConfig => write!(f, "host_group_config"),
DomainType::ServiceGroupConfig => write!(f, "service_group_config"), DomainType::ServiceGroupConfig => write!(f, "service_group_config"),
DomainType::HostTagGroup => write!(f, "host_tag_group"),
DomainType::ActivationRun => write!(f, "activation_run"),
DomainType::Ruleset => write!(f, "ruleset"),
DomainType::Rule => write!(f, "rule"),
DomainType::Dict => write!(f, "dict"),
} }
} }
} }
@@ -81,24 +97,251 @@ pub(crate) struct DomainCollection<E> {
// pub extensions: Option<E>, // pub extensions: Option<E>,
} }
pub trait DomainExtention: DeserializeOwned + Serialize { impl<E> From<DomainCollection<E>> for Vec<E> {
fn from(value: DomainCollection<E>) -> Self {
value.value.into_iter().map(|obj| obj.extensions).collect()
}
}
pub trait DomainExtension: DeserializeOwned + Serialize {
const DOMAIN_TYPE: DomainType; const DOMAIN_TYPE: DomainType;
type CreationRequest: Serialize;
type CreationQuery: Serialize;
type ReadQuery: Serialize;
type UpdateRequest: Serialize;
type DeleteQuery: Serialize;
} }
// these are essentials tags for domain extentions to signal that bulk operations are possible impl InnerClient {
// not every extention supports bulk operations. and not all extentions that do support all of them pub(crate) async fn create_domain_object<E, O, Q>(&self, object: &O, query: &Q) -> Result<()>
// and those that do tend to have a bit different types where
pub trait BulkCreateDomainExtention: DomainExtention {} E: DomainExtension,
pub trait BulkReadDomainExtention: DomainExtention { O: Serialize,
type BulkReadQuery: Serialize; Q: Serialize,
{
self.create(E::DOMAIN_TYPE, object, query).await
} }
pub trait BulkUpdateDomainExtention: DomainExtention { pub(crate) async fn read_domain_object<E, Q>(
type BulkUpdateRequest: Serialize; &self,
id: impl Display,
query: &Q,
) -> Result<DomainObject<E>>
where
E: DomainExtension,
Q: Serialize,
{
self.read(E::DOMAIN_TYPE, id, query).await
} }
pub trait BulkDeleteDomainExtention: DomainExtention {} pub(crate) async fn update_domain_object<E, O>(
&self,
id: impl Display,
object: &O,
) -> Result<()>
where
E: DomainExtension,
O: Serialize,
{
self.update(E::DOMAIN_TYPE, id, object).await
}
pub(crate) async fn delete_domain_object<E, Q>(&self, id: impl Display, query: &Q) -> Result<()>
where
E: DomainExtension,
Q: Serialize,
{
self.delete(E::DOMAIN_TYPE, id, query).await
}
pub(crate) async fn bulk_create_domain_objects<E, O, Q>(
&self,
entries: &[O],
query: &Q,
) -> Result<()>
where
E: DomainExtension,
O: Serialize,
Q: Serialize,
{
self.bulk_create(E::DOMAIN_TYPE, entries, query).await
}
pub(crate) async fn bulk_read_domain_objects<E, Q>(
&self,
query: &Q,
) -> Result<DomainCollection<E>>
where
E: DomainExtension,
Q: Serialize,
{
self.bulk_read(E::DOMAIN_TYPE, query).await
}
pub(crate) async fn bulk_update_domain_objects<E, O>(&self, entries: &[O]) -> Result<()>
where
E: DomainExtension,
O: Serialize,
{
self.bulk_update(E::DOMAIN_TYPE, entries).await
}
pub(crate) async fn bulk_delete_domain_objects<E>(&self, entries: &[String]) -> Result<()>
where
E: DomainExtension,
{
self.bulk_delete(E::DOMAIN_TYPE, entries).await
}
}
macro_rules! domain_client {
($id:ident, $func:ident) => {
impl Client {
pub fn $func(&self) -> ApiClient<$id> {
self.into()
}
}
domain_client!($id);
};
($id:ident) => {
impl From<&Client> for ApiClient<$id> {
fn from(value: &Client) -> Self {
Self::new(value)
}
}
};
}
pub(super) use domain_client;
macro_rules! domain_create {
($id:ident, $object:ident) => {
impl ApiClient<$id> {
pub async fn create(&self, object: &$object) -> Result<()> {
self.inner
.create_domain_object::<$id, _, _>(object, &())
.await
}
}
};
($id:ident, $object:ident, $qry:ident) => {
impl ApiClient<$id> {
pub async fn create(&self, object: &$object, query: &$qry) -> Result<()> {
self.inner
.create_domain_object::<$id, _, _>(object, query)
.await
}
}
};
}
pub(super) use domain_create;
macro_rules! domain_read {
($id:ident) => {
impl ApiClient<$id> {
pub async fn read(&self, id: impl Display) -> Result<$id> {
self.inner
.read_domain_object::<$id, _>(id, &())
.await
.map(|obj| obj.extensions)
}
}
};
($id:ident, $qry:ident) => {
impl ApiClient<$id> {
pub async fn read(&self, id: impl Display, query: &$qry) -> Result<$id> {
self.inner
.read_domain_object::<$id, _>(id, &query)
.await
.map(|obj| obj.extensions)
}
}
};
}
pub(super) use domain_read;
macro_rules! domain_update {
($id:ident, $object:ident) => {
impl ApiClient<$id> {
pub async fn update(&self, id: impl Display, object: &$object) -> Result<()> {
self.inner.update_domain_object::<$id, _>(id, object).await
}
}
};
}
pub(super) use domain_update;
macro_rules! domain_delete {
($id:ident) => {
impl ApiClient<$id> {
pub async fn delete(&self, id: impl Display) -> Result<()> {
self.inner.delete_domain_object::<$id, _>(id, &()).await
}
}
};
($id:ident, $qry:ident) => {
impl ApiClient<$id> {
pub async fn delete(&self, id: impl Display, query: &$qry) -> Result<()> {
self.inner.delete_domain_object::<$id, _>(id, &query).await
}
}
};
}
pub(super) use domain_delete;
macro_rules! domain_bulk_create {
($id:ident, $object:ident) => {
impl ApiClient<$id> {
pub async fn bulk_create(&self, entries: &[$object]) -> Result<()> {
self.inner
.bulk_create_domain_objects::<$id, _, _>(entries, &())
.await
}
}
};
($id:ident, $object:ident, $qry:ident) => {
impl ApiClient<$id> {
pub async fn bulk_create(&self, entries: &[$object], query: &$qry) -> Result<()> {
self.inner
.bulk_create_domain_objects::<$id, _, _>(entries, query)
.await
}
}
};
}
pub(super) use domain_bulk_create;
macro_rules! domain_bulk_read {
($id:ident) => {
impl ApiClient<$id> {
pub async fn bulk_read(&self) -> Result<Vec<$id>> {
let collection = self.inner.bulk_read_domain_objects::<$id, _>(&()).await?;
Ok(collection.into())
}
}
};
($id:ident, $qry:ident) => {
impl ApiClient<$id> {
pub async fn bulk_read(&self, query: &$qry) -> Result<Vec<$id>> {
let collection = self
.inner
.bulk_read_domain_objects::<$id, _>(&query)
.await?;
Ok(collection.into())
}
}
};
}
pub(super) use domain_bulk_read;
macro_rules! domain_bulk_update {
($id:ident, $object:ident) => {
impl ApiClient<$id> {
pub async fn bulk_update(&self, entries: &[$object]) -> Result<()> {
self.inner
.bulk_update_domain_objects::<$id, _>(entries)
.await
}
}
};
}
pub(super) use domain_bulk_update;
macro_rules! domain_bulk_delete {
($id:ident) => {
impl ApiClient<$id> {
pub async fn bulk_delete(&self, entries: &[String]) -> Result<()> {
self.inner.bulk_delete_domain_objects::<$id>(entries).await
}
}
};
}
pub(super) use domain_bulk_delete;
+695
View File
@@ -0,0 +1,695 @@
use std::fmt::Display;
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::api::{
DomainExtension, DomainObject, domain_bulk_read, domain_client, domain_create, domain_delete,
domain_update,
};
use crate::{ApiClient, Client, Error, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct Ruleset {
pub name: String,
pub title: String,
// this value is not set when searching for a specific ruleset (dont ask me why)
pub help: Option<String>,
// this value should be set, but never is. wrong documentation?
pub folder: Option<String>,
// this value is not set when searching for a specific ruleset (dont ask me why)
pub item_type: Option<String>,
// this value is not set when searching for a specific ruleset (dont ask me why)
pub item_name: Option<String>,
// now sure what this is supposed to be right now. no documentation
// item_enum: Option<()>,
// this value is not set when searching for a specific ruleset (dont ask me why)
pub match_type: Option<MatchType>,
pub number_of_rules: usize,
}
impl DomainExtension for Ruleset {
const DOMAIN_TYPE: super::DomainType = super::DomainType::Ruleset;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum MatchType {
All,
First,
Dict,
List,
Varies,
}
domain_client!(Ruleset, ruleset_api);
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct RulesetQuery {
name: Option<String>,
fulltext: Option<String>,
folder: Option<String>,
deprecated: bool,
used: bool,
}
impl RulesetQuery {
pub fn set_name(&mut self, name: String) {
self.name = Some(name);
}
pub fn with_name(mut self, name: String) -> Self {
self.set_name(name);
self
}
pub fn set_fulltext(&mut self, fulltext: String) {
self.fulltext = Some(fulltext);
}
pub fn with_fulltext(mut self, fulltext: String) -> Self {
self.set_fulltext(fulltext);
self
}
pub fn set_folder(&mut self, folder: String) {
self.folder = Some(folder);
}
pub fn with_folder(mut self, folder: String) -> Self {
self.set_folder(folder);
self
}
pub fn set_deprecated(&mut self, deprecated: bool) {
self.deprecated = deprecated;
}
pub fn with_deprecated(mut self, deprecated: bool) -> Self {
self.set_deprecated(deprecated);
self
}
pub fn set_used(&mut self, used: bool) {
self.used = used;
}
pub fn with_used(mut self, used: bool) -> Self {
self.set_used(used);
self
}
}
impl ApiClient<Ruleset> {
pub async fn read(&self, id: impl Display) -> Result<Ruleset> {
#[derive(Deserialize, Serialize)]
struct RulesetCompact {
name: String,
folder: Option<String>,
number_of_rules: usize,
}
impl DomainExtension for RulesetCompact {
const DOMAIN_TYPE: super::DomainType = super::DomainType::Ruleset;
}
impl From<DomainObject<RulesetCompact>> for Ruleset {
fn from(value: DomainObject<RulesetCompact>) -> Self {
Ruleset {
name: value.extensions.name,
folder: value.extensions.folder,
number_of_rules: value.extensions.number_of_rules,
title: value.title.unwrap_or_default(),
help: None,
item_type: None,
item_name: None,
match_type: None,
}
}
}
self.inner
.read_domain_object::<RulesetCompact, _>(id, &())
.await
.map(|obj| obj.into())
}
}
domain_bulk_read!(Ruleset, RulesetQuery);
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct Rule {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub folder_index: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<Uuid>,
pub ruleset: String,
pub folder: String,
pub properties: RuleProperties,
pub value_raw: String,
pub conditions: RuleCondition,
}
impl DomainExtension for Rule {
const DOMAIN_TYPE: super::DomainType = super::DomainType::Rule;
}
impl Rule {
pub fn new(ruleset: String, folder: String) -> Self {
Self {
ruleset,
folder,
..Default::default()
}
}
pub fn set_ruleset(&mut self, ruleset: String) {
self.ruleset = ruleset;
}
pub fn with_ruleset(mut self, ruleset: String) -> Self {
self.set_ruleset(ruleset);
self
}
pub fn set_folder(&mut self, folder: String) {
self.folder = folder;
}
pub fn with_folder(mut self, folder: String) -> Self {
self.set_folder(folder);
self
}
pub fn set_properties(&mut self, properties: RuleProperties) {
self.properties = properties;
}
pub fn with_properties(mut self, properties: RuleProperties) -> Self {
self.set_properties(properties);
self
}
pub fn set_value_raw(&mut self, value_raw: String) {
self.value_raw = value_raw;
}
pub fn with_value_raw(mut self, value_raw: String) -> Self {
self.set_value_raw(value_raw);
self
}
pub fn set_conditions(&mut self, conditions: RuleCondition) {
self.conditions = conditions;
}
pub fn with_conditions(mut self, conditions: RuleCondition) -> Self {
self.set_conditions(conditions);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct RuleProperties {
pub description: String,
pub comment: String,
pub documentation_url: String,
pub disabled: bool,
}
impl RuleProperties {
pub fn new(description: String) -> Self {
Self {
description,
..Default::default()
}
}
pub fn set_description(&mut self, description: String) {
self.description = description;
}
pub fn with_description(mut self, description: String) -> Self {
self.set_description(description);
self
}
pub fn set_comment(&mut self, comment: String) {
self.comment = comment;
}
pub fn with_comment(mut self, comment: String) -> Self {
self.set_comment(comment);
self
}
pub fn set_documentation_url(&mut self, url: String) {
self.documentation_url = url;
}
pub fn with_documentation_url(mut self, url: String) -> Self {
self.set_documentation_url(url);
self
}
pub fn set_disabled(&mut self, disabled: bool) {
self.disabled = disabled;
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.set_disabled(disabled);
self
}
}
// TODO: implement
pub trait RuleMatch {
fn matches(&self, other: Self) -> bool;
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct RuleCondition {
pub host_name: Option<MatchCondition>,
pub host_tags: Vec<HostTagCondition>,
pub host_label_groups: Vec<LabelGroupCondition>,
pub service_label_groups: Vec<LabelGroupCondition>,
pub service_description: Option<MatchCondition>,
}
impl RuleCondition {
/// Match hosts whose name is one of the given values.
pub fn match_hosts(&mut self, hosts: Vec<String>) {
self.host_name = Some(MatchCondition {
match_on: hosts,
operator: MatchOperator::OneOf,
});
}
pub fn matching_hosts(mut self, hosts: Vec<String>) -> Self {
self.match_hosts(hosts);
self
}
/// Exclude hosts whose name is one of the given values.
pub fn exclude_hosts(&mut self, hosts: Vec<String>) {
self.host_name = Some(MatchCondition {
match_on: hosts,
operator: MatchOperator::NoneOf,
});
}
pub fn excluding_hosts(mut self, hosts: Vec<String>) -> Self {
self.exclude_hosts(hosts);
self
}
/// Clear any host name condition.
pub fn clear_hosts(&mut self) {
self.host_name = None;
}
/// Match services whose description is one of the given values.
pub fn match_services(&mut self, services: Vec<String>) {
self.service_description = Some(MatchCondition {
match_on: services,
operator: MatchOperator::OneOf,
});
}
pub fn matching_services(mut self, services: Vec<String>) -> Self {
self.match_services(services);
self
}
/// Exclude services whose description is one of the given values.
pub fn exclude_services(&mut self, services: Vec<String>) {
self.service_description = Some(MatchCondition {
match_on: services,
operator: MatchOperator::NoneOf,
});
}
pub fn excluding_services(mut self, services: Vec<String>) -> Self {
self.exclude_services(services);
self
}
/// Clear any service description condition.
pub fn clear_services(&mut self) {
self.service_description = None;
}
/// Require the host tag `key` to equal `value`.
pub fn require_host_tag(&mut self, key: String, value: String) {
self.host_tags.push(HostTagCondition {
key,
match_on: TagMatchOperator::Is(value),
});
}
pub fn with_host_tag(mut self, key: String, value: String) -> Self {
self.require_host_tag(key, value);
self
}
/// Require the host tag `key` to not equal `value`.
pub fn exclude_host_tag(&mut self, key: String, value: String) {
self.host_tags.push(HostTagCondition {
key,
match_on: TagMatchOperator::IsNot(value),
});
}
pub fn without_host_tag(mut self, key: String, value: String) -> Self {
self.exclude_host_tag(key, value);
self
}
/// Require the host tag `key` to be one of `values`.
pub fn require_any_host_tag(&mut self, key: String, values: Vec<String>) {
self.host_tags.push(HostTagCondition {
key,
match_on: TagMatchOperator::OneOff(values),
});
}
pub fn with_any_host_tag(mut self, key: String, values: Vec<String>) -> Self {
self.require_any_host_tag(key, values);
self
}
/// Require the host tag `key` to be none of `values`.
pub fn exclude_any_host_tag(&mut self, key: String, values: Vec<String>) {
self.host_tags.push(HostTagCondition {
key,
match_on: TagMatchOperator::NoneOff(values),
});
}
pub fn without_any_host_tag(mut self, key: String, values: Vec<String>) -> Self {
self.exclude_any_host_tag(key, values);
self
}
/// Add a host label group that must ALL match (AND).
pub fn require_host_labels(&mut self, labels: Vec<LabelCondition>) {
self.host_label_groups.push(LabelGroupCondition {
operator: LabelOperator::And,
label_group: labels,
});
}
pub fn with_host_labels(mut self, labels: Vec<LabelCondition>) -> Self {
self.require_host_labels(labels);
self
}
/// Add a host label group where ANY must match (OR).
pub fn require_any_host_label(&mut self, labels: Vec<LabelCondition>) {
self.host_label_groups.push(LabelGroupCondition {
operator: LabelOperator::Or,
label_group: labels,
});
}
pub fn with_any_host_label(mut self, labels: Vec<LabelCondition>) -> Self {
self.require_any_host_label(labels);
self
}
/// Add a host label group that must NOT match (NOT).
pub fn exclude_host_labels(&mut self, labels: Vec<LabelCondition>) {
self.host_label_groups.push(LabelGroupCondition {
operator: LabelOperator::Not,
label_group: labels,
});
}
pub fn excluding_host_labels(mut self, labels: Vec<LabelCondition>) -> Self {
self.exclude_host_labels(labels);
self
}
/// Add a service label group that must ALL match (AND).
pub fn require_service_labels(&mut self, labels: Vec<LabelCondition>) {
self.service_label_groups.push(LabelGroupCondition {
operator: LabelOperator::And,
label_group: labels,
});
}
pub fn with_service_labels(mut self, labels: Vec<LabelCondition>) -> Self {
self.require_service_labels(labels);
self
}
/// Add a service label group where ANY must match (OR).
pub fn require_any_service_label(&mut self, labels: Vec<LabelCondition>) {
self.service_label_groups.push(LabelGroupCondition {
operator: LabelOperator::Or,
label_group: labels,
});
}
pub fn with_any_service_label(mut self, labels: Vec<LabelCondition>) -> Self {
self.require_any_service_label(labels);
self
}
/// Add a service label group that must NOT match (NOT).
pub fn exclude_service_labels(&mut self, labels: Vec<LabelCondition>) {
self.service_label_groups.push(LabelGroupCondition {
operator: LabelOperator::Not,
label_group: labels,
});
}
pub fn excluding_service_labels(mut self, labels: Vec<LabelCondition>) -> Self {
self.exclude_service_labels(labels);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct MatchCondition {
match_on: Vec<String>,
operator: MatchOperator,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum MatchOperator {
#[default]
OneOf,
NoneOf,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostTagCondition {
key: String,
match_on: TagMatchOperator,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum TagMatchOperator {
Is(String),
IsNot(String),
OneOff(Vec<String>),
NoneOff(Vec<String>),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct LabelGroupCondition {
operator: LabelOperator,
label_group: Vec<LabelCondition>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct LabelCondition {
operator: LabelOperator,
label: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum LabelOperator {
#[default]
And,
Or,
Not,
}
domain_client!(Rule, rule_api);
domain_create!(Rule, Rule);
domain_update!(Rule, Rule);
domain_delete!(Rule);
impl ApiClient<Rule> {
pub async fn read(&self, id: &str) -> Result<Rule> {
let mut object = self.inner.read_domain_object::<Rule, _>(id, &()).await?;
object.extensions.id = Some(Uuid::parse_str(id)?);
Ok(object.extensions)
}
pub async fn bulk_read(&self) -> Result<Vec<Rule>> {
let objects = self.inner.bulk_read_domain_objects::<Rule, _>(&()).await?;
let objects = objects
.value
.into_iter()
.map(|mut object| {
object.extensions.id = Some(Uuid::parse_str(&object.id)?);
Ok::<_, Error>(object.extensions)
})
.collect::<Result<Vec<_>>>()?;
Ok(objects)
}
}
mod serde_tags {
use serde::{
Deserialize, Serialize,
de::{self, Error, Unexpected, Visitor},
ser::SerializeStruct,
};
use crate::api::rules::{HostTagCondition, TagMatchOperator};
impl Serialize for HostTagCondition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_struct("HostTagCondition", 3)?;
map.serialize_field("key", &self.key)?;
match &self.match_on {
TagMatchOperator::Is(value) => {
map.serialize_field("operator", "is")?;
map.serialize_field("value", &value)?;
}
TagMatchOperator::IsNot(value) => {
map.serialize_field("operator", "is_not")?;
map.serialize_field("value", &value)?;
}
TagMatchOperator::OneOff(items) => {
map.serialize_field("operator", "one_of")?;
map.serialize_field("value", items)?;
}
TagMatchOperator::NoneOff(items) => {
map.serialize_field("operator", "none_of")?;
map.serialize_field("value", items)?;
}
};
map.end()
}
}
struct HostTagConditionVisitor;
impl<'de> Visitor<'de> for HostTagConditionVisitor {
type Value = HostTagCondition;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("expected a struct with fields key, operator and value")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Field {
Key,
Operator,
Value,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Operator {
Is,
IsNot,
OneOff,
NoneOff,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ValueKind {
Single(String),
Multiple(Vec<String>),
}
impl ValueKind {
fn into_single(self) -> Option<String> {
if let Self::Single(s) = self {
Some(s)
} else {
None
}
}
fn into_multiple(self) -> Result<Vec<String>, String> {
match self {
Self::Multiple(ss) => Ok(ss),
Self::Single(s) => Err(s),
}
}
}
let mut tkey: Option<String> = None;
let mut toperator: Option<Operator> = None;
let mut tvalue: Option<ValueKind> = None;
while let Some(key) = map.next_key()? {
#[allow(unused_must_use)]
match key {
Field::Key => {
tkey.insert(map.next_value()?);
}
Field::Operator => {
toperator.insert(map.next_value()?);
}
Field::Value => {
tvalue.insert(map.next_value()?);
}
};
}
let key = tkey.ok_or(A::Error::missing_field("key"))?;
let match_on = match toperator.ok_or(de::Error::missing_field("operator"))? {
Operator::Is => TagMatchOperator::Is(
tvalue
.and_then(|v| v.into_single())
.ok_or(A::Error::invalid_value(Unexpected::Seq, &"single string"))?,
),
Operator::IsNot => TagMatchOperator::IsNot(
tvalue
.and_then(|v| v.into_single())
.ok_or(A::Error::invalid_value(Unexpected::Seq, &"single string"))?,
),
Operator::OneOff => TagMatchOperator::OneOff(
tvalue
.ok_or(A::Error::missing_field("value"))?
.into_multiple()
.map_err(|s| {
A::Error::invalid_value(
Unexpected::Str(s.as_str()),
&"sequence of stirngs",
)
})?,
),
Operator::NoneOff => TagMatchOperator::NoneOff(
tvalue
.ok_or(A::Error::missing_field("value"))?
.into_multiple()
.map_err(|s| {
A::Error::invalid_value(
Unexpected::Str(s.as_str()),
&"sequence of stirngs",
)
})?,
),
};
Ok(HostTagCondition { key, match_on })
}
}
impl<'de> Deserialize<'de> for HostTagCondition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(HostTagConditionVisitor)
}
}
}
-10
View File
@@ -1,10 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HostContactGroups {
groups: Vec<String>,
r#use: bool,
use_for_services: bool,
recuse_use: bool,
recuse_perms: bool,
}
-2
View File
@@ -1,2 +0,0 @@
pub mod contactgroups;
pub mod snmp;
-69
View File
@@ -1,69 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum SnmpV3AuthProtocol {
#[serde(rename = "MD5-96")]
Md5,
#[serde(rename = "SHA-1-96")]
Sha96,
#[serde(rename = "SHA-2-224")]
Sha224,
#[serde(rename = "SHA-2-256")]
Sha256,
#[serde(rename = "SHA-2-384")]
Sha384,
#[serde(rename = "SHA-2-512")]
Sha512,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnmpV3Auth {
pub auth_protocol: SnmpV3AuthProtocol,
pub security_name: String,
pub auth_password: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum SnmpV3PrivProtocol {
#[serde(rename = "CBC-DES")]
Des,
#[serde(rename = "3DES-EDE")]
Des3,
#[serde(rename = "AES-128")]
Aes128,
#[serde(rename = "AES-192")]
Aes192,
#[serde(rename = "AES-256")]
Aes256,
#[serde(rename = "AES-192-Blumenthal")]
Aes192Blumenthal,
#[serde(rename = "AES-256-Blumenthal")]
Aes256Blumenthal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnmpV3Priv {
pub privacy_protocol: SnmpV3PrivProtocol,
pub privacy_password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SnmpCommunity {
V1V2Community {
community: String,
},
NoAuthNoPriv {
security_name: String,
},
AuthNoPriv {
#[serde(flatten)]
auth: SnmpV3Auth,
},
AuthPriv {
#[serde(flatten)]
authentication: SnmpV3Auth,
#[serde(flatten)]
privacy: SnmpV3Priv,
},
}
+170
View File
@@ -0,0 +1,170 @@
use crate::api::Display;
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::api::{
DomainExtension, DomainType, domain_bulk_read, domain_client, domain_create, domain_delete,
domain_read, domain_update,
};
use crate::{ApiClient, Client, Result};
domain_client!(TagGroup, tag_api);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ExtendedTagGroup {
pub id: String,
pub title: String,
#[serde(flatten, default)]
pub inner: TagGroup,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct TagGroup {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub topic: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub help: String,
pub tags: Vec<Tag>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct Tag {
pub id: Option<String>,
pub title: String,
pub aux_tags: Vec<String>,
}
impl ExtendedTagGroup {
pub fn new(id: String, title: String) -> Self {
Self {
id,
title,
inner: Default::default(),
}
}
pub fn set_topic(&mut self, topic: String) {
self.inner.topic = topic;
}
pub fn with_topic(mut self, topic: String) -> Self {
self.set_topic(topic);
self
}
pub fn set_help(&mut self, help: String) {
self.inner.help = help;
}
pub fn with_help(mut self, help: String) -> Self {
self.set_help(help);
self
}
pub fn add_tag(&mut self, tag: Tag) {
self.inner.tags.push(tag);
}
pub fn with_tag(mut self, tag: Tag) -> Self {
self.add_tag(tag);
self
}
}
impl Tag {
pub fn new(id: String, title: String) -> Self {
Self {
id: Some(id),
title,
aux_tags: Vec::new(),
}
}
pub fn new_default(title: String) -> Self {
Self {
id: None,
title,
aux_tags: Vec::new(),
}
}
pub fn add_aux_tag(&mut self, tag: String) {
self.aux_tags.push(tag);
}
pub fn with_aux_tag(mut self, tag: String) -> Self {
self.add_aux_tag(tag);
self
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct TagUpdateRequest {
title: String,
#[serde(flatten, default)]
inner: TagGroup,
repair: bool,
}
impl TagUpdateRequest {
pub fn new(title: String, repair: bool) -> Self {
Self {
title,
repair,
inner: Default::default(),
}
}
pub fn set_topic(&mut self, topic: String) {
self.inner.topic = topic;
}
pub fn with_topic(mut self, topic: String) -> Self {
self.set_topic(topic);
self
}
pub fn set_help(&mut self, help: String) {
self.inner.help = help;
}
pub fn with_help(mut self, help: String) -> Self {
self.set_help(help);
self
}
pub fn add_tag(&mut self, tag: Tag) {
self.inner.tags.push(tag);
}
pub fn with_tag(mut self, tag: Tag) -> Self {
self.add_tag(tag);
self
}
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct TagDeleteQuery {
repair: bool,
mode: Option<TagDeleteMode>,
}
impl TagDeleteQuery {
pub fn new(repair: bool, mode: Option<TagDeleteMode>) -> Self {
Self { repair, mode }
}
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum TagDeleteMode {
Abort,
#[default]
Delete,
Remove,
}
impl DomainExtension for TagGroup {
const DOMAIN_TYPE: super::DomainType = DomainType::HostTagGroup;
}
domain_create!(TagGroup, ExtendedTagGroup);
domain_read!(TagGroup);
domain_update!(TagGroup, TagUpdateRequest);
domain_delete!(TagGroup);
domain_bulk_read!(TagGroup);
+107 -149
View File
@@ -10,10 +10,7 @@ use serde::Serialize;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use crate::api::{ use crate::api::{DomainCollection, DomainType};
BulkCreateDomainExtention, BulkDeleteDomainExtention, BulkReadDomainExtention,
BulkUpdateDomainExtention, DomainCollection, DomainExtention, DomainObject, DomainType,
};
use crate::{Error, Result}; use crate::{Error, Result};
#[derive(Clone)] #[derive(Clone)]
@@ -26,8 +23,8 @@ pub(crate) struct InnerClient {
#[derive(Default, PartialEq, Eq)] #[derive(Default, PartialEq, Eq)]
enum SslStrategy { enum SslStrategy {
NoSll, NoSsl,
IngoreHostname, IgnoreHostname,
IgnoreCertificate, IgnoreCertificate,
#[default] #[default]
Strict, Strict,
@@ -96,7 +93,7 @@ pub enum ClientBuildError {
#[error("failed to build client: {0}")] #[error("failed to build client: {0}")]
BuildClient(#[from] reqwest::Error), BuildClient(#[from] reqwest::Error),
#[error( #[error(
"invalid credentials. thise can only consist of visibly ASCII characters (ie. (32-127))" "invalid credentials. these can only consist of visible ASCII characters (i.e. (32-127))"
)] )]
InvalidCredentials(#[from] reqwest::header::InvalidHeaderValue), InvalidCredentials(#[from] reqwest::header::InvalidHeaderValue),
} }
@@ -107,14 +104,14 @@ impl ClientBuilder<RequiresNothing> {
self self
} }
pub fn without_ssl(mut self) -> Self { pub fn without_ssl(mut self) -> Self {
self.ssl = SslStrategy::NoSll; self.ssl = SslStrategy::NoSsl;
self self
} }
pub fn ignore_hostname_verification(mut self) -> Self { pub fn ignore_hostname_verification(mut self) -> Self {
self.ssl = SslStrategy::IngoreHostname; self.ssl = SslStrategy::IgnoreHostname;
self self
} }
pub fn ignore_certificate_veritifcation(mut self) -> Self { pub fn ignore_certificate_verification(mut self) -> Self {
self.ssl = SslStrategy::IgnoreCertificate; self.ssl = SslStrategy::IgnoreCertificate;
self self
} }
@@ -128,7 +125,7 @@ impl ClientBuilder<RequiresNothing> {
} }
fn protocol(&self) -> &str { fn protocol(&self) -> &str {
if self.ssl == SslStrategy::NoSll { if self.ssl == SslStrategy::NoSsl {
"http" "http"
} else { } else {
"https" "https"
@@ -137,7 +134,7 @@ impl ClientBuilder<RequiresNothing> {
fn port(&self) -> u16 { fn port(&self) -> u16 {
if self.port != 0 { if self.port != 0 {
self.port self.port
} else if self.ssl == SslStrategy::NoSll { } else if self.ssl == SslStrategy::NoSsl {
80 80
} else { } else {
443 443
@@ -165,7 +162,7 @@ impl ClientBuilder<RequiresNothing> {
let mut builder = reqwest::ClientBuilder::new() let mut builder = reqwest::ClientBuilder::new()
.default_headers(headers) .default_headers(headers)
.danger_accept_invalid_certs(self.ssl == SslStrategy::IgnoreCertificate) .danger_accept_invalid_certs(self.ssl == SslStrategy::IgnoreCertificate)
.danger_accept_invalid_hostnames(self.ssl == SslStrategy::IngoreHostname); .danger_accept_invalid_hostnames(self.ssl == SslStrategy::IgnoreHostname);
if let Some(sock) = self.resolve { if let Some(sock) = self.resolve {
builder = builder.resolve(&self.hostname, sock); builder = builder.resolve(&self.hostname, sock);
@@ -181,7 +178,7 @@ impl ClientBuilder<RequiresNothing> {
}; };
// checkmk api struggles with more than 10 requests at a time // checkmk api struggles with more than 10 requests at a time
// not sure if this has been inproved since 2.2 // not sure if this has been improved since 2.2
let semaphore = Semaphore::new(10); let semaphore = Semaphore::new(10);
Ok(Client(Arc::new(InnerClient { Ok(Client(Arc::new(InnerClient {
@@ -206,12 +203,15 @@ impl Client {
} }
impl InnerClient { impl InnerClient {
#[inline]
pub(crate) fn object_url(&self, domain_type: DomainType, id: impl Display) -> String { pub(crate) fn object_url(&self, domain_type: DomainType, id: impl Display) -> String {
format!("{}/objects/{}/{}", self.url, domain_type, id) format!("{}/objects/{}/{}", self.url, domain_type, id)
} }
#[inline]
pub(crate) fn collection_url(&self, domain_type: DomainType) -> String { pub(crate) fn collection_url(&self, domain_type: DomainType) -> String {
format!("{}/domain-types/{}/collections/all", self.url, domain_type) format!("{}/domain-types/{}/collections/all", self.url, domain_type)
} }
#[inline]
pub(crate) fn bulk_action_url(&self, domain_type: DomainType, action: BulkAction) -> String { pub(crate) fn bulk_action_url(&self, domain_type: DomainType, action: BulkAction) -> String {
format!( format!(
"{}/domain-types/{}/actions/bulk-{}/invoke", "{}/domain-types/{}/actions/bulk-{}/invoke",
@@ -222,15 +222,9 @@ impl InnerClient {
pub(crate) async fn handle_request(&self, request: Request) -> Result<String> { pub(crate) async fn handle_request(&self, request: Request) -> Result<String> {
let permit = self.semaphore.acquire().await.unwrap(); let permit = self.semaphore.acquire().await.unwrap();
debug!("sending {}-request to {}", request.method(), request.url()); debug!("sending {}-request to {}", request.method(), request.url());
trace!( if let Some(body) = request.body().as_ref().and_then(|b| b.as_bytes()) {
"with body: {}", trace!("with body: {}", String::from_utf8_lossy(body))
request }
.body()
.as_ref()
.and_then(|b| b.as_bytes())
.map(String::from_utf8_lossy)
.unwrap_or_default()
);
let response = self let response = self
.http .http
@@ -282,121 +276,141 @@ impl InnerClient {
} }
} }
pub(crate) async fn create_domain_object<E: DomainExtention>( pub(crate) async fn create<O, Q>(
&self, &self,
body: &E::CreationRequest, domain_type: DomainType,
query: &E::CreationQuery, object: &O,
) -> Result<()> { query: &Q,
) -> Result<()>
where
O: Serialize,
Q: Serialize,
{
let url = self.collection_url(domain_type);
let request = self let request = self
.http .http
.post(self.collection_url(E::DOMAIN_TYPE)) .post(url)
.json(body) .json(object)
.query(query) .query(query)
.build() .build()
.unwrap(); .unwrap();
self.invoke_api(request).await self.invoke_api(request).await
} }
pub(crate) async fn read_domain_object<E: DomainExtention>(
pub(crate) async fn read<O, Q>(
&self, &self,
domain_type: DomainType,
id: impl Display, id: impl Display,
query: &E::ReadQuery, query: &Q,
) -> Result<DomainObject<E>> { ) -> Result<O>
let request = self where
.http O: DeserializeOwned,
.get(self.object_url(E::DOMAIN_TYPE, id)) Q: Serialize,
.query(query) {
.build() let url = self.object_url(domain_type, id);
.unwrap(); let request = self.http.get(url).query(query).build().unwrap();
self.query_api(request).await self.query_api(request).await
} }
pub(crate) async fn update_domain_object<E: DomainExtention>( pub(crate) async fn update<O>(
&self, &self,
domain_type: DomainType,
id: impl Display, id: impl Display,
request: &E::UpdateRequest, object: &O,
) -> Result<()> { ) -> Result<()>
let url = self.object_url(E::DOMAIN_TYPE, id); where
O: Serialize,
{
let url = self.object_url(domain_type, id);
let etag = self.get_etag(&url).await?; let etag = self.get_etag(&url).await?;
let request = self let request = self
.http .http
.put(url) .put(url)
.json(&request) .json(object)
.header(reqwest::header::IF_MATCH, etag) .header(reqwest::header::IF_MATCH, etag)
.build() .build()
.unwrap(); .unwrap();
self.invoke_api(request).await self.invoke_api(request).await
} }
pub(crate) async fn delete_domain_object<E: DomainExtention>( pub(crate) async fn delete<Q>(
&self, &self,
domain_type: DomainType,
id: impl Display, id: impl Display,
query: &E::DeleteQuery, query: &Q,
) -> Result<()> { ) -> Result<()>
let request = self where
.http Q: Serialize,
.delete(self.object_url(E::DOMAIN_TYPE, id)) {
.query(&query) let url = self.object_url(domain_type, id);
.build() let request = self.http.delete(url).query(&query).build().unwrap();
.unwrap();
self.invoke_api(request).await self.invoke_api(request).await
} }
pub(crate) async fn bulk_create_domain_objects<E: BulkCreateDomainExtention>( pub(crate) async fn bulk_create<O, Q>(
&self, &self,
entries: &[E::CreationRequest], domain_type: DomainType,
query: &E::CreationQuery, entries: &[O],
) -> Result<()> { query: &Q,
) -> Result<()>
where
O: Serialize,
Q: Serialize,
{
#[derive(Serialize)] #[derive(Serialize)]
struct Request<'a, D: BulkCreateDomainExtention> { struct Request<'a, E: Serialize> {
entries: &'a [D::CreationRequest], entries: &'a [E],
} }
let url = self.bulk_action_url(domain_type, BulkAction::Create);
let body = Request::<O> { entries };
let request = self let request = self
.http .http
.post(self.bulk_action_url(E::DOMAIN_TYPE, BulkAction::Create)) .post(url)
.json(&Request::<E> { entries }) .json(&body)
.query(query) .query(query)
.build() .build()
.unwrap(); .unwrap();
self.invoke_api(request).await self.invoke_api(request).await
} }
pub(crate) async fn bulk_read_domain_objects<E: BulkReadDomainExtention>(
&self,
query: &E::BulkReadQuery,
) -> Result<Vec<DomainObject<E>>> {
let request = self
.http
.get(self.collection_url(E::DOMAIN_TYPE))
.query(query)
.build()
.unwrap();
let response: DomainCollection<E> = self.query_api(request).await?; pub(crate) async fn bulk_read<O, Q>(
Ok(response.value)
}
pub(crate) async fn bulk_update_domain_objects<E: BulkUpdateDomainExtention>(
&self, &self,
entries: &[E::BulkUpdateRequest], domain_type: DomainType,
) -> Result<()> { query: &Q,
) -> Result<DomainCollection<O>>
where
O: DeserializeOwned,
Q: Serialize,
{
let url = self.collection_url(domain_type);
let request = self.http.get(url).query(query).build().unwrap();
self.query_api(request).await
}
pub(crate) async fn bulk_update<O>(&self, domain_type: DomainType, entries: &[O]) -> Result<()>
where
O: Serialize,
{
#[derive(Serialize)] #[derive(Serialize)]
struct Request<'a, D: BulkUpdateDomainExtention> { struct Request<'a, E: Serialize> {
entries: &'a [D::BulkUpdateRequest], entries: &'a [E],
} }
let request = self let url = self.bulk_action_url(domain_type, BulkAction::Update);
.http let body = Request::<O> { entries };
.put(self.bulk_action_url(E::DOMAIN_TYPE, BulkAction::Update))
.json(&Request::<E> { entries }) let request = self.http.put(url).json(&body).build().unwrap();
.build()
.unwrap();
self.invoke_api(request).await self.invoke_api(request).await
} }
pub(crate) async fn bulk_delete_domain_objects<E: BulkDeleteDomainExtention>( pub(crate) async fn bulk_delete(
&self, &self,
domain_type: DomainType,
entries: &[String], entries: &[String],
) -> Result<()> { ) -> Result<()> {
#[derive(Serialize)] #[derive(Serialize)]
@@ -404,12 +418,10 @@ impl InnerClient {
entries: &'a [String], entries: &'a [String],
} }
let request = self let url = self.bulk_action_url(domain_type, BulkAction::Delete);
.http let body = Request { entries };
.post(self.bulk_action_url(E::DOMAIN_TYPE, BulkAction::Delete))
.json(&Request { entries }) let request = self.http.post(url).json(&body).build().unwrap();
.build()
.unwrap();
self.invoke_api(request).await self.invoke_api(request).await
} }
@@ -433,70 +445,16 @@ impl Display for BulkAction {
} }
} }
pub struct ApiClient<D: DomainExtention> { pub struct ApiClient<D> {
pub(crate) inner: Arc<InnerClient>, pub(crate) inner: Arc<InnerClient>,
_marker: PhantomData<D>, _marker: PhantomData<D>,
} }
impl<D: DomainExtention> From<&Client> for ApiClient<D> { impl<D> ApiClient<D> {
fn from(value: &Client) -> Self { pub(crate) fn new(inner: &Client) -> Self {
Self { Self {
inner: value.0.clone(), inner: inner.0.clone(),
_marker: Default::default(), _marker: Default::default(),
} }
} }
} }
impl<D: DomainExtention> ApiClient<D> {
pub async fn create(
&self,
request: &D::CreationRequest,
query: &D::CreationQuery,
) -> Result<()> {
self.inner.create_domain_object::<D>(request, query).await
}
pub async fn read(&self, id: impl Display, query: &D::ReadQuery) -> Result<D> {
self.inner
.read_domain_object(id, query)
.await
.map(|obj| obj.extensions)
}
pub async fn update(&self, id: impl Display, request: &D::UpdateRequest) -> Result<()> {
self.inner.update_domain_object::<D>(id, request).await
}
pub async fn delete(&self, id: impl Display, query: &D::DeleteQuery) -> Result<()> {
self.inner.delete_domain_object::<D>(id, query).await
}
}
impl<D: BulkCreateDomainExtention> ApiClient<D> {
pub async fn bulk_create(
&self,
request: &[D::CreationRequest],
query: &D::CreationQuery,
) -> Result<()> {
self.inner
.bulk_create_domain_objects::<D>(request, query)
.await
}
}
impl<D: BulkReadDomainExtention> ApiClient<D> {
pub async fn bulk_read(&self, query: &D::BulkReadQuery) -> Result<Vec<D>> {
let objs = self.inner.bulk_read_domain_objects::<D>(query).await?;
let exts = objs.into_iter().map(|obj| obj.extensions).collect();
Ok(exts)
}
}
impl<D: BulkUpdateDomainExtention> ApiClient<D> {
pub async fn bulk_update(&self, entries: &[D::BulkUpdateRequest]) -> Result<()> {
self.inner.bulk_update_domain_objects::<D>(entries).await
}
}
impl<D: BulkDeleteDomainExtention> ApiClient<D> {
pub async fn bulk_delete(&self, ids: &[String]) -> Result<()> {
self.inner.bulk_delete_domain_objects::<D>(ids).await
}
}
+4 -2
View File
@@ -15,14 +15,14 @@ pub enum Error {
#[error( #[error(
"Failed to send request: {}: {}", .0, "Failed to send request: {}: {}", .0,
.0.cause().map(|e| e.to_string()) .0.cause().map(|e| e.to_string())
.unwrap_or_else(|| "Uknown Cause".to_string()) .unwrap_or_else(|| "Unknown Cause".to_string())
)] )]
SendRequest(#[source] reqwest::Error), SendRequest(#[source] reqwest::Error),
#[error("Failed to receive response-body: {0}")] #[error("Failed to receive response-body: {0}")]
ReceiveBody(#[source] reqwest::Error), ReceiveBody(#[source] reqwest::Error),
#[error("Failed to deserialize response: {0}")] #[error("Failed to deserialize response: {0}")]
DeserializeResponse(#[source] serde_json::Error), DeserializeResponse(#[source] serde_json::Error),
#[error("Recieved an error from checkmk ({}): {}", .0.status, .0.detail)] #[error("Received an error from checkmk ({}): {}", .0.status, .0.detail)]
CheckmkError(#[source] CheckmkError), CheckmkError(#[source] CheckmkError),
#[error("Checkmk site has not yet started. log in as site user and execute 'omd start'")] #[error("Checkmk site has not yet started. log in as site user and execute 'omd start'")]
@@ -32,6 +32,8 @@ pub enum Error {
MissingHeader(&'static str), MissingHeader(&'static str),
#[error("Failed to parse ETag header: {0}")] #[error("Failed to parse ETag header: {0}")]
ParseEtag(#[source] std::string::FromUtf8Error), ParseEtag(#[source] std::string::FromUtf8Error),
#[error("failed to parse domain object id: {0}")]
ParseId(#[from] uuid::Error),
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
+1 -2
View File
@@ -2,6 +2,5 @@ pub mod api;
mod client; mod client;
mod error; mod error;
pub(crate) use client::ApiClient; pub use client::{ApiClient, Client, ClientBuilder};
pub use client::{Client, ClientBuilder};
pub use error::{Error, Result}; pub use error::{Error, Result};
+432 -3
View File
@@ -10,7 +10,9 @@ const PASSWORD: &str = "C5t71DUPAu2D";
async fn main() -> Result<()> { async fn main() -> Result<()> {
simplelog::TermLogger::init( simplelog::TermLogger::init(
simplelog::LevelFilter::Trace, simplelog::LevelFilter::Trace,
simplelog::Config::default(), simplelog::ConfigBuilder::new()
.set_location_level(simplelog::LevelFilter::Error)
.build(),
simplelog::TerminalMode::Stderr, simplelog::TerminalMode::Stderr,
simplelog::ColorChoice::Auto, simplelog::ColorChoice::Auto,
) )
@@ -24,7 +26,15 @@ async fn main() -> Result<()> {
.unwrap(); .unwrap();
test_folders(client.clone()).await?; test_folders(client.clone()).await?;
test_hosts(client.clone()).await?;
test_hosttags(client.clone()).await?;
test_host_groups(client.clone()).await?;
test_service_groups(client.clone()).await?;
test_contact_groups(client.clone()).await?;
test_rulesets(client.clone()).await?;
test_activations(client.clone()).await?;
info!("tests done; all pass");
Ok(()) Ok(())
} }
@@ -45,14 +55,16 @@ async fn test_folders(client: Client) -> Result<()> {
info!("creating testfolder"); info!("creating testfolder");
folderapi folderapi
.create(&creq, &()) .create(&creq)
.await .await
.inspect_err(|e| error!("failed to create folder: {e}"))?; .inspect_err(|e| error!("failed to create folder: {e}"))?;
let folder = folderapi let folder = folderapi
.read(TESTDIRQRY, &()) .read(TESTDIRQRY)
.await .await
.inspect_err(|e| error!("failed to read folder: {e}"))?; .inspect_err(|e| error!("failed to read folder: {e}"))?;
info!("folder config on site: {folder:?}"); info!("folder config on site: {folder:?}");
info!("updating folder"); info!("updating folder");
folderapi folderapi
.update(TESTDIRQRY, &ureq1) .update(TESTDIRQRY, &ureq1)
@@ -64,6 +76,7 @@ async fn test_folders(client: Client) -> Result<()> {
.await .await
.inspect_err(|e| error!("failed to read all folders: {e}")); .inspect_err(|e| error!("failed to read all folders: {e}"));
info!("folder config on site: {folders:?}"); info!("folder config on site: {folders:?}");
folderapi folderapi
.bulk_update(&[FolderBulkUpdateRequest::new(TESTDIRQRY.to_string(), ureq2)]) .bulk_update(&[FolderBulkUpdateRequest::new(TESTDIRQRY.to_string(), ureq2)])
.await .await
@@ -77,3 +90,419 @@ async fn test_folders(client: Client) -> Result<()> {
Ok(()) Ok(())
} }
async fn test_hosts(client: Client) -> Result<()> {
use checkmk_api::api::folders::FolderAttributes;
use checkmk_api::api::hosts::*;
const TESTHOST: &str = "test-host";
info!("testing hosts");
let hostapi = client.host_api();
let creq = HostCreationRequest::new(
TESTHOST.to_string(),
"~".to_string(),
FolderAttributes::default().with_site("dev".to_string()),
);
let cquery = HostCreationQuery::new(false);
let rquery = HostReadQuery::new(true);
let ureq1 = HostUpdateRequest::replace(
FolderAttributes::default()
.with_site("dev".to_string())
.with_label("environment".to_string(), "test".to_string()),
);
let ureq2 = HostUpdateRequest::update(
FolderAttributes::default().with_label("purpose".to_string(), "testing".to_string()),
);
info!("creating test host");
hostapi
.create(&creq, &cquery)
.await
.inspect_err(|e| error!("failed to create host: {e}"))?;
let host = hostapi
.read(TESTHOST, &rquery)
.await
.inspect_err(|e| error!("failed to read host: {e}"))?;
info!("host config: {host:#?}");
info!("updating host");
hostapi
.update(TESTHOST, &ureq1)
.await
.inspect_err(|e| error!("failed to update host: {e}"))?;
let hosts = hostapi
.bulk_read(&HostBulkReadQuery::new().with_effective_attributes(true))
.await
.inspect_err(|e| error!("failed to read all hosts: {e}"));
info!("all hosts: {hosts:?}");
hostapi
.bulk_update(&[HostBulkUpdateRequest::new(TESTHOST.to_string(), ureq2)])
.await
.inspect_err(|e| error!("failed to do a bulk update of hosts: {e}"))?;
info!("deleting host");
hostapi
.delete(TESTHOST)
.await
.inspect_err(|e| error!("failed to delete host: {e}"))?;
Ok(())
}
async fn test_hosttags(client: Client) -> Result<()> {
use checkmk_api::api::tags::*;
const TESTTAG: &str = "test-tag-group";
info!("testing hosttags");
let tagapi = client.tag_api();
let creq = ExtendedTagGroup::new(TESTTAG.to_string(), "Test Tag Group".to_string())
.with_topic("Testing".to_string())
.with_help("A test tag group".to_string())
.with_tag(Tag::new("test1".to_string(), "Test Tag 1".to_string()))
.with_tag(Tag::new("test2".to_string(), "Test Tag 2".to_string()));
let ureq = TagUpdateRequest::new("Updated Test Tag Group".to_string(), false)
.with_topic("Updated Testing".to_string())
.with_help("An updated test tag group".to_string())
.with_tag(Tag::new("test3".to_string(), "Test Tag 3".to_string()));
info!("creating test tag group");
tagapi
.create(&creq)
.await
.inspect_err(|e| error!("failed to create tag group: {e}"))?;
let tag_group = tagapi
.read(TESTTAG)
.await
.inspect_err(|e| error!("failed to read tag group: {e}"))?;
info!("tag group config: {tag_group:#?}");
info!("updating tag group");
tagapi
.update(TESTTAG, &ureq)
.await
.inspect_err(|e| error!("failed to update tag group: {e}"))?;
let tag_groups = tagapi
.bulk_read()
.await
.inspect_err(|e| error!("failed to read all tag groups: {e}"));
info!("all tag groups: {tag_groups:?}");
info!("deleting tag group");
tagapi
.delete(TESTTAG)
.await
.inspect_err(|e| error!("failed to delete tag group: {e}"))?;
Ok(())
}
async fn test_host_groups(client: Client) -> Result<()> {
use checkmk_api::api::groups::*;
const TESTGROUP: &str = "test-host-group";
info!("testing host groups");
let api = client.host_group_api();
info!("creating test host group");
api.create(&HostGroup::new(
TESTGROUP.to_string(),
"Test Host Group".to_string(),
))
.await
.inspect_err(|e| error!("failed to create host group: {e}"))?;
let group = api
.read(TESTGROUP)
.await
.inspect_err(|e| error!("failed to read host group: {e}"))?;
info!("host group config: {group:#?}");
info!("updating host group");
api.update(TESTGROUP, "Updated Test Host Group")
.await
.inspect_err(|e| error!("failed to update host group: {e}"))?;
info!("bulk creating additional host groups");
api.bulk_create(&[
HostGroup::new(
"test-host-group-2".to_string(),
"Test Host Group 2".to_string(),
),
HostGroup::new(
"test-host-group-3".to_string(),
"Test Host Group 3".to_string(),
),
])
.await
.inspect_err(|e| error!("failed to bulk create host groups: {e}"))?;
let groups = api
.bulk_read()
.await
.inspect_err(|e| error!("failed to bulk read host groups: {e}"));
info!("all host groups: {groups:?}");
info!("bulk updating host groups");
api.bulk_update(&[
Group::new(
"test-host-group-2".to_string(),
"Updated Host Group 2".to_string(),
),
Group::new(
"test-host-group-3".to_string(),
"Updated Host Group 3".to_string(),
),
])
.await
.inspect_err(|e| error!("failed to bulk update host groups: {e}"))?;
info!("bulk deleting extra host groups");
api.bulk_delete(&[
"test-host-group-2".to_string(),
"test-host-group-3".to_string(),
])
.await
.inspect_err(|e| error!("failed to bulk delete host groups: {e}"))?;
info!("deleting test host group");
api.delete(TESTGROUP)
.await
.inspect_err(|e| error!("failed to delete host group: {e}"))?;
Ok(())
}
async fn test_service_groups(client: Client) -> Result<()> {
use checkmk_api::api::groups::*;
const TESTGROUP: &str = "test-service-group";
info!("testing service groups");
let api = client.service_group_api();
info!("creating test service group");
api.create(&ServiceGroup::new(
TESTGROUP.to_string(),
"Test Service Group".to_string(),
))
.await
.inspect_err(|e| error!("failed to create service group: {e}"))?;
let group = api
.read(TESTGROUP)
.await
.inspect_err(|e| error!("failed to read service group: {e}"))?;
info!("service group config: {group:#?}");
info!("updating service group");
api.update(TESTGROUP, "Updated Test Service Group")
.await
.inspect_err(|e| error!("failed to update service group: {e}"))?;
info!("bulk creating additional service groups");
api.bulk_create(&[
ServiceGroup::new(
"test-service-group-2".to_string(),
"Test Service Group 2".to_string(),
),
ServiceGroup::new(
"test-service-group-3".to_string(),
"Test Service Group 3".to_string(),
),
])
.await
.inspect_err(|e| error!("failed to bulk create service groups: {e}"))?;
let groups = api
.bulk_read()
.await
.inspect_err(|e| error!("failed to bulk read service groups: {e}"));
info!("all service groups: {groups:?}");
info!("bulk updating service groups");
api.bulk_update(&[
Group::new(
"test-service-group-2".to_string(),
"Updated Service Group 2".to_string(),
),
Group::new(
"test-service-group-3".to_string(),
"Updated Service Group 3".to_string(),
),
])
.await
.inspect_err(|e| error!("failed to bulk update service groups: {e}"))?;
info!("bulk deleting extra service groups");
api.bulk_delete(&[
"test-service-group-2".to_string(),
"test-service-group-3".to_string(),
])
.await
.inspect_err(|e| error!("failed to bulk delete service groups: {e}"))?;
info!("deleting test service group");
api.delete(TESTGROUP)
.await
.inspect_err(|e| error!("failed to delete service group: {e}"))?;
Ok(())
}
async fn test_activations(client: Client) -> Result<()> {
use checkmk_api::api::activations::*;
info!("testing activations");
let api = client.activation_api();
let pending = api
.read_pending_changes()
.await
.inspect_err(|e| error!("failed to read pending changes: {e}"))?;
info!("pending changes: {pending:#?}");
let running = api
.read_running_activations()
.await
.inspect_err(|e| error!("failed to read running activations: {e}"))?;
info!("running activations: {running:#?}");
let options = ActivationOptions::default()
.with_redirect(false)
.with_force_foreign_changes(false)
.with_site(SITENAME.to_string());
info!("activating pending changes");
let activation_id = api
.activate_pending_changes(&options)
.await
.inspect_err(|e| error!("failed to activate pending changes: {e}"))?;
info!("activation started with id: {activation_id}");
info!("waiting for activation {activation_id} to complete");
api.await_completion(&activation_id)
.await
.inspect_err(|e| error!("failed to await activation completion: {e}"))?;
info!("activation {activation_id} completed");
Ok(())
}
async fn test_rulesets(client: Client) -> Result<()> {
use checkmk_api::api::rules::*;
const KNOWN_RULESET: &str = "inv_exports:software_csv";
info!("testing rulesets");
let api = client.ruleset_api();
info!("reading single ruleset '{KNOWN_RULESET}'");
let ruleset = api
.read(KNOWN_RULESET)
.await
.inspect_err(|e| error!("failed to read ruleset: {e}"))?;
info!("ruleset: {ruleset:#?}");
info!("bulk reading all rulesets");
let all = api
.bulk_read(&RulesetQuery::default())
.await
.inspect_err(|e| error!("failed to bulk read rulesets: {e}"))?;
info!("total rulesets: {}", all.len());
info!("bulk reading only used rulesets");
let used = api
.bulk_read(&RulesetQuery::default().with_used(true))
.await
.inspect_err(|e| error!("failed to bulk read used rulesets: {e}"))?;
info!("used rulesets: {}", used.len());
info!("bulk reading rulesets with fulltext search");
let search = api
.bulk_read(&RulesetQuery::default().with_fulltext("memory".to_string()))
.await
.inspect_err(|e| error!("failed to bulk read rulesets by fulltext: {e}"))?;
info!("rulesets matching 'memory': {}", search.len());
Ok(())
}
async fn test_contact_groups(client: Client) -> Result<()> {
use checkmk_api::api::groups::*;
const TESTGROUP: &str = "test-contact-group";
info!("testing contact groups");
let api = client.contact_group_api();
info!("creating test contact group");
api.create(&ContactGroup::new(
TESTGROUP.to_string(),
"Test Contact Group".to_string(),
))
.await
.inspect_err(|e| error!("failed to create contact group: {e}"))?;
let group = api
.read(TESTGROUP)
.await
.inspect_err(|e| error!("failed to read contact group: {e}"))?;
info!("contact group config: {group:#?}");
info!("updating contact group");
api.update(TESTGROUP, "Updated Test Contact Group")
.await
.inspect_err(|e| error!("failed to update contact group: {e}"))?;
info!("bulk creating additional contact groups");
api.bulk_create(&[
ContactGroup::new(
"test-contact-group-2".to_string(),
"Test Contact Group 2".to_string(),
),
ContactGroup::new(
"test-contact-group-3".to_string(),
"Test Contact Group 3".to_string(),
),
])
.await
.inspect_err(|e| error!("failed to bulk create contact groups: {e}"))?;
let groups = api
.bulk_read()
.await
.inspect_err(|e| error!("failed to bulk read contact groups: {e}"));
info!("all contact groups: {groups:?}");
info!("bulk updating contact groups");
api.bulk_update(&[
Group::new(
"test-contact-group-2".to_string(),
"Updated Contact Group 2".to_string(),
),
Group::new(
"test-contact-group-3".to_string(),
"Updated Contact Group 3".to_string(),
),
])
.await
.inspect_err(|e| error!("failed to bulk update contact groups: {e}"))?;
info!("bulk deleting extra contact groups");
api.bulk_delete(&[
"test-contact-group-2".to_string(),
"test-contact-group-3".to_string(),
])
.await
.inspect_err(|e| error!("failed to bulk delete contact groups: {e}"))?;
info!("deleting test contact group");
api.delete(TESTGROUP)
.await
.inspect_err(|e| error!("failed to delete contact group: {e}"))?;
Ok(())
}