Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:jcronenberg:migrate-wicked
migrate-wicked-git
migrate-wicked-git-0.1.git.1713729795.afeae692....
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File migrate-wicked-git-0.1.git.1713729795.afeae692.obscpio of Package migrate-wicked-git
07070100000000000081A40000000000000000000000016625710300000083000000000000000000000000000000000000003A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/.gitignore# Generated by Cargo # will have compiled files and executables /target/ # These are backup files generated by rustfmt **/*.rs.bk 07070100000001000081A40000000000000000000000016625710300018CEB000000000000000000000000000000000000003A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/Cargo.lock# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "agama-cli" version = "1.0.0" dependencies = [ "agama-lib", "agama-settings", "anyhow", "async-trait", "clap", "console", "convert_case", "fs_extra", "home", "indicatif", "log", "nix 0.27.1", "reqwest", "serde", "serde_json", "serde_yaml", "tempfile", "thiserror", "tokio", "zbus", ] [[package]] name = "agama-derive" version = "1.0.0" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "agama-lib" version = "1.0.0" dependencies = [ "agama-settings", "anyhow", "async-trait", "cidr", "curl", "futures-util", "jsonschema", "log", "serde", "serde_json", "tempfile", "thiserror", "tokio", "tokio-stream", "url", "utoipa", "zbus", ] [[package]] name = "agama-locale-data" version = "0.1.0" dependencies = [ "anyhow", "chrono-tz", "flate2", "quick-xml", "regex", "serde", "thiserror", ] [[package]] name = "agama-server" version = "0.1.0" dependencies = [ "agama-lib", "agama-locale-data", "anyhow", "async-trait", "axum", "axum-extra", "chrono", "cidr", "clap", "config", "futures-util", "gettext-rs", "http-body-util", "hyper 1.2.0", "hyper-util", "jsonwebtoken", "log", "macaddr", "once_cell", "openssl", "pam", "rand", "regex", "serde", "serde_json", "serde_with", "serde_yaml", "simplelog", "systemd-journal-logger", "thiserror", "tokio", "tokio-openssl", "tokio-stream", "tower", "tower-http", "tracing", "tracing-journald", "tracing-subscriber", "utoipa", "uuid", "zbus", "zbus_macros", ] [[package]] name = "agama-settings" version = "1.0.0" dependencies = [ "agama-derive", "thiserror", ] [[package]] name = "ahash" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", "once_cell", "serde", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "alloc-no-stdlib" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", "windows-sys 0.52.0", ] [[package]] name = "anyhow" version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "async-broadcast" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" dependencies = [ "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-channel" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", "event-listener 5.2.0", "event-listener-strategy 0.5.0", "futures-core", "pin-project-lite", ] [[package]] name = "async-compression" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" dependencies = [ "brotli", "futures-core", "memchr", "pin-project-lite", "tokio", ] [[package]] name = "async-io" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock 2.8.0", "autocfg", "cfg-if", "concurrent-queue", "futures-lite 1.13.0", "log", "parking", "polling 2.8.0", "rustix 0.37.27", "slab", "socket2 0.4.10", "waker-fn", ] [[package]] name = "async-io" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" dependencies = [ "async-lock 3.3.0", "cfg-if", "concurrent-queue", "futures-io", "futures-lite 2.3.0", "parking", "polling 3.5.0", "rustix 0.38.32", "slab", "tracing", "windows-sys 0.52.0", ] [[package]] name = "async-lock" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener 2.5.3", ] [[package]] name = "async-lock" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ "event-listener 4.0.3", "event-listener-strategy 0.4.0", "pin-project-lite", ] [[package]] name = "async-process" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ "async-io 1.13.0", "async-lock 2.8.0", "async-signal", "blocking", "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", "rustix 0.38.32", "windows-sys 0.48.0", ] [[package]] name = "async-recursion" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "async-signal" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" dependencies = [ "async-io 2.3.2", "async-lock 2.8.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", "rustix 0.38.32", "signal-hook-registry", "slab", "windows-sys 0.48.0", ] [[package]] name = "async-task" version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" version = "0.1.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" dependencies = [ "async-trait", "axum-core", "base64 0.21.7", "bytes", "futures-util", "http 1.1.0", "http-body 1.0.0", "http-body-util", "hyper 1.2.0", "hyper-util", "itoa", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", "rustversion", "serde", "serde_json", "serde_path_to_error", "serde_urlencoded", "sha1", "sync_wrapper", "tokio", "tokio-tungstenite", "tower", "tower-layer", "tower-service", "tracing", ] [[package]] name = "axum-core" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", "futures-util", "http 1.1.0", "http-body 1.0.0", "http-body-util", "mime", "pin-project-lite", "rustversion", "sync_wrapper", "tower-layer", "tower-service", "tracing", ] [[package]] name = "axum-extra" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" dependencies = [ "axum", "axum-core", "bytes", "futures-util", "headers", "http 1.1.0", "http-body 1.0.0", "http-body-util", "mime", "pin-project-lite", "serde", "tower", "tower-layer", "tower-service", ] [[package]] name = "backtrace" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bindgen" version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ "bitflags 2.5.0", "cexpr", "clang-sys", "itertools", "lazy_static", "lazycell", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn 2.0.53", ] [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] [[package]] name = "block" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel", "async-lock 3.3.0", "async-task", "fastrand 2.0.1", "futures-io", "futures-lite 2.3.0", "piper", "tracing", ] [[package]] name = "brotli" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", "brotli-decompressor", ] [[package]] name = "brotli-decompressor" version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] name = "bumpalo" version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytecount" version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", "windows-targets 0.52.4", ] [[package]] name = "chrono-tz" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" dependencies = [ "chrono", "chrono-tz-build", "phf", ] [[package]] name = "chrono-tz-build" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" dependencies = [ "parse-zoneinfo", "phf", "phf_codegen", ] [[package]] name = "cidr" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d18b093eba54c9aaa1e3784d4361eb2ba944cf7d0a932a830132238f483e8d8" dependencies = [ "serde", ] [[package]] name = "clang-sys" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" dependencies = [ "glob", "libc", ] [[package]] name = "clap" version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim 0.11.0", "terminal_size", ] [[package]] name = "clap_derive" version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" dependencies = [ "heck", "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "clap_lex" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] [[package]] name = "config" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ "async-trait", "convert_case", "json5", "lazy_static", "nom", "pathdiff", "ron", "rust-ini", "serde", "serde_json", "toml", "yaml-rust", ] [[package]] name = "console" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", "windows-sys 0.52.0", ] [[package]] name = "const-random" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ "const-random-macro", ] [[package]] name = "const-random-macro" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ "getrandom", "once_cell", "tiny-keccak", ] [[package]] name = "convert_case" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" dependencies = [ "unicode-segmentation", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crunchy" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "curl" version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", "socket2 0.5.6", "windows-sys 0.52.0", ] [[package]] name = "curl-sys" version = "0.4.72+curl-8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", "windows-sys 0.52.0", ] [[package]] name = "darling" version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.10.0", "syn 2.0.53", ] [[package]] name = "darling_macro" version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", "syn 2.0.53", ] [[package]] name = "data-encoding" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", ] [[package]] name = "derivative" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "dlv-list" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" dependencies = [ "const-random", ] [[package]] name = "either" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] [[package]] name = "enumflags2" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener" version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ "event-listener 4.0.3", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" dependencies = [ "event-listener 5.2.0", "pin-project-lite", ] [[package]] name = "fancy-regex" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0678ab2d46fa5195aaf59ad034c083d351377d4af57f3e073c074d0da3e3c766" dependencies = [ "bit-set", "regex", ] [[package]] name = "fastrand" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "flate2" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fraction" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aa5de57a62c2440ece64342ea59efb7171aa7d016faf8dfcb8795066a17146b" dependencies = [ "lazy_static", "num", ] [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ "fastrand 1.9.0", "futures-core", "futures-io", "memchr", "parking", "pin-project-lite", "waker-fn", ] [[package]] name = "futures-lite" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-macro", "futures-sink", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "gettext-rs" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" dependencies = [ "gettext-sys", "locale_config", ] [[package]] name = "gettext-sys" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" dependencies = [ "cc", "temp-dir", ] [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", "indexmap 2.2.5", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "h2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http 1.1.0", "indexmap 2.2.5", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "headers" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ "base64 0.21.7", "bytes", "headers-core", "http 1.1.0", "httpdate", "mime", "sha1", ] [[package]] name = "headers-core" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ "http 1.1.0", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "home" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "http" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http 0.2.12", "pin-project-lite", ] [[package]] name = "http-body" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", "http 1.1.0", ] [[package]] name = "http-body-util" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", "futures-core", "http 1.1.0", "http-body 1.0.0", "pin-project-lite", ] [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2 0.3.25", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", "socket2 0.5.6", "tokio", "tower-service", "tracing", "want", ] [[package]] name = "hyper" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ "bytes", "futures-channel", "futures-util", "h2 0.4.3", "http 1.1.0", "http-body 1.0.0", "httparse", "httpdate", "itoa", "pin-project-lite", "smallvec", "tokio", ] [[package]] name = "hyper-tls" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", ] [[package]] name = "hyper-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", "futures-util", "http 1.1.0", "http-body 1.0.0", "hyper 1.2.0", "pin-project-lite", "socket2 0.5.6", "tokio", ] [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", "serde", ] [[package]] name = "indexmap" version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.3", "serde", ] [[package]] name = "indicatif" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", "instant", "number_prefix", "portable-atomic", "unicode-width", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "iso8601" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "296af15e112ec6dc38c9fd3ae027b5337a75466e8eed757bd7d5cf742ea85eb6" dependencies = [ "nom", ] [[package]] name = "itertools" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "json5" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" dependencies = [ "pest", "pest_derive", "serde", ] [[package]] name = "jsonschema" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ca9e2b45609132ae2214d50482c03aeee78826cd6fd53a8940915b81acedf16" dependencies = [ "ahash", "anyhow", "base64 0.13.1", "bytecount", "fancy-regex", "fraction", "iso8601", "itoa", "lazy_static", "memchr", "num-cmp", "parking_lot", "percent-encoding", "regex", "serde", "serde_json", "time", "url", "uuid", ] [[package]] name = "jsonwebtoken" version = "9.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" dependencies = [ "base64 0.21.7", "js-sys", "pem", "ring", "serde", "serde_json", "simple_asn1", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libsystemd" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88b9597a67aa1c81a6624603e6bd0bcefb9e0f94c9c54970ec53771082104b4e" dependencies = [ "hmac", "libc", "log", "nix 0.26.4", "nom", "once_cell", "serde", "sha2", "thiserror", "uuid", ] [[package]] name = "libz-sys" version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "locale_config" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" dependencies = [ "lazy_static", "objc", "objc-foundation", "regex", "winapi", ] [[package]] name = "lock_api" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ "value-bag", ] [[package]] name = "macaddr" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" [[package]] name = "malloc_buf" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ "libc", ] [[package]] name = "matchit" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "memoffset" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", "windows-sys 0.48.0", ] [[package]] name = "native-tls" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "nix" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", "memoffset 0.7.1", ] [[package]] name = "nix" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ "bitflags 2.5.0", "cfg-if", "libc", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "num" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" dependencies = [ "num-bigint", "num-complex", "num-integer", "num-iter", "num-rational", "num-traits", ] [[package]] name = "num-bigint" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "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.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" dependencies = [ "num-traits", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", "num-bigint", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "num_threads" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] [[package]] name = "number_prefix" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "objc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", ] [[package]] name = "objc-foundation" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" dependencies = [ "block", "objc", "objc_id", ] [[package]] name = "objc_id" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" dependencies = [ "objc", ] [[package]] name = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" version = "0.9.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "ordered-multimap" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" dependencies = [ "dlv-list", "hashbrown 0.13.2", ] [[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pam" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba" dependencies = [ "libc", "memchr", "pam-macros", "pam-sys", "users", ] [[package]] name = "pam-macros" version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "pam-sys" version = "1.0.0-alpha5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b" dependencies = [ "bindgen", "libc", ] [[package]] name = "parking" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.48.5", ] [[package]] name = "parse-zoneinfo" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" dependencies = [ "regex", ] [[package]] name = "pathdiff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pem" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" dependencies = [ "base64 0.21.7", "serde", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" dependencies = [ "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" dependencies = [ "pest", "pest_generator", ] [[package]] name = "pest_generator" version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "pest_meta" version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" dependencies = [ "once_cell", "pest", "sha2", ] [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator", "phf_shared", ] [[package]] name = "phf_generator" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", ] [[package]] name = "phf_shared" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] [[package]] name = "pin-project" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", "fastrand 2.0.1", "futures-io", ] [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", "bitflags 1.3.2", "cfg-if", "concurrent-queue", "libc", "log", "pin-project-lite", "windows-sys 0.48.0", ] [[package]] name = "polling" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" dependencies = [ "cfg-if", "concurrent-queue", "pin-project-lite", "rustix 0.38.32", "tracing", "windows-sys 0.52.0", ] [[package]] name = "portable-atomic" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", "toml_edit 0.19.15", ] [[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", "syn 1.0.109", "version_check", ] [[package]] name = "proc-macro-error-attr" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", "version_check", ] [[package]] name = "proc-macro2" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" dependencies = [ "memchr", "serde", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", "bytes", "encoding_rs", "futures-core", "futures-util", "h2 0.3.25", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", "hyper-tls", "ipnet", "js-sys", "log", "mime", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "winreg", ] [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "ron" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", "bitflags 2.5.0", "serde", "serde_derive", ] [[package]] name = "rust-ini" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" dependencies = [ "cfg-if", "ordered-multimap", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] [[package]] name = "rustix" version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ "bitflags 2.5.0", "errno", "libc", "linux-raw-sys 0.4.13", "windows-sys 0.52.0", ] [[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64 0.21.7", ] [[package]] name = "rustversion" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schannel" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "serde_json" version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_path_to_error" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ "itoa", "serde", ] [[package]] name = "serde_repr" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "serde_spanned" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "serde_with" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" dependencies = [ "base64 0.21.7", "chrono", "hex", "indexmap 1.9.3", "indexmap 2.2.5", "serde", "serde_derive", "serde_json", "serde_with_macros", "time", ] [[package]] name = "serde_with_macros" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" dependencies = [ "darling", "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "serde_yaml" version = "0.9.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0623d197252096520c6f2a5e1171ee436e5af99a5d7caa2891e55e61950e6d9" dependencies = [ "indexmap 2.2.5", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "simple_asn1" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", "thiserror", "time", ] [[package]] name = "simplelog" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" dependencies = [ "log", "termcolor", "time", ] [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] [[package]] name = "socket2" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "system-configuration" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "systemd-journal-logger" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "356b5cb52ce54916cbfaee19b07d305c7ea8ce5435a088c58743d4a0211f3eff" dependencies = [ "libsystemd", "log", ] [[package]] name = "temp-dir" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd16aa9ffe15fe021c6ee3766772132c6e98dfa395a167e16864f61a9cfb71d6" [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand 2.0.1", "rustix 0.38.32", "windows-sys 0.52.0", ] [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "terminal_size" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix 0.38.32", "windows-sys 0.48.0", ] [[package]] name = "thiserror" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "time" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", "libc", "num-conv", "num_threads", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", "signal-hook-registry", "socket2 0.5.6", "tokio-macros", "tracing", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-openssl" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ffab79df67727f6acf57f1ff743091873c24c579b1e2ce4d8f53e47ded4d63d" dependencies = [ "futures-util", "openssl", "openssl-sys", "tokio", ] [[package]] name = "tokio-stream" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-tungstenite" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", "tokio", "tungstenite", ] [[package]] name = "tokio-util" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", "tracing", ] [[package]] name = "toml" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit 0.22.9", ] [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.2.5", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ "indexmap 2.2.5", "serde", "serde_spanned", "toml_datetime", "winnow 0.6.5", ] [[package]] name = "tower" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", "pin-project", "pin-project-lite", "tokio", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-http" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "async-compression", "bitflags 2.5.0", "bytes", "futures-core", "http 1.1.0", "http-body 1.0.0", "http-body-util", "pin-project-lite", "tokio", "tokio-util", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-layer" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-journald" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba316a74e8fc3c3896a850dba2375928a9fa171b085ecddfc7c054d39970f3fd" dependencies = [ "libc", "tracing-core", "tracing-subscriber", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "nu-ansi-term", "sharded-slab", "smallvec", "thread_local", "tracing-core", "tracing-log", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", "bytes", "data-encoding", "http 1.1.0", "httparse", "log", "rand", "sha1", "thiserror", "url", "utf-8", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "uds_windows" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ "memoffset 0.9.0", "tempfile", "winapi", ] [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "users" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" dependencies = [ "libc", "log", ] [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "utoipa" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "272ebdfbc99111033031d2f10e018836056e4d2c8e2acda76450ec7974269fa7" dependencies = [ "indexmap 2.2.5", "serde", "serde_json", "utoipa-gen", ] [[package]] name = "utoipa-gen" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3c9f4d08338c1bfa70dde39412a040a884c6f318b3d09aaaf3437a1e52027fc" dependencies = [ "proc-macro-error", "proc-macro2", "quote", "regex", "syn 2.0.53", ] [[package]] name = "uuid" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", "serde", ] [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.53", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm 0.52.4", "windows_aarch64_msvc 0.52.4", "windows_i686_gnu 0.52.4", "windows_i686_msvc 0.52.4", "windows_x86_64_gnu 0.52.4", "windows_x86_64_gnullvm 0.52.4", "windows_x86_64_msvc 0.52.4", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] [[package]] name = "winnow" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" dependencies = [ "memchr", ] [[package]] name = "winreg" version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", "windows-sys 0.48.0", ] [[package]] name = "xdg-home" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" dependencies = [ "libc", "winapi", ] [[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] [[package]] name = "zbus" version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" dependencies = [ "async-broadcast", "async-process", "async-recursion", "async-trait", "byteorder", "derivative", "enumflags2", "event-listener 2.5.3", "futures-core", "futures-sink", "futures-util", "hex", "nix 0.26.4", "once_cell", "ordered-stream", "rand", "serde", "serde_repr", "sha1", "static_assertions", "tokio", "tracing", "uds_windows", "winapi", "xdg-home", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zbus_names" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" dependencies = [ "serde", "static_assertions", "zvariant", ] [[package]] name = "zerocopy" version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", "syn 2.0.53", ] [[package]] name = "zvariant" version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" dependencies = [ "byteorder", "enumflags2", "libc", "serde", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] 07070100000002000081A400000000000000000000000166257103000000F5000000000000000000000000000000000000003A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/Cargo.toml[workspace] members = [ "agama-cli", "agama-server", "agama-derive", "agama-lib", "agama-locale-data", "agama-settings", ] resolver = "2" exclude = [ "migrate-wicked" ] [workspace.package] rust-version = "1.74" edition = "2021" 07070100000003000081A400000000000000000000000166257103000046AC000000000000000000000000000000000000003700000000migrate-wicked-git-0.1.git.1713729795.afeae692/LICENSE GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. <one line to give the program's name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. <signature of Ty Coon>, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. 07070100000004000081A400000000000000000000000166257103000010E5000000000000000000000000000000000000003900000000migrate-wicked-git-0.1.git.1713729795.afeae692/README.md# Agama Command Line and D-Bus Interface This project aims to build a command-line interface for [Agama](https://github.com/yast/agama), a service-based Linux installer featuring a nice web interface. The second aim is D-Bus service that does not depend heavily on YaST to reduce memory consumption and also provide better performance. ## Code organization We have set up [Cargo workspace](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) with three packages: * [agama-lib](./agama-lib): code that can be reused to access the [Agama D-Bus API](https://github.com/yast/agama/blob/master/doc/dbus_api.md) and a model for the configuration settings. * [agama-cli](./agama-cli): code specific to the command line interface. * [agama-derive](./agama-derive): includes a [procedural macro](https://doc.rust-lang.org/reference/procedural-macros.html) to reduce the boilerplate code. * [agama-locale-data](./agama-locale-data): specific library to provide data for localization D-Bus API * [agama-dbus-server](./agama-dbus-server): provides D-Bus API for services implemented in rust ## Status Agama CLI is still a work in progress, although it is already capable of doing a few things: * Querying and setting the configuration for the users, storage and software services. * Handling the auto-installation profiles. * Triggering the *probing* and the *installation* processes. Agama D-Bus API is also a work in progress, but it is already used by Agama. ## Installation You can grab the [RPM package](https://build.opensuse.org/package/show/systemsmanagement:Agama:Devel/agama-cli) from the [systemsmanagement:Agama:Devel](https://build.opensuse.org/project/show/systemsmanagement:Agama:Devel) project. If you prefer, you can install it from sources with [Cargo](https://doc.rust-lang.org/cargo/): ``` git clone https://github.com/openSUSE/agama cd rust cargo install --path . ``` ## Running For D-Bus API just run as root agama-dbus-server binary and it will properly attach to D-Bus. For CLI take into account that you need to run `agama-cli` as root when you want to query or change the Agama configuration. Assuming that the Agama D-Bus service is running, the next command prints the current settings using JSON (hint: you can use `jq` to make result look better): ``` $ sudo agama --format json config show {"user":{"fullName":"","userName":"","password":"","autologin":false},"software":{"product":""}} ``` To set one or multiple parameters, just use the `config set` command: ``` $ sudo agama config set software.product=Tumbleweed user.fullName="Jane Doe" user.userName="jane.doe" user.password="12345" user.autologin=true ``` The following operation can take some time. Please, make sure to read the *Caveats* section for more information. ``` $ sudo agama config show {"user":{"fullName":"Jane Doe","userName":"jane.doe","password":"","autologin":true},"software":{"product":"Tumbleweed"}} ``` If, at some point you want to force a new probing, you can ask Agama to repeat the process again: ``` $ sudo agama probe ``` It is possible to handle auto-installation profiles too: ``` $ agama profile download http://192.168.122.1/profile.jsonnet $ agama profile evaluate profile.jsonnet > profile.json $ agama profile validate profile.json ``` Now that you have a ready to use profile, you can load it into Agama: ``` $ sudo agama config load profile.json ``` ## Building and running You can build and run the project using the `cargo` command: ``` cargo build sudo ./target/debug/agama --help ``` ## A Testing Backend The previous section assumes that the Agama D-Bus services are running on the same machine. For an alternative setup using a containerized backend, see *[How to set up a backend for testing this frontend](./agama-cli/doc/backend-for-testing.md)*. ## Testing OBS Build To test if cargo packages build in OBS push your changes to remote branch. Then do osc checkout of Agama:Staging. Modify `_service` file and point it to your branch in `<param name="revision">`. Run `osc service manualrun obs_scm` and then try `osc build` to build it. If it failed push fixes to your branch and repeat service and build step. ## Caveats * If no product is selected, the `probe` command fails. [c_a_bug]: https://github.com/openSUSE/obs-service-cargo_audit/pull/6 07070100000005000081A4000000000000000000000001662571030000131A000000000000000000000000000000000000003D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/WEB-SERVER.md# Web server development notes This document includes some notes about the usage and development of the new Agama web server. ## Installing Rust and related tools It is recommended to use Rustup to install Rust and related tools. In openSUSE distributions, rustup is available as a package. After rustup is installed, you can proceed to install the toolchain: ``` zypper --non-interactive in rustup rustup install stable ``` In addition to the Rust compiler, the previous command would install some additionall components like `cargo`, `clippy`, `rustfmt`, documentation, etc. Another interesting addition might be [cargo-binstall](https://github.com/cargo-bins/cargo-binstall), which allows to install Rust binaries. If you are fine with this approach, just run: ``` cargo install cargo-binstall ``` ## Setting up PAM The web sever will use [Pluggable Authentication Modules (PAM)](https://github.com/linux-pam/linux-pam) for authentication. For that reason, you need to copy the `agama` service definition for PAM to `/usr/lib/pam.d`. Otherwise, PAM would not know how to authenticate the service: ``` cp share/agama.pam /usr/lib/pam.d/agama ``` For further information, see [Authenticating with PAM](https://doc.opensuse.org/documentation/leap/security/single-html/book-security/index.html#cha-pam). ## Running the server > [!NOTE] > The web server needs to connect to Agama's D-Bus daemon. So you can either start the Agama service > or just start the D-Bus daemon (`sudo bundle exec bin/agamactl -f` from the `service/` directory). You need to run the server as `root`, so you cannot use `cargo run` directly. Instead, just do: ``` $ cargo build $ sudo ./target/debug/agama-web-server serve ``` If it fails to compile, please check whether `clang-devel` and `pam-devel` are installed. By default the server uses port 3000 and listens on all network interfaces. You can use the `--address` option if you want to use a different port or a specific network interface: ``` $ sudo ./target/debug/agama-web-server serve --address :::5678 ``` Some more examples: - Both IPv6 and IPv4, all interfaces: `--address :::5678` - Both IPv6 and IPv4, only local loopback : `--address ::1:5678` - IPv4 only, all interfaces: `--address 0.0.0.0:5678` - IPv4 only, only local loopback : `--address 127.0.0.1:5678` - IPv4, only specific interface: `--address 192.168.1.2:5678` (use the IP address of that interface) The server can optionally listen on a secondary address, use the `--address2` option for that. ## Trying the server You can check whether the server is up and running by just performing a ping: ``` $ curl http://localhost:3000/ping ``` ### Authentication The web server uses a bearer token for HTTP authentication. You can get the token by providing your password to the `/authenticate` endpoint. ``` $ curl http://localhost:3000/authenticate \ -H "Content-Type: application/json" \ -d '{"password": "your-password"}' {"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U"}⏎ ``` Now you can access protected routes by including the token in the header: ``` $ curl -X GET http://localhost:3000/protected \ -H "Accept: application/json" \ -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U" ``` ### Connecting to the websocket You can use `websocat` to connect to the websocket. To install the tool, just run: ``` $ cargo binstall websocat ``` If you did not install `cargo-binstall`, you can do: ``` $ cargo install websocat ``` Now, you can use the following command to connect: ``` $ websocat ws://localhost:3000/ws -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U" ``` ## SSL/TLS (HTTPS) Support The web server supports encrypted communication using the HTTPS protocol. The SSL certificate used by the server can be specified by the `--cert` and `--key` command line options which should point to the PEM files: ``` $ sudo ./target/debug/agama-web-server serve --cert certificate.pem --key key.pem ``` The certificate is expected in the PEM format, if you have a certificate in another format you can convert it using the openSSL tools. If a SSL certificate is not specified via command line then the server generates a self-signed certificate. Currently it is only kept in memory and generated again at each start. The HTTPS protocol is required for external connections, the HTTP connections are automatically redirected to HTTPS. *But it still means that the original HTTP communication can be intercepted by an attacker, do not rely on this redirection!* For internal connections coming from the same machine (via the `http://localhost` URL) the unencrypted HTTP communication is allowed. 07070100000006000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli07070100000007000081A400000000000000000000000166257103000003A5000000000000000000000000000000000000004400000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/Cargo.toml[package] name = "agama-cli" version = "1.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] clap = { version = "4.1.4", features = ["derive", "wrap_help"] } agama-lib = { path="../agama-lib" } agama-settings = { path="../agama-settings" } serde = { version = "1.0.152" } serde_json = "1.0.91" serde_yaml = "0.9.17" indicatif= "0.17.3" thiserror = "1.0.39" convert_case = "0.6.0" console = "0.15.7" anyhow = "1.0.71" log = "0.4" # tempdir, fs_extra, nix is for logs (sub)command tempfile = "3.8.1" fs_extra = "1.3.0" nix = { version = "0.27.1", features = ["user"] } zbus = { version = "3", default-features = false, features = ["tokio"] } tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } async-trait = "0.1.77" reqwest = { version = "0.11", features = ["json"] } home = "0.5.9" [[bin]] name = "agama" path = "src/main.rs" 07070100000008000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/doc07070100000009000081A400000000000000000000000166257103000028AE000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/doc/CLI_API.md# Agama CLI Agama already shipped an initial CLI prototype for managing and driving the installation process. Note that such a CLI was created as a proof of concept, and its current API needs some refactoring. This document is intended to discuss how the new CLI should look like, what patterns to follow, etc. ## CLI Guidelines There already are guidelines for creating modern CLI applications. For example [clig.dev](https://clig.dev/) defines a guide that is agnostic about programming languages and tooling in general, and it can be perfectly used as reference for Agama CLI. ## Command name Some naming recommendations from the guidelines: * Make it a simple, memorable word * Use only lowercase letters, and dashes if you really need to * Keep it short * Make it easy to type Currently we have two executables: `agamactl` for managing the D-Bus services and `agama` for configuring and performing the installation. ## Subcommands Let's list the recommendations from the guidelines: * Be consistent across subcommands. Use the same flag names for the same things, have similar output formatting, etc. * Use consistent names for multiple levels of subcommand. If a complex piece of software has lots of objects and operations that can be performed on those objects, it is a common pattern to use two levels of subcommand for this, where one is a noun and one is a verb. For example, `docker container create`. Be consistent with the verbs you use across different types of objects. * Don’t have ambiguous or similarly-named commands. For example, having two subcommands called “update” and “upgrade” is quite confusing. ## New CLI The API of the current CLI is not consistent. It sometimes uses verbs for the subcommand action (e.g., `agama user clear`), and for other subcommands adjectives or nouns are used (e.g., `agama language selected <id>`). Moreover, there is a subcommand per each area, for example `agama language`, `agama software`, `agama storage`, etc. Having a subcommand for each area is not bad per se, but for some areas like storage the subcommand could grow with too many actions and options. The new CLI could be designed with more generic subcommands and verbs, allowing to configure any installation setting in a standard way. Note that the installation process can be already configured by means of a YAML config file with `agama config load <file>`. And the options currently supported by the config file are: ~~~ --- product: "Tumbleweed" languages: - "es_ES" - "en_US" disks: - /dev/vda - /dev/vdb user: name: "test" fullname: "User Test" password: "12345" autologin: true root: ssh_key: "1234abcd" password: "12345" ~~~ We could extend the `config` subcommand for editing such a config without the need of a subcommand per area. In general, the `config` subcommand should have verbs for the following actions: * To load a YAML config file with the values for the installation. * To edit any value of the config without loading a new complete file again. * To show the current config for the installation. * To validate the current config. Moreover, the CLI should also offer subcommands for these actions: * To ask for the possible values that can be used for some setting (e.g., list of available products). * To start and abort the installation. * To see the installation status. Let's assume we will use `agamactl` for managing D-Bus services and `agama` for driving the installation (the opposite as it is now). The CLI for Agama could look like something similar to this: ~~~ $ agama install Starts the installation. $ agama abort Aborts the installation. $ agama status Prints the current status of the installation process and informs about pending actions (e.g., if there are questions waiting to be answered, if a product is not selected yet, etc). $ agama watch Prints messages from the installation process (e.g., progress, questions, etc). $ agama config load <file> Loads installation config from a YAML file, keeping the rest of the config as it is. $ agama config show [<key>] Prints the current installation config in YAML format. If a <key> is given, then it only prints the content for the given key. $ agama config set <key>=<value> ... Sets a config value for the given key. $ agama config unset <key> Removes the current value for the given key. $ agama config reset [<key>] Sets the default value for the given <key>. If no key is given, then the whole config is reset. $ agama config add <list-key> [<key>=]<value> ... Adds a new entry with all the given key-value pairs to a config list. The key is omitted for a list of scalar values (e.g., languages). $ agama config delete <list-key> [<key>=]<value> ... Deletes any entry matching all the given key-value pairs from a config list. The key is omitted for a list of scalar values. $ agama config check Validates the config and prints errors $ agama info <key> [<value>] Prints info about the given key. If no value is given, then it prints what values are admitted by the given key. If a value is given, then it shows extra info about such a value. $ agama summary [<section>] Prints a summary with the actions to perform in the system. If a section is given (e.g., storage, software, ...), then it only shows the section summary. $ agama questions Prints questions and allows to answer them. ~~~ In those commands `<key>` represents a YAML key from the config file (e.g., `root.ssh_key`) and `<value>` is the value associated to the given key. Note that dots are used for nested keys. Let's see some examples: ~~~ # Set a product $ agama config set product=Tumbleweed # Set user values $ agama config set user.name=linux $ agama config set user.fullname=linux $ agama config set user.password=linux $ agama config set user.name=linux user.fullname=linux user.password=12345 # Unset user $ agama config unset user # Add and delete languages $ agama config add languages en_US $ agama config delete languages en_US # Set storage settings $ agama config set storage.lvm=false $ agama config set storage.encryption_password=12345 # Add and delete candidate devices $ agama config add storage.candidate_devices /dev/sda $ agama config delete storage.candidate_devices /dev/sdb # Add and delete storage volumes $ agama config add storage.volumes mountpoint=/ minsize=10GiB $ agama config delete storage.volumes mountpoint=/home # Reset storage config $ agama config reset storage # Show some config values $ agama config show storage.candidate_devices $ agama config show user # Dump config into a file $ agama config show > ~/config.yaml # Show info of a key $ agama info storage.candidate_devices $ agama info storage.candidate_devices /dev/sda $ agama info languages ~~~ ### Config file The current YAML config file needs to be extended in order to support the new storage proposal settings offered by the D-Bus API: ~~~ ... storage: candidate_devices: - /dev/sda lvm: true encryption_password: 12345 volumes: - mountpoint: / fstype: btrfs - mountpoint: /home fstype: ext4 minsize: 10GiB ~~~ ### Product Selection Agama can automatically infers all the config values, but at least one product must be selected. Selecting a product implies some actions in the D-Bus services (e.g., storage devices are probed). And the D-Bus services might emit some questions if needed (e.g., asking to provide a LUKS password). Because of that, the command for selecting a product could ask questions to the user: ~~~ $ agama config set product=ALP > The device /dev/sda is encrypted. Provide an encryption password if you want to open it (enter to skip): ~~~ Another option would be to avoid asking questions directly, and to request the answer when another command is used (see *D-Bus Questions* section). If a product is not selected yet, then many commands cannot work. In that case, commands should inform about it: ~~~ $ agama config show A product is not selected yet. Please, select a product first: agama config set product=<product>. ~~~ ### D-Bus Questions The CLI should offer a way of answering pending questions. For example, for single product live images the storage proposal is automatically done because the target product is already known. If some questions were emitted during the process, then they have to be answered before continuing using the CLI. Therefore, most of the commands would show a warning to inform about the situation and how to proceed: ~~~ $ agama config show There are pending questions. Please, answer questions first: agama questions. ~~~ ### Non Interactive Mode Commands should offer a `--non-interactive` option to make scripting possible. The non interactive mode should offer a way to answer questions automatically. Non interactive mode will be defined later in a following interation of the CLI definition. ## Current CLI As reference, this was the old CLI: ~~~ dinstallerctl install # Perform the installation dinstallerctl config dump # Dump the current installation config to stdout dinstallerctl config load <config> # Load a config file and apply the configuration dinstallerctl language available # List available languages for the installation dinstallerctl language selected [<id>...] # Select the languages to install in the target system dinstallerctl rootuser clear # Clear root configuration dinstallerctl rootuser password [<plain password>] # Set the root password dinstallerctl rootuser ssh_key [<key>] # Set the SSH key for root dinstallerctl software available_products # List available products for the installation dinstallerctl software selected_product [<id>] # Select the product to install in the target system dinstallerctl storage actions # List the storage actions to perform dinstallerctl storage available_devices # List available devices for the installation dinstallerctl storage selected_devices [<device>...] # Select devices for the installation dinstallerctl user clear # Clear the user configuration dinstallerctl user set <name> # Configure the user that will be created during the installation dinstallerctl user show # Show the user configuration` ~~~ Original post with discussion is at https://gist.github.com/joseivanlopez/808c2be0cf668b4b457fc5d9ec20dc73 0707010000000A000081A40000000000000000000000016625710300000602000000000000000000000000000000000000005400000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/doc/backend-for-testing.md# How to set up a backend for testing the CLI frontend I needed a testing instance of the Agama backend so that the Rust command-line frontend has something to talk to. ## Summary 1. Take the container used for continuous integration (CI) testing of the backend 2. Give it a git checkout of this repo 3. Install the backend within the container 4. Copy the frontend binary into the container ## Considered Alternatives My first plan had a different finale, 4. Make the D-Bus service visible ouside the container, but I hit an issue with D-Bus authentication, hopefully solvable. (Update: `xdg-dbus-proxy` seems to work, ask mvidner about it) Josef wanted to test against a different container (`d-installer-backend`) but that one was a bit old and the D-Bus API was mismatched between frontend and backend. ## Details The container used is built in [OBS systemsmanagement:Agama:Staging/agama-testing][agama-testing] and downloaded from registry.o.o specified below. [agama-testing]: https://build.opensuse.org/package/show/systemsmanagement:Agama:Staging/agama-testing I basically picked the useful bits from the `integration-tests` part of [.github/workflows/ci.yml][ci.yml]. [ci.yml]: https://github.com/openSUSE/agama/blob/25462f57ab695d6910beb59ff0b21a7afaeda47e/.github/workflows/ci.yml ## Resulting Script The script, which used to be inlined here, is now at [`/testing_using_container.sh`](../../../testing_using_container.sh). >>>>>>> 8f2f0404 (copied the script part of rust/agama-cli/doc/backend-for-testing.md) 0707010000000B000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src0707010000000C000081A40000000000000000000000016625710300001AB7000000000000000000000000000000000000004500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src/auth.rsuse clap::{arg, Args, Subcommand}; use home; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use std::fs; use std::fs::File; use std::io; use std::io::{BufRead, BufReader}; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; const DEFAULT_JWT_FILE: &str = ".agama/agama-jwt"; const DEFAULT_AUTH_URL: &str = "http://localhost:3000/api/authenticate"; const DEFAULT_FILE_MODE: u32 = 0o600; #[derive(Subcommand, Debug)] pub enum AuthCommands { /// Login with defined server. Result is JWT stored locally and made available to /// further use. Password can be provided by commandline option, from a file or it fallbacks /// into an interactive prompt. Login(LoginArgs), /// Release currently stored JWT Logout, /// Prints currently stored JWT to stdout Show, } /// Main entry point called from agama CLI main loop pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> { match subcommand { AuthCommands::Login(options) => login(LoginArgs::proceed(options).password()?).await, AuthCommands::Logout => logout(), AuthCommands::Show => show(), } } /// Reads stored token and returns it fn jwt() -> anyhow::Result<String> { if let Some(file) = jwt_file() { if let Ok(token) = read_line_from_file(&file.as_path()) { return Ok(token); } } Err(anyhow::anyhow!("Authentication token not available")) } /// Stores user provided configuration for login command #[derive(Args, Debug)] pub struct LoginArgs { #[arg(long, short = 'p')] password: Option<String>, #[arg(long, short = 'f')] file: Option<PathBuf>, } impl LoginArgs { /// Transforms user provided options into internal representation /// See Credentials trait fn proceed(options: LoginArgs) -> Box<dyn Credentials> { if let Some(password) = options.password { Box::new(KnownCredentials { password }) } else if let Some(path) = options.file { Box::new(FileCredentials { path }) } else { Box::new(MissingCredentials {}) } } } /// Placeholder for no configuration provided by user struct MissingCredentials; /// Stores whatever is needed for reading credentials from a file struct FileCredentials { path: PathBuf, } /// Stores credentials as provided by the user directly struct KnownCredentials { password: String, } /// Transforms credentials from user's input into format used internaly trait Credentials { fn password(&self) -> io::Result<String>; } impl Credentials for KnownCredentials { fn password(&self) -> io::Result<String> { Ok(self.password.clone()) } } impl Credentials for FileCredentials { fn password(&self) -> io::Result<String> { read_line_from_file(&self.path.as_path()) } } impl Credentials for MissingCredentials { fn password(&self) -> io::Result<String> { let password = read_credential("Password".to_string())?; Ok(password) } } /// Path to file where JWT is stored fn jwt_file() -> Option<PathBuf> { Some(home::home_dir()?.join(DEFAULT_JWT_FILE)) } /// Reads first line from given file fn read_line_from_file(path: &Path) -> io::Result<String> { if !path.exists() { return Err(io::Error::new( io::ErrorKind::Other, "Cannot find the file containing the credentials.", )); } if let Ok(file) = File::open(&path) { // cares only of first line, take everything. No comments // or something like that supported let raw = BufReader::new(file).lines().next(); if let Some(line) = raw { return line; } } Err(io::Error::new( io::ErrorKind::Other, "Failed to open the file", )) } /// Asks user to provide a line of input. Displays a prompt. fn read_credential(caption: String) -> io::Result<String> { let mut cred = String::new(); println!("{}: ", caption); io::stdin().read_line(&mut cred)?; if cred.pop().is_none() || cred.is_empty() { return Err(io::Error::new( io::ErrorKind::Other, format!("Failed to read {}", caption), )); } Ok(cred) } /// Sets the archive owner to root:root. Also sets the file permissions to read/write for the /// owner only. fn set_file_permissions(file: &Path) -> io::Result<()> { let attr = fs::metadata(file)?; let mut permissions = attr.permissions(); // set the file file permissions to -rw------- permissions.set_mode(DEFAULT_FILE_MODE); fs::set_permissions(file, permissions)?; Ok(()) } /// Necessary http request header for authenticate fn authenticate_headers() -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); headers } /// Query web server for JWT async fn get_jwt(url: String, password: String) -> anyhow::Result<String> { let client = reqwest::Client::new(); let response = client .post(url) .headers(authenticate_headers()) .body(format!("{{\"password\": \"{}\"}}", password)) .send() .await?; let body = response .json::<std::collections::HashMap<String, String>>() .await?; let value = body.get(&"token".to_string()); if let Some(token) = value { return Ok(token.clone()); } Err(anyhow::anyhow!("Failed to get authentication token")) } /// Logs into the installation web server and stores JWT for later use. async fn login(password: String) -> anyhow::Result<()> { // 1) ask web server for JWT let res = get_jwt(DEFAULT_AUTH_URL.to_string(), password).await?; // 2) if successful store the JWT for later use if let Some(path) = jwt_file() { if let Some(dir) = path.parent() { fs::create_dir_all(dir)?; } else { return Err(anyhow::anyhow!("Cannot store the authentication token")); } fs::write(path.as_path(), res)?; set_file_permissions(path.as_path())?; } Ok(()) } /// Releases JWT fn logout() -> anyhow::Result<()> { let path = jwt_file(); if !&path.clone().is_some_and(|p| p.exists()) { // mask if the file with the JWT doesn't exist (most probably no login before logout) return Ok(()); } // panicking is right thing to do if expect fails, becase it was already checked twice that // the path exists let file = path.expect("Cannot locate stored JWT"); Ok(fs::remove_file(file)?) } /// Shows stored JWT on stdout fn show() -> anyhow::Result<()> { // we do not care if jwt() fails or not. If there is something to print, show it otherwise // stay silent if let Ok(token) = jwt() { println!("{}", token); } Ok(()) } 0707010000000D000081A4000000000000000000000001662571030000057F000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src/commands.rsuse crate::auth::AuthCommands; use crate::config::ConfigCommands; use crate::logs::LogsCommands; use crate::profile::ProfileCommands; use crate::questions::QuestionsCommands; use clap::Subcommand; #[derive(Subcommand, Debug)] pub enum Commands { /// Change or show installation settings #[command(subcommand)] Config(ConfigCommands), /// Display information about installation settings (e.g., possible values) Info { /// Configuration keys (e.g., software.products) keys: Vec<String>, }, /// Start probing Probe, // Start Installation Install, /// Autoinstallation profile handling #[command(subcommand)] Profile(ProfileCommands), /// Configuration for questions that come from installer /// /// Questions are raised when an unexpected (by the user) situation happens in the installer: /// like if an encrypted partition is detected and cannot be inspected, /// if a repository is signed by an unknown GPG key, or if the installer is not sure /// if multipath should be activated. /// /// For more details see official agama documentation for Questions. #[command(subcommand)] Questions(QuestionsCommands), /// Collects logs #[command(subcommand)] Logs(LogsCommands), /// Request an action on the web server like Login / Logout #[command(subcommand)] Auth(AuthCommands), } 0707010000000E000081A40000000000000000000000016625710300001180000000000000000000000000000000000000004700000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src/config.rsuse crate::error::CliError; use crate::printers::{print, Format}; use agama_lib::connection; use agama_lib::install_settings::{InstallSettings, Scope}; use agama_lib::Store as SettingsStore; use agama_settings::{settings::Settings, SettingObject, SettingValue}; use clap::Subcommand; use convert_case::{Case, Casing}; use std::str::FromStr; use std::{collections::HashMap, error::Error, io}; #[derive(Subcommand, Debug)] pub enum ConfigCommands { /// Add an element to a collection Add { key: String, values: Vec<String> }, /// Set one or many installation settings Set { /// key-value pairs (e.g., user.name="Jane Doe") values: Vec<String>, }, /// Shows the value of one or many configuration settings Show, /// Loads the configuration from a JSON file Load { path: String }, } pub enum ConfigAction { Add(String, HashMap<String, String>), Set(HashMap<String, String>), Show, Load(String), } pub async fn run(subcommand: ConfigCommands, format: Format) -> anyhow::Result<()> { let store = SettingsStore::new(connection().await?).await?; let command = parse_config_command(subcommand)?; match command { ConfigAction::Set(changes) => { let scopes = changes .keys() .filter_map(|k| key_to_scope(k).ok()) .collect(); let mut model = store.load(Some(scopes)).await?; for (key, value) in changes { model.set(&key.to_case(Case::Snake), SettingValue(value))?; } Ok(store.store(&model).await?) } ConfigAction::Show => { let model = store.load(None).await?; print(model, io::stdout(), format)?; Ok(()) } ConfigAction::Add(key, values) => { let scope = key_to_scope(&key).unwrap(); let mut model = store.load(Some(vec![scope])).await?; model.add(&key.to_case(Case::Snake), SettingObject::from(values))?; Ok(store.store(&model).await?) } ConfigAction::Load(path) => { let contents = std::fs::read_to_string(path)?; let result: InstallSettings = serde_json::from_str(&contents)?; let scopes = result.defined_scopes(); let mut model = store.load(Some(scopes)).await?; model.merge(&result); Ok(store.store(&model).await?) } } } fn parse_config_command(subcommand: ConfigCommands) -> Result<ConfigAction, CliError> { match subcommand { ConfigCommands::Add { key, values } => { Ok(ConfigAction::Add(key, parse_keys_values(values)?)) } ConfigCommands::Show => Ok(ConfigAction::Show), ConfigCommands::Set { values } => Ok(ConfigAction::Set(parse_keys_values(values)?)), ConfigCommands::Load { path } => Ok(ConfigAction::Load(path)), } } /// Split the elements on '=' to make a hash of them. fn parse_keys_values(keys_values: Vec<String>) -> Result<HashMap<String, String>, CliError> { let mut changes = HashMap::new(); for s in keys_values { let Some((key, value)) = s.split_once('=') else { return Err(CliError::MissingSeparator(s)); }; changes.insert(key.to_string(), value.to_string()); } Ok(changes) } #[test] fn test_parse_keys_values() { // happy path, make a hash out of the vec let happy_in = vec!["one=first".to_string(), "two=second".to_string()]; let happy_out = HashMap::from([ ("one".to_string(), "first".to_string()), ("two".to_string(), "second".to_string()), ]); let r = parse_keys_values(happy_in); assert!(r.is_ok()); assert_eq!(r.unwrap(), happy_out); // an empty list is fine let empty_vec = Vec::<String>::new(); let empty_hash = HashMap::<String, String>::new(); let r = parse_keys_values(empty_vec); assert!(r.is_ok()); assert_eq!(r.unwrap(), empty_hash); // an empty member fails let empty_string = vec!["".to_string(), "two=second".to_string()]; let r = parse_keys_values(empty_string); assert!(r.is_err()); assert_eq!( format!("{}", r.unwrap_err()), "Missing the '=' separator in ''" ); } fn key_to_scope(key: &str) -> Result<Scope, Box<dyn Error>> { if let Some((name, _)) = key.split_once('.') { return Ok(Scope::from_str(name)?); } Err(Box::new(CliError::InvalidKeyName(key.to_string()))) } 0707010000000F000081A40000000000000000000000016625710300000185000000000000000000000000000000000000004600000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src/error.rsuse thiserror::Error; #[derive(Error, Debug)] pub enum CliError { #[error("Invalid key name: '{0}'")] InvalidKeyName(String), #[error("Cannot perform the installation as the settings are not valid")] ValidationError, #[error("Could not start the installation")] InstallationError, #[error("Missing the '=' separator in '{0}'")] MissingSeparator(String), } 07070100000010000081A400000000000000000000000166257103000033B6000000000000000000000000000000000000004500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src/logs.rsuse clap::Subcommand; use fs_extra::copy_items; use fs_extra::dir::CopyOptions; use nix::unistd::Uid; use std::fs; use std::fs::File; use std::io; use std::io::Write; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::TempDir; // definition of "agama logs" subcommands, see clap crate for details #[derive(Subcommand, Debug)] pub enum LogsCommands { /// Collects and stores logs in a tar archive Store { #[clap(long, short = 'v')] /// Verbose output verbose: bool, #[clap(long, short = 'd')] /// Path to destination directory. Optionally with the archive file name at the end. /// An extension will be appended automatically depending on used compression. destination: Option<PathBuf>, }, /// List logs which will be collected List, } /// Main entry point called from agama CLI main loop pub async fn run(subcommand: LogsCommands) -> anyhow::Result<()> { match subcommand { LogsCommands::Store { verbose, destination, } => { // feed internal options structure by what was received from user // for now we always use / add defaults if any let destination = parse_destination(destination)?; let options = LogOptions { verbose, destination, ..Default::default() }; Ok(store(options)?) } LogsCommands::List => { list(LogOptions::default()); Ok(()) } } } /// Whatewer passed in destination formed into an absolute path with archive name /// /// # Arguments: /// * destination /// - if None then a default is returned /// - if a path to a directory then a default file name for the archive will be appended to the /// path /// - if path with a file name then it is used as is for resulting archive, just extension will /// be appended later on (depends on used compression) fn parse_destination(destination: Option<PathBuf>) -> Result<PathBuf, io::Error> { let err = io::Error::new(io::ErrorKind::InvalidInput, "Invalid destination path"); let mut buffer = destination.unwrap_or(PathBuf::from(DEFAULT_RESULT)); let path = buffer.as_path(); // existing directory -> append an archive name if path.is_dir() { buffer.push("agama-logs"); // a path with file name // sadly, is_some_and is unstable } else if path.parent().is_some() { // validate if parent directory realy exists if !path.parent().unwrap().is_dir() { return Err(err); } } // buffer is <directory> or <directory>/<file_name> here // and we know that directory tree which leads to the <file_name> is valid. // <file_name> creation can still fail later on. Ok(buffer) } const DEFAULT_COMMANDS: [(&str, &str); 3] = [ // (<command to be executed>, <file name used for storing result of the command>) ("journalctl -u agama", "agama"), ("journalctl -u agama-auto", "agama-auto"), ("journalctl --dmesg", "dmesg"), ]; const DEFAULT_PATHS: [&str; 14] = [ // logs "/var/log/YaST2", "/var/log/zypper.log", "/var/log/zypper/history*", "/var/log/zypper/pk_backend_zypp", "/var/log/pbl.log", "/var/log/linuxrc.log", "/var/log/wickedd.log", "/var/log/NetworkManager", "/var/log/messages", "/var/log/boot.msg", "/var/log/udev.log", // config "/etc/install.inf", "/etc/os-release", "/linuxrc.config", ]; const DEFAULT_RESULT: &str = "/tmp/agama-logs"; // what compression is used by default: // (<compression as distinguished by tar>, <an extension for resulting archive>) const DEFAULT_COMPRESSION: (&str, &str) = ("bzip2", "tar.bz2"); const TMP_DIR_PREFIX: &str = "agama-logs."; /// A wrapper around println which shows (or not) the text depending on the boolean variable fn showln(show: bool, text: &str) { if !show { return; } println!("{}", text); } /// A wrapper around println which shows (or not) the text depending on the boolean variable fn show(show: bool, text: &str) { if !show { return; } print!("{}", text); } /// Configurable parameters of the "agama logs" which can be /// set by user when calling a (sub)command struct LogOptions { paths: Vec<String>, commands: Vec<(String, String)>, verbose: bool, destination: PathBuf, } impl Default for LogOptions { fn default() -> Self { Self { paths: DEFAULT_PATHS.iter().map(|p| p.to_string()).collect(), commands: DEFAULT_COMMANDS .iter() .map(|(cmd, name)| (cmd.to_string(), name.to_string())) .collect(), verbose: false, destination: PathBuf::from(DEFAULT_RESULT), } } } /// Struct for log represented by a file struct LogPath { // log source src_path: String, // directory where to collect logs dst_path: PathBuf, } impl LogPath { fn new(src: &str, dst: &Path) -> Self { Self { src_path: src.to_string(), dst_path: dst.to_owned(), } } } /// Struct for log created on demand by a command struct LogCmd { // command which stdout / stderr is logged cmd: String, // user defined log file name (if any) file_name: String, // place where to collect logs dst_path: PathBuf, } impl LogCmd { fn new(cmd: &str, file_name: &str, dst: &Path) -> Self { Self { cmd: cmd.to_string(), file_name: file_name.to_string(), dst_path: dst.to_owned(), } } } trait LogItem { // definition of log source fn from(&self) -> &String; // definition of destination as path to a file fn to(&self) -> PathBuf; // performs whatever is needed to store logs from "from" at "to" path fn store(&self) -> Result<(), io::Error>; } impl LogItem for LogPath { fn from(&self) -> &String { &self.src_path } fn to(&self) -> PathBuf { // remove leading '/' if any from the path (reason see later) let r_path = Path::new(self.src_path.as_str()).strip_prefix("/").unwrap(); // here is the reason, join overwrites the content if the joined path is absolute self.dst_path.join(r_path) } fn store(&self) -> Result<(), io::Error> { let dst_file = self.to(); let dst_path = dst_file.parent().unwrap(); // for now keep directory structure close to the original // e.g. what was in /etc will be in /<tmp dir>/etc/ fs::create_dir_all(dst_path)?; let options = CopyOptions::new(); // fs_extra's own Error doesn't implement From trait so ? operator is unusable match copy_items(&[self.src_path.as_str()], dst_path, &options) { Ok(_p) => Ok(()), Err(_e) => Err(io::Error::new( io::ErrorKind::Other, "Copying of a file failed", )), } } } impl LogItem for LogCmd { fn from(&self) -> &String { &self.cmd } fn to(&self) -> PathBuf { let mut file_name; if self.file_name.is_empty() { file_name = self.cmd.clone(); } else { file_name = self.file_name.clone(); }; file_name.retain(|c| c != ' '); self.dst_path.as_path().join(&file_name) } fn store(&self) -> Result<(), io::Error> { let cmd_parts = self.cmd.split_whitespace().collect::<Vec<&str>>(); let file_path = self.to(); let output = Command::new(cmd_parts[0]) .args(cmd_parts[1..].iter()) .output()?; let mut file_stdout = File::create(format!("{}.out.log", file_path.display()))?; let mut file_stderr = File::create(format!("{}.err.log", file_path.display()))?; file_stdout.write_all(&output.stdout)?; file_stderr.write_all(&output.stderr)?; Ok(()) } } /// Collect existing / requested paths which should already exist in the system. /// Turns them into list of log sources fn paths_to_log_sources(paths: &[String], tmp_dir: &TempDir) -> Vec<Box<dyn LogItem>> { let mut log_sources: Vec<Box<dyn LogItem>> = Vec::new(); for path in paths.iter() { // assumption: path is full path if Path::new(path).try_exists().is_ok() { log_sources.push(Box::new(LogPath::new(path.as_str(), tmp_dir.path()))); } } log_sources } /// Some info can be collected via particular commands only, turn it into log sources fn cmds_to_log_sources(commands: &[(String, String)], tmp_dir: &TempDir) -> Vec<Box<dyn LogItem>> { let mut log_sources: Vec<Box<dyn LogItem>> = Vec::new(); for cmd in commands.iter() { log_sources.push(Box::new(LogCmd::new( cmd.0.as_str(), cmd.1.as_str(), tmp_dir.path(), ))); } log_sources } /// Compress given directory into a tar archive fn compress_logs(tmp_dir: &TempDir, result: &String) -> io::Result<()> { let compression = DEFAULT_COMPRESSION.0; let tmp_path = tmp_dir .path() .parent() .and_then(|p| p.as_os_str().to_str()) .ok_or(io::Error::new( io::ErrorKind::InvalidInput, "Malformed path to temporary directory", ))?; let dir = tmp_dir .path() .file_name() .and_then(|f| f.to_str()) .ok_or(io::Error::new( io::ErrorKind::InvalidInput, "Malformed path to temporary director", ))?; let compress_cmd = format!( "tar -c -f {} --warning=no-file-changed --{} --dereference -C {} {}", result, compression, tmp_path, dir, ); let cmd_parts = compress_cmd.split_whitespace().collect::<Vec<&str>>(); let res = Command::new(cmd_parts[0]) .args(cmd_parts[1..].iter()) .status()?; if res.success() { set_archive_permissions(result) } else { Err(io::Error::new( io::ErrorKind::Other, "Cannot create tar archive", )) } } /// Sets the archive owner to root:root. Also sets the file permissions to read/write for the /// owner only. fn set_archive_permissions(archive: &String) -> io::Result<()> { let attr = fs::metadata(archive)?; let mut permissions = attr.permissions(); // set the archive file permissions to -rw------- permissions.set_mode(0o600); fs::set_permissions(archive, permissions)?; // set the archive owner to root:root // note: std::os::unix::fs::chown is unstable for now Command::new("chown") .args(["root:root", archive.as_str()]) .status()?; Ok(()) } /// Handler for the "agama logs store" subcommand fn store(options: LogOptions) -> Result<(), io::Error> { if !Uid::effective().is_root() { panic!("No Root, no logs. Sorry."); } // preparation, e.g. in later features some log commands can be added / excluded per users request or let commands = options.commands; let paths = options.paths; let verbose = options.verbose; let opt_dest = options.destination.into_os_string(); let destination = opt_dest.to_str().ok_or(io::Error::new( io::ErrorKind::InvalidInput, "Malformed destination path", ))?; let result = format!("{}.{}", destination, DEFAULT_COMPRESSION.1); showln(verbose, "Collecting Agama logs:"); // create temporary directory where to collect all files (similar to what old save_y2logs // does) let tmp_dir = TempDir::with_prefix(TMP_DIR_PREFIX)?; let mut log_sources = paths_to_log_sources(&paths, &tmp_dir); showln(verbose, "\t- proceeding well known paths"); log_sources.append(&mut cmds_to_log_sources(&commands, &tmp_dir)); // some info can be collected via particular commands only showln(verbose, "\t- proceeding output of commands"); // store it if verbose { showln(true, format!("Storing result in: \"{}\"", result).as_str()); } else { showln(true, result.as_str()); } for log in log_sources.iter() { show( verbose, format!("\t- storing: \"{}\" ... ", log.from()).as_str(), ); // for now keep directory structure close to the original // e.g. what was in /etc will be in /<tmp dir>/etc/ let res = match fs::create_dir_all(log.to().parent().unwrap()) { Ok(_p) => match log.store() { Ok(_p) => "[Ok]", Err(_e) => "[Failed]", }, Err(_e) => "[Failed]", }; showln(verbose, res.to_string().as_str()); } compress_logs(&tmp_dir, &result) } /// Handler for the "agama logs list" subcommand fn list(options: LogOptions) { for list in [ ("Log paths: ", options.paths), ( "Log commands: ", options.commands.iter().map(|c| c.0.clone()).collect(), ), ] { println!("{}", list.0); for item in list.1.iter() { println!("\t{}", item); } println!(); } } 07070100000011000081A40000000000000000000000016625710300001260000000000000000000000000000000000000004500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src/main.rsuse clap::Parser; mod auth; mod commands; mod config; mod error; mod logs; mod printers; mod profile; mod progress; mod questions; use crate::error::CliError; use agama_lib::error::ServiceError; use agama_lib::manager::ManagerClient; use agama_lib::progress::ProgressMonitor; use auth::run as run_auth_cmd; use commands::Commands; use config::run as run_config_cmd; use logs::run as run_logs_cmd; use printers::Format; use profile::run as run_profile_cmd; use progress::InstallerProgress; use questions::run as run_questions_cmd; use std::{ process::{ExitCode, Termination}, thread::sleep, time::Duration, }; #[derive(Parser)] #[command(name = "agama", version, about, long_about = None)] struct Cli { #[command(subcommand)] pub command: Commands, /// Format output #[arg(value_enum, short, long, default_value_t = Format::Json)] pub format: Format, } async fn probe() -> anyhow::Result<()> { let another_manager = build_manager().await?; let probe = tokio::spawn(async move { let _ = another_manager.probe().await; }); show_progress().await?; Ok(probe.await?) } /// Starts the installation process /// /// Before starting, it makes sure that the manager is idle. /// /// * `manager`: the manager client. async fn install(manager: &ManagerClient<'_>, max_attempts: u8) -> anyhow::Result<()> { if manager.is_busy().await { println!("Agama's manager is busy. Waiting until it is ready..."); } // Make sure that the manager is ready manager.wait().await?; if !manager.can_install().await? { return Err(CliError::ValidationError)?; } let progress = tokio::spawn(async { show_progress().await }); // Try to start the installation up to max_attempts times. let mut attempts = 1; loop { match manager.install().await { Ok(()) => break, Err(e) => { eprintln!( "Could not start the installation process: {e}. Attempt {}/{}.", attempts, max_attempts ); } } if attempts == max_attempts { eprintln!("Giving up."); return Err(CliError::InstallationError)?; } attempts += 1; sleep(Duration::from_secs(1)); } let _ = progress.await; Ok(()) } async fn show_progress() -> Result<(), ServiceError> { // wait 1 second to give other task chance to start, so progress can display something tokio::time::sleep(Duration::from_secs(1)).await; let conn = agama_lib::connection().await?; let mut monitor = ProgressMonitor::new(conn).await.unwrap(); let presenter = InstallerProgress::new(); monitor .run(presenter) .await .expect("failed to monitor the progress"); Ok(()) } async fn wait_for_services(manager: &ManagerClient<'_>) -> Result<(), ServiceError> { let services = manager.busy_services().await?; // TODO: having it optional if !services.is_empty() { eprintln!("The Agama service is busy. Waiting for it to be available..."); show_progress().await? } Ok(()) } async fn build_manager<'a>() -> anyhow::Result<ManagerClient<'a>> { let conn = agama_lib::connection().await?; Ok(ManagerClient::new(conn).await?) } async fn run_command(cli: Cli) -> anyhow::Result<()> { match cli.command { Commands::Config(subcommand) => { let manager = build_manager().await?; wait_for_services(&manager).await?; run_config_cmd(subcommand, cli.format).await } Commands::Probe => { let manager = build_manager().await?; wait_for_services(&manager).await?; probe().await } Commands::Profile(subcommand) => Ok(run_profile_cmd(subcommand)?), Commands::Install => { let manager = build_manager().await?; install(&manager, 3).await } Commands::Questions(subcommand) => run_questions_cmd(subcommand).await, Commands::Logs(subcommand) => run_logs_cmd(subcommand).await, Commands::Auth(subcommand) => run_auth_cmd(subcommand).await, _ => unimplemented!(), } } /// Represents the result of execution. pub enum CliResult { /// Successful execution. Ok = 0, /// Something went wrong. Error = 1, } impl Termination for CliResult { fn report(self) -> ExitCode { ExitCode::from(self as u8) } } #[tokio::main] async fn main() -> CliResult { let cli = Cli::parse(); if let Err(error) = run_command(cli).await { eprintln!("{:?}", error); return CliResult::Error; } CliResult::Ok } 07070100000012000081A40000000000000000000000016625710300000718000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src/printers.rsuse serde::Serialize; use std::fmt::Debug; use std::io::Write; /// Prints the content using the given format /// /// # Example /// ///```rust /// use agama_lib::users; /// use agama_cli::printers::{print, Format}; /// use std::io; /// /// let user = users::User { login: "jane doe".to_string() }; /// print(user, io::stdout(), Some(Format::Json)) /// .expect("Something went wrong!") /// ``` pub fn print<T, W>(content: T, writer: W, format: Format) -> anyhow::Result<()> where T: serde::Serialize + Debug, W: Write, { let printer: Box<dyn Printer<T, W>> = match format { Format::Json => Box::new(JsonPrinter { content, writer }), Format::Yaml => Box::new(YamlPrinter { content, writer }), _ => Box::new(TextPrinter { content, writer }), }; printer.print() } /// Supported output formats #[derive(clap::ValueEnum, Clone)] pub enum Format { Json, Yaml, Text, } pub trait Printer<T, W> { fn print(self: Box<Self>) -> anyhow::Result<()>; } pub struct JsonPrinter<T, W> { content: T, writer: W, } impl<T: Serialize, W: Write> Printer<T, W> for JsonPrinter<T, W> { fn print(mut self: Box<Self>) -> anyhow::Result<()> { let json = serde_json::to_string(&self.content)?; Ok(writeln!(self.writer, "{}", json)?) } } pub struct TextPrinter<T, W> { content: T, writer: W, } impl<T: Debug, W: Write> Printer<T, W> for TextPrinter<T, W> { fn print(mut self: Box<Self>) -> anyhow::Result<()> { Ok(writeln!(self.writer, "{:?}", &self.content)?) } } pub struct YamlPrinter<T, W> { content: T, writer: W, } impl<T: Serialize, W: Write> Printer<T, W> for YamlPrinter<T, W> { fn print(self: Box<Self>) -> anyhow::Result<()> { Ok(serde_yaml::to_writer(self.writer, &self.content)?) } } 07070100000013000081A400000000000000000000000166257103000006F8000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src/profile.rsuse agama_lib::profile::{ProfileEvaluator, ProfileReader, ProfileValidator, ValidationResult}; use anyhow::Context; use clap::Subcommand; use std::path::Path; #[derive(Subcommand, Debug)] pub enum ProfileCommands { /// Download the profile from a given location Download { url: String }, /// Validate a profile using JSON Schema Validate { path: String }, /// Evaluate a profile, injecting the hardware information from D-Bus Evaluate { path: String }, } fn download(url: &str) -> anyhow::Result<()> { let reader = ProfileReader::new(url)?; let contents = reader.read()?; print!("{}", contents); Ok(()) } fn validate(path: String) -> anyhow::Result<()> { let validator = ProfileValidator::default_schema()?; let path = Path::new(&path); let result = validator .validate_file(path) .context(format!("Could not validate the profile {:?}", path))?; match result { ValidationResult::Valid => { println!("The profile is valid") } ValidationResult::NotValid(errors) => { eprintln!("The profile is not valid. Please, check the following errors:\n"); for error in errors { println!("* {error}") } } } Ok(()) } fn evaluate(path: String) -> anyhow::Result<()> { let evaluator = ProfileEvaluator {}; evaluator .evaluate(Path::new(&path)) .context("Could not evaluate the profile".to_string())?; Ok(()) } pub fn run(subcommand: ProfileCommands) -> anyhow::Result<()> { match subcommand { ProfileCommands::Download { url } => download(&url), ProfileCommands::Validate { path } => validate(path), ProfileCommands::Evaluate { path } => evaluate(path), } } 07070100000014000081A40000000000000000000000016625710300000706000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src/progress.rsuse agama_lib::progress::{Progress, ProgressPresenter}; use async_trait::async_trait; use console::style; use indicatif::{ProgressBar, ProgressStyle}; use std::time::Duration; /// Reports the installer progress through the terminal pub struct InstallerProgress { bar: Option<ProgressBar>, } impl InstallerProgress { pub fn new() -> Self { Self { bar: None } } fn update_bar(&mut self, progress: &Progress) { let bar = self.bar.get_or_insert_with(|| { let style = ProgressStyle::with_template("{spinner:.green} {msg}").unwrap(); let bar = ProgressBar::new(0).with_style(style); bar.enable_steady_tick(Duration::from_millis(120)); bar }); bar.set_length(progress.max_steps.into()); bar.set_position(progress.current_step.into()); bar.set_message(progress.current_title.to_owned()); } } #[async_trait] impl ProgressPresenter for InstallerProgress { async fn start(&mut self, progress: &Progress) { if !progress.finished { self.update_main(progress).await; } } async fn update_main(&mut self, progress: &Progress) { let counter = format!("[{}/{}]", &progress.current_step, &progress.max_steps); println!( "{} {}", style(&counter).bold().green(), &progress.current_title ); } async fn update_detail(&mut self, progress: &Progress) { if progress.finished { if let Some(bar) = self.bar.take() { bar.finish_and_clear(); } } else { self.update_bar(progress); } } async fn finish(&mut self) { if let Some(bar) = self.bar.take() { bar.finish_and_clear(); } } } 07070100000015000081A400000000000000000000000166257103000007E9000000000000000000000000000000000000004A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-cli/src/questions.rsuse agama_lib::connection; use agama_lib::proxies::Questions1Proxy; use anyhow::Context; use clap::{Args, Subcommand, ValueEnum}; #[derive(Subcommand, Debug)] pub enum QuestionsCommands { /// Sets the mode for answering questions. /// /// It allows to decide if questions will be interactive or /// if they should not block installation. Mode(ModesArgs), /// Loads predefined answers to questions. /// /// It allows to predefine answers for certain questions to skip /// them in interactive mode or change answer in automatic mode. /// /// For more details and examples see official Agama documentation. /// https://github.com/openSUSE/agama/blob/master/doc/questions.md Answers { /// Local path to file with answers in YAML format path: String, }, } #[derive(Args, Debug)] pub struct ModesArgs { #[arg(value_enum)] value: Modes, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum Modes { Interactive, NonInteractive, } async fn set_mode(proxy: Questions1Proxy<'_>, value: Modes) -> anyhow::Result<()> { // TODO: how to print dbus error in that anyhow? proxy .set_interactive(value == Modes::Interactive) .await .context("Failed to set mode for answering questions.") } async fn set_answers(proxy: Questions1Proxy<'_>, path: String) -> anyhow::Result<()> { // TODO: how to print dbus error in that anyhow? proxy .add_answer_file(path.as_str()) .await .context("Failed to set answers from answers file") } pub async fn run(subcommand: QuestionsCommands) -> anyhow::Result<()> { let connection = connection().await?; let proxy = Questions1Proxy::new(&connection) .await .context("Failed to connect to Questions service")?; match subcommand { QuestionsCommands::Mode(value) => set_mode(proxy, value.value).await, QuestionsCommands::Answers { path } => set_answers(proxy, path).await, } } 07070100000016000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-derive07070100000017000081A400000000000000000000000166257103000000FE000000000000000000000000000000000000004700000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-derive/Cargo.toml[package] name = "agama-derive" version = "1.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] proc-macro = true [dependencies] proc-macro2 = "1.0.51" quote = "1.0" syn = "2.0" 07070100000018000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-derive/src07070100000019000081A4000000000000000000000001662571030000279B000000000000000000000000000000000000004700000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-derive/src/lib.rs//! Implements a derive macro to implement the Settings from the `agama_settings` crate. //! //! ```no_compile //! use agama_settings::{Settings, settings::Settings}; //! //! #[derive(Default, Settings)] //! struct UserSettings { //! name: Option<String>, //! enabled: Option<bool> //! } //! //! #[derive(Default, Settings)] //! struct InstallSettings { //! #[settings(nested, alias = "first_user")] //! user: Option<UserSettings>, //! reboot: Option<bool> //! product: Option<String>, //! #[settings(collection)] //! packages: Vec<String> //! } //! //! ## Supported attributes //! //! * `nested`: the field is another struct that implements `Settings`. //! * `collection`: the attribute is a vector of elements of type T. You might need to implement //! `TryFrom<SettingObject> for T` for your custom types. //! * `flatten`: the field is flatten (in serde jargon). //! * `alias`: and alternative name for the field. It can be specified several times. //! ``` use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::{parse_macro_input, DeriveInput, Fields, LitStr}; #[derive(Debug, Clone, Copy, PartialEq)] enum SettingKind { /// A single value; the default. Scalar, /// An array of scalars, use `#[settings(collection)]`. Collection, /// The value is another FooSettings, use `#[settings(nested)]`. Nested, } /// Represents a setting and its configuration #[derive(Debug, Clone)] struct SettingField { /// Setting ident ("A word of Rust code, may be a keyword or variable name"). pub ident: syn::Ident, /// Setting kind (scalar, collection, struct). pub kind: SettingKind, /// Whether it is a flatten (in serde jargon) value. pub flatten: bool, /// Aliases for the field (especially useful for flatten fields). pub aliases: Vec<String>, } impl SettingField { pub fn new(ident: syn::Ident) -> Self { Self { ident, kind: SettingKind::Scalar, flatten: false, aliases: vec![], } } } /// List of setting fields #[derive(Debug)] struct SettingFieldsList(Vec<SettingField>); impl SettingFieldsList { pub fn by_type(&self, kind: SettingKind) -> Vec<&SettingField> { self.0.iter().filter(|f| f.kind == kind).collect() } pub fn is_empty(&self) -> bool { self.0.is_empty() } // TODO: implement Iterator? pub fn all(&self) -> &Vec<SettingField> { &self.0 } } /// Derive Settings, typically for a FooSettings struct. /// (see the trait agama_settings::settings::Settings but I cannot link to it without a circular dependency) #[proc_macro_derive(Settings, attributes(settings))] pub fn agama_attributes_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let fields = match &input.data { syn::Data::Struct(syn::DataStruct { fields: Fields::Named(fields), .. }) => &fields.named, _ => panic!("only structs are supported"), }; let fields: Vec<&syn::Field> = fields.iter().collect(); let settings = parse_setting_fields(fields); let set_fn = expand_set_fn(&settings); let add_fn = expand_add_fn(&settings); let merge_fn = expand_merge_fn(&settings); let name = input.ident; let expanded = quote! { impl agama_settings::settings::Settings for #name { #set_fn #add_fn #merge_fn } }; expanded.into() } fn expand_set_fn(settings: &SettingFieldsList) -> TokenStream2 { let scalar_fields = settings.by_type(SettingKind::Scalar); let nested_fields = settings.by_type(SettingKind::Nested); if scalar_fields.is_empty() && nested_fields.is_empty() { return quote! {}; } let mut scalar_handling = quote! { Ok(()) }; if !scalar_fields.is_empty() { let field_name = scalar_fields.iter().map(|s| s.ident.clone()); scalar_handling = quote! { match attr { #(stringify!(#field_name) => self.#field_name = value.try_into().map_err(|e| { agama_settings::SettingsError::UpdateFailed(attr.to_string(), e) })?,)* _ => return Err(agama_settings::SettingsError::UnknownAttribute(attr.to_string())) } Ok(()) } } let mut nested_handling = quote! {}; if !nested_fields.is_empty() { let field_name = nested_fields.iter().map(|s| s.ident.clone()); let aliases = quote_fields_aliases(&nested_fields); let attr = nested_fields .iter() .map(|s| if s.flatten { quote!(attr) } else { quote!(id) }); nested_handling = quote! { if let Some((ns, id)) = attr.split_once('.') { match ns { #(stringify!(#field_name) #aliases => { let #field_name = self.#field_name.get_or_insert(Default::default()); #field_name.set(#attr, value).map_err(|e| e.with_attr(attr))? })* _ => return Err(agama_settings::SettingsError::UnknownAttribute(attr.to_string())) } return Ok(()) } } } quote! { fn set(&mut self, attr: &str, value: agama_settings::SettingValue) -> Result<(), agama_settings::SettingsError> { #nested_handling #scalar_handling } } } fn expand_merge_fn(settings: &SettingFieldsList) -> TokenStream2 { if settings.is_empty() { return quote! {}; } let arms = settings.all().iter().map(|s| { let field_name = &s.ident; match s.kind { SettingKind::Scalar => quote! { if let Some(value) = &other.#field_name { self.#field_name = Some(value.clone()) } }, SettingKind::Nested => quote! { if let Some(other_value) = &other.#field_name { let nested = self.#field_name.get_or_insert(Default::default()); nested.merge(other_value); } }, SettingKind::Collection => quote! { self.#field_name = other.#field_name.clone(); }, } }); quote! { fn merge(&mut self, other: &Self) where Self: Sized, { #(#arms)* } } } fn expand_add_fn(settings: &SettingFieldsList) -> TokenStream2 { let collection_fields = settings.by_type(SettingKind::Collection); let nested_fields = settings.by_type(SettingKind::Nested); if collection_fields.is_empty() && nested_fields.is_empty() { return quote! {}; } let mut collection_handling = quote! { Ok(()) }; if !collection_fields.is_empty() { let field_name = collection_fields.iter().map(|s| s.ident.clone()); collection_handling = quote! { match attr { #(stringify!(#field_name) => { let converted = value.try_into().map_err(|e| { agama_settings::SettingsError::UpdateFailed(attr.to_string(), e) })?; self.#field_name.push(converted) },)* _ => return Err(agama_settings::SettingsError::UnknownAttribute(attr.to_string())) } Ok(()) }; } let mut nested_handling = quote! {}; if !nested_fields.is_empty() { let field_name = nested_fields.iter().map(|s| s.ident.clone()); nested_handling = quote! { if let Some((ns, id)) = attr.split_once('.') { match ns { #(stringify!(#field_name) => { let #field_name = self.#field_name.get_or_insert(Default::default()); #field_name.add(id, value).map_err(|e| e.with_attr(attr))? })* _ => return Err(agama_settings::SettingsError::UnknownAttribute(attr.to_string())) } return Ok(()) } } } quote! { fn add(&mut self, attr: &str, value: agama_settings::SettingObject) -> Result<(), agama_settings::SettingsError> { #nested_handling #collection_handling } } } // Extracts information about the settings fields. // // syn::Field is "A field of a struct or enum variant.", // has .ident .ty(pe) .mutability .vis(ibility)... fn parse_setting_fields(fields: Vec<&syn::Field>) -> SettingFieldsList { let mut settings = vec![]; for field in fields { let ident = field.ident.clone().expect("to find a field ident"); let mut setting = SettingField::new(ident); for attr in &field.attrs { if !attr.path().is_ident("settings") { continue; } attr.parse_nested_meta(|meta| { if meta.path.is_ident("collection") { setting.kind = SettingKind::Collection; }; if meta.path.is_ident("nested") { setting.kind = SettingKind::Nested; } if meta.path.is_ident("flatten") { setting.flatten = true; } if meta.path.is_ident("alias") { let value = meta.value()?; let alias: LitStr = value.parse()?; setting.aliases.push(alias.value()); } Ok(()) }) .expect("settings arguments do not follow the expected structure"); } settings.push(setting); } SettingFieldsList(settings) } fn quote_fields_aliases(nested_fields: &[&SettingField]) -> Vec<TokenStream2> { nested_fields .iter() .map(|f| { let aliases = f.aliases.clone(); if aliases.is_empty() { quote! {} } else { quote! { #(| #aliases)* } } }) .collect() } 0707010000001A000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib0707010000001B000081A40000000000000000000000016625710300000314000000000000000000000000000000000000004400000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/Cargo.toml[package] name = "agama-lib" version = "1.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] agama-settings = { path="../agama-settings" } anyhow = "1.0" async-trait = "0.1.77" cidr = { version = "0.2.2", features = ["serde"] } curl = { version = "0.4.44", features = ["protocol-ftp"] } futures-util = "0.3.29" jsonschema = { version = "0.16.1", default-features = false } log = "0.4" serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.94" tempfile = "3.4.0" thiserror = "1.0.39" tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1.14" url = "2.5.0" utoipa = "4.2.0" zbus = { version = "3", default-features = false, features = ["tokio"] } 0707010000001C000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/share0707010000001D000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/share/examples0707010000001E000081A40000000000000000000000016625710300000799000000000000000000000000000000000000005800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/share/examples/profile.jsonnet// This is a Jsonnet file. Please, check https://jsonnet.org/ for more // information about the language. // For the schema, see // https://github.com/openSUSE/agama/blob/master/rust/agama-lib/share/profile.schema.json // The "hw.libsonnet" file contains hardware information of the storage devices // from the "lshw" tool. Agama generates this file at runtime by running (with // root privileges): // // lshw -json -class disk // // However, it is expected to change in the near future to include information // from other subsystems (e.g., network). local agama = import 'hw.libsonnet'; // Find the biggest disk which is suitable for installing the system. local findBiggestDisk(disks) = local sizedDisks = std.filter(function(d) std.objectHas(d, 'size'), disks); local sorted = std.sort(sizedDisks, function(x) -x.size); sorted[0].logicalname; { software: { product: 'ALP-Bedrock', }, user: { fullName: 'Jane Doe', userName: 'jane.doe', password: '123456', }, root: { password: 'nots3cr3t', sshKey: '...', }, // look ma, there are comments! localization: { language: 'en_US', keyboard: 'en_US', }, storage: { bootDevice: findBiggestDisk(agama.disks), }, network: { connections: [ { id: 'AgamaNetwork', wireless: { password: 'agama.test', security: 'wpa-psk', ssid: 'AgamaNetwork', mode: 'infrastructure' } }, { id: 'Etherned device 1', method4: 'manual', gateway4: '192.168.122.1', addresses: [ '192.168.122.100/24,' ], nameservers: [ '1.2.3.4' ], match: { path: ["pci-0000:00:19.0"] } }, { id: 'bond0', bond: { ports: ['eth0', 'eth1'], mode: 'active-backup', options: "primary=eth1" } } ] } } 0707010000001F000081A40000000000000000000000016625710300000375000000000000000000000000000000000000005E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/share/examples/profile_Dolomite.json{ "localization": { "keyboard": "en_US", "language": "en_US" }, "product": { "id": "ALP-Dolomite", "registrationCode": "FILL IT UP", "registrationEmail": "jreidinger@suse.com" }, "storage": { "bootDevice": "/dev/dm-1" }, "user": { "fullName": "Jane Doe", "password": "123456", "userName": "jane.doe" }, "root": { "password": "nots3cr3t", "sshKey": "..." }, "network": { "connections": [ { "id": "Ethernet network device 1", "method4": "manual", "method6": "manual", "interface": "eth0", "addresses": [ "192.168.122.100/24", "::ffff:c0a8:7ac7/64" ], "gateway4": "192.168.122.1", "gateway6": "::ffff:c0a8:7a01", "nameservers": [ "192.168.122.1", "2001:4860:4860::8888" ] } ] } } 07070100000020000081A40000000000000000000000016625710300000359000000000000000000000000000000000000005800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/share/examples/profile_tw.json{ "localization": { "keyboard": "en_US", "language": "en_US" }, "software": { "patterns": [ "gnome" ] }, "product": { "id": "Tumbleweed" }, "storage": { "bootDevice": "/dev/dm-1" }, "user": { "fullName": "Jane Doe", "password": "123456", "userName": "jane.doe" }, "root": { "password": "nots3cr3t", "sshKey": "..." }, "network": { "connections": [ { "id": "Ethernet network device 1", "method4": "manual", "method6": "manual", "interface": "eth0", "addresses": [ "192.168.122.100/24", "::ffff:c0a8:7ac7/64" ], "gateway4": "192.168.122.1", "gateway6": "::ffff:c0a8:7a01", "nameservers": [ "192.168.122.1", "2001:4860:4860::8888" ], } ] } } 07070100000021000081A40000000000000000000000016625710300002147000000000000000000000000000000000000005300000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/share/profile.schema.json{ "$schema": "https://json-schema.org/draft-07/schema", "$id": "https://github.com/openSUSE/agama/blob/master/rust/agama-lib/share/profile.schema.json", "title": "Profile", "description": "Profile definition for automated installation", "type": "object", "additionalProperties": false, "properties": { "software": { "description": "Software settings (e.g., product to install)", "type": "object", "properties": { "patterns": { "description": "List of patterns to install", "type": "array" } } }, "product": { "description": "Software settings (e.g., product to install)", "type": "object", "properties": { "id": { "description": "Product identifier", "type": "string" }, "registrationCode": { "description": "Product registration code", "type": "string" }, "registrationEmail": { "description": "Product registration email", "type": "string" } } }, "network": { "description": "Network settings", "type": "object", "additionalProperties": false, "properties": { "connections": { "description": "Network connections to be defined", "type": "array", "items": { "type": "object", "additionalProperties": false, "properties": { "id": { "description": "Connection ID", "type": "string" }, "interface": { "description": "The name of the network interface bound to this connection", "type": "string" }, "mac-address": { "description": "Custom mac-address (can also be 'preserve', 'permanent', 'random' or 'stable')", "type": "string" }, "method4": { "description": "IPv4 configuration method (e.g., 'auto')", "type": "string", "enum": [ "auto", "manual", "link-local", "disabled" ] }, "method6": { "description": "IPv6 configuration method (e.g., 'auto')", "type": "string", "enum": [ "auto", "manual", "link-local", "disabled" ] }, "gateway4": { "description": "Connection gateway address (e.g., '192.168.122.1')", "type": "string" }, "gateway6": { "description": "Connection gateway address (e.g., '::ffff:c0a8:7a01')", "type": "string" }, "addresses": { "type": "array", "items": { "description": "Connection addresses", "type": "string", "additionalProperties": false } }, "nameservers": { "type": "array", "items": { "description": "Nameservers (IPv4 and/or IPv6 are allowed)", "type": "string", "additionalProperties": false } }, "wireless": { "type": "object", "description": "Wireless configuration", "additionalProperties": false, "properties": { "password": { "type": "string" }, "security": { "type": "string" }, "ssid": { "type": "string" }, "mode": { "type": "string", "enum": [ "infrastructure", "adhoc", "mesh", "ap" ] } } }, "bond": { "type": "object", "description": "Bonding configuration", "additionalProperties": false, "properties": { "mode": { "type": "string" }, "options": { "type": "string" }, "ports": { "type": "array", "items": { "description": "A list of the interfaces or connections to be bonded", "type": "string", "additionalProperties": false } } } }, "match": { "type": "object", "description": "Match settings", "additionalProperties": false, "properties": { "kernel": { "type": "array", "items": { "description": "A list of kernel command line arguments to match", "type": "string", "additionalProperties": false } }, "interface": { "type": "array", "items": { "description": "A list of interface names to match", "type": "string", "additionalProperties": false } }, "driver": { "type": "array", "items": { "description": "A list of driver names to match", "type": "string", "additionalProperties": false } }, "path": { "type": "array", "items": { "description": "A list of paths to match against the ID_PATH udev property of devices", "type": "string", "additionalProperties": false } } } } }, "required": [ "id" ] } } } }, "user": { "description": "First user settings", "type": "object", "properties": { "fullName": { "description": "Full name (e.g., 'Jane Doe')", "type": "string" }, "userName": { "description": "User login name (e.g., 'jane.doe')", "type": "string" }, "password": { "description": "User password (e.g., 'nots3cr3t')", "type": "string" } }, "required": [ "fullName", "userName", "password" ] }, "root": { "description": "Root authentication settings", "type": "object", "properties": { "password": { "description": "Root password", "type": "string" }, "sshPublicKey": { "description": "SSH public key", "type": "string" } } }, "localization": { "description": "Localization settings", "type": "object", "properties": { "language": { "description": "System language ID (e.g., 'en_US')", "type": "string" }, "keyboard": { "description": "Keyboard layout ID", "type": "string" }, "timezone": { "description": "Time zone identifier such as 'Europe/Berlin'", "type": "string" } } }, "storage": { "description": "Storage settings", "type": "object", "properties": { "bootDevice": { "description": "Device used for booting (e.g., '/dev/sda'). By default, all file systems are created in the boot device.", "type": "string" }, "lvm": { "description": "Whether LVM is used.", "type": "boolean" }, "encryptionPassword": { "description": "If set, the devices are encrypted using the given password.", "type": "string" } } } } } 07070100000022000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src07070100000023000081A40000000000000000000000016625710300000124000000000000000000000000000000000000004500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/dbus.rsuse std::collections::HashMap; use zbus::zvariant; /// Nested hash to send to D-Bus. pub type NestedHash<'a> = HashMap<&'a str, HashMap<&'a str, zvariant::Value<'a>>>; /// Nested hash as it comes from D-Bus. pub type OwnedNestedHash = HashMap<String, HashMap<String, zvariant::OwnedValue>>; 07070100000024000081A400000000000000000000000166257103000004C8000000000000000000000000000000000000004600000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/error.rsuse curl; use serde_json; use std::io; use thiserror::Error; use zbus; #[derive(Error, Debug)] pub enum ServiceError { #[error("D-Bus service error: {0}")] DBus(#[from] zbus::Error), #[error("Could not connect to Agama bus at '{0}': {1}")] DBusConnectionError(String, #[source] zbus::Error), // it's fine to say only "Error" because the original // specific error will be printed too #[error("Error: {0}")] Anyhow(#[from] anyhow::Error), #[error("Wrong user parameters: '{0:?}'")] WrongUser(Vec<String>), #[error("Registration failed: '{0}'")] FailedRegistration(String), #[error("Failed to find these patterns: {0:?}")] UnknownPatterns(Vec<String>), #[error("Could not perform action '{0}'")] UnsuccessfulAction(String), } #[derive(Error, Debug)] pub enum ProfileError { #[error("Could not read the profile")] Unreachable(#[from] curl::Error), #[error("Jsonnet evaluation failed:\n{0}")] EvaluationError(String), #[error("I/O error")] InputOutputError(#[from] io::Error), #[error("The profile is not a valid JSON file")] FormatError(#[from] serde_json::Error), #[error("Error: {0}")] Anyhow(#[from] anyhow::Error), } 07070100000025000081A40000000000000000000000016625710300000CE4000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/install_settings.rs//! Configuration settings handling //! //! This module implements the mechanisms to load and store the installation settings. use crate::{ localization::LocalizationSettings, network::NetworkSettings, product::ProductSettings, software::SoftwareSettings, storage::StorageSettings, users::UserSettings, }; use agama_settings::Settings; use serde::{Deserialize, Serialize}; use std::default::Default; use std::str::FromStr; /// Settings scopes /// /// They are used to limit the reading/writing of settings. For instance, if the Scope::Users is /// given, only the data related to users (UsersStore) are read/written. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Scope { /// User settings Users, /// Software settings Software, /// Storage settings Storage, /// Network settings Network, /// Product settings Product, /// Localization settings Localization, } impl Scope { /// Returns known scopes /// // TODO: we can rely on strum so we do not forget to add them pub fn all() -> [Scope; 6] { [ Scope::Localization, Scope::Network, Scope::Product, Scope::Software, Scope::Storage, Scope::Users, ] } } impl FromStr for Scope { type Err = &'static str; fn from_str(s: &str) -> Result<Self, Self::Err> { match s { "users" => Ok(Self::Users), "software" => Ok(Self::Software), "storage" => Ok(Self::Storage), "network" => Ok(Self::Network), "product" => Ok(Self::Product), "localization" => Ok(Self::Localization), _ => Err("Unknown section"), } } } /// Installation settings /// /// This struct represents installation settings. It serves as an entry point and it is composed of /// other structs which hold the settings for each area ("users", "software", etc.). #[derive(Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InstallSettings { #[serde(default, flatten)] #[settings(nested, flatten, alias = "root")] pub user: Option<UserSettings>, #[serde(default)] #[settings(nested)] pub software: Option<SoftwareSettings>, #[serde(default)] #[settings(nested)] pub product: Option<ProductSettings>, #[serde(default)] #[settings(nested)] pub storage: Option<StorageSettings>, #[serde(default)] #[settings(nested)] pub network: Option<NetworkSettings>, #[serde(default)] #[settings(nested)] pub localization: Option<LocalizationSettings>, } impl InstallSettings { pub fn defined_scopes(&self) -> Vec<Scope> { let mut scopes = vec![]; if self.user.is_some() { scopes.push(Scope::Users); } if self.storage.is_some() { scopes.push(Scope::Storage); } if self.software.is_some() { scopes.push(Scope::Software); } if self.network.is_some() { scopes.push(Scope::Network); } if self.product.is_some() { scopes.push(Scope::Product); } if self.localization.is_some() { scopes.push(Scope::Localization); } scopes } } 07070100000026000081A40000000000000000000000016625710300000840000000000000000000000000000000000000004400000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/lib.rs//! # Interacting with Agama //! //! This library offers an API to interact with Agama services. At this point, the library allows: //! //! * Reading and writing [installation settings](install_settings::InstallSettings). //! * Monitoring the [progress](progress). //! * Triggering actions through the [manager] (e.g., starting installation). //! //! ## Handling installation settings //! //! Let's have a look to the components that are involved when dealing with the installation //! settings, as it is the most complex part of the library. The code is organized in a set of //! modules, one for each topic, like [network], [software], and so on. //! //! Each of those modules contains, at least: //! //! * A settings model: it is a representation of the installation settings for the given topic. It //! is expected to implement the [serde::Serialize], [serde::Deserialize] and //! [agama_settings::settings::Settings] traits. //! * A store: it is the responsible for reading/writing the settings to the service. Usually, it //! relies on a D-Bus client for communicating with the service, although it could implement that //! logic itself. Note: we are considering defining a trait for stores too. //! //! As said, those modules might implement additional stuff, like specific types, clients, etc. pub mod error; pub mod install_settings; pub mod localization; pub mod manager; pub mod network; pub mod product; pub mod profile; pub mod software; pub mod storage; pub mod users; // TODO: maybe expose only clients when we have it? pub mod dbus; pub mod progress; pub mod proxies; mod store; pub use store::Store; pub mod questions; use crate::error::ServiceError; const ADDRESS: &str = "unix:path=/run/agama/bus"; pub async fn connection() -> Result<zbus::Connection, ServiceError> { connection_to(ADDRESS).await } pub async fn connection_to(address: &str) -> Result<zbus::Connection, ServiceError> { let connection = zbus::ConnectionBuilder::address(address)? .build() .await .map_err(|e| ServiceError::DBusConnectionError(address.to_string(), e))?; Ok(connection) } 07070100000027000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/localization07070100000028000081A400000000000000000000000166257103000000E0000000000000000000000000000000000000004D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/localization.rs//! Implements support for handling the localization settings mod client; mod proxies; mod settings; mod store; pub use client::LocalizationClient; pub use settings::LocalizationSettings; pub use store::LocalizationStore; 07070100000029000081A40000000000000000000000016625710300000590000000000000000000000000000000000000005400000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/localization/client.rsuse super::proxies::LocaleProxy; use crate::error::ServiceError; use zbus::Connection; /// D-Bus client for the software service pub struct LocalizationClient<'a> { localization_proxy: LocaleProxy<'a>, } impl<'a> LocalizationClient<'a> { pub async fn new(connection: Connection) -> Result<LocalizationClient<'a>, ServiceError> { Ok(Self { localization_proxy: LocaleProxy::new(&connection).await?, }) } pub async fn language(&self) -> Result<Option<String>, ServiceError> { let locales = self.localization_proxy.locales().await?; let mut iter = locales.into_iter(); let first = iter.next(); // may be None Ok(first) } pub async fn keyboard(&self) -> Result<String, ServiceError> { Ok(self.localization_proxy.keymap().await?) } pub async fn timezone(&self) -> Result<String, ServiceError> { Ok(self.localization_proxy.timezone().await?) } pub async fn set_language(&self, language: &str) -> zbus::Result<()> { let locales = [language]; self.localization_proxy.set_locales(&locales).await } pub async fn set_keyboard(&self, keyboard: &str) -> zbus::Result<()> { self.localization_proxy.set_keymap(keyboard).await } pub async fn set_timezone(&self, timezone: &str) -> zbus::Result<()> { self.localization_proxy.set_timezone(timezone).await } } 0707010000002A000081A40000000000000000000000016625710300000545000000000000000000000000000000000000005500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/localization/proxies.rsuse zbus::dbus_proxy; #[dbus_proxy( interface = "org.opensuse.Agama1.Locale", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Locale" )] trait Locale { /// Commit method fn commit(&self) -> zbus::Result<()>; /// ListKeymaps method fn list_keymaps(&self) -> zbus::Result<Vec<(String, String)>>; /// ListLocales method fn list_locales(&self) -> zbus::Result<Vec<(String, String, String)>>; /// ListTimezones method fn list_timezones(&self) -> zbus::Result<Vec<(String, Vec<String>)>>; /// Keymap property #[dbus_proxy(property)] fn keymap(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_keymap(&self, value: &str) -> zbus::Result<()>; /// Locales property #[dbus_proxy(property)] fn locales(&self) -> zbus::Result<Vec<String>>; #[dbus_proxy(property)] fn set_locales(&self, value: &[&str]) -> zbus::Result<()>; /// Timezone property #[dbus_proxy(property)] fn timezone(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_timezone(&self, value: &str) -> zbus::Result<()>; /// UILocale property #[dbus_proxy(property, name = "UILocale")] fn uilocale(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_uilocale(&self, value: &str) -> zbus::Result<()>; } 0707010000002B000081A400000000000000000000000166257103000001F6000000000000000000000000000000000000005600000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/localization/settings.rs//! Representation of the localization settings use agama_settings::Settings; use serde::{Deserialize, Serialize}; /// Localization settings for the system being installed (not the UI) #[derive(Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LocalizationSettings { /// like "en_US.UTF-8" pub language: Option<String>, /// like "cz(qwerty)" pub keyboard: Option<String>, /// like "Europe/Berlin" pub timezone: Option<String>, } 0707010000002C000081A400000000000000000000000166257103000006EC000000000000000000000000000000000000005300000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/localization/store.rs//! Implements the store for the localization settings. // TODO: for an overview see crate::store (?) use super::{LocalizationClient, LocalizationSettings}; use crate::error::ServiceError; use zbus::Connection; /// Loads and stores the storage settings from/to the D-Bus service. pub struct LocalizationStore<'a> { localization_client: LocalizationClient<'a>, } impl<'a> LocalizationStore<'a> { pub async fn new(connection: Connection) -> Result<LocalizationStore<'a>, ServiceError> { Ok(Self { localization_client: LocalizationClient::new(connection).await?, }) } pub async fn load(&self) -> Result<LocalizationSettings, ServiceError> { // TODO: we should use a single D-Bus call with Properties.GetAll // but LocaleProxy does not have it, only get_property for individual methods // and properties_proxy is private let opt_language = self.localization_client.language().await?; let keyboard = self.localization_client.keyboard().await?; let timezone = self.localization_client.timezone().await?; Ok(LocalizationSettings { language: opt_language, keyboard: Some(keyboard), timezone: Some(timezone), }) } pub async fn store(&self, settings: &LocalizationSettings) -> Result<(), ServiceError> { if let Some(language) = &settings.language { self.localization_client.set_language(language).await?; } if let Some(keyboard) = &settings.keyboard { self.localization_client.set_keyboard(keyboard).await?; } if let Some(timezone) = &settings.timezone { self.localization_client.set_timezone(timezone).await?; } Ok(()) } } 0707010000002D000081A40000000000000000000000016625710300000859000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/manager.rsuse crate::error::ServiceError; use crate::proxies::ServiceStatusProxy; use crate::{ progress::Progress, proxies::{ManagerProxy, ProgressProxy}, }; use tokio_stream::StreamExt; use zbus::Connection; /// D-Bus client for the manager service pub struct ManagerClient<'a> { manager_proxy: ManagerProxy<'a>, progress_proxy: ProgressProxy<'a>, status_proxy: ServiceStatusProxy<'a>, } impl<'a> ManagerClient<'a> { pub async fn new(connection: Connection) -> zbus::Result<ManagerClient<'a>> { Ok(Self { manager_proxy: ManagerProxy::new(&connection).await?, progress_proxy: ProgressProxy::new(&connection).await?, status_proxy: ServiceStatusProxy::new(&connection).await?, }) } pub async fn busy_services(&self) -> Result<Vec<String>, ServiceError> { Ok(self.manager_proxy.busy_services().await?) } pub async fn probe(&self) -> Result<(), ServiceError> { self.wait().await?; Ok(self.manager_proxy.probe().await?) } pub async fn install(&self) -> Result<(), ServiceError> { Ok(self.manager_proxy.commit().await?) } pub async fn can_install(&self) -> Result<bool, ServiceError> { Ok(self.manager_proxy.can_install().await?) } pub async fn progress(&self) -> zbus::Result<Progress> { Progress::from_proxy(&self.progress_proxy).await } /// Returns whether the service is busy or not /// /// TODO: move this code to a trait with functions related to the service status. pub async fn is_busy(&self) -> bool { if let Ok(status) = self.status_proxy.current().await { return status != 0; } true } /// Waits until the manager is idle. pub async fn wait(&self) -> Result<(), ServiceError> { let mut stream = self.status_proxy.receive_current_changed().await; if !self.is_busy().await { return Ok(()); } while let Some(change) = stream.next().await { if change.get().await? == 0 { return Ok(()); } } Ok(()) } } 0707010000002E000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/network0707010000002F000081A400000000000000000000000166257103000000DF000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/network.rs//! Implements support for handling the network settings mod client; mod proxies; pub mod settings; mod store; pub mod types; pub use client::NetworkClient; pub use settings::NetworkSettings; pub use store::NetworkStore; 07070100000030000081A40000000000000000000000016625710300003264000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/network/client.rsuse super::proxies::{ BondProxy, ConnectionProxy, ConnectionsProxy, DeviceProxy, DevicesProxy, IPProxy, MatchProxy, WirelessProxy, }; use super::settings::{BondSettings, MatchSettings, NetworkConnection, WirelessSettings}; use super::types::{Device, DeviceType, SSID}; use crate::error::ServiceError; use tokio_stream::StreamExt; use zbus::zvariant::OwnedObjectPath; use zbus::Connection; /// D-BUS client for the network service pub struct NetworkClient<'a> { pub connection: Connection, connections_proxy: ConnectionsProxy<'a>, devices_proxy: DevicesProxy<'a>, } impl<'a> NetworkClient<'a> { pub async fn new(connection: Connection) -> Result<NetworkClient<'a>, ServiceError> { Ok(Self { connections_proxy: ConnectionsProxy::new(&connection).await?, devices_proxy: DevicesProxy::new(&connection).await?, connection, }) } pub async fn get_connection(&self, id: &str) -> Result<NetworkConnection, ServiceError> { let path = self.connections_proxy.get_connection_by_id(id).await?; self.connection_from(path.as_str()).await } pub async fn available_devices(&self) -> Result<Vec<Device>, ServiceError> { let devices_paths = self.devices_proxy.get_devices().await?; let mut devices = vec![]; for path in devices_paths { let device = self.device_from(path.as_str()).await?; devices.push(device); } Ok(devices) } /// Returns an array of network connections pub async fn connections(&self) -> Result<Vec<NetworkConnection>, ServiceError> { let connection_paths = self.connections_proxy.get_connections().await?; let mut connections = vec![]; for path in connection_paths { let mut connection = self.connection_from(path.as_str()).await?; if let Ok(bond) = self.bond_from(path.as_str()).await { connection.bond = Some(bond); } if let Ok(wireless) = self.wireless_from(path.as_str()).await { connection.wireless = Some(wireless); } let match_settings = self.match_settings_from(path.as_str()).await?; if !match_settings.is_empty() { connection.match_settings = Some(match_settings); } connections.push(connection); } Ok(connections) } /// Applies the network configuration. pub async fn apply(&self) -> Result<(), ServiceError> { self.connections_proxy.apply().await?; Ok(()) } /// Returns the NetworkDevice for the given device path /// /// * `path`: the connections path to get the config from async fn device_from(&self, path: &str) -> Result<Device, ServiceError> { let device_proxy = DeviceProxy::builder(&self.connection) .path(path)? .build() .await?; let name = device_proxy.name().await?; let device_type = device_proxy.type_().await?; Ok(Device { name, type_: DeviceType::try_from(device_type).unwrap(), }) } /// Returns the NetworkConnection for the given connection path /// /// * `path`: the connections path to get the config from async fn connection_from(&self, path: &str) -> Result<NetworkConnection, ServiceError> { let connection_proxy = ConnectionProxy::builder(&self.connection) .path(path)? .build() .await?; let id = connection_proxy.id().await?; let interface = match connection_proxy.interface().await?.as_str() { "" => None, value => Some(value.to_string()), }; let mac_address = match connection_proxy.mac_address().await?.as_str() { "" => None, value => Some(value.to_string()), }; let ip_proxy = IPProxy::builder(&self.connection) .path(path)? .build() .await?; let method4 = ip_proxy.method4().await?; let gateway4 = ip_proxy.gateway4().await?.parse().ok(); let method6 = ip_proxy.method6().await?; let gateway6 = ip_proxy.gateway6().await?.parse().ok(); let nameservers = ip_proxy.nameservers().await?; let nameservers = nameservers.iter().filter_map(|a| a.parse().ok()).collect(); let addresses = ip_proxy.addresses().await?; let addresses = addresses.iter().filter_map(|a| a.parse().ok()).collect(); Ok(NetworkConnection { id, method4: Some(method4.to_string()), gateway4, method6: Some(method6.to_string()), gateway6, addresses, nameservers, interface, mac_address, ..Default::default() }) } /// Returns the [bond settings][BondSettings] for the given connection /// /// * `path`: the connection's path to get the wireless config from async fn bond_from(&self, path: &str) -> Result<BondSettings, ServiceError> { let bond_proxy = BondProxy::builder(&self.connection) .path(path)? .build() .await?; let bond = BondSettings { mode: bond_proxy.mode().await?, options: Some(bond_proxy.options().await?), ports: bond_proxy.ports().await?, }; Ok(bond) } /// Returns the [wireless settings][WirelessSettings] for the given connection /// /// * `path`: the connections path to get the wireless config from async fn wireless_from(&self, path: &str) -> Result<WirelessSettings, ServiceError> { let wireless_proxy = WirelessProxy::builder(&self.connection) .path(path)? .build() .await?; let wireless = WirelessSettings { mode: wireless_proxy.mode().await?, password: wireless_proxy.password().await?, security: wireless_proxy.security().await?, ssid: SSID(wireless_proxy.ssid().await?).to_string(), }; Ok(wireless) } /// Returns the [match settings][MatchSettings] for the given connection /// /// * `path`: the connections path to get the match settings from async fn match_settings_from(&self, path: &str) -> Result<MatchSettings, ServiceError> { let match_proxy = MatchProxy::builder(&self.connection) .path(path)? .build() .await?; let match_settings = MatchSettings { path: match_proxy.path().await?, kernel: match_proxy.kernel().await?, interface: match_proxy.interface().await?, driver: match_proxy.driver().await?, }; Ok(match_settings) } /// Adds or updates a network connection. /// /// If a network connection with the same name exists, it updates its settings. Otherwise, it /// adds a new connection. /// /// * `conn`: settings of the network connection to add/update. pub async fn add_or_update_connection( &self, conn: &NetworkConnection, ) -> Result<(), ServiceError> { let path = match self.connections_proxy.get_connection_by_id(&conn.id).await { Ok(path) => path, Err(_) => self.add_connection(conn).await?, }; self.update_connection(&path, conn).await?; Ok(()) } /// Adds a network connection. /// /// * `conn`: settings of the network connection to add. async fn add_connection( &self, conn: &NetworkConnection, ) -> Result<OwnedObjectPath, ServiceError> { let mut stream = self.connections_proxy.receive_connection_added().await?; self.connections_proxy .add_connection(&conn.id, conn.device_type() as u8) .await?; loop { let signal = stream.next().await.unwrap(); let (id, _path): (String, OwnedObjectPath) = signal.body().unwrap(); if id == conn.id { break; }; } Ok(self .connections_proxy .get_connection_by_id(&conn.id) .await?) } /// Updates a network connection. /// /// * `path`: connection D-Bus path. /// * `conn`: settings of the network connection. async fn update_connection( &self, path: &OwnedObjectPath, conn: &NetworkConnection, ) -> Result<(), ServiceError> { let proxy = ConnectionProxy::builder(&self.connection) .path(path)? .build() .await?; if let Some(ref interface) = conn.interface { proxy.set_interface(interface).await?; } let mac_address = conn.mac_address.as_deref().unwrap_or(""); proxy.set_mac_address(mac_address).await?; self.update_ip_settings(path, conn).await?; if let Some(ref bond) = conn.bond { self.update_bond_settings(path, bond).await?; } if let Some(ref wireless) = conn.wireless { self.update_wireless_settings(path, wireless).await?; } if let Some(ref match_settings) = conn.match_settings { self.update_match_settings(path, match_settings).await?; } Ok(()) } /// Updates the IPv4 setttings for the network connection. /// /// * `path`: connection D-Bus path. /// * `conn`: network connection. async fn update_ip_settings( &self, path: &OwnedObjectPath, conn: &NetworkConnection, ) -> Result<(), ServiceError> { let proxy = IPProxy::builder(&self.connection) .path(path)? .build() .await?; if let Some(ref method) = conn.method4 { proxy.set_method4(method.as_str()).await?; } if let Some(ref method) = conn.method6 { proxy.set_method6(method.as_str()).await?; } let addresses: Vec<_> = conn.addresses.iter().map(|a| a.to_string()).collect(); let addresses: Vec<&str> = addresses.iter().map(|a| a.as_str()).collect(); proxy.set_addresses(&addresses).await?; let nameservers: Vec<_> = conn.nameservers.iter().map(|a| a.to_string()).collect(); let nameservers: Vec<_> = nameservers.iter().map(|a| a.as_str()).collect(); proxy.set_nameservers(&nameservers).await?; let gateway = conn.gateway4.map_or(String::from(""), |g| g.to_string()); proxy.set_gateway4(&gateway).await?; let gateway = conn.gateway6.map_or(String::from(""), |g| g.to_string()); proxy.set_gateway6(&gateway).await?; Ok(()) } /// Updates the bond settings for a network connection. /// /// * `path`: connection D-Bus path. /// * `bond`: bond settings of the network connection. async fn update_bond_settings( &self, path: &OwnedObjectPath, bond: &BondSettings, ) -> Result<(), ServiceError> { let proxy = BondProxy::builder(&self.connection) .path(path)? .build() .await?; let ports: Vec<_> = bond.ports.iter().map(String::as_ref).collect(); proxy.set_ports(ports.as_slice()).await?; if let Some(ref options) = bond.options { proxy.set_options(options.to_string().as_str()).await?; } proxy.set_mode(bond.mode.as_str()).await?; Ok(()) } /// Updates the wireless settings for network connection. /// /// * `path`: connection D-Bus path. /// * `wireless`: wireless settings of the network connection. async fn update_wireless_settings( &self, path: &OwnedObjectPath, wireless: &WirelessSettings, ) -> Result<(), ServiceError> { let proxy = WirelessProxy::builder(&self.connection) .path(path)? .build() .await?; proxy.set_ssid(wireless.ssid.as_bytes()).await?; proxy.set_mode(wireless.mode.to_string().as_str()).await?; proxy .set_security(wireless.security.to_string().as_str()) .await?; proxy.set_password(&wireless.password).await?; Ok(()) } /// Updates the match settings for network connection. /// /// * `path`: connection D-Bus path. /// * `match_settings`: match settings of the network connection. async fn update_match_settings( &self, path: &OwnedObjectPath, match_settings: &MatchSettings, ) -> Result<(), ServiceError> { let proxy = MatchProxy::builder(&self.connection) .path(path)? .build() .await?; let paths: Vec<_> = match_settings.path.iter().map(String::as_ref).collect(); proxy.set_path(paths.as_slice()).await?; Ok(()) } } 07070100000031000081A40000000000000000000000016625710300001A53000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/network/proxies.rs//! D-Bus interface proxies for: `org.opensuse.Agama*.**.*` //! //! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.`. use zbus::dbus_proxy; #[dbus_proxy( interface = "org.opensuse.Agama1.Network.Devices", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Network/devices" )] trait Devices { /// GetDevices method fn get_devices(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; } #[dbus_proxy( interface = "org.opensuse.Agama1.Network.Device", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Network/devices/1" )] trait Device { /// Name property #[dbus_proxy(property)] fn name(&self) -> zbus::Result<String>; /// Type property #[dbus_proxy(property)] fn type_(&self) -> zbus::Result<u8>; } #[dbus_proxy( interface = "org.opensuse.Agama1.Network.Connections", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Network/connections" )] trait Connections { /// AddConnection method fn add_connection(&self, id: &str, ty: u8) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// Apply method fn apply(&self) -> zbus::Result<()>; /// GetConnection method fn get_connection(&self, uuid: &str) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// GetConnectionById method fn get_connection_by_id(&self, id: &str) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// GetConnections method fn get_connections(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// RemoveConnection method fn remove_connection(&self, uuid: &str) -> zbus::Result<()>; /// ConnectionAdded signal #[dbus_proxy(signal)] fn connection_added(&self, id: &str, path: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; } #[dbus_proxy( interface = "org.opensuse.Agama1.Network.Connection.Wireless", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Network" )] trait Wireless { /// Returns the operating mode of the Wireless device /// /// Possible values are 'unknown', 'adhoc', 'infrastructure', 'ap' or 'mesh' #[dbus_proxy(property)] fn mode(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_mode(&self, value: &str) -> zbus::Result<()>; /// Password property #[dbus_proxy(property)] fn password(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_password(&self, value: &str) -> zbus::Result<()>; /// SSID property #[dbus_proxy(property, name = "SSID")] fn ssid(&self) -> zbus::Result<Vec<u8>>; #[dbus_proxy(property, name = "SSID")] fn set_ssid(&self, value: &[u8]) -> zbus::Result<()>; /// Wireless Security property /// /// Possible values are 'none', 'owe', 'ieee8021x', 'wpa-psk', 'sae', /// 'wpa-eap', 'wpa-eap-suite-b192' #[dbus_proxy(property)] fn security(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_security(&self, value: &str) -> zbus::Result<()>; } #[dbus_proxy( interface = "org.opensuse.Agama1.Network.Connection", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Network" )] trait Connection { /// Id property #[dbus_proxy(property)] fn id(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn interface(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_interface(&self, interface: &str) -> zbus::Result<()>; #[dbus_proxy(property)] fn mac_address(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_mac_address(&self, mac_address: &str) -> zbus::Result<()>; } #[dbus_proxy( interface = "org.opensuse.Agama1.Network.Connection.IP", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Network/connections/0" )] trait IP { /// Addresses property #[dbus_proxy(property)] fn addresses(&self) -> zbus::Result<Vec<String>>; #[dbus_proxy(property)] fn set_addresses(&self, value: &[&str]) -> zbus::Result<()>; /// Gateway4 property #[dbus_proxy(property)] fn gateway4(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_gateway4(&self, value: &str) -> zbus::Result<()>; /// Gateway6 property #[dbus_proxy(property)] fn gateway6(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_gateway6(&self, value: &str) -> zbus::Result<()>; /// Method4 property #[dbus_proxy(property)] fn method4(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_method4(&self, value: &str) -> zbus::Result<()>; /// Method6 property #[dbus_proxy(property)] fn method6(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_method6(&self, value: &str) -> zbus::Result<()>; /// Nameservers property #[dbus_proxy(property)] fn nameservers(&self) -> zbus::Result<Vec<String>>; #[dbus_proxy(property)] fn set_nameservers(&self, value: &[&str]) -> zbus::Result<()>; } #[dbus_proxy( interface = "org.opensuse.Agama1.Network.Connection.Match", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Network" )] trait Match { /// Driver property #[dbus_proxy(property)] fn driver(&self) -> zbus::Result<Vec<String>>; fn set_driver(&self, value: &[&str]) -> zbus::Result<()>; /// Interface property #[dbus_proxy(property)] fn interface(&self) -> zbus::Result<Vec<String>>; fn set_interface(&self, value: &[&str]) -> zbus::Result<()>; /// Path property #[dbus_proxy(property)] fn path(&self) -> zbus::Result<Vec<String>>; #[dbus_proxy(property)] fn set_path(&self, value: &[&str]) -> zbus::Result<()>; /// Path property #[dbus_proxy(property)] fn kernel(&self) -> zbus::Result<Vec<String>>; fn set_kernel(&self, value: &[&str]) -> zbus::Result<()>; } #[dbus_proxy( interface = "org.opensuse.Agama1.Network.Connection.Bond", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Network" )] trait Bond { /// Mode property #[dbus_proxy(property)] fn mode(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_mode(&self, value: &str) -> zbus::Result<()>; /// Ports property #[dbus_proxy(property)] fn options(&self) -> zbus::Result<String>; #[dbus_proxy(property)] fn set_options(&self, value: &str) -> zbus::Result<()>; /// Ports property #[dbus_proxy(property)] fn ports(&self) -> zbus::Result<Vec<String>>; #[dbus_proxy(property)] fn set_ports(&self, value: &[&str]) -> zbus::Result<()>; } 07070100000032000081A40000000000000000000000016625710300001A3D000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/network/settings.rs//! Representation of the network settings use super::types::DeviceType; use agama_settings::error::ConversionError; use agama_settings::{SettingObject, SettingValue, Settings}; use cidr::IpInet; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::default::Default; use std::net::IpAddr; /// Network settings for installation #[derive(Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NetworkSettings { /// Connections to use in the installation #[settings(collection)] pub connections: Vec<NetworkConnection>, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct MatchSettings { #[serde(skip_serializing_if = "Vec::is_empty", default)] pub driver: Vec<String>, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub path: Vec<String>, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub kernel: Vec<String>, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub interface: Vec<String>, } impl MatchSettings { pub fn is_empty(&self) -> bool { self.path.is_empty() && self.driver.is_empty() && self.kernel.is_empty() && self.interface.is_empty() } } #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct WirelessSettings { #[serde(skip_serializing_if = "String::is_empty")] pub password: String, pub security: String, pub ssid: String, pub mode: String, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BondSettings { pub mode: String, #[serde(skip_serializing_if = "Option::is_none")] pub options: Option<String>, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub ports: Vec<String>, } impl Default for BondSettings { fn default() -> Self { Self { mode: "balance-rr".to_string(), options: None, ports: vec![], } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NetworkDevice { pub id: String, pub type_: DeviceType, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct NetworkConnection { pub id: String, #[serde(skip_serializing_if = "Option::is_none")] pub method4: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub gateway4: Option<IpAddr>, #[serde(skip_serializing_if = "Option::is_none")] pub method6: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub gateway6: Option<IpAddr>, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub addresses: Vec<IpInet>, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub nameservers: Vec<IpAddr>, #[serde(skip_serializing_if = "Option::is_none")] pub wireless: Option<WirelessSettings>, #[serde(skip_serializing_if = "Option::is_none")] pub interface: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub match_settings: Option<MatchSettings>, #[serde(skip_serializing_if = "Option::is_none")] pub parent: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub bond: Option<BondSettings>, #[serde(rename = "mac-address", skip_serializing_if = "Option::is_none")] pub mac_address: Option<String>, } impl NetworkConnection { /// Device type expected for the network connection. /// /// Which device type to use is inferred from the included settings. For instance, if it has /// wireless settings, it should be applied to a wireless device. pub fn device_type(&self) -> DeviceType { if self.wireless.is_some() { DeviceType::Wireless } else if self.bond.is_some() { DeviceType::Bond } else { DeviceType::Ethernet } } } impl TryFrom<SettingObject> for NetworkConnection { type Error = ConversionError; fn try_from(value: SettingObject) -> Result<Self, Self::Error> { let Some(id) = value.get("id") else { return Err(ConversionError::MissingKey("id".to_string())); }; let default_method = SettingValue("disabled".to_string()); let method4 = value.get("method4").unwrap_or(&default_method); let method6 = value.get("method6").unwrap_or(&default_method); let conn = NetworkConnection { id: id.clone().try_into()?, method4: method4.clone().try_into()?, method6: method6.clone().try_into()?, ..Default::default() }; Ok(conn) } } #[cfg(test)] mod tests { use super::*; use agama_settings::{settings::Settings, SettingObject, SettingValue}; use std::collections::HashMap; #[test] fn test_device_type() { let eth = NetworkConnection::default(); assert_eq!(eth.device_type(), DeviceType::Ethernet); let wlan = NetworkConnection { wireless: Some(WirelessSettings::default()), ..Default::default() }; let bond = NetworkConnection { bond: Some(BondSettings::default()), ..Default::default() }; assert_eq!(wlan.device_type(), DeviceType::Wireless); assert_eq!(bond.device_type(), DeviceType::Bond); } #[test] fn test_bonding_defaults() { let bond = BondSettings::default(); assert_eq!(bond.mode, "balance-rr".to_string()); assert_eq!(bond.ports.len(), 0); assert_eq!(bond.options, None); } #[test] fn test_add_connection_to_setting() { let name = SettingValue("Ethernet 1".to_string()); let method = SettingValue("auto".to_string()); let conn = HashMap::from([("id".to_string(), name), ("method".to_string(), method)]); let conn = SettingObject(conn); let mut settings = NetworkSettings::default(); settings.add("connections", conn).unwrap(); assert_eq!(settings.connections.len(), 1); } #[test] fn test_setting_object_to_network_connection() { let name = SettingValue("Ethernet 1".to_string()); let method_auto = SettingValue("auto".to_string()); let method_disabled = SettingValue("disabled".to_string()); let settings = HashMap::from([ ("id".to_string(), name), ("method4".to_string(), method_auto), ("method6".to_string(), method_disabled), ]); let settings = SettingObject(settings); let conn: NetworkConnection = settings.try_into().unwrap(); assert_eq!(conn.id, "Ethernet 1"); assert_eq!(conn.method4, Some("auto".to_string())); assert_eq!(conn.method6, Some("disabled".to_string())); } } 07070100000033000081A40000000000000000000000016625710300000FDA000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/network/store.rsuse super::settings::NetworkConnection; use crate::error::ServiceError; use crate::network::{NetworkClient, NetworkSettings}; use zbus::Connection; /// Loads and stores the network settings from/to the D-Bus service. pub struct NetworkStore<'a> { network_client: NetworkClient<'a>, } impl<'a> NetworkStore<'a> { pub async fn new(connection: Connection) -> Result<NetworkStore<'a>, ServiceError> { Ok(Self { network_client: NetworkClient::new(connection).await?, }) } // TODO: read the settings from the service pub async fn load(&self) -> Result<NetworkSettings, ServiceError> { let connections = self.network_client.connections().await?; Ok(NetworkSettings { connections }) } pub async fn store(&self, settings: &NetworkSettings) -> Result<(), ServiceError> { for id in ordered_connections(&settings.connections) { let id = id.as_str(); let fallback = default_connection(id); let conn = find_connection(id, &settings.connections).unwrap_or(&fallback); self.network_client.add_or_update_connection(conn).await?; } self.network_client.apply().await?; Ok(()) } } /// Returns the list of connections in the order they should be written to the D-Bus service. /// /// * `conns`: connections to write. fn ordered_connections(conns: &Vec<NetworkConnection>) -> Vec<String> { let mut ordered: Vec<String> = Vec::with_capacity(conns.len()); for conn in conns { add_ordered_connection(conn, conns, &mut ordered); } ordered } /// Adds a connections and its dependencies to the list. /// /// * `conn`: connection to add. /// * `conns`: existing connections. /// * `ordered`: ordered list of connections. fn add_ordered_connection( conn: &NetworkConnection, conns: &Vec<NetworkConnection>, ordered: &mut Vec<String>, ) { if let Some(bond) = &conn.bond { for port in &bond.ports { if let Some(conn) = find_connection(port, conns) { add_ordered_connection(conn, conns, ordered); } else if !ordered.contains(&conn.id) { ordered.push(port.clone()); } } } if !ordered.contains(&conn.id) { ordered.push(conn.id.to_owned()) } } /// Finds a connection by id in the list. /// /// * `id`: connection ID. fn find_connection<'a>(id: &str, conns: &'a [NetworkConnection]) -> Option<&'a NetworkConnection> { conns .iter() .find(|c| c.id == id || c.interface == Some(id.to_string())) } fn default_connection(id: &str) -> NetworkConnection { NetworkConnection { id: id.to_string(), interface: Some(id.to_string()), ..Default::default() } } #[cfg(test)] mod tests { use super::ordered_connections; use crate::network::settings::{BondSettings, NetworkConnection}; #[test] fn test_ordered_connections() { let bond = NetworkConnection { id: "bond0".to_string(), bond: Some(BondSettings { ports: vec!["eth0".to_string(), "eth1".to_string(), "eth3".to_string()], ..Default::default() }), ..Default::default() }; let eth0 = NetworkConnection { id: "eth0".to_string(), ..Default::default() }; let eth1 = NetworkConnection { id: "Wired connection".to_string(), interface: Some("eth1".to_string()), ..Default::default() }; let eth2 = NetworkConnection { id: "eth2".to_string(), ..Default::default() }; let conns = vec![bond, eth0, eth1, eth2]; let ordered = ordered_connections(&conns); assert_eq!( ordered, vec![ "eth0".to_string(), "Wired connection".to_string(), "eth3".to_string(), "bond0".to_string(), "eth2".to_string() ] ) } } 07070100000034000081A40000000000000000000000016625710300001394000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/network/types.rsuse serde::{Deserialize, Serialize}; use std::{fmt, str}; use thiserror::Error; use zbus; /// Network device #[derive(Debug, Clone)] pub struct Device { pub name: String, pub type_: DeviceType, } #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] pub struct SSID(pub Vec<u8>); impl SSID { pub fn to_vec(&self) -> &Vec<u8> { &self.0 } } impl fmt::Display for SSID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", str::from_utf8(&self.0).unwrap()) } } impl From<SSID> for Vec<u8> { fn from(value: SSID) -> Self { value.0 } } #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum DeviceType { Loopback = 0, Ethernet = 1, Wireless = 2, Dummy = 3, Bond = 4, Vlan = 5, Bridge = 6, } /// Bond mode #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub enum BondMode { #[serde(rename = "balance-rr")] RoundRobin = 0, #[serde(rename = "active-backup")] ActiveBackup = 1, #[serde(rename = "balance-xor")] BalanceXOR = 2, #[serde(rename = "broadcast")] Broadcast = 3, #[serde(rename = "802.3ad")] LACP = 4, #[serde(rename = "balance-tlb")] BalanceTLB = 5, #[serde(rename = "balance-alb")] BalanceALB = 6, } impl Default for BondMode { fn default() -> Self { Self::RoundRobin } } impl std::fmt::Display for BondMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { BondMode::RoundRobin => "balance-rr", BondMode::ActiveBackup => "active-backup", BondMode::BalanceXOR => "balance-xor", BondMode::Broadcast => "broadcast", BondMode::LACP => "802.3ad", BondMode::BalanceTLB => "balance-tlb", BondMode::BalanceALB => "balance-alb", } ) } } #[derive(Debug, Error, PartialEq)] #[error("Invalid bond mode: {0}")] pub struct InvalidBondMode(String); impl TryFrom<&str> for BondMode { type Error = InvalidBondMode; fn try_from(value: &str) -> Result<Self, Self::Error> { match value { "balance-rr" => Ok(BondMode::RoundRobin), "active-backup" => Ok(BondMode::ActiveBackup), "balance-xor" => Ok(BondMode::BalanceXOR), "broadcast" => Ok(BondMode::Broadcast), "802.3ad" => Ok(BondMode::LACP), "balance-tlb" => Ok(BondMode::BalanceTLB), "balance-alb" => Ok(BondMode::BalanceALB), _ => Err(InvalidBondMode(value.to_string())), } } } impl TryFrom<u8> for BondMode { type Error = InvalidBondMode; fn try_from(value: u8) -> Result<Self, Self::Error> { match value { 0 => Ok(BondMode::RoundRobin), 1 => Ok(BondMode::ActiveBackup), 2 => Ok(BondMode::BalanceXOR), 3 => Ok(BondMode::Broadcast), 4 => Ok(BondMode::LACP), 5 => Ok(BondMode::BalanceTLB), 6 => Ok(BondMode::BalanceALB), _ => Err(InvalidBondMode(value.to_string())), } } } impl From<InvalidBondMode> for zbus::fdo::Error { fn from(value: InvalidBondMode) -> zbus::fdo::Error { zbus::fdo::Error::Failed(format!("Network error: {value}")) } } #[derive(Debug, Error, PartialEq)] #[error("Invalid device type: {0}")] pub struct InvalidDeviceType(u8); impl TryFrom<u8> for DeviceType { type Error = InvalidDeviceType; fn try_from(value: u8) -> Result<Self, Self::Error> { match value { 0 => Ok(DeviceType::Loopback), 1 => Ok(DeviceType::Ethernet), 2 => Ok(DeviceType::Wireless), 3 => Ok(DeviceType::Dummy), 4 => Ok(DeviceType::Bond), 5 => Ok(DeviceType::Vlan), 6 => Ok(DeviceType::Bridge), _ => Err(InvalidDeviceType(value)), } } } impl From<InvalidDeviceType> for zbus::fdo::Error { fn from(value: InvalidDeviceType) -> zbus::fdo::Error { zbus::fdo::Error::Failed(format!("Network error: {value}")) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_display_ssid() { let ssid = SSID(vec![97, 103, 97, 109, 97]); assert_eq!(format!("{}", ssid), "agama"); } #[test] fn test_ssid_to_vec() { let vec = vec![97, 103, 97, 109, 97]; let ssid = SSID(vec.clone()); assert_eq!(ssid.to_vec(), &vec); } #[test] fn test_device_type_from_u8() { let dtype = DeviceType::try_from(0); assert_eq!(dtype, Ok(DeviceType::Loopback)); let dtype = DeviceType::try_from(128); assert_eq!(dtype, Err(InvalidDeviceType(128))); } #[test] fn test_display_bond_mode() { let mode = BondMode::try_from(1).unwrap(); assert_eq!(format!("{}", mode), "active-backup"); } } 07070100000035000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/product07070100000036000081A400000000000000000000000166257103000000D7000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/product.rs//! Implements support for handling the product settings mod client; mod proxies; mod settings; mod store; pub use client::{Product, ProductClient}; pub use settings::ProductSettings; pub use store::ProductStore; 07070100000037000081A40000000000000000000000016625710300000CFF000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/product/client.rsuse std::collections::HashMap; use crate::error::ServiceError; use crate::software::proxies::SoftwareProductProxy; use serde::Serialize; use zbus::Connection; use super::proxies::RegistrationProxy; /// Represents a software product #[derive(Default, Debug, Serialize, utoipa::ToSchema)] pub struct Product { /// Product ID (eg., "ALP", "Tumbleweed", etc.) pub id: String, /// Product name (e.g., "openSUSE Tumbleweed") pub name: String, /// Product description pub description: String, } /// D-Bus client for the software service #[derive(Clone)] pub struct ProductClient<'a> { product_proxy: SoftwareProductProxy<'a>, registration_proxy: RegistrationProxy<'a>, } impl<'a> ProductClient<'a> { pub async fn new(connection: Connection) -> Result<ProductClient<'a>, ServiceError> { Ok(Self { product_proxy: SoftwareProductProxy::new(&connection).await?, registration_proxy: RegistrationProxy::new(&connection).await?, }) } /// Returns the available products pub async fn products(&self) -> Result<Vec<Product>, ServiceError> { let products: Vec<Product> = self .product_proxy .available_products() .await? .into_iter() .map(|(id, name, data)| { let description = match data.get("description") { Some(value) => value.try_into().unwrap(), None => "", }; Product { id, name, description: description.to_string(), } }) .collect(); Ok(products) } /// Returns the id of the selected product to install pub async fn product(&self) -> Result<String, ServiceError> { Ok(self.product_proxy.selected_product().await?) } /// Selects the product to install pub async fn select_product(&self, product_id: &str) -> Result<(), ServiceError> { let result = self.product_proxy.select_product(product_id).await?; match result { (0, _) => Ok(()), (3, description) => { let products = self.products().await?; let ids: Vec<String> = products.into_iter().map(|p| p.id).collect(); let error = format!("{0}. Available products: '{1:?}'", description, ids); Err(ServiceError::UnsuccessfulAction(error)) } (_, description) => Err(ServiceError::UnsuccessfulAction(description)), } } /// registration code used to register product pub async fn registration_code(&self) -> Result<String, ServiceError> { Ok(self.registration_proxy.reg_code().await?) } /// email used to register product pub async fn email(&self) -> Result<String, ServiceError> { Ok(self.registration_proxy.email().await?) } /// register product pub async fn register(&self, code: &str, email: &str) -> Result<(u32, String), ServiceError> { let mut options: HashMap<&str, zbus::zvariant::Value> = HashMap::new(); if !email.is_empty() { options.insert("Email", zbus::zvariant::Value::new(email)); } Ok(self.registration_proxy.register(code, options).await?) } } 07070100000038000081A400000000000000000000000166257103000003C6000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/product/proxies.rs//! # DBus interface proxy for: `org.opensuse.Agama1.Registration` //! //! This code was generated by `zbus-xmlgen` `3.1.1` from DBus introspection data. use zbus::dbus_proxy; #[dbus_proxy( interface = "org.opensuse.Agama1.Registration", default_service = "org.opensuse.Agama.Software1", default_path = "/org/opensuse/Agama/Software1/Product" )] trait Registration { /// Deregister method fn deregister(&self) -> zbus::Result<(u32, String)>; /// Register method fn register( &self, reg_code: &str, options: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, ) -> zbus::Result<(u32, String)>; /// Email property #[dbus_proxy(property)] fn email(&self) -> zbus::Result<String>; /// RegCode property #[dbus_proxy(property)] fn reg_code(&self) -> zbus::Result<String>; /// Requirement property #[dbus_proxy(property)] fn requirement(&self) -> zbus::Result<u32>; } 07070100000039000081A400000000000000000000000166257103000001CF000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/product/settings.rs//! Representation of the product settings use agama_settings::Settings; use serde::{Deserialize, Serialize}; /// Software settings for installation #[derive(Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProductSettings { /// ID of the product to install (e.g., "ALP", "Tumbleweed", etc.) pub id: Option<String>, pub registration_code: Option<String>, pub registration_email: Option<String>, } 0707010000003A000081A40000000000000000000000016625710300000953000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/product/store.rs//! Implements the store for the product settings. use super::{ProductClient, ProductSettings}; use crate::error::ServiceError; use crate::manager::ManagerClient; use zbus::Connection; /// Loads and stores the product settings from/to the D-Bus service. pub struct ProductStore<'a> { product_client: ProductClient<'a>, manager_client: ManagerClient<'a>, } impl<'a> ProductStore<'a> { pub async fn new(connection: Connection) -> Result<ProductStore<'a>, ServiceError> { Ok(Self { product_client: ProductClient::new(connection.clone()).await?, manager_client: ManagerClient::new(connection).await?, }) } pub async fn load(&self) -> Result<ProductSettings, ServiceError> { let product = self.product_client.product().await?; let registration_code = self.product_client.registration_code().await?; let email = self.product_client.email().await?; Ok(ProductSettings { id: Some(product), registration_code: Some(registration_code), registration_email: Some(email), }) } pub async fn store(&self, settings: &ProductSettings) -> Result<(), ServiceError> { let mut probe = false; if let Some(product) = &settings.id { let existing_product = self.product_client.product().await?; if *product != existing_product { // avoid selecting same product and unnecessary probe self.product_client.select_product(product).await?; probe = true; } } if let Some(reg_code) = &settings.registration_code { let (result, message); if let Some(email) = &settings.registration_email { (result, message) = self.product_client.register(reg_code, email).await?; } else { (result, message) = self.product_client.register(reg_code, "").await?; } // FIXME: name the magic numbers. 3 is Registration not required // FIXME: well don't register when not required (no regcode in profile) if result != 0 && result != 3 { return Err(ServiceError::FailedRegistration(message)); } probe = true; } if probe { self.manager_client.probe().await?; } Ok(()) } } 0707010000003B000081A4000000000000000000000001662571030000167F000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/profile.rsuse crate::error::ProfileError; use anyhow::Context; use curl::easy::Easy; use jsonschema::JSONSchema; use log::info; use serde_json; use std::{fs, io, io::Write, path::Path, process::Command}; use tempfile::{tempdir, TempDir}; use url::Url; /// Downloads a profile for a given location. pub struct ProfileReader { url: Url, } impl ProfileReader { pub fn new(url: &str) -> anyhow::Result<Self> { let url = Url::parse(url)?; Ok(Self { url }) } pub fn read(&self) -> anyhow::Result<String> { let path = self.url.path(); if path.ends_with(".xml") || path.ends_with(".erb") || path.ends_with('/') { self.read_from_autoyast() } else { self.read_from_url() } } fn read_from_url(&self) -> anyhow::Result<String> { let mut buf = Vec::new(); { let mut handle = Easy::new(); handle.url(self.url.as_str())?; let mut transfer = handle.transfer(); transfer.write_function(|data| { buf.extend(data); Ok(data.len()) })?; transfer.perform().unwrap(); } Ok(String::from_utf8(buf)?) } fn read_from_autoyast(&self) -> anyhow::Result<String> { const TMP_DIR_PREFIX: &str = "autoyast"; const AUTOINST_JSON: &str = "autoinst.json"; let tmp_dir = TempDir::with_prefix(TMP_DIR_PREFIX)?; Command::new("agama-autoyast") .args([self.url.as_str(), &tmp_dir.path().to_string_lossy()]) .status()?; let autoinst_json = tmp_dir.path().join(AUTOINST_JSON); Ok(fs::read_to_string(autoinst_json)?) } } #[derive(Debug)] pub enum ValidationResult { Valid, NotValid(Vec<String>), } /// Checks whether an autoinstallation profile is valid /// /// ``` /// # use agama_lib::profile::{ProfileValidator, ValidationResult}; /// # use std::path::Path; /// let validator = ProfileValidator::new( /// Path::new("share/profile.schema.json") /// ).expect("the default validator"); /// /// // you can validate a &str /// let wrong_profile = r#" /// { "product": { "name": "Tumbleweed" } } /// "#; /// let result = validator.validate_str(&wrong_profile).unwrap(); /// assert!(matches!(ValidationResult::NotValid, result)); /// /// // or a file /// validator.validate_file(Path::new("share/examples/profile.json")); /// assert!(matches!(ValidationResult::Valid, result)); /// ``` pub struct ProfileValidator { schema: JSONSchema, } impl ProfileValidator { pub fn default_schema() -> Result<Self, ProfileError> { let relative_path = Path::new("agama-lib/share/profile.schema.json"); let path = if relative_path.exists() { relative_path } else { Path::new("/usr/share/agama-cli/profile.schema.json") }; info!("Validation with path {:?}", path); Self::new(path) } pub fn new(schema_path: &Path) -> Result<Self, ProfileError> { let contents = fs::read_to_string(schema_path) .context(format!("Failed to read schema at {:?}", schema_path))?; let schema = serde_json::from_str(&contents)?; let schema = JSONSchema::compile(&schema).expect("A valid schema"); Ok(Self { schema }) } pub fn validate_file(&self, profile_path: &Path) -> Result<ValidationResult, ProfileError> { let contents = fs::read_to_string(profile_path)?; self.validate_str(&contents) } pub fn validate_str(&self, profile: &str) -> Result<ValidationResult, ProfileError> { let contents = serde_json::from_str(profile)?; let result = self.schema.validate(&contents); if let Err(errors) = result { let messages: Vec<String> = errors.map(|e| format!("{e}. {e:?}")).collect(); return Ok(ValidationResult::NotValid(messages)); } Ok(ValidationResult::Valid) } } /// Evaluates a profile /// /// Evaluating a profile means injecting the hardware information (coming from D-Bus) /// and running the jsonnet code to generate a plain JSON file. For this struct to /// work, the `/usr/bin/jsonnet` command must be available. pub struct ProfileEvaluator {} impl ProfileEvaluator { pub fn evaluate(&self, profile_path: &Path) -> anyhow::Result<()> { let dir = tempdir()?; let working_path = dir.path().join("profile.jsonnet"); fs::copy(profile_path, working_path)?; let hwinfo_path = dir.path().join("hw.libsonnet"); self.write_hwinfo(&hwinfo_path) .context("Failed to read system's hardware information")?; let result = Command::new("/usr/bin/jsonnet") .arg("profile.jsonnet") .current_dir(&dir) .output() .context("Failed to run jsonnet")?; if !result.status.success() { let message = String::from_utf8(result.stderr).context("Invalid UTF-8 sequence from jsonnet")?; return Err(ProfileError::EvaluationError(message).into()); } io::stdout().write_all(&result.stdout)?; Ok(()) } // Write the hardware information in JSON format to a given path // // TODO: we need a better way to generate this information, as lshw and hwinfo are not usable // out of the box. fn write_hwinfo(&self, path: &Path) -> anyhow::Result<()> { let result = Command::new("/usr/sbin/lshw") .args(["-json", "-class", "disk"]) .output() .context("Failed to run lshw")?; let mut file = fs::File::create(path)?; file.write_all(b"{ \"disks\":\n")?; file.write_all(&result.stdout)?; file.write_all(b"\n}")?; Ok(()) } } 0707010000003C000081A40000000000000000000000016625710300001832000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/progress.rs//! This module offers a mechanism to report the installation progress in Agama's command-line //! interface. //! //! The library does not prescribe any way to present that information to the user. As shown in the //! example below, you can build your own presenter and implement the [ProgressPresenter] trait. //! //! ```no_run //! # use agama_lib::progress::{Progress, ProgressMonitor, ProgressPresenter}; //! # use async_trait::async_trait; //! # use tokio::{runtime::Handle, task}; //! # use zbus; //! //! // Custom presenter //! struct SimplePresenter {} //! //! impl SimplePresenter { //! fn report_progress(&self, progress: &Progress) { //! println!("{}/{} {}", &progress.current_step, &progress.max_steps, &progress.current_title); //! } //! } //! //! #[async_trait] //! impl ProgressPresenter for SimplePresenter { //! async fn start(&mut self, progress: &Progress) { //! println!("Starting..."); //! self.report_progress(progress); //! } //! //! async fn update_main(&mut self, progress: &Progress) { //! self.report_progress(progress); //! } //! //! async fn update_detail(&mut self, progress: &Progress) { //! self.report_progress(progress); //! } //! //! async fn finish(&mut self) { //! println!("Done"); //! } //! } //! //! async fn run_monitor() { //! let connection = zbus::Connection::system().await.unwrap(); //! let mut monitor = ProgressMonitor::new(connection).await.unwrap(); //! monitor.run(SimplePresenter {}).await; //!} //! ``` use crate::{error::ServiceError, proxies::ProgressProxy}; use async_trait::async_trait; use serde::Serialize; use tokio_stream::{StreamExt, StreamMap}; use zbus::Connection; /// Represents the progress for an Agama service. #[derive(Clone, Default, Debug, Serialize)] pub struct Progress { /// Current step pub current_step: u32, /// Number of steps pub max_steps: u32, /// Title of the current step pub current_title: String, /// Whether the progress reporting is finished pub finished: bool, } impl Progress { pub async fn from_proxy(proxy: &crate::proxies::ProgressProxy<'_>) -> zbus::Result<Progress> { let (current_step, max_steps, finished) = tokio::join!(proxy.current_step(), proxy.total_steps(), proxy.finished()); let (current_step, current_title) = current_step?; Ok(Self { current_step, current_title, max_steps: max_steps?, finished: finished?, }) } } /// Monitorizes and reports the progress of Agama's current operation. /// /// It implements a main/details reporter by listening to the manager and software services, /// similar to Agama's web UI. How this information is displayed depends on the presenter (see /// [ProgressMonitor.run]). pub struct ProgressMonitor<'a> { manager_proxy: ProgressProxy<'a>, software_proxy: ProgressProxy<'a>, } impl<'a> ProgressMonitor<'a> { pub async fn new(connection: Connection) -> Result<ProgressMonitor<'a>, ServiceError> { let manager_proxy = ProgressProxy::builder(&connection) .path("/org/opensuse/Agama/Manager1")? .destination("org.opensuse.Agama.Manager1")? .build() .await?; let software_proxy = ProgressProxy::builder(&connection) .path("/org/opensuse/Agama/Software1")? .destination("org.opensuse.Agama.Software1")? .build() .await?; Ok(Self { manager_proxy, software_proxy, }) } /// Runs the monitor until the current operation finishes. pub async fn run(&mut self, mut presenter: impl ProgressPresenter) -> Result<(), ServiceError> { presenter.start(&self.main_progress().await?).await; let mut changes = self.build_stream().await; while let Some(stream) = changes.next().await { match stream { ("/org/opensuse/Agama/Manager1", _) => { let progress = self.main_progress().await?; if progress.finished { presenter.finish().await; return Ok(()); } presenter.update_main(&progress).await; } ("/org/opensuse/Agama/Software1", _) => { let progress = &self.detail_progress().await?; presenter.update_detail(progress).await; } _ => eprintln!("Unknown"), }; } Ok(()) } /// Proxy that reports the progress. async fn main_progress(&self) -> Result<Progress, ServiceError> { Ok(Progress::from_proxy(&self.manager_proxy).await?) } /// Proxy that reports the progress detail. async fn detail_progress(&self) -> Result<Progress, ServiceError> { Ok(Progress::from_proxy(&self.software_proxy).await?) } /// Builds an stream of progress changes. /// /// It listens for changes in the `Current` property and generates a stream identifying the /// proxy where the change comes from. async fn build_stream(&self) -> StreamMap<&str, zbus::PropertyStream<'_, (u32, String)>> { let mut streams = StreamMap::new(); let proxies = [&self.manager_proxy, &self.software_proxy]; for proxy in proxies.iter() { let stream = proxy.receive_current_step_changed().await; let path = proxy.path().as_str(); streams.insert(path, stream); } streams } } /// Presents the progress to the user. #[async_trait] pub trait ProgressPresenter { /// Starts the progress reporting. /// /// * `progress`: current main progress. async fn start(&mut self, progress: &Progress); /// Updates the progress. /// /// * `progress`: current progress. async fn update_main(&mut self, progress: &Progress); /// Updates the progress detail. /// /// * `progress`: current progress detail. async fn update_detail(&mut self, progress: &Progress); /// Finishes the progress reporting. async fn finish(&mut self); } 0707010000003D000081A40000000000000000000000016625710300000CFB000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/proxies.rs//! D-Bus interface proxies for: `org.opensuse.Agama*.**.*` //! //! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.`. use zbus::dbus_proxy; /// Progress1Proxy can be used also with Software and Storage object. /// /// TODO: example #[dbus_proxy( interface = "org.opensuse.Agama1.Progress", default_service = "org.opensuse.Agama.Manager1", default_path = "/org/opensuse/Agama/Manager1" )] trait Progress { /// CurrentStep property #[dbus_proxy(property)] fn current_step(&self) -> zbus::Result<(u32, String)>; /// Finished property #[dbus_proxy(property)] fn finished(&self) -> zbus::Result<bool>; /// TotalSteps property #[dbus_proxy(property)] fn total_steps(&self) -> zbus::Result<u32>; } #[dbus_proxy( interface = "org.opensuse.Agama1.ServiceStatus", default_service = "org.opensuse.Agama.Manager1", default_path = "/org/opensuse/Agama/Manager1" )] trait ServiceStatus { /// All property #[dbus_proxy(property)] fn all( &self, ) -> zbus::Result<Vec<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>>; /// Current property #[dbus_proxy(property)] fn current(&self) -> zbus::Result<u32>; } #[dbus_proxy( interface = "org.opensuse.Agama.Manager1", default_service = "org.opensuse.Agama.Manager1", default_path = "/org/opensuse/Agama/Manager1" )] trait Manager { /// CanInstall method fn can_install(&self) -> zbus::Result<bool>; /// CollectLogs method fn collect_logs(&self, user: &str) -> zbus::Result<String>; /// Commit method fn commit(&self) -> zbus::Result<()>; /// Probe method fn probe(&self) -> zbus::Result<()>; /// BusyServices property #[dbus_proxy(property)] fn busy_services(&self) -> zbus::Result<Vec<String>>; /// CurrentInstallationPhase property #[dbus_proxy(property)] fn current_installation_phase(&self) -> zbus::Result<u32>; /// InstallationPhases property #[dbus_proxy(property)] fn installation_phases( &self, ) -> zbus::Result<Vec<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>>; } #[dbus_proxy( interface = "org.opensuse.Agama1.Questions", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Questions" )] trait Questions1 { /// AddAnswerFile method fn add_answer_file(&self, path: &str) -> zbus::Result<()>; /// Delete method fn delete(&self, question: &zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; /// New method #[dbus_proxy(name = "New")] fn new_quetion( &self, class: &str, text: &str, options: &[&str], default_option: &str, data: std::collections::HashMap<&str, &str>, ) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// NewWithPassword method fn new_with_password( &self, class: &str, text: &str, options: &[&str], default_option: &str, data: std::collections::HashMap<&str, &str>, ) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// Interactive property #[dbus_proxy(property)] fn interactive(&self) -> zbus::Result<bool>; #[dbus_proxy(property)] fn set_interactive(&self, value: bool) -> zbus::Result<()>; } 0707010000003E000081A40000000000000000000000016625710300000C30000000000000000000000000000000000000004A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/questions.rs//! Data model for Agama questions use std::collections::HashMap; /// Basic generic question that fits question without special needs #[derive(Clone, Debug)] pub struct GenericQuestion { /// numeric id used to identify question on D-Bus pub id: u32, /// class of questions. Similar kinds of questions share same class. /// It is dot separated list of elements. Examples are /// `storage.luks.actication` or `software.repositories.unknown_gpg` pub class: String, /// Textual representation of question. Expected to be read by people pub text: String, /// possible answers for question pub options: Vec<String>, /// default answer. Can be used as hint or preselection and it is used as answer for unattended questions. pub default_option: String, /// additional data to help identify questions. Useful for automatic answers. It is question specific. pub data: HashMap<String, String>, /// Confirmed answer. If empty then not answered yet. pub answer: String, } impl GenericQuestion { pub fn new( id: u32, class: String, text: String, options: Vec<String>, default_option: String, data: HashMap<String, String>, ) -> Self { Self { id, class, text, options, default_option, data, answer: String::from(""), } } /// Gets object path of given question. It is useful as parameter /// for deleting it. /// /// # Examples /// /// ``` /// use std::collections::HashMap; /// use agama_lib::questions::GenericQuestion; /// let question = GenericQuestion::new( /// 2, /// "test_class".to_string(), /// "Really?".to_string(), /// vec!["Yes".to_string(), "No".to_string()], /// "No".to_string(), /// HashMap::new() /// ); /// assert_eq!(question.object_path(), "/org/opensuse/Agama1/Questions/2".to_string()); /// ``` pub fn object_path(&self) -> String { format!("/org/opensuse/Agama1/Questions/{}", self.id) } } /// Composition for questions which include password. /// /// ## Extension /// If there is need to provide more mixins, then this structure does not work /// well as it is hard do various combinations. Idea is when need for more /// mixins arise to convert it to Question Struct that have optional mixins /// inside like /// /// ```no_compile /// struct Question { /// base: GenericQuestion, /// with_password: Option<WithPassword>, /// another_mixin: Option<AnotherMixin> /// } /// ``` /// /// This way all handling code can check if given mixin is used and /// act appropriate. #[derive(Clone, Debug)] pub struct WithPassword { /// Luks password. Empty means no password set. pub password: String, /// rest of question data that is same as for other questions pub base: GenericQuestion, } impl WithPassword { pub fn new(base: GenericQuestion) -> Self { Self { password: "".to_string(), base, } } } 0707010000003F000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004600000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/software07070100000040000081A400000000000000000000000166257103000000FE000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/software.rs//! Implements support for handling the software settings mod client; pub mod proxies; mod settings; mod store; pub use client::{Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy}; pub use settings::SoftwareSettings; pub use store::SoftwareStore; 07070100000041000081A400000000000000000000000166257103000011DE000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/software/client.rsuse super::proxies::Software1Proxy; use crate::error::ServiceError; use serde::Serialize; use std::collections::HashMap; use zbus::Connection; /// Represents a software product #[derive(Debug, Serialize, utoipa::ToSchema)] pub struct Pattern { /// Pattern ID (eg., "aaa_base", "gnome") pub id: String, /// Pattern category (e.g., "Production") pub category: String, /// Pattern icon path locally on system pub icon: String, /// Pattern description pub description: String, /// Pattern summary pub summary: String, /// Pattern order pub order: String, } /// Represents the reason why a pattern is selected. #[derive(Clone, Copy, Debug, PartialEq, Serialize)] pub enum SelectedBy { /// The pattern was selected by the user. User = 0, /// The pattern was selected automatically. Auto = 1, /// The pattern has not be selected. None = 2, } #[derive(Debug, thiserror::Error)] #[error("Unknown selected by value: '{0}'")] pub struct UnknownSelectedBy(u8); impl TryFrom<u8> for SelectedBy { type Error = UnknownSelectedBy; fn try_from(value: u8) -> Result<Self, Self::Error> { match value { 0 => Ok(Self::User), 1 => Ok(Self::Auto), _ => Err(UnknownSelectedBy(value)), } } } /// D-Bus client for the software service #[derive(Clone)] pub struct SoftwareClient<'a> { software_proxy: Software1Proxy<'a>, } impl<'a> SoftwareClient<'a> { pub async fn new(connection: Connection) -> Result<SoftwareClient<'a>, ServiceError> { Ok(Self { software_proxy: Software1Proxy::new(&connection).await?, }) } /// Returns the available patterns pub async fn patterns(&self, filtered: bool) -> Result<Vec<Pattern>, ServiceError> { let patterns: Vec<Pattern> = self .software_proxy .list_patterns(filtered) .await? .into_iter() .map( |(id, (category, description, icon, summary, order))| Pattern { id, category, icon, description, summary, order, }, ) .collect(); Ok(patterns) } /// Returns the ids of patterns selected by user pub async fn user_selected_patterns(&self) -> Result<Vec<String>, ServiceError> { let patterns: Vec<String> = self .software_proxy .selected_patterns() .await? .into_iter() .filter_map(|(id, reason)| match SelectedBy::try_from(reason) { Ok(reason) if reason == SelectedBy::User => Some(id), Ok(_reason) => None, Err(e) => { log::warn!("Ignoring pattern {}. Error: {}", &id, e); None } }) .collect(); Ok(patterns) } /// Returns the selected pattern and the reason each one selected. pub async fn selected_patterns(&self) -> Result<HashMap<String, SelectedBy>, ServiceError> { let patterns = self.software_proxy.selected_patterns().await?; let patterns = patterns .into_iter() .filter_map(|(id, reason)| match SelectedBy::try_from(reason) { Ok(reason) => Some((id, reason)), Err(e) => { log::warn!("Ignoring pattern {}. Error: {}", &id, e); None } }) .collect(); Ok(patterns) } /// Selects patterns by user pub async fn select_patterns(&self, patterns: &[String]) -> Result<(), ServiceError> { let patterns: Vec<&str> = patterns.iter().map(AsRef::as_ref).collect(); let wrong_patterns = self .software_proxy .set_user_patterns(patterns.as_slice()) .await?; if !wrong_patterns.is_empty() { Err(ServiceError::UnknownPatterns(wrong_patterns)) } else { Ok(()) } } /// Returns the required space for installing the selected patterns. /// /// It returns a formatted string including the size and the unit. pub async fn used_disk_space(&self) -> Result<String, ServiceError> { Ok(self.software_proxy.used_disk_space().await?) } /// Starts the process to read the repositories data. pub async fn probe(&self) -> Result<(), ServiceError> { Ok(self.software_proxy.probe().await?) } } 07070100000042000081A40000000000000000000000016625710300000DE0000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/software/proxies.rs//! D-Bus interface proxies for: `org.opensuse.Agama.Software1.*` //! //! This code was generated by `zbus-xmlgen` `3.1.1` from DBus introspection data. use zbus::dbus_proxy; /// Software patterns map. /// /// It uses the pattern name as key and a tuple containing the following information as value: /// /// * Category. /// * Description. /// * Icon. /// * Summary. /// * Order. pub type PatternsMap = std::collections::HashMap<String, (String, String, String, String, String)>; #[dbus_proxy( interface = "org.opensuse.Agama.Software1", default_service = "org.opensuse.Agama.Software1", default_path = "/org/opensuse/Agama/Software1" )] trait Software1 { /// AddPattern method fn add_pattern(&self, id: &str) -> zbus::Result<bool>; /// Finish method fn finish(&self) -> zbus::Result<()>; /// Install method fn install(&self) -> zbus::Result<()>; /// IsPackageInstalled method fn is_package_installed(&self, name: &str) -> zbus::Result<bool>; /// ListPatterns method fn list_patterns(&self, filtered: bool) -> zbus::Result<PatternsMap>; /// Probe method fn probe(&self) -> zbus::Result<()>; /// Propose method fn propose(&self) -> zbus::Result<()>; /// ProvisionsSelected method fn provisions_selected(&self, provisions: &[&str]) -> zbus::Result<Vec<bool>>; /// RemovePattern method fn remove_pattern(&self, id: &str) -> zbus::Result<bool>; /// SetUserPatterns method fn set_user_patterns(&self, ids: &[&str]) -> zbus::Result<Vec<String>>; /// UsedDiskSpace method fn used_disk_space(&self) -> zbus::Result<String>; /// SelectedPatterns property #[dbus_proxy(property)] fn selected_patterns(&self) -> zbus::Result<std::collections::HashMap<String, u8>>; } /// Product definition. /// /// It is composed of the following elements: /// /// * Product ID. /// * Display name. /// * Some additional data which includes a "description" key. pub type Product = ( String, String, std::collections::HashMap<String, zbus::zvariant::OwnedValue>, ); #[dbus_proxy( interface = "org.opensuse.Agama.Software1.Product", default_service = "org.opensuse.Agama.Software1", default_path = "/org/opensuse/Agama/Software1/Product" )] trait SoftwareProduct { /// SelectProduct method fn select_product(&self, id: &str) -> zbus::Result<(u32, String)>; /// AvailableProducts property #[dbus_proxy(property)] fn available_products(&self) -> zbus::Result<Vec<Product>>; /// SelectedProduct property #[dbus_proxy(property)] fn selected_product(&self) -> zbus::Result<String>; } #[dbus_proxy( interface = "org.opensuse.Agama.Software1.Proposal", default_service = "org.opensuse.Agama.Software1", default_path = "/org/opensuse/Agama/Software1/Proposal" )] trait SoftwareProposal { /// AddResolvables method fn add_resolvables( &self, id: &str, r#type: u8, resolvables: &[&str], optional: bool, ) -> zbus::Result<()>; /// GetResolvables method fn get_resolvables(&self, id: &str, r#type: u8, optional: bool) -> zbus::Result<Vec<String>>; /// RemoveResolvables method fn remove_resolvables( &self, id: &str, r#type: u8, resolvables: &[&str], optional: bool, ) -> zbus::Result<()>; /// SetResolvables method fn set_resolvables( &self, id: &str, r#type: u8, resolvables: &[&str], optional: bool, ) -> zbus::Result<()>; } 07070100000043000081A4000000000000000000000001662571030000018D000000000000000000000000000000000000005200000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/software/settings.rs//! Representation of the software settings use agama_settings::Settings; use serde::{Deserialize, Serialize}; /// Software settings for installation #[derive(Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SoftwareSettings { /// List of patterns to install. If empty use default. #[settings(collection)] pub patterns: Vec<String>, } 07070100000044000081A400000000000000000000000166257103000003AE000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/software/store.rs//! Implements the store for the storage settings. use super::{SoftwareClient, SoftwareSettings}; use crate::error::ServiceError; use zbus::Connection; /// Loads and stores the software settings from/to the D-Bus service. pub struct SoftwareStore<'a> { software_client: SoftwareClient<'a>, } impl<'a> SoftwareStore<'a> { pub async fn new(connection: Connection) -> Result<SoftwareStore<'a>, ServiceError> { Ok(Self { software_client: SoftwareClient::new(connection.clone()).await?, }) } pub async fn load(&self) -> Result<SoftwareSettings, ServiceError> { let patterns = self.software_client.user_selected_patterns().await?; Ok(SoftwareSettings { patterns }) } pub async fn store(&self, settings: &SoftwareSettings) -> Result<(), ServiceError> { self.software_client .select_patterns(&settings.patterns) .await?; Ok(()) } } 07070100000045000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/storage07070100000046000081A400000000000000000000000166257103000000CC000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/storage.rs//! Implements support for handling the storage settings mod client; mod proxies; mod settings; mod store; pub use client::StorageClient; pub use settings::StorageSettings; pub use store::StorageStore; 07070100000047000081A400000000000000000000000166257103000012A0000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/storage/client.rs//! Implements a client to access Agama's storage service. use super::proxies::{BlockDeviceProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy}; use super::StorageSettings; use crate::error::ServiceError; use futures_util::future::join_all; use serde::Serialize; use std::collections::HashMap; use zbus::zvariant::OwnedObjectPath; use zbus::Connection; /// Represents a storage device #[derive(Serialize, Debug)] pub struct StorageDevice { name: String, description: String, } /// D-Bus client for the storage service pub struct StorageClient<'a> { pub connection: Connection, calculator_proxy: ProposalCalculatorProxy<'a>, storage_proxy: Storage1Proxy<'a>, } impl<'a> StorageClient<'a> { pub async fn new(connection: Connection) -> Result<StorageClient<'a>, ServiceError> { Ok(Self { calculator_proxy: ProposalCalculatorProxy::new(&connection).await?, storage_proxy: Storage1Proxy::new(&connection).await?, connection, }) } /// Returns the proposal proxy /// /// The proposal might not exist. // NOTE: should we implement some kind of memoization? async fn proposal_proxy(&self) -> Result<ProposalProxy<'a>, ServiceError> { Ok(ProposalProxy::new(&self.connection).await?) } /// Returns the available devices /// /// These devices can be used for installing the system. pub async fn available_devices(&self) -> Result<Vec<StorageDevice>, ServiceError> { let devices: Vec<_> = self .calculator_proxy .available_devices() .await? .into_iter() .map(|path| self.storage_device(path)) .collect(); join_all(devices).await.into_iter().collect() } /// Returns the storage device for the given D-Bus path async fn storage_device( &self, dbus_path: OwnedObjectPath, ) -> Result<StorageDevice, ServiceError> { let proxy = BlockDeviceProxy::builder(&self.connection) .path(dbus_path)? .build() .await?; let name = proxy.name().await?; // TODO: The description is not used yet. Decide what info to show, for example the device // size, see https://crates.io/crates/size. let description = name.clone(); Ok(StorageDevice { name, description }) } /// Returns the boot device proposal setting pub async fn boot_device(&self) -> Result<Option<String>, ServiceError> { let proxy = self.proposal_proxy().await?; let value = self.proposal_value(proxy.boot_device().await)?; match value { Some(v) if v.is_empty() => Ok(None), Some(v) => Ok(Some(v)), None => Ok(None), } } /// Returns the lvm proposal setting pub async fn lvm(&self) -> Result<Option<bool>, ServiceError> { let proxy = self.proposal_proxy().await?; self.proposal_value(proxy.lvm().await) } /// Returns the encryption password proposal setting pub async fn encryption_password(&self) -> Result<Option<String>, ServiceError> { let proxy = self.proposal_proxy().await?; let value = self.proposal_value(proxy.encryption_password().await)?; match value { Some(v) if v.is_empty() => Ok(None), Some(v) => Ok(Some(v)), None => Ok(None), } } fn proposal_value<T>(&self, value: Result<T, zbus::Error>) -> Result<Option<T>, ServiceError> { match value { Ok(v) => Ok(Some(v)), Err(zbus::Error::MethodError(name, _, _)) if name.as_str() == "org.freedesktop.DBus.Error.UnknownObject" => { Ok(None) } Err(e) => Err(e.into()), } } /// Runs the probing process pub async fn probe(&self) -> Result<(), ServiceError> { Ok(self.storage_proxy.probe().await?) } pub async fn calculate(&self, settings: &StorageSettings) -> Result<u32, ServiceError> { let mut dbus_settings: HashMap<&str, zbus::zvariant::Value<'_>> = HashMap::new(); if let Some(boot_device) = settings.boot_device.clone() { dbus_settings.insert("BootDevice", zbus::zvariant::Value::new(boot_device)); } if let Some(encryption_password) = settings.encryption_password.clone() { dbus_settings.insert( "EncryptionPassword", zbus::zvariant::Value::new(encryption_password), ); } if let Some(lvm) = settings.lvm { dbus_settings.insert("LVM", zbus::zvariant::Value::new(lvm)); } Ok(self.calculator_proxy.calculate(dbus_settings).await?) } } 07070100000048000081A40000000000000000000000016625710300000F12000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/storage/proxies.rs//! D-Bus interface proxies for interfaces implemented by objects in the storage service. //! //! This code was generated by `zbus-xmlgen` `3.1.1` from DBus introspection data. use zbus::dbus_proxy; #[dbus_proxy( interface = "org.opensuse.Agama.Storage1", default_service = "org.opensuse.Agama.Storage1", default_path = "/org/opensuse/Agama/Storage1" )] trait Storage1 { /// Finish method fn finish(&self) -> zbus::Result<()>; /// Install method fn install(&self) -> zbus::Result<()>; /// Probe method fn probe(&self) -> zbus::Result<()>; /// DeprecatedSystem property #[dbus_proxy(property)] fn deprecated_system(&self) -> zbus::Result<bool>; } #[dbus_proxy( interface = "org.opensuse.Agama.Storage1.Proposal.Calculator", default_service = "org.opensuse.Agama.Storage1", default_path = "/org/opensuse/Agama/Storage1" )] trait ProposalCalculator { /// Calculate method fn calculate( &self, settings: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, ) -> zbus::Result<u32>; /// DefaultVolume method fn default_volume( &self, mount_path: &str, ) -> zbus::Result<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>; /// AvailableDevices property #[dbus_proxy(property)] fn available_devices(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// ProductMountPoints property #[dbus_proxy(property)] fn product_mount_points(&self) -> zbus::Result<Vec<String>>; /// Result property #[dbus_proxy(property)] fn result(&self) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; } #[dbus_proxy( interface = "org.opensuse.Agama.Storage1.Proposal", default_service = "org.opensuse.Agama.Storage1", default_path = "/org/opensuse/Agama/Storage1/Proposal" )] trait Proposal { /// Actions property #[dbus_proxy(property)] fn actions( &self, ) -> zbus::Result<Vec<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>>; /// BootDevice property #[dbus_proxy(property)] fn boot_device(&self) -> zbus::Result<String>; /// EncryptionMethod property #[dbus_proxy(property)] fn encryption_method(&self) -> zbus::Result<String>; /// EncryptionPBKDFunction property #[dbus_proxy(property, name = "EncryptionPBKDFunction")] fn encryption_pbkdfunction(&self) -> zbus::Result<String>; /// EncryptionPassword property #[dbus_proxy(property)] fn encryption_password(&self) -> zbus::Result<String>; /// LVM property #[dbus_proxy(property, name = "LVM")] fn lvm(&self) -> zbus::Result<bool>; /// SpacePolicy property #[dbus_proxy(property)] fn space_policy(&self) -> zbus::Result<String>; /// SystemVGDevices property #[dbus_proxy(property, name = "SystemVGDevices")] fn system_vg_devices(&self) -> zbus::Result<Vec<Vec<String>>>; /// Volumes property #[dbus_proxy(property)] fn volumes( &self, ) -> zbus::Result<Vec<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>>; } #[dbus_proxy( interface = "org.opensuse.Agama.Storage1.Block", default_service = "org.opensuse.Agama.Storage1" )] trait BlockDevice { /// Active property #[dbus_proxy(property)] fn active(&self) -> zbus::Result<bool>; /// Name property #[dbus_proxy(property)] fn name(&self) -> zbus::Result<String>; /// Size property #[dbus_proxy(property)] fn size(&self) -> zbus::Result<u64>; /// Systems property #[dbus_proxy(property)] fn systems(&self) -> zbus::Result<Vec<String>>; /// UdevIds property #[dbus_proxy(property)] fn udev_ids(&self) -> zbus::Result<Vec<String>>; /// UdevPaths property #[dbus_proxy(property)] fn udev_paths(&self) -> zbus::Result<Vec<String>>; } 07070100000049000081A4000000000000000000000001662571030000021A000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/storage/settings.rs//! Representation of the storage settings use agama_settings::Settings; use serde::{Deserialize, Serialize}; /// Storage settings for installation #[derive(Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StorageSettings { /// Whether LVM should be enabled pub lvm: Option<bool>, /// Encryption password for the storage devices (in clear text) pub encryption_password: Option<String>, /// Boot device to use in the installation pub boot_device: Option<String>, } 0707010000004A000081A40000000000000000000000016625710300000437000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/storage/store.rs//! Implements the store for the storage settings. use super::{StorageClient, StorageSettings}; use crate::error::ServiceError; use zbus::Connection; /// Loads and stores the storage settings from/to the D-Bus service. pub struct StorageStore<'a> { storage_client: StorageClient<'a>, } impl<'a> StorageStore<'a> { pub async fn new(connection: Connection) -> Result<StorageStore<'a>, ServiceError> { Ok(Self { storage_client: StorageClient::new(connection).await?, }) } pub async fn load(&self) -> Result<StorageSettings, ServiceError> { let boot_device = self.storage_client.boot_device().await?; let lvm = self.storage_client.lvm().await?; let encryption_password = self.storage_client.encryption_password().await?; Ok(StorageSettings { boot_device, lvm, encryption_password, }) } pub async fn store(&self, settings: &StorageSettings) -> Result<(), ServiceError> { self.storage_client.calculate(settings).await?; Ok(()) } } 0707010000004B000081A40000000000000000000000016625710300000E76000000000000000000000000000000000000004600000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/store.rs//! Load/store the settings from/to the D-Bus services. // TODO: quickly explain difference between FooSettings and FooStore, with an example use crate::error::ServiceError; use crate::install_settings::{InstallSettings, Scope}; use crate::{ localization::LocalizationStore, network::NetworkStore, product::ProductStore, software::SoftwareStore, storage::StorageStore, users::UsersStore, }; use zbus::Connection; /// Struct that loads/stores the settings from/to the D-Bus services. /// /// It is composed by a set of "stores" that are able to load/store the /// settings for each service. /// /// This struct uses the default connection built by [connection function](super::connection). pub struct Store<'a> { users: UsersStore<'a>, network: NetworkStore<'a>, product: ProductStore<'a>, software: SoftwareStore<'a>, storage: StorageStore<'a>, localization: LocalizationStore<'a>, } impl<'a> Store<'a> { pub async fn new(connection: Connection) -> Result<Store<'a>, ServiceError> { Ok(Self { localization: LocalizationStore::new(connection.clone()).await?, users: UsersStore::new(connection.clone()).await?, network: NetworkStore::new(connection.clone()).await?, product: ProductStore::new(connection.clone()).await?, software: SoftwareStore::new(connection.clone()).await?, storage: StorageStore::new(connection).await?, }) } /// Loads the installation settings from the D-Bus service pub async fn load(&self, only: Option<Vec<Scope>>) -> Result<InstallSettings, ServiceError> { let scopes = match only { Some(scopes) => scopes, None => Scope::all().to_vec(), }; let mut settings: InstallSettings = Default::default(); if scopes.contains(&Scope::Network) { settings.network = Some(self.network.load().await?); } if scopes.contains(&Scope::Storage) { settings.storage = Some(self.storage.load().await?); } if scopes.contains(&Scope::Software) { settings.software = Some(self.software.load().await?); } if scopes.contains(&Scope::Users) { settings.user = Some(self.users.load().await?); } if scopes.contains(&Scope::Product) { settings.product = Some(self.product.load().await?); } if scopes.contains(&Scope::Localization) { settings.localization = Some(self.localization.load().await?); } // TODO: use try_join here Ok(settings) } /// Stores the given installation settings in the D-Bus service pub async fn store(&self, settings: &InstallSettings) -> Result<(), ServiceError> { if let Some(network) = &settings.network { self.network.store(network).await?; } // order is important here as network can be critical for connection // to registration server and selecting product is important for rest if let Some(product) = &settings.product { self.product.store(product).await?; } // ordering: localization after product as some product may miss some locales if let Some(localization) = &settings.localization { self.localization.store(localization).await?; } if let Some(software) = &settings.software { self.software.store(software).await?; } if let Some(user) = &settings.user { self.users.store(user).await?; } if let Some(storage) = &settings.storage { self.storage.store(storage).await?; } Ok(()) } } 0707010000004C000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004300000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/users0707010000004D000081A400000000000000000000000166257103000000F7000000000000000000000000000000000000004600000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/users.rs//! Implements support for handling the users settings mod client; mod proxies; mod settings; mod store; pub use client::{FirstUser, UsersClient}; pub use settings::{FirstUserSettings, RootUserSettings, UserSettings}; pub use store::UsersStore; 0707010000004E000081A40000000000000000000000016625710300000FA2000000000000000000000000000000000000004D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/users/client.rs//! Implements a client to access Agama's users service. use super::proxies::{FirstUser as FirstUserFromDBus, Users1Proxy}; use crate::error::ServiceError; use agama_settings::{settings::Settings, SettingValue, SettingsError}; use serde::Serialize; use zbus::Connection; /// Represents the settings for the first user #[derive(Serialize, Debug, Default)] pub struct FirstUser { /// First user's full name pub full_name: String, /// First user's username pub user_name: String, /// First user's password (in clear text) pub password: String, /// Whether auto-login should enabled or not pub autologin: bool, /// Additional data coming from the D-Bus service pub data: std::collections::HashMap<String, zbus::zvariant::OwnedValue>, } impl FirstUser { pub fn from_dbus(dbus_data: zbus::Result<FirstUserFromDBus>) -> zbus::Result<Self> { let data = dbus_data?; Ok(Self { full_name: data.0, user_name: data.1, password: data.2, autologin: data.3, data: data.4, }) } } // TODO: use the Settings macro (add support for ignoring fields to the macro and use Option for // FirstUser fields) impl Settings for FirstUser { fn set(&mut self, attr: &str, value: SettingValue) -> Result<(), SettingsError> { match attr { "full_name" => { self.full_name = value .try_into() .map_err(|e| SettingsError::UpdateFailed(attr.to_string(), e))? } "user_name" => { self.user_name = value .try_into() .map_err(|e| SettingsError::UpdateFailed(attr.to_string(), e))? } "password" => { self.full_name = value .try_into() .map_err(|e| SettingsError::UpdateFailed(attr.to_string(), e))? } "autologin" => { self.full_name = value .try_into() .map_err(|e| SettingsError::UpdateFailed(attr.to_string(), e))? } _ => return Err(SettingsError::UnknownAttribute(attr.to_string())), } Ok(()) } } /// D-Bus client for the users service pub struct UsersClient<'a> { users_proxy: Users1Proxy<'a>, } impl<'a> UsersClient<'a> { pub async fn new(connection: Connection) -> zbus::Result<UsersClient<'a>> { Ok(Self { users_proxy: Users1Proxy::new(&connection).await?, }) } /// Returns the settings for first non admin user pub async fn first_user(&self) -> zbus::Result<FirstUser> { FirstUser::from_dbus(self.users_proxy.first_user().await) } /// SetRootPassword method pub async fn set_root_password( &self, value: &str, encrypted: bool, ) -> Result<u32, ServiceError> { Ok(self.users_proxy.set_root_password(value, encrypted).await?) } /// Whether the root password is set or not pub async fn is_root_password(&self) -> Result<bool, ServiceError> { Ok(self.users_proxy.root_password_set().await?) } /// Returns the SSH key for the root user pub async fn root_ssh_key(&self) -> zbus::Result<String> { self.users_proxy.root_sshkey().await } /// SetRootSSHKey method pub async fn set_root_sshkey(&self, value: &str) -> Result<u32, ServiceError> { Ok(self.users_proxy.set_root_sshkey(value).await?) } /// Set the configuration for the first user pub async fn set_first_user( &self, first_user: &FirstUser, ) -> zbus::Result<(bool, Vec<String>)> { self.users_proxy .set_first_user( &first_user.full_name, &first_user.user_name, &first_user.password, first_user.autologin, std::collections::HashMap::new(), ) .await } } 0707010000004F000081A4000000000000000000000001662571030000071C000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/users/proxies.rs//! D-Bus interface proxies for: `org.opensuse.Agama.Users1.*` //! //! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.`. use zbus::dbus_proxy; /// First user as it comes from D-Bus. /// /// It is composed of: /// /// * full name /// * user name /// * password /// * auto-login (enabled or not) /// * some optional and additional data pub type FirstUser = ( String, String, String, bool, std::collections::HashMap<String, zbus::zvariant::OwnedValue>, ); #[dbus_proxy( interface = "org.opensuse.Agama.Users1", default_service = "org.opensuse.Agama.Manager1", default_path = "/org/opensuse/Agama/Users1" )] trait Users1 { /// RemoveFirstUser method fn remove_first_user(&self) -> zbus::Result<u32>; /// RemoveRootPassword method fn remove_root_password(&self) -> zbus::Result<u32>; /// SetFirstUser method fn set_first_user( &self, full_name: &str, user_name: &str, password: &str, auto_login: bool, data: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, ) -> zbus::Result<(bool, Vec<String>)>; /// SetRootPassword method fn set_root_password(&self, value: &str, encrypted: bool) -> zbus::Result<u32>; /// SetRootSSHKey method #[dbus_proxy(name = "SetRootSSHKey")] fn set_root_sshkey(&self, value: &str) -> zbus::Result<u32>; /// Write method fn write(&self) -> zbus::Result<u32>; /// FirstUser property #[dbus_proxy(property)] fn first_user(&self) -> zbus::Result<FirstUser>; /// RootPasswordSet property #[dbus_proxy(property)] fn root_password_set(&self) -> zbus::Result<bool>; /// RootSSHKey property #[dbus_proxy(property, name = "RootSSHKey")] fn root_sshkey(&self) -> zbus::Result<String>; } 07070100000050000081A400000000000000000000000166257103000009AF000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/users/settings.rsuse agama_settings::Settings; use serde::{Deserialize, Serialize}; /// User settings /// /// Holds the user settings for the installation. #[derive(Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UserSettings { #[serde(rename = "user")] #[settings(nested, alias = "user")] pub first_user: Option<FirstUserSettings>, #[settings(nested)] pub root: Option<RootUserSettings>, } /// First user settings /// /// Holds the settings for the first user. #[derive(Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FirstUserSettings { /// First user's full name pub full_name: Option<String>, /// First user's username pub user_name: Option<String>, /// First user's password (in clear text) pub password: Option<String>, /// Whether auto-login should enabled or not pub autologin: Option<bool>, } /// Root user settings /// /// Holds the settings for the root user. #[derive(Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RootUserSettings { /// Root's password (in clear text) #[serde(skip_serializing)] pub password: Option<String>, /// Root SSH public key pub ssh_public_key: Option<String>, } #[cfg(test)] mod tests { use super::*; use agama_settings::settings::Settings; #[test] fn test_user_settings_merge() { let mut user1 = UserSettings::default(); let user2 = UserSettings { first_user: Some(FirstUserSettings { full_name: Some("Jane Doe".to_string()), ..Default::default() }), root: Some(RootUserSettings { password: Some("nots3cr3t".to_string()), ..Default::default() }), }; user1.merge(&user2); let first_user = user1.first_user.unwrap(); assert_eq!(first_user.full_name, Some("Jane Doe".to_string())); let root_user = user1.root.unwrap(); assert_eq!(root_user.password, Some("nots3cr3t".to_string())); } #[test] fn test_merge() { let mut user1 = FirstUserSettings::default(); let user2 = FirstUserSettings { full_name: Some("Jane Doe".to_owned()), autologin: Some(true), ..Default::default() }; user1.merge(&user2); assert_eq!(user1.full_name.unwrap(), "Jane Doe") } } 07070100000051000081A40000000000000000000000016625710300000A84000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-lib/src/users/store.rsuse super::{FirstUser, FirstUserSettings, RootUserSettings, UserSettings, UsersClient}; use crate::error::ServiceError; use zbus::Connection; /// Loads and stores the users settings from/to the D-Bus service. pub struct UsersStore<'a> { users_client: UsersClient<'a>, } impl<'a> UsersStore<'a> { pub async fn new(connection: Connection) -> Result<UsersStore<'a>, ServiceError> { Ok(Self { users_client: UsersClient::new(connection).await?, }) } pub async fn load(&self) -> Result<UserSettings, ServiceError> { let first_user = self.users_client.first_user().await?; let first_user = FirstUserSettings { user_name: Some(first_user.user_name), autologin: Some(first_user.autologin), full_name: Some(first_user.full_name), password: Some(first_user.password), }; let mut root_user = RootUserSettings::default(); let ssh_public_key = self.users_client.root_ssh_key().await?; if !ssh_public_key.is_empty() { root_user.ssh_public_key = Some(ssh_public_key) } Ok(UserSettings { first_user: Some(first_user), root: Some(root_user), }) } pub async fn store(&self, settings: &UserSettings) -> Result<(), ServiceError> { // fixme: improve if let Some(settings) = &settings.first_user { self.store_first_user(settings).await?; } if let Some(settings) = &settings.root { self.store_root_user(settings).await?; } Ok(()) } async fn store_first_user(&self, settings: &FirstUserSettings) -> Result<(), ServiceError> { let first_user = FirstUser { user_name: settings.user_name.clone().unwrap_or_default(), full_name: settings.full_name.clone().unwrap_or_default(), autologin: settings.autologin.unwrap_or_default(), password: settings.password.clone().unwrap_or_default(), ..Default::default() }; let (success, issues) = self.users_client.set_first_user(&first_user).await?; if !success { return Err(ServiceError::WrongUser(issues)); } Ok(()) } async fn store_root_user(&self, settings: &RootUserSettings) -> Result<(), ServiceError> { if let Some(root_password) = &settings.password { self.users_client .set_root_password(root_password, false) .await?; } if let Some(ssh_public_key) = &settings.ssh_public_key { self.users_client.set_root_sshkey(ssh_public_key).await?; } Ok(()) } } 07070100000052000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data07070100000053000081A40000000000000000000000016625710300000184000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/Cargo.toml[package] name = "agama-locale-data" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0" serde = { version = "1.0.152", features = ["derive"] } quick-xml = { version = "0.28.2", features = ["serialize"] } flate2 = "1.0.25" chrono-tz = "0.8.2" regex = "1" thiserror = "1.0.50" 07070100000054000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src07070100000055000081A4000000000000000000000001662571030000102B000000000000000000000000000000000000005D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/deprecated_timezones.rs/// List of timezones which are deprecated and langtables missing translations for it /// /// Filtering it out also helps with returning smaller list of real timezones. /// Sadly many libraries facing issues with deprecated timezones, see e.g. /// pub(crate) const DEPRECATED_TIMEZONES: &[&str] = &[ "Africa/Asmera", // replaced by Africa/Asmara "Africa/Timbuktu", // replaced by Africa/Bamako "America/Argentina/ComodRivadavia", // replaced by America/Argentina/Catamarca "America/Atka", // replaced by America/Adak "America/Ciudad_Juarez", // failed to find replacement "America/Coral_Harbour", // replaced by America/Atikokan "America/Ensenada", // replaced by America/Tijuana "America/Fort_Nelson", "America/Fort_Wayne", // replaced by America/Indiana/Indianapolis "America/Knox_IN", // replaced by America/Indiana/Knox "America/Nuuk", "America/Porto_Acre", // replaced by America/Rio_Branco "America/Punta_Arenas", "America/Rosario", "America/Virgin", "Antarctica/Troll", "Asia/Ashkhabad", // looks like typo/wrong transcript, it should be Asia/Ashgabat "Asia/Atyrau", "Asia/Barnaul", "Asia/Calcutta", // renamed to Asia/Kolkata "Asia/Chita", "Asia/Chungking", "Asia/Dacca", "Asia/Famagusta", "Asia/Katmandu", "Asia/Macao", "Asia/Qostanay", "Asia/Saigon", "Asia/Srednekolymsk", "Asia/Tel_Aviv", "Asia/Thimbu", "Asia/Tomsk", "Asia/Ujung_Pandang", "Asia/Ulan_Bator", "Asia/Yangon", "Atlantic/Faeroe", "Atlantic/Jan_Mayen", "Australia/ACT", "Australia/Canberra", "Australia/LHI", "Australia/NSW", "Australia/North", "Australia/Queensland", "Australia/South", "Australia/Tasmania", "Australia/Victoria", "Australia/West", "Australia/Yancowinna", "Brazil/Acre", "Brazil/DeNoronha", "Brazil/East", "Brazil/West", "CET", "CST6CDT", "Canada/Atlantic", // all canada TZ was replaced by America ones "Canada/Central", "Canada/Eastern", "Canada/Mountain", "Canada/Newfoundland", "Canada/Pacific", "Canada/Saskatchewan", "Canada/Yukon", "Chile/Continental", // all Chile was replaced by continental America tz "Chile/EasterIsland", "Cuba", "EET", "EST", // not sure why it is not in langtable "EST5EDT", "Egypt", "Eire", "Etc/GMT", "Etc/GMT+0", "Etc/GMT+1", "Etc/GMT+2", "Etc/GMT+3", "Etc/GMT+4", "Etc/GMT+5", "Etc/GMT+6", "Etc/GMT+7", "Etc/GMT+8", "Etc/GMT+9", "Etc/GMT+10", "Etc/GMT+11", "Etc/GMT+12", "Etc/GMT-0", "Etc/GMT-1", "Etc/GMT-2", "Etc/GMT-3", "Etc/GMT-4", "Etc/GMT-5", "Etc/GMT-6", "Etc/GMT-7", "Etc/GMT-8", "Etc/GMT-9", "Etc/GMT-10", "Etc/GMT-11", "Etc/GMT-12", "Etc/GMT-13", "Etc/GMT-14", "Etc/GMT0", "Etc/Greenwich", "Etc/UCT", "Etc/UTC", "Etc/Universal", "Etc/Zulu", "Europe/Astrakhan", "Europe/Belfast", "Europe/Kirov", "Europe/Kyiv", "Europe/Saratov", "Europe/Tiraspol", "Europe/Ulyanovsk", "GB", "GB-Eire", "GMT", "GMT+0", "GMT-0", "GMT0", "Greenwich", "HST", "Hongkong", "Iceland", "Iran", "Israel", "Jamaica", "Japan", "Kwajalein", "Libya", "MET", "Mexico/BajaNorte", "Mexico/BajaSur", "Mexico/General", "MST", "MST7MDT", "NZ", "NZ-CHAT", "Navajo", "Pacific/Bougainville", "Pacific/Kanton", "Pacific/Ponape", "Pacific/Samoa", "Pacific/Truk", "Pacific/Yap", "PRC", "PST8PDT", "Poland", "Portugal", "ROC", "ROK", "Singapore", "Turkey", "UCT", "Universal", "US/Aleutian", // all US/ replaced by America "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa", "W-SU", "WET", "Zulu", ]; 07070100000056000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/keyboard07070100000057000081A40000000000000000000000016625710300000080000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/keyboard.rspub mod xkb_config_registry; pub mod xkeyboard; pub use xkb_config_registry::XkbConfigRegistry; pub use xkeyboard::XKeyboards; 07070100000058000081A400000000000000000000000166257103000006BB000000000000000000000000000000000000006500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/keyboard/xkb_config_registry.rs//! This module aims to read the information in the X Keyboard Configuration Database. //! //! https://freedesktop.org/Software/XKeyboardConfig use quick_xml::de::from_str; use serde::Deserialize; use std::{error::Error, fs}; const DB_PATH: &str = "/usr/share/X11/xkb/rules/base.xml"; /// X Keyboard Configuration Database #[derive(Deserialize, Debug)] pub struct XkbConfigRegistry { #[serde(rename = "layoutList")] pub layout_list: LayoutList, } impl XkbConfigRegistry { /// Reads the database from the given file /// /// - `path`: database path. pub fn from(path: &str) -> Result<Self, Box<dyn Error>> { let contents = fs::read_to_string(path)?; Ok(from_str(&contents)?) } /// Reads the database from the default path. pub fn from_system() -> Result<Self, Box<dyn Error>> { Self::from(DB_PATH) } } #[derive(Deserialize, Debug)] pub struct LayoutList { #[serde(rename = "layout")] pub layouts: Vec<Layout>, } #[derive(Deserialize, Debug)] pub struct Layout { #[serde(rename = "configItem")] pub config_item: ConfigItem, #[serde(rename = "variantList", default)] pub variants_list: VariantList, } #[derive(Deserialize, Debug)] pub struct ConfigItem { pub name: String, #[serde(rename = "description")] pub description: String, } #[derive(Deserialize, Debug, Default)] pub struct VariantList { #[serde(rename = "variant", default)] pub variants: Vec<Variant>, } #[derive(Deserialize, Debug)] pub struct Variant { #[serde(rename = "configItem")] pub config_item: VariantConfigItem, } #[derive(Deserialize, Debug)] pub struct VariantConfigItem { pub name: String, pub description: String, } 07070100000059000081A40000000000000000000000016625710300000231000000000000000000000000000000000000005B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/keyboard/xkeyboard.rsuse serde::Deserialize; use crate::ranked::{RankedLanguages, RankedTerritories}; #[derive(Debug, Deserialize)] pub struct XKeyboard { #[serde(rename(deserialize = "keyboardId"))] /// like "layout(variant)", for example "us" or "ua(phonetic)" pub id: String, /// like "Ukrainian (phonetic)" pub description: String, pub ascii: bool, pub comment: Option<String>, pub languages: RankedLanguages, pub territories: RankedTerritories, } #[derive(Debug, Deserialize)] pub struct XKeyboards { pub keyboard: Vec<XKeyboard>, } 0707010000005A000081A40000000000000000000000016625710300000229000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/language.rsuse serde::Deserialize; use crate::ranked::{RankedLocales, RankedTerritories}; #[derive(Debug, Deserialize)] pub struct Language { #[serde(rename(deserialize = "languageId"))] pub id: String, pub territories: RankedTerritories, pub locales: RankedLocales, pub names: crate::localization::Localization, } #[derive(Debug, Deserialize)] pub struct Languages { pub language: Vec<Language>, } impl Languages { pub fn find_by_id(&self, id: &str) -> Option<&Language> { self.language.iter().find(|t| t.id == id) } } 0707010000005B000081A40000000000000000000000016625710300001905000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/lib.rsuse anyhow::Context; use flate2::bufread::GzDecoder; use quick_xml::de::Deserializer; use serde::Deserialize; use std::collections::HashMap; use std::fs::File; use std::io::BufRead; use std::io::BufReader; use std::process::Command; pub mod deprecated_timezones; pub mod keyboard; pub mod language; mod locale; pub mod localization; pub mod ranked; pub mod territory; pub mod timezone_part; use keyboard::xkeyboard; pub use locale::{InvalidKeymap, InvalidLocaleCode, KeymapId, LocaleCode}; fn file_reader(file_path: &str) -> anyhow::Result<impl BufRead> { let file = File::open(file_path) .with_context(|| format!("Failed to read langtable-data ({})", file_path))?; let reader = BufReader::new(GzDecoder::new(BufReader::new(file))); Ok(reader) } /// Gets list of X11 keyboards structs pub fn get_xkeyboards() -> anyhow::Result<xkeyboard::XKeyboards> { const FILE_PATH: &str = "/usr/share/langtable/data/keyboards.xml.gz"; let reader = file_reader(FILE_PATH)?; let mut deserializer = Deserializer::from_reader(reader); let ret = xkeyboard::XKeyboards::deserialize(&mut deserializer) .context("Failed to deserialize keyboard entry")?; Ok(ret) } /// Gets list of available keymaps /// /// ## Examples /// Requires working localectl. /// /// ```no_run /// use agama_locale_data::KeymapId; /// /// let key_maps = agama_locale_data::get_localectl_keymaps().unwrap(); /// let us: KeymapId = "us".parse().unwrap(); /// assert!(key_maps.contains(&us)); /// ``` pub fn get_localectl_keymaps() -> anyhow::Result<Vec<KeymapId>> { let output = Command::new("localectl") .arg("list-keymaps") .output() .context("failed to execute localectl list-maps")? .stdout; let output = String::from_utf8(output).context("Strange localectl output formatting")?; let ret: Vec<_> = output.lines().flat_map(|l| l.parse().ok()).collect(); Ok(ret) } /// Returns struct which contain list of known languages pub fn get_languages() -> anyhow::Result<language::Languages> { const FILE_PATH: &str = "/usr/share/langtable/data/languages.xml.gz"; let reader = file_reader(FILE_PATH)?; let mut deserializer = Deserializer::from_reader(reader); let ret = language::Languages::deserialize(&mut deserializer) .context("Failed to deserialize language entry")?; Ok(ret) } /// Returns struct which contain list of known territories pub fn get_territories() -> anyhow::Result<territory::Territories> { const FILE_PATH: &str = "/usr/share/langtable/data/territories.xml.gz"; let reader = file_reader(FILE_PATH)?; let mut deserializer = Deserializer::from_reader(reader); let ret = territory::Territories::deserialize(&mut deserializer) .context("Failed to deserialize territory entry")?; Ok(ret) } /// Returns struct which contain list of known parts of timezones. Useful for translation pub fn get_timezone_parts() -> anyhow::Result<timezone_part::TimezoneIdParts> { const FILE_PATH: &str = "/usr/share/langtable/data/timezoneidparts.xml.gz"; let reader = file_reader(FILE_PATH)?; let mut deserializer = Deserializer::from_reader(reader); let ret = timezone_part::TimezoneIdParts::deserialize(&mut deserializer) .context("Failed to deserialize timezone part entry")?; Ok(ret) } /// Returns a hash mapping timezones to its main country (typically, the country of /// the city that is used to name the timezone). The information is read from the /// file /usr/share/zoneinfo/zone.tab. pub fn get_timezone_countries() -> anyhow::Result<HashMap<String, String>> { const FILE_PATH: &str = "/usr/share/zoneinfo/zone.tab"; let content = std::fs::read_to_string(FILE_PATH) .with_context(|| format!("Failed to read {}", FILE_PATH))?; let countries = content .lines() .filter_map(|line| { if line.starts_with('#') { return None; } let fields: Vec<&str> = line.split('\t').collect(); Some((fields.get(2)?.to_string(), fields.first()?.to_string())) }) .collect(); Ok(countries) } /// Gets list of non-deprecated timezones pub fn get_timezones() -> Vec<String> { chrono_tz::TZ_VARIANTS .iter() .filter(|&tz| !crate::deprecated_timezones::DEPRECATED_TIMEZONES.contains(&tz.name())) // Filter out deprecated asmera .map(|e| e.name().to_string()) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_get_keyboards() { let result = get_xkeyboards().unwrap(); let first = result.keyboard.first().expect("no keyboards"); assert_eq!(first.id, "ad") } #[test] fn test_get_languages() { let result = get_languages().unwrap(); let first = result.language.first().expect("no languages"); assert_eq!(first.id, "aa") } #[test] fn test_get_territories() { let result = get_territories().unwrap(); let first = result.territory.first().expect("no territories"); assert_eq!(first.id, "001") // looks strange, but it is meta id for whole world } #[test] fn test_get_timezone_parts() { let result = get_timezone_parts().unwrap(); let first = result.timezone_part.first().expect("no timezone parts"); assert_eq!(first.id, "Abidjan") } #[test] fn test_get_timezones() { let result = get_timezones(); assert_eq!(result.len(), 430); let first = result.first().expect("no keyboards"); assert_eq!(first, "Africa/Abidjan"); // test that we filter out deprecates Asmera ( there is already recent Asmara) let asmera = result.iter().find(|&t| t == "Africa/Asmera"); assert_eq!(asmera, None); let asmara = result.iter().find(|&t| t == "Africa/Asmara"); assert_eq!(asmara, Some(&"Africa/Asmara".to_string())); // here test that timezones from timezones matches ones in langtable ( as timezones can contain deprecated ones) // so this test catch if there is new zone that is not translated or if a zone is become deprecated let timezones = get_timezones(); let localized = get_timezone_parts() .unwrap() .localize_timezones("de", &timezones); let _res: Vec<(String, String)> = timezones.into_iter().zip(localized).collect(); } } 0707010000005C000081A4000000000000000000000001662571030000176C000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/locale.rs//! Defines useful types to deal with localization values use regex::Regex; use serde::Serialize; use std::sync::OnceLock; use std::{fmt::Display, str::FromStr}; use thiserror::Error; #[derive(Clone, Debug, PartialEq, Serialize)] pub struct LocaleCode { // ISO-639 pub language: String, // ISO-3166 pub territory: String, pub encoding: String, } impl Display for LocaleCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}_{}.{}", &self.language, &self.territory, &self.encoding ) } } impl Default for LocaleCode { fn default() -> Self { Self { language: "en".to_string(), territory: "US".to_string(), encoding: "UTF-8".to_string(), } } } #[derive(Error, Debug)] #[error("Not a valid locale string: {0}")] pub struct InvalidLocaleCode(String); impl TryFrom<&str> for LocaleCode { type Error = InvalidLocaleCode; fn try_from(value: &str) -> Result<Self, Self::Error> { let locale_regexp: Regex = Regex::new(r"^([[:alpha:]]+)_([[:alpha:]]+)(?:\.(.+))?").unwrap(); let captures = locale_regexp .captures(value) .ok_or_else(|| InvalidLocaleCode(value.to_string()))?; let encoding = captures .get(3) .map(|e| e.as_str()) .unwrap_or("UTF-8") .to_string(); Ok(Self { language: captures.get(1).unwrap().as_str().to_string(), territory: captures.get(2).unwrap().as_str().to_string(), encoding, }) } } static KEYMAP_ID_REGEX: OnceLock<Regex> = OnceLock::new(); /// Keymap layout identifier /// /// ``` /// use agama_locale_data::KeymapId; /// use std::str::FromStr; /// /// let id: KeymapId = "es(ast)".parse().unwrap(); /// assert_eq!(id.layout, "es"); /// assert_eq!(id.variant, Some("ast".to_string())); /// assert_eq!(id.dashed(), "es-ast".to_string()); /// /// let id_with_dashes: KeymapId = "es-ast".parse().unwrap(); /// assert_eq!(id, id_with_dashes); /// ``` #[derive(Clone, Debug, PartialEq, Serialize)] pub struct KeymapId { pub layout: String, pub variant: Option<String>, } #[derive(Error, Debug, PartialEq)] #[error("Invalid keymap ID: {0}")] pub struct InvalidKeymap(String); impl KeymapId { pub fn dashed(&self) -> String { if let Some(variant) = &self.variant { format!("{}-{}", &self.layout, variant) } else { self.layout.to_owned() } } } impl Display for KeymapId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(variant) = &self.variant { write!(f, "{}({})", &self.layout, variant) } else { write!(f, "{}", &self.layout) } } } impl FromStr for KeymapId { type Err = InvalidKeymap; fn from_str(s: &str) -> Result<Self, Self::Err> { let re = KEYMAP_ID_REGEX // https://docs.rs/regex/latest/regex/#example-verbose-mode .get_or_init(|| { Regex::new( r"(?x) ^ ([\w.]+) # layout part ( # optional variant: \( (?<var1>.+) \) # in parentheses, X11 style | - (?<var2>.+) # or after a minus, console style )? $ # must match whole input, no substring allowed ", ) .unwrap() }); if let Some(parts) = re.captures(s) { let mut variant = None; if let Some(var1) = parts.name("var1") { variant = Some(var1.as_str().to_string()); } if let Some(var2) = parts.name("var2") { variant = Some(var2.as_str().to_string()); } Ok(KeymapId { layout: parts[1].to_string(), variant, }) } else { Err(InvalidKeymap(s.to_string())) } } } #[cfg(test)] mod test { use super::KeymapId; use std::str::FromStr; #[test] fn test_parse_keymap_id() { let keymap_id0 = KeymapId::from_str("es").unwrap(); assert_eq!( KeymapId { layout: "es".to_string(), variant: None }, keymap_id0 ); let keymap_id1 = KeymapId::from_str("es(ast)").unwrap(); assert_eq!( KeymapId { layout: "es".to_string(), variant: Some("ast".to_string()) }, keymap_id1 ); let keymap_id2 = KeymapId::from_str("es-ast").unwrap(); assert_eq!( KeymapId { layout: "es".to_string(), variant: Some("ast".to_string()) }, keymap_id2 ); let keymap_id3 = KeymapId::from_str("pt-nativo-us").unwrap(); assert_eq!( KeymapId { layout: "pt".to_string(), variant: Some("nativo-us".to_string()) }, keymap_id3 ); let keymap_id4 = KeymapId::from_str("lt.std").unwrap(); assert_eq!( KeymapId { layout: "lt.std".to_string(), variant: None }, keymap_id4 ); } #[test] fn test_parse_keymap_id_err() { // no word characters for layout let result = KeymapId::from_str("$%&"); assert!(result.is_err()); // layout is there but with trailing garbage let result = KeymapId::from_str("cz@"); assert!(result.is_err()); // variant but then another variant let result = KeymapId::from_str("cz(qwerty)-yeah"); assert!(result.is_err()); } } 0707010000005D000081A4000000000000000000000001662571030000020E000000000000000000000000000000000000005500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/localization.rsuse serde::Deserialize; #[derive(Debug, Deserialize)] pub struct Localization { pub name: Vec<LocalizationEntry>, } impl Localization { pub fn name_for(&self, language: &str) -> Option<String> { let entry = self.name.iter().find(|n| n.language == language)?; Some(entry.value.clone()) } } #[derive(Debug, Deserialize)] pub struct LocalizationEntry { #[serde(rename(deserialize = "languageId"))] pub language: String, #[serde(rename(deserialize = "trName"))] pub value: String, } 0707010000005E000081A400000000000000000000000166257103000003CD000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/ranked.rs//! Bigger rank means it is more important use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct RankedLanguage { #[serde(rename(deserialize = "languageId"))] pub id: String, /// Bigger rank means it is more important pub rank: u16, } #[derive(Debug, Deserialize)] pub struct RankedLanguages { #[serde(default)] pub language: Vec<RankedLanguage>, } #[derive(Debug, Deserialize)] pub struct RankedTerritory { #[serde(rename(deserialize = "territoryId"))] pub id: String, /// Bigger rank means it is more important pub rank: u16, } #[derive(Debug, Deserialize)] pub struct RankedTerritories { #[serde(default)] pub territory: Vec<RankedTerritory>, } #[derive(Debug, Deserialize)] pub struct RankedLocale { #[serde(rename(deserialize = "localeId"))] pub id: String, pub rank: u16, } #[derive(Debug, Deserialize)] pub struct RankedLocales { #[serde(default)] pub locale: Vec<RankedLocale>, } 0707010000005F000081A400000000000000000000000166257103000001E6000000000000000000000000000000000000005200000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/territory.rsuse serde::Deserialize; #[derive(Debug, Deserialize)] pub struct Territory { #[serde(rename(deserialize = "territoryId"))] pub id: String, pub languages: crate::ranked::RankedLanguages, pub names: crate::localization::Localization, } #[derive(Debug, Deserialize)] pub struct Territories { pub territory: Vec<Territory>, } impl Territories { pub fn find_by_id(&self, id: &str) -> Option<&Territory> { self.territory.iter().find(|t| t.id == id) } } 07070100000060000081A40000000000000000000000016625710300000A26000000000000000000000000000000000000005600000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-locale-data/src/timezone_part.rsuse std::collections::HashMap; use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct TimezoneIdPart { #[serde(rename(deserialize = "timezoneIdPartId"))] /// "Prague" pub id: String, /// [{language: "cs", value: "Praha"}, {"language": "de", value: "Prag"} ...] pub names: crate::localization::Localization, } // Timezone id parts are useful mainly for localization of timezones // Just search each part of timezone for translation #[derive(Debug, Deserialize)] pub struct TimezoneIdParts { #[serde(rename(deserialize = "timezoneIdPart"))] pub timezone_part: Vec<TimezoneIdPart>, } impl TimezoneIdParts { // TODO: Implement a caching mechanism pub fn localize_part(&self, part_id: &str, language: &str) -> Option<String> { self.timezone_part .iter() .find(|p| p.id == part_id) .and_then(|p| p.names.name_for(language)) } /// Localized given list of timezones to given language /// # Examples /// /// ``` /// let parts = agama_locale_data::get_timezone_parts().expect("missing timezone parts"); /// let timezones = vec!["Europe/Prague".to_string(), "Europe/Berlin".to_string()]; /// let result = vec!["Evropa/Praha".to_string(), "Evropa/Berlín".to_string()]; /// assert_eq!(parts.localize_timezones("cs", &timezones), result); /// ``` pub fn localize_timezones(&self, language: &str, timezones: &[String]) -> Vec<String> { let mapping = self.construct_mapping(language); timezones .iter() .map(|tz| self.translate_timezone(&mapping, tz)) .collect() } fn construct_mapping(&self, language: &str) -> HashMap<String, String> { let mut res: HashMap<String, String> = HashMap::with_capacity(self.timezone_part.len()); self.timezone_part .iter() .map(|part| (part.id.clone(), part.names.name_for(language))) .for_each(|(time_id, names)| { // skip missing translations if let Some(trans) = names { res.insert(time_id, trans); } }); res } fn translate_timezone(&self, mapping: &HashMap<String, String>, timezone: &str) -> String { timezone .split('/') .map(|tzp| { mapping .get(&tzp.to_string()) .unwrap_or_else(|| panic!("Unknown timezone part {tzp}")) .to_owned() }) .collect::<Vec<String>>() .join("/") } } 07070100000061000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server07070100000062000081A40000000000000000000000016625710300000764000000000000000000000000000000000000004700000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/Cargo.toml[package] name = "agama-server" version = "0.1.0" edition = "2021" rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0" agama-locale-data = { path = "../agama-locale-data" } agama-lib = { path = "../agama-lib" } log = "0.4" simplelog = "0.12.1" systemd-journal-logger = "1.0" zbus = { version = "3", default-features = false, features = ["tokio"] } zbus_macros = "3" uuid = { version = "1.3.4", features = ["v4"] } thiserror = "1.0.40" serde = { version = "1.0.152", features = ["derive"] } serde_yaml = "0.9.24" cidr = { version = "0.2.2", features = ["serde"] } tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1.14" gettext-rs = { version = "0.7.0", features = ["gettext-system"] } regex = "1.10.2" once_cell = "1.18.0" macaddr = "1.0" async-trait = "0.1.75" axum = { version = "0.7.4", features = ["ws"] } serde_json = "1.0.113" tower-http = { version = "0.5.1", features = ["compression-br", "trace"] } tracing-subscriber = "0.3.18" tracing-journald = "0.3.0" tracing = "0.1.40" clap = { version = "4.5.0", features = ["derive", "wrap_help"] } tower = "0.4.13" utoipa = { version = "4.2.0", features = ["axum_extras"] } config = "0.14.0" rand = "0.8.5" jsonwebtoken = "9.2.0" axum-extra = { version = "0.9.2", features = ["typed-header"] } chrono = { version = "0.4.34", default-features = false, features = [ "now", "std", "alloc", "clock", ] } pam = "0.8.0" serde_with = "3.6.1" openssl = "0.10.64" hyper = "1.2.0" hyper-util = "0.1.3" tokio-openssl = "0.6.4" futures-util = { version = "0.3.30", default-features = false, features = ["alloc"] } [[bin]] name = "agama-dbus-server" path = "src/agama-dbus-server.rs" [[bin]] name = "agama-web-server" path = "src/agama-web-server.rs" [dev-dependencies] http-body-util = "0.1.0" 07070100000063000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004200000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/share07070100000064000081A4000000000000000000000001662571030000001A000000000000000000000000000000000000005600000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/share/server-example.yaml--- jwt_key: "replace-me" 07070100000065000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src07070100000066000081A4000000000000000000000001662571030000077B000000000000000000000000000000000000005500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/agama-dbus-server.rsuse agama_server::{ l10n::{self, helpers}, network, questions, }; use agama_lib::connection_to; use anyhow::Context; use log::{self, LevelFilter}; use std::future::pending; const ADDRESS: &str = "unix:path=/run/agama/bus"; const SERVICE_NAME: &str = "org.opensuse.Agama1"; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let locale = helpers::init_locale()?; // be smart with logging and log directly to journal if connected to it if systemd_journal_logger::connected_to_journal() { // unwrap here is intentional as we are sure no other logger is active yet systemd_journal_logger::JournalLog::default() .install() .unwrap(); log::set_max_level(LevelFilter::Info); // log only info for journal logger } else { simplelog::TermLogger::init( LevelFilter::Info, // lets use info, trace provides too much output from libraries simplelog::Config::default(), simplelog::TerminalMode::Stderr, // only stderr output for easier filtering simplelog::ColorChoice::Auto, ) .unwrap(); // unwrap here as we are sure no other logger active } let connection = connection_to(ADDRESS) .await .expect("Could not connect to the D-Bus daemon"); // When adding more services here, the order might be important. questions::export_dbus_objects(&connection).await?; log::info!("Started questions interface"); l10n::export_dbus_objects(&connection, &locale).await?; log::info!("Started locale interface"); network::export_dbus_objects(&connection).await?; log::info!("Started network interface"); connection .request_name(SERVICE_NAME) .await .context(format!("Requesting name {SERVICE_NAME}"))?; // Do other things or go to wait forever pending::<()>().await; Ok(()) } 07070100000067000081A40000000000000000000000016625710300002D5A000000000000000000000000000000000000005400000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/agama-web-server.rsuse agama_lib::connection_to; use agama_server::{ l10n::helpers, web::{self, run_monitor}, }; use axum::{ extract::Request as AxumRequest, http::{Request, Response}, Router, }; use clap::{Args, Parser, Subcommand}; use futures_util::pin_mut; use hyper::body::Incoming; use hyper_util::rt::{TokioExecutor, TokioIo}; use hyper_util::server::conn::auto::Builder; use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod}; use std::process::{ExitCode, Termination}; use std::{path::PathBuf, pin::Pin}; use tokio::sync::broadcast::channel; use tokio_openssl::SslStream; use tower::Service; use tracing_subscriber::prelude::*; use utoipa::OpenApi; #[derive(Subcommand, Debug)] enum Commands { /// Start the API server. Serve(ServeArgs), /// Display the API documentation in OpenAPI format. Openapi, } #[derive(Parser, Debug)] #[command( version, about = "Starts the Agama web-based API.", long_about = None)] struct Cli { #[command(subcommand)] pub command: Commands, } #[derive(Args, Debug)] struct ServeArgs { // Address/port to listen on (":::3000" listens for both IPv6 and IPv4 // connections unless manually disabled in /proc/sys/net/ipv6/bindv6only) #[arg(long, default_value = ":::3000", help = "Primary address to listen on")] address: String, #[arg( long, default_value = None, help = "Optional secondary address to listen on" )] address2: Option<String>, #[arg( long, default_value = None, help = "Path to the SSL private key file in PEM format" )] key: Option<String>, #[arg( long, default_value = None, help = "Path to the SSL certificate file in PEM format" )] cert: Option<String>, // Agama D-Bus address #[arg( long, default_value = "unix:path=/run/agama/bus", help = "The D-Bus address for connecting to the Agama service" )] dbus_address: String, } impl ServeArgs { /// Builds an SSL acceptor using a provided SSL certificate or generates a self-signed one fn ssl_acceptor(&self) -> Result<SslAcceptor, openssl::error::ErrorStack> { let mut tls_builder = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server())?; if let (Some(cert), Some(key)) = (self.cert.clone(), self.key.clone()) { tracing::info!("Loading PEM certificate: {}", cert); tls_builder.set_certificate_file(PathBuf::from(cert), SslFiletype::PEM)?; tracing::info!("Loading PEM key: {}", key); tls_builder.set_private_key_file(PathBuf::from(key), SslFiletype::PEM)?; } else { let (cert, key) = agama_server::cert::create_certificate()?; tls_builder.set_private_key(&key)?; tls_builder.set_certificate(&cert)?; } // check that the key belongs to the certificate tls_builder.check_private_key()?; Ok(tls_builder.build()) } } /// Checks whether the connection uses SSL or not /// `stream`: the TCP stream containing a request from client async fn is_ssl_stream(stream: &tokio::net::TcpStream) -> bool { // a buffer for reading the first byte from the TCP connection let mut buf = [0u8; 1]; // peek() receives the data without removing it from the stream, // the data is not consumed, it will be read from the stream again later stream .peek(&mut buf) .await // SSL3.0/TLS1.x starts with byte 0x16 // SSL2 starts with 0x80 (but should not be used as it is considered insecure) // see https://stackoverflow.com/q/3897883 // otherwise consider the stream as a plain HTTP stream possibly starting with // "GET ... HTTP/1.1" or "POST ... HTTP/1.1" or a similar line .is_ok_and(|_| buf[0] == 0x16u8 || buf[0] == 0x80u8) } /// Builds a response for the HTTP -> HTTPS redirection /// returns (HTTP response status code) 308 permanent redirect fn redirect_https(host: &str, uri: &hyper::Uri) -> Response<String> { let builder = Response::builder() // build the redirection target URL .header("Location", format!("https://{}{}", host, uri)) .status(hyper::StatusCode::PERMANENT_REDIRECT); // according to documentation this can fail only if builder was previosly fed with data // which failed to parse into an internal representation (e.g. invalid header) builder .body(String::from("")) .expect("Failed to create redirection request") } /// Builds an error response for the HTTP -> HTTPS redirection when we cannot build /// the redirect response from the original request /// returns error 400 fn redirect_error() -> Response<String> { let builder = Response::builder().status(hyper::StatusCode::BAD_REQUEST); let msg = "HTTP protocol is not allowed for external requests, please use HTTPS.\n"; // according to documentation this can fail only if builder was previosly fed with data // which failed to parse into an internal representation (e.g. invalid header) builder .body(String::from(msg)) .expect("Failed to create an error response") } /// Builds a router for the HTTP -> HTTPS redirection /// if the redirection URL cannot be built from the original request it returns error 400 /// instead of the redirection fn https_redirect() -> Router { // see https://docs.rs/axum/latest/axum/routing/struct.Router.html#example let redirect_service = tower::service_fn(|req: AxumRequest| async move { if let Some(host) = req.headers().get("host").and_then(|h| h.to_str().ok()) { Ok(redirect_https(host, req.uri())) } else { Ok(redirect_error()) } }); Router::new() // the wildcard path below does not match an empty path, we need to match it explicitly .route_service("/", redirect_service) .route_service("/*path", redirect_service) } /// handle the HTTPS connection async fn handle_https_stream( tls_acceptor: SslAcceptor, addr: std::net::SocketAddr, tcp_stream: tokio::net::TcpStream, service: axum::Router, ) { // handle HTTPS connection let ssl = Ssl::new(tls_acceptor.context()).unwrap(); let mut tls_stream = SslStream::new(ssl, tcp_stream).unwrap(); if let Err(err) = SslStream::accept(Pin::new(&mut tls_stream)).await { tracing::error!("Error during TSL handshake from {}: {}", addr, err); } else { let stream = TokioIo::new(tls_stream); let hyper_service = hyper::service::service_fn(move |request: Request<Incoming>| { service.clone().call(request) }); let ret = Builder::new(TokioExecutor::new()) .serve_connection_with_upgrades(stream, hyper_service) .await; if let Err(err) = ret { tracing::error!("Error serving connection from {}: {}", addr, err); } } } /// handle the HTTP connection async fn handle_http_stream( addr: std::net::SocketAddr, tcp_stream: tokio::net::TcpStream, service: axum::Router, redirector_service: axum::Router, ) { let stream = TokioIo::new(tcp_stream); let hyper_service = hyper::service::service_fn(move |request: Request<Incoming>| { // check if it is local connection or external // the to_canonical() converts IPv4-mapped IPv6 addresses // to plain IPv4, then is_loopback() works correctly for the IPv4 connections if addr.ip().to_canonical().is_loopback() { // accept plain HTTP on the local connection service.clone().call(request) } else { // redirect external connections to HTTPS redirector_service.clone().call(request) } }); let ret = Builder::new(TokioExecutor::new()) .serve_connection_with_upgrades(stream, hyper_service) .await; if let Err(err) = ret { tracing::error!("Error serving connection from {}: {}", addr, err); } } /// Starts the web server async fn start_server(address: String, service: Router, ssl_acceptor: SslAcceptor) { tracing::info!("Starting Agama web server at {}", address); // see https://github.com/tokio-rs/axum/blob/main/examples/low-level-openssl/src/main.rs // how to use axum with openSSL let listener = tokio::net::TcpListener::bind(&address) .await .unwrap_or_else(|error| { let msg = format!("Error: could not listen on {}: {}", &address, error); tracing::error!(msg); panic!("{}", msg) }); pin_mut!(listener); let redirector = https_redirect(); loop { let tower_service = service.clone(); let redirector_service = redirector.clone(); let tls_acceptor = ssl_acceptor.clone(); // Wait for a new tcp connection; if it fails we cannot do much, so print an error and die let (tcp_stream, addr) = listener .accept() .await .expect("Failed to open port for listening"); tokio::spawn(async move { if is_ssl_stream(&tcp_stream).await { // handle HTTPS connection handle_https_stream(tls_acceptor, addr, tcp_stream, tower_service).await; } else { // handle HTTP connection handle_http_stream(addr, tcp_stream, tower_service, redirector_service).await; } }); } } /// Start serving the API. /// `options`: command-line arguments. async fn serve_command(args: ServeArgs) -> anyhow::Result<()> { let journald = tracing_journald::layer().expect("could not connect to journald"); tracing_subscriber::registry().with(journald).init(); let (tx, _) = channel(16); run_monitor(tx.clone()).await?; let config = web::ServiceConfig::load()?; let dbus = connection_to(&args.dbus_address).await?; let service = web::service(config, tx, dbus).await?; // TODO: Move elsewhere? Use a singleton? (It would be nice to use the same // generated self-signed certificate on both ports.) let ssl_acceptor = if let Ok(ssl_acceptor) = args.ssl_acceptor() { ssl_acceptor } else { return Err(anyhow::anyhow!("SSL initialization failed")); }; let mut addresses = vec![args.address]; if let Some(a) = args.address2 { addresses.push(a) } let servers: Vec<_> = addresses .iter() .map(|a| { tokio::spawn(start_server( a.clone(), service.clone(), ssl_acceptor.clone(), )) }) .collect(); futures_util::future::join_all(servers).await; Ok(()) } /// Display the API documentation in OpenAPI format. fn openapi_command() -> anyhow::Result<()> { println!("{}", web::ApiDoc::openapi().to_pretty_json().unwrap()); Ok(()) } async fn run_command(cli: Cli) -> anyhow::Result<()> { match cli.command { Commands::Serve(options) => serve_command(options).await, Commands::Openapi => openapi_command(), } } /// Represents the result of execution. pub enum CliResult { /// Successful execution. Ok = 0, /// Something went wrong. Error = 1, } impl Termination for CliResult { fn report(self) -> ExitCode { ExitCode::from(self as u8) } } #[tokio::main] async fn main() -> CliResult { let cli = Cli::parse(); _ = helpers::init_locale(); if let Err(error) = run_command(cli).await { eprintln!("{:?}", error); return CliResult::Error; } CliResult::Ok } 07070100000068000081A40000000000000000000000016625710300000CCB000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/cert.rsuse openssl::asn1::Asn1Time; use openssl::bn::{BigNum, MsbOption}; use openssl::error::ErrorStack; use openssl::hash::MessageDigest; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; use openssl::x509::extension::{ BasicConstraints, KeyUsage, SubjectAlternativeName, SubjectKeyIdentifier, }; use openssl::x509::{X509NameBuilder, X509}; // TODO: move the certificate related functions into a struct // // struct Certificate { // certificate: X509, // key: PKey<Private>, // } // // impl Certificate { // // read from file, support some default location // // (like /etc/agama.d/ssl/{certificate,key}.pem ?) // pub read(cert: &str, key: &str) -> Result<Self>; // // generate a self-signed certificate // pub new() -> Self // // dump to file // pub write(...) // } /// Generates a self-signed SSL certificate /// see https://github.com/sfackler/rust-openssl/blob/master/openssl/examples/mk_certs.rs pub fn create_certificate() -> Result<(X509, PKey<Private>), ErrorStack> { let rsa = Rsa::generate(2048)?; let key = PKey::from_rsa(rsa)?; let mut x509_name = X509NameBuilder::new()?; x509_name.append_entry_by_text("O", "Agama")?; x509_name.append_entry_by_text("CN", "localhost")?; let x509_name = x509_name.build(); let mut builder = X509::builder()?; builder.set_version(2)?; let serial_number = { let mut serial = BigNum::new()?; serial.rand(159, MsbOption::MAYBE_ZERO, false)?; serial.to_asn1_integer()? }; builder.set_serial_number(&serial_number)?; builder.set_subject_name(&x509_name)?; builder.set_pubkey(&key)?; let not_before = Asn1Time::days_from_now(0)?; builder.set_not_before(¬_before)?; let not_after = Asn1Time::days_from_now(365)?; builder.set_not_after(¬_after)?; builder.append_extension(BasicConstraints::new().critical().ca().build()?)?; builder.append_extension( KeyUsage::new() .critical() .key_cert_sign() .crl_sign() .build()?, )?; builder.append_extension( SubjectAlternativeName::new() // use the default Agama host name // TODO: use the gethostname crate and use the current real hostname .dns("agama") // use the default name for the mDNS/Avahi // TODO: check which name is actually used by mDNS, to avoid // conflicts it might actually use something like agama-2.local .dns("agama.local") .build(&builder.x509v3_context(None, None))?, )?; let subject_key_identifier = SubjectKeyIdentifier::new().build(&builder.x509v3_context(None, None))?; builder.append_extension(subject_key_identifier)?; builder.sign(&key, MessageDigest::sha256())?; let cert = builder.build(); // for debugging you might dump the certificate to a file: // use std::io::Write; // let mut cert_file = std::fs::File::create("agama_cert.pem").unwrap(); // let mut key_file = std::fs::File::create("agama_key.pem").unwrap(); // cert_file.write_all(cert.to_pem().unwrap().as_ref()).unwrap(); // key_file.write_all(key.private_key_to_pem_pkcs8().unwrap().as_ref()).unwrap(); Ok((cert, key)) } 07070100000069000081A400000000000000000000000166257103000002E8000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/error.rsuse zbus_macros::DBusError; #[derive(DBusError, Debug)] #[dbus_error(prefix = "org.opensuse.Agama1.Locale")] pub enum Error { #[dbus_error(zbus_error)] ZBus(zbus::Error), Anyhow(String), } // This would be nice, but using it for a return type // results in a confusing error message about // error[E0277]: the trait bound `MyError: Serialize` is not satisfied //type MyResult<T> = Result<T, MyError>; impl From<anyhow::Error> for Error { fn from(e: anyhow::Error) -> Self { // {:#} includes causes Self::Anyhow(format!("{:#}", e)) } } impl From<Error> for zbus::fdo::Error { fn from(value: Error) -> zbus::fdo::Error { zbus::fdo::Error::Failed(format!("Localization error: {value}")) } } 0707010000006A000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/l10n0707010000006B000081A40000000000000000000000016625710300001E20000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/l10n.rspub mod helpers; mod keyboard; mod locale; mod timezone; pub mod web; use crate::error::Error; use agama_locale_data::{KeymapId, LocaleCode}; use anyhow::Context; pub use keyboard::Keymap; use keyboard::KeymapsDatabase; pub use locale::LocaleEntry; use locale::LocalesDatabase; use std::process::Command; pub use timezone::TimezoneEntry; use timezone::TimezonesDatabase; use zbus::{dbus_interface, Connection}; pub struct Locale { timezone: String, timezones_db: TimezonesDatabase, locales: Vec<String>, pub locales_db: LocalesDatabase, keymap: KeymapId, keymaps_db: KeymapsDatabase, ui_locale: LocaleCode, } #[dbus_interface(name = "org.opensuse.Agama1.Locale")] impl Locale { /// Gets the supported locales information. /// /// Each element of the list has these parts: /// /// * The locale code (e.g., "es_ES.UTF-8"). /// * The name of the language according to the language defined by the /// UILocale property. /// * The name of the territory according to the language defined by the /// UILocale property. fn list_locales(&self) -> Result<Vec<(String, String, String)>, Error> { let locales = self .locales_db .entries() .iter() .map(|l| { ( l.code.to_string(), l.language.to_string(), l.territory.to_string(), ) }) .collect::<Vec<_>>(); Ok(locales) } #[dbus_interface(property)] fn locales(&self) -> Vec<String> { self.locales.to_owned() } #[dbus_interface(property)] fn set_locales(&mut self, locales: Vec<String>) -> zbus::fdo::Result<()> { for loc in &locales { if !self.locales_db.exists(loc.as_str()) { return Err(zbus::fdo::Error::Failed(format!( "Unsupported locale value '{loc}'" ))); } } self.locales = locales; Ok(()) } #[dbus_interface(property, name = "UILocale")] fn ui_locale(&self) -> String { self.ui_locale.to_string() } #[dbus_interface(property, name = "UILocale")] fn set_ui_locale(&mut self, locale: &str) -> zbus::fdo::Result<()> { let locale: LocaleCode = locale .try_into() .map_err(|_e| zbus::fdo::Error::Failed(format!("Invalid locale value '{locale}'")))?; helpers::set_service_locale(&locale); Ok(self.translate(&locale)?) } /// Returns a list of the supported keymaps. /// /// Each element of the list contains: /// /// * The keymap identifier (e.g., "es" or "es(ast)"). /// * The name of the keyboard in language set by the UILocale property. fn list_keymaps(&self) -> Result<Vec<(String, String)>, Error> { let keymaps = self .keymaps_db .entries() .iter() .map(|k| (k.id.to_string(), k.localized_description())) .collect(); Ok(keymaps) } #[dbus_interface(property)] fn keymap(&self) -> String { self.keymap.to_string() } #[dbus_interface(property)] fn set_keymap(&mut self, keymap_id: &str) -> Result<(), zbus::fdo::Error> { let keymap_id: KeymapId = keymap_id .parse() .map_err(|_e| zbus::fdo::Error::InvalidArgs("Cannot parse keymap ID".to_string()))?; if !self.keymaps_db.exists(&keymap_id) { return Err(zbus::fdo::Error::Failed( "Cannot find this keymap".to_string(), )); } self.keymap = keymap_id; Ok(()) } /// Returns a list of the supported timezones. /// /// Each element of the list contains: /// /// * The timezone identifier (e.g., "Europe/Berlin"). /// * A list containing each part of the name in the language set by the /// UILocale property. /// * The name, in the language set by UILocale, of the main country /// associated to the timezone (typically, the name of the city that is /// part of the identifier) or empty string if there is no country. fn list_timezones(&self) -> Result<Vec<(String, Vec<String>, String)>, Error> { let timezones: Vec<_> = self .timezones_db .entries() .iter() .map(|tz| { ( tz.code.to_string(), tz.parts.clone(), tz.country.clone().unwrap_or_default(), ) }) .collect(); Ok(timezones) } #[dbus_interface(property)] fn timezone(&self) -> &str { self.timezone.as_str() } #[dbus_interface(property)] fn set_timezone(&mut self, timezone: &str) -> Result<(), zbus::fdo::Error> { let timezone = timezone.to_string(); if !self.timezones_db.exists(&timezone) { return Err(zbus::fdo::Error::Failed(format!( "Unsupported timezone value '{timezone}'" ))); } self.timezone = timezone; Ok(()) } // TODO: what should be returned value for commit? fn commit(&mut self) -> Result<(), Error> { const ROOT: &str = "/mnt"; Command::new("/usr/bin/systemd-firstboot") .args([ "--root", ROOT, "--force", "--locale", self.locales.first().context("missing locale")?.as_str(), "--keymap", &self.keymap.to_string(), "--timezone", &self.timezone, ]) .status() .context("Failed to execute systemd-firstboot")?; Ok(()) } } impl Locale { pub fn new_with_locale(ui_locale: &LocaleCode) -> Result<Self, Error> { const DEFAULT_TIMEZONE: &str = "Europe/Berlin"; let locale = ui_locale.to_string(); let mut locales_db = LocalesDatabase::new(); locales_db.read(&locale)?; let mut default_locale = ui_locale.to_string(); if !locales_db.exists(locale.as_str()) { // TODO: handle the case where the database is empty (not expected!) default_locale = locales_db.entries().first().unwrap().code.to_string(); }; let mut timezones_db = TimezonesDatabase::new(); timezones_db.read(&ui_locale.language)?; let mut default_timezone = DEFAULT_TIMEZONE.to_string(); if !timezones_db.exists(&default_timezone) { default_timezone = timezones_db.entries().first().unwrap().code.to_string(); }; let mut keymaps_db = KeymapsDatabase::new(); keymaps_db.read()?; let locale = Self { keymap: "us".parse().unwrap(), timezone: default_timezone, locales: vec![default_locale], locales_db, timezones_db, keymaps_db, ui_locale: ui_locale.clone(), }; Ok(locale) } pub fn translate(&mut self, locale: &LocaleCode) -> Result<(), Error> { self.timezones_db.read(&locale.language)?; self.locales_db.read(&locale.language)?; self.ui_locale = locale.clone(); Ok(()) } } pub async fn export_dbus_objects( connection: &Connection, locale: &LocaleCode, ) -> Result<(), Box<dyn std::error::Error>> { const PATH: &str = "/org/opensuse/Agama1/Locale"; // When serving, request the service name _after_ exposing the main object let locale_iface = Locale::new_with_locale(locale)?; connection.object_server().at(PATH, locale_iface).await?; Ok(()) } 0707010000006C000081A4000000000000000000000001662571030000037E000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/l10n/helpers.rs//! Helpers functions //! //! FIXME: find a better place for the localization function use agama_locale_data::LocaleCode; use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory}; use std::env; /// Initializes the service locale. /// /// It returns the used locale. Defaults to `en_US.UTF-8`. pub fn init_locale() -> Result<LocaleCode, Box<dyn std::error::Error>> { let lang = env::var("LANG").unwrap_or("en_US.UTF-8".to_string()); let locale: LocaleCode = lang.as_str().try_into().unwrap_or_default(); set_service_locale(&locale); textdomain("xkeyboard-config")?; bind_textdomain_codeset("xkeyboard-config", "UTF-8")?; Ok(locale) } /// Sets the service locale. /// pub fn set_service_locale(locale: &LocaleCode) { if setlocale(LocaleCategory::LcAll, locale.to_string()).is_none() { log::warn!("Could not set the locale"); } } 0707010000006D000081A40000000000000000000000016625710300000AED000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/l10n/keyboard.rsuse agama_locale_data::{get_localectl_keymaps, keyboard::XkbConfigRegistry, KeymapId}; use gettextrs::*; use serde::Serialize; use std::collections::HashMap; // Minimal representation of a keymap #[derive(Clone, Debug, Serialize, utoipa::ToSchema)] pub struct Keymap { /// Keymap identifier (e.g., "us") pub id: KeymapId, /// Keymap description description: String, } impl Keymap { pub fn new(id: KeymapId, description: &str) -> Self { Self { id, description: description.to_string(), } } pub fn localized_description(&self) -> String { gettext(&self.description) } } /// Represents the keymaps database. /// /// The list of supported keymaps is read from `systemd-localed` and the /// descriptions from the X Keyboard Configuraiton Database (see /// `agama_locale_data::XkbConfigRegistry`). #[derive(Default)] pub struct KeymapsDatabase { keymaps: Vec<Keymap>, } impl KeymapsDatabase { pub fn new() -> Self { Self::default() } /// Reads the list of keymaps. pub fn read(&mut self) -> anyhow::Result<()> { self.keymaps = get_keymaps()?; Ok(()) } pub fn exists(&self, id: &KeymapId) -> bool { self.keymaps.iter().any(|k| &k.id == id) } /// Returns the list of keymaps. pub fn entries(&self) -> &Vec<Keymap> { &self.keymaps } } /// Returns the list of keymaps to offer. /// /// It only includes the keyboards supported by `localectl` but getting /// the description from the X Keyboard Configuration Database. fn get_keymaps() -> anyhow::Result<Vec<Keymap>> { let mut keymaps: Vec<Keymap> = vec![]; let xkb_descriptions = get_keymap_descriptions(); let keymap_ids = get_localectl_keymaps()?; for keymap_id in keymap_ids { let keymap_id_str = keymap_id.to_string(); if let Some(description) = xkb_descriptions.get(&keymap_id_str) { keymaps.push(Keymap::new(keymap_id, description)); } else { log::debug!("Keyboard '{}' not found in xkb database", keymap_id_str); } } Ok(keymaps) } /// Returns a map of keymaps ids and its descriptions from the X Keyboard /// Configuration Database. fn get_keymap_descriptions() -> HashMap<String, String> { let layouts = XkbConfigRegistry::from_system().unwrap(); let mut keymaps = HashMap::new(); for layout in layouts.layout_list.layouts { let name = layout.config_item.name; keymaps.insert(name.to_string(), layout.config_item.description.to_string()); for variant in layout.variants_list.variants { let id = format!("{}({})", &name, &variant.config_item.name); keymaps.insert(id, variant.config_item.description); } } keymaps } 0707010000006E000081A4000000000000000000000001662571030000117D000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/l10n/locale.rs//! This module provides support for reading the locales database. use crate::error::Error; use agama_locale_data::{InvalidLocaleCode, LocaleCode}; use anyhow::Context; use serde::Serialize; use serde_with::{serde_as, DisplayFromStr}; use std::process::Command; /// Represents a locale, including the localized language and territory. #[serde_as] #[derive(Debug, Serialize, Clone, utoipa::ToSchema)] pub struct LocaleEntry { /// The locale code (e.g., "es_ES.UTF-8"). #[serde_as(as = "DisplayFromStr")] pub code: LocaleCode, /// Localized language name (e.g., "Spanish", "Español", etc.) pub language: String, /// Localized territory name (e.g., "Spain", "España", etc.) pub territory: String, } /// Represents the locales database. /// /// The list of supported locales is read from `systemd-localed`. However, the /// translations are obtained from the `agama_locale_data` crate. #[derive(Default)] pub struct LocalesDatabase { known_locales: Vec<LocaleCode>, locales: Vec<LocaleEntry>, } impl LocalesDatabase { pub fn new() -> Self { Self::default() } /// Loads the list of locales. /// /// * `ui_language`: language to translate the descriptions (e.g., "en"). pub fn read(&mut self, ui_language: &str) -> Result<(), Error> { let result = Command::new("localectl") .args(["list-locales"]) .output() .context("Failed to get the list of locales")?; let output = String::from_utf8(result.stdout).context("Invalid UTF-8 sequence from list-locales")?; self.known_locales = output .lines() .filter_map(|line| TryInto::<LocaleCode>::try_into(line).ok()) .collect(); self.locales = self.get_locales(ui_language)?; Ok(()) } /// Determines whether a locale exists in the database. pub fn exists<T>(&self, locale: T) -> bool where T: TryInto<LocaleCode>, T::Error: Into<InvalidLocaleCode>, { if let Ok(locale) = TryInto::<LocaleCode>::try_into(locale) { return self.known_locales.contains(&locale); } false } /// Returns the list of locales. pub fn entries(&self) -> &Vec<LocaleEntry> { &self.locales } /// Gets the supported locales information. /// /// * `ui_language`: language to use in the translations. fn get_locales(&self, ui_language: &str) -> Result<Vec<LocaleEntry>, Error> { const DEFAULT_LANG: &str = "en"; let mut result = Vec::with_capacity(self.known_locales.len()); let languages = agama_locale_data::get_languages()?; let territories = agama_locale_data::get_territories()?; for code in self.known_locales.as_slice() { let language = languages .find_by_id(&code.language) .context("language not found")?; let names = &language.names; let language_label = names .name_for(ui_language) .or_else(|| names.name_for(DEFAULT_LANG)) .unwrap_or(language.id.to_string()); let territory = territories .find_by_id(&code.territory) .context("territory not found")?; let names = &territory.names; let territory_label = names .name_for(ui_language) .or_else(|| names.name_for(DEFAULT_LANG)) .unwrap_or(territory.id.to_string()); let entry = LocaleEntry { code: code.clone(), language: language_label, territory: territory_label, }; result.push(entry) } Ok(result) } } #[cfg(test)] mod tests { use super::LocalesDatabase; use agama_locale_data::LocaleCode; #[test] fn test_read_locales() { let mut db = LocalesDatabase::new(); db.read("de").unwrap(); let found_locales = db.entries(); let spanish: LocaleCode = "es_ES".try_into().unwrap(); let found = found_locales.iter().find(|l| l.code == spanish).unwrap(); assert_eq!(&found.language, "Spanisch"); assert_eq!(&found.territory, "Spanien"); } #[test] fn test_locale_exists() { let mut db = LocalesDatabase::new(); db.read("en").unwrap(); assert!(db.exists("en_US")); assert!(!db.exists("unknown_UNKNOWN")); } } 0707010000006F000081A4000000000000000000000001662571030000135C000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/l10n/timezone.rs//! This module provides support for reading the timezones database. use crate::error::Error; use agama_locale_data::territory::Territories; use agama_locale_data::timezone_part::TimezoneIdParts; use serde::Serialize; use std::collections::HashMap; /// Represents a timezone, including each part as localized. #[derive(Clone, Debug, Serialize, utoipa::ToSchema)] pub struct TimezoneEntry { /// Timezone identifier (e.g. "Atlantic/Canary"). pub code: String, /// Localized parts (e.g., "Atlántico", "Canarias"). pub parts: Vec<String>, /// Localized name of the territory this timezone is associated to pub country: Option<String>, } #[derive(Default)] pub struct TimezonesDatabase { timezones: Vec<TimezoneEntry>, } impl TimezonesDatabase { pub fn new() -> Self { Self::default() } /// Initializes the list of known timezones. /// /// * `ui_language`: language to translate the descriptions (e.g., "en"). pub fn read(&mut self, ui_language: &str) -> Result<(), Error> { self.timezones = self.get_timezones(ui_language)?; Ok(()) } /// Determines whether a timezone exists in the database. pub fn exists(&self, timezone: &String) -> bool { self.timezones.iter().any(|t| &t.code == timezone) } /// Returns the list of timezones. pub fn entries(&self) -> &Vec<TimezoneEntry> { &self.timezones } /// Returns a list of the supported timezones. /// /// Each element of the list contains a timezone identifier and a vector /// containing the translation of each part of the language. /// /// * `ui_language`: language to translate the descriptions (e.g., "en"). fn get_timezones(&self, ui_language: &str) -> Result<Vec<TimezoneEntry>, Error> { let timezones = agama_locale_data::get_timezones(); let tz_parts = agama_locale_data::get_timezone_parts()?; let territories = agama_locale_data::get_territories()?; let tz_countries = agama_locale_data::get_timezone_countries()?; const COUNTRYLESS: [&str; 2] = ["UTC", "Antarctica/South_Pole"]; let ret = timezones .into_iter() .filter_map(|tz| { let parts = translate_parts(&tz, ui_language, &tz_parts); let country = translate_country(&tz, ui_language, &tz_countries, &territories); match country { None if !COUNTRYLESS.contains(&tz.as_str()) => None, _ => Some(TimezoneEntry { code: tz, parts, country, }), } }) .collect(); Ok(ret) } } fn translate_parts(timezone: &str, ui_language: &str, tz_parts: &TimezoneIdParts) -> Vec<String> { timezone .split('/') .map(|part| { tz_parts .localize_part(part, ui_language) .unwrap_or(part.to_owned()) }) .collect() } fn translate_country( timezone: &str, lang: &str, countries: &HashMap<String, String>, territories: &Territories, ) -> Option<String> { let tz = match timezone { "Asia/Rangoon" => "Asia/Yangon", "Europe/Kiev" => "Europe/Kyiv", _ => timezone, }; let country_id = countries.get(tz)?; let territory = territories.find_by_id(country_id)?; let name = territory.names.name_for(lang)?; Some(name) } #[cfg(test)] mod tests { use super::TimezonesDatabase; #[test] fn test_read_timezones() { let mut db = TimezonesDatabase::new(); db.read("es").unwrap(); let found_timezones = db.entries(); let found = found_timezones .iter() .find(|tz| tz.code == "Europe/Berlin") .unwrap(); assert_eq!(&found.code, "Europe/Berlin"); assert_eq!( found.parts, vec!["Europa".to_string(), "Berlín".to_string()] ); assert_eq!(found.country, Some("Alemania".to_string())); } #[test] fn test_read_timezone_without_country() { let mut db = TimezonesDatabase::new(); db.read("es").unwrap(); let timezone = db.entries().iter().find(|tz| tz.code == "UTC").unwrap(); assert_eq!(timezone.country, None); } #[test] fn test_read_kiev_country() { let mut db = TimezonesDatabase::new(); db.read("en").unwrap(); let timezone = db .entries() .iter() .find(|tz| tz.code == "Europe/Kiev") .unwrap(); assert_eq!(timezone.country, Some("Ukraine".to_string())); } #[test] fn test_timezone_exists() { let mut db = TimezonesDatabase::new(); db.read("es").unwrap(); assert!(db.exists(&"Atlantic/Canary".to_string())); assert!(!db.exists(&"Unknown/Unknown".to_string())); } } 07070100000070000081A40000000000000000000000016625710300001951000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/l10n/web.rs//! This module implements the web API for the localization module. use super::{keyboard::Keymap, locale::LocaleEntry, timezone::TimezoneEntry, Locale}; use crate::{ error::Error, l10n::helpers, web::{Event, EventsSender}, }; use agama_locale_data::{InvalidKeymap, LocaleCode}; use axum::{ extract::State, http::StatusCode, response::{IntoResponse, Response}, routing::{get, put}, Json, Router, }; use serde::{Deserialize, Serialize}; use serde_json::json; use std::sync::{Arc, RwLock}; use thiserror::Error; #[derive(Error, Debug)] pub enum LocaleError { #[error("Unknown locale code: {0}")] UnknownLocale(String), #[error("Unknown timezone: {0}")] UnknownTimezone(String), #[error("Invalid keymap: {0}")] InvalidKeymap(#[from] InvalidKeymap), #[error("Cannot translate: {0}")] OtherError(#[from] Error), } impl IntoResponse for LocaleError { fn into_response(self) -> Response { let body = json!({ "error": self.to_string() }); (StatusCode::BAD_REQUEST, Json(body)).into_response() } } #[derive(Clone)] struct LocaleState { locale: Arc<RwLock<Locale>>, events: EventsSender, } /// Sets up and returns the axum service for the localization module. /// /// * `events`: channel to send the events to the main service. pub fn l10n_service(events: EventsSender) -> Router { let code = LocaleCode::default(); let locale = Locale::new_with_locale(&code).unwrap(); let state = LocaleState { locale: Arc::new(RwLock::new(locale)), events, }; Router::new() .route("/keymaps", get(keymaps)) .route("/locales", get(locales)) .route("/timezones", get(timezones)) .route("/config", put(set_config).get(get_config)) .with_state(state) } #[utoipa::path(get, path = "/l10n/locales", responses( (status = 200, description = "List of known locales", body = Vec<LocaleEntry>) ))] async fn locales(State(state): State<LocaleState>) -> Json<Vec<LocaleEntry>> { let data = state.locale.read().unwrap(); let locales = data.locales_db.entries().to_vec(); Json(locales) } #[derive(Serialize, Deserialize, utoipa::ToSchema)] pub struct LocaleConfig { /// Locales to install in the target system locales: Option<Vec<String>>, /// Keymap for the target system keymap: Option<String>, /// Timezone for the target system timezone: Option<String>, /// User-interface locale. It is actually not related to the `locales` property. ui_locale: Option<String>, } #[utoipa::path(get, path = "/l10n/timezones", responses( (status = 200, description = "List of known timezones") ))] async fn timezones(State(state): State<LocaleState>) -> Json<Vec<TimezoneEntry>> { let data = state.locale.read().unwrap(); let timezones = data.timezones_db.entries().to_vec(); Json(timezones) } #[utoipa::path(get, path = "/l10n/keymaps", responses( (status = 200, description = "List of known keymaps", body = Vec<Keymap>) ))] async fn keymaps(State(state): State<LocaleState>) -> Json<Vec<Keymap>> { let data = state.locale.read().unwrap(); let keymaps = data.keymaps_db.entries().to_vec(); Json(keymaps) } #[utoipa::path(put, path = "/l10n/config", responses( (status = 200, description = "Set the locale configuration", body = LocaleConfig) ))] async fn set_config( State(state): State<LocaleState>, Json(value): Json<LocaleConfig>, ) -> Result<Json<()>, LocaleError> { let mut data = state.locale.write().unwrap(); if let Some(locales) = &value.locales { for loc in locales { if !data.locales_db.exists(loc.as_str()) { return Err(LocaleError::UnknownLocale(loc.to_string())); } } data.locales = locales.clone(); } if let Some(timezone) = &value.timezone { if !data.timezones_db.exists(timezone) { return Err(LocaleError::UnknownTimezone(timezone.to_string())); } data.timezone = timezone.to_owned(); } if let Some(keymap_id) = &value.keymap { data.keymap = keymap_id.parse()?; } if let Some(ui_locale) = &value.ui_locale { let locale: LocaleCode = ui_locale .as_str() .try_into() .map_err(|_e| LocaleError::UnknownLocale(ui_locale.to_string()))?; helpers::set_service_locale(&locale); data.translate(&locale)?; _ = state.events.send(Event::LocaleChanged { locale: locale.to_string(), }); } Ok(Json(())) } #[utoipa::path(get, path = "/l10n/config", responses( (status = 200, description = "Localization configuration", body = LocaleConfig) ))] async fn get_config(State(state): State<LocaleState>) -> Json<LocaleConfig> { let data = state.locale.read().unwrap(); Json(LocaleConfig { locales: Some(data.locales.clone()), keymap: Some(data.keymap()), timezone: Some(data.timezone().to_string()), ui_locale: Some(data.ui_locale().to_string()), }) } #[cfg(test)] mod tests { use crate::l10n::{web::LocaleState, Locale}; use agama_locale_data::{KeymapId, LocaleCode}; use std::sync::{Arc, RwLock}; use tokio::{sync::broadcast::channel, test}; fn build_state() -> LocaleState { let (tx, _) = channel(16); let default_code = LocaleCode::default(); let locale = Locale::new_with_locale(&default_code).unwrap(); LocaleState { locale: Arc::new(RwLock::new(locale)), events: tx, } } #[test] async fn test_locales() { let state = build_state(); let response = super::locales(axum::extract::State(state)).await; let default = LocaleCode::default(); let found = response.iter().find(|l| l.code == default); assert!(found.is_some()); } #[test] async fn test_keymaps() { let state = build_state(); let response = super::keymaps(axum::extract::State(state)).await; let english: KeymapId = "us".parse().unwrap(); let found = response.iter().find(|k| k.id == english); assert!(found.is_some()); } #[test] async fn test_timezones() { let state = build_state(); let response = super::timezones(axum::extract::State(state)).await; let found = response.iter().find(|t| t.code == "Atlantic/Canary"); assert!(found.is_some()); } } 07070100000071000081A40000000000000000000000016625710300000084000000000000000000000000000000000000004700000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/lib.rspub mod cert; pub mod error; pub mod l10n; pub mod network; pub mod questions; pub mod software; pub mod web; pub use web::service; 07070100000072000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network07070100000073000081A40000000000000000000000016625710300000A4B000000000000000000000000000000000000004B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network.rs//! Network configuration service for Agama //! //! This library implements the network configuration service for Agama. //! //! ## Connections and devices //! //! The library is built around the concepts of network devices and connections, akin to //! NetworkManager approach. //! //! Each network device is exposed as a D-Bus object using a path like //! `/org/opensuse/Agama1/Network/devices/[0-9]+`. At this point, those objects expose a bit of //! information about network devices. The entry point for the devices is the //! `/org/opensuse/Agama1/Network/devices` object, that expose a `GetDevices` method that returns //! the paths for the devices objects. //! //! The network configuration is exposed through the connections objects as //! `/org/opensuse/Agama1/Network/connections/[0-9]+`. Those objects are composed of several //! D-Bus interfaces depending on its type: //! //! * `org.opensuse.Agama1.Network.Connection` exposes common information across all connection //! types. //! * `org.opensuse.Agama1.Network.Connection.IPv4` includes IPv4 settings, like the configuration method //! (DHCP, manual, etc.), IP addresses, name servers and so on. //! * `org.opensuse.Agama1.Network.Connection.Wireless` exposes the configuration for wireless //! connections. //! //! Analogous to the devices API, there is a special `/org/opensuse/Agama1/Network/connections` //! object that implements a few methods that are related to the collection of connections like //! `GetConnections`, `AddConnection` and `RemoveConnection`. Additionally, it implements an //! `Apply` method to write the changes to the NetworkManager service. //! //! ## Limitations //! //! We expect to address the following problems as we evolve the API, but it is noteworthy to have //! them in mind: //! //! * The devices list does not reflect the changes in the system. For instance, it is not updated //! when a device is connected to the system. //! * Many configuration types are still missing (bridges, bonding, etc.). mod action; mod adapter; pub mod dbus; pub mod error; pub mod model; mod nm; pub mod system; pub use action::Action; pub use adapter::{Adapter, NetworkAdapterError}; pub use dbus::NetworkService; pub use model::NetworkState; pub use nm::NetworkManagerAdapter; pub use system::NetworkSystem; use zbus::Connection; pub async fn export_dbus_objects( connection: &Connection, ) -> Result<(), Box<dyn std::error::Error>> { let adapter = NetworkManagerAdapter::from_system() .await .expect("Could not connect to NetworkManager to read the configuration."); NetworkService::start(connection, adapter).await } 07070100000074000081A40000000000000000000000016625710300000738000000000000000000000000000000000000005200000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/action.rsuse crate::network::model::Connection; use agama_lib::network::types::DeviceType; use tokio::sync::oneshot; use uuid::Uuid; use zbus::zvariant::OwnedObjectPath; use super::{error::NetworkStateError, NetworkAdapterError}; pub type Responder<T> = oneshot::Sender<T>; pub type ControllerConnection = (Connection, Vec<String>); /// Networking actions, like adding, updating or removing connections. /// /// These actions are meant to be processed by [crate::network::system::NetworkSystem], updating the model /// and the D-Bus tree as needed. #[derive(Debug)] pub enum Action { /// Add a new connection with the given name and type. AddConnection( String, DeviceType, Responder<Result<OwnedObjectPath, NetworkStateError>>, ), /// Gets a connection GetConnection(Uuid, Responder<Option<Connection>>), /// Gets a connection GetConnectionPath(Uuid, Responder<Option<OwnedObjectPath>>), /// Gets a connection GetConnectionPathById(String, Responder<Option<OwnedObjectPath>>), /// Get connections paths GetConnectionsPaths(Responder<Vec<OwnedObjectPath>>), /// Gets a controller connection GetController( Uuid, Responder<Result<ControllerConnection, NetworkStateError>>, ), /// Get devices paths GetDevicesPaths(Responder<Vec<OwnedObjectPath>>), /// Sets a controller's ports. It uses the Uuid of the controller and the IDs or interface names /// of the ports. SetPorts( Uuid, Box<Vec<String>>, Responder<Result<(), NetworkStateError>>, ), /// Update a connection (replacing the old one). UpdateConnection(Box<Connection>), /// Remove the connection with the given Uuid. RemoveConnection(Uuid), /// Apply the current configuration. Apply(Responder<Result<(), NetworkAdapterError>>), } 07070100000075000081A400000000000000000000000166257103000003AC000000000000000000000000000000000000005300000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/adapter.rsuse crate::network::NetworkState; use agama_lib::error::ServiceError; use async_trait::async_trait; use thiserror::Error; #[derive(Error, Debug)] pub enum NetworkAdapterError { #[error("Could not read the network configuration: {0}")] Read(ServiceError), #[error("Could not update the network configuration: {0}")] Write(ServiceError), #[error("Checkpoint handling error: {0}")] Checkpoint(ServiceError), // only relevant for adapters that implement a checkpoint mechanism } /// A trait for the ability to read/write from/to a network service #[async_trait] pub trait Adapter { async fn read(&self) -> Result<NetworkState, NetworkAdapterError>; async fn write(&self, network: &NetworkState) -> Result<(), NetworkAdapterError>; } impl From<NetworkAdapterError> for zbus::fdo::Error { fn from(value: NetworkAdapterError) -> zbus::fdo::Error { zbus::fdo::Error::Failed(value.to_string()) } } 07070100000076000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus07070100000077000081A40000000000000000000000016625710300000108000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus.rs//! D-Bus service and interfaces. //! //! This module contains a [D-Bus network service](NetworkService) which expose the network //! configuration for Agama. mod interfaces; pub mod service; mod tree; pub use service::NetworkService; pub(crate) use tree::Tree; 07070100000078000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus/interfaces07070100000079000081A40000000000000000000000016625710300000191000000000000000000000000000000000000005B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus/interfaces.rs//! Network D-Bus interfaces. //! //! This module contains the set of D-Bus interfaces that are exposed by [D-Bus network //! service](crate::NetworkService). mod common; mod connection_configs; mod connections; mod devices; mod ip_config; pub use connection_configs::{Bond, Wireless}; pub use connections::{Connection, Connections, Match}; pub use devices::{Device, Devices}; pub use ip_config::Ip; 0707010000007A000081A40000000000000000000000016625710300000AED000000000000000000000000000000000000006200000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus/interfaces/common.rs//! Traits to build network D-Bus interfaces. //! //! There are a set of operations that are shared by many D-Bus interfaces (retrieving or updating a connection, a configuration etc.). //! The traits in this module implements the common pieces to make it easier to build new //! interfaces and reduce code duplication. //! //! Note: it is not clear to us whether using traits or simple structs is better for this use case. //! We could change the approach in the future. use crate::network::{ error::NetworkStateError, model::{Connection as NetworkConnection, ConnectionConfig}, Action, }; use async_trait::async_trait; use tokio::sync::{mpsc::UnboundedSender, oneshot, MutexGuard}; use uuid::Uuid; #[async_trait] pub trait ConnectionInterface { fn uuid(&self) -> Uuid; async fn actions(&self) -> MutexGuard<UnboundedSender<Action>>; async fn get_connection(&self) -> Result<NetworkConnection, NetworkStateError> { let actions = self.actions().await; let (tx, rx) = oneshot::channel(); actions .send(Action::GetConnection(self.uuid(), tx)) .unwrap(); rx.await .unwrap() .ok_or(NetworkStateError::UnknownConnection( self.uuid().to_string(), )) } /// Updates the connection data in the NetworkSystem. /// /// * `func`: function to update the connection. async fn update_connection<F>(&self, func: F) -> Result<(), NetworkStateError> where F: FnOnce(&mut NetworkConnection) + std::marker::Send, { let mut connection = self.get_connection().await?; func(&mut connection); let actions = self.actions().await; actions .send(Action::UpdateConnection(Box::new(connection))) .unwrap(); Ok(()) } } #[async_trait] pub trait ConnectionConfigInterface: ConnectionInterface { async fn get_config<T>(&self) -> Result<T, NetworkStateError> where T: TryFrom<ConnectionConfig, Error = NetworkStateError>, { let connection = self.get_connection().await?; connection.config.try_into() } async fn update_config<T, F>(&self, func: F) -> Result<(), NetworkStateError> where F: FnOnce(&mut T) + std::marker::Send, T: Into<ConnectionConfig> + TryFrom<ConnectionConfig, Error = NetworkStateError> + std::marker::Send, { let mut connection = self.get_connection().await?; let mut config: T = connection.config.clone().try_into()?; func(&mut config); connection.config = config.into(); let actions = self.actions().await; actions .send(Action::UpdateConnection(Box::new(connection))) .unwrap(); Ok(()) } } 0707010000007B000081A40000000000000000000000016625710300001B1F000000000000000000000000000000000000006E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus/interfaces/connection_configs.rsuse agama_lib::network::types::SSID; use async_trait::async_trait; use std::sync::Arc; use tokio::sync::{mpsc::UnboundedSender, oneshot, Mutex, MutexGuard}; use uuid::Uuid; use zbus::dbus_interface; use crate::network::{ action::Action, error::NetworkStateError, model::{BondConfig, SecurityProtocol, WirelessConfig, WirelessMode}, }; use super::common::{ConnectionConfigInterface, ConnectionInterface}; /// D-Bus interface for Bond settings. pub struct Bond { actions: Arc<Mutex<UnboundedSender<Action>>>, uuid: Uuid, } impl Bond { /// Creates a Bond interface object. /// /// * `actions`: sending-half of a channel to send actions. /// * `uuid`: connection UUID. pub fn new(actions: UnboundedSender<Action>, uuid: Uuid) -> Self { Self { actions: Arc::new(Mutex::new(actions)), uuid, } } } #[dbus_interface(name = "org.opensuse.Agama1.Network.Connection.Bond")] impl Bond { /// Bonding mode. #[dbus_interface(property)] pub async fn mode(&self) -> zbus::fdo::Result<String> { let config = self.get_config::<BondConfig>().await?; Ok(config.mode.to_string()) } #[dbus_interface(property)] pub async fn set_mode(&mut self, mode: &str) -> zbus::fdo::Result<()> { let mode = mode.try_into()?; self.update_config::<BondConfig, _>(|c| c.mode = mode) .await?; Ok(()) } /// List of bonding options. #[dbus_interface(property)] pub async fn options(&self) -> zbus::fdo::Result<String> { let config = self.get_config::<BondConfig>().await?; Ok(config.options.to_string()) } #[dbus_interface(property)] pub async fn set_options(&mut self, opts: &str) -> zbus::fdo::Result<()> { let opts = opts.try_into()?; self.update_config::<BondConfig, _>(|c| c.options = opts) .await?; Ok(()) } /// List of bond ports. /// /// For the port names, it uses the interface name (preferred) or, as a fallback, /// the connection ID of the port. #[dbus_interface(property)] pub async fn ports(&self) -> zbus::fdo::Result<Vec<String>> { let actions = self.actions.lock().await; let (tx, rx) = oneshot::channel(); actions.send(Action::GetController(self.uuid, tx)).unwrap(); let (_, ports) = rx.await.unwrap()?; Ok(ports) } #[dbus_interface(property)] pub async fn set_ports(&mut self, ports: Vec<String>) -> zbus::fdo::Result<()> { let actions = self.actions.lock().await; let (tx, rx) = oneshot::channel(); actions .send(Action::SetPorts(self.uuid, Box::new(ports), tx)) .unwrap(); let result = rx.await.unwrap(); Ok(result?) } } #[async_trait] impl ConnectionInterface for Bond { fn uuid(&self) -> Uuid { self.uuid } async fn actions(&self) -> MutexGuard<UnboundedSender<Action>> { self.actions.lock().await } } impl ConnectionConfigInterface for Bond {} /// D-Bus interface for wireless settings pub struct Wireless { actions: Arc<Mutex<UnboundedSender<Action>>>, uuid: Uuid, } impl Wireless { /// Creates a Wireless interface object. /// /// * `actions`: sending-half of a channel to send actions. /// * `uuid`: connection UUID. pub fn new(actions: UnboundedSender<Action>, uuid: Uuid) -> Self { Self { actions: Arc::new(Mutex::new(actions)), uuid, } } } #[dbus_interface(name = "org.opensuse.Agama1.Network.Connection.Wireless")] impl Wireless { /// Network SSID. #[dbus_interface(property, name = "SSID")] pub async fn ssid(&self) -> zbus::fdo::Result<Vec<u8>> { let config = self.get_config::<WirelessConfig>().await?; Ok(config.ssid.into()) } #[dbus_interface(property, name = "SSID")] pub async fn set_ssid(&mut self, ssid: Vec<u8>) -> zbus::fdo::Result<()> { self.update_config::<WirelessConfig, _>(|c| c.ssid = SSID(ssid)) .await?; Ok(()) } /// Wireless connection mode. /// /// Possible values: "unknown", "adhoc", "infrastructure", "ap" or "mesh". /// /// See [crate::network::model::WirelessMode]. #[dbus_interface(property)] pub async fn mode(&self) -> zbus::fdo::Result<String> { let config = self.get_config::<WirelessConfig>().await?; Ok(config.mode.to_string()) } #[dbus_interface(property)] pub async fn set_mode(&mut self, mode: &str) -> zbus::fdo::Result<()> { let mode: WirelessMode = mode.try_into()?; self.update_config::<WirelessConfig, _>(|c| c.mode = mode) .await?; Ok(()) } /// Password to connect to the wireless network. #[dbus_interface(property)] pub async fn password(&self) -> zbus::fdo::Result<String> { let config = self.get_config::<WirelessConfig>().await?; Ok(config.password.unwrap_or_default()) } #[dbus_interface(property)] pub async fn set_password(&mut self, password: String) -> zbus::fdo::Result<()> { self.update_config::<WirelessConfig, _>(|c| { c.password = if password.is_empty() { None } else { Some(password) }; }) .await?; Ok(()) } /// Wireless security protocol. /// /// Possible values: "none", "owe", "ieee8021x", "wpa-psk", "sae", "wpa-eap", /// "wpa-eap-suite-b192". /// /// See [crate::network::model::SecurityProtocol]. #[dbus_interface(property)] pub async fn security(&self) -> zbus::fdo::Result<String> { let config = self.get_config::<WirelessConfig>().await?; Ok(config.security.to_string()) } #[dbus_interface(property)] pub async fn set_security(&mut self, security: &str) -> zbus::fdo::Result<()> { let security: SecurityProtocol = security .try_into() .map_err(|_| NetworkStateError::InvalidSecurityProtocol(security.to_string()))?; self.update_config::<WirelessConfig, _>(|c| c.security = security) .await?; Ok(()) } /// Whether the network is hidden or not. #[dbus_interface(property)] pub async fn hidden(&self) -> zbus::fdo::Result<bool> { let config = self.get_config::<WirelessConfig>().await?; Ok(config.hidden) } #[dbus_interface(property)] pub async fn set_hidden(&mut self, hidden: bool) -> zbus::fdo::Result<()> { self.update_config::<WirelessConfig, _>(|c| c.hidden = hidden) .await?; Ok(()) } } #[async_trait] impl ConnectionInterface for Wireless { fn uuid(&self) -> Uuid { self.uuid } async fn actions(&self) -> MutexGuard<UnboundedSender<Action>> { self.actions.lock().await } } impl ConnectionConfigInterface for Wireless {} 0707010000007C000081A400000000000000000000000166257103000028A3000000000000000000000000000000000000006700000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus/interfaces/connections.rsuse async_trait::async_trait; use std::{str::FromStr, sync::Arc}; use tokio::sync::{mpsc::UnboundedSender, oneshot, Mutex, MutexGuard}; use uuid::Uuid; use zbus::{ dbus_interface, zvariant::{ObjectPath, OwnedObjectPath}, SignalContext, }; use super::common::ConnectionInterface; use crate::network::{error::NetworkStateError, model::MacAddress, Action}; /// D-Bus interface for the set of connections. /// /// It offers an API to query the connections collection. pub struct Connections { actions: Arc<Mutex<UnboundedSender<Action>>>, } impl Connections { /// Creates a Connections interface object. /// /// * `objects`: Objects paths registry. pub fn new(actions: UnboundedSender<Action>) -> Self { Self { actions: Arc::new(Mutex::new(actions)), } } } #[dbus_interface(name = "org.opensuse.Agama1.Network.Connections")] impl Connections { /// Returns the D-Bus paths of the network connections. pub async fn get_connections(&self) -> zbus::fdo::Result<Vec<OwnedObjectPath>> { let actions = self.actions.lock().await; let (tx, rx) = oneshot::channel(); actions.send(Action::GetConnectionsPaths(tx)).unwrap(); let result = rx.await.unwrap(); Ok(result) } /// Adds a new network connection. /// /// * `id`: connection name. /// * `ty`: connection type (see [agama_lib::network::types::DeviceType]). pub async fn add_connection( &mut self, id: String, ty: u8, #[zbus(signal_context)] ctxt: SignalContext<'_>, ) -> zbus::fdo::Result<OwnedObjectPath> { let actions = self.actions.lock().await; let (tx, rx) = oneshot::channel(); actions .send(Action::AddConnection(id.clone(), ty.try_into()?, tx)) .unwrap(); let path = rx.await.unwrap()?; Self::connection_added(&ctxt, &id, &path).await?; Ok(path) } /// Returns the D-Bus path of the network connection by its UUID. /// /// * `uuid`: connection UUID. pub async fn get_connection(&self, uuid: &str) -> zbus::fdo::Result<OwnedObjectPath> { let uuid: Uuid = uuid .parse() .map_err(|_| NetworkStateError::InvalidUuid(uuid.to_string()))?; let actions = self.actions.lock().await; let (tx, rx) = oneshot::channel(); actions.send(Action::GetConnectionPath(uuid, tx)).unwrap(); let path = rx .await .unwrap() .ok_or(NetworkStateError::UnknownConnection(uuid.to_string()))?; Ok(path) } /// Returns the D-Bus path of the network connection by its ID. /// /// * `id`: connection ID. pub async fn get_connection_by_id(&self, id: &str) -> zbus::fdo::Result<OwnedObjectPath> { let actions = self.actions.lock().await; let (tx, rx) = oneshot::channel(); actions .send(Action::GetConnectionPathById(id.to_string(), tx)) .unwrap(); let path = rx .await .unwrap() .ok_or(NetworkStateError::UnknownConnection(id.to_string()))?; Ok(path) } /// Removes a network connection. /// /// * `uuid`: connection UUID.. pub async fn remove_connection(&mut self, uuid: &str) -> zbus::fdo::Result<()> { let uuid = uuid .parse() .map_err(|_| NetworkStateError::InvalidUuid(uuid.to_string()))?; let actions = self.actions.lock().await; actions.send(Action::RemoveConnection(uuid)).unwrap(); Ok(()) } /// Applies the network configuration. /// /// It includes adding, updating and removing connections as needed. pub async fn apply(&self) -> zbus::fdo::Result<()> { let actions = self.actions.lock().await; let (tx, rx) = oneshot::channel(); actions.send(Action::Apply(tx)).unwrap(); rx.await.unwrap()?; Ok(()) } /// Notifies than a new interface has been added. #[dbus_interface(signal)] pub async fn connection_added( ctxt: &SignalContext<'_>, id: &str, path: &ObjectPath<'_>, ) -> zbus::Result<()>; } /// D-Bus interface for a network connection /// /// It offers an API to query a connection. pub struct Connection { actions: Arc<Mutex<UnboundedSender<Action>>>, uuid: Uuid, } impl Connection { /// Creates a Connection interface object. /// /// * `actions`: sending-half of a channel to send actions. /// * `uuid`: network connection's UUID. pub fn new(actions: UnboundedSender<Action>, uuid: Uuid) -> Self { Self { actions: Arc::new(Mutex::new(actions)), uuid, } } } #[dbus_interface(name = "org.opensuse.Agama1.Network.Connection")] impl Connection { /// Connection ID. /// /// Unique identifier of the network connection. It may or not be the same that the used by the /// backend. For instance, when using NetworkManager (which is the only supported backend by /// now), it uses the original ID but appending a number in case the ID is duplicated. #[dbus_interface(property)] pub async fn id(&self) -> zbus::fdo::Result<String> { let connection = self.get_connection().await?; Ok(connection.id) } /// Connection UUID. /// /// Unique identifier of the network connection. It may or not be the same that the used by the /// backend. #[dbus_interface(property)] pub async fn uuid(&self) -> String { self.uuid.to_string() } #[dbus_interface(property)] pub async fn controller(&self) -> zbus::fdo::Result<String> { let connection = self.get_connection().await?; let result = match connection.controller { Some(uuid) => uuid.to_string(), None => "".to_string(), }; Ok(result) } #[dbus_interface(property)] pub async fn interface(&self) -> zbus::fdo::Result<String> { let connection = self.get_connection().await?; Ok(connection.interface.unwrap_or_default()) } #[dbus_interface(property)] pub async fn set_interface(&mut self, name: &str) -> zbus::fdo::Result<()> { let interface = Some(name.to_string()); self.update_connection(|c| c.interface = interface).await?; Ok(()) } /// Custom mac-address #[dbus_interface(property)] pub async fn mac_address(&self) -> zbus::fdo::Result<String> { let connection = self.get_connection().await?; Ok(connection.mac_address.to_string()) } #[dbus_interface(property)] pub async fn set_mac_address(&mut self, mac_address: &str) -> zbus::fdo::Result<()> { let mac_address = MacAddress::from_str(mac_address)?; self.update_connection(|c| c.mac_address = mac_address) .await?; Ok(()) } /// Whether the network interface should be active or not #[dbus_interface(property)] pub async fn active(&self) -> zbus::fdo::Result<bool> { let connection = self.get_connection().await?; Ok(connection.is_up()) } #[dbus_interface(property)] pub async fn set_active(&mut self, active: bool) -> zbus::fdo::Result<()> { self.update_connection(|c| { if active { c.set_up(); } else { c.set_down(); } }) .await?; Ok(()) } } #[async_trait] impl ConnectionInterface for Connection { fn uuid(&self) -> Uuid { self.uuid } async fn actions(&self) -> MutexGuard<UnboundedSender<Action>> { self.actions.lock().await } } /// D-Bus interface for Match settings pub struct Match { actions: Arc<Mutex<UnboundedSender<Action>>>, uuid: Uuid, } impl Match { /// Creates a Match Settings interface object. /// /// * `actions`: sending-half of a channel to send actions. /// * `uuid`: nework connection's UUID. pub fn new(actions: UnboundedSender<Action>, uuid: Uuid) -> Self { Self { actions: Arc::new(Mutex::new(actions)), uuid, } } } #[dbus_interface(name = "org.opensuse.Agama1.Network.Connection.Match")] impl Match { /// List of driver names to match. #[dbus_interface(property)] pub async fn driver(&self) -> zbus::fdo::Result<Vec<String>> { let connection = self.get_connection().await?; Ok(connection.match_config.driver) } #[dbus_interface(property)] pub async fn set_driver(&mut self, driver: Vec<String>) -> zbus::fdo::Result<()> { self.update_connection(|c| c.match_config.driver = driver) .await?; Ok(()) } /// List of paths to match agains the ID_PATH udev property of devices. #[dbus_interface(property)] pub async fn path(&self) -> zbus::fdo::Result<Vec<String>> { let connection = self.get_connection().await?; Ok(connection.match_config.path) } #[dbus_interface(property)] pub async fn set_path(&mut self, path: Vec<String>) -> zbus::fdo::Result<()> { self.update_connection(|c| c.match_config.path = path) .await?; Ok(()) } /// List of interface names to match. #[dbus_interface(property)] pub async fn interface(&self) -> zbus::fdo::Result<Vec<String>> { let connection = self.get_connection().await?; Ok(connection.match_config.interface) } #[dbus_interface(property)] pub async fn set_interface(&mut self, interface: Vec<String>) -> zbus::fdo::Result<()> { self.update_connection(|c| c.match_config.interface = interface) .await?; Ok(()) } /// List of kernel options to match. #[dbus_interface(property)] pub async fn kernel(&self) -> zbus::fdo::Result<Vec<String>> { let connection = self.get_connection().await?; Ok(connection.match_config.kernel) } #[dbus_interface(property)] pub async fn set_kernel(&mut self, kernel: Vec<String>) -> zbus::fdo::Result<()> { self.update_connection(|c| c.match_config.kernel = kernel) .await?; Ok(()) } } #[async_trait] impl ConnectionInterface for Match { fn uuid(&self) -> Uuid { self.uuid } async fn actions(&self) -> MutexGuard<UnboundedSender<Action>> { self.actions.lock().await } } 0707010000007D000081A400000000000000000000000166257103000007B3000000000000000000000000000000000000006300000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus/interfaces/devices.rsuse crate::network::{model::Device as NetworkDevice, Action}; use std::sync::Arc; use tokio::sync::{mpsc::UnboundedSender, oneshot, Mutex}; use zbus::{dbus_interface, zvariant::OwnedObjectPath}; /// D-Bus interface for the network devices collection /// /// It offers an API to query the devices collection. pub struct Devices { actions: Arc<Mutex<UnboundedSender<Action>>>, } impl Devices { /// Creates a Devices interface object. /// /// * `objects`: Objects paths registry. pub fn new(actions: UnboundedSender<Action>) -> Self { Self { actions: Arc::new(Mutex::new(actions)), } } } #[dbus_interface(name = "org.opensuse.Agama1.Network.Devices")] impl Devices { /// Returns the D-Bus paths of the network devices. pub async fn get_devices(&self) -> zbus::fdo::Result<Vec<OwnedObjectPath>> { let actions = self.actions.lock().await; let (tx, rx) = oneshot::channel(); actions.send(Action::GetDevicesPaths(tx)).unwrap(); let result = rx.await.unwrap(); Ok(result) } } /// D-Bus interface for a network device /// /// It offers an API to query basic networking devices information (e.g., the name). pub struct Device { device: NetworkDevice, } impl Device { /// Creates an interface object. /// /// * `device`: network device. pub fn new(device: NetworkDevice) -> Self { Self { device } } } #[dbus_interface(name = "org.opensuse.Agama1.Network.Device")] impl Device { /// Device name. /// /// Kernel device name, e.g., eth0, enp1s0, etc. #[dbus_interface(property)] pub fn name(&self) -> &str { &self.device.name } /// Device type. /// /// Possible values: 0 = loopback, 1 = ethernet, 2 = wireless. /// /// See [agama_lib::network::types::DeviceType]. #[dbus_interface(property, name = "Type")] pub fn device_type(&self) -> u8 { self.device.type_ as u8 } } 0707010000007E000081A40000000000000000000000016625710300001C08000000000000000000000000000000000000006500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus/interfaces/ip_config.rs//! Network D-Bus interfaces for IP configuration. //! //! This module contains the D-Bus interfaces to deal with IPv4 and IPv6 configuration. //! The `dbus_interface` macro should be applied to structs, that's the reason there are //! two different structs for IPv4 and IPv6 settings. The common code have been moved //! to the `Ip<T>` struct. use crate::network::{ action::Action, error::NetworkStateError, model::{IpConfig, Ipv4Method, Ipv6Method}, }; use async_trait::async_trait; use cidr::IpInet; use std::{net::IpAddr, sync::Arc}; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::{Mutex, MutexGuard}; use uuid::Uuid; use zbus::dbus_interface; use super::common::ConnectionInterface; /// D-Bus interface for IPv4 and IPv6 settings pub struct Ip { actions: Arc<Mutex<UnboundedSender<Action>>>, uuid: Uuid, } impl Ip { /// Creates an IP interface object. /// /// * `actions`: sending-half of a channel to send actions. /// * `uuid`: connection UUID.. pub fn new(actions: UnboundedSender<Action>, uuid: Uuid) -> Self { Self { actions: Arc::new(Mutex::new(actions)), uuid, } } /// Returns the IpConfig struct. async fn get_ip_config(&self) -> Result<IpConfig, NetworkStateError> { self.get_connection().await.map(|c| c.ip_config) } /// Updates the IpConfig struct. /// /// * `func`: function to update the configuration. async fn update_ip_config<F>(&self, func: F) -> zbus::fdo::Result<()> where F: Fn(&mut IpConfig) + std::marker::Send, { self.update_connection(move |c| func(&mut c.ip_config)) .await?; Ok(()) } } #[dbus_interface(name = "org.opensuse.Agama1.Network.Connection.IP")] impl Ip { /// List of IP addresses. /// /// When the method is 'auto', these addresses are used as additional addresses. #[dbus_interface(property)] pub async fn addresses(&self) -> zbus::fdo::Result<Vec<String>> { let ip_config = self.get_ip_config().await?; let addresses = ip_config.addresses.iter().map(|a| a.to_string()).collect(); Ok(addresses) } #[dbus_interface(property)] pub async fn set_addresses(&mut self, addresses: Vec<String>) -> zbus::fdo::Result<()> { let addresses = helpers::parse_addresses::<IpInet>(addresses); self.update_ip_config(|ip| ip.addresses = addresses.clone()) .await } /// IPv4 configuration method. /// /// Possible values: "disabled", "auto", "manual" or "link-local". /// /// See [crate::network::model::Ipv4Method]. #[dbus_interface(property)] pub async fn method4(&self) -> zbus::fdo::Result<String> { let ip_config = self.get_ip_config().await?; Ok(ip_config.method4.to_string()) } #[dbus_interface(property)] pub async fn set_method4(&mut self, method: &str) -> zbus::fdo::Result<()> { let method: Ipv4Method = method.parse()?; self.update_ip_config(|ip| ip.method4 = method).await } /// IPv6 configuration method. /// /// Possible values: "disabled", "auto", "manual", "link-local", "ignore" or "dhcp". /// /// See [crate::network::model::Ipv6Method]. #[dbus_interface(property)] pub async fn method6(&self) -> zbus::fdo::Result<String> { let ip_config = self.get_ip_config().await?; Ok(ip_config.method6.to_string()) } #[dbus_interface(property)] pub async fn set_method6(&mut self, method: &str) -> zbus::fdo::Result<()> { let method: Ipv6Method = method.parse()?; self.update_ip_config(|ip| ip.method6 = method).await } /// Name server addresses. #[dbus_interface(property)] pub async fn nameservers(&self) -> zbus::fdo::Result<Vec<String>> { let ip_config = self.get_ip_config().await?; let nameservers = ip_config .nameservers .iter() .map(IpAddr::to_string) .collect(); Ok(nameservers) } #[dbus_interface(property)] pub async fn set_nameservers(&mut self, addresses: Vec<String>) -> zbus::fdo::Result<()> { let addresses = helpers::parse_addresses::<IpAddr>(addresses); self.update_ip_config(|ip| ip.nameservers = addresses.clone()) .await } /// Network gateway for IPv4. /// /// An empty string removes the current value. #[dbus_interface(property)] pub async fn gateway4(&self) -> zbus::fdo::Result<String> { let ip_config = self.get_ip_config().await?; let gateway = match ip_config.gateway4 { Some(ref address) => address.to_string(), None => "".to_string(), }; Ok(gateway) } #[dbus_interface(property)] pub async fn set_gateway4(&mut self, gateway: String) -> zbus::fdo::Result<()> { let gateway = helpers::parse_gateway(gateway)?; self.update_ip_config(|ip| ip.gateway4 = gateway).await } /// Network gateway for IPv6. /// /// An empty string removes the current value. #[dbus_interface(property)] pub async fn gateway6(&self) -> zbus::fdo::Result<String> { let ip_config = self.get_ip_config().await?; let result = match ip_config.gateway6 { Some(ref address) => address.to_string(), None => "".to_string(), }; Ok(result) } #[dbus_interface(property)] pub async fn set_gateway6(&mut self, gateway: String) -> zbus::fdo::Result<()> { let gateway = helpers::parse_gateway(gateway)?; self.update_ip_config(|ip| ip.gateway6 = gateway).await } } mod helpers { use crate::network::error::NetworkStateError; use log; use std::{ fmt::{Debug, Display}, str::FromStr, }; /// Parses a set of addresses in textual form into T. /// /// * `addresses`: addresses to parse. pub fn parse_addresses<T>(addresses: Vec<String>) -> Vec<T> where T: FromStr, <T as FromStr>::Err: Display, { addresses .into_iter() .filter_map(|ip| match ip.parse::<T>() { Ok(address) => Some(address), Err(error) => { log::error!("Ignoring the invalid IP address: {} ({})", ip, error); None } }) .collect() } /// Sets the gateway for an IP configuration. /// /// * `ip`: IpConfig object. /// * `gateway`: IP in textual form. pub fn parse_gateway<T>(gateway: String) -> Result<Option<T>, NetworkStateError> where T: FromStr, <T as FromStr>::Err: Debug + Display, { if gateway.is_empty() { Ok(None) } else { let parsed = gateway .parse() .map_err(|_| NetworkStateError::InvalidIpAddr(gateway))?; Ok(Some(parsed)) } } } #[async_trait] impl ConnectionInterface for Ip { fn uuid(&self) -> Uuid { self.uuid } async fn actions(&self) -> MutexGuard<UnboundedSender<Action>> { self.actions.lock().await } } 0707010000007F000081A400000000000000000000000166257103000003AE000000000000000000000000000000000000005800000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus/service.rs//! Network D-Bus service. //! //! This module defines a D-Bus service which exposes Agama's network configuration. use crate::network::{Adapter, NetworkSystem}; use std::error::Error; use tokio; use zbus::Connection; /// Represents the Agama networking D-Bus service. /// /// It is responsible for starting the [NetworkSystem] on a different thread. pub struct NetworkService; impl NetworkService { /// Starts listening and dispatching events on the D-Bus connection. pub async fn start<T: Adapter + std::marker::Send + 'static>( connection: &Connection, adapter: T, ) -> Result<(), Box<dyn Error>> { let mut network = NetworkSystem::new(connection.clone(), adapter); tokio::spawn(async move { network .setup() .await .expect("Could not set up the D-Bus tree"); network.listen().await; }); Ok(()) } } 07070100000080000081A40000000000000000000000016625710300002189000000000000000000000000000000000000005500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/dbus/tree.rsuse agama_lib::error::ServiceError; use uuid::Uuid; use zbus::zvariant::{ObjectPath, OwnedObjectPath}; use crate::network::{action::Action, dbus::interfaces, model::*}; use log; use std::collections::HashMap; use tokio::sync::mpsc::UnboundedSender; const CONNECTIONS_PATH: &str = "/org/opensuse/Agama1/Network/connections"; const DEVICES_PATH: &str = "/org/opensuse/Agama1/Network/devices"; /// Handle the objects in the D-Bus tree for the network state pub struct Tree { connection: zbus::Connection, actions: UnboundedSender<Action>, objects: ObjectsRegistry, } impl Tree { /// Creates a new tree handler. /// /// * `connection`: D-Bus connection to use. /// * `actions`: sending-half of a channel to send actions. pub fn new(connection: zbus::Connection, actions: UnboundedSender<Action>) -> Self { Self { connection, actions, objects: Default::default(), } } /// Refreshes the list of connections. /// /// TODO: re-creating the tree is kind of brute-force and it sends signals about /// adding/removing interfaces. We should add/update/delete objects as needed. /// /// * `connections`: list of connections. pub async fn set_connections( &mut self, connections: &mut [Connection], ) -> Result<(), ServiceError> { self.remove_connections().await?; self.add_connections(connections).await?; Ok(()) } /// Refreshes the list of devices. /// /// * `devices`: list of devices. pub async fn set_devices(&mut self, devices: &[Device]) -> Result<(), ServiceError> { self.remove_devices().await?; self.add_devices(devices).await?; Ok(()) } /// Adds devices to the D-Bus tree. /// /// * `devices`: list of devices. pub async fn add_devices(&mut self, devices: &[Device]) -> Result<(), ServiceError> { for (i, dev) in devices.iter().enumerate() { let path = format!("{}/{}", DEVICES_PATH, i); let path = ObjectPath::try_from(path.as_str()).unwrap(); self.add_interface(&path, interfaces::Device::new(dev.clone())) .await?; self.objects.register_device(&dev.name, path); } self.add_interface(DEVICES_PATH, interfaces::Devices::new(self.actions.clone())) .await?; Ok(()) } /// Adds a connection to the D-Bus tree and returns the D-Bus path. /// /// * `conn`: connection to add. /// * `notify`: whether to notify the added connection pub async fn add_connection( &mut self, conn: &Connection, ) -> Result<OwnedObjectPath, ServiceError> { let uuid = conn.uuid; let path: OwnedObjectPath = self.objects.register_connection(conn.uuid).into(); log::info!( "Publishing network connection '{}' on '{}'", &conn.id, &path ); self.add_interface( &path, interfaces::Connection::new(self.actions.clone(), uuid), ) .await?; self.add_interface(&path, interfaces::Ip::new(self.actions.clone(), uuid)) .await?; self.add_interface(&path, interfaces::Match::new(self.actions.clone(), uuid)) .await?; if let ConnectionConfig::Bond(_) = conn.config { self.add_interface(&path, interfaces::Bond::new(self.actions.clone(), uuid)) .await?; } if let ConnectionConfig::Wireless(_) = conn.config { self.add_interface(&path, interfaces::Wireless::new(self.actions.clone(), uuid)) .await?; } Ok(path) } /// Removes a connection from the tree /// /// * `uuid`: connection UUID. pub async fn remove_connection(&mut self, uuid: Uuid) -> Result<(), ServiceError> { let Some(path) = self.objects.connection_path(uuid) else { return Ok(()); }; self.remove_connection_on(path.as_str()).await?; self.objects.deregister_connection(uuid).unwrap(); Ok(()) } /// Returns all devices paths. pub fn devices_paths(&self) -> Vec<OwnedObjectPath> { self.objects.devices_paths() } /// Returns all connection paths. pub fn connections_paths(&self) -> Vec<OwnedObjectPath> { self.objects.connections_paths() } pub fn connection_path(&self, uuid: Uuid) -> Option<OwnedObjectPath> { self.objects.connection_path(uuid).map(|o| o.into()) } /// Adds connections to the D-Bus tree. /// /// * `connections`: list of connections. async fn add_connections( &mut self, connections: &mut [Connection], ) -> Result<(), ServiceError> { for conn in connections.iter_mut() { self.add_connection(conn).await?; } self.add_interface( CONNECTIONS_PATH, interfaces::Connections::new(self.actions.clone()), ) .await?; Ok(()) } /// Clears all the connections from the tree. async fn remove_connections(&mut self) -> Result<(), ServiceError> { for path in self.objects.connections.values() { self.remove_connection_on(path.as_str()).await?; } self.objects.connections.clear(); Ok(()) } /// Clears all the devices from the tree. async fn remove_devices(&mut self) -> Result<(), ServiceError> { let object_server = self.connection.object_server(); for path in self.objects.devices.values() { object_server .remove::<interfaces::Device, _>(path.as_str()) .await?; } self.objects.devices.clear(); Ok(()) } /// Removes a connection object on the given path /// /// * `path`: connection D-Bus path. async fn remove_connection_on(&self, path: &str) -> Result<(), ServiceError> { let object_server = self.connection.object_server(); _ = object_server.remove::<interfaces::Bond, _>(path).await; _ = object_server.remove::<interfaces::Wireless, _>(path).await; object_server.remove::<interfaces::Ip, _>(path).await?; object_server.remove::<interfaces::Match, _>(path).await?; object_server .remove::<interfaces::Connection, _>(path) .await?; Ok(()) } async fn add_interface<T>(&mut self, path: &str, iface: T) -> Result<bool, ServiceError> where T: zbus::Interface, { let object_server = self.connection.object_server(); Ok(object_server.at(path, iface).await?) } } /// Objects paths for known devices and connections /// /// Connections are indexed by its Id, which is expected to be unique. #[derive(Debug, Default)] struct ObjectsRegistry { /// device_name (eth0) -> object_path devices: HashMap<String, OwnedObjectPath>, /// id -> object_path connections: HashMap<Uuid, OwnedObjectPath>, } impl ObjectsRegistry { /// Registers a network device. /// /// * `id`: device name. /// * `path`: object path. pub fn register_device(&mut self, id: &str, path: ObjectPath) { self.devices.insert(id.to_string(), path.into()); } /// Registers a network connection and returns its D-Bus path. /// /// It returns the connection Id and the D-Bus path. Bear in mind that the Id can be different /// in case the original one already existed. /// /// * `uuid`: network connection's UUID. pub fn register_connection(&mut self, uuid: Uuid) -> ObjectPath { let path = format!("{}/{}", CONNECTIONS_PATH, self.connections.len()); let path = ObjectPath::try_from(path).unwrap(); self.connections.insert(uuid, path.clone().into()); path } /// Returns the path for a connection. /// /// * `uuid`: connection ID. pub fn connection_path(&self, uuid: Uuid) -> Option<ObjectPath> { self.connections.get(&uuid).map(|p| p.as_ref()) } /// Deregisters a network connection. /// /// * `id`: connection ID. pub fn deregister_connection(&mut self, uuid: Uuid) -> Option<OwnedObjectPath> { self.connections.remove(&uuid) } /// Returns all devices paths. pub fn devices_paths(&self) -> Vec<OwnedObjectPath> { self.devices.values().cloned().collect() } /// Returns all connection paths. pub fn connections_paths(&self) -> Vec<OwnedObjectPath> { self.connections.values().cloned().collect() } } 07070100000081000081A4000000000000000000000001662571030000056A000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/error.rs//! Error types. use thiserror::Error; /// Errors that are related to the network configuration. #[derive(Error, Debug)] pub enum NetworkStateError { #[error("Unknown connection '{0}'")] UnknownConnection(String), #[error("Invalid connection UUID: '{0}'")] InvalidUuid(String), #[error("Invalid IP address: '{0}'")] InvalidIpAddr(String), #[error("Invalid IP method: '{0}'")] InvalidIpMethod(u8), #[error("Invalid wireless mode: '{0}'")] InvalidWirelessMode(String), #[error("Connection '{0}' already exists")] ConnectionExists(String), #[error("Invalid security wireless protocol: '{0}'")] InvalidSecurityProtocol(String), #[error("Adapter error: '{0}'")] AdapterError(String), #[error("Invalid bond mode '{0}'")] InvalidBondMode(String), #[error("Invalid bond options")] InvalidBondOptions, #[error("Not a controller connection: '{0}'")] NotControllerConnection(String), #[error("Unexpected configuration")] UnexpectedConfiguration, #[error("Invalid WEP authentication algorithm: '{0}'")] InvalidWEPAuthAlg(String), #[error("Invalid WEP key type: '{0}'")] InvalidWEPKeyType(u32), } impl From<NetworkStateError> for zbus::fdo::Error { fn from(value: NetworkStateError) -> zbus::fdo::Error { zbus::fdo::Error::Failed(format!("Network error: {value}")) } } 07070100000082000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/model07070100000083000081A4000000000000000000000001662571030000767E000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/model.rs//! Representation of the network configuration //! //! * This module contains the types that represent the network concepts. They are supposed to be //! agnostic from the real network service (e.g., NetworkManager). use crate::network::error::NetworkStateError; use agama_lib::network::types::{BondMode, DeviceType, SSID}; use cidr::IpInet; use std::{ collections::HashMap, default::Default, fmt, net::IpAddr, str::{self, FromStr}, }; use thiserror::Error; use uuid::Uuid; use zbus::zvariant::Value; #[derive(Default, Clone, Debug)] pub struct NetworkState { pub devices: Vec<Device>, pub connections: Vec<Connection>, } impl NetworkState { /// Returns a NetworkState struct with the given devices and connections. /// /// * `devices`: devices to include in the state. /// * `connections`: connections to include in the state. pub fn new(devices: Vec<Device>, connections: Vec<Connection>) -> Self { Self { devices, connections, } } /// Get device by name /// /// * `name`: device name pub fn get_device(&self, name: &str) -> Option<&Device> { self.devices.iter().find(|d| d.name == name) } /// Get connection by UUID /// /// * `uuid`: connection UUID pub fn get_connection_by_uuid(&self, uuid: Uuid) -> Option<&Connection> { self.connections.iter().find(|c| c.uuid == uuid) } /// Get connection by UUID as mutable /// /// * `uuid`: connection UUID pub fn get_connection_by_uuid_mut(&mut self, uuid: Uuid) -> Option<&mut Connection> { self.connections.iter_mut().find(|c| c.uuid == uuid) } /// Get connection by interface /// /// * `name`: connection interface name pub fn get_connection_by_interface(&self, name: &str) -> Option<&Connection> { let interface = Some(name); self.connections .iter() .find(|c| c.interface.as_deref() == interface) } /// Get connection by ID /// /// * `id`: connection ID pub fn get_connection(&self, id: &str) -> Option<&Connection> { self.connections.iter().find(|c| c.id == id) } /// Get connection by ID as mutable /// /// * `id`: connection ID pub fn get_connection_mut(&mut self, id: &str) -> Option<&mut Connection> { self.connections.iter_mut().find(|c| c.id == id) } pub fn get_controlled_by(&mut self, uuid: Uuid) -> Vec<&Connection> { let uuid = Some(uuid); self.connections .iter() .filter(|c| c.controller == uuid) .collect() } /// Adds a new connection. /// /// It uses the `id` to decide whether the connection already exists. pub fn add_connection(&mut self, conn: Connection) -> Result<(), NetworkStateError> { if self.get_connection(&conn.id).is_some() { return Err(NetworkStateError::ConnectionExists(conn.id)); } self.connections.push(conn); Ok(()) } /// Updates a connection with a new one. /// /// It uses the `id` to decide which connection to update. /// /// Additionally, it registers the connection to be removed when the changes are applied. pub fn update_connection(&mut self, conn: Connection) -> Result<(), NetworkStateError> { let Some(old_conn) = self.get_connection_mut(&conn.id) else { return Err(NetworkStateError::UnknownConnection(conn.id.clone())); }; *old_conn = conn; Ok(()) } /// Removes a connection from the state. /// /// Additionally, it registers the connection to be removed when the changes are applied. pub fn remove_connection(&mut self, uuid: Uuid) -> Result<(), NetworkStateError> { let Some(conn) = self.get_connection_by_uuid_mut(uuid) else { return Err(NetworkStateError::UnknownConnection(uuid.to_string())); }; conn.remove(); Ok(()) } /// Sets a controller's ports. /// /// If the connection is not a controller, returns an error. /// /// * `controller`: controller to set ports on. /// * `ports`: list of port names (using the connection ID or the interface name). pub fn set_ports( &mut self, controller: &Connection, ports: Vec<String>, ) -> Result<(), NetworkStateError> { if let ConnectionConfig::Bond(_) = &controller.config { let mut controlled = vec![]; for port in ports { let connection = self .get_connection_by_interface(&port) .or_else(|| self.get_connection(&port)) .ok_or(NetworkStateError::UnknownConnection(port))?; controlled.push(connection.uuid); } for conn in self.connections.iter_mut() { if controlled.contains(&conn.uuid) { conn.controller = Some(controller.uuid); } else if conn.controller == Some(controller.uuid) { conn.controller = None; } } Ok(()) } else { Err(NetworkStateError::NotControllerConnection( controller.id.to_owned(), )) } } } #[cfg(test)] mod tests { use super::*; use crate::network::error::NetworkStateError; use uuid::Uuid; #[test] fn test_macaddress() { let mut val: Option<String> = None; assert!(matches!( MacAddress::try_from(&val).unwrap(), MacAddress::Unset )); val = Some(String::from("")); assert!(matches!( MacAddress::try_from(&val).unwrap(), MacAddress::Unset )); val = Some(String::from("preserve")); assert!(matches!( MacAddress::try_from(&val).unwrap(), MacAddress::Preserve )); val = Some(String::from("permanent")); assert!(matches!( MacAddress::try_from(&val).unwrap(), MacAddress::Permanent )); val = Some(String::from("random")); assert!(matches!( MacAddress::try_from(&val).unwrap(), MacAddress::Random )); val = Some(String::from("stable")); assert!(matches!( MacAddress::try_from(&val).unwrap(), MacAddress::Stable )); val = Some(String::from("This is not a MACAddr")); assert!(matches!( MacAddress::try_from(&val), Err(InvalidMacAddress(_)) )); val = Some(String::from("de:ad:be:ef:2b:ad")); assert_eq!( MacAddress::try_from(&val).unwrap().to_string(), String::from("de:ad:be:ef:2b:ad").to_uppercase() ); } #[test] fn test_add_connection() { let mut state = NetworkState::default(); let uuid = Uuid::new_v4(); let conn0 = Connection { id: "eth0".to_string(), uuid, ..Default::default() }; state.add_connection(conn0).unwrap(); let found = state.get_connection("eth0").unwrap(); assert_eq!(found.uuid, uuid); } #[test] fn test_add_duplicated_connection() { let mut state = NetworkState::default(); let mut conn0 = Connection::new("eth0".to_string(), DeviceType::Ethernet); conn0.uuid = Uuid::new_v4(); state.add_connection(conn0.clone()).unwrap(); let error = state.add_connection(conn0).unwrap_err(); assert!(matches!(error, NetworkStateError::ConnectionExists(_))); } #[test] fn test_update_connection() { let mut state = NetworkState::default(); let conn0 = Connection { id: "eth0".to_string(), uuid: Uuid::new_v4(), ..Default::default() }; state.add_connection(conn0).unwrap(); let uuid = Uuid::new_v4(); let conn1 = Connection { id: "eth0".to_string(), uuid, ..Default::default() }; state.update_connection(conn1).unwrap(); let found = state.get_connection("eth0").unwrap(); assert_eq!(found.uuid, uuid); } #[test] fn test_update_unknown_connection() { let mut state = NetworkState::default(); let conn0 = Connection::new("eth0".to_string(), DeviceType::Ethernet); let error = state.update_connection(conn0).unwrap_err(); assert!(matches!(error, NetworkStateError::UnknownConnection(_))); } #[test] fn test_remove_connection() { let mut state = NetworkState::default(); let conn0 = Connection::new("eth0".to_string(), DeviceType::Ethernet); let uuid = conn0.uuid; state.add_connection(conn0).unwrap(); state.remove_connection(uuid).unwrap(); let found = state.get_connection("eth0").unwrap(); assert!(found.is_removed()); } #[test] fn test_remove_unknown_connection() { let mut state = NetworkState::default(); let error = state.remove_connection(Uuid::new_v4()).unwrap_err(); assert!(matches!(error, NetworkStateError::UnknownConnection(_))); } #[test] fn test_is_loopback() { let conn = Connection::new("eth0".to_string(), DeviceType::Ethernet); assert!(!conn.is_loopback()); let conn = Connection::new("eth0".to_string(), DeviceType::Loopback); assert!(conn.is_loopback()); } #[test] fn test_set_bonding_ports() { let mut state = NetworkState::default(); let eth0 = Connection { id: "eth0".to_string(), interface: Some("eth0".to_string()), ..Default::default() }; let eth1 = Connection { id: "eth1".to_string(), interface: Some("eth1".to_string()), ..Default::default() }; let bond0 = Connection { id: "bond0".to_string(), interface: Some("bond0".to_string()), config: ConnectionConfig::Bond(Default::default()), ..Default::default() }; state.add_connection(eth0).unwrap(); state.add_connection(eth1).unwrap(); state.add_connection(bond0.clone()).unwrap(); state.set_ports(&bond0, vec!["eth1".to_string()]).unwrap(); let eth1_found = state.get_connection("eth1").unwrap(); assert_eq!(eth1_found.controller, Some(bond0.uuid)); let eth0_found = state.get_connection("eth0").unwrap(); assert_eq!(eth0_found.controller, None); } #[test] fn test_set_bonding_missing_port() { let mut state = NetworkState::default(); let bond0 = Connection { id: "bond0".to_string(), interface: Some("bond0".to_string()), config: ConnectionConfig::Bond(Default::default()), ..Default::default() }; state.add_connection(bond0.clone()).unwrap(); let error = state .set_ports(&bond0, vec!["eth0".to_string()]) .unwrap_err(); assert!(matches!(error, NetworkStateError::UnknownConnection(_))); } #[test] fn test_set_non_controller_ports() { let mut state = NetworkState::default(); let eth0 = Connection { id: "eth0".to_string(), ..Default::default() }; state.add_connection(eth0.clone()).unwrap(); let error = state .set_ports(ð0, vec!["eth1".to_string()]) .unwrap_err(); assert!(matches!( error, NetworkStateError::NotControllerConnection(_), )); } } /// Network device #[derive(Debug, Clone)] pub struct Device { pub name: String, pub type_: DeviceType, } /// Represents an availble network connection. #[derive(Debug, Clone, PartialEq)] pub struct Connection { pub id: String, pub uuid: Uuid, pub mac_address: MacAddress, pub firewall_zone: Option<String>, pub ip_config: IpConfig, pub status: Status, pub interface: Option<String>, pub controller: Option<Uuid>, pub port_config: PortConfig, pub match_config: MatchConfig, pub config: ConnectionConfig, } impl Connection { pub fn new(id: String, device_type: DeviceType) -> Self { let config = match device_type { DeviceType::Ethernet => ConnectionConfig::Ethernet, DeviceType::Wireless => ConnectionConfig::Wireless(Default::default()), DeviceType::Loopback => ConnectionConfig::Loopback, DeviceType::Dummy => ConnectionConfig::Dummy, DeviceType::Bond => ConnectionConfig::Bond(Default::default()), DeviceType::Vlan => ConnectionConfig::Vlan(Default::default()), DeviceType::Bridge => ConnectionConfig::Bridge(Default::default()), }; Self { id, config, ..Default::default() } } pub fn remove(&mut self) { self.status = Status::Removed; } pub fn is_removed(&self) -> bool { self.status == Status::Removed } pub fn is_up(&self) -> bool { self.status == Status::Up } pub fn set_up(&mut self) { self.status = Status::Up } pub fn set_down(&mut self) { self.status = Status::Down } /// Determines whether it is a loopback interface. pub fn is_loopback(&self) -> bool { matches!(self.config, ConnectionConfig::Loopback) } pub fn is_ethernet(&self) -> bool { matches!(self.config, ConnectionConfig::Loopback) || matches!(self.config, ConnectionConfig::Ethernet) || matches!(self.config, ConnectionConfig::Dummy) || matches!(self.config, ConnectionConfig::Bond(_)) || matches!(self.config, ConnectionConfig::Vlan(_)) || matches!(self.config, ConnectionConfig::Bridge(_)) } } impl Default for Connection { fn default() -> Self { Self { id: Default::default(), uuid: Uuid::new_v4(), mac_address: Default::default(), firewall_zone: Default::default(), ip_config: Default::default(), status: Default::default(), interface: Default::default(), controller: Default::default(), port_config: Default::default(), match_config: Default::default(), config: Default::default(), } } } #[derive(Default, Debug, PartialEq, Clone)] pub enum ConnectionConfig { #[default] Ethernet, Wireless(WirelessConfig), Loopback, Dummy, Bond(BondConfig), Vlan(VlanConfig), Bridge(BridgeConfig), Infiniband(InfinibandConfig), } #[derive(Default, Debug, PartialEq, Clone)] pub enum PortConfig { #[default] None, Bridge(BridgePortConfig), } impl From<BondConfig> for ConnectionConfig { fn from(value: BondConfig) -> Self { Self::Bond(value) } } impl From<WirelessConfig> for ConnectionConfig { fn from(value: WirelessConfig) -> Self { Self::Wireless(value) } } #[derive(Debug, Error)] #[error("Invalid MAC address: {0}")] pub struct InvalidMacAddress(String); #[derive(Debug, Default, Clone, PartialEq)] pub enum MacAddress { MacAddress(macaddr::MacAddr6), Preserve, Permanent, Random, Stable, #[default] Unset, } impl FromStr for MacAddress { type Err = InvalidMacAddress; fn from_str(s: &str) -> Result<Self, Self::Err> { match s { "preserve" => Ok(Self::Preserve), "permanent" => Ok(Self::Permanent), "random" => Ok(Self::Random), "stable" => Ok(Self::Stable), "" => Ok(Self::Unset), _ => Ok(Self::MacAddress(match macaddr::MacAddr6::from_str(s) { Ok(mac) => mac, Err(e) => return Err(InvalidMacAddress(e.to_string())), })), } } } impl TryFrom<&Option<String>> for MacAddress { type Error = InvalidMacAddress; fn try_from(value: &Option<String>) -> Result<Self, Self::Error> { match &value { Some(str) => MacAddress::from_str(str), None => Ok(Self::Unset), } } } impl fmt::Display for MacAddress { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let output = match &self { Self::MacAddress(mac) => mac.to_string(), Self::Preserve => "preserve".to_string(), Self::Permanent => "permanent".to_string(), Self::Random => "random".to_string(), Self::Stable => "stable".to_string(), Self::Unset => "".to_string(), }; write!(f, "{}", output) } } impl From<InvalidMacAddress> for zbus::fdo::Error { fn from(value: InvalidMacAddress) -> Self { zbus::fdo::Error::Failed(value.to_string()) } } #[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum Status { #[default] Up, Down, Removed, } #[derive(Default, Debug, PartialEq, Clone)] pub struct IpConfig { pub method4: Ipv4Method, pub method6: Ipv6Method, pub addresses: Vec<IpInet>, pub nameservers: Vec<IpAddr>, pub gateway4: Option<IpAddr>, pub gateway6: Option<IpAddr>, pub routes4: Option<Vec<IpRoute>>, pub routes6: Option<Vec<IpRoute>>, } #[derive(Debug, Default, PartialEq, Clone)] pub struct MatchConfig { pub driver: Vec<String>, pub interface: Vec<String>, pub path: Vec<String>, pub kernel: Vec<String>, } #[derive(Debug, Error)] #[error("Unknown IP configuration method name: {0}")] pub struct UnknownIpMethod(String); #[derive(Debug, Default, Copy, Clone, PartialEq)] pub enum Ipv4Method { #[default] Disabled = 0, Auto = 1, Manual = 2, LinkLocal = 3, } impl fmt::Display for Ipv4Method { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match &self { Ipv4Method::Disabled => "disabled", Ipv4Method::Auto => "auto", Ipv4Method::Manual => "manual", Ipv4Method::LinkLocal => "link-local", }; write!(f, "{}", name) } } impl FromStr for Ipv4Method { type Err = UnknownIpMethod; fn from_str(s: &str) -> Result<Self, Self::Err> { match s { "disabled" => Ok(Ipv4Method::Disabled), "auto" => Ok(Ipv4Method::Auto), "manual" => Ok(Ipv4Method::Manual), "link-local" => Ok(Ipv4Method::LinkLocal), _ => Err(UnknownIpMethod(s.to_string())), } } } #[derive(Debug, Default, Copy, Clone, PartialEq)] pub enum Ipv6Method { #[default] Disabled = 0, Auto = 1, Manual = 2, LinkLocal = 3, Ignore = 4, Dhcp = 5, } impl fmt::Display for Ipv6Method { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match &self { Ipv6Method::Disabled => "disabled", Ipv6Method::Auto => "auto", Ipv6Method::Manual => "manual", Ipv6Method::LinkLocal => "link-local", Ipv6Method::Ignore => "ignore", Ipv6Method::Dhcp => "dhcp", }; write!(f, "{}", name) } } impl FromStr for Ipv6Method { type Err = UnknownIpMethod; fn from_str(s: &str) -> Result<Self, Self::Err> { match s { "disabled" => Ok(Ipv6Method::Disabled), "auto" => Ok(Ipv6Method::Auto), "manual" => Ok(Ipv6Method::Manual), "link-local" => Ok(Ipv6Method::LinkLocal), "ignore" => Ok(Ipv6Method::Ignore), "dhcp" => Ok(Ipv6Method::Dhcp), _ => Err(UnknownIpMethod(s.to_string())), } } } impl From<UnknownIpMethod> for zbus::fdo::Error { fn from(value: UnknownIpMethod) -> zbus::fdo::Error { zbus::fdo::Error::Failed(value.to_string()) } } #[derive(Debug, PartialEq, Clone)] pub struct IpRoute { pub destination: IpInet, pub next_hop: Option<IpAddr>, pub metric: Option<u32>, } impl From<&IpRoute> for HashMap<&str, Value<'_>> { fn from(route: &IpRoute) -> Self { let mut map: HashMap<&str, Value> = HashMap::from([ ("dest", Value::new(route.destination.address().to_string())), ( "prefix", Value::new(route.destination.network_length() as u32), ), ]); if let Some(next_hop) = route.next_hop { map.insert("next-hop", Value::new(next_hop.to_string())); } if let Some(metric) = route.metric { map.insert("metric", Value::new(metric)); } map } } #[derive(Debug, Default, PartialEq, Clone)] pub enum VlanProtocol { #[default] IEEE802_1Q, IEEE802_1ad, } #[derive(Debug, Error)] #[error("Invalid VlanProtocol: {0}")] pub struct InvalidVlanProtocol(String); impl std::str::FromStr for VlanProtocol { type Err = InvalidVlanProtocol; fn from_str(s: &str) -> Result<VlanProtocol, Self::Err> { match s { "802.1Q" => Ok(VlanProtocol::IEEE802_1Q), "802.1ad" => Ok(VlanProtocol::IEEE802_1ad), _ => Err(InvalidVlanProtocol(s.to_string())), } } } impl fmt::Display for VlanProtocol { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match &self { VlanProtocol::IEEE802_1Q => "802.1Q", VlanProtocol::IEEE802_1ad => "802.1ad", }; write!(f, "{}", name) } } #[derive(Debug, Default, PartialEq, Clone)] pub struct VlanConfig { pub parent: String, pub id: u32, pub protocol: VlanProtocol, } #[derive(Debug, Default, PartialEq, Clone)] pub struct WirelessConfig { pub mode: WirelessMode, pub ssid: SSID, pub password: Option<String>, pub security: SecurityProtocol, pub band: Option<WirelessBand>, pub channel: Option<u32>, pub bssid: Option<macaddr::MacAddr6>, pub wep_security: Option<WEPSecurity>, pub hidden: bool, } impl TryFrom<ConnectionConfig> for WirelessConfig { type Error = NetworkStateError; fn try_from(value: ConnectionConfig) -> Result<Self, Self::Error> { match value { ConnectionConfig::Wireless(config) => Ok(config), _ => Err(NetworkStateError::UnexpectedConfiguration), } } } #[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum WirelessMode { Unknown = 0, AdHoc = 1, #[default] Infra = 2, AP = 3, Mesh = 4, } impl TryFrom<&str> for WirelessMode { type Error = NetworkStateError; fn try_from(value: &str) -> Result<Self, Self::Error> { match value { "unknown" => Ok(WirelessMode::Unknown), "adhoc" => Ok(WirelessMode::AdHoc), "infrastructure" => Ok(WirelessMode::Infra), "ap" => Ok(WirelessMode::AP), "mesh" => Ok(WirelessMode::Mesh), _ => Err(NetworkStateError::InvalidWirelessMode(value.to_string())), } } } impl fmt::Display for WirelessMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match &self { WirelessMode::Unknown => "unknown", WirelessMode::AdHoc => "adhoc", WirelessMode::Infra => "infrastructure", WirelessMode::AP => "ap", WirelessMode::Mesh => "mesh", }; write!(f, "{}", name) } } #[derive(Debug, Clone, Copy, Default, PartialEq)] pub enum SecurityProtocol { #[default] WEP, // No encryption or WEP ("none") OWE, // Opportunistic Wireless Encryption ("owe") DynamicWEP, // Dynamic WEP ("ieee8021x") WPA2, // WPA2 + WPA3 personal ("wpa-psk") WPA3Personal, // WPA3 personal only ("sae") WPA2Enterprise, // WPA2 + WPA3 Enterprise ("wpa-eap") WPA3Only, // WPA3 only ("wpa-eap-suite-b192") } impl fmt::Display for SecurityProtocol { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let value = match &self { SecurityProtocol::WEP => "none", SecurityProtocol::OWE => "owe", SecurityProtocol::DynamicWEP => "ieee8021x", SecurityProtocol::WPA2 => "wpa-psk", SecurityProtocol::WPA3Personal => "sae", SecurityProtocol::WPA2Enterprise => "wpa-eap", SecurityProtocol::WPA3Only => "wpa-eap-suite-b192", }; write!(f, "{}", value) } } impl TryFrom<&str> for SecurityProtocol { type Error = NetworkStateError; fn try_from(value: &str) -> Result<Self, Self::Error> { match value { "none" => Ok(SecurityProtocol::WEP), "owe" => Ok(SecurityProtocol::OWE), "ieee8021x" => Ok(SecurityProtocol::DynamicWEP), "wpa-psk" => Ok(SecurityProtocol::WPA2), "sae" => Ok(SecurityProtocol::WPA3Personal), "wpa-eap" => Ok(SecurityProtocol::WPA2Enterprise), "wpa-eap-suite-b192" => Ok(SecurityProtocol::WPA3Only), _ => Err(NetworkStateError::InvalidSecurityProtocol( value.to_string(), )), } } } #[derive(Debug, Default, PartialEq, Clone)] pub struct WEPSecurity { pub auth_alg: WEPAuthAlg, pub wep_key_type: WEPKeyType, pub keys: Vec<String>, pub wep_key_index: u32, } #[derive(Debug, Default, PartialEq, Clone)] pub enum WEPKeyType { #[default] Unknown = 0, Key = 1, Passphrase = 2, } impl TryFrom<u32> for WEPKeyType { type Error = NetworkStateError; fn try_from(value: u32) -> Result<Self, Self::Error> { match value { 0 => Ok(WEPKeyType::Unknown), 1 => Ok(WEPKeyType::Key), 2 => Ok(WEPKeyType::Passphrase), _ => Err(NetworkStateError::InvalidWEPKeyType(value)), } } } #[derive(Debug, Default, PartialEq, Clone)] pub enum WEPAuthAlg { #[default] Unset, Open, Shared, Leap, } impl TryFrom<&str> for WEPAuthAlg { type Error = NetworkStateError; fn try_from(value: &str) -> Result<Self, Self::Error> { match value { "open" => Ok(WEPAuthAlg::Open), "shared" => Ok(WEPAuthAlg::Shared), "leap" => Ok(WEPAuthAlg::Leap), "" => Ok(WEPAuthAlg::Unset), _ => Err(NetworkStateError::InvalidWEPAuthAlg(value.to_string())), } } } impl fmt::Display for WEPAuthAlg { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match &self { WEPAuthAlg::Open => "open", WEPAuthAlg::Shared => "shared", WEPAuthAlg::Leap => "shared", WEPAuthAlg::Unset => "", }; write!(f, "{}", name) } } #[derive(Debug, Clone, Copy, PartialEq)] pub enum WirelessBand { A, // 5GHz BG, // 2.4GHz } impl fmt::Display for WirelessBand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let value = match &self { WirelessBand::A => "a", WirelessBand::BG => "bg", }; write!(f, "{}", value) } } impl TryFrom<&str> for WirelessBand { type Error = anyhow::Error; fn try_from(value: &str) -> Result<Self, Self::Error> { match value { "a" => Ok(WirelessBand::A), "bg" => Ok(WirelessBand::BG), _ => Err(anyhow::anyhow!("Invalid band: {}", value)), } } } #[derive(Debug, Default, Clone, PartialEq)] pub struct BondOptions(pub HashMap<String, String>); impl TryFrom<&str> for BondOptions { type Error = NetworkStateError; fn try_from(value: &str) -> Result<Self, Self::Error> { let mut options = HashMap::new(); for opt in value.split_whitespace() { let (key, value) = opt .trim() .split_once('=') .ok_or(NetworkStateError::InvalidBondOptions)?; options.insert(key.to_string(), value.to_string()); } Ok(BondOptions(options)) } } impl fmt::Display for BondOptions { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let opts = &self .0 .iter() .map(|(key, value)| format!("{key}={value}")) .collect::<Vec<_>>(); write!(f, "{}", opts.join(" ")) } } #[derive(Debug, Default, PartialEq, Clone)] pub struct BondConfig { pub mode: BondMode, pub options: BondOptions, } impl TryFrom<ConnectionConfig> for BondConfig { type Error = NetworkStateError; fn try_from(value: ConnectionConfig) -> Result<Self, Self::Error> { match value { ConnectionConfig::Bond(config) => Ok(config), _ => Err(NetworkStateError::UnexpectedConfiguration), } } } #[derive(Debug, Default, PartialEq, Clone)] pub struct BridgeConfig { pub stp: bool, pub priority: Option<u32>, pub forward_delay: Option<u32>, pub hello_time: Option<u32>, pub max_age: Option<u32>, pub ageing_time: Option<u32>, } #[derive(Debug, Default, PartialEq, Clone)] pub struct BridgePortConfig { pub priority: Option<u32>, pub path_cost: Option<u32>, } #[derive(Default, Debug, PartialEq, Clone)] pub struct InfinibandConfig { pub p_key: Option<i32>, pub parent: Option<String>, pub transport_mode: InfinibandTransportMode, } #[derive(Default, Debug, PartialEq, Clone)] pub enum InfinibandTransportMode { #[default] Datagram, Connected, } #[derive(Debug, Error)] #[error("Invalid infiniband transport-mode: {0}")] pub struct InvalidInfinibandTransportMode(String); impl FromStr for InfinibandTransportMode { type Err = InvalidInfinibandTransportMode; fn from_str(s: &str) -> Result<Self, Self::Err> { match s { "datagram" => Ok(Self::Datagram), "connected" => Ok(Self::Connected), _ => Err(InvalidInfinibandTransportMode(s.to_string())), } } } impl fmt::Display for InfinibandTransportMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match &self { InfinibandTransportMode::Datagram => "datagram", InfinibandTransportMode::Connected => "connected", }; write!(f, "{}", name) } } 07070100000084000081A40000000000000000000000016625710300000468000000000000000000000000000000000000005900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/model/builder.rsuse super::{Connection, DeviceType}; use uuid::Uuid; #[derive(Debug, Default)] pub struct ConnectionBuilder { id: String, interface: Option<String>, controller: Option<Uuid>, type_: Option<DeviceType>, } impl ConnectionBuilder { pub fn new(id: &str) -> Self { Self { id: id.to_string(), ..Default::default() } } pub fn with_interface(mut self, interface: &str) -> Self { self.interface = Some(interface.to_string()); self } pub fn with_controller(mut self, controller: Uuid) -> Self { self.controller = Some(controller); self } pub fn with_type(mut self, type_: DeviceType) -> Self { self.type_ = Some(type_); self } pub fn build(self) -> Connection { let mut conn = Connection::new(self.id, self.type_.unwrap_or(DeviceType::Ethernet)); if let Some(interface) = self.interface { conn.set_interface(&interface); } if let Some(controller) = self.controller { conn.controller = Some(controller) } conn } } 07070100000085000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/nm07070100000086000081A400000000000000000000000166257103000001DE000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/nm.rs//! Support for interacting with [NetworkManager](https://networkmanager.dev/). //! //! This module defines [a NetworkManager client](client::NetworkManagerClient) and a set of //! structs and enums to work with NetworkManager configuration. It is intended to be used //! internally, so the API is focused on Agama's use cases. mod adapter; mod client; mod dbus; mod error; mod model; mod proxies; pub use adapter::NetworkManagerAdapter; pub use client::NetworkManagerClient; 07070100000087000081A40000000000000000000000016625710300000F4F000000000000000000000000000000000000005600000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/nm/adapter.rsuse crate::network::{ model::{Connection, NetworkState}, nm::NetworkManagerClient, Adapter, NetworkAdapterError, }; use agama_lib::error::ServiceError; use async_trait::async_trait; use log; /// An adapter for NetworkManager pub struct NetworkManagerAdapter<'a> { client: NetworkManagerClient<'a>, } impl<'a> NetworkManagerAdapter<'a> { /// Returns the adapter for system's NetworkManager. pub async fn from_system() -> Result<NetworkManagerAdapter<'a>, ServiceError> { let client = NetworkManagerClient::from_system().await?; Ok(Self { client }) } /// Determines whether the write operation is supported for a connection /// /// * `conn`: connection fn is_writable(conn: &Connection) -> bool { !conn.is_loopback() } } #[async_trait] impl<'a> Adapter for NetworkManagerAdapter<'a> { async fn read(&self) -> Result<NetworkState, NetworkAdapterError> { let devices = self .client .devices() .await .map_err(NetworkAdapterError::Read)?; let connections = self .client .connections() .await .map_err(NetworkAdapterError::Read)?; Ok(NetworkState::new(devices, connections)) } /// Writes the connections to NetworkManager. /// /// Internally, it creates an ordered list of connections before processing them. The reason is /// that using async recursive functions is giving us some troubles, so we decided to go with a /// simpler approach. /// /// * `network`: network model. async fn write(&self, network: &NetworkState) -> Result<(), NetworkAdapterError> { let old_state = self.read().await?; let checkpoint = self .client .create_checkpoint() .await .map_err(NetworkAdapterError::Checkpoint)?; for conn in ordered_connections(network) { if !Self::is_writable(conn) { continue; } if let Some(old_conn) = old_state.get_connection_by_uuid(conn.uuid) { if old_conn == conn { continue; } } log::info!("Updating connection {} ({})", conn.id, conn.uuid); let result = if conn.is_removed() { self.client.remove_connection(conn.uuid).await } else { let ctrl = conn .controller .and_then(|uuid| network.get_connection_by_uuid(uuid)); self.client.add_or_update_connection(conn, ctrl).await }; if let Err(e) = result { self.client .rollback_checkpoint(&checkpoint.as_ref()) .await .map_err(NetworkAdapterError::Checkpoint)?; log::error!("Could not process the connection {}: {}", conn.id, &e); return Err(NetworkAdapterError::Write(e)); } } self.client .destroy_checkpoint(&checkpoint.as_ref()) .await .map_err(NetworkAdapterError::Checkpoint)?; Ok(()) } } /// Returns the connections in the order they should be processed. /// /// * `network`: network model. fn ordered_connections(network: &NetworkState) -> Vec<&Connection> { let mut conns: Vec<&Connection> = vec![]; for conn in &network.connections { add_ordered_connections(conn, network, &mut conns); } conns } fn add_ordered_connections<'b>( conn: &'b Connection, network: &'b NetworkState, conns: &mut Vec<&'b Connection>, ) { if let Some(uuid) = conn.controller { let controller = network.get_connection_by_uuid(uuid).unwrap(); add_ordered_connections(controller, network, conns); } if !conns.contains(&conn) { conns.push(conn); } } 07070100000088000081A40000000000000000000000016625710300001EDA000000000000000000000000000000000000005500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/nm/client.rs//! NetworkManager client. use std::collections::HashMap; use super::dbus::{ cleanup_dbus_connection, connection_from_dbus, connection_to_dbus, controller_from_dbus, merge_dbus_connections, }; use super::model::NmDeviceType; use super::proxies::{ConnectionProxy, DeviceProxy, NetworkManagerProxy, SettingsProxy}; use crate::network::model::{Connection, Device}; use agama_lib::error::ServiceError; use log; use uuid::Uuid; use zbus; use zbus::zvariant::{ObjectPath, OwnedObjectPath}; /// Simplified NetworkManager D-Bus client. /// /// Implements a minimal API to be used internally. At this point, it allows to query the list of /// network devices and connections, converting them to its own data types. pub struct NetworkManagerClient<'a> { connection: zbus::Connection, nm_proxy: NetworkManagerProxy<'a>, } impl<'a> NetworkManagerClient<'a> { /// Creates a NetworkManagerClient connecting to the system bus. pub async fn from_system() -> Result<NetworkManagerClient<'a>, ServiceError> { let connection = zbus::Connection::system().await?; Self::new(connection).await } /// Creates a NetworkManagerClient using the given D-Bus connection. /// /// * `connection`: D-Bus connection. pub async fn new( connection: zbus::Connection, ) -> Result<NetworkManagerClient<'a>, ServiceError> { Ok(Self { nm_proxy: NetworkManagerProxy::new(&connection).await?, connection, }) } /// Returns the list of network devices. pub async fn devices(&self) -> Result<Vec<Device>, ServiceError> { let mut devs = vec![]; for path in &self.nm_proxy.get_devices().await? { let proxy = DeviceProxy::builder(&self.connection) .path(path.as_str())? .build() .await?; let device_name = proxy.interface().await?; let device_type = NmDeviceType(proxy.device_type().await?); if let Ok(device_type) = device_type.try_into() { devs.push(Device { name: device_name, type_: device_type, }); } else { // TODO: use a logger log::warn!( "Ignoring network device '{}' (unsupported type '{}')", &device_name, &device_type ); } } Ok(devs) } /// Returns the list of network connections. pub async fn connections(&self) -> Result<Vec<Connection>, ServiceError> { let mut controlled_by: HashMap<Uuid, String> = HashMap::new(); let mut uuids_map: HashMap<String, Uuid> = HashMap::new(); let proxy = SettingsProxy::new(&self.connection).await?; let paths = proxy.list_connections().await?; let mut connections: Vec<Connection> = Vec::with_capacity(paths.len()); for path in paths { let proxy = ConnectionProxy::builder(&self.connection) .path(path.as_str())? .build() .await?; let settings = proxy.get_settings().await?; if let Some(connection) = connection_from_dbus(settings.clone()) { if let Some(controller) = controller_from_dbus(&settings) { controlled_by.insert(connection.uuid, controller.to_string()); } if let Some(iname) = &connection.interface { uuids_map.insert(iname.to_string(), connection.uuid); } connections.push(connection); } } for conn in connections.iter_mut() { let Some(interface_name) = controlled_by.get(&conn.uuid) else { continue; }; if let Some(uuid) = uuids_map.get(interface_name) { conn.controller = Some(*uuid); } else { log::warn!( "Could not found a connection for the interface '{}' (required by connection '{}')", interface_name, conn.id ); } } Ok(connections) } /// Adds or updates a connection if it already exists. /// /// * `conn`: connection to add or update. pub async fn add_or_update_connection( &self, conn: &Connection, controller: Option<&Connection>, ) -> Result<(), ServiceError> { let mut new_conn = connection_to_dbus(conn, controller); let path = if let Ok(proxy) = self.get_connection_proxy(conn.uuid).await { let original = proxy.get_settings().await?; let merged = merge_dbus_connections(&original, &new_conn); proxy.update(merged).await?; OwnedObjectPath::from(proxy.path().to_owned()) } else { let proxy = SettingsProxy::new(&self.connection).await?; cleanup_dbus_connection(&mut new_conn); proxy.add_connection(new_conn).await? }; if conn.is_up() { self.activate_connection(path).await?; } else { self.deactivate_connection(path).await?; } Ok(()) } /// Removes a network connection. pub async fn remove_connection(&self, uuid: Uuid) -> Result<(), ServiceError> { let proxy = self.get_connection_proxy(uuid).await?; proxy.delete().await?; Ok(()) } /// Creates a checkpoint. pub async fn create_checkpoint(&self) -> Result<OwnedObjectPath, ServiceError> { let path = self.nm_proxy.checkpoint_create(&[], 0, 0).await?; Ok(path) } /// Destroys a checkpoint. /// /// * `checkpoint`: checkpoint's D-Bus path. pub async fn destroy_checkpoint( &self, checkpoint: &ObjectPath<'_>, ) -> Result<(), ServiceError> { self.nm_proxy.checkpoint_destroy(checkpoint).await?; Ok(()) } /// Rolls the configuration back to the given checkpoint. /// /// * `checkpoint`: checkpoint's D-Bus path. pub async fn rollback_checkpoint( &self, checkpoint: &ObjectPath<'_>, ) -> Result<(), ServiceError> { self.nm_proxy.checkpoint_rollback(checkpoint).await?; Ok(()) } /// Activates a NetworkManager connection. /// /// * `path`: D-Bus patch of the connection. async fn activate_connection(&self, path: OwnedObjectPath) -> Result<(), ServiceError> { let proxy = NetworkManagerProxy::new(&self.connection).await?; let root = ObjectPath::try_from("/").unwrap(); proxy .activate_connection(&path.as_ref(), &root, &root) .await?; Ok(()) } /// Deactivates a NetworkManager connection. /// /// * `path`: D-Bus patch of the connection. async fn deactivate_connection(&self, path: OwnedObjectPath) -> Result<(), ServiceError> { let proxy = NetworkManagerProxy::new(&self.connection).await?; match proxy.deactivate_connection(&path.as_ref()).await { Err(e) => { // Ignore ConnectionNotActive error since this just means the state is already correct if e.to_string().contains("ConnectionNotActive") { Ok(()) } else { Err(ServiceError::DBus(e)) } } _ => Ok(()), } } async fn get_connection_proxy(&self, uuid: Uuid) -> Result<ConnectionProxy, ServiceError> { let proxy = SettingsProxy::new(&self.connection).await?; let uuid_s = uuid.to_string(); let path = proxy.get_connection_by_uuid(uuid_s.as_str()).await?; let proxy = ConnectionProxy::builder(&self.connection) .path(path)? .build() .await?; Ok(proxy) } } 07070100000089000081A4000000000000000000000001662571030000CF78000000000000000000000000000000000000005300000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/nm/dbus.rs//! This module implements some functions to convert from/to D-Bus types //! //! Working with hash maps coming from D-Bus is rather tedious and it is even worse when working //! with nested hash maps (see [NestedHash] and [OwnedNestedHash]). use super::model::*; use crate::network::model::*; use agama_lib::{ dbus::{NestedHash, OwnedNestedHash}, network::types::{BondMode, SSID}, }; use cidr::IpInet; use macaddr::MacAddr6; use std::{collections::HashMap, net::IpAddr, str::FromStr}; use uuid::Uuid; use zbus::zvariant::{self, OwnedValue, Value}; const ETHERNET_KEY: &str = "802-3-ethernet"; const BOND_KEY: &str = "bond"; const WIRELESS_KEY: &str = "802-11-wireless"; const WIRELESS_SECURITY_KEY: &str = "802-11-wireless-security"; const LOOPBACK_KEY: &str = "loopback"; const DUMMY_KEY: &str = "dummy"; const VLAN_KEY: &str = "vlan"; const BRIDGE_KEY: &str = "bridge"; const BRIDGE_PORT_KEY: &str = "bridge-port"; const INFINIBAND_KEY: &str = "infiniband"; /// Converts a connection struct into a HashMap that can be sent over D-Bus. /// /// * `conn`: Connection to convert. pub fn connection_to_dbus<'a>( conn: &'a Connection, controller: Option<&'a Connection>, ) -> NestedHash<'a> { let mut result = NestedHash::new(); let mut connection_dbus = HashMap::from([ ("id", conn.id.as_str().into()), ("type", ETHERNET_KEY.into()), ]); if let Some(interface) = &conn.interface { connection_dbus.insert("interface-name", interface.to_owned().into()); } if let Some(controller) = controller { let slave_type = match controller.config { ConnectionConfig::Bond(_) => BOND_KEY, ConnectionConfig::Bridge(_) => BRIDGE_KEY, _ => { log::error!("Controller {} has unhandled config type", controller.id); "" } }; connection_dbus.insert("slave-type", slave_type.into()); let master = controller .interface .as_deref() .unwrap_or(controller.id.as_str()); connection_dbus.insert("master", master.into()); } else { connection_dbus.insert("slave-type", "".into()); connection_dbus.insert("master", "".into()); } if let Some(zone) = &conn.firewall_zone { connection_dbus.insert("zone", zone.into()); } result.insert("ipv4", ip_config_to_ipv4_dbus(&conn.ip_config)); result.insert("ipv6", ip_config_to_ipv6_dbus(&conn.ip_config)); result.insert("match", match_config_to_dbus(&conn.match_config)); if conn.is_ethernet() { let ethernet_config = HashMap::from([( "assigned-mac-address", Value::new(conn.mac_address.to_string()), )]); result.insert(ETHERNET_KEY, ethernet_config); } match &conn.config { ConnectionConfig::Wireless(wireless) => { connection_dbus.insert("type", WIRELESS_KEY.into()); let wireless_dbus = wireless_config_to_dbus(wireless, &conn.mac_address); result.extend(wireless_dbus); } ConnectionConfig::Bond(bond) => { connection_dbus.insert("type", BOND_KEY.into()); if !connection_dbus.contains_key("interface-name") { connection_dbus.insert("interface-name", conn.id.as_str().into()); } result.insert(BOND_KEY, bond_config_to_dbus(bond)); } ConnectionConfig::Dummy => { connection_dbus.insert("type", DUMMY_KEY.into()); } ConnectionConfig::Vlan(vlan) => { connection_dbus.insert("type", VLAN_KEY.into()); result.extend(vlan_config_to_dbus(vlan)); } ConnectionConfig::Bridge(bridge) => { connection_dbus.insert("type", BRIDGE_KEY.into()); result.insert(BRIDGE_KEY, bridge_config_to_dbus(bridge)); } ConnectionConfig::Infiniband(infiniband) => { connection_dbus.insert("type", INFINIBAND_KEY.into()); result.insert(INFINIBAND_KEY, infiniband_config_to_dbus(infiniband)); } _ => {} } match &conn.port_config { PortConfig::Bridge(bridge_port) => { result.insert(BRIDGE_PORT_KEY, bridge_port_config_to_dbus(bridge_port)); } PortConfig::None => {} } result.insert("connection", connection_dbus); result } /// Converts an OwnedNestedHash from D-Bus into a Connection. /// /// This functions tries to turn a OwnedHashMap coming from D-Bus into a Connection. pub fn connection_from_dbus(conn: OwnedNestedHash) -> Option<Connection> { let mut connection = base_connection_from_dbus(&conn)?; if let Some(bridge_port_config) = bridge_port_config_from_dbus(&conn) { connection.port_config = PortConfig::Bridge(bridge_port_config); } if let Some(wireless_config) = wireless_config_from_dbus(&conn) { connection.config = ConnectionConfig::Wireless(wireless_config); return Some(connection); } if let Some(bond_config) = bond_config_from_dbus(&conn) { connection.config = ConnectionConfig::Bond(bond_config); return Some(connection); } if let Some(vlan_config) = vlan_config_from_dbus(&conn) { connection.config = ConnectionConfig::Vlan(vlan_config); return Some(connection); } if let Some(bridge_config) = bridge_config_from_dbus(&conn) { connection.config = ConnectionConfig::Bridge(bridge_config); return Some(connection); } if let Some(infiniband_config) = infiniband_config_from_dbus(&conn) { connection.config = ConnectionConfig::Infiniband(infiniband_config); return Some(connection); } if conn.get(DUMMY_KEY).is_some() { connection.config = ConnectionConfig::Dummy; return Some(connection); }; if conn.get(LOOPBACK_KEY).is_some() { connection.config = ConnectionConfig::Loopback; return Some(connection); }; if conn.get(ETHERNET_KEY).is_some() { return Some(connection); }; None } /// Merges a NestedHash and an OwnedNestedHash connections. /// /// Only the top-level sections that are present in the `original` hash are considered for update. /// /// * `original`: original hash coming from D-Bus. /// * `updated`: updated hash to write to D-Bus. pub fn merge_dbus_connections<'a>( original: &'a OwnedNestedHash, updated: &'a NestedHash, ) -> NestedHash<'a> { let mut merged = HashMap::with_capacity(original.len()); for (key, orig_section) in original { let mut inner: HashMap<&str, zbus::zvariant::Value> = HashMap::with_capacity(orig_section.len()); for (inner_key, value) in orig_section { inner.insert(inner_key.as_str(), value.into()); } if let Some(upd_section) = updated.get(key.as_str()) { for (inner_key, value) in upd_section { inner.insert(inner_key, value.clone()); } } merged.insert(key.as_str(), inner); } cleanup_dbus_connection(&mut merged); merged } /// Cleans up the NestedHash that represents a connection. /// /// By now it just removes the "addresses" key from the "ipv4" and "ipv6" objects, which is /// replaced with "address-data". However, if "addresses" is present, it takes precedence. /// /// * `conn`: connection represented as a NestedHash. pub fn cleanup_dbus_connection(conn: &mut NestedHash) { if let Some(connection) = conn.get_mut("connection") { if connection.get("interface-name").is_some_and(is_empty_value) { connection.remove("interface-name"); } if connection.get("master").is_some_and(is_empty_value) { connection.remove("master"); } if connection.get("slave-type").is_some_and(is_empty_value) { connection.remove("slave-type"); } } if let Some(ipv4) = conn.get_mut("ipv4") { ipv4.remove("addresses"); ipv4.remove("dns"); if ipv4.get("address-data").is_some_and(is_empty_value) { ipv4.remove("gateway"); } } if let Some(ipv6) = conn.get_mut("ipv6") { ipv6.remove("addresses"); ipv6.remove("dns"); if ipv6.get("address-data").is_some_and(is_empty_value) { ipv6.remove("gateway"); } } } /// Ancillary function to get the controller for a given interface. pub fn controller_from_dbus(conn: &OwnedNestedHash) -> Option<String> { let Some(connection) = conn.get("connection") else { return None; }; let master: &str = connection.get("master")?.downcast_ref()?; Some(master.to_string()) } fn ip_config_to_ipv4_dbus(ip_config: &IpConfig) -> HashMap<&str, zvariant::Value> { let addresses: Vec<HashMap<&str, Value>> = ip_config .addresses .iter() .filter(|ip| ip.is_ipv4()) .map(|ip| { HashMap::from([ ("address", Value::new(ip.address().to_string())), ("prefix", Value::new(ip.network_length() as u32)), ]) }) .collect(); let address_data: Value = addresses.into(); let dns_data: Value = ip_config .nameservers .iter() .filter(|ip| ip.is_ipv4()) .map(|ns| ns.to_string()) .collect::<Vec<_>>() .into(); let mut ipv4_dbus = HashMap::from([ ("address-data", address_data), ("dns-data", dns_data), ("method", ip_config.method4.to_string().into()), ]); if let Some(routes4) = &ip_config.routes4 { ipv4_dbus.insert( "route-data", routes4 .iter() .map(|route| route.into()) .collect::<Vec<HashMap<&str, Value>>>() .into(), ); } if let Some(gateway) = &ip_config.gateway4 { ipv4_dbus.insert("gateway", gateway.to_string().into()); } ipv4_dbus } fn ip_config_to_ipv6_dbus(ip_config: &IpConfig) -> HashMap<&str, zvariant::Value> { let addresses: Vec<HashMap<&str, Value>> = ip_config .addresses .iter() .filter(|ip| ip.is_ipv6()) .map(|ip| { HashMap::from([ ("address", Value::new(ip.address().to_string())), ("prefix", Value::new(ip.network_length() as u32)), ]) }) .collect(); let address_data: Value = addresses.into(); let dns_data: Value = ip_config .nameservers .iter() .filter(|ip| ip.is_ipv6()) .map(|ns| ns.to_string()) .collect::<Vec<_>>() .into(); let mut ipv6_dbus = HashMap::from([ ("address-data", address_data), ("dns-data", dns_data), ("method", ip_config.method6.to_string().into()), ]); if let Some(routes6) = &ip_config.routes6 { ipv6_dbus.insert( "route-data", routes6 .iter() .map(|route| route.into()) .collect::<Vec<HashMap<&str, Value>>>() .into(), ); } if let Some(gateway) = &ip_config.gateway6 { ipv6_dbus.insert("gateway", gateway.to_string().into()); } ipv6_dbus } fn wireless_config_to_dbus<'a>( config: &'a WirelessConfig, mac_address: &MacAddress, ) -> NestedHash<'a> { let mut wireless: HashMap<&str, zvariant::Value> = HashMap::from([ ("mode", Value::new(config.mode.to_string())), ("ssid", Value::new(config.ssid.to_vec())), ("assigned-mac-address", Value::new(mac_address.to_string())), ("hidden", Value::new(config.hidden)), ]); if let Some(band) = &config.band { wireless.insert("band", band.to_string().into()); if let Some(channel) = config.channel { wireless.insert("channel", channel.into()); } } if let Some(bssid) = &config.bssid { wireless.insert("bssid", bssid.as_bytes().into()); } let mut security: HashMap<&str, zvariant::Value> = HashMap::from([("key-mgmt", config.security.to_string().into())]); if let Some(password) = &config.password { security.insert("psk", password.to_string().into()); } if let Some(wep_security) = &config.wep_security { security.insert( "wep-key-type", (wep_security.wep_key_type.clone() as u32).into(), ); security.insert("auth-alg", wep_security.auth_alg.to_string().into()); for (i, wep_key) in wep_security.keys.clone().into_iter().enumerate() { security.insert( // FIXME: lifetimes are fun if i == 0 { "wep-key0" } else if i == 1 { "wep-key1" } else if i == 2 { "wep-key2" } else if i == 3 { "wep-key3" } else { break; }, wep_key.into(), ); } security.insert("wep-tx-keyidx", wep_security.wep_key_index.into()); } NestedHash::from([(WIRELESS_KEY, wireless), (WIRELESS_SECURITY_KEY, security)]) } fn bond_config_to_dbus(config: &BondConfig) -> HashMap<&str, zvariant::Value> { let mut options = config.options.0.clone(); options.insert("mode".to_string(), config.mode.to_string()); HashMap::from([("options", Value::new(options))]) } fn bridge_config_to_dbus(bridge: &BridgeConfig) -> HashMap<&str, zvariant::Value> { let mut hash = HashMap::new(); hash.insert("stp", bridge.stp.into()); if let Some(prio) = bridge.priority { hash.insert("priority", prio.into()); } if let Some(fwd_delay) = bridge.forward_delay { hash.insert("forward-delay", fwd_delay.into()); } if let Some(hello_time) = bridge.hello_time { hash.insert("hello-time", hello_time.into()); } if let Some(max_age) = bridge.max_age { hash.insert("max-age", max_age.into()); } if let Some(ageing_time) = bridge.ageing_time { hash.insert("ageing-time", ageing_time.into()); } hash } fn bridge_config_from_dbus(conn: &OwnedNestedHash) -> Option<BridgeConfig> { let Some(bridge) = conn.get(BRIDGE_KEY) else { return None; }; let Some(stp) = bridge.get("stp") else { return None; }; let mut bc = BridgeConfig { stp: *stp.downcast_ref::<bool>()?, ..Default::default() }; if let Some(prio) = bridge.get("priority") { bc.priority = Some(*prio.downcast_ref::<u32>()?); } if let Some(fwd_delay) = bridge.get("forward-delay") { bc.forward_delay = Some(*fwd_delay.downcast_ref::<u32>()?); } if let Some(hello_time) = bridge.get("hello-time") { bc.hello_time = Some(*hello_time.downcast_ref::<u32>()?); } if let Some(max_age) = bridge.get("max-age") { bc.max_age = Some(*max_age.downcast_ref::<u32>()?); } if let Some(ageing_time) = bridge.get("ageing-time") { bc.ageing_time = Some(*ageing_time.downcast_ref::<u32>()?); } Some(bc) } fn bridge_port_config_to_dbus(bridge_port: &BridgePortConfig) -> HashMap<&str, zvariant::Value> { let mut hash = HashMap::new(); if let Some(prio) = bridge_port.priority { hash.insert("priority", prio.into()); } if let Some(pc) = bridge_port.path_cost { hash.insert("path-cost", pc.into()); } hash } fn bridge_port_config_from_dbus(conn: &OwnedNestedHash) -> Option<BridgePortConfig> { let Some(bridge_port) = conn.get(BRIDGE_PORT_KEY) else { return None; }; let mut bpc = BridgePortConfig::default(); if let Some(prio) = bridge_port.get("priority") { bpc.priority = Some(*prio.downcast_ref::<u32>()?); } if let Some(path_cost) = bridge_port.get("path_cost") { bpc.path_cost = Some(*path_cost.downcast_ref::<u32>()?); } Some(bpc) } fn infiniband_config_to_dbus(config: &InfinibandConfig) -> HashMap<&str, zvariant::Value> { let mut infiniband_config: HashMap<&str, zvariant::Value> = HashMap::from([ ( "transport-mode", Value::new(config.transport_mode.to_string()), ), ("p-key", Value::new(config.p_key.unwrap_or(-1))), ]); if let Some(parent) = &config.parent { infiniband_config.insert("parent", parent.into()); } infiniband_config } fn infiniband_config_from_dbus(conn: &OwnedNestedHash) -> Option<InfinibandConfig> { let Some(infiniband) = conn.get(INFINIBAND_KEY) else { return None; }; let mut infiniband_config = InfinibandConfig::default(); if let Some(p_key) = infiniband.get("p-key") { infiniband_config.p_key = Some(*p_key.downcast_ref::<i32>()?); } if let Some(parent) = infiniband.get("parent") { infiniband_config.parent = Some(parent.downcast_ref::<str>()?.to_string()); } if let Some(transport_mode) = infiniband.get("transport-mode") { infiniband_config.transport_mode = InfinibandTransportMode::from_str(transport_mode.downcast_ref::<str>()?).ok()?; } Some(infiniband_config) } /// Converts a MatchConfig struct into a HashMap that can be sent over D-Bus. /// /// * `match_config`: MatchConfig to convert. fn match_config_to_dbus(match_config: &MatchConfig) -> HashMap<&str, zvariant::Value> { let drivers: Value = match_config.driver.to_vec().into(); let kernels: Value = match_config.kernel.to_vec().into(); let paths: Value = match_config.path.to_vec().into(); let interfaces: Value = match_config.interface.to_vec().into(); HashMap::from([ ("driver", drivers), ("kernel-command-line", kernels), ("path", paths), ("interface-name", interfaces), ]) } fn base_connection_from_dbus(conn: &OwnedNestedHash) -> Option<Connection> { let Some(connection) = conn.get("connection") else { return None; }; let id: &str = connection.get("id")?.downcast_ref()?; let uuid: &str = connection.get("uuid")?.downcast_ref()?; let uuid: Uuid = uuid.try_into().ok()?; let mut base_connection = Connection { id: id.to_string(), uuid, ..Default::default() }; if let Some(interface) = connection.get("interface-name") { let interface: &str = interface.downcast_ref()?; base_connection.interface = Some(interface.to_string()); } if let Some(match_config) = conn.get("match") { base_connection.match_config = match_config_from_dbus(match_config)?; } if let Some(zone) = connection.get("zone") { let zone: &str = zone.downcast_ref()?; base_connection.firewall_zone = Some(zone.to_string()); } if let Some(ethernet_config) = conn.get(ETHERNET_KEY) { base_connection.mac_address = mac_address_from_dbus(ethernet_config)?; } else if let Some(wireless_config) = conn.get(WIRELESS_KEY) { base_connection.mac_address = mac_address_from_dbus(wireless_config)?; } base_connection.ip_config = ip_config_from_dbus(conn)?; Some(base_connection) } fn mac_address_from_dbus(config: &HashMap<String, OwnedValue>) -> Option<MacAddress> { if let Some(mac_address) = config.get("assigned-mac-address") { match MacAddress::from_str(mac_address.downcast_ref::<str>()?) { Ok(mac) => Some(mac), Err(e) => { log::warn!("Couldn't parse MAC: {}", e); None } } } else { Some(MacAddress::Unset) } } fn match_config_from_dbus( match_config: &HashMap<String, zvariant::OwnedValue>, ) -> Option<MatchConfig> { let mut match_conf = MatchConfig::default(); if let Some(drivers) = match_config.get("driver") { let drivers = drivers.downcast_ref::<zbus::zvariant::Array>()?; for driver in drivers.get() { let driver: &str = driver.downcast_ref()?; match_conf.driver.push(driver.to_string()); } } if let Some(interface_names) = match_config.get("interface-name") { let interface_names = interface_names.downcast_ref::<zbus::zvariant::Array>()?; for name in interface_names.get() { let name: &str = name.downcast_ref()?; match_conf.interface.push(name.to_string()); } } if let Some(paths) = match_config.get("path") { let paths = paths.downcast_ref::<zbus::zvariant::Array>()?; for path in paths.get() { let path: &str = path.downcast_ref()?; match_conf.path.push(path.to_string()); } } if let Some(kernel_options) = match_config.get("kernel-command-line") { let options = kernel_options.downcast_ref::<zbus::zvariant::Array>()?; for option in options.get() { let option: &str = option.downcast_ref()?; match_conf.kernel.push(option.to_string()); } } Some(match_conf) } fn ip_config_from_dbus(conn: &OwnedNestedHash) -> Option<IpConfig> { let mut ip_config = IpConfig::default(); if let Some(ipv4) = conn.get("ipv4") { let method4: &str = ipv4.get("method")?.downcast_ref()?; ip_config.method4 = NmMethod(method4.to_string()).try_into().ok()?; let address_data = ipv4.get("address-data")?; let mut addresses = addresses_with_prefix_from_dbus(address_data)?; ip_config.addresses.append(&mut addresses); if let Some(dns_data) = ipv4.get("dns-data") { let mut servers = nameservers_from_dbus(dns_data)?; ip_config.nameservers.append(&mut servers); } if let Some(route_data) = ipv4.get("route-data") { ip_config.routes4 = routes_from_dbus(route_data); } if let Some(gateway) = ipv4.get("gateway") { let gateway: &str = gateway.downcast_ref()?; ip_config.gateway4 = Some(gateway.parse().unwrap()); } } if let Some(ipv6) = conn.get("ipv6") { let method6: &str = ipv6.get("method")?.downcast_ref()?; ip_config.method6 = NmMethod(method6.to_string()).try_into().ok()?; let address_data = ipv6.get("address-data")?; let mut addresses = addresses_with_prefix_from_dbus(address_data)?; ip_config.addresses.append(&mut addresses); if let Some(dns_data) = ipv6.get("dns-data") { let mut servers = nameservers_from_dbus(dns_data)?; ip_config.nameservers.append(&mut servers); } if let Some(route_data) = ipv6.get("route-data") { ip_config.routes6 = routes_from_dbus(route_data); } if let Some(gateway) = ipv6.get("gateway") { let gateway: &str = gateway.downcast_ref()?; ip_config.gateway6 = Some(gateway.parse().unwrap()); } } Some(ip_config) } fn addresses_with_prefix_from_dbus(address_data: &OwnedValue) -> Option<Vec<IpInet>> { let address_data = address_data.downcast_ref::<zbus::zvariant::Array>()?; let mut addresses: Vec<IpInet> = vec![]; for addr in address_data.get() { let dict = addr.downcast_ref::<zvariant::Dict>()?; let map = <HashMap<String, zvariant::Value<'_>>>::try_from(dict.clone()).unwrap(); let addr_str: &str = map.get("address")?.downcast_ref()?; let prefix: &u32 = map.get("prefix")?.downcast_ref()?; let prefix = *prefix as u8; let address = IpInet::new(addr_str.parse().unwrap(), prefix).ok()?; addresses.push(address) } Some(addresses) } fn routes_from_dbus(route_data: &OwnedValue) -> Option<Vec<IpRoute>> { let route_data = route_data.downcast_ref::<zbus::zvariant::Array>()?; let mut routes: Vec<IpRoute> = vec![]; for route in route_data.get() { let route_dict = route.downcast_ref::<zvariant::Dict>()?; let route_map = <HashMap<String, zvariant::Value<'_>>>::try_from(route_dict.clone()).ok()?; let dest_str: &str = route_map.get("dest")?.downcast_ref()?; let prefix: u8 = *route_map.get("prefix")?.downcast_ref::<u32>()? as u8; let destination = IpInet::new(dest_str.parse().unwrap(), prefix).ok()?; let mut new_route = IpRoute { destination, next_hop: None, metric: None, }; if let Some(next_hop) = route_map.get("next-hop") { let next_hop_str: &str = next_hop.downcast_ref()?; new_route.next_hop = Some(IpAddr::from_str(next_hop_str).unwrap()); } if let Some(metric) = route_map.get("metric") { let metric: u32 = *metric.downcast_ref()?; new_route.metric = Some(metric); } routes.push(new_route) } Some(routes) } fn nameservers_from_dbus(dns_data: &OwnedValue) -> Option<Vec<IpAddr>> { let dns_data = dns_data.downcast_ref::<zbus::zvariant::Array>()?; let mut servers: Vec<IpAddr> = vec![]; for server in dns_data.get() { let server: &str = server.downcast_ref()?; servers.push(server.parse().unwrap()); } Some(servers) } fn wireless_config_from_dbus(conn: &OwnedNestedHash) -> Option<WirelessConfig> { let Some(wireless) = conn.get(WIRELESS_KEY) else { return None; }; let mode: &str = wireless.get("mode")?.downcast_ref()?; let ssid = wireless.get("ssid")?; let ssid: &zvariant::Array = ssid.downcast_ref()?; let ssid: Vec<u8> = ssid .get() .iter() .map(|u| *u.downcast_ref::<u8>().unwrap()) .collect(); let mut wireless_config = WirelessConfig { mode: NmWirelessMode(mode.to_string()).try_into().ok()?, ssid: SSID(ssid), ..Default::default() }; if let Some(band) = wireless.get("band") { wireless_config.band = Some(band.downcast_ref::<str>()?.try_into().ok()?) } if let Some(channel) = wireless.get("channel") { wireless_config.channel = Some(*channel.downcast_ref()?); } if let Some(bssid) = wireless.get("bssid") { let bssid: &zvariant::Array = bssid.downcast_ref()?; let bssid: Vec<u8> = bssid .get() .iter() .map(|u| *u.downcast_ref::<u8>().unwrap()) .collect(); wireless_config.bssid = Some(MacAddr6::new( *bssid.first()?, *bssid.get(1)?, *bssid.get(2)?, *bssid.get(3)?, *bssid.get(4)?, *bssid.get(5)?, )); } if let Some(hidden) = wireless.get("hidden") { wireless_config.hidden = *hidden.downcast_ref::<bool>()?; } if let Some(security) = conn.get(WIRELESS_SECURITY_KEY) { let key_mgmt: &str = security.get("key-mgmt")?.downcast_ref()?; wireless_config.security = NmKeyManagement(key_mgmt.to_string()).try_into().ok()?; let wep_key_type = security .get("wep-key-type") .and_then(|alg| WEPKeyType::try_from(*alg.downcast_ref::<u32>()?).ok()) .unwrap_or_default(); let auth_alg = security .get("auth-alg") .and_then(|alg| WEPAuthAlg::try_from(alg.downcast_ref()?).ok()) .unwrap_or_default(); let wep_key_index = security .get("wep-tx-keyidx") .and_then(|idx| idx.downcast_ref::<u32>().cloned()) .unwrap_or_default(); wireless_config.wep_security = Some(WEPSecurity { wep_key_type, auth_alg, wep_key_index, ..Default::default() }); } Some(wireless_config) } fn bond_config_from_dbus(conn: &OwnedNestedHash) -> Option<BondConfig> { let Some(bond) = conn.get(BOND_KEY) else { return None; }; let dict: &zvariant::Dict = bond.get("options")?.downcast_ref()?; let mut options = <HashMap<String, String>>::try_from(dict.clone()).unwrap(); let mode = options.remove("mode"); let mut bond = BondConfig { options: BondOptions(options), ..Default::default() }; if let Some(mode) = mode { bond.mode = BondMode::try_from(mode.as_str()).unwrap_or_default(); } Some(bond) } fn vlan_config_to_dbus(cfg: &VlanConfig) -> NestedHash { let vlan: HashMap<&str, zvariant::Value> = HashMap::from([ ("id", cfg.id.into()), ("parent", cfg.parent.clone().into()), ("protocol", cfg.protocol.to_string().into()), ]); NestedHash::from([("vlan", vlan)]) } fn vlan_config_from_dbus(conn: &OwnedNestedHash) -> Option<VlanConfig> { let Some(vlan) = conn.get(VLAN_KEY) else { return None; }; let Some(id) = vlan.get("id") else { return None; }; let id = id.downcast_ref::<u32>()?; let Some(parent) = vlan.get("parent") else { return None; }; let parent: &str = parent.downcast_ref()?; let protocol = match vlan.get("protocol") { Some(x) => { let x: &str = x.downcast_ref()?; VlanProtocol::from_str(x).unwrap_or_default() } _ => Default::default(), }; Some(VlanConfig { id: *id, parent: String::from(parent), protocol, }) } /// Determines whether a value is empty. /// /// TODO: Generalize for other kind of values, like dicts or arrays. /// /// * `value`: value to analyze fn is_empty_value(value: &zvariant::Value) -> bool { if let Some(value) = value.downcast_ref::<zvariant::Str>() { return value.is_empty(); } if let Some(value) = value.downcast_ref::<zvariant::Array>() { return value.is_empty(); } false } #[cfg(test)] mod test { use super::{ connection_from_dbus, connection_to_dbus, merge_dbus_connections, NestedHash, OwnedNestedHash, }; use crate::network::{ model::*, nm::dbus::{BOND_KEY, ETHERNET_KEY, INFINIBAND_KEY, WIRELESS_KEY, WIRELESS_SECURITY_KEY}, }; use agama_lib::network::types::{BondMode, SSID}; use cidr::IpInet; use std::{collections::HashMap, net::IpAddr, str::FromStr}; use uuid::Uuid; use zbus::zvariant::{self, Array, Dict, OwnedValue, Value}; #[test] fn test_connection_from_dbus() { let uuid = Uuid::new_v4().to_string(); let connection_section = HashMap::from([ ("id".to_string(), Value::new("eth0").to_owned()), ("uuid".to_string(), Value::new(uuid).to_owned()), ]); let address_v4_data = vec![HashMap::from([ ("address".to_string(), Value::new("192.168.0.10")), ("prefix".to_string(), Value::new(24_u32)), ])]; let route_v4_data = vec![HashMap::from([ ("dest".to_string(), Value::new("192.168.0.0")), ("prefix".to_string(), Value::new(24_u32)), ("next-hop".to_string(), Value::new("192.168.0.1")), ("metric".to_string(), Value::new(100_u32)), ])]; let ipv4_section = HashMap::from([ ("method".to_string(), Value::new("auto").to_owned()), ( "address-data".to_string(), Value::new(address_v4_data).to_owned(), ), ("gateway".to_string(), Value::new("192.168.0.1").to_owned()), ( "dns-data".to_string(), Value::new(vec!["192.168.0.2"]).to_owned(), ), ( "route-data".to_string(), Value::new(route_v4_data).to_owned(), ), ]); let address_v6_data = vec![HashMap::from([ ("address".to_string(), Value::new("::ffff:c0a8:10a")), ("prefix".to_string(), Value::new(24_u32)), ])]; let route_v6_data = vec![HashMap::from([ ("dest".to_string(), Value::new("2001:db8::")), ("prefix".to_string(), Value::new(64_u32)), ("next-hop".to_string(), Value::new("2001:db8::1")), ("metric".to_string(), Value::new(100_u32)), ])]; let ipv6_section = HashMap::from([ ("method".to_string(), Value::new("auto").to_owned()), ( "address-data".to_string(), Value::new(address_v6_data).to_owned(), ), ( "gateway".to_string(), Value::new("::ffff:c0a8:101").to_owned(), ), ( "dns-data".to_string(), Value::new(vec!["::ffff:c0a8:102"]).to_owned(), ), ( "route-data".to_string(), Value::new(route_v6_data).to_owned(), ), ]); let match_section = HashMap::from([( "kernel-command-line".to_string(), Value::new(vec!["pci-0000:00:19.0"]).to_owned(), )]); let dbus_conn = HashMap::from([ ("connection".to_string(), connection_section), ("ipv4".to_string(), ipv4_section), ("ipv6".to_string(), ipv6_section), ("match".to_string(), match_section), (ETHERNET_KEY.to_string(), build_ethernet_section_from_dbus()), ]); let connection = connection_from_dbus(dbus_conn).unwrap(); assert_eq!(connection.id, "eth0"); let ip_config = connection.ip_config; let match_config = connection.match_config; assert_eq!(match_config.kernel, vec!["pci-0000:00:19.0"]); assert_eq!(connection.mac_address.to_string(), "12:34:56:78:9A:BC"); assert_eq!( ip_config.addresses, vec![ "192.168.0.10/24".parse().unwrap(), "::ffff:c0a8:10a/24".parse().unwrap() ] ); assert_eq!( ip_config.nameservers, vec![ "192.168.0.2".parse::<IpAddr>().unwrap(), "::ffff:c0a8:102".parse::<IpAddr>().unwrap() ] ); assert_eq!(ip_config.method4, Ipv4Method::Auto); assert_eq!(ip_config.method6, Ipv6Method::Auto); assert_eq!( ip_config.routes4, Some(vec![IpRoute { destination: IpInet::new("192.168.0.0".parse().unwrap(), 24_u8).unwrap(), next_hop: Some(IpAddr::from_str("192.168.0.1").unwrap()), metric: Some(100) }]) ); assert_eq!( ip_config.routes6, Some(vec![IpRoute { destination: IpInet::new("2001:db8::".parse().unwrap(), 64_u8).unwrap(), next_hop: Some(IpAddr::from_str("2001:db8::1").unwrap()), metric: Some(100) }]) ); } #[test] fn test_connection_from_dbus_missing_connection() { let dbus_conn: HashMap<String, HashMap<String, OwnedValue>> = HashMap::new(); let connection = connection_from_dbus(dbus_conn); assert_eq!(connection, None); } #[test] fn test_connection_from_dbus_wireless() { let uuid = Uuid::new_v4().to_string(); let connection_section = HashMap::from([ ("id".to_string(), Value::new("wlan0").to_owned()), ("uuid".to_string(), Value::new(uuid).to_owned()), ]); let wireless_section = HashMap::from([ ("mode".to_string(), Value::new("infrastructure").to_owned()), ( "ssid".to_string(), Value::new("agama".as_bytes()).to_owned(), ), ( "assigned-mac-address".to_string(), Value::new("13:45:67:89:AB:CD").to_owned(), ), ("band".to_string(), Value::new("a").to_owned()), ("channel".to_string(), Value::new(32_u32).to_owned()), ( "bssid".to_string(), Value::new(vec![18_u8, 52_u8, 86_u8, 120_u8, 154_u8, 188_u8]).to_owned(), ), ("hidden".to_string(), Value::new(false).to_owned()), ]); let security_section = HashMap::from([ ("key-mgmt".to_string(), Value::new("wpa-psk").to_owned()), ( "wep-key-type".to_string(), Value::new(WEPKeyType::Key as u32).to_owned(), ), ("auth-alg".to_string(), Value::new("open").to_owned()), ("wep-tx-keyidx".to_string(), Value::new(1_u32).to_owned()), ]); let dbus_conn = HashMap::from([ ("connection".to_string(), connection_section), (WIRELESS_KEY.to_string(), wireless_section), (WIRELESS_SECURITY_KEY.to_string(), security_section), ]); let connection = connection_from_dbus(dbus_conn).unwrap(); assert_eq!(connection.mac_address.to_string(), "13:45:67:89:AB:CD"); assert!(matches!(connection.config, ConnectionConfig::Wireless(_))); if let ConnectionConfig::Wireless(wireless) = &connection.config { assert_eq!(wireless.ssid, SSID(vec![97, 103, 97, 109, 97])); assert_eq!(wireless.mode, WirelessMode::Infra); assert_eq!(wireless.security, SecurityProtocol::WPA2); assert_eq!(wireless.band, Some(WirelessBand::A)); assert_eq!(wireless.channel, Some(32_u32)); assert_eq!( wireless.bssid, Some(macaddr::MacAddr6::from_str("12:34:56:78:9A:BC").unwrap()) ); assert!(!wireless.hidden); let wep_security = wireless.wep_security.as_ref().unwrap(); assert_eq!(wep_security.wep_key_type, WEPKeyType::Key); assert_eq!(wep_security.auth_alg, WEPAuthAlg::Open); assert_eq!(wep_security.wep_key_index, 1); } } #[test] fn test_connection_from_dbus_bonding() { let uuid = Uuid::new_v4().to_string(); let connection_section = HashMap::from([ ("id".to_string(), Value::new("bond0").to_owned()), ("uuid".to_string(), Value::new(uuid).to_owned()), ]); let bond_options = Value::new(HashMap::from([( "options".to_string(), HashMap::from([("mode".to_string(), Value::new("active-backup").to_owned())]), )])); let dbus_conn = HashMap::from([ ("connection".to_string(), connection_section), (BOND_KEY.to_string(), bond_options.try_into().unwrap()), ]); let connection = connection_from_dbus(dbus_conn).unwrap(); if let ConnectionConfig::Bond(config) = connection.config { assert_eq!(config.mode, BondMode::ActiveBackup); } } #[test] fn test_connection_from_dbus_infiniband() { let uuid = Uuid::new_v4().to_string(); let connection_section = HashMap::from([ ("id".to_string(), Value::new("ib0").to_owned()), ("uuid".to_string(), Value::new(uuid).to_owned()), ]); let infiniband_section = HashMap::from([ ("p-key".to_string(), Value::new(0x8001 as i32).to_owned()), ("parent".to_string(), Value::new("ib0").to_owned()), ( "transport-mode".to_string(), Value::new("datagram").to_owned(), ), ]); let dbus_conn = HashMap::from([ ("connection".to_string(), connection_section), (INFINIBAND_KEY.to_string(), infiniband_section), ]); let connection = connection_from_dbus(dbus_conn).unwrap(); let ConnectionConfig::Infiniband(infiniband) = &connection.config else { panic!("Wrong connection type") }; assert_eq!(infiniband.p_key, Some(0x8001)); assert_eq!(infiniband.parent, Some("ib0".to_string())); assert_eq!(infiniband.transport_mode, InfinibandTransportMode::Datagram); } #[test] fn test_dbus_from_infiniband_connection() { let config = InfinibandConfig { p_key: Some(0x8002), parent: Some("ib1".to_string()), transport_mode: InfinibandTransportMode::Connected, }; let mut infiniband = build_base_connection(); infiniband.config = ConnectionConfig::Infiniband(config); let infiniband_dbus = connection_to_dbus(&infiniband, None); let infiniband = infiniband_dbus.get(INFINIBAND_KEY).unwrap(); let p_key: i32 = *infiniband.get("p-key").unwrap().downcast_ref().unwrap(); assert_eq!(p_key, 0x8002); let parent: &str = infiniband.get("parent").unwrap().downcast_ref().unwrap(); assert_eq!(parent, "ib1"); let transport_mode: &str = infiniband .get("transport-mode") .unwrap() .downcast_ref() .unwrap(); assert_eq!( transport_mode, InfinibandTransportMode::Connected.to_string() ); } #[test] fn test_dbus_from_wireless_connection() { let config = WirelessConfig { mode: WirelessMode::Infra, security: SecurityProtocol::WPA2, password: Some("wpa-password".to_string()), ssid: SSID(vec![97, 103, 97, 109, 97]), band: Some(WirelessBand::BG), channel: Some(10), bssid: Some(macaddr::MacAddr6::from_str("12:34:56:78:9A:BC").unwrap()), wep_security: Some(WEPSecurity { auth_alg: WEPAuthAlg::Open, wep_key_type: WEPKeyType::Key, wep_key_index: 1, keys: vec![ "5b73215e232f4c577c5073455d".to_string(), "hello".to_string(), ], }), hidden: true, ..Default::default() }; let mut wireless = build_base_connection(); wireless.config = ConnectionConfig::Wireless(config); let wireless_dbus = connection_to_dbus(&wireless, None); let wireless = wireless_dbus.get(WIRELESS_KEY).unwrap(); let mode: &str = wireless.get("mode").unwrap().downcast_ref().unwrap(); assert_eq!(mode, "infrastructure"); let mac_address: &str = wireless .get("assigned-mac-address") .unwrap() .downcast_ref() .unwrap(); assert_eq!(mac_address, "FD:CB:A9:87:65:43"); let ssid: &zvariant::Array = wireless.get("ssid").unwrap().downcast_ref().unwrap(); let ssid: Vec<u8> = ssid .get() .iter() .map(|u| *u.downcast_ref::<u8>().unwrap()) .collect(); assert_eq!(ssid, "agama".as_bytes()); let band: &str = wireless.get("band").unwrap().downcast_ref().unwrap(); assert_eq!(band, "bg"); let channel: u32 = *wireless.get("channel").unwrap().downcast_ref().unwrap(); assert_eq!(channel, 10); let bssid: &zvariant::Array = wireless.get("bssid").unwrap().downcast_ref().unwrap(); let bssid: Vec<u8> = bssid .get() .iter() .map(|u| *u.downcast_ref::<u8>().unwrap()) .collect(); assert_eq!(bssid, vec![18, 52, 86, 120, 154, 188]); let hidden: bool = *wireless.get("hidden").unwrap().downcast_ref().unwrap(); assert!(hidden); let security = wireless_dbus.get(WIRELESS_SECURITY_KEY).unwrap(); let key_mgmt: &str = security.get("key-mgmt").unwrap().downcast_ref().unwrap(); assert_eq!(key_mgmt, "wpa-psk"); let password: &str = security.get("psk").unwrap().downcast_ref().unwrap(); assert_eq!(password, "wpa-password"); let auth_alg: WEPAuthAlg = security .get("auth-alg") .unwrap() .downcast_ref::<str>() .unwrap() .try_into() .unwrap(); assert_eq!(auth_alg, WEPAuthAlg::Open); let wep_key_type: u32 = *security .get("wep-key-type") .unwrap() .downcast_ref::<u32>() .unwrap(); assert_eq!(wep_key_type, WEPKeyType::Key as u32); let wep_key_index: u32 = *security .get("wep-tx-keyidx") .unwrap() .downcast_ref() .unwrap(); assert_eq!(wep_key_index, 1); let wep_key0: &str = security.get("wep-key0").unwrap().downcast_ref().unwrap(); assert_eq!(wep_key0, "5b73215e232f4c577c5073455d"); let wep_key1: &str = security.get("wep-key1").unwrap().downcast_ref().unwrap(); assert_eq!(wep_key1, "hello"); } #[test] fn test_dbus_from_ethernet_connection() { let ethernet = build_base_connection(); let ethernet_dbus = connection_to_dbus(ðernet, None); check_dbus_base_connection(ðernet_dbus); } #[test] fn test_merge_dbus_connections() { let mut original = OwnedNestedHash::new(); let connection = HashMap::from([ ("id".to_string(), Value::new("conn0".to_string()).to_owned()), ( "type".to_string(), Value::new(ETHERNET_KEY.to_string()).to_owned(), ), ]); let ipv4 = HashMap::from([ ( "method".to_string(), Value::new("manual".to_string()).to_owned(), ), ( "gateway".to_string(), Value::new("192.168.1.1".to_string()).to_owned(), ), ( "addresses".to_string(), Value::new(vec!["192.168.1.1"]).to_owned(), ), ]); let ipv6 = HashMap::from([ ( "method".to_string(), Value::new("manual".to_string()).to_owned(), ), ( "gateway".to_string(), Value::new("::ffff:c0a8:101".to_string()).to_owned(), ), ( "addresses".to_string(), Value::new(vec!["::ffff:c0a8:102"]).to_owned(), ), ]); original.insert("connection".to_string(), connection); original.insert("ipv4".to_string(), ipv4); original.insert("ipv6".to_string(), ipv6); let ethernet = Connection { id: "agama".to_string(), interface: Some("eth0".to_string()), ..Default::default() }; let updated = connection_to_dbus(ðernet, None); let merged = merge_dbus_connections(&original, &updated); let connection = merged.get("connection").unwrap(); assert_eq!( *connection.get("id").unwrap(), Value::new("agama".to_string()) ); assert_eq!( *connection.get("interface-name").unwrap(), Value::new("eth0".to_string()) ); let ipv4 = merged.get("ipv4").unwrap(); assert_eq!( *ipv4.get("method").unwrap(), Value::new("disabled".to_string()) ); // there are not addresses ("address-data"), so no gateway is allowed assert!(ipv4.get("gateway").is_none()); assert!(ipv4.get("addresses").is_none()); let ipv6 = merged.get("ipv6").unwrap(); assert_eq!( *ipv6.get("method").unwrap(), Value::new("disabled".to_string()) ); // there are not addresses ("address-data"), so no gateway is allowed assert!(ipv6.get("gateway").is_none()); } #[test] fn test_merged_connections_are_clean() { let mut original = OwnedNestedHash::new(); let connection = HashMap::from([ ("id".to_string(), Value::new("conn0".to_string()).to_owned()), ( "type".to_string(), Value::new(ETHERNET_KEY.to_string()).to_owned(), ), ( "interface-name".to_string(), Value::new("eth0".to_string()).to_owned(), ), ]); let ethernet = HashMap::from([( "assigned-mac-address".to_string(), Value::new("12:34:56:78:9A:BC".to_string()).to_owned(), )]); original.insert("connection".to_string(), connection); original.insert(ETHERNET_KEY.to_string(), ethernet); let updated = Connection { interface: Some("".to_string()), mac_address: MacAddress::Unset, ..Default::default() }; let updated = connection_to_dbus(&updated, None); let merged = merge_dbus_connections(&original, &updated); let connection = merged.get("connection").unwrap(); assert_eq!(connection.get("interface-name"), None); let ethernet = merged.get(ETHERNET_KEY).unwrap(); assert_eq!(ethernet.get("assigned-mac-address"), Some(&Value::from(""))); } fn build_ethernet_section_from_dbus() -> HashMap<String, OwnedValue> { HashMap::from([ ("auto-negotiate".to_string(), true.into()), ( "assigned-mac-address".to_string(), Value::new("12:34:56:78:9A:BC").to_owned(), ), ]) } fn build_base_connection() -> Connection { let addresses = vec![ "192.168.0.2/24".parse().unwrap(), "::ffff:c0a8:2".parse().unwrap(), ]; let ip_config = IpConfig { addresses, gateway4: Some("192.168.0.1".parse().unwrap()), gateway6: Some("::ffff:c0a8:1".parse().unwrap()), routes4: Some(vec![IpRoute { destination: IpInet::new("192.168.0.0".parse().unwrap(), 24_u8).unwrap(), next_hop: Some(IpAddr::from_str("192.168.0.1").unwrap()), metric: Some(100), }]), routes6: Some(vec![IpRoute { destination: IpInet::new("2001:db8::".parse().unwrap(), 64_u8).unwrap(), next_hop: Some(IpAddr::from_str("2001:db8::1").unwrap()), metric: Some(100), }]), ..Default::default() }; let mac_address = MacAddress::from_str("FD:CB:A9:87:65:43").unwrap(); Connection { id: "agama".to_string(), ip_config, mac_address, ..Default::default() } } fn check_dbus_base_connection(conn_dbus: &NestedHash) { let connection_dbus = conn_dbus.get("connection").unwrap(); let id: &str = connection_dbus.get("id").unwrap().downcast_ref().unwrap(); assert_eq!(id, "agama"); let ethernet_connection = conn_dbus.get(ETHERNET_KEY).unwrap(); let mac_address: &str = ethernet_connection .get("assigned-mac-address") .unwrap() .downcast_ref() .unwrap(); assert_eq!(mac_address, "FD:CB:A9:87:65:43"); let ipv4_dbus = conn_dbus.get("ipv4").unwrap(); let gateway4: &str = ipv4_dbus.get("gateway").unwrap().downcast_ref().unwrap(); assert_eq!(gateway4, "192.168.0.1"); let routes4_array: Array = ipv4_dbus .get("route-data") .unwrap() .downcast_ref::<Value>() .unwrap() .try_into() .unwrap(); for route4 in routes4_array.iter() { let route4_dict: Dict = route4.downcast_ref::<Value>().unwrap().try_into().unwrap(); let route4_hashmap: HashMap<String, Value> = route4_dict.try_into().unwrap(); assert!(route4_hashmap.contains_key("dest")); assert_eq!(route4_hashmap["dest"], Value::from("192.168.0.0")); assert!(route4_hashmap.contains_key("prefix")); assert_eq!(route4_hashmap["prefix"], Value::from(24_u32)); assert!(route4_hashmap.contains_key("next-hop")); assert_eq!(route4_hashmap["next-hop"], Value::from("192.168.0.1")); assert!(route4_hashmap.contains_key("metric")); assert_eq!(route4_hashmap["metric"], Value::from(100_u32)); } let ipv6_dbus = conn_dbus.get("ipv6").unwrap(); let gateway6: &str = ipv6_dbus.get("gateway").unwrap().downcast_ref().unwrap(); assert_eq!(gateway6, "::ffff:192.168.0.1"); let routes6_array: Array = ipv6_dbus .get("route-data") .unwrap() .downcast_ref::<Value>() .unwrap() .try_into() .unwrap(); for route6 in routes6_array.iter() { let route6_dict: Dict = route6.downcast_ref::<Value>().unwrap().try_into().unwrap(); let route6_hashmap: HashMap<String, Value> = route6_dict.try_into().unwrap(); assert!(route6_hashmap.contains_key("dest")); assert_eq!(route6_hashmap["dest"], Value::from("2001:db8::")); assert!(route6_hashmap.contains_key("prefix")); assert_eq!(route6_hashmap["prefix"], Value::from(64_u32)); assert!(route6_hashmap.contains_key("next-hop")); assert_eq!(route6_hashmap["next-hop"], Value::from("2001:db8::1")); assert!(route6_hashmap.contains_key("metric")); assert_eq!(route6_hashmap["metric"], Value::from(100_u32)); } } } 0707010000008A000081A40000000000000000000000016625710300000283000000000000000000000000000000000000005400000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/nm/error.rs//! NetworkManager error types use crate::network::error::NetworkStateError; use thiserror::Error; #[derive(Error, Debug)] pub enum NmError { #[error("Unsupported IP method: '{0}'")] UnsupportedIpMethod(String), #[error("Unsupported device type: '{0}'")] UnsupportedDeviceType(u32), #[error("Unsupported security protocol: '{0}'")] UnsupportedSecurityProtocol(String), #[error("Unsupported wireless mode: '{0}'")] UnsupporedWirelessMode(String), } impl From<NmError> for NetworkStateError { fn from(value: NmError) -> NetworkStateError { NetworkStateError::AdapterError(value.to_string()) } } 0707010000008B000081A40000000000000000000000016625710300001472000000000000000000000000000000000000005400000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/nm/model.rs//! Set of structs and enums to handle devices and connections from NetworkManager. //! //! This are meant to be used internally, so we omit everything it is not useful for us. /// NetworkManager wireless mode /// /// Using the newtype pattern around an String is enough. For proper support, we might replace this /// struct with an enum. use crate::network::{ model::{Ipv4Method, Ipv6Method, SecurityProtocol, WirelessMode}, nm::error::NmError, }; use agama_lib::network::types::DeviceType; use std::fmt; use std::str::FromStr; #[derive(Debug, PartialEq)] pub struct NmWirelessMode(pub String); impl Default for NmWirelessMode { fn default() -> Self { NmWirelessMode("infrastructure".to_string()) } } impl From<&str> for NmWirelessMode { fn from(value: &str) -> Self { Self(value.to_string()) } } impl NmWirelessMode { pub fn as_str(&self) -> &str { self.0.as_str() } } impl TryFrom<NmWirelessMode> for WirelessMode { type Error = NmError; fn try_from(value: NmWirelessMode) -> Result<Self, Self::Error> { match value.as_str() { "infrastructure" => Ok(WirelessMode::Infra), "adhoc" => Ok(WirelessMode::AdHoc), "mesh" => Ok(WirelessMode::Mesh), "ap" => Ok(WirelessMode::AP), _ => Err(NmError::UnsupporedWirelessMode(value.to_string())), } } } impl fmt::Display for NmWirelessMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", &self.0) } } /// Device types /// /// As we are using the number just to filter wireless devices, using the newtype /// pattern around an u32 is enough. For proper support, we might replace this /// struct with an enum. #[derive(Debug, Default, Clone, Copy)] pub struct NmDeviceType(pub u32); impl From<NmDeviceType> for u32 { fn from(value: NmDeviceType) -> u32 { value.0 } } impl fmt::Display for NmDeviceType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl TryFrom<NmDeviceType> for DeviceType { type Error = NmError; fn try_from(value: NmDeviceType) -> Result<Self, Self::Error> { match value { NmDeviceType(32) => Ok(DeviceType::Loopback), NmDeviceType(1) => Ok(DeviceType::Ethernet), NmDeviceType(2) => Ok(DeviceType::Wireless), NmDeviceType(3) => Ok(DeviceType::Dummy), NmDeviceType(10) => Ok(DeviceType::Bond), NmDeviceType(20) => Ok(DeviceType::Ethernet), NmDeviceType(_) => Err(NmError::UnsupportedDeviceType(value.into())), } } } /// Key management /// /// Using the newtype pattern around an String is enough. For proper support, we might replace this /// struct with an enum. #[derive(Debug, PartialEq)] pub struct NmKeyManagement(pub String); impl Default for NmKeyManagement { fn default() -> Self { NmKeyManagement("none".to_string()) } } impl From<&str> for NmKeyManagement { fn from(value: &str) -> Self { Self(value.to_string()) } } impl TryFrom<NmKeyManagement> for SecurityProtocol { type Error = NmError; fn try_from(value: NmKeyManagement) -> Result<Self, Self::Error> { match value.as_str() { "owe" => Ok(SecurityProtocol::OWE), "ieee8021x" => Ok(SecurityProtocol::DynamicWEP), "wpa-psk" => Ok(SecurityProtocol::WPA2), "wpa-eap" => Ok(SecurityProtocol::WPA3Personal), "sae" => Ok(SecurityProtocol::WPA2Enterprise), "wpa-eap-suite-b192" => Ok(SecurityProtocol::WPA2Enterprise), "none" => Ok(SecurityProtocol::WEP), _ => Err(NmError::UnsupportedSecurityProtocol(value.to_string())), } } } impl NmKeyManagement { pub fn as_str(&self) -> &str { self.0.as_str() } } impl fmt::Display for NmKeyManagement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", &self.0) } } #[derive(Debug, PartialEq)] pub struct NmMethod(pub String); impl Default for NmMethod { fn default() -> Self { NmMethod("auto".to_string()) } } impl NmMethod { pub fn as_str(&self) -> &str { self.0.as_str() } } impl TryFrom<NmMethod> for Ipv4Method { type Error = NmError; fn try_from(value: NmMethod) -> Result<Self, Self::Error> { match Ipv4Method::from_str(value.as_str()) { Ok(method) => Ok(method), _ => Err(NmError::UnsupportedIpMethod(value.to_string())), } } } impl TryFrom<NmMethod> for Ipv6Method { type Error = NmError; fn try_from(value: NmMethod) -> Result<Self, Self::Error> { match Ipv6Method::from_str(value.as_str()) { Ok(method) => Ok(method), _ => Err(NmError::UnsupportedIpMethod(value.to_string())), } } } impl fmt::Display for NmMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", &self.0) } } #[derive(Debug, Default, PartialEq)] pub struct NmIp4Config { pub addresses: Vec<(String, u32)>, pub nameservers: Vec<String>, pub gateway: Option<String>, pub method: NmMethod, } 0707010000008C000081A40000000000000000000000016625710300004793000000000000000000000000000000000000005600000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/nm/proxies.rs//! D-Bus interface proxy for: `org.freedesktop.NetworkManager` //! //! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data. //! //! These D-Bus objects implements //! [standard D-Bus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), //! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: //! //! * [`zbus::fdo::PropertiesProxy`] //! * [`zbus::fdo::IntrospectableProxy`] //! //! …consequently `zbus-xmlgen` did not generate code for the above interfaces. //! Also some proxies can be used against multiple services when they share interface. use agama_lib::dbus::OwnedNestedHash; use zbus::dbus_proxy; #[dbus_proxy( interface = "org.freedesktop.NetworkManager", default_service = "org.freedesktop.NetworkManager", default_path = "/org/freedesktop/NetworkManager", gen_blocking = false )] trait NetworkManager { /// ActivateConnection method fn activate_connection( &self, connection: &zbus::zvariant::ObjectPath<'_>, device: &zbus::zvariant::ObjectPath<'_>, specific_object: &zbus::zvariant::ObjectPath<'_>, ) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// AddAndActivateConnection method fn add_and_activate_connection( &self, connection: std::collections::HashMap< &str, std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, >, device: &zbus::zvariant::ObjectPath<'_>, specific_object: &zbus::zvariant::ObjectPath<'_>, ) -> zbus::Result<( zbus::zvariant::OwnedObjectPath, zbus::zvariant::OwnedObjectPath, )>; /// AddAndActivateConnection2 method fn add_and_activate_connection2( &self, connection: std::collections::HashMap< &str, std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, >, device: &zbus::zvariant::ObjectPath<'_>, specific_object: &zbus::zvariant::ObjectPath<'_>, options: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, ) -> zbus::Result<( zbus::zvariant::OwnedObjectPath, zbus::zvariant::OwnedObjectPath, std::collections::HashMap<String, zbus::zvariant::OwnedValue>, )>; /// CheckConnectivity method fn check_connectivity(&self) -> zbus::Result<u32>; /// CheckpointAdjustRollbackTimeout method fn checkpoint_adjust_rollback_timeout( &self, checkpoint: &zbus::zvariant::ObjectPath<'_>, add_timeout: u32, ) -> zbus::Result<()>; /// CheckpointCreate method fn checkpoint_create( &self, devices: &[zbus::zvariant::ObjectPath<'_>], rollback_timeout: u32, flags: u32, ) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// CheckpointDestroy method fn checkpoint_destroy(&self, checkpoint: &zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; /// CheckpointRollback method fn checkpoint_rollback( &self, checkpoint: &zbus::zvariant::ObjectPath<'_>, ) -> zbus::Result<std::collections::HashMap<String, u32>>; /// DeactivateConnection method fn deactivate_connection( &self, active_connection: &zbus::zvariant::ObjectPath<'_>, ) -> zbus::Result<()>; /// Enable method fn enable(&self, enable: bool) -> zbus::Result<()>; /// GetAllDevices method fn get_all_devices(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// GetDeviceByIpIface method fn get_device_by_ip_iface(&self, iface: &str) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// GetDevices method fn get_devices(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// GetLogging method fn get_logging(&self) -> zbus::Result<(String, String)>; /// GetPermissions method fn get_permissions(&self) -> zbus::Result<std::collections::HashMap<String, String>>; /// Reload method fn reload(&self, flags: u32) -> zbus::Result<()>; /// SetLogging method fn set_logging(&self, level: &str, domains: &str) -> zbus::Result<()>; /// Sleep method fn sleep(&self, sleep: bool) -> zbus::Result<()>; /// CheckPermissions signal #[dbus_proxy(signal)] fn check_permissions(&self) -> zbus::Result<()>; /// DeviceAdded signal #[dbus_proxy(signal)] fn device_added(&self, device_path: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; /// DeviceRemoved signal #[dbus_proxy(signal)] fn device_removed(&self, device_path: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; /// ActivatingConnection property #[dbus_proxy(property)] fn activating_connection(&self) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// ActiveConnections property #[dbus_proxy(property)] fn active_connections(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// AllDevices property #[dbus_proxy(property)] fn all_devices(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// Capabilities property #[dbus_proxy(property)] fn capabilities(&self) -> zbus::Result<Vec<u32>>; /// Checkpoints property #[dbus_proxy(property)] fn checkpoints(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// Connectivity property #[dbus_proxy(property)] fn connectivity(&self) -> zbus::Result<u32>; /// ConnectivityCheckAvailable property #[dbus_proxy(property)] fn connectivity_check_available(&self) -> zbus::Result<bool>; /// ConnectivityCheckEnabled property #[dbus_proxy(property)] fn connectivity_check_enabled(&self) -> zbus::Result<bool>; #[dbus_proxy(property)] fn set_connectivity_check_enabled(&self, value: bool) -> zbus::Result<()>; /// ConnectivityCheckUri property #[dbus_proxy(property)] fn connectivity_check_uri(&self) -> zbus::Result<String>; /// Devices property #[dbus_proxy(property)] fn devices(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// GlobalDnsConfiguration property #[dbus_proxy(property)] fn global_dns_configuration( &self, ) -> zbus::Result<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>; #[dbus_proxy(property)] fn set_global_dns_configuration( &self, value: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, ) -> zbus::Result<()>; /// Metered property #[dbus_proxy(property)] fn metered(&self) -> zbus::Result<u32>; /// NetworkingEnabled property #[dbus_proxy(property)] fn networking_enabled(&self) -> zbus::Result<bool>; /// PrimaryConnection property #[dbus_proxy(property)] fn primary_connection(&self) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// PrimaryConnectionType property #[dbus_proxy(property)] fn primary_connection_type(&self) -> zbus::Result<String>; /// RadioFlags property #[dbus_proxy(property)] fn radio_flags(&self) -> zbus::Result<u32>; /// Startup property #[dbus_proxy(property)] fn startup(&self) -> zbus::Result<bool>; /// State property #[dbus_proxy(property)] fn state(&self) -> zbus::Result<u32>; /// Version property #[dbus_proxy(property)] fn version(&self) -> zbus::Result<String>; /// VersionInfo property #[dbus_proxy(property)] fn version_info(&self) -> zbus::Result<Vec<u32>>; /// WimaxEnabled property #[dbus_proxy(property)] fn wimax_enabled(&self) -> zbus::Result<bool>; #[dbus_proxy(property)] fn set_wimax_enabled(&self, value: bool) -> zbus::Result<()>; /// WimaxHardwareEnabled property #[dbus_proxy(property)] fn wimax_hardware_enabled(&self) -> zbus::Result<bool>; /// WirelessEnabled property #[dbus_proxy(property)] fn wireless_enabled(&self) -> zbus::Result<bool>; #[dbus_proxy(property)] fn set_wireless_enabled(&self, value: bool) -> zbus::Result<()>; /// WirelessHardwareEnabled property #[dbus_proxy(property)] fn wireless_hardware_enabled(&self) -> zbus::Result<bool>; /// WwanEnabled property #[dbus_proxy(property)] fn wwan_enabled(&self) -> zbus::Result<bool>; #[dbus_proxy(property)] fn set_wwan_enabled(&self, value: bool) -> zbus::Result<()>; /// WwanHardwareEnabled property #[dbus_proxy(property)] fn wwan_hardware_enabled(&self) -> zbus::Result<bool>; } /// # DBus interface proxies for: `org.freedesktop.NetworkManager.Device` /// /// This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data. #[dbus_proxy( interface = "org.freedesktop.NetworkManager.Device", default_service = "org.freedesktop.NetworkManager", default_path = "/org/freedesktop/NetworkManager/Devices/1" )] trait Device { /// Delete method fn delete(&self) -> zbus::Result<()>; /// Disconnect method fn disconnect(&self) -> zbus::Result<()>; /// GetAppliedConnection method fn get_applied_connection(&self, flags: u32) -> zbus::Result<(OwnedNestedHash, u64)>; /// Reapply method fn reapply( &self, connection: std::collections::HashMap< &str, std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, >, version_id: u64, flags: u32, ) -> zbus::Result<()>; /// ActiveConnection property #[dbus_proxy(property)] fn active_connection(&self) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// Autoconnect property #[dbus_proxy(property)] fn autoconnect(&self) -> zbus::Result<bool>; #[dbus_proxy(property)] fn set_autoconnect(&self, value: bool) -> zbus::Result<()>; /// AvailableConnections property #[dbus_proxy(property)] fn available_connections(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// Capabilities property #[dbus_proxy(property)] fn capabilities(&self) -> zbus::Result<u32>; /// DeviceType property #[dbus_proxy(property)] fn device_type(&self) -> zbus::Result<u32>; /// Dhcp4Config property #[dbus_proxy(property)] fn dhcp4_config(&self) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// Dhcp6Config property #[dbus_proxy(property)] fn dhcp6_config(&self) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// Driver property #[dbus_proxy(property)] fn driver(&self) -> zbus::Result<String>; /// DriverVersion property #[dbus_proxy(property)] fn driver_version(&self) -> zbus::Result<String>; /// FirmwareMissing property #[dbus_proxy(property)] fn firmware_missing(&self) -> zbus::Result<bool>; /// FirmwareVersion property #[dbus_proxy(property)] fn firmware_version(&self) -> zbus::Result<String>; /// HwAddress property #[dbus_proxy(property)] fn hw_address(&self) -> zbus::Result<String>; /// Interface property #[dbus_proxy(property)] fn interface(&self) -> zbus::Result<String>; /// InterfaceFlags property #[dbus_proxy(property)] fn interface_flags(&self) -> zbus::Result<u32>; /// Ip4Address property #[dbus_proxy(property)] fn ip4_address(&self) -> zbus::Result<u32>; /// Ip4Config property #[dbus_proxy(property)] fn ip4_config(&self) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// Ip4Connectivity property #[dbus_proxy(property)] fn ip4_connectivity(&self) -> zbus::Result<u32>; /// Ip6Config property #[dbus_proxy(property)] fn ip6_config(&self) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// Ip6Connectivity property #[dbus_proxy(property)] fn ip6_connectivity(&self) -> zbus::Result<u32>; /// IpInterface property #[dbus_proxy(property)] fn ip_interface(&self) -> zbus::Result<String>; /// LldpNeighbors property #[dbus_proxy(property)] fn lldp_neighbors( &self, ) -> zbus::Result<Vec<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>>; /// Managed property #[dbus_proxy(property)] fn managed(&self) -> zbus::Result<bool>; #[dbus_proxy(property)] fn set_managed(&self, value: bool) -> zbus::Result<()>; /// Metered property #[dbus_proxy(property)] fn metered(&self) -> zbus::Result<u32>; /// Mtu property #[dbus_proxy(property)] fn mtu(&self) -> zbus::Result<u32>; /// NmPluginMissing property #[dbus_proxy(property)] fn nm_plugin_missing(&self) -> zbus::Result<bool>; /// Path property #[dbus_proxy(property)] fn path(&self) -> zbus::Result<String>; /// PhysicalPortId property #[dbus_proxy(property)] fn physical_port_id(&self) -> zbus::Result<String>; /// Ports property #[dbus_proxy(property)] fn ports(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// Real property #[dbus_proxy(property)] fn real(&self) -> zbus::Result<bool>; /// State property #[dbus_proxy(property)] fn state(&self) -> zbus::Result<u32>; /// StateReason property #[dbus_proxy(property)] fn state_reason(&self) -> zbus::Result<(u32, u32)>; /// Udi property #[dbus_proxy(property)] fn udi(&self) -> zbus::Result<String>; } /// # DBus interface proxy for: `org.freedesktop.NetworkManager.Settings` /// /// This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data. #[dbus_proxy( interface = "org.freedesktop.NetworkManager.Settings", default_service = "org.freedesktop.NetworkManager", default_path = "/org/freedesktop/NetworkManager/Settings", gen_blocking = false )] trait Settings { /// AddConnection method fn add_connection( &self, connection: std::collections::HashMap< &str, std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, >, ) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// AddConnection2 method fn add_connection2( &self, settings: std::collections::HashMap< &str, std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, >, flags: u32, args: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, ) -> zbus::Result<( zbus::zvariant::OwnedObjectPath, std::collections::HashMap<String, zbus::zvariant::OwnedValue>, )>; /// AddConnectionUnsaved method fn add_connection_unsaved( &self, connection: std::collections::HashMap< &str, std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, >, ) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// GetConnectionByUuid method fn get_connection_by_uuid(&self, uuid: &str) -> zbus::Result<zbus::zvariant::OwnedObjectPath>; /// ListConnections method fn list_connections(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// LoadConnections method fn load_connections(&self, filenames: &[&str]) -> zbus::Result<(bool, Vec<String>)>; /// ReloadConnections method fn reload_connections(&self) -> zbus::Result<bool>; /// SaveHostname method fn save_hostname(&self, hostname: &str) -> zbus::Result<()>; /// ConnectionRemoved signal #[dbus_proxy(signal)] fn connection_removed(&self, connection: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; /// NewConnection signal #[dbus_proxy(signal)] fn new_connection(&self, connection: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; /// CanModify property #[dbus_proxy(property)] fn can_modify(&self) -> zbus::Result<bool>; /// Connections property #[dbus_proxy(property)] fn connections(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>; /// Hostname property #[dbus_proxy(property)] fn hostname(&self) -> zbus::Result<String>; } /// # DBus interface proxy for: `org.freedesktop.NetworkManager.Settings.Connection` /// /// This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data. #[dbus_proxy( interface = "org.freedesktop.NetworkManager.Settings.Connection", default_service = "org.freedesktop.NetworkManager", default_path = "/org/freedesktop/NetworkManager/Settings/1", gen_blocking = false )] trait Connection { /// ClearSecrets method fn clear_secrets(&self) -> zbus::Result<()>; /// Delete method fn delete(&self) -> zbus::Result<()>; /// GetSecrets method fn get_secrets( &self, setting_name: &str, ) -> zbus::Result< std::collections::HashMap< String, std::collections::HashMap<String, zbus::zvariant::OwnedValue>, >, >; /// GetSettings method fn get_settings( &self, ) -> zbus::Result< std::collections::HashMap< String, std::collections::HashMap<String, zbus::zvariant::OwnedValue>, >, >; /// Save method fn save(&self) -> zbus::Result<()>; /// Update method fn update( &self, properties: std::collections::HashMap< &str, std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, >, ) -> zbus::Result<()>; /// Update2 method fn update2( &self, settings: std::collections::HashMap< &str, std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, >, flags: u32, args: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, ) -> zbus::Result<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>; /// UpdateUnsaved method fn update_unsaved( &self, properties: std::collections::HashMap< &str, std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, >, ) -> zbus::Result<()>; /// Removed signal #[dbus_proxy(signal)] fn removed(&self) -> zbus::Result<()>; /// Updated signal #[dbus_proxy(signal)] fn updated(&self) -> zbus::Result<()>; /// Filename property #[dbus_proxy(property)] fn filename(&self) -> zbus::Result<String>; /// Flags property #[dbus_proxy(property)] fn flags(&self) -> zbus::Result<u32>; /// Unsaved property #[dbus_proxy(property)] fn unsaved(&self) -> zbus::Result<bool>; } 0707010000008D000081A40000000000000000000000016625710300001C14000000000000000000000000000000000000005200000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/network/system.rsuse super::{error::NetworkStateError, NetworkAdapterError}; use crate::network::{dbus::Tree, model::Connection, Action, Adapter, NetworkState}; use agama_lib::network::types::DeviceType; use std::{error::Error, sync::Arc}; use tokio::sync::{ mpsc::{self, UnboundedReceiver, UnboundedSender}, Mutex, }; use uuid::Uuid; use zbus::zvariant::OwnedObjectPath; /// Represents the network system using holding the state and setting up the D-Bus tree. pub struct NetworkSystem<T: Adapter> { /// Network state pub state: NetworkState, /// Side of the channel to send actions. actions_tx: UnboundedSender<Action>, actions_rx: UnboundedReceiver<Action>, tree: Arc<Mutex<Tree>>, /// Adapter to read/write the network state. adapter: T, } impl<T: Adapter> NetworkSystem<T> { pub fn new(conn: zbus::Connection, adapter: T) -> Self { let (actions_tx, actions_rx) = mpsc::unbounded_channel(); let tree = Tree::new(conn, actions_tx.clone()); Self { state: NetworkState::default(), actions_tx, actions_rx, tree: Arc::new(Mutex::new(tree)), adapter, } } /// Writes the network configuration. pub async fn write(&mut self) -> Result<(), NetworkAdapterError> { self.adapter.write(&self.state).await?; self.state = self.adapter.read().await?; Ok(()) } /// Returns a clone of the /// [UnboundedSender](https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.UnboundedSender.html) /// to execute [actions](Action). pub fn actions_tx(&self) -> UnboundedSender<Action> { self.actions_tx.clone() } /// Populates the D-Bus tree with the known devices and connections. pub async fn setup(&mut self) -> Result<(), Box<dyn Error>> { self.state = self.adapter.read().await?; let mut tree = self.tree.lock().await; tree.set_connections(&mut self.state.connections).await?; tree.set_devices(&self.state.devices).await?; Ok(()) } /// Process incoming actions. /// /// This function is expected to be executed on a separate thread. pub async fn listen(&mut self) { while let Some(action) = self.actions_rx.recv().await { if let Err(error) = self.dispatch_action(action).await { eprintln!("Could not process the action: {}", error); } } } /// Dispatch an action. pub async fn dispatch_action(&mut self, action: Action) -> Result<(), Box<dyn Error>> { match action { Action::AddConnection(name, ty, tx) => { let result = self.add_connection_action(name, ty).await; tx.send(result).unwrap(); } Action::GetConnection(uuid, tx) => { let conn = self.state.get_connection_by_uuid(uuid); tx.send(conn.cloned()).unwrap(); } Action::GetConnectionPath(uuid, tx) => { let tree = self.tree.lock().await; let path = tree.connection_path(uuid); tx.send(path).unwrap(); } Action::GetConnectionPathById(id, tx) => { let path = self.get_connection_path_by_id_action(&id).await; tx.send(path).unwrap(); } Action::GetController(uuid, tx) => { let result = self.get_controller_action(uuid); tx.send(result).unwrap() } Action::GetDevicesPaths(tx) => { let tree = self.tree.lock().await; tx.send(tree.devices_paths()).unwrap(); } Action::GetConnectionsPaths(tx) => { let tree = self.tree.lock().await; tx.send(tree.connections_paths()).unwrap(); } Action::SetPorts(uuid, ports, rx) => { let result = self.set_ports_action(uuid, *ports); rx.send(result).unwrap(); } Action::UpdateConnection(conn) => { self.state.update_connection(*conn)?; } Action::RemoveConnection(uuid) => { let mut tree = self.tree.lock().await; tree.remove_connection(uuid).await?; self.state.remove_connection(uuid)?; } Action::Apply(tx) => { let result = self.write().await; let failed = result.is_err(); tx.send(result).unwrap(); if failed { return Ok(()); } // TODO: re-creating the tree is kind of brute-force and it sends signals about // adding/removing interfaces. We should add/update/delete objects as needed. // NOTE updating the tree at the same time than dispatching actions can cause a // deadlock. We might consider using message passing too but at this point // is enough to use a separate task. let mut connections = self.state.connections.clone(); let tree = Arc::clone(&self.tree); tokio::spawn(async move { let mut tree = tree.lock().await; if let Err(e) = tree.set_connections(&mut connections).await { log::error!("Could not update the D-Bus tree: {}", e); } }); } } Ok(()) } async fn add_connection_action( &mut self, name: String, ty: DeviceType, ) -> Result<OwnedObjectPath, NetworkStateError> { let conn = Connection::new(name, ty); // TODO: handle tree handling problems self.state.add_connection(conn.clone())?; let mut tree = self.tree.lock().await; let path = tree .add_connection(&conn) .await .expect("Could not update the D-Bus tree"); Ok(path) } fn set_ports_action( &mut self, uuid: Uuid, ports: Vec<String>, ) -> Result<(), NetworkStateError> { let conn = self .state .get_connection_by_uuid(uuid) .ok_or(NetworkStateError::UnknownConnection(uuid.to_string()))?; self.state.set_ports(&conn.clone(), ports) } fn get_controller_action( &mut self, uuid: Uuid, ) -> Result<(Connection, Vec<String>), NetworkStateError> { let conn = self .state .get_connection_by_uuid(uuid) .ok_or(NetworkStateError::UnknownConnection(uuid.to_string()))?; let conn = conn.clone(); let controlled = self .state .get_controlled_by(uuid) .iter() .map(|c| c.interface.as_deref().unwrap_or(&c.id).to_string()) .collect::<Vec<_>>(); Ok((conn, controlled)) } async fn get_connection_path_by_id_action(&mut self, id: &str) -> Option<OwnedObjectPath> { let conn = self.state.get_connection(id)?; let tree = self.tree.lock().await; tree.connection_path(conn.uuid) } } 0707010000008E000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/questions0707010000008F000081A40000000000000000000000016625710300002C30000000000000000000000000000000000000004D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/questions.rsuse std::collections::HashMap; use crate::error::Error; use agama_lib::questions::{self, GenericQuestion, WithPassword}; use log; use zbus::{dbus_interface, fdo::ObjectManager, zvariant::ObjectPath, Connection}; mod answers; #[derive(Clone, Debug)] struct GenericQuestionObject(questions::GenericQuestion); #[dbus_interface(name = "org.opensuse.Agama1.Questions.Generic")] impl GenericQuestionObject { #[dbus_interface(property)] pub fn id(&self) -> u32 { self.0.id } #[dbus_interface(property)] pub fn class(&self) -> &str { &self.0.class } #[dbus_interface(property)] pub fn data(&self) -> HashMap<String, String> { self.0.data.to_owned() } #[dbus_interface(property)] pub fn text(&self) -> &str { self.0.text.as_str() } #[dbus_interface(property)] pub fn options(&self) -> Vec<String> { self.0.options.to_owned() } #[dbus_interface(property)] pub fn default_option(&self) -> &str { self.0.default_option.as_str() } #[dbus_interface(property)] pub fn answer(&self) -> &str { &self.0.answer } #[dbus_interface(property)] pub fn set_answer(&mut self, value: &str) -> Result<(), zbus::fdo::Error> { // TODO verify if answer exists in options or if it is valid in other way self.0.answer = value.to_string(); Ok(()) } } /// Mixin interface for questions that are base + contain question for password struct WithPasswordObject(questions::WithPassword); #[dbus_interface(name = "org.opensuse.Agama1.Questions.WithPassword")] impl WithPasswordObject { #[dbus_interface(property)] pub fn password(&self) -> &str { self.0.password.as_str() } #[dbus_interface(property)] pub fn set_password(&mut self, value: &str) { self.0.password = value.to_string(); } } /// Question types used to be able to properly remove object from dbus enum QuestionType { Base, BaseWithPassword, } /// Trait for objects that can provide answers to all kind of Question. /// /// If no strategy is selected or the answer is unknown, then ask to the user. trait AnswerStrategy { /// Id for quick runtime inspection of strategy type fn id(&self) -> u8; /// Provides answer for generic question /// /// I gets as argument the question to answer. Returned value is `answer` /// property or None. If `None` is used, it means that this object does not /// answer to given question. fn answer(&self, question: &GenericQuestion) -> Option<String>; /// Provides answer and password for base question with password /// /// I gets as argument the question to answer. Returned value is pair /// of `answer` and `password` properties. If `None` is used in any /// position it means that this object does not respond to given property. /// /// It is object responsibility to provide correct pair. For example if /// possible answer can be "Ok" and "Cancel". Then for `Ok` password value /// should be provided and for `Cancel` it can be `None`. fn answer_with_password(&self, question: &WithPassword) -> (Option<String>, Option<String>); } /// AnswerStrategy that provides as answer the default option. struct DefaultAnswers; impl DefaultAnswers { pub fn id() -> u8 { 1 } } impl AnswerStrategy for DefaultAnswers { fn id(&self) -> u8 { DefaultAnswers::id() } fn answer(&self, question: &GenericQuestion) -> Option<String> { Some(question.default_option.clone()) } fn answer_with_password(&self, question: &WithPassword) -> (Option<String>, Option<String>) { (Some(question.base.default_option.clone()), None) } } pub struct Questions { questions: HashMap<u32, QuestionType>, connection: Connection, last_id: u32, answer_strategies: Vec<Box<dyn AnswerStrategy + Sync + Send>>, } #[dbus_interface(name = "org.opensuse.Agama1.Questions")] impl Questions { /// creates new generic question without answer #[dbus_interface(name = "New")] async fn new_question( &mut self, class: &str, text: &str, options: Vec<&str>, default_option: &str, data: HashMap<String, String>, ) -> Result<ObjectPath, zbus::fdo::Error> { log::info!("Creating new question with text: {}.", text); let id = self.last_id; self.last_id += 1; // TODO use some thread safety let options = options.iter().map(|o| o.to_string()).collect(); let mut question = questions::GenericQuestion::new( id, class.to_string(), text.to_string(), options, default_option.to_string(), data, ); self.fill_answer(&mut question); let object_path = ObjectPath::try_from(question.object_path()).unwrap(); let question_object = GenericQuestionObject(question); self.connection .object_server() .at(object_path.clone(), question_object) .await?; self.questions.insert(id, QuestionType::Base); Ok(object_path) } /// creates new specialized luks activation question without answer and password async fn new_with_password( &mut self, class: &str, text: &str, options: Vec<&str>, default_option: &str, data: HashMap<String, String>, ) -> Result<ObjectPath, zbus::fdo::Error> { log::info!("Creating new question with password with text: {}.", text); let id = self.last_id; self.last_id += 1; // TODO use some thread safety // TODO: share code better let options = options.iter().map(|o| o.to_string()).collect(); let base = questions::GenericQuestion::new( id, class.to_string(), text.to_string(), options, default_option.to_string(), data, ); let mut question = questions::WithPassword::new(base); let object_path = ObjectPath::try_from(question.base.object_path()).unwrap(); let base_question = question.base.clone(); self.fill_answer_with_password(&mut question); let base_object = GenericQuestionObject(base_question); self.connection .object_server() .at(object_path.clone(), WithPasswordObject(question)) .await?; // NOTE: order here is important as each interface cause signal, so frontend should wait only for GenericQuestions // which should be the last interface added self.connection .object_server() .at(object_path.clone(), base_object) .await?; self.questions.insert(id, QuestionType::BaseWithPassword); Ok(object_path) } /// Removes question at given object path async fn delete(&mut self, question: ObjectPath<'_>) -> Result<(), Error> { // TODO: error checking let id: u32 = question.rsplit('/').next().unwrap().parse().unwrap(); let qtype = self.questions.get(&id).unwrap(); match qtype { QuestionType::Base => { self.connection .object_server() .remove::<GenericQuestionObject, _>(question.clone()) .await?; } QuestionType::BaseWithPassword => { self.connection .object_server() .remove::<GenericQuestionObject, _>(question.clone()) .await?; self.connection .object_server() .remove::<WithPasswordObject, _>(question.clone()) .await?; } }; self.questions.remove(&id); Ok(()) } /// property that defines if questions is interactive or automatically answered with /// default answer #[dbus_interface(property)] fn interactive(&self) -> bool { let last = self.answer_strategies.last(); if let Some(real_strategy) = last { real_strategy.id() != DefaultAnswers::id() } else { true } } #[dbus_interface(property)] fn set_interactive(&mut self, value: bool) { if value != self.interactive() { log::info!("interactive value unchanged - {}", value); return; } log::info!("set interactive to {}", value); if value { self.answer_strategies.pop(); } else { self.answer_strategies.push(Box::new(DefaultAnswers {})); } } fn add_answer_file(&mut self, path: String) -> Result<(), Error> { log::info!("Adding answer file {}", path); let answers = answers::Answers::new_from_file(path.as_str()); match answers { Ok(answers) => { self.answer_strategies.push(Box::new(answers)); Ok(()) } Err(e) => Err(e.into()), } } } impl Questions { /// Creates new questions interface with clone of connection to be able to /// attach or detach question objects fn new(connection: &Connection) -> Self { Self { questions: HashMap::new(), connection: connection.to_owned(), last_id: 0, answer_strategies: vec![], } } /// tries to provide answer to question using answer strategies /// /// What happens under the hood is that it uses answer_strategies vector /// and try to find the first strategy that provides answer. When /// answer is provided, it returns immediately. fn fill_answer(&self, question: &mut GenericQuestion) { for strategy in self.answer_strategies.iter() { match strategy.answer(question) { None => (), Some(answer) => { question.answer = answer; return; } } } } /// tries to provide answer to question using answer strategies /// /// What happens under the hood is that it uses answer_strategies vector /// and try to find the first strategy that provides answer. When /// answer is provided, it returns immediately. fn fill_answer_with_password(&self, question: &mut WithPassword) { for strategy in self.answer_strategies.iter() { let (answer, password) = strategy.answer_with_password(question); if let Some(password) = password { question.password = password; } if let Some(answer) = answer { question.base.answer = answer; return; } } } } /// Starts questions dbus service together with Object manager pub async fn export_dbus_objects( connection: &Connection, ) -> Result<(), Box<dyn std::error::Error>> { const PATH: &str = "/org/opensuse/Agama1/Questions"; // When serving, request the service name _after_ exposing the main object let questions = Questions::new(connection); connection.object_server().at(PATH, questions).await?; connection.object_server().at(PATH, ObjectManager).await?; Ok(()) } 07070100000090000081A400000000000000000000000166257103000026BB000000000000000000000000000000000000005500000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/questions/answers.rsuse std::collections::HashMap; use agama_lib::questions::GenericQuestion; use anyhow::Context; use serde::{Deserialize, Serialize}; /// Data structure for single yaml answer. For variables specification see /// corresponding [agama_lib::questions::GenericQuestion] fields. /// The *matcher* part is: `class`, `text`, `data`. /// The *answer* part is: `answer`, `password`. #[derive(Serialize, Deserialize, PartialEq, Debug)] struct Answer { pub class: Option<String>, pub text: Option<String>, /// A matching GenericQuestion can have other data fields too pub data: Option<HashMap<String, String>>, /// The answer text is the only mandatory part of an Answer pub answer: String, /// All possible mixins have to be here, so they can be specified in an Answer pub password: Option<String>, } impl Answer { /// Determines whether the answer responds to the given question. /// /// * `question`: question to compare with. pub fn responds(&self, question: &GenericQuestion) -> bool { if let Some(class) = &self.class { if question.class != *class { return false; } } if let Some(text) = &self.text { if question.text != *text { return false; } } if let Some(data) = &self.data { return data.iter().all(|(key, value)| { let Some(e_val) = question.data.get(key) else { return false; }; e_val == value }); } true } } /// Data structure holding list of Answer. /// The first matching Answer is used, even if there is /// a better (more specific) match later in the list. #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Answers { answers: Vec<Answer>, } impl Answers { pub fn new_from_file(path: &str) -> anyhow::Result<Self> { let f = std::fs::File::open(path).context(format!("Failed to open {}", path))?; let result: Self = serde_yaml::from_reader(f).context(format!("Failed to parse values at {}", path))?; Ok(result) } pub fn id() -> u8 { 2 } fn find_answer(&self, question: &GenericQuestion) -> Option<&Answer> { self.answers.iter().find(|a| a.responds(question)) } } impl crate::questions::AnswerStrategy for Answers { fn id(&self) -> u8 { Answers::id() } fn answer(&self, question: &GenericQuestion) -> Option<String> { let answer = self.find_answer(question); answer.map(|answer| answer.answer.clone()) } fn answer_with_password( &self, question: &agama_lib::questions::WithPassword, ) -> (Option<String>, Option<String>) { // use here fact that with password share same matchers as generic one let answer = self.find_answer(&question.base); if let Some(answer) = answer { (Some(answer.answer.clone()), answer.password.clone()) } else { (None, None) } } } #[cfg(test)] mod tests { use agama_lib::questions::{GenericQuestion, WithPassword}; use crate::questions::AnswerStrategy; use super::*; // set of fixtures for test fn get_answers() -> Answers { Answers { answers: vec![ Answer { class: Some("without_data".to_string()), data: None, text: None, answer: "Ok".to_string(), password: Some("testing pwd".to_string()), // ignored for generic question }, Answer { class: Some("with_data".to_string()), data: Some(HashMap::from([ ("data1".to_string(), "value1".to_string()), ("data2".to_string(), "value2".to_string()), ])), text: None, answer: "Maybe".to_string(), password: None, }, Answer { class: Some("with_data".to_string()), data: Some(HashMap::from([( "data1".to_string(), "another_value1".to_string(), )])), text: None, answer: "Ok2".to_string(), password: None, }, ], } } #[test] fn test_class_match() { let answers = get_answers(); let question = GenericQuestion { id: 1, class: "without_data".to_string(), text: "JFYI we will kill all bugs during installation.".to_string(), options: vec!["Ok".to_string(), "Cancel".to_string()], default_option: "Cancel".to_string(), data: HashMap::new(), answer: "".to_string(), }; assert_eq!(Some("Ok".to_string()), answers.answer(&question)); } #[test] fn test_no_match() { let answers = get_answers(); let question = GenericQuestion { id: 1, class: "non-existing".to_string(), text: "Hard question?".to_string(), options: vec!["Ok".to_string(), "Cancel".to_string()], default_option: "Cancel".to_string(), data: HashMap::new(), answer: "".to_string(), }; assert_eq!(None, answers.answer(&question)); } #[test] fn test_with_password() { let answers = get_answers(); let question = GenericQuestion { id: 1, class: "without_data".to_string(), text: "Please provide password for dooms day.".to_string(), options: vec!["Ok".to_string(), "Cancel".to_string()], default_option: "Cancel".to_string(), data: HashMap::new(), answer: "".to_string(), }; let with_password = WithPassword { password: "".to_string(), base: question, }; let expected = (Some("Ok".to_string()), Some("testing pwd".to_string())); assert_eq!(expected, answers.answer_with_password(&with_password)); } /// An Answer matches on *data* if all its keys and values are in the GenericQuestion *data*. /// The GenericQuestion can have other *data* keys. #[test] fn test_partial_data_match() { let answers = get_answers(); let question = GenericQuestion { id: 1, class: "with_data".to_string(), text: "Hard question?".to_string(), options: vec!["Ok2".to_string(), "Maybe".to_string(), "Cancel".to_string()], default_option: "Cancel".to_string(), data: HashMap::from([ ("data1".to_string(), "value1".to_string()), ("data2".to_string(), "value2".to_string()), ("data3".to_string(), "value3".to_string()), ]), answer: "".to_string(), }; assert_eq!(Some("Maybe".to_string()), answers.answer(&question)); } #[test] fn test_full_data_match() { let answers = get_answers(); let question = GenericQuestion { id: 1, class: "with_data".to_string(), text: "Hard question?".to_string(), options: vec!["Ok2".to_string(), "Maybe".to_string(), "Cancel".to_string()], default_option: "Cancel".to_string(), data: HashMap::from([ ("data1".to_string(), "another_value1".to_string()), ("data2".to_string(), "value2".to_string()), ("data3".to_string(), "value3".to_string()), ]), answer: "".to_string(), }; assert_eq!(Some("Ok2".to_string()), answers.answer(&question)); } #[test] fn test_no_data_match() { let answers = get_answers(); let question = GenericQuestion { id: 1, class: "with_data".to_string(), text: "Hard question?".to_string(), options: vec!["Ok2".to_string(), "Maybe".to_string(), "Cancel".to_string()], default_option: "Cancel".to_string(), data: HashMap::from([ ("data1".to_string(), "different value".to_string()), ("data2".to_string(), "value2".to_string()), ("data3".to_string(), "value3".to_string()), ]), answer: "".to_string(), }; assert_eq!(None, answers.answer(&question)); } // A "universal answer" with unspecified class+text+data is possible #[test] fn test_universal_match() { let answers = Answers { answers: vec![Answer { class: None, text: None, data: None, answer: "Yes".into(), password: None, }], }; let question = GenericQuestion { id: 1, class: "without_data".to_string(), text: "JFYI we will kill all bugs during installation.".to_string(), options: vec!["Ok".to_string(), "Cancel".to_string()], default_option: "Cancel".to_string(), data: HashMap::new(), answer: "".to_string(), }; assert_eq!(Some("Yes".to_string()), answers.answer(&question)); } #[test] fn test_loading_yaml() { let file = r#" answers: - class: "without_data" answer: "OK" - class: "with_data" data: testk: testv testk2: testv2 answer: "Cancel" "#; let result: Answers = serde_yaml::from_str(file).expect("failed to load yaml string"); assert_eq!(result.answers.len(), 2); } } 07070100000091000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/software07070100000092000081A4000000000000000000000001662571030000003F000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/software.rspub mod web; pub use web::{software_service, software_stream}; 07070100000093000081A400000000000000000000000166257103000020FF000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/software/web.rs//! This module implements the web API for the software module. //! //! The module offers two public functions: //! //! * `software_service` which returns the Axum service. //! * `software_stream` which offers an stream that emits the software events coming from D-Bus. use crate::{error::Error, web::Event}; use agama_lib::{ error::ServiceError, product::{Product, ProductClient}, software::{ proxies::{Software1Proxy, SoftwareProductProxy}, Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy, }, }; use axum::{ extract::State, http::StatusCode, response::{IntoResponse, Response}, routing::{get, post, put}, Json, Router, }; use serde::{Deserialize, Serialize}; use serde_json::json; use std::collections::HashMap; use thiserror::Error; use tokio_stream::{Stream, StreamExt}; #[derive(Clone)] struct SoftwareState<'a> { product: ProductClient<'a>, software: SoftwareClient<'a>, } #[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct SoftwareConfig { patterns: Option<Vec<String>>, product: Option<String>, } #[derive(Error, Debug)] pub enum SoftwareError { #[error("Software service error: {0}")] Error(#[from] ServiceError), } impl IntoResponse for SoftwareError { fn into_response(self) -> Response { let body = json!({ "error": self.to_string() }); (StatusCode::BAD_REQUEST, Json(body)).into_response() } } /// Returns an stream that emits software related events coming from D-Bus. /// /// * `connection`: D-Bus connection to listen for events. pub async fn software_stream(dbus: zbus::Connection) -> Result<impl Stream<Item = Event>, Error> { Ok(StreamExt::merge( product_changed_stream(dbus.clone()).await?, patterns_changed_stream(dbus.clone()).await?, )) } async fn product_changed_stream( dbus: zbus::Connection, ) -> Result<impl Stream<Item = Event>, Error> { let proxy = SoftwareProductProxy::new(&dbus).await?; let stream = proxy .receive_selected_product_changed() .await .then(|change| async move { if let Ok(id) = change.get().await { return Some(Event::ProductChanged { id }); } None }) .filter_map(|e| e); Ok(stream) } async fn patterns_changed_stream( dbus: zbus::Connection, ) -> Result<impl Stream<Item = Event>, Error> { let proxy = Software1Proxy::new(&dbus).await?; let stream = proxy .receive_selected_patterns_changed() .await .then(|change| async move { if let Ok(patterns) = change.get().await { return match reason_to_selected_by(patterns) { Ok(patterns) => Some(patterns), Err(error) => { log::warn!("Ignoring the list of changed patterns. Error: {}", error); None } }; } None }) .filter_map(|e| e.map(Event::PatternsChanged)); Ok(stream) } // Returns a hash replacing the selection "reason" from D-Bus with a SelectedBy variant. fn reason_to_selected_by( patterns: HashMap<String, u8>, ) -> Result<HashMap<String, SelectedBy>, UnknownSelectedBy> { let mut selected: HashMap<String, SelectedBy> = HashMap::new(); for (id, reason) in patterns { match SelectedBy::try_from(reason) { Ok(selected_by) => selected.insert(id, selected_by), Err(e) => return Err(e), }; } Ok(selected) } /// Sets up and returns the axum service for the software module. pub async fn software_service(dbus: zbus::Connection) -> Result<Router, ServiceError> { let product = ProductClient::new(dbus.clone()).await?; let software = SoftwareClient::new(dbus).await?; let state = SoftwareState { product, software }; let router = Router::new() .route("/patterns", get(patterns)) .route("/products", get(products)) .route("/proposal", get(proposal)) .route("/config", put(set_config).get(get_config)) .route("/probe", post(probe)) .with_state(state); Ok(router) } /// Returns the list of available products. /// /// * `state`: service state. #[utoipa::path(get, path = "/software/products", responses( (status = 200, description = "List of known products", body = Vec<Product>), (status = 400, description = "The D-Bus service could not perform the action") ))] async fn products( State(state): State<SoftwareState<'_>>, ) -> Result<Json<Vec<Product>>, SoftwareError> { let products = state.product.products().await?; Ok(Json(products)) } /// Represents a pattern. /// /// It augments the information coming from the D-Bus client. #[derive(Serialize, utoipa::ToSchema)] pub struct PatternEntry { #[serde(flatten)] pattern: Pattern, selected_by: SelectedBy, } /// Returns the list of software patterns. /// /// * `state`: service state. #[utoipa::path(get, path = "/software/patterns", responses( (status = 200, description = "List of known software patterns", body = Vec<PatternEntry>), (status = 400, description = "The D-Bus service could not perform the action") ))] async fn patterns( State(state): State<SoftwareState<'_>>, ) -> Result<Json<Vec<PatternEntry>>, SoftwareError> { let patterns = state.software.patterns(true).await?; let selected = state.software.selected_patterns().await?; let items = patterns .into_iter() .map(|pattern| { let selected_by: SelectedBy = selected .get(&pattern.id) .copied() .unwrap_or(SelectedBy::None); PatternEntry { pattern, selected_by, } }) .collect(); Ok(Json(items)) } /// Sets the software configuration. /// /// * `state`: service state. /// * `config`: software configuration. #[utoipa::path(put, path = "/software/config", responses( (status = 200, description = "Set the software configuration"), (status = 400, description = "The D-Bus service could not perform the action") ))] async fn set_config( State(state): State<SoftwareState<'_>>, Json(config): Json<SoftwareConfig>, ) -> Result<(), SoftwareError> { if let Some(product) = config.product { state.product.select_product(&product).await?; } if let Some(patterns) = config.patterns { state.software.select_patterns(&patterns).await?; } Ok(()) } /// Returns the software configuration. /// /// * `state` : service state. #[utoipa::path(get, path = "/software/config", responses( (status = 200, description = "Software configuration", body = SoftwareConfig), (status = 400, description = "The D-Bus service could not perform the action") ))] async fn get_config( State(state): State<SoftwareState<'_>>, ) -> Result<Json<SoftwareConfig>, SoftwareError> { let product = state.product.product().await?; let patterns = state.software.user_selected_patterns().await?; let config = SoftwareConfig { patterns: Some(patterns), product: Some(product), }; Ok(Json(config)) } #[derive(Serialize, utoipa::ToSchema)] /// Software proposal information. pub struct SoftwareProposal { /// Space required for installation. It is returned as a formatted string which includes /// a number and a unit (e.g., "GiB"). size: String, } /// Returns the proposal information. /// /// At this point, only the required space is reported. #[utoipa::path( get, path = "/software/proposal", responses( (status = 200, description = "Software proposal", body = SoftwareProposal) ))] async fn proposal( State(state): State<SoftwareState<'_>>, ) -> Result<Json<SoftwareProposal>, SoftwareError> { let size = state.software.used_disk_space().await?; let proposal = SoftwareProposal { size }; Ok(Json(proposal)) } /// Returns the proposal information. /// /// At this point, only the required space is reported. #[utoipa::path( post, path = "/software/probe", responses( (status = 200, description = "Read repositories data"), (status = 400, description = "The D-Bus service could not perform the action ") ))] async fn probe(State(state): State<SoftwareState<'_>>) -> Result<Json<()>, SoftwareError> { state.software.probe().await?; Ok(Json(())) } 07070100000094000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004400000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web07070100000095000081A400000000000000000000000166257103000009A1000000000000000000000000000000000000004700000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web.rs//! This module implements a web-based API for Agama. It is responsible for: //! //! * Exposing an HTTP API to interact with Agama. //! * Emit relevant events via websocket. //! * Serve the code for the web user interface (not implemented yet). use self::progress::EventsProgressPresenter; use crate::{ error::Error, l10n::web::l10n_service, software::web::{software_service, software_stream}, }; use axum::Router; mod auth; mod config; mod docs; mod event; mod http; mod progress; mod service; mod state; mod ws; use agama_lib::{connection, error::ServiceError, progress::ProgressMonitor}; pub use auth::generate_token; pub use config::ServiceConfig; pub use docs::ApiDoc; pub use event::{Event, EventsReceiver, EventsSender}; pub use service::MainServiceBuilder; use tokio_stream::StreamExt; /// Returns a service that implements the web-based Agama API. /// /// * `config`: service configuration. /// * `events`: D-Bus connection. pub async fn service( config: ServiceConfig, events: EventsSender, dbus: zbus::Connection, ) -> Result<Router, ServiceError> { let router = MainServiceBuilder::new(events.clone()) .add_service("/l10n", l10n_service(events.clone())) .add_service("/software", software_service(dbus).await?) .with_config(config) .build(); Ok(router) } /// Starts monitoring the D-Bus service progress. /// /// The events are sent to the `events` channel. /// /// * `events`: channel to send the events to. pub async fn run_monitor(events: EventsSender) -> Result<(), ServiceError> { let presenter = EventsProgressPresenter::new(events.clone()); let connection = connection().await?; let mut monitor = ProgressMonitor::new(connection.clone()).await?; tokio::spawn(async move { if let Err(error) = monitor.run(presenter).await { eprintln!("Could not monitor the D-Bus server: {}", error); } }); tokio::spawn(run_events_monitor(connection, events.clone())); Ok(()) } /// Emits the events from the system streams through the events channel. /// /// * `connection`: D-Bus connection. /// * `events`: channel to send the events to. pub async fn run_events_monitor(dbus: zbus::Connection, events: EventsSender) -> Result<(), Error> { let stream = software_stream(dbus).await?; tokio::pin!(stream); let e = events.clone(); while let Some(event) = stream.next().await { _ = e.send(event); } Ok(()) } 07070100000096000081A40000000000000000000000016625710300000A44000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web/auth.rs//! Contains the code to handle access authorization. use super::state::ServiceState; use async_trait::async_trait; use axum::{ extract::FromRequestParts, http::{request, StatusCode}, response::{IntoResponse, Response}, Json, RequestPartsExt, }; use axum_extra::{ headers::{authorization::Bearer, Authorization}, TypedHeader, }; use chrono::{Duration, Utc}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; use pam::PamError; use serde::{Deserialize, Serialize}; use serde_json::json; use thiserror::Error; /// Represents an authentication error. #[derive(Error, Debug)] pub enum AuthError { /// The authentication error is not included in the headers. #[error("Missing authentication token")] MissingToken, /// The authentication error is invalid. #[error("Invalid authentication token: {0}")] InvalidToken(#[from] jsonwebtoken::errors::Error), /// The authentication failed (most probably the password is wrong) #[error("Authentication via PAM failed: {0}")] Failed(#[from] PamError), } impl IntoResponse for AuthError { fn into_response(self) -> Response { let body = json!({ "error": self.to_string() }); (StatusCode::BAD_REQUEST, Json(body)).into_response() } } /// Claims that are included in the token. /// /// See https://datatracker.ietf.org/doc/html/rfc7519 for reference. #[derive(Debug, Serialize, Deserialize)] pub struct TokenClaims { exp: i64, } impl Default for TokenClaims { fn default() -> Self { let exp = Utc::now() + Duration::days(1); Self { exp: exp.timestamp(), } } } #[async_trait] impl FromRequestParts<ServiceState> for TokenClaims { type Rejection = AuthError; async fn from_request_parts( parts: &mut request::Parts, state: &ServiceState, ) -> Result<Self, Self::Rejection> { let TypedHeader(Authorization(bearer)) = parts .extract::<TypedHeader<Authorization<Bearer>>>() .await .map_err(|_| AuthError::MissingToken)?; let decoding = DecodingKey::from_secret(state.config.jwt_secret.as_ref()); let token_data = jsonwebtoken::decode(bearer.token(), &decoding, &Validation::default())?; Ok(token_data.claims) } } /// Generates a JWT. /// /// - `secret`: secret to encrypt/sign the token. pub fn generate_token(secret: &str) -> String { let claims = TokenClaims::default(); jsonwebtoken::encode( &Header::default(), &claims, &EncodingKey::from_secret(secret.as_ref()), ) .unwrap() } 07070100000097000081A40000000000000000000000016625710300000600000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web/config.rs//! Handles Agama web server configuration. //! //! The configuration can be written in YAML or JSON formats, although we plan to choose just one //! of them in the future. It is read from the following locations: //! //! * `/usr/etc/agama.d/server.{json/yaml}` //! * `/etc/agama.d/server.{json/yaml}` //! * `./agama-dbus-server/share/server.{json/yaml}` //! //! All the settings are merged into a single configuration. The values in the latter locations //! take precedence. use config::{Config, ConfigError, File}; use rand::distributions::{Alphanumeric, DistString}; use serde::Deserialize; /// Web service configuration. #[derive(Clone, Debug, Deserialize)] pub struct ServiceConfig { /// Key to sign the JSON Web Tokens. pub jwt_secret: String, } impl ServiceConfig { pub fn load() -> Result<Self, ConfigError> { const JWT_SECRET_SIZE: usize = 30; let jwt_secret: String = Alphanumeric.sample_string(&mut rand::thread_rng(), JWT_SECRET_SIZE); let config = Config::builder() .set_default("jwt_secret", jwt_secret)? .add_source(File::with_name("/usr/etc/agama.d/server").required(false)) .add_source(File::with_name("/etc/agama.d/server").required(false)) .add_source(File::with_name("agama-dbus-server/share/server").required(false)) .build()?; config.try_deserialize() } } impl Default for ServiceConfig { fn default() -> Self { Self { jwt_secret: "".to_string(), } } } 07070100000098000081A400000000000000000000000166257103000003FA000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web/docs.rsuse utoipa::OpenApi; #[derive(OpenApi)] #[openapi( info(description = "Agama web API description"), paths( crate::l10n::web::get_config, crate::l10n::web::keymaps, crate::l10n::web::locales, crate::l10n::web::set_config, crate::l10n::web::timezones, crate::software::web::get_config, crate::software::web::patterns, crate::software::web::patterns, crate::software::web::set_config, super::http::ping, ), components( schemas(agama_lib::product::Product), schemas(agama_lib::software::Pattern), schemas(crate::l10n::Keymap), schemas(crate::l10n::LocaleEntry), schemas(crate::l10n::TimezoneEntry), schemas(crate::l10n::web::LocaleConfig), schemas(crate::software::web::PatternEntry), schemas(crate::software::web::SoftwareConfig), schemas(crate::software::web::SoftwareProposal), schemas(super::http::PingResponse), ) )] pub struct ApiDoc; 07070100000099000081A400000000000000000000000166257103000001CD000000000000000000000000000000000000004D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web/event.rsuse agama_lib::{progress::Progress, software::SelectedBy}; use serde::Serialize; use std::collections::HashMap; use tokio::sync::broadcast::{Receiver, Sender}; #[derive(Clone, Serialize)] #[serde(tag = "type")] pub enum Event { LocaleChanged { locale: String }, Progress(Progress), ProductChanged { id: String }, PatternsChanged(HashMap<String, SelectedBy>), } pub type EventsSender = Sender<Event>; pub type EventsReceiver = Receiver<Event>; 0707010000009A000081A40000000000000000000000016625710300000573000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web/http.rs//! Implements the handlers for the HTTP-based API. use super::{ auth::{generate_token, AuthError}, state::ServiceState, }; use axum::{extract::State, Json}; use pam::Client; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Serialize, ToSchema)] pub struct PingResponse { /// API status status: String, } #[utoipa::path(get, path = "/ping", responses( (status = 200, description = "The API is working", body = PingResponse) ))] pub async fn ping() -> Json<PingResponse> { Json(PingResponse { status: "success".to_string(), }) } #[derive(Serialize)] pub struct AuthResponse { /// Bearer token to use on subsequent calls token: String, } #[derive(Deserialize)] pub struct LoginRequest { /// User password pub password: String, } #[utoipa::path(get, path = "/authenticate", responses( (status = 200, description = "The user have been successfully authenticated", body = AuthResponse) ))] pub async fn authenticate( State(state): State<ServiceState>, Json(login): Json<LoginRequest>, ) -> Result<Json<AuthResponse>, AuthError> { let mut pam_client = Client::with_password("agama")?; pam_client .conversation_mut() .set_credentials("root", login.password); pam_client.authenticate()?; let token = generate_token(&state.config.jwt_secret); Ok(Json(AuthResponse { token })) } 0707010000009B000081A400000000000000000000000166257103000004C5000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web/progress.rs//! Implements a mechanism to monitor track service progress. use super::event::{Event, EventsSender}; use agama_lib::progress::{Progress, ProgressPresenter}; use async_trait::async_trait; // let presenter = EventsProgressPresenter::new(socket); // let mut monitor = ProgressMonitor::new(connection).await.unwrap(); // _ = monitor.run(presenter).await; /// Experimental ProgressPresenter to emit progress events over a Events. pub struct EventsProgressPresenter(EventsSender); impl EventsProgressPresenter { pub fn new(events: EventsSender) -> Self { Self(events) } pub async fn report_progress(&mut self, progress: &Progress) { _ = self.0.send(Event::Progress(progress.clone())) // _ = self.events.send(Message::Text(payload)).await; } } #[async_trait] impl ProgressPresenter for EventsProgressPresenter { async fn start(&mut self, progress: &Progress) { self.report_progress(progress).await; } async fn update_main(&mut self, progress: &Progress) { self.report_progress(progress).await; } async fn update_detail(&mut self, progress: &Progress) { self.report_progress(progress).await; } async fn finish(&mut self) {} } 0707010000009C000081A400000000000000000000000166257103000006C4000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web/service.rsuse super::{auth::TokenClaims, config::ServiceConfig, state::ServiceState, EventsSender}; use axum::{ extract::Request, middleware, response::IntoResponse, routing::{get, post}, Router, }; use std::convert::Infallible; use tower::Service; use tower_http::{compression::CompressionLayer, trace::TraceLayer}; pub struct MainServiceBuilder { config: ServiceConfig, events: EventsSender, router: Router<ServiceState>, } impl MainServiceBuilder { pub fn new(events: EventsSender) -> Self { let router = Router::new().route("/ws", get(super::ws::ws_handler)); let config = ServiceConfig::default(); Self { events, router, config, } } pub fn with_config(self, config: ServiceConfig) -> Self { Self { config, ..self } } pub fn add_service<T>(self, path: &str, service: T) -> Self where T: Service<Request, Error = Infallible> + Clone + Send + 'static, T::Response: IntoResponse, T::Future: Send + 'static, { Self { router: self.router.nest_service(path, service), ..self } } pub fn build(self) -> Router { let state = ServiceState { config: self.config, events: self.events, }; self.router .route_layer(middleware::from_extractor_with_state::<TokenClaims, _>( state.clone(), )) .route("/ping", get(super::http::ping)) .route("/authenticate", post(super::http::authenticate)) .layer(TraceLayer::new_for_http()) .layer(CompressionLayer::new().br(true)) .with_state(state) } } 0707010000009D000081A40000000000000000000000016625710300000142000000000000000000000000000000000000004D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web/state.rs//! Implements the web service state. use super::{config::ServiceConfig, EventsSender}; /// Web service state. /// /// It holds the service configuration, the current D-Bus connection and a channel to send events. #[derive(Clone)] pub struct ServiceState { pub config: ServiceConfig, pub events: EventsSender, } 0707010000009E000081A400000000000000000000000166257103000002AF000000000000000000000000000000000000004A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/src/web/ws.rs//! Implements the websocket handling. use super::{state::ServiceState, EventsSender}; use axum::{ extract::{ ws::{Message, WebSocket}, State, WebSocketUpgrade, }, response::IntoResponse, }; pub async fn ws_handler( State(state): State<ServiceState>, ws: WebSocketUpgrade, ) -> impl IntoResponse { ws.on_upgrade(move |socket| handle_socket(socket, state.events)) } async fn handle_socket(mut socket: WebSocket, events: EventsSender) { let mut rx = events.subscribe(); while let Ok(msg) = rx.recv().await { if let Ok(json) = serde_json::to_string(&msg) { _ = socket.send(Message::Text(json)).await; } } } 0707010000009F000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004200000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/tests070701000000A0000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/tests/common070701000000A1000081A400000000000000000000000166257103000010DF000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/tests/common/mod.rsuse agama_lib::error::ServiceError; use axum::body::{to_bytes, Body}; use std::{ error::Error, future::Future, process::{Child, Command}, time::Duration, }; use tokio_stream::StreamExt; use uuid::Uuid; use zbus::{MatchRule, MessageStream, MessageType}; const DBUS_SERVICE: &str = "org.opensuse.Agama1"; /// D-Bus server to be used on tests. /// /// Takes care of starting and stopping a dbus-daemon to be used on integration tests. Each server /// uses a different socket, so they do not collide. /// /// NOTE: this struct implements the [typestate pattern](http://cliffle.com/blog/rust-typestate/). pub struct DBusServer<S: ServerState> { address: String, extra: S, } pub struct Started { connection: zbus::Connection, child: Child, } impl Drop for Started { fn drop(&mut self) { self.child.kill().unwrap(); } } pub struct Stopped; pub trait ServerState {} impl ServerState for Started {} impl ServerState for Stopped {} impl Default for DBusServer<Stopped> { fn default() -> Self { Self::new() } } impl DBusServer<Stopped> { pub fn new() -> Self { let uuid = Uuid::new_v4(); DBusServer { address: format!("unix:path=/tmp/agama-tests-{uuid}"), extra: Stopped, } } pub async fn start(self) -> Result<DBusServer<Started>, ServiceError> { let child = Command::new("/usr/bin/dbus-daemon") .args([ "--config-file", "../share/dbus-test.conf", "--address", &self.address, ]) .spawn() .expect("to start the testing D-Bus daemon"); let connection = async_retry(|| agama_lib::connection_to(&self.address)).await?; Ok(DBusServer { address: self.address, extra: Started { child, connection }, }) } } impl DBusServer<Started> { pub fn connection(&self) -> zbus::Connection { self.extra.connection.clone() } pub async fn request_name(&mut self) -> Result<(), Box<dyn Error>> { let connection = self.connection(); let mut stream = NameOwnerChangedStream::for_connection(&connection).await?; let cloned = connection.clone(); tokio::spawn(async move { cloned .request_name(DBUS_SERVICE) .await .expect("Request the D-Bus service name"); }); stream.wait_for("org.opensuse.Agama1").await; Ok(()) } } // FIXME: check whether zbus has an API for this use case. struct NameOwnerChangedStream(MessageStream); impl NameOwnerChangedStream { pub async fn for_connection(connection: &zbus::Connection) -> Result<Self, Box<dyn Error>> { let rule = MatchRule::builder() .msg_type(MessageType::Signal) .sender("org.freedesktop.DBus")? .member("NameOwnerChanged")? .build(); let stream = MessageStream::for_match_rule(rule, connection, None).await?; Ok(Self(stream)) } pub async fn wait_for(&mut self, name: &str) { loop { let signal = self.0.next().await.unwrap().unwrap(); let (sname, _, _): (String, String, String) = signal.body().unwrap(); if sname == name { return; } } } } /// Run and retry an async function. /// /// Beware that, if the function is failing for a legit reason, you will /// introduce a delay in your code. /// /// * `func`: async function to run. pub async fn async_retry<O, F, T, E>(func: F) -> Result<T, E> where F: Fn() -> O, O: Future<Output = Result<T, E>>, { const RETRIES: u8 = 10; const INTERVAL: u64 = 500; let mut retry = 0; loop { match func().await { Ok(result) => return Ok(result), Err(error) => { if retry > RETRIES { return Err(error); } retry += 1; let wait_time = Duration::from_millis(INTERVAL); tokio::time::sleep(wait_time).await; } } } } pub async fn body_to_string(body: Body) -> String { let bytes = to_bytes(body, usize::MAX).await.unwrap(); String::from_utf8(bytes.to_vec()).unwrap() } 070701000000A2000081A4000000000000000000000001662571030000072B000000000000000000000000000000000000004A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/tests/l10n.rspub mod common; use agama_server::l10n::web::l10n_service; use axum::{ body::Body, http::{Request, StatusCode}, Router, }; use common::body_to_string; use tokio::{sync::broadcast::channel, test}; use tower::ServiceExt; fn build_service() -> Router { let (tx, _) = channel(16); l10n_service(tx) } #[test] async fn test_get_config() { let service = build_service(); let request = Request::builder() .uri("/config") .body(Body::empty()) .unwrap(); let response = service.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[test] async fn test_locales() { let service = build_service(); let request = Request::builder() .uri("/locales") .body(Body::empty()) .unwrap(); let response = service.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = body_to_string(response.into_body()).await; assert!(body.contains(r#""language":"English""#)); } #[test] async fn test_keymaps() { let service = build_service(); let request = Request::builder() .uri("/keymaps") .body(Body::empty()) .unwrap(); let response = service.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = body_to_string(response.into_body()).await; assert!(body.contains(r#""layout":"us""#)); } #[test] async fn test_timezones() { let service = build_service(); let request = Request::builder() .uri("/timezones") .body(Body::empty()) .unwrap(); let response = service.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = body_to_string(response.into_body()).await; assert!(body.contains(r#""code":"Atlantic/Canary""#)); } 070701000000A3000081A40000000000000000000000016625710300001633000000000000000000000000000000000000004D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/tests/network.rspub mod common; use self::common::{async_retry, DBusServer}; use agama_lib::network::{ settings::{self}, types::DeviceType, NetworkClient, }; use agama_server::network::{ self, model::{self, Ipv4Method, Ipv6Method}, Adapter, NetworkAdapterError, NetworkService, NetworkState, }; use async_trait::async_trait; use cidr::IpInet; use std::error::Error; use tokio::test; #[derive(Default)] pub struct NetworkTestAdapter(network::NetworkState); #[async_trait] impl Adapter for NetworkTestAdapter { async fn read(&self) -> Result<network::NetworkState, NetworkAdapterError> { Ok(self.0.clone()) } async fn write(&self, _network: &network::NetworkState) -> Result<(), NetworkAdapterError> { unimplemented!("Not used in tests"); } } #[test] async fn test_read_connections() -> Result<(), Box<dyn Error>> { let mut server = DBusServer::new().start().await?; let device = model::Device { name: String::from("eth0"), type_: DeviceType::Ethernet, }; let eth0 = model::Connection::new("eth0".to_string(), DeviceType::Ethernet); let state = NetworkState::new(vec![device], vec![eth0]); let adapter = NetworkTestAdapter(state); NetworkService::start(&server.connection(), adapter).await?; server.request_name().await?; let client = NetworkClient::new(server.connection()).await?; let conns = async_retry(|| client.connections()).await?; assert_eq!(conns.len(), 1); let dbus_eth0 = conns.first().unwrap(); assert_eq!(dbus_eth0.id, "eth0"); assert_eq!(dbus_eth0.device_type(), DeviceType::Ethernet); Ok(()) } #[test] async fn test_add_connection() -> Result<(), Box<dyn Error>> { let mut server = DBusServer::new().start().await?; let adapter = NetworkTestAdapter(NetworkState::default()); NetworkService::start(&server.connection(), adapter).await?; server.request_name().await?; let client = NetworkClient::new(server.connection().clone()).await?; let addresses: Vec<IpInet> = vec!["192.168.0.2/24".parse()?, "::ffff:c0a8:7ac7/64".parse()?]; let wlan0 = settings::NetworkConnection { id: "wlan0".to_string(), mac_address: Some("FD:CB:A9:87:65:43".to_string()), method4: Some("auto".to_string()), method6: Some("disabled".to_string()), addresses: addresses.clone(), wireless: Some(settings::WirelessSettings { password: "123456".to_string(), security: "wpa-psk".to_string(), ssid: "TEST".to_string(), mode: "infrastructure".to_string(), }), ..Default::default() }; client.add_or_update_connection(&wlan0).await?; let conns = async_retry(|| client.connections()).await?; assert_eq!(conns.len(), 1); let conn = conns.first().unwrap(); assert_eq!(conn.id, "wlan0"); assert_eq!(conn.mac_address, Some("FD:CB:A9:87:65:43".to_string())); assert_eq!(conn.device_type(), DeviceType::Wireless); assert_eq!(&conn.addresses, &addresses); let method4 = conn.method4.as_ref().unwrap(); assert_eq!(method4, &Ipv4Method::Auto.to_string()); let method6 = conn.method6.as_ref().unwrap(); assert_eq!(method6, &Ipv6Method::Disabled.to_string()); Ok(()) } #[test] async fn test_add_bond_connection() -> Result<(), Box<dyn Error>> { let mut server = DBusServer::new().start().await?; let adapter = NetworkTestAdapter(NetworkState::default()); NetworkService::start(&server.connection(), adapter).await?; server.request_name().await?; let client = NetworkClient::new(server.connection().clone()).await?; let eth0 = settings::NetworkConnection { id: "eth0".to_string(), ..Default::default() }; let bond0 = settings::NetworkConnection { id: "bond0".to_string(), method4: Some("auto".to_string()), method6: Some("disabled".to_string()), interface: Some("bond0".to_string()), bond: Some(settings::BondSettings { mode: "active-backup".to_string(), ports: vec!["eth0".to_string()], options: Some("primary=eth1".to_string()), }), ..Default::default() }; client.add_or_update_connection(ð0).await?; client.add_or_update_connection(&bond0).await?; let conns = async_retry(|| client.connections()).await?; assert_eq!(conns.len(), 2); let conn = conns.iter().find(|c| &c.id == "bond0").unwrap(); assert_eq!(conn.id, "bond0"); assert_eq!(conn.device_type(), DeviceType::Bond); let bond = conn.bond.clone().unwrap(); assert_eq!(bond.mode, "active-backup"); Ok(()) } #[test] async fn test_update_connection() -> Result<(), Box<dyn Error>> { let mut server = DBusServer::new().start().await?; let device = model::Device { name: String::from("eth0"), type_: DeviceType::Ethernet, }; let eth0 = model::Connection::new("eth0".to_string(), DeviceType::Ethernet); let state = NetworkState::new(vec![device], vec![eth0]); let adapter = NetworkTestAdapter(state); NetworkService::start(&server.connection(), adapter).await?; server.request_name().await?; let client = NetworkClient::new(server.connection()).await?; // make sure connections have been published. let _conns = async_retry(|| client.connections()).await?; let mut dbus_eth0 = async_retry(|| client.get_connection("eth0")).await?; dbus_eth0.interface = Some("eth0".to_string()); client.add_or_update_connection(&dbus_eth0).await?; let dbus_eth0 = client.get_connection("eth0").await?; assert_eq!(dbus_eth0.interface, Some("eth0".to_string())); Ok(()) } 070701000000A4000081A4000000000000000000000001662571030000096B000000000000000000000000000000000000004D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-server/tests/service.rspub mod common; use agama_server::{ service, web::{generate_token, MainServiceBuilder, ServiceConfig}, }; use axum::{ body::Body, http::{Method, Request, StatusCode}, response::Response, routing::get, Router, }; use common::{body_to_string, DBusServer}; use std::error::Error; use tokio::{sync::broadcast::channel, test}; use tower::ServiceExt; async fn build_service() -> Router { let (tx, _) = channel(16); let server = DBusServer::new().start().await.unwrap(); service(ServiceConfig::default(), tx, server.connection()) .await .unwrap() } #[test] async fn test_ping() -> Result<(), Box<dyn Error>> { let web_service = build_service().await; let request = Request::builder().uri("/ping").body(Body::empty()).unwrap(); let response = web_service.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = body_to_string(response.into_body()).await; assert_eq!(&body, "{\"status\":\"success\"}"); Ok(()) } async fn protected() -> String { "OK".to_string() } async fn access_protected_route(token: &str, jwt_secret: &str) -> Response { let config = ServiceConfig { jwt_secret: jwt_secret.to_string(), }; let (tx, _) = channel(16); let web_service = MainServiceBuilder::new(tx) .add_service("/protected", get(protected)) .with_config(config) .build(); let request = Request::builder() .uri("/protected") .method(Method::GET) .header("Authorization", format!("Bearer {}", token)) .body(Body::empty()) .unwrap(); web_service.oneshot(request).await.unwrap() } // TODO: The following test should belong to `auth.rs` #[test] async fn test_access_protected_route() -> Result<(), Box<dyn Error>> { let token = generate_token("nots3cr3t"); let response = access_protected_route(&token, "nots3cr3t").await; assert_eq!(response.status(), StatusCode::OK); let body = body_to_string(response.into_body()).await; assert_eq!(body, "OK"); Ok(()) } // // TODO: The following test should belong to `auth.rs`. #[test] async fn test_access_protected_route_failed() -> Result<(), Box<dyn Error>> { let token = generate_token("nots3cr3t"); let response = access_protected_route(&token, "wrong").await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); Ok(()) } 070701000000A5000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-settings070701000000A6000081A400000000000000000000000166257103000000F6000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-settings/Cargo.toml[package] name = "agama-settings" version = "1.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] agama-derive = { path="../agama-derive" } thiserror = "1.0.43" 070701000000A7000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004200000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-settings/src070701000000A8000081A40000000000000000000000016625710300000301000000000000000000000000000000000000004B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-settings/src/error.rsuse thiserror::Error; #[derive(Error, Debug)] pub enum SettingsError { #[error("Unknown attribute '{0}'")] UnknownAttribute(String), #[error("Could not update '{0}': {1}")] UpdateFailed(String, ConversionError), } #[derive(Error, Debug)] pub enum ConversionError { #[error("Invalid value '{0}', expected a {1}")] InvalidValue(String, String), #[error("Missing key '{0}'")] MissingKey(String), } impl SettingsError { /// Returns the an error with the updated attribute pub fn with_attr(self, name: &str) -> Self { match self { Self::UnknownAttribute(_) => Self::UnknownAttribute(name.to_string()), Self::UpdateFailed(_, source) => Self::UpdateFailed(name.to_string(), source), } } } 070701000000A9000081A40000000000000000000000016625710300000969000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-settings/src/lib.rs//! This module offers a mechanism to easily map the values from the command //! line to proper installation settings. //! //! In Agama, the installation settings are modeled using structs with optional fields and vectors. //! To specify a value in the command line, the user needs to specify: //! //! * a setting ID (`"users.name"`, `"storage.lvm"`, and so on), that must be used to find the //! setting. //! * a value, which is captured as a string (`"Foo Bar"`, `"true"`, etc.) and it should be //! converted to the proper type. //! //! Implementing the [Settings](crate::settings::Settings) trait adds support for setting the value //! in an straightforward way, taking care of the conversions automatically. The newtype //! [SettingValue] takes care of such a conversion. //! //! ## Example //! //! The best way to understand how it works is to see it in action. In the example below, there is //! a simplified `InstallSettings` struct that is composed by the user settings, which is another //! struct, and a boolean field. //! //! In this case, the trait is automatically derived, implementing a `set` method that allows //! setting configuration value by specifying: //! //! * An ID, like `users.name`. //! * A string-based value, which is automatically converted to the corresponding type in the //! struct. //! //! ``` //! use agama_settings::{Settings, settings::{SettingValue, Settings}}; //! //! #[derive(Default, Settings)] //! struct UserSettings { //! name: Option<String>, //! enabled: Option<bool> //! } //! //! #[derive(Default, Settings)] //! struct InstallSettings { //! #[settings(nested)] //! user: Option<UserSettings>, //! reboot: Option<bool> //! } //! //! let user = UserSettings { name: Some(String::from("foo")), enabled: Some(false) }; //! let mut settings = InstallSettings { user: Some(user), reboot: None }; //! //! settings.set("user.name", SettingValue("foo.bar".to_string())); //! settings.set("user.enabled", SettingValue("true".to_string())); //! settings.set("reboot", SettingValue("true".to_string())); //! //! let user = settings.user.unwrap(); //! assert_eq!(user.name, Some("foo.bar".to_string())); //! assert_eq!(user.enabled, Some(true)); //! assert_eq!(settings.reboot, Some(true)); //! ``` pub mod error; pub mod settings; pub use self::error::SettingsError; pub use self::settings::{SettingObject, SettingValue}; pub use agama_derive::Settings; 070701000000AA000081A4000000000000000000000001662571030000133E000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-settings/src/settings.rsuse crate::error::{ConversionError, SettingsError}; use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::Display; /// Implements support for easily settings attributes values given an ID (`"users.name"`) and a /// string value (`"Foo bar"`). pub trait Settings { /// Adds a new element to a collection. /// /// * `attr`: attribute name (e.g., `user.name`, `product`). /// * `_value`: element to add to the collection. fn add(&mut self, attr: &str, _value: SettingObject) -> Result<(), SettingsError> { Err(SettingsError::UnknownAttribute(attr.to_string())) } /// Sets an attribute's value. /// /// * `attr`: attribute name (e.g., `user.name`, `product`). /// * `_value`: string-based value coming from the CLI. It will automatically /// converted to the underlying type. fn set(&mut self, attr: &str, _value: SettingValue) -> Result<(), SettingsError> { Err(SettingsError::UnknownAttribute(attr.to_string())) } /// Merges two settings structs. /// /// * `_other`: struct to copy the values from. fn merge(&mut self, _other: &Self) where Self: Sized, { unimplemented!() } } /// Represents a string-based value and allows converting them to other types /// /// Supporting more conversions is a matter of implementing the [std::convert::TryFrom] trait for /// more types. /// /// ``` /// # use agama_settings::settings::SettingValue; // /// let value = SettingValue("true".to_string()); /// let value: bool = value.try_into().expect("the conversion failed"); /// assert_eq!(value, true); /// ``` #[derive(Clone, Debug)] pub struct SettingValue(pub String); impl Display for SettingValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } /// Represents a string-based collection and allows converting to other types /// /// It wraps a hash which uses String as key and SettingValue as value. #[derive(Debug)] pub struct SettingObject(pub HashMap<String, SettingValue>); impl SettingObject { /// Returns the value for the given key. /// /// * `key`: setting key. pub fn get(&self, key: &str) -> Option<&SettingValue> { self.0.get(key) } } impl From<HashMap<String, String>> for SettingObject { fn from(value: HashMap<String, String>) -> SettingObject { let mut hash: HashMap<String, SettingValue> = HashMap::new(); for (k, v) in value { hash.insert(k, SettingValue(v)); } SettingObject(hash) } } impl From<String> for SettingObject { fn from(value: String) -> SettingObject { SettingObject(HashMap::from([("value".to_string(), SettingValue(value))])) } } impl TryFrom<SettingObject> for String { type Error = ConversionError; fn try_from(value: SettingObject) -> Result<Self, Self::Error> { if let Some(v) = value.get("value") { return Ok(v.to_string()); } Err(ConversionError::MissingKey("value".to_string())) } } impl TryFrom<SettingValue> for bool { type Error = ConversionError; fn try_from(value: SettingValue) -> Result<Self, Self::Error> { match value.0.to_lowercase().as_str() { "true" | "yes" | "t" => Ok(true), "false" | "no" | "f" => Ok(false), _ => Err(ConversionError::InvalidValue( value.to_string(), "boolean".to_string(), )), } } } impl TryFrom<SettingValue> for Option<bool> { type Error = ConversionError; fn try_from(value: SettingValue) -> Result<Self, Self::Error> { Ok(Some(value.try_into()?)) } } impl TryFrom<SettingValue> for String { type Error = ConversionError; fn try_from(value: SettingValue) -> Result<Self, Self::Error> { Ok(value.0) } } impl TryFrom<SettingValue> for Option<String> { type Error = ConversionError; fn try_from(value: SettingValue) -> Result<Self, Self::Error> { Ok(Some(value.try_into()?)) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_try_from_bool() { let value = SettingValue("true".to_string()); let value: bool = value.try_into().unwrap(); assert!(value); let value = SettingValue("false".to_string()); let value: bool = value.try_into().unwrap(); assert!(!value); let value = SettingValue("fasle".to_string()); let value: Result<bool, ConversionError> = value.try_into(); let error = value.unwrap_err(); assert_eq!( error.to_string(), "Invalid value 'fasle', expected a boolean" ); } #[test] fn test_try_from_string() { let value = SettingValue("some value".to_string()); let value: String = value.try_into().unwrap(); assert_eq!(value, "some value"); } } 070701000000AB000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004400000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-settings/tests070701000000AC000081A40000000000000000000000016625710300000A6E000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/agama-settings/tests/settings.rsuse agama_settings::{ error::ConversionError, settings::Settings, SettingObject, SettingValue, Settings, }; use std::collections::HashMap; /// Main settings #[derive(Debug, Default, Settings)] pub struct Main { product: Option<String>, #[settings(collection)] patterns: Vec<Pattern>, #[settings(nested)] network: Option<Network>, } /// Software patterns #[derive(Debug, Clone)] pub struct Pattern { id: String, } #[derive(Default, Debug, Settings)] pub struct Network { enabled: Option<bool>, ssid: Option<String>, } /// TODO: deriving this trait could be easy. impl TryFrom<SettingObject> for Pattern { type Error = ConversionError; fn try_from(value: SettingObject) -> Result<Self, Self::Error> { match value.get("id") { Some(id) => Ok(Pattern { id: id.clone().to_string(), }), _ => Err(ConversionError::MissingKey("id".to_string())), } } } #[test] fn test_set() { let mut main = Main::default(); main.set("product", SettingValue("Tumbleweed".to_string())) .unwrap(); assert_eq!(main.product, Some("Tumbleweed".to_string())); main.set("network.enabled", SettingValue("true".to_string())) .unwrap(); let network = main.network.unwrap(); assert_eq!(network.enabled, Some(true)); } #[test] fn test_set_unknown_attribute() { let mut main = Main::default(); let error = main .set("missing", SettingValue("".to_string())) .unwrap_err(); assert_eq!(error.to_string(), "Unknown attribute 'missing'"); } #[test] fn test_invalid_set() { let mut main = Main::default(); let error = main .set("network.enabled", SettingValue("fasle".to_string())) .unwrap_err(); assert_eq!( error.to_string(), "Could not update 'network.enabled': Invalid value 'fasle', expected a boolean" ); } #[test] fn test_add() { let mut main = Main::default(); let pattern = HashMap::from([("id".to_string(), SettingValue("base".to_string()))]); main.add("patterns", SettingObject(pattern)).unwrap(); let pattern = main.patterns.first().unwrap(); assert_eq!(pattern.id, "base"); } #[test] fn test_merge() { let mut main0 = Main { product: Some("Tumbleweed".to_string()), ..Default::default() }; let patterns = vec![Pattern { id: "enhanced".to_string(), }]; let main1 = Main { product: Some("ALP".to_string()), patterns, ..Default::default() }; main0.merge(&main1); assert_eq!(main0.product, Some("ALP".to_string())); assert_eq!(main0.patterns.len(), 1); } 070701000000AD000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked070701000000AE000081A40000000000000000000000016625710300000049000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/.gitignore# Generated by Cargo # will have compiled files and executables /target/ 070701000000AF000081A40000000000000000000000016625710300017021000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/Cargo.lock# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "agama-derive" version = "1.0.0" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "agama-lib" version = "1.0.0" dependencies = [ "agama-settings", "anyhow", "async-trait", "cidr", "curl", "futures-util", "jsonschema", "log", "serde", "serde_json", "tempfile", "thiserror", "tokio", "tokio-stream", "url", "utoipa", "zbus", ] [[package]] name = "agama-locale-data" version = "0.1.0" dependencies = [ "anyhow", "chrono-tz", "flate2", "quick-xml", "regex", "serde", "thiserror", ] [[package]] name = "agama-server" version = "0.1.0" dependencies = [ "agama-lib", "agama-locale-data", "anyhow", "async-trait", "axum", "axum-extra", "chrono", "cidr", "clap", "config", "futures-util", "gettext-rs", "hyper", "hyper-util", "jsonwebtoken", "log", "macaddr", "once_cell", "openssl", "pam", "rand", "regex", "serde", "serde_json", "serde_with", "serde_yaml", "simplelog", "systemd-journal-logger", "thiserror", "tokio", "tokio-openssl", "tokio-stream", "tower", "tower-http", "tracing", "tracing-journald", "tracing-subscriber", "utoipa", "uuid", "zbus", "zbus_macros", ] [[package]] name = "agama-settings" version = "1.0.0" dependencies = [ "agama-derive", "thiserror", ] [[package]] name = "ahash" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", "once_cell", "serde", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "alloc-no-stdlib" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", "windows-sys 0.52.0", ] [[package]] name = "anyhow" version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "async-broadcast" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" dependencies = [ "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-channel" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", "event-listener 5.3.0", "event-listener-strategy 0.5.1", "futures-core", "pin-project-lite", ] [[package]] name = "async-compression" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60" dependencies = [ "brotli", "futures-core", "memchr", "pin-project-lite", "tokio", ] [[package]] name = "async-io" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock 2.8.0", "autocfg", "cfg-if", "concurrent-queue", "futures-lite 1.13.0", "log", "parking", "polling 2.8.0", "rustix 0.37.27", "slab", "socket2 0.4.10", "waker-fn", ] [[package]] name = "async-io" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" dependencies = [ "async-lock 3.3.0", "cfg-if", "concurrent-queue", "futures-io", "futures-lite 2.3.0", "parking", "polling 3.6.0", "rustix 0.38.32", "slab", "tracing", "windows-sys 0.52.0", ] [[package]] name = "async-lock" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener 2.5.3", ] [[package]] name = "async-lock" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ "event-listener 4.0.3", "event-listener-strategy 0.4.0", "pin-project-lite", ] [[package]] name = "async-process" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ "async-io 1.13.0", "async-lock 2.8.0", "async-signal", "blocking", "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", "rustix 0.38.32", "windows-sys 0.48.0", ] [[package]] name = "async-recursion" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "async-signal" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" dependencies = [ "async-io 2.3.2", "async-lock 2.8.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", "rustix 0.38.32", "signal-hook-registry", "slab", "windows-sys 0.48.0", ] [[package]] name = "async-task" version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "axum" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core", "base64 0.21.7", "bytes", "futures-util", "http", "http-body", "http-body-util", "hyper", "hyper-util", "itoa", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", "rustversion", "serde", "serde_json", "serde_path_to_error", "serde_urlencoded", "sha1", "sync_wrapper 1.0.1", "tokio", "tokio-tungstenite", "tower", "tower-layer", "tower-service", "tracing", ] [[package]] name = "axum-core" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", "futures-util", "http", "http-body", "http-body-util", "mime", "pin-project-lite", "rustversion", "sync_wrapper 0.1.2", "tower-layer", "tower-service", "tracing", ] [[package]] name = "axum-extra" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" dependencies = [ "axum", "axum-core", "bytes", "futures-util", "headers", "http", "http-body", "http-body-util", "mime", "pin-project-lite", "serde", "tower", "tower-layer", "tower-service", "tracing", ] [[package]] name = "backtrace" version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "bindgen" version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ "bitflags 2.5.0", "cexpr", "clang-sys", "itertools", "lazy_static", "lazycell", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn 2.0.58", ] [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] [[package]] name = "block" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel", "async-lock 3.3.0", "async-task", "fastrand 2.0.2", "futures-io", "futures-lite 2.3.0", "piper", "tracing", ] [[package]] name = "brotli" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", "brotli-decompressor", ] [[package]] name = "brotli-decompressor" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecount" version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", "windows-targets 0.52.4", ] [[package]] name = "chrono-tz" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" dependencies = [ "chrono", "chrono-tz-build", "phf", ] [[package]] name = "chrono-tz-build" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" dependencies = [ "parse-zoneinfo", "phf", "phf_codegen", ] [[package]] name = "cidr" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d18b093eba54c9aaa1e3784d4361eb2ba944cf7d0a932a830132238f483e8d8" dependencies = [ "serde", ] [[package]] name = "clang-sys" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" dependencies = [ "glob", "libc", ] [[package]] name = "clap" version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim 0.11.1", "terminal_size", ] [[package]] name = "clap_derive" version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "clap_lex" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] [[package]] name = "config" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ "async-trait", "convert_case", "json5", "lazy_static", "nom", "pathdiff", "ron", "rust-ini", "serde", "serde_json", "toml", "yaml-rust", ] [[package]] name = "const-random" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ "const-random-macro", ] [[package]] name = "const-random-macro" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ "getrandom", "once_cell", "tiny-keccak", ] [[package]] name = "convert_case" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" dependencies = [ "unicode-segmentation", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crunchy" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "curl" version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", "socket2 0.5.6", "windows-sys 0.52.0", ] [[package]] name = "curl-sys" version = "0.4.72+curl-8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", "windows-sys 0.52.0", ] [[package]] name = "darling" version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.10.0", "syn 2.0.58", ] [[package]] name = "darling_macro" version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", "syn 2.0.58", ] [[package]] name = "data-encoding" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", ] [[package]] name = "derivative" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "dlv-list" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" dependencies = [ "const-random", ] [[package]] name = "either" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "enumflags2" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener" version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ "event-listener 4.0.3", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" dependencies = [ "event-listener 5.3.0", "pin-project-lite", ] [[package]] name = "fancy-regex" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0678ab2d46fa5195aaf59ad034c083d351377d4af57f3e073c074d0da3e3c766" dependencies = [ "bit-set", "regex", ] [[package]] name = "fastrand" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "fastrand" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "flate2" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fraction" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aa5de57a62c2440ece64342ea59efb7171aa7d016faf8dfcb8795066a17146b" dependencies = [ "lazy_static", "num", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ "fastrand 1.9.0", "futures-core", "futures-io", "memchr", "parking", "pin-project-lite", "waker-fn", ] [[package]] name = "futures-lite" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-macro", "futures-sink", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "gettext-rs" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" dependencies = [ "gettext-sys", "locale_config", ] [[package]] name = "gettext-sys" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" dependencies = [ "cc", "temp-dir", ] [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "headers" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ "base64 0.21.7", "bytes", "headers-core", "http", "httpdate", "mime", "sha1", ] [[package]] name = "headers-core" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ "http", ] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "http" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", "futures-core", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "smallvec", "tokio", ] [[package]] name = "hyper-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", "futures-util", "http", "http-body", "hyper", "pin-project-lite", "socket2 0.5.6", "tokio", ] [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", "serde", ] [[package]] name = "indexmap" version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.3", "serde", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "iso8601" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "296af15e112ec6dc38c9fd3ae027b5337a75466e8eed757bd7d5cf742ea85eb6" dependencies = [ "nom", ] [[package]] name = "itertools" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "json5" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" dependencies = [ "pest", "pest_derive", "serde", ] [[package]] name = "jsonschema" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ca9e2b45609132ae2214d50482c03aeee78826cd6fd53a8940915b81acedf16" dependencies = [ "ahash", "anyhow", "base64 0.13.1", "bytecount", "fancy-regex", "fraction", "iso8601", "itoa", "lazy_static", "memchr", "num-cmp", "parking_lot", "percent-encoding", "regex", "serde", "serde_json", "time", "url", "uuid", ] [[package]] name = "jsonwebtoken" version = "9.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" dependencies = [ "base64 0.21.7", "js-sys", "pem", "ring", "serde", "serde_json", "simple_asn1", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libsystemd" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88b9597a67aa1c81a6624603e6bd0bcefb9e0f94c9c54970ec53771082104b4e" dependencies = [ "hmac", "libc", "log", "nix", "nom", "once_cell", "serde", "sha2", "thiserror", "uuid", ] [[package]] name = "libz-sys" version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "locale_config" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" dependencies = [ "lazy_static", "objc", "objc-foundation", "regex", "winapi", ] [[package]] name = "lock_api" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ "value-bag", ] [[package]] name = "macaddr" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" [[package]] name = "malloc_buf" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ "libc", ] [[package]] name = "matchit" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "migrate-wicked" version = "0.1.0" dependencies = [ "agama-lib", "agama-server", "anyhow", "cidr", "clap", "log", "macaddr", "quick-xml", "regex", "serde", "serde_ignored", "serde_json", "serde_with", "serde_yaml", "simplelog", "strum", "strum_macros", "tokio", "uuid", ] [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", "windows-sys 0.48.0", ] [[package]] name = "nix" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", "memoffset 0.7.1", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "num" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" dependencies = [ "num-bigint", "num-complex", "num-integer", "num-iter", "num-rational", "num-traits", ] [[package]] name = "num-bigint" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "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.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" dependencies = [ "num-traits", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", "num-bigint", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "num_threads" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] [[package]] name = "objc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", ] [[package]] name = "objc-foundation" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" dependencies = [ "block", "objc", "objc_id", ] [[package]] name = "objc_id" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" dependencies = [ "objc", ] [[package]] name = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "ordered-multimap" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" dependencies = [ "dlv-list", "hashbrown 0.13.2", ] [[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pam" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba" dependencies = [ "libc", "memchr", "pam-macros", "pam-sys", "users", ] [[package]] name = "pam-macros" version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "pam-sys" version = "1.0.0-alpha5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b" dependencies = [ "bindgen", "libc", ] [[package]] name = "parking" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.48.5", ] [[package]] name = "parse-zoneinfo" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" dependencies = [ "regex", ] [[package]] name = "pathdiff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pem" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ "base64 0.22.0", "serde", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" dependencies = [ "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" dependencies = [ "pest", "pest_generator", ] [[package]] name = "pest_generator" version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "pest_meta" version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" dependencies = [ "once_cell", "pest", "sha2", ] [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator", "phf_shared", ] [[package]] name = "phf_generator" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", ] [[package]] name = "phf_shared" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] [[package]] name = "pin-project" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", "fastrand 2.0.2", "futures-io", ] [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", "bitflags 1.3.2", "cfg-if", "concurrent-queue", "libc", "log", "pin-project-lite", "windows-sys 0.48.0", ] [[package]] name = "polling" version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix 0.38.32", "tracing", "windows-sys 0.52.0", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", "toml_edit 0.19.15", ] [[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", "syn 1.0.109", "version_check", ] [[package]] name = "proc-macro-error-attr" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", "version_check", ] [[package]] name = "proc-macro2" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" dependencies = [ "memchr", "serde", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "ron" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", "bitflags 2.5.0", "serde", "serde_derive", ] [[package]] name = "rust-ini" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" dependencies = [ "cfg-if", "ordered-multimap", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] [[package]] name = "rustix" version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ "bitflags 2.5.0", "errno", "libc", "linux-raw-sys 0.4.13", "windows-sys 0.52.0", ] [[package]] name = "rustversion" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "ryu" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schannel" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "serde_ignored" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8e319a36d1b52126a0d608f24e93b2d81297091818cd70625fcf50a15d84ddf" dependencies = [ "serde", ] [[package]] name = "serde_json" version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_path_to_error" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ "itoa", "serde", ] [[package]] name = "serde_repr" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "serde_spanned" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "serde_with" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" dependencies = [ "base64 0.21.7", "chrono", "hex", "indexmap 1.9.3", "indexmap 2.2.6", "serde", "serde_derive", "serde_json", "serde_with_macros", "time", ] [[package]] name = "serde_with_macros" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" dependencies = [ "darling", "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap 2.2.6", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "simple_asn1" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", "thiserror", "time", ] [[package]] name = "simplelog" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" dependencies = [ "log", "termcolor", "time", ] [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] [[package]] name = "socket2" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" [[package]] name = "strum_macros" version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", "rustversion", "syn 2.0.58", ] [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "systemd-journal-logger" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "356b5cb52ce54916cbfaee19b07d305c7ea8ce5435a088c58743d4a0211f3eff" dependencies = [ "libsystemd", "log", ] [[package]] name = "temp-dir" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f227968ec00f0e5322f9b8173c7a0cbcff6181a0a5b28e9892491c286277231" [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand 2.0.2", "rustix 0.38.32", "windows-sys 0.52.0", ] [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "terminal_size" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix 0.38.32", "windows-sys 0.48.0", ] [[package]] name = "thiserror" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "time" version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", "libc", "num-conv", "num_threads", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", "signal-hook-registry", "socket2 0.5.6", "tokio-macros", "tracing", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "tokio-openssl" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ffab79df67727f6acf57f1ff743091873c24c579b1e2ce4d8f53e47ded4d63d" dependencies = [ "futures-util", "openssl", "openssl-sys", "tokio", ] [[package]] name = "tokio-stream" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-tungstenite" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", "tokio", "tungstenite", ] [[package]] name = "tokio-util" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "toml" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit 0.22.9", ] [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", "winnow 0.6.6", ] [[package]] name = "tower" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", "pin-project", "pin-project-lite", "tokio", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-http" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "async-compression", "bitflags 2.5.0", "bytes", "futures-core", "http", "http-body", "http-body-util", "pin-project-lite", "tokio", "tokio-util", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-layer" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-journald" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba316a74e8fc3c3896a850dba2375928a9fa171b085ecddfc7c054d39970f3fd" dependencies = [ "libc", "tracing-core", "tracing-subscriber", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "nu-ansi-term", "sharded-slab", "smallvec", "thread_local", "tracing-core", "tracing-log", ] [[package]] name = "tungstenite" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", "bytes", "data-encoding", "http", "httparse", "log", "rand", "sha1", "thiserror", "url", "utf-8", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "uds_windows" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ "memoffset 0.9.1", "tempfile", "winapi", ] [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "users" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" dependencies = [ "libc", "log", ] [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "utoipa" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "272ebdfbc99111033031d2f10e018836056e4d2c8e2acda76450ec7974269fa7" dependencies = [ "indexmap 2.2.6", "serde", "serde_json", "utoipa-gen", ] [[package]] name = "utoipa-gen" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3c9f4d08338c1bfa70dde39412a040a884c6f318b3d09aaaf3437a1e52027fc" dependencies = [ "proc-macro-error", "proc-macro2", "quote", "regex", "syn 2.0.58", ] [[package]] name = "uuid" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", "serde", ] [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.58", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm 0.52.4", "windows_aarch64_msvc 0.52.4", "windows_i686_gnu 0.52.4", "windows_i686_msvc 0.52.4", "windows_x86_64_gnu 0.52.4", "windows_x86_64_gnullvm 0.52.4", "windows_x86_64_msvc 0.52.4", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] [[package]] name = "winnow" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" dependencies = [ "memchr", ] [[package]] name = "xdg-home" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" dependencies = [ "libc", "winapi", ] [[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] [[package]] name = "zbus" version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" dependencies = [ "async-broadcast", "async-process", "async-recursion", "async-trait", "byteorder", "derivative", "enumflags2", "event-listener 2.5.3", "futures-core", "futures-sink", "futures-util", "hex", "nix", "once_cell", "ordered-stream", "rand", "serde", "serde_repr", "sha1", "static_assertions", "tokio", "tracing", "uds_windows", "winapi", "xdg-home", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zbus_names" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" dependencies = [ "serde", "static_assertions", "zvariant", ] [[package]] name = "zerocopy" version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", "syn 2.0.58", ] [[package]] name = "zvariant" version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" dependencies = [ "byteorder", "enumflags2", "libc", "serde", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] 070701000000B0000081A40000000000000000000000016625710300000378000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/Cargo.toml[package] name = "migrate-wicked" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" serde_yaml = "0.9.17" quick-xml = { version = "0.28.2", features = ["serialize"] } agama-lib = { path="../agama-lib" } regex = "1.9.5" agama-server = { path="../agama-server" } cidr = { version = "0.2.2", features = ["serde"] } clap = { version = "4.1.4", features = ["derive", "wrap_help", "env"] } anyhow = "1.0.71" log = "0.4" simplelog = "0.12.1" strum = "0.25.0" strum_macros = "0.25.2" serde_with = "3.3.0" tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } serde_ignored = "0.1.9" uuid = { version = "1.3.4", features = ["v4"] } macaddr = "1.0" [[bin]] name = "migrate-wicked" path = "src/main.rs" 070701000000B1000081A400000000000000000000000166257103000005F9000000000000000000000000000000000000004800000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/README.md# Migrate wicked This project creates a `migrate-wicked` binary which is able to parse wicked xml configs and send them to a NetworkManager dbus service. ## Installation The main way to use the migration is through the container image available at `registry.opensuse.org/home/jcronenberg/migrate-wicked/containers/opensuse/migrate-wicked-git:latest`. For openSUSE a package that includes all the latest changes is available at https://build.opensuse.org/package/show/home:jcronenberg:migrate-wicked/migrate-wicked-git. ## Usage The main recommended way to use `migrate-wicked` is with a container. ```bash podman run -v /etc/sysconfig/network:/etc/sysconfig/network registry.opensuse.org/home/jcronenberg/migrate-wicked/containers/opensuse/migrate-wicked-git:latest ``` This will create `*.nmconnection` files inside `/etc/sysconfig/network/NM-migrated`. See also the [Container's README](https://build.opensuse.org/projects/home:jcronenberg:migrate-wicked/packages/migrate-wicked-git-container/files/README?expand=1) for further infos. If you want to run the migration on the system itself the required xml config needs to be generated via wicked and can the be passed to `migrate-wicked`. ```bash wicked show-config > wicked.xml migrate-wicked migrate wicked.xml # See also migrate-wicked --help for further info ``` ## Architecture `migrate-wicked` uses agama as a library to communicate the parsed network state to NetworkManager but the binary is completely independent of any agama services and can be run standalone. 070701000000B2000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004200000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/src070701000000B3000081A40000000000000000000000016625710300003874000000000000000000000000000000000000004A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/src/bond.rsuse agama_lib::network::types::BondMode as AgamaBondMode; use agama_server::network::model::{self}; use serde::{Deserialize, Deserializer, Serialize}; use serde_with::{skip_serializing_none, DeserializeFromStr, SerializeDisplay}; use std::collections::HashMap; use strum_macros::{Display, EnumString}; #[derive(Debug, PartialEq, Serialize, Clone, Deserialize)] pub enum ParentKind { Bond, } #[derive(Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] #[strum(serialize_all = "kebab_case")] pub enum FailOverMac { None, Active, Follow, } #[derive(Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] #[strum(serialize_all = "kebab_case")] pub enum XmitHashPolicy { Layer2, Layer23, Layer34, Encap23, Encap34, } #[derive(Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] #[strum(serialize_all = "kebab_case")] pub enum LacpRate { Slow, Fast, } #[derive(Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] #[strum(serialize_all = "kebab_case")] pub enum AdSelect { Stable, Bandwidth, Count, } #[derive(Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] #[strum(serialize_all = "kebab_case")] pub enum PrimaryReselect { Always, Better, Failure, } #[derive(Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] #[strum(serialize_all = "kebab_case")] pub enum WickedBondMode { BalanceRr = 0, ActiveBackup, BalanceXor, Broadcast, #[strum(serialize = "802.3ad")] IEEE8023ad, BalanceTlb, BalanceAlb, } #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Bond { pub mode: WickedBondMode, pub miimon: Option<Miimon>, pub arpmon: Option<ArpMon>, #[serde(deserialize_with = "unwrap_slaves")] pub slaves: Vec<Slave>, /* only on mode=[802.3ad, balance_xor] */ pub xmit_hash_policy: Option<XmitHashPolicy>, /* only on mode=balance_rr */ pub packets_per_slave: Option<u32>, /* only on mode=balance_tlb */ pub tlb_dynamic_lb: Option<bool>, /* only on mode=802.3ad */ pub lacp_rate: Option<LacpRate>, /* only on mode=802.3ad */ pub ad_select: Option<AdSelect>, /* only on mode=802.3ad */ pub ad_user_port_key: Option<u32>, /* only on mode=802.3ad */ pub ad_actor_sys_prio: Option<u32>, /* only on mode=802.3ad */ pub ad_actor_system: Option<String>, /* only on mode=802.3ad */ pub min_links: Option<u32>, /* only on mode=active-backup */ pub primary_reselect: Option<PrimaryReselect>, /* only on mode=active-backup */ pub fail_over_mac: Option<FailOverMac>, /* only on mode=active-backup */ pub num_grat_arp: Option<u32>, /* only on mode=active-backup */ pub num_unsol_na: Option<u32>, /* only on mode=[balance_tlb|balance_alb] */ pub lp_interval: Option<u32>, /* only on mode=[balance_tlb|balance_alb|balance_RR|active-backup] */ pub resend_igmp: Option<u32>, pub all_slaves_active: Option<bool>, pub address: Option<String>, } impl Bond { pub fn primary(self: &Bond) -> Option<&String> { for s in self.slaves.iter() { if s.primary.unwrap_or(false) { return Some(&s.device); } } None } } #[skip_serializing_none] #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] pub struct Slave { pub device: String, pub primary: Option<bool>, } #[derive(Debug, PartialEq, Default, SerializeDisplay, DeserializeFromStr, EnumString, Display)] #[strum(serialize_all = "kebab_case")] pub enum CarrierDetect { Ioctl = 0, #[default] Netif = 1, } #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] pub struct Miimon { pub frequency: u32, #[serde(rename = "carrier-detect")] pub carrier_detect: CarrierDetect, pub downdelay: Option<u32>, pub updelay: Option<u32>, } #[derive(Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] #[strum(serialize_all = "kebab_case")] pub enum ArpValidateTargets { Any = 0, All = 1, } #[derive(Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] #[strum(serialize_all = "snake_case")] pub enum ArpValidate { None = 0, Active = 1, Backup = 2, All = 3, Filter = 4, FilterActive = 5, FilterBackup = 6, } #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct ArpMon { pub interval: u32, pub validate: ArpValidate, #[serde(rename = "validate-targets")] pub validate_targets: Option<ArpValidateTargets>, #[serde(deserialize_with = "unwrap_arpmon_targets")] pub targets: Vec<String>, } fn unwrap_arpmon_targets<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> where D: Deserializer<'de>, { #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] pub struct ArpMonTargetAddressV4 { #[serde(rename = "ipv4-address")] pub ipv4_address: Vec<String>, } Ok(ArpMonTargetAddressV4::deserialize(deserializer)?.ipv4_address) } fn unwrap_slaves<'de, D>(deserializer: D) -> Result<Vec<Slave>, D::Error> where D: Deserializer<'de>, { #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] struct Slaves { slave: Vec<Slave>, } Ok(Slaves::deserialize(deserializer)?.slave) } impl From<&WickedBondMode> for AgamaBondMode { fn from(bondmode: &WickedBondMode) -> AgamaBondMode { match bondmode { WickedBondMode::BalanceRr => AgamaBondMode::RoundRobin, WickedBondMode::ActiveBackup => AgamaBondMode::ActiveBackup, WickedBondMode::BalanceXor => AgamaBondMode::BalanceXOR, WickedBondMode::Broadcast => AgamaBondMode::Broadcast, WickedBondMode::IEEE8023ad => AgamaBondMode::LACP, WickedBondMode::BalanceTlb => AgamaBondMode::BalanceTLB, WickedBondMode::BalanceAlb => AgamaBondMode::BalanceALB, } } } impl From<&Bond> for model::ConnectionConfig { fn from(bond: &Bond) -> model::ConnectionConfig { let mut h: HashMap<String, String> = HashMap::new(); if let Some(p) = bond.primary() { h.insert(String::from("primary"), p.clone()); } if let Some(m) = &bond.miimon { h.insert(String::from("miimon"), format!("{}", m.frequency)); h.insert( String::from("use_carrier"), match m.carrier_detect { CarrierDetect::Ioctl => 0, CarrierDetect::Netif => 1, } .to_string(), ); if let Some(v) = m.downdelay { h.insert(String::from("downdelay"), v.to_string()); } if let Some(v) = m.updelay { h.insert(String::from("updelay"), v.to_string()); } } if let Some(a) = &bond.arpmon { h.insert(String::from("arp_interval"), format!("{}", a.interval)); h.insert(String::from("arp_validate"), a.validate.to_string()); if !a.targets.is_empty() { let sv = a .targets .iter() .map(|c| c.as_ref()) .collect::<Vec<&str>>() .join(","); h.insert(String::from("arp_ip_target"), sv); } if let Some(v) = &a.validate_targets { h.insert(String::from("arp_all_targets"), v.to_string()); } } if let Some(fom) = &bond.fail_over_mac { h.insert(String::from("fail_over_mac"), fom.to_string()); } if let Some(v) = &bond.xmit_hash_policy { h.insert(String::from("xmit_hash_policy"), v.to_string()); } if let Some(v) = &bond.packets_per_slave { h.insert(String::from("packets_per_slave"), v.to_string()); } if let Some(v) = &bond.tlb_dynamic_lb { h.insert( String::from("tlb_dynamic_lb"), if *v { 1.to_string() } else { 0.to_string() }, ); } if let Some(v) = &bond.lacp_rate { h.insert(String::from("lacp_rate"), v.to_string()); } if let Some(v) = &bond.ad_select { h.insert(String::from("ad_select"), v.to_string()); } if let Some(v) = &bond.ad_user_port_key { h.insert(String::from("ad_user_port_key"), v.to_string()); } if let Some(v) = &bond.ad_actor_sys_prio { h.insert(String::from("ad_actor_sys_prio"), v.to_string()); } if let Some(v) = &bond.ad_actor_system { h.insert(String::from("ad_actor_system"), v.clone()); } if let Some(v) = &bond.min_links { h.insert(String::from("min_links"), v.to_string()); } if let Some(v) = &bond.primary_reselect { h.insert(String::from("primary_reselect"), v.to_string()); } if let Some(v) = &bond.num_grat_arp { h.insert(String::from("num_grat_arp"), v.to_string()); } if let Some(v) = &bond.num_unsol_na { h.insert(String::from("num_unsol_na"), v.to_string()); } if let Some(v) = &bond.lp_interval { h.insert(String::from("lp_interval"), v.to_string()); } if let Some(v) = &bond.resend_igmp { h.insert(String::from("resend_igmp"), v.to_string()); } if let Some(v) = &bond.all_slaves_active { h.insert( String::from("all_slaves_active"), if *v { 1.to_string() } else { 0.to_string() }, ); } model::ConnectionConfig::Bond(model::BondConfig { options: model::BondOptions(h), mode: AgamaBondMode::from(&bond.mode), }) } } #[cfg(test)] mod tests { use super::*; use crate::interface::*; use crate::MIGRATION_SETTINGS; #[allow(dead_code)] fn setup_default_migration_settings() { let _ = MIGRATION_SETTINGS.set(crate::MigrationSettings { continue_migration: false, dry_run: false, activate_connections: true, }); } #[test] fn test_bond_options() { setup_default_migration_settings(); let bond_interface = Interface { bond: Some(Bond { mode: WickedBondMode::IEEE8023ad, xmit_hash_policy: Some(XmitHashPolicy::Encap34), fail_over_mac: Some(FailOverMac::Active), packets_per_slave: Some(23), tlb_dynamic_lb: Some(true), lacp_rate: Some(LacpRate::Slow), ad_select: Some(AdSelect::Bandwidth), ad_user_port_key: Some(42), ad_actor_sys_prio: Some(5), ad_actor_system: Some(String::from("00:de:ad:be:ef:00")), min_links: Some(3), primary_reselect: Some(PrimaryReselect::Better), num_grat_arp: Some(7), num_unsol_na: Some(13), lp_interval: Some(17), resend_igmp: Some(19), all_slaves_active: Some(true), miimon: Some(Miimon { frequency: 42, carrier_detect: CarrierDetect::Netif, downdelay: Some(23), updelay: Some(5), }), arpmon: Some(ArpMon { interval: 23, validate: ArpValidate::FilterBackup, validate_targets: Some(ArpValidateTargets::Any), targets: vec![String::from("1.2.3.4"), String::from("4.3.2.1")], }), slaves: vec![], address: Some(String::from("02:11:22:33:44:55")), }), ..Default::default() }; let connection: &model::Connection = &bond_interface.to_connection().unwrap().connections[0]; assert!(matches!( connection.config, model::ConnectionConfig::Bond(_) )); assert_eq!(connection.mac_address.to_string(), "02:11:22:33:44:55"); if let model::ConnectionConfig::Bond(bond) = &connection.config { assert_eq!(bond.mode, AgamaBondMode::LACP); let s = HashMap::from([ ("xmit_hash_policy", String::from("encap34")), ("packets_per_slave", 23.to_string()), ("tlb_dynamic_lb", 1.to_string()), ("lacp_rate", String::from("slow")), ("ad_select", String::from("bandwidth")), ("ad_user_port_key", 42.to_string()), ("ad_actor_sys_prio", 5.to_string()), ("ad_actor_system", String::from("00:de:ad:be:ef:00")), ("min_links", 3.to_string()), ("primary_reselect", String::from("better")), ("fail_over_mac", String::from("active")), ("num_grat_arp", 7.to_string()), ("num_unsol_na", 13.to_string()), ("lp_interval", 17.to_string()), ("resend_igmp", 19.to_string()), ("all_slaves_active", 1.to_string()), // miimon ("miimon", 42.to_string()), ("use_carrier", 1.to_string()), ("downdelay", 23.to_string()), ("updelay", 5.to_string()), // arpmon ("arp_validate", String::from("filter_backup")), ("arp_all_targets", String::from("any")), ("arp_ip_target", String::from("1.2.3.4,4.3.2.1")), ("arp_interval", 23.to_string()), ]); for (k, v) in s.iter() { assert!( bond.options.0.contains_key(*k), "Missing key '{}' in bond hash {:?}", *k, bond.options.0 ); assert_eq!( bond.options.0.get(*k).unwrap(), v, "Unexpected value '{}' in key '{}'", *k, v ); } } } } 070701000000B4000081A400000000000000000000000166257103000007B4000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/src/bridge.rsuse agama_server::network::model; use serde::{Deserialize, Deserializer, Serialize}; use serde_with::skip_serializing_none; #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Bridge { #[serde(default)] pub stp: bool, pub priority: Option<u16>, pub forward_delay: Option<f32>, pub hello_time: Option<f32>, pub max_age: Option<f32>, pub aging_time: Option<f32>, // wicked uses US english, but kernel and nm UK (ageing) #[serde(deserialize_with = "unwrap_ports")] pub ports: Vec<BridgePort>, pub address: Option<String>, } fn unwrap_ports<'de, D>(deserializer: D) -> Result<Vec<BridgePort>, D::Error> where D: Deserializer<'de>, { #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] struct BridgePorts { port: Vec<BridgePort>, } Ok(BridgePorts::deserialize(deserializer)?.port) } #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub struct BridgePort { pub device: String, pub priority: Option<u32>, pub path_cost: Option<u32>, } impl From<&Bridge> for model::ConnectionConfig { fn from(bridge: &Bridge) -> model::ConnectionConfig { model::ConnectionConfig::Bridge(model::BridgeConfig { stp: bridge.stp, priority: bridge.priority.map(|v| v as u32), forward_delay: bridge.forward_delay.map(|v| v.round() as u32), hello_time: bridge.hello_time.map(|v| v.round() as u32), max_age: bridge.max_age.map(|v| v.round() as u32), ageing_time: bridge.aging_time.map(|v| v.round() as u32), }) } } impl From<&BridgePort> for model::PortConfig { fn from(port: &BridgePort) -> model::PortConfig { model::PortConfig::Bridge(model::BridgePortConfig { priority: port.priority, path_cost: port.path_cost, }) } } 070701000000B5000081A400000000000000000000000166257103000012D2000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/src/infiniband.rsuse agama_server::network::model::{self, InfinibandConfig, InfinibandTransportMode}; use serde::{Deserialize, Deserializer, Serialize}; use serde_with::skip_serializing_none; use std::str::FromStr; #[skip_serializing_none] #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] pub struct Infiniband { pub mode: Option<String>, pub multicast: Option<String>, } #[skip_serializing_none] #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] pub struct InfinibandChild { pub device: String, #[serde(deserialize_with = "deserialize_pkey")] pub pkey: u16, pub mode: Option<String>, pub multicast: Option<String>, } fn deserialize_pkey<'de, D>(deserializer: D) -> Result<u16, D::Error> where D: Deserializer<'de>, { let pkey_string: String = String::deserialize(deserializer)?; let pkey_string: &str = pkey_string.trim_start_matches("0x"); Ok(u16::from_str_radix(pkey_string, 16).unwrap()) } impl From<&Infiniband> for model::ConnectionConfig { fn from(value: &Infiniband) -> Self { model::ConnectionConfig::Infiniband(InfinibandConfig { transport_mode: InfinibandTransportMode::from_str( value .mode .as_ref() .unwrap_or(&"datagram".to_string()) .as_str(), ) .unwrap(), ..Default::default() }) } } impl From<&InfinibandChild> for model::ConnectionConfig { fn from(value: &InfinibandChild) -> Self { model::ConnectionConfig::Infiniband(InfinibandConfig { p_key: Some(value.pkey as i32), parent: Some(value.device.clone()), transport_mode: InfinibandTransportMode::from_str( value .mode .as_ref() .unwrap_or(&"datagram".to_string()) .as_str(), ) .unwrap(), }) } } #[cfg(test)] mod tests { use super::*; use crate::interface::*; use crate::MIGRATION_SETTINGS; #[allow(dead_code)] fn setup_default_migration_settings() { let _ = MIGRATION_SETTINGS.set(crate::MigrationSettings { continue_migration: false, dry_run: false, activate_connections: true, }); } #[test] fn test_infiniband_migration() { setup_default_migration_settings(); let infiniband_interface = Interface { infiniband: Some(Infiniband { mode: Some("datagram".to_string()), multicast: Some("allowed".to_string()), }), ..Default::default() }; let connections = infiniband_interface.to_connection(); assert!(connections.is_ok()); let connection = &connections.unwrap().connections[0]; let model::ConnectionConfig::Infiniband(infiniband) = &connection.config else { panic!() }; assert_eq!( infiniband.transport_mode, InfinibandTransportMode::from_str("datagram").unwrap() ); } #[test] fn test_infiniband_child_migration() { setup_default_migration_settings(); let infiniband_child_interface = Interface { infiniband_child: Some(InfinibandChild { mode: Some("datagram".to_string()), multicast: Some("allowed".to_string()), pkey: 0x8001, device: "ib0".to_string(), }), ..Default::default() }; let connections = infiniband_child_interface.to_connection(); assert!(connections.is_ok()); // Check multicast warning is generated assert_eq!(connections.as_ref().unwrap().warnings.len(), 1); assert_eq!( connections.as_ref().unwrap().warnings[0].to_string(), "Infiniband multicast isn't supported by NetworkManager" ); let connection = &connections.unwrap().connections[0]; let model::ConnectionConfig::Infiniband(infiniband_child) = &connection.config else { panic!() }; assert_eq!( infiniband_child.transport_mode, InfinibandTransportMode::from_str("datagram").unwrap() ); assert_eq!(infiniband_child.p_key, Some(0x8001)); assert_eq!(infiniband_child.parent, Some("ib0".to_string())); } #[test] fn test_deserialize_pkey() { let xml = r##" <interface-child> <pkey>0x8001</pkey> <device>ib0</device> </interface-child> "##; let infiniband_child = quick_xml::de::from_str::<InfinibandChild>(xml).unwrap(); assert_eq!(infiniband_child.pkey, 0x8001); } } 070701000000B6000081A40000000000000000000000016625710300004102000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/src/interface.rsuse crate::bond::Bond; use crate::bridge::Bridge; use crate::infiniband::{Infiniband, InfinibandChild}; use crate::vlan::Vlan; use crate::wireless::Wireless; use crate::MIGRATION_SETTINGS; use agama_server::network::model::{self, IpConfig, IpRoute, Ipv4Method, Ipv6Method, MacAddress}; use cidr::IpInet; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none}; use std::{net::IpAddr, str::FromStr}; #[skip_serializing_none] #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] pub struct Interface { pub name: String, pub firewall: Firewall, pub link: Link, pub ipv4: Ipv4, #[serde(rename = "ipv4-static")] pub ipv4_static: Option<Ipv4Static>, pub ipv6: Ipv6, #[serde(rename = "ipv6-static")] pub ipv6_static: Option<Ipv6Static>, #[serde(rename = "ipv6-dhcp")] pub ipv6_dhcp: Option<Ipv6Dhcp>, #[serde(rename = "ipv6-auto")] pub ipv6_auto: Option<Ipv6Auto>, pub dummy: Option<Dummy>, pub ethernet: Option<Ethernet>, pub bond: Option<Bond>, #[serde(skip_serializing_if = "Option::is_none")] pub wireless: Option<Wireless>, #[serde(rename = "@origin")] pub origin: String, pub vlan: Option<Vlan>, pub bridge: Option<Bridge>, pub infiniband: Option<Infiniband>, #[serde(rename = "infiniband-child")] pub infiniband_child: Option<InfinibandChild>, } #[skip_serializing_none] #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] pub struct Firewall { pub zone: Option<String>, } #[skip_serializing_none] #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] pub struct Link { pub master: Option<String>, } #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] pub struct Ipv4 { pub enabled: bool, } #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] pub struct Ipv6 { pub enabled: bool, } #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Ipv4Static { #[serde(rename = "address")] pub addresses: Option<Vec<Address>>, #[serde(rename = "route")] pub routes: Option<Vec<Route>>, } #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Ipv6Static { #[serde(rename = "address")] pub addresses: Option<Vec<Address>>, #[serde(rename = "route")] pub routes: Option<Vec<Route>>, } #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] pub struct Address { pub local: String, } #[serde_as] #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] pub struct Ipv6Dhcp { pub enabled: bool, pub mode: String, } #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] pub struct Ipv6Auto { pub enabled: bool, } #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] pub struct Dummy { pub address: Option<String>, } #[skip_serializing_none] #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] pub struct Route { pub destination: Option<String>, #[serde(rename = "nexthop")] pub nexthops: Option<Vec<Nexthop>>, pub priority: Option<u32>, } #[skip_serializing_none] #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] pub struct Ethernet { pub address: Option<String>, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Nexthop { pub gateway: String, } pub struct ConnectionResult { pub connections: Vec<model::Connection>, pub warnings: Vec<anyhow::Error>, } pub struct IpConfigResult { ip_config: IpConfig, warnings: Vec<anyhow::Error>, } impl Interface { pub fn to_connection(&self) -> Result<ConnectionResult, anyhow::Error> { let settings = MIGRATION_SETTINGS.get().unwrap(); let ip_config = self.to_ip_config()?; let mut warnings = ip_config.warnings; let mut connection = model::Connection { id: self.name.clone(), firewall_zone: self.firewall.zone.clone(), interface: Some(self.name.clone()), ip_config: ip_config.ip_config, status: model::Status::Down, ..Default::default() }; let mut connections: Vec<model::Connection> = vec![]; if settings.activate_connections { connection.status = model::Status::Up; } if let Some(ethernet) = &self.ethernet { connection.mac_address = MacAddress::try_from(ðernet.address)?; connection.config = model::ConnectionConfig::Ethernet; connections.push(connection); } else if let Some(dummy) = &self.dummy { connection.mac_address = MacAddress::try_from(&dummy.address)?; connection.config = model::ConnectionConfig::Dummy; connections.push(connection); } else if let Some(bond) = &self.bond { connection.mac_address = MacAddress::try_from(&bond.address)?; connection.config = bond.into(); connections.push(connection); } else if let Some(vlan) = &self.vlan { connection.mac_address = MacAddress::try_from(&vlan.address)?; connection.config = vlan.into(); connections.push(connection); } else if let Some(bridge) = &self.bridge { connection.mac_address = MacAddress::try_from(&bridge.address)?; connection.config = bridge.into(); connections.push(connection); } else if let Some(wireless) = &self.wireless { if let Some(networks) = &wireless.networks { if networks.len() > 1 { log::info!("{} has multiple networks defined, these will be split into different connections in NM", connection.id); } for (i, network) in networks.iter().enumerate() { let mut wireless_connection = connection.clone(); if networks.len() > 1 { wireless_connection.id.push_str(&format!("-{}", i)); } wireless_connection.config = network.try_into()?; connections.push(wireless_connection); } } } else if let Some(infiniband) = &self.infiniband { if infiniband.multicast.is_some() { warnings.push(anyhow::anyhow!( "Infiniband multicast isn't supported by NetworkManager" )); } connection.config = infiniband.into(); connections.push(connection) } else if let Some(infiniband_child) = &self.infiniband_child { if infiniband_child.multicast.is_some() { warnings.push(anyhow::anyhow!( "Infiniband multicast isn't supported by NetworkManager" )); } connection.config = infiniband_child.into(); connections.push(connection) } else { connections.push(connection); } Ok(ConnectionResult { connections, warnings, }) } pub fn to_ip_config(&self) -> Result<IpConfigResult, anyhow::Error> { let mut connection_result = IpConfigResult { ip_config: IpConfig { ..Default::default() }, warnings: vec![], }; let method4 = if self.ipv4.enabled && self.ipv4_static.is_some() { Ipv4Method::Manual } else if !self.ipv4.enabled { Ipv4Method::Disabled } else { Ipv4Method::Auto }; let method6 = if self.ipv6.enabled && self.ipv6_static.is_some() { Ipv6Method::Manual } else if self.ipv6.enabled && self.ipv6_dhcp.is_some() && self.ipv6_dhcp.as_ref().unwrap().mode == "managed" { Ipv6Method::Dhcp } else if !self.ipv6.enabled { Ipv6Method::Disabled } else { Ipv6Method::Auto }; let mut addresses: Vec<IpInet> = vec![]; let mut new_routes4: Vec<IpRoute> = vec![]; let mut new_routes6: Vec<IpRoute> = vec![]; if let Some(ipv4_static) = &self.ipv4_static { if let Some(addresses_in) = &ipv4_static.addresses { for addr in addresses_in { addresses.push(IpInet::from_str(addr.local.as_str()).unwrap()); } } if let Some(routes) = &ipv4_static.routes { for route in routes { new_routes4.push(match route.try_into() { Ok(route) => route, Err(e) => { connection_result.warnings.push(e); continue; } }); } } } if let Some(ipv6_static) = &self.ipv6_static { if let Some(addresses_in) = &ipv6_static.addresses { for addr in addresses_in { addresses.push(IpInet::from_str(addr.local.as_str()).unwrap()); } } if let Some(routes) = &ipv6_static.routes { for route in routes { new_routes6.push(match route.try_into() { Ok(route) => route, Err(e) => { connection_result.warnings.push(e); continue; } }); } } } let routes4 = if !new_routes4.is_empty() { Some(new_routes4) } else { None }; let routes6 = if !new_routes6.is_empty() { Some(new_routes6) } else { None }; connection_result.ip_config = IpConfig { addresses, method4, method6, routes4, routes6, ..Default::default() }; Ok(connection_result) } } impl TryFrom<&Route> for IpRoute { type Error = anyhow::Error; fn try_from(route: &Route) -> Result<Self, Self::Error> { let mut next_hop: Option<IpAddr> = None; if let Some(nexthops) = &route.nexthops { if nexthops.len() > 1 { return Err(anyhow::anyhow!( "Multipath routing isn't natively supported by NetworkManager" )); } else { next_hop = Some(IpAddr::from_str(&nexthops[0].gateway).unwrap()); } } let destination = if route.destination.is_some() { IpInet::from_str(route.destination.clone().unwrap().as_str())? } else if next_hop.is_some() { // default route let default_ip = if next_hop.unwrap().is_ipv4() { IpAddr::from_str("0.0.0.0")? } else { IpAddr::from_str("::")? }; IpInet::new(default_ip, 0)? } else { return Err(anyhow::anyhow!("Error occurred when parsing a route")); }; let metric = route.priority; Ok(IpRoute { destination, next_hop, metric, }) } } #[cfg(test)] mod tests { use super::*; #[allow(dead_code)] fn setup_default_migration_settings() { let _ = MIGRATION_SETTINGS.set(crate::MigrationSettings { continue_migration: false, dry_run: false, activate_connections: true, }); } #[test] fn test_static_interface_to_connection() { setup_default_migration_settings(); let static_interface = Interface { ipv4: Ipv4 { enabled: true }, ipv4_static: Some(Ipv4Static { addresses: Some(vec![Address { local: "127.0.0.1/8".to_string(), }]), routes: Some(vec![Route { nexthops: Some(vec![Nexthop { gateway: "127.0.0.1".to_string(), }]), ..Default::default() }]), }), ipv6: Ipv6 { enabled: true }, ipv6_static: Some(Ipv6Static { addresses: Some(vec![Address { local: "::1/128".to_string(), }]), routes: Some(vec![Route { nexthops: Some(vec![Nexthop { gateway: "::1".to_string(), }]), ..Default::default() }]), }), ..Default::default() }; let static_connection: model::Connection = static_interface.to_connection().unwrap().connections[0].to_owned(); assert_eq!(static_connection.ip_config.method4, Ipv4Method::Manual); assert_eq!( static_connection.ip_config.addresses[0].to_string(), "127.0.0.1/8" ); assert_eq!(static_connection.ip_config.method6, Ipv6Method::Manual); assert_eq!(static_connection.ip_config.addresses[1].to_string(), "::1"); assert_eq!( static_connection.ip_config.addresses[1] .network_length() .to_string(), "128" ); assert!(static_connection.ip_config.routes4.is_some()); assert!(static_connection.ip_config.routes4.clone().unwrap().len() == 1); assert_eq!( static_connection.ip_config.routes4.clone().unwrap()[0] .destination .to_string(), "0.0.0.0/0" ); assert_eq!( static_connection.ip_config.routes4.clone().unwrap()[0] .next_hop .unwrap() .to_string(), "127.0.0.1" ); assert!(static_connection.ip_config.routes6.is_some()); assert!(static_connection.ip_config.routes6.clone().unwrap().len() == 1); assert_eq!( static_connection.ip_config.routes6.clone().unwrap()[0] .destination .to_string(), "::/0" ); assert_eq!( static_connection.ip_config.routes6.clone().unwrap()[0] .next_hop .unwrap() .to_string(), "::1" ); } #[test] fn test_dhcp_interface_to_connection() { setup_default_migration_settings(); let static_interface = Interface { ipv4: Ipv4 { enabled: true }, ipv6: Ipv6 { enabled: true }, ..Default::default() }; let static_connection: model::Connection = static_interface.to_connection().unwrap().connections[0].to_owned(); assert_eq!(static_connection.ip_config.method4, Ipv4Method::Auto); assert_eq!(static_connection.ip_config.method6, Ipv6Method::Auto); assert_eq!(static_connection.ip_config.addresses.len(), 0); } #[test] fn test_dummy_interface_to_connection() { setup_default_migration_settings(); let dummy_interface = Interface { dummy: Some(Dummy { address: Some("12:34:56:78:9A:BC".to_string()), }), ..Default::default() }; let connection: &model::Connection = &dummy_interface.to_connection().unwrap().connections[0]; assert!(matches!(connection.config, model::ConnectionConfig::Dummy)); assert_eq!(connection.mac_address.to_string(), "12:34:56:78:9A:BC"); let dummy_interface = Interface { dummy: Some(Dummy { ..Default::default() }), ..Default::default() }; let connection: &model::Connection = &dummy_interface.to_connection().unwrap().connections[0]; assert!(matches!(connection.config, model::ConnectionConfig::Dummy)); assert_eq!(dummy_interface.dummy.unwrap().address, None); assert!(matches!(connection.mac_address, MacAddress::Unset)); } #[test] fn test_firewall_zone_to_connection() { setup_default_migration_settings(); let ifc = Interface { firewall: Firewall { zone: Some("topsecret".to_string()), }, ..Default::default() }; let con: model::Connection = ifc.to_connection().unwrap().connections[0].to_owned(); assert_eq!(con.firewall_zone, Some("topsecret".to_string())); } } 070701000000B7000081A400000000000000000000000166257103000012B5000000000000000000000000000000000000004A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/src/main.rsmod bond; mod bridge; mod infiniband; mod interface; mod migrate; mod reader; mod vlan; mod wireless; use clap::builder::TypedValueParser; use clap::{Args, Parser, Subcommand}; use log::*; use migrate::migrate; use reader::read as wicked_read; use std::process::{ExitCode, Termination}; use tokio::sync::OnceCell; #[derive(Parser)] #[command(name = "migrate-wicked", version, about, long_about = None)] struct Cli { #[clap(flatten)] global_opts: GlobalOpts, #[command(subcommand)] pub command: Commands, } #[derive(Debug, Args)] struct GlobalOpts { #[arg(long, global = true, default_value_t = LevelFilter::Warn, value_parser = clap::builder::PossibleValuesParser::new(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]).map(|s| s.parse::<LevelFilter>().unwrap()),)] pub log_level: LevelFilter, } #[derive(Subcommand)] pub enum Commands { /// Shows the current xml wicked configuration Show { /// Format output #[arg(value_enum, short, long, default_value_t = Format::Json)] format: Format, /// Wicked XML Files or directories where the wicked xml configs are located paths: Vec<String>, }, /// Migrate wicked state at path Migrate { /// Wicked XML Files or directories where the wicked xml configs are located paths: Vec<String>, /// Continue migration if warnings are encountered #[arg(short, long, global = true, env = "MIGRATE_WICKED_CONTINUE_MIGRATION")] continue_migration: bool, /// Run migration without sending connections to NetworkManager (can be run without NetworkManager installed) #[arg(long, global = true, env = "MIGRATE_WICKED_DRY_RUN")] dry_run: bool, /// Activate connections immediately #[arg(long, global = true, env = "MIGRATE_WICKED_ACTIVATE_CONNECTIONS")] activate_connections: bool, }, } /// Supported output formats #[derive(clap::ValueEnum, Clone)] pub enum Format { Json, PrettyJson, Yaml, Xml, Text, } async fn run_command(cli: Cli) -> anyhow::Result<()> { match cli.command { Commands::Show { paths, format } => { MIGRATION_SETTINGS .set(MigrationSettings { continue_migration: true, dry_run: false, activate_connections: true, }) .expect("MIGRATION_SETTINGS was set too early"); let interfaces_result = wicked_read(paths)?; let output: String = match format { Format::Json => serde_json::to_string(&interfaces_result.interfaces)?, Format::PrettyJson => serde_json::to_string_pretty(&interfaces_result.interfaces)?, Format::Yaml => serde_yaml::to_string(&interfaces_result.interfaces)?, Format::Xml => { quick_xml::se::to_string_with_root("interface", &interfaces_result.interfaces)? } Format::Text => format!("{:?}", interfaces_result.interfaces), }; println!("{}", output); Ok(()) } Commands::Migrate { paths, continue_migration, dry_run, activate_connections, } => { MIGRATION_SETTINGS .set(MigrationSettings { continue_migration, dry_run, activate_connections, }) .expect("MIGRATION_SETTINGS was set too early"); log::debug!( "Running migration with MigrationSettings: {:#?}", MIGRATION_SETTINGS.get().unwrap() ); match migrate(paths).await { Ok(()) => Ok(()), Err(e) => Err(anyhow::anyhow!("Migration failed: {:?}", e)), } } } } /// Represents the result of execution. pub enum CliResult { /// Successful execution. Ok = 0, /// Something went wrong. Error = 1, } impl Termination for CliResult { fn report(self) -> ExitCode { ExitCode::from(self as u8) } } #[derive(Debug)] struct MigrationSettings { continue_migration: bool, dry_run: bool, activate_connections: bool, } static MIGRATION_SETTINGS: OnceCell<MigrationSettings> = OnceCell::const_new(); #[tokio::main] async fn main() -> CliResult { let cli = Cli::parse(); simplelog::TermLogger::init( cli.global_opts.log_level, simplelog::Config::default(), simplelog::TerminalMode::Stderr, simplelog::ColorChoice::Auto, ) .unwrap(); if let Err(error) = run_command(cli).await { eprintln!("{:?}", error); return CliResult::Error; } CliResult::Ok } 070701000000B8000081A400000000000000000000000166257103000010F1000000000000000000000000000000000000004D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/src/migrate.rsuse crate::bridge::BridgePort; use crate::{reader::read as wicked_read, MIGRATION_SETTINGS}; use agama_server::network::model::Connection; use agama_server::network::{Adapter, NetworkManagerAdapter, NetworkState}; use std::{collections::HashMap, error::Error}; use uuid::Uuid; fn update_parent_connection( connections: &mut [Connection], parents: HashMap<String, String>, ) -> Result<(), anyhow::Error> { let settings = MIGRATION_SETTINGS.get().unwrap(); let mut parent_uuid: HashMap<String, Uuid> = HashMap::new(); for (id, parent) in parents { if let Some(parent_con) = connections .iter() .find(|c| c.interface.as_deref() == Some(&parent)) { parent_uuid.insert(id, parent_con.uuid); } else { log::warn!("Missing parent {} connection for {}", parent, id); if !settings.continue_migration { return Err(anyhow::anyhow!("Migration of {} failed", id)); } } } for (id, uuid) in parent_uuid { if let Some(connection) = connections .iter_mut() .find(|c| c.interface.as_deref() == Some(&id)) { connection.controller = Some(uuid); } else { return Err(anyhow::anyhow!( "Unexpected failure - missing connection {}", id )); } } Ok(()) } fn update_bridge_ports( connections: &mut [Connection], portconfigs: HashMap<String, BridgePort>, ) -> Result<(), anyhow::Error> { let settings = MIGRATION_SETTINGS.get().unwrap(); for (ifc, portconfig) in portconfigs { let id; if let Some(c) = connections .iter() .find(|c| c.interface.as_deref() == Some(&ifc)) { id = c.id.clone(); } else { log::warn!("Missing bridge port {} connection", ifc); if !settings.continue_migration { return Err(anyhow::anyhow!( "Migration failed, because bridge is missing port {}", ifc )); } continue; } let c = connections.iter_mut().find(|c| c.id == id).unwrap(); c.port_config = (&portconfig).into(); } Ok(()) } pub async fn migrate(paths: Vec<String>) -> Result<(), Box<dyn Error>> { let interfaces = wicked_read(paths.clone())?; let settings = MIGRATION_SETTINGS.get().unwrap(); let mut parents: HashMap<String, String> = HashMap::new(); let mut bridge_ports: HashMap<String, BridgePort> = HashMap::new(); let mut connections: Vec<Connection> = vec![]; if !settings.continue_migration && interfaces.warning.is_some() { return Err(interfaces.warning.unwrap().into()); } for interface in interfaces.interfaces { let connection_result = interface.to_connection()?; if !connection_result.warnings.is_empty() { for connection_error in &connection_result.warnings { log::warn!("{}", connection_error); } if !settings.continue_migration { return Err(anyhow::anyhow!( "Migration of {} failed", connection_result.connections[0].id ) .into()); } } for connection in connection_result.connections { if let Some(parent) = &interface.link.master { parents.insert(connection.id.clone(), parent.clone()); } connections.push(connection); if let Some(bridge) = &interface.bridge { for port in &bridge.ports { bridge_ports.insert(port.device.clone(), port.clone()); } } } } update_parent_connection(&mut connections, parents)?; update_bridge_ports(&mut connections, bridge_ports)?; let mut state = NetworkState::new(vec![], vec![]); for connection in &connections { state.add_connection(connection.clone())?; } if settings.dry_run { for connection in state.connections { log::debug!("{:#?}", connection); } return Ok(()); } let nm = NetworkManagerAdapter::from_system().await?; nm.write(&state).await?; Ok(()) } 070701000000B9000081A40000000000000000000000016625710300002276000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/src/reader.rsuse crate::interface::Interface; use regex::Regex; use std::fs::{self, read_dir}; use std::path::{Path, PathBuf}; pub struct InterfacesResult { pub interfaces: Vec<Interface>, pub warning: Option<anyhow::Error>, } pub fn read_xml_file(path: PathBuf) -> Result<InterfacesResult, anyhow::Error> { let contents = match fs::read_to_string(path.clone()) { Ok(contents) => contents, Err(e) => { return Err(anyhow::anyhow!( "Couldn't read {}: {}", path.as_path().display(), e )) } }; let replaced_string = replace_colons(contents.as_str()); let deserializer = &mut quick_xml::de::Deserializer::from_str(replaced_string.as_str()); let mut unhandled_fields = vec![]; let interfaces: Vec<Interface> = serde_ignored::deserialize(deserializer, |path| { unhandled_fields.push(path.to_string()); })?; let mut result = InterfacesResult { interfaces, warning: None, }; if !unhandled_fields.is_empty() { for unused_str in unhandled_fields { let split_str = unused_str.split_once('.').unwrap(); log::warn!( "Unhandled field in interface {}: {}", result.interfaces[split_str.0.parse::<usize>().unwrap()].name, split_str.1 ); } result.warning = Some(anyhow::anyhow!("Unhandled fields")) } Ok(result) } fn replace_colons(colon_string: &str) -> String { let re = Regex::new(r"<([\/]?)(\w+):(\w+)\b").unwrap(); let replaced = re.replace_all(colon_string, "<$1$2-$3").to_string(); replaced } // https://stackoverflow.com/a/76820878 fn recurse_files(path: impl AsRef<Path>) -> std::io::Result<Vec<PathBuf>> { let mut buf = vec![]; let entries = read_dir(path)?; for entry in entries { let entry = entry?; let meta = entry.metadata()?; if meta.is_dir() { let mut subdir = recurse_files(entry.path())?; buf.append(&mut subdir); } if meta.is_file() { buf.push(entry.path()); } } Ok(buf) } pub fn read(paths: Vec<String>) -> Result<InterfacesResult, anyhow::Error> { let mut result = InterfacesResult { interfaces: vec![], warning: None, }; for path in paths { let path: PathBuf = path.into(); if path.is_dir() { let files = recurse_files(path)?; for file in files { let mut read_xml = read_xml_file(file)?; if result.warning.is_none() && read_xml.warning.is_some() { result.warning = read_xml.warning } result.interfaces.append(&mut read_xml.interfaces); } } else { let mut read_xml = read_xml_file(path)?; if result.warning.is_none() && read_xml.warning.is_some() { result.warning = read_xml.warning } result.interfaces.append(&mut read_xml.interfaces); } } // Filter loopback as it doesn't need to be migrated result.interfaces.retain(|interface| interface.name != "lo"); Ok(result) } #[cfg(test)] mod tests { use super::*; use crate::bond::*; use crate::interface::*; #[test] fn test_bond_options_from_xml() { let xml = r##" <interface> <name>bond0</name> <bond> <mode>active-backup</mode> <xmit-hash-policy>layer34</xmit-hash-policy> <fail-over-mac>none</fail-over-mac> <packets-per-slave>1</packets-per-slave> <tlb-dynamic-lb>true</tlb-dynamic-lb> <lacp-rate>slow</lacp-rate> <ad-select>bandwidth</ad-select> <ad-user-port-key>5</ad-user-port-key> <ad-actor-sys-prio>7</ad-actor-sys-prio> <ad-actor-system>00:de:ad:be:ef:00</ad-actor-system> <min-links>11</min-links> <primary-reselect>better</primary-reselect> <num-grat-arp>13</num-grat-arp> <num-unsol-na>17</num-unsol-na> <lp-interval>19</lp-interval> <resend-igmp>23</resend-igmp> <all-slaves-active>true</all-slaves-active> <slaves> <slave><device>en0</device></slave> </slaves> <miimon> <frequency>23</frequency> <updelay>27</updelay> <downdelay>31</downdelay> <carrier-detect>ioctl</carrier-detect> </miimon> <arpmon> <interval>23</interval> <validate>filter_backup</validate> <validate-targets>any</validate-targets> <targets> <ipv4-address>1.2.3.4</ipv4-address> <ipv4-address>4.3.2.1</ipv4-address> </targets> </arpmon> <address>02:11:22:33:44:55</address> </bond> </interface> "##; let ifc = quick_xml::de::from_str::<Vec<Interface>>(replace_colons(xml).as_str()) .unwrap() .pop() .unwrap(); assert!(ifc.bond.is_some()); let bond = ifc.bond.unwrap(); assert_eq!( bond, Bond { mode: WickedBondMode::ActiveBackup, xmit_hash_policy: Some(XmitHashPolicy::Layer34), fail_over_mac: Some(FailOverMac::None), packets_per_slave: Some(1), tlb_dynamic_lb: Some(true), lacp_rate: Some(LacpRate::Slow), ad_select: Some(AdSelect::Bandwidth), ad_user_port_key: Some(5), ad_actor_sys_prio: Some(7), ad_actor_system: Some(String::from("00:de:ad:be:ef:00")), min_links: Some(11), primary_reselect: Some(PrimaryReselect::Better), num_grat_arp: Some(13), num_unsol_na: Some(17), lp_interval: Some(19), resend_igmp: Some(23), all_slaves_active: Some(true), slaves: vec![Slave { device: String::from("en0"), primary: None }], miimon: Some(Miimon { frequency: 23, carrier_detect: CarrierDetect::Ioctl, downdelay: Some(31), updelay: Some(27), }), arpmon: Some(ArpMon { interval: 23, validate: ArpValidate::FilterBackup, validate_targets: Some(ArpValidateTargets::Any), targets: vec![String::from("1.2.3.4"), String::from("4.3.2.1")] }), address: Some(String::from("02:11:22:33:44:55")), } ); } /// This test check that the default for stp from wicked is False. #[test] fn test_bridge_default_stp() { let xml = r##" <interface> <name>br0</name> <bridge> <ports> <port> <device>en0</device> </port> </ports> </bridge> </interface> "##; let ifc = quick_xml::de::from_str::<Vec<Interface>>(replace_colons(xml).as_str()) .unwrap() .pop() .unwrap(); assert!(ifc.bridge.is_some()); assert!(!ifc.bridge.unwrap().stp); } #[test] fn test_broken_xml() { let xml = r##" <interface> <name>eth1</name> <ipv4:static> <address>127.0.0.1</> </ipv4:static> </interface> "##; let err = quick_xml::de::from_str::<Vec<Interface>>(replace_colons(xml).as_str()); assert!(err.is_err()); } #[test] fn test_xml_firewall_zone() { let xml = r##" <interface> <name>eth1</name> <firewall> <zone>foo</zone> </firewall> </interface> "##; let ifc = quick_xml::de::from_str::<Vec<Interface>>(replace_colons(xml).as_str()) .unwrap() .pop() .unwrap(); assert_eq!(ifc.firewall.zone, Some("foo".to_string())); } } 070701000000BA000081A40000000000000000000000016625710300000B94000000000000000000000000000000000000004A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/src/vlan.rsuse agama_server::network::model; use serde::{Deserialize, Serialize}; use serde_with::{skip_serializing_none, DeserializeFromStr, SerializeDisplay}; use strum_macros::{Display, EnumString}; #[derive( Debug, Clone, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display, Default, )] #[strum(serialize_all = "kebab_case")] pub enum WickedVlanProtocol { #[default] #[strum(serialize = "ieee802-1Q")] Ieee802_1Q, Ieee802Ad, } #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Vlan { pub device: String, pub address: Option<String>, #[serde(default)] pub protocol: WickedVlanProtocol, pub tag: u16, } impl From<&Vlan> for model::ConnectionConfig { fn from(vlan: &Vlan) -> model::ConnectionConfig { model::ConnectionConfig::Vlan(model::VlanConfig { parent: vlan.device.clone(), id: (vlan.tag as u32), protocol: (&vlan.protocol).into(), }) } } impl From<&WickedVlanProtocol> for model::VlanProtocol { fn from(v: &WickedVlanProtocol) -> model::VlanProtocol { match v { WickedVlanProtocol::Ieee802_1Q => model::VlanProtocol::IEEE802_1Q, WickedVlanProtocol::Ieee802Ad => model::VlanProtocol::IEEE802_1ad, } } } #[cfg(test)] mod tests { use super::*; use crate::interface::*; use crate::MIGRATION_SETTINGS; #[allow(dead_code)] fn setup_default_migration_settings() { let _ = MIGRATION_SETTINGS.set(crate::MigrationSettings { continue_migration: false, dry_run: false, activate_connections: true, }); } #[test] fn test_vlan_protocol() { setup_default_migration_settings(); let v: WickedVlanProtocol = Default::default(); assert_eq!(v, WickedVlanProtocol::Ieee802_1Q); assert_eq!(v.to_string(), String::from("ieee802-1Q")); let v = WickedVlanProtocol::Ieee802Ad; assert_eq!(v.to_string(), String::from("ieee802-ad")); } #[test] fn test_vlan_interface() { setup_default_migration_settings(); let vlan_interface = Interface { vlan: Some(Vlan { device: String::from("en0"), tag: 10, protocol: WickedVlanProtocol::Ieee802Ad, address: Some(String::from("02:11:22:33:44:55")), }), ..Default::default() }; let ifc = vlan_interface.to_connection(); assert!(ifc.is_ok()); let ifc = &ifc.unwrap().connections[0]; assert!(matches!(ifc.config, model::ConnectionConfig::Vlan(_))); if let model::ConnectionConfig::Vlan(v) = &ifc.config { assert_eq!(v.id, 10); assert_eq!(v.protocol, model::VlanProtocol::IEEE802_1ad); assert_eq!(v.parent, "en0"); } assert_eq!(ifc.mac_address.to_string(), "02:11:22:33:44:55"); } } 070701000000BB000081A40000000000000000000000016625710300002789000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/src/wireless.rsuse crate::MIGRATION_SETTINGS; use agama_lib::network::types::SSID; use agama_server::network::model::{self, WEPAuthAlg, WEPKeyType, WEPSecurity}; use anyhow::anyhow; use macaddr::MacAddr6; use serde::{Deserialize, Deserializer, Serialize}; use serde_with::formats::CommaSeparator; use serde_with::StringWithSeparator; use serde_with::{serde_as, skip_serializing_none, DeserializeFromStr, SerializeDisplay}; use std::str::FromStr; use strum_macros::{Display, EnumString}; #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Wireless { #[serde(rename = "ap-scan")] pub ap_scan: u32, #[serde(default)] #[serde(deserialize_with = "unwrap_wireless_networks")] pub networks: Option<Vec<Network>>, } #[serde_as] #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Network { pub essid: String, #[serde(rename = "scan-ssid")] pub scan_ssid: bool, pub mode: WickedWirelessMode, #[serde(rename = "wpa-psk")] pub wpa_psk: Option<WpaPsk>, #[serde(default)] #[serde(rename = "key-management")] #[serde_as(as = "StringWithSeparator::<CommaSeparator, String>")] pub key_management: Vec<String>, pub channel: Option<u32>, #[serde(rename = "access-point")] pub access_point: Option<String>, pub wep: Option<Wep>, } #[derive(Default, Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] #[strum(serialize_all = "kebab-case")] pub enum WickedWirelessMode { AdHoc = 0, #[default] Infrastructure = 1, AP = 2, } impl From<&WickedWirelessMode> for model::WirelessMode { fn from(value: &WickedWirelessMode) -> Self { match value { WickedWirelessMode::AdHoc => model::WirelessMode::AdHoc, WickedWirelessMode::Infrastructure => model::WirelessMode::Infra, WickedWirelessMode::AP => model::WirelessMode::AP, } } } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct WpaPsk { pub passphrase: String, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Wep { #[serde(rename = "auth-algo")] pub auth_algo: String, #[serde(rename = "default-key")] pub default_key: u32, pub key: Vec<String>, } fn unwrap_wireless_networks<'de, D>(deserializer: D) -> Result<Option<Vec<Network>>, D::Error> where D: Deserializer<'de>, { #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] struct Networks { network: Vec<Network>, } Ok(Some(Networks::deserialize(deserializer)?.network)) } fn wireless_security_protocol( wicked_value: &[String], ) -> Result<model::SecurityProtocol, anyhow::Error> { if wicked_value.contains(&"wpa-psk".to_string()) || wicked_value.contains(&"wpa-psk-sha256".to_string()) { Ok(model::SecurityProtocol::WPA2) } else if wicked_value.contains(&"sae".to_string()) { Ok(model::SecurityProtocol::WPA3Personal) } else if wicked_value.contains(&"wpa-eap".to_string()) || wicked_value.contains(&"wpa-eap-sha256".to_string()) { Ok(model::SecurityProtocol::WPA2Enterprise) } else if wicked_value.contains(&"owe".to_string()) { Ok(model::SecurityProtocol::OWE) } else if wicked_value.contains(&"wpa-eap-suite-b-192".to_string()) { Ok(model::SecurityProtocol::WPA3Only) } else if wicked_value.contains(&"none".to_string()) { Ok(model::SecurityProtocol::WEP) } else { Err(anyhow!("Unrecognized key-management protocol")) } } impl TryFrom<&Network> for model::ConnectionConfig { type Error = anyhow::Error; fn try_from(network: &Network) -> Result<Self, Self::Error> { let settings = MIGRATION_SETTINGS.get().unwrap(); let mut config = model::WirelessConfig { ssid: SSID(network.essid.as_bytes().to_vec()), hidden: network.scan_ssid, ..Default::default() }; if network.key_management.len() > 1 && settings.continue_migration { log::warn!("Migration of multiple key-management algorithms isn't supported") } else if network.key_management.len() > 1 { return Err(anyhow!( "Migration of multiple key-management algorithms isn't supported" )); } config.security = wireless_security_protocol(&network.key_management)?; if let Some(wpa_psk) = &network.wpa_psk { config.password = Some(wpa_psk.passphrase.clone()) } if let Some(channel) = network.channel { config.channel = Some(channel); if channel <= 14 { config.band = Some("bg".try_into().unwrap()); } else { config.band = Some("a".try_into().unwrap()); } log::warn!( "NetworkManager requires setting a band for wireless when a channel is set. The band has been set to \"{}\". This may in certain regions be incorrect.", config.band.unwrap() ); } if let Some(access_point) = &network.access_point { config.bssid = Some(MacAddr6::from_str(access_point)?); } if let Some(wep) = &network.wep { // filter out `s:`, `h:`, `:`, and `-` of wep keys let keys: Vec<String> = wep .key .clone() .into_iter() .map(|mut x| { x = x.replace("s:", ""); x = x.replace("h:", ""); x = x.replace(':', ""); x.replace('-', "") }) .collect(); let wep_security = WEPSecurity { auth_alg: WEPAuthAlg::try_from(wep.auth_algo.as_str())?, wep_key_type: WEPKeyType::Key, keys, wep_key_index: wep.default_key, }; config.wep_security = Some(wep_security); } config.mode = (&network.mode).into(); Ok(model::ConnectionConfig::Wireless(config)) } } #[cfg(test)] mod tests { use super::*; use crate::interface::*; use crate::MIGRATION_SETTINGS; #[allow(dead_code)] fn setup_default_migration_settings() { let _ = MIGRATION_SETTINGS.set(crate::MigrationSettings { continue_migration: false, dry_run: false, activate_connections: true, }); } #[test] fn test_wireless_bands() { setup_default_migration_settings(); let mut wireless_interface = Interface { wireless: Some(Wireless { networks: Some(vec![Network { channel: Some(0), essid: "testssid".to_string(), scan_ssid: false, mode: WickedWirelessMode::AP, wpa_psk: None, key_management: vec!["wpa-psk".to_string()], access_point: None, wep: None, }]), ap_scan: 0, }), ..Default::default() }; let connections = wireless_interface.to_connection(); assert!(connections.is_ok()); let connection = &connections.unwrap().connections[0]; let model::ConnectionConfig::Wireless(wireless) = &connection.config else { panic!() }; assert_eq!(wireless.band, Some("bg".try_into().unwrap())); wireless_interface .wireless .as_mut() .unwrap() .networks .as_mut() .unwrap()[0] .channel = Some(32); let ifc = wireless_interface.to_connection(); assert!(ifc.is_ok()); let ifc = &ifc.unwrap().connections[0]; let model::ConnectionConfig::Wireless(wireless) = &ifc.config else { panic!() }; assert_eq!(wireless.band, Some("a".try_into().unwrap())); } #[test] fn test_wireless_migration() { setup_default_migration_settings(); let wireless_interface = Interface { wireless: Some(Wireless { networks: Some(vec![Network { essid: "testssid".to_string(), scan_ssid: true, mode: WickedWirelessMode::Infrastructure, wpa_psk: Some(WpaPsk { passphrase: "testpassword".to_string(), }), key_management: vec!["wpa-psk".to_string()], channel: Some(14), access_point: Some("12:34:56:78:9A:BC".to_string()), wep: Some(Wep { auth_algo: "open".to_string(), default_key: 1, key: vec!["01020304ff".to_string(), "s:hello".to_string()], }), }]), ap_scan: 0, }), ..Default::default() }; let connections = wireless_interface.to_connection(); assert!(connections.is_ok()); let connection = &connections.unwrap().connections[0]; let model::ConnectionConfig::Wireless(wireless) = &connection.config else { panic!() }; assert_eq!(wireless.ssid, SSID("testssid".as_bytes().to_vec())); assert!(wireless.hidden); assert_eq!(wireless.mode, model::WirelessMode::Infra); assert_eq!(wireless.password, Some("testpassword".to_string())); assert_eq!(wireless.security, model::SecurityProtocol::WPA2); assert_eq!( wireless.bssid, Some(MacAddr6::from_str("12:34:56:78:9A:BC").unwrap()) ); assert_eq!( wireless.wep_security, Some(WEPSecurity { auth_alg: WEPAuthAlg::Open, wep_key_type: WEPKeyType::Key, keys: vec!["01020304ff".to_string(), "hello".to_string()], wep_key_index: 1, }) ); assert_eq!(wireless.band, Some("bg".try_into().unwrap())); } } 070701000000BC000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004400000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests070701000000BD000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005700000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bond_active-backup070701000000BE000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bond_active-backup/system-connections070701000000BF000081A4000000000000000000000001662571030000012C000000000000000000000000000000000000007D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bond_active-backup/system-connections/bond0.nmconnection[connection] id=bond0 uuid=766a5c05-2ba1-45be-960d-9f2dbf89b293 type=bond interface-name=bond0 zone=public [ethernet] cloned-mac-address=02:00:33:44:55:11 [bond] miimon=100 mode=active-backup primary=en0 use_carrier=1 [match] [ipv4] method=auto [ipv6] addr-gen-mode=default method=auto [proxy] 070701000000C0000081A40000000000000000000000016625710300000091000000000000000000000000000000000000007B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bond_active-backup/system-connections/en0.nmconnection[connection] id=en0 uuid=38d979e6-aa0e-4bae-8cff-a6dda281d0c7 type=ethernet interface-name=en0 master=bond0 slave-type=bond [ethernet] [match] 070701000000C1000081A40000000000000000000000016625710300000091000000000000000000000000000000000000007B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bond_active-backup/system-connections/en1.nmconnection[connection] id=en1 uuid=9269bbb4-a771-406f-ba66-3f65ed15634c type=ethernet interface-name=en1 master=bond0 slave-type=bond [ethernet] [match] 070701000000C2000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006200000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bond_active-backup/wicked_xml070701000000C3000081A400000000000000000000000166257103000007CD000000000000000000000000000000000000007900000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bond_active-backup/wicked_xml/bond_active-backup.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-bond0"> <name>bond0</name> <control> <mode>boot</mode> </control> <firewall> <zone>public</zone> </firewall> <bond> <mode>active-backup</mode> <miimon> <frequency>100</frequency> <carrier-detect>netif</carrier-detect> </miimon> <slaves> <slave> <device>en0</device> <primary>true</primary> </slave> <slave> <device>en1</device> </slave> </slaves> <address>02:00:33:44:55:11</address> </bond> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv4:dhcp> <enabled>true</enabled> <flags>group</flags> <update>default-route,hostname,dns,nis,ntp,smb,nds,mtu,tz,boot</update> <defer-timeout>15</defer-timeout> <recover-lease>true</recover-lease> <release-lease>false</release-lease> </ipv4:dhcp> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> <ipv6:dhcp> <enabled>true</enabled> <flags>group</flags> <update>dns,nis,ntp,tz,boot</update> <mode>auto</mode> <rapid-commit>true</rapid-commit> <defer-timeout>15</defer-timeout> <recover-lease>true</recover-lease> <refresh-lease>false</refresh-lease> <release-lease>false</release-lease> </ipv6:dhcp> </interface> <interface origin="compat:suse:/etc/sysconfig/network/ifcfg-bond0"> <name>en0</name> <control> <mode>hotplug</mode> </control> <link> <master>bond0</master> </link> <ipv4> <enabled>false</enabled> </ipv4> <ipv6> <enabled>false</enabled> </ipv6> </interface> <interface origin="compat:suse:/etc/sysconfig/network/ifcfg-bond0"> <name>en1</name> <control> <mode>hotplug</mode> </control> <link> <master>bond0</master> </link> <ipv4> <enabled>false</enabled> </ipv4> <ipv6> <enabled>false</enabled> </ipv6> </interface> 070701000000C4000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bridge1070701000000C5000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bridge1/system-connections070701000000C6000081A40000000000000000000000016625710300000125000000000000000000000000000000000000007000000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bridge1/system-connections/br0.nmconnection[connection] id=br0 uuid=1f4f97a3-2928-473b-abc0-3d4dc89b19ae type=bridge interface-name=br0 [ethernet] [bridge] ageing-time=1 forward-delay=3 hello-time=1 max-age=40 priority=5 stp=false [match] [ipv4] address1=10.0.0.1/24 method=manual [ipv6] addr-gen-mode=default method=auto [proxy] 070701000000C7000081A400000000000000000000000166257103000000B7000000000000000000000000000000000000007000000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bridge1/system-connections/en0.nmconnection[connection] id=en0 uuid=4c941c30-b610-470a-b24a-5a6c73ab6710 type=ethernet interface-name=en0 master=br0 slave-type=bridge [ethernet] [bridge-port] path-cost=2 priority=9 [match] 070701000000C8000081A400000000000000000000000166257103000000AB000000000000000000000000000000000000007000000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bridge1/system-connections/en1.nmconnection[connection] id=en1 uuid=f760dace-c275-4983-ab36-51c8e9642dfc type=ethernet interface-name=en1 master=br0 slave-type=bridge [ethernet] [bridge-port] priority=0 [match] 070701000000C9000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005700000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bridge1/wicked_xml070701000000CA000081A400000000000000000000000166257103000005EB000000000000000000000000000000000000006300000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/bridge1/wicked_xml/bridge1.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-en0"> <name>en0</name> <control> <mode>boot</mode> </control> <firewall/> <link> <master>br0</master> </link> <ipv4> <enabled>false</enabled> </ipv4> <ipv6> <enabled>false</enabled> </ipv6> </interface> <interface origin="compat:suse:/etc/sysconfig/network/ifcfg-en1"> <name>en1</name> <control> <mode>boot</mode> </control> <firewall/> <link> <master>br0</master> </link> <ipv4> <enabled>false</enabled> </ipv4> <ipv6> <enabled>false</enabled> </ipv6> </interface> <interface origin="compat:suse:/etc/sysconfig/network/ifcfg-br0"> <name>br0</name> <control> <mode>boot</mode> </control> <firewall/> <bridge> <stp>false</stp> <priority>5</priority> <forward-delay>3.20</forward-delay> <aging-time>1.20</aging-time> <hello-time>1.10</hello-time> <max-age>40.30</max-age> <ports> <port> <device>en0</device> <priority>9</priority> <path-cost>2</path-cost> </port> <port> <device>en1</device> <priority>0</priority> </port> </ports> </bridge> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv4:static> <address> <local>10.0.0.1/24</local> </address> </ipv4:static> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> </interface> 070701000000CB000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/dummy070701000000CC000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/dummy/system-connections070701000000CD000081A40000000000000000000000016625710300000121000000000000000000000000000000000000007100000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/dummy/system-connections/dummy0.nmconnection[connection] id=dummy0 uuid=d5c90a99-bde4-4c92-884c-d8208d8fb6da type=dummy interface-name=dummy0 [ethernet] cloned-mac-address=12:34:56:78:9A:BC [dummy] [match] [ipv4] address1=10.0.0.100/24 method=manual [ipv6] addr-gen-mode=default address1=2001:db8:1::1/64 method=manual [proxy] 070701000000CE000081A400000000000000000000000166257103000000FC000000000000000000000000000000000000007100000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/dummy/system-connections/dummy1.nmconnection[connection] id=dummy1 uuid=b551cd5a-d139-4297-a79f-d7dd33a97879 type=dummy interface-name=dummy1 [ethernet] [dummy] [match] [ipv4] address1=10.0.0.101/24 method=manual [ipv6] addr-gen-mode=default address1=2001:db8:1::2/64 method=manual [proxy] 070701000000CF000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005500000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/dummy/wicked_xml070701000000D0000081A40000000000000000000000016625710300000495000000000000000000000000000000000000005F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/dummy/wicked_xml/dummy.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-dummy0"> <name>dummy0</name> <control> <mode>boot</mode> </control> <firewall/> <dummy> <address>12:34:56:78:9A:BC</address> </dummy> <link/> <ipv4> <enabled>true</enabled> </ipv4> <ipv4:static> <address> <local>10.0.0.100/24</local> </address> </ipv4:static> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> <ipv6:static> <address> <local>2001:db8:1::1/64</local> </address> </ipv6:static> </interface> <interface origin="compat:suse:/etc/sysconfig/network/ifcfg-dummy1"> <name>dummy1</name> <control> <mode>boot</mode> </control> <firewall/> <dummy/> <link/> <ipv4> <enabled>true</enabled> </ipv4> <ipv4:static> <address> <local>10.0.0.101/24</local> </address> </ipv4:static> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> <ipv6:static> <address> <local>2001:db8:1::2/64</local> </address> </ipv6:static> </interface> 070701000000D1000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005900000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ethernet_mac_address070701000000D2000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ethernet_mac_address/system-connections070701000000D3000081A400000000000000000000000166257103000000FE000000000000000000000000000000000000007E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ethernet_mac_address/system-connections/eth1.nmconnection[connection] id=eth1 uuid=4935f592-3e86-491b-847b-2ce4f180001c type=ethernet interface-name=eth1 [ethernet] cloned-mac-address=12:34:56:78:9A:BC [match] [ipv4] address1=192.168.100.5/24 method=manual [ipv6] addr-gen-mode=default method=auto [proxy] 070701000000D4000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006400000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ethernet_mac_address/wicked_xml070701000000D5000081A40000000000000000000000016625710300000227000000000000000000000000000000000000007400000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ethernet_mac_address/wicked_xml/ipv4_static.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth1"> <name>eth1</name> <control> <mode>boot</mode> </control> <firewall/> <ethernet> <address>12:34:56:78:9a:bc</address> </ethernet> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv4:static> <address> <local>192.168.100.5/24</local> </address> </ipv4:static> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> </interface> 070701000000D6000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005200000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/firewall_zone070701000000D7000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006500000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/firewall_zone/system-connections070701000000D8000081A400000000000000000000000166257103000000D4000000000000000000000000000000000000007700000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/firewall_zone/system-connections/eth1.nmconnection[connection] id=eth1 uuid=17dad0f7-8cad-411a-9ae4-af22f8feb67c type=ethernet interface-name=eth1 zone=topsecret [ethernet] [match] [ipv4] method=disabled [ipv6] addr-gen-mode=default method=disabled [proxy] 070701000000D9000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/firewall_zone/wicked_xml070701000000DA000081A400000000000000000000000166257103000000D3000000000000000000000000000000000000006F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/firewall_zone/wicked_xml/firewall_zone.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth1"> <name>eth1</name> <control> <mode>boot</mode> </control> <firewall> <zone>topsecret</zone> </firewall> <link/> </interface> 070701000000DB000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/infiniband070701000000DC000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006200000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/infiniband/system-connections070701000000DD000081A40000000000000000000000016625710300000101000000000000000000000000000000000000007800000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/infiniband/system-connections/ib0.8001.nmconnection[connection] id=ib0.8001 uuid=9b67f665-65f9-46a5-a8f2-312a25c76848 type=infiniband interface-name=ib0.8001 [infiniband] p-key=32769 parent=ib0 transport-mode=connected [match] [ipv4] method=disabled [ipv6] addr-gen-mode=default method=disabled [proxy] 070701000000DE000081A40000000000000000000000016625710300000100000000000000000000000000000000000000007800000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/infiniband/system-connections/ib0.8002.nmconnection[connection] id=ib0.8002 uuid=01ee4005-008c-487e-a288-13bc1aca0d71 type=infiniband interface-name=ib0.8002 [infiniband] p-key=32770 parent=ib0 transport-mode=datagram [match] [ipv4] method=disabled [ipv6] addr-gen-mode=default method=disabled [proxy] 070701000000DF000081A400000000000000000000000166257103000000DF000000000000000000000000000000000000007300000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/infiniband/system-connections/ib0.nmconnection[connection] id=ib0 uuid=e700a7e8-3598-4554-a8d8-8f1f494573f7 type=infiniband interface-name=ib0 [infiniband] transport-mode=datagram [match] [ipv4] method=disabled [ipv6] addr-gen-mode=default method=disabled [proxy] 070701000000E0000081A400000000000000000000000166257103000000DF000000000000000000000000000000000000007300000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/infiniband/system-connections/ib1.nmconnection[connection] id=ib1 uuid=d0364ba4-f028-42b1-9bfd-c584db341c59 type=infiniband interface-name=ib1 [infiniband] transport-mode=datagram [match] [ipv4] method=disabled [ipv6] addr-gen-mode=default method=disabled [proxy] 070701000000E1000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005A00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/infiniband/wicked_xml070701000000E2000081A4000000000000000000000001662571030000059D000000000000000000000000000000000000006900000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/infiniband/wicked_xml/infiniband.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-ib0"> <name>ib0</name> <control> <mode>boot</mode> </control> <firewall/> <infiniband/> <link/> <ipv4> <enabled>false</enabled> </ipv4> <ipv6> <enabled>false</enabled> </ipv6> </interface> <interface origin="compat:suse:/etc/sysconfig/network/ifcfg-ib0.8001"> <name>ib0.8001</name> <control> <mode>boot</mode> </control> <firewall/> <infiniband:child> <device>ib0</device> <pkey>0x8001</pkey> <mode>connected</mode> <multicast>allowed</multicast> </infiniband:child> <link/> <ipv4> <enabled>false</enabled> </ipv4> <ipv6> <enabled>false</enabled> </ipv6> </interface> <interface origin="compat:suse:/etc/sysconfig/network/ifcfg-ib0.8002"> <name>ib0.8002</name> <control> <mode>boot</mode> </control> <firewall/> <infiniband:child> <device>ib0</device> <pkey>0x8002</pkey> </infiniband:child> <link/> <ipv4> <enabled>false</enabled> </ipv4> <ipv6> <enabled>false</enabled> </ipv6> </interface> <interface origin="compat:suse:/etc/sysconfig/network/ifcfg-ib1"> <name>ib1</name> <control> <mode>boot</mode> </control> <firewall/> <infiniband> <mode>datagram</mode> <multicast>disallowed</multicast> </infiniband> <link/> <ipv4> <enabled>false</enabled> </ipv4> <ipv6> <enabled>false</enabled> </ipv6> </interface> 070701000000E3000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv4+6_dhcp070701000000E4000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006300000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv4+6_dhcp/system-connections070701000000E5000081A400000000000000000000000166257103000000BD000000000000000000000000000000000000007500000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv4+6_dhcp/system-connections/eth0.nmconnection[connection] id=eth0 uuid=a123047b-232e-4b86-9f69-851e7690deaa type=ethernet interface-name=eth0 [ethernet] [match] [ipv4] method=auto [ipv6] addr-gen-mode=default method=auto [proxy] 070701000000E6000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv4+6_dhcp/wicked_xml070701000000E7000081A4000000000000000000000001662571030000041A000000000000000000000000000000000000006B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv4+6_dhcp/wicked_xml/ipv4+6_dhcp.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth0"> <name>eth0</name> <control> <mode>boot</mode> </control> <firewall/> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv4:dhcp> <enabled>true</enabled> <flags>group</flags> <update>default-route,dns,nis,ntp,nds,mtu,tz,boot</update> <hostname>dev2</hostname> <defer-timeout>15</defer-timeout> <recover-lease>true</recover-lease> <release-lease>false</release-lease> </ipv4:dhcp> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> <ipv6:dhcp> <enabled>true</enabled> <flags>group</flags> <update>dns,nis,ntp,tz,boot</update> <mode>auto</mode> <rapid-commit>true</rapid-commit> <hostname>dev2</hostname> <defer-timeout>15</defer-timeout> <recover-lease>true</recover-lease> <refresh-lease>false</refresh-lease> <release-lease>false</release-lease> </ipv6:dhcp> </interface> 070701000000E8000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv4_static070701000000E9000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006300000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv4_static/system-connections070701000000EA000081A400000000000000000000000166257103000000F3000000000000000000000000000000000000007500000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv4_static/system-connections/eth1.nmconnection[connection] id=eth1 uuid=4935f592-3e86-491b-847b-2ce4f180001c type=ethernet interface-name=eth1 [ethernet] [match] [ipv4] address1=192.168.100.5/24 address2=192.168.101.5/24 method=manual [ipv6] addr-gen-mode=default method=auto [proxy] 070701000000EB000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv4_static/wicked_xml070701000000EC000081A40000000000000000000000016625710300000226000000000000000000000000000000000000006B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv4_static/wicked_xml/ipv4_static.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth1"> <name>eth1</name> <control> <mode>boot</mode> </control> <firewall/> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv4:static> <address> <local>192.168.100.5/24</local> </address> <address> <local>192.168.101.5/24</local> </address> </ipv4:static> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> </interface> 070701000000ED000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_auto070701000000EE000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006100000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_auto/system-connections070701000000EF000081A400000000000000000000000166257103000000BD000000000000000000000000000000000000007300000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_auto/system-connections/eth2.nmconnection[connection] id=eth2 uuid=85378eec-2abb-4cbb-a3d3-69c10921b2e0 type=ethernet interface-name=eth2 [ethernet] [match] [ipv4] method=auto [ipv6] addr-gen-mode=default method=auto [proxy] 070701000000F0000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005900000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_auto/wicked_xml070701000000F1000081A400000000000000000000000166257103000001D1000000000000000000000000000000000000006700000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_auto/wicked_xml/ipv6_auto.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth2"> <name>eth2</name> <control> <mode>boot</mode> </control> <firewall/> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> <ipv6:auto> <enabled>true</enabled> <update>dns</update> </ipv6:auto> </interface> 070701000000F2000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_dhcp070701000000F3000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006100000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_dhcp/system-connections070701000000F4000081A400000000000000000000000166257103000000BD000000000000000000000000000000000000007300000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_dhcp/system-connections/eth3.nmconnection[connection] id=eth3 uuid=ee291fe1-7dd3-4376-9c63-4228fab9df27 type=ethernet interface-name=eth3 [ethernet] [match] [ipv4] method=auto [ipv6] addr-gen-mode=default method=auto [proxy] 070701000000F5000081A400000000000000000000000166257103000000BD000000000000000000000000000000000000007300000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_dhcp/system-connections/eth4.nmconnection[connection] id=eth4 uuid=ee291fe1-7dd3-4376-9c63-4228fab9df27 type=ethernet interface-name=eth4 [ethernet] [match] [ipv4] method=auto [ipv6] addr-gen-mode=default method=dhcp [proxy] 070701000000F6000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005900000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_dhcp/wicked_xml070701000000F7000081A400000000000000000000000166257103000005B9000000000000000000000000000000000000006700000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_dhcp/wicked_xml/ipv6_dhcp.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth3"> <name>eth3</name> <control> <mode>boot</mode> </control> <firewall/> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> <ipv6:dhcp> <enabled>true</enabled> <update>dns,nis,ntp,tz,boot</update> <mode>auto</mode> <rapid-commit>true</rapid-commit> <hostname>dev3</hostname> <defer-timeout>15</defer-timeout> <recover-lease>true</recover-lease> <refresh-lease>false</refresh-lease> <release-lease>false</release-lease> </ipv6:dhcp> </interface> <interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth4"> <name>eth4</name> <control> <mode>boot</mode> </control> <firewall/> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> <ipv6:dhcp> <enabled>true</enabled> <update>dns,nis,ntp,tz,boot</update> <mode>managed</mode> <rapid-commit>true</rapid-commit> <hostname>dev3</hostname> <defer-timeout>15</defer-timeout> <recover-lease>true</recover-lease> <refresh-lease>false</refresh-lease> <release-lease>false</release-lease> </ipv6:dhcp> </interface> 070701000000F8000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_static070701000000F9000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006300000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_static/system-connections070701000000FA000081A400000000000000000000000166257103000000F7000000000000000000000000000000000000007500000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_static/system-connections/eth4.nmconnection[connection] id=eth4 uuid=79dac508-d8ae-4bae-82bc-8590c9e501cd type=ethernet interface-name=eth4 [ethernet] [match] [ipv4] method=auto [ipv6] addr-gen-mode=default address1=2001:db8:0:1::5/64 address2=2001:db8:0:2::5/64 method=manual [proxy] 070701000000FB000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_static/wicked_xml070701000000FC000081A4000000000000000000000001662571030000022A000000000000000000000000000000000000006B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/ipv6_static/wicked_xml/ipv6_static.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth4"> <name>eth4</name> <control> <mode>boot</mode> </control> <firewall/> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> <ipv6:static> <address> <local>2001:db8:0:1::5/64</local> </address> <address> <local>2001:db8:0:2::5/64</local> </address> </ipv6:static> </interface> 070701000000FD000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/multipath_routing_failure070701000000FE000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006900000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/multipath_routing_failure/wicked_xml070701000000FF000081A4000000000000000000000001662571030000043B000000000000000000000000000000000000007C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/multipath_routing_failure/wicked_xml/single_gateway.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth0"> <name>eth0</name> <control> <mode>boot</mode> </control> <firewall/> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv4:static> <address> <local>192.168.101.5/24</local> </address> <address> <local>192.168.102.5/24</local> </address> <route> <destination>192.168.101.0/24</destination> </route> <route> <destination>192.168.102.0/24</destination> </route> <route> <nexthop> <gateway>192.168.102.1</gateway> </nexthop> <nexthop> <gateway>192.168.102.2</gateway> </nexthop> <priority>1</priority> </route> </ipv4:static> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> </ipv6> <ipv6:static> <address> <local>2001:db8:1::5/64</local> </address> <route> <nexthop> <gateway>2001:db8:1::1</gateway> </nexthop> <priority>1</priority> </route> </ipv6:static> </interface> 07070100000100000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004B00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/routes07070100000101000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/routes/system-connections07070100000102000081A40000000000000000000000016625710300000233000000000000000000000000000000000000007000000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/routes/system-connections/eth0.nmconnection[connection] id=eth0 uuid=e7736944-bd8b-4da3-8be9-84b333b2ccfa type=ethernet interface-name=eth0 [ethernet] [match] [ipv4] address1=192.168.101.5/24 address2=192.168.102.5/24 method=manual route1=192.168.101.0/24,192.168.101.1 route2=192.168.102.0/24,192.168.102.1,1 route3=192.168.102.0/24,192.168.102.2,2 route4=192.168.103.0/24,192.168.101.1 route5=192.168.104.0/24,192.168.102.1 route6=0.0.0.0/0,192.168.102.1,1 [ipv6] addr-gen-mode=default address1=2001:db8:1::5/64 method=manual route1=::/0,2001:db8:1::1 route2=2001:db8:1::/64,2001:db8:1::1,1 [proxy] 07070100000103000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005600000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/routes/wicked_xml07070100000104000081A40000000000000000000000016625710300000741000000000000000000000000000000000000006100000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/routes/wicked_xml/routes.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth0"> <name>eth0</name> <control> <mode>boot</mode> </control> <firewall/> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv4:static> <address> <local>192.168.101.5/24</local> </address> <address> <local>192.168.102.5/24</local> </address> <route> <destination>192.168.101.0/24</destination> <nexthop> <gateway>192.168.101.1</gateway> </nexthop> </route> <route> <destination>192.168.102.0/24</destination> <nexthop> <gateway>192.168.102.1</gateway> </nexthop> <priority>1</priority> </route> <route> <destination>192.168.102.0/24</destination> <nexthop> <gateway>192.168.102.2</gateway> </nexthop> <priority>2</priority> </route> <route> <destination>192.168.103.0/24</destination> <nexthop> <gateway>192.168.101.1</gateway> </nexthop> </route> <route> <destination>192.168.104.0/24</destination> <nexthop> <gateway>192.168.102.1</gateway> </nexthop> </route> <route> <nexthop> <gateway>192.168.102.1</gateway> </nexthop> <priority>1</priority> </route> </ipv4:static> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> <ipv6:static> <address> <local>2001:db8:1::5/64</local> </address> <route> <nexthop> <gateway>2001:db8:1::1</gateway> </nexthop> </route> <route> <destination>2001:db8:1::/64</destination> <nexthop> <gateway>2001:db8:1::1</gateway> </nexthop> <priority>1</priority> </route> </ipv6:static> </interface> 07070100000105000081ED0000000000000000000000016625710300000982000000000000000000000000000000000000004C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/test.sh#!/bin/bash RED='\033[0;31m' GREEN='\033[0;32m' BOLD='\033[1m' NC='\033[0m' RESULT=0 MIGRATE_WICKED_BIN=../target/debug/migrate-wicked SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) cd $SCRIPT_DIR TEST_DIRS=${TEST_DIRS:-$(ls -d */ | sed 's#/##')} NO_CLEANUP=${NO_CLEANUP:-0} error_msg() { echo -e "${RED}Error for test $1: $2${NC}" } if [[ $(ls -A /etc/NetworkManager/system-connections/) ]]; then echo -e "${RED}There are already NM connections. You may be running this script on a live system, which is highly discouraged!${NC}" exit 1 fi nm_connections="$(nmcli connection | tail -n +2 | awk '{print $1}')"; nm_cleanup() { for i in $(nmcli connection | tail -n +2 | awk '{print $1}'); do if ! printf '%s\0' "${nm_connections[@]}" | grep -qwz $i; then nmcli connection delete "$i" fi done } if [ ! -f $MIGRATE_WICKED_BIN ]; then echo -e "${RED}No migrate-wicked binary found${NC}" exit 1 fi for test_dir in ${TEST_DIRS}; do echo -e "${BOLD}Testing ${test_dir}${NC}" if [[ $test_dir == *"failure" ]]; then expect_fail=true else expect_fail=false fi $MIGRATE_WICKED_BIN show $test_dir/wicked_xml if [ $? -ne 0 ] && [ "$expect_fail" = false ]; then error_msg ${test_dir} "show failed" RESULT=1 fi if [ "$expect_fail" = true ]; then $MIGRATE_WICKED_BIN migrate $test_dir/wicked_xml else $MIGRATE_WICKED_BIN migrate -c $test_dir/wicked_xml fi if [ $? -ne 0 ] && [ "$expect_fail" = false ]; then error_msg ${test_dir} "migration failed" RESULT=1 continue elif [ $? -ne 0 ] && [ "$expect_fail" = true ]; then echo -e "${GREEN}Migration for $test_dir failed as expected${NC}" fi for cmp_file in $(ls -1 $test_dir/system-connections/); do diff --unified=0 --color=always -I uuid $test_dir/system-connections/$cmp_file /etc/NetworkManager/system-connections/${cmp_file} if [ $? -ne 0 ]; then error_msg ${test_dir} "$cmp_file didn't match" RESULT=1 else echo -e "${GREEN}Migration for connection ${cmp_file/\.nmconnection/} successful${NC}" fi done [ "$NO_CLEANUP" -gt 0 ] || nm_cleanup done if [ $RESULT -eq 0 ]; then echo -e "${GREEN}All tests successful${NC}" else echo -e "${RED}Some tests failed${NC}" fi exit $RESULT 07070100000106000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004900000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/vlan07070100000107000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/vlan/system-connections07070100000108000081A4000000000000000000000001662571030000012D000000000000000000000000000000000000007100000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/vlan/system-connections/eth0.10.nmconnection[connection] id=eth0.10 uuid=91726373-17de-4fbc-b794-91f2e13127aa type=vlan interface-name=eth0.10 [ethernet] cloned-mac-address=DE:EA:DD:BE:EE:FF [vlan] flags=0 id=10 parent=eth0 protocol=802.1Q [match] [ipv4] address1=10.1.0.1/24 method=manual [ipv6] addr-gen-mode=default method=auto [proxy] 07070100000109000081A400000000000000000000000166257103000000D9000000000000000000000000000000000000006E00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/vlan/system-connections/eth0.nmconnection[connection] id=eth0 uuid=413aa828-9cb9-4dfe-8b16-8774fbe2d9f4 type=ethernet interface-name=eth0 [ethernet] [match] [ipv4] address1=192.168.100.5/24 method=manual [ipv6] addr-gen-mode=default method=auto [proxy] 0707010000010A000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005400000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/vlan/wicked_xml0707010000010B000081A400000000000000000000000166257103000001E3000000000000000000000000000000000000005D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/vlan/wicked_xml/eth0.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth0"> <name>eth0</name> <control> <mode>boot</mode> </control> <firewall/> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv4:static> <address> <local>192.168.100.5/24</local> </address> </ipv4:static> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> </interface> 0707010000010C000081A40000000000000000000000016625710300000270000000000000000000000000000000000000005D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/vlan/wicked_xml/vlan.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-eth0.10"> <name>eth0.10</name> <control> <mode>boot</mode> </control> <firewall/> <vlan> <device>eth0</device> <address>de:ea:dd:be:ee:ff</address> <protocol>ieee802-1Q</protocol> <tag>10</tag> </vlan> <link/> <ipv4> <enabled>true</enabled> <arp-verify>true</arp-verify> </ipv4> <ipv4:static> <address> <local>10.1.0.1/24</local> </address> </ipv4:static> <ipv6> <enabled>true</enabled> <privacy>prefer-public</privacy> <accept-redirects>false</accept-redirects> </ipv6> </interface> 0707010000010D000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000004D00000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/wireless0707010000010E000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000006000000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/wireless/system-connections0707010000010F000081A4000000000000000000000001662571030000014A000000000000000000000000000000000000007500000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/wireless/system-connections/wlan0-0.nmconnection[connection] id=wlan0-0 uuid=1089cc84-f99f-4bb8-8a0d-fafde6544e85 type=wifi interface-name=wlan0 [wifi] band=a bssid=12:34:56:78:9A:BC channel=100 hidden=true mode=adhoc ssid=example_ssid [wifi-security] key-mgmt=wpa-psk psk=example_passwd [match] [ipv4] method=disabled [ipv6] addr-gen-mode=default method=disabled [proxy] 07070100000110000081A40000000000000000000000016625710300000121000000000000000000000000000000000000007500000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/wireless/system-connections/wlan0-1.nmconnection[connection] id=wlan0-1 uuid=b0c56f00-cf58-4c6c-b6dd-edf97a612063 type=wifi interface-name=wlan0 [wifi] hidden=true mode=adhoc ssid=example_ssid2 [wifi-security] key-mgmt=wpa-psk psk=example_passwd2 [match] [ipv4] method=disabled [ipv6] addr-gen-mode=default method=disabled [proxy] 07070100000111000081A40000000000000000000000016625710300000176000000000000000000000000000000000000007300000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/wireless/system-connections/wlan1.nmconnection[connection] id=wlan1 uuid=3bc52d5e-4095-4e1a-9976-d46f766bb627 type=wifi interface-name=wlan1 [wifi] bssid=12:34:56:78:9A:BC mode=infrastructure ssid=test [wifi-security] auth-alg=shared key-mgmt=none wep-key-type=1 wep-key0=hello wep-key1=5b73215e232f4c577c5073455d wep-tx-keyidx=1 [match] [ipv4] method=disabled [ipv6] addr-gen-mode=default method=disabled [proxy] 07070100000112000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000005800000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/wireless/wicked_xml07070100000113000081A400000000000000000000000166257103000007F8000000000000000000000000000000000000006500000000migrate-wicked-git-0.1.git.1713729795.afeae692/migrate-wicked/tests/wireless/wicked_xml/wireless.xml<interface origin="compat:suse:/etc/sysconfig/network/ifcfg-wlan0"> <name>wlan0</name> <control> <mode>manual</mode> </control> <firewall/> <wireless> <ap-scan>1</ap-scan> <networks> <network> <essid>example_ssid</essid> <channel>100</channel> <access-point>12:34:56:78:9A:BC</access-point> <scan-ssid>true</scan-ssid> <mode>ad-hoc</mode> <key-management>wpa-psk,wpa-psk-sha256,sae</key-management> <wpa-psk> <passphrase>example_passwd</passphrase> <pairwise-cipher>CCMP</pairwise-cipher> <group-cipher>CCMP</group-cipher> <pmf>optional</pmf> </wpa-psk> </network> <network> <essid>example_ssid2</essid> <scan-ssid>true</scan-ssid> <mode>ad-hoc</mode> <key-management>wpa-psk,wpa-psk-sha256,sae</key-management> <wpa-psk> <passphrase>example_passwd2</passphrase> <pairwise-cipher>CCMP</pairwise-cipher> <group-cipher>CCMP</group-cipher> <pmf>optional</pmf> </wpa-psk> </network> </networks> </wireless> <link/> <ipv4> <enabled>false</enabled> </ipv4> <ipv6> <enabled>false</enabled> </ipv6> </interface> <interface origin="compat:suse:/etc/sysconfig/network/ifcfg-wlan1"> <name>wlan1</name> <control> <mode>manual</mode> </control> <firewall/> <wireless> <ap-scan>1</ap-scan> <networks> <network> <essid>test</essid> <scan-ssid>false</scan-ssid> <mode>infrastructure</mode> <access-point>12:34:56:78:9a:bc</access-point> <key-management>none</key-management> <wep> <auth-algo>shared</auth-algo> <default-key>1</default-key> <key>s:hello</key> <key>5b73215e232f4c577c5073455d</key> </wep> </network> </networks> </wireless> <link/> <ipv4> <enabled>false</enabled> </ipv4> <ipv6> <enabled>false</enabled> </ipv6> </interface> 07070100000114000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003700000000migrate-wicked-git-0.1.git.1713729795.afeae692/package07070100000115000081A40000000000000000000000016625710300000431000000000000000000000000000000000000004000000000migrate-wicked-git-0.1.git.1713729795.afeae692/package/_service<services> <service name="obs_scm" mode="manual"> <param name="url">https://github.com/openSUSE/agama.git</param> <param name="versionformat">@PARENT_TAG@+@TAG_OFFSET@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="scm">git</param> <param name="revision">master</param> <param name="subdir">rust</param> <param name="without-version">enable</param> <param name="extract">package/agama.changes</param> <param name="extract">package/agama.spec</param> </service> <service name="cargo_vendor" mode="manual"> <param name="srcdir">agama/rust</param> <param name="compression">zst</param> <param name="update">true</param> </service> <service name="cargo_audit" mode="manual"> <param name="srcdir">agama/rust</param> </service> <service mode="buildtime" name="tar"> <param name="obsinfo">agama.obsinfo</param> <param name="filename">agama</param> </service> <service mode="buildtime" name="set_version"> <param name="basename">agama</param> </service> </services> 07070100000116000081A40000000000000000000000016625710300003F8B000000000000000000000000000000000000004500000000migrate-wicked-git-0.1.git.1713729795.afeae692/package/agama.changes------------------------------------------------------------------- Wed Mar 13 12:42:58 UTC 2024 - Jorik Cronenberg <jorik.cronenberg@suse.com> - Add infiniband to network model (gh#openSUSE/agama#1032). ------------------------------------------------------------------- Thu Mar 7 10:52:58 UTC 2024 - Michal Filka <mfilka@suse.com> - CLI: added auth command with login / logout / show subcommands for handling authentication token management with new agama web server ------------------------------------------------------------------- Thu Feb 29 09:49:18 UTC 2024 - Ladislav Slezák <lslezak@suse.com> - Web server: - Accept also IPv6 connections (gh#openSUSE/agama#1057) - Added SSL (HTTPS) support (gh#openSUSE/agama#1062) - Use either the cerfificate specified via command line arguments or generate a self-signed certificate - Redirect external HTTP requests to HTTPS - Allow HTTP for internal connections (http://localhost) - Optionally listen on a secondary address (to allow listening on both HTTP/80 and HTTPS/433 ports) ------------------------------------------------------------------- Tue Feb 27 15:55:28 UTC 2024 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Reorganize RPM packages (gh#openSUSE/agama#1056): * agama is now the main package and it contains agama-dbus-server and agama-web-server. * agama-cli is a subpackage. ------------------------------------------------------------------- Wed Feb 7 11:49:02 UTC 2024 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Add preliminary support to import AutoYaST profiles (gh#openSUSE/agama#1029). ------------------------------------------------------------------- Mon Jan 29 15:53:56 UTC 2024 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Better network configuration handling (gh#openSUSE/agama#1006): * Write only changed connections. * Roll back when updating the NetworkManager configuration failed. * Improved error handling when reading or writing the changes. * Properly remove deleted connections from the D-Bus tree. * Use the UUID to identify connections. * Do not support multiple connections with the same ID. ------------------------------------------------------------------- Mon Jan 29 15:37:56 UTC 2024 - Jorik Cronenberg <jorik.cronenberg@suse.com> - Add hidden property for wireless in network model (gh#openSUSE/agama#1024). ------------------------------------------------------------------- Mon Jan 29 10:22:49 UTC 2024 - Jorik Cronenberg <jorik.cronenberg@suse.com> - Add more wireless options to network model (gh#openSUSE/agama#1014). ------------------------------------------------------------------- Thu Jan 23 18:00:00 UTC 2024 - Clemens Famulla-Conrad <cfamullaconrad@suse.de> - Add Bridge model (gh#openSUSE/agama#1008) ------------------------------------------------------------------- Thu Jan 23 17:38:23 UTC 2024 - Clemens Famulla-Conrad <cfamullaconrad@suse.de> - Add VLAN model (gh#openSUSE/agama#918) ------------------------------------------------------------------- Thu Jan 11 15:34:15 UTC 2024 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Include the encoding as part of the locales (gh#openSUSE/agama#987). ------------------------------------------------------------------- Mon Jan 8 17:02:40 UTC 2024 - José Iván López González <jlopez@suse.com> - Fix the list of keymaps to avoid duplicated values (gh#openSUSE/agama#981). ------------------------------------------------------------------- Thu Dec 21 14:23:33 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Version 7 ------------------------------------------------------------------- Thu Dec 21 11:12:45 UTC 2023 - Ancor Gonzalez Sosa <ancor@suse.com> - The result of ListTimezones includes the localized country name for each timezone (gh#openSUSE/agama#946) ------------------------------------------------------------------- Fri Dec 15 16:29:20 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Update agama-cli dependencies including the zerocopy crate to address a security alert (see gh#google/zerocopy#716). ------------------------------------------------------------------- Wed Dec 13 22:41:34 UTC 2023 - Knut Anderssen <kanderssen@suse.com> - Add support for bonding connections (gh#openSUSE/agama#885). ------------------------------------------------------------------- Fri Dec 8 09:23:09 UTC 2023 - Josef Reidinger <jreidinger@suse.com> - Change the config in a way that: (gh#openSUSE/agama#919) 1. product is moved to own section and is now under product.id 2. in product section is now also registrationCode and registrationEmail 3. in software section is now patterns to select patterns to install - adapt profile.schema according to above changes - org.opensuse.Agama.Software1 API changed to report missing patterns ------------------------------------------------------------------- Tue Dec 5 11:18:41 UTC 2023 - Jorik Cronenberg <jorik.cronenberg@suse.com> - Add ability to assign a custom MAC address for network connections (gh#openSUSE/agama#893) ------------------------------------------------------------------- Tue Dec 5 09:46:48 UTC 2023 - José Iván López González <jlopez@suse.com> - Explicitly add dependencies instead of relying on the live ISO to provide the required packages (gh#openSUSE/agama/911). ------------------------------------------------------------------- Tue Dec 5 08:56:13 UTC 2023 - Jorik Cronenberg <jorik.cronenberg@suse.com> - Add support for dummy network devices although they are not exposed on D-Bus yet (gh#openSUSE/agama#913). ------------------------------------------------------------------- Sun Dec 3 15:53:34 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Use a single call to systemd-firstboot to write the localization settings (gh#openSUSE/agama#903). ------------------------------------------------------------------- Sat Dec 2 18:05:54 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Version 6 ------------------------------------------------------------------- Wed Nov 29 11:19:51 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Rework the org.opensuse.Agama1.Locale interface (gh#openSUSE/agama#881): * Replace LabelsForLocales function with ListLocales. * Add a ListKeymaps function. * Extend the ListTimezone function to include the translation of each part. * Drop ListUILocales and ListVConsoleKeyboards functions. * Remove the SupportedLocales and VConsoleKeyboard properties. * Do not read the lists of locales, keymaps and timezones on each request. * Peform some validation when trying to change the Locales, Keymap and Timezone properties. ------------------------------------------------------------------- Thu Nov 16 11:06:30 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Update dependencies to compatible versions (gh#openSUSE/agama#874). - Replace tempdir with tempfile to prevent RUSTSEC-2023-0018. ------------------------------------------------------------------- Wed Nov 15 12:35:32 UTC 2023 - José Iván López González <jlopez@suse.com> - Adapt to changes in software D-Bus API (gh#openSUSE/agama#869). ------------------------------------------------------------------- Wed Nov 15 11:27:10 UTC 2023 - Michal Filka <mfilka@suse.com> - Improved "agama logs store" (gh#openSUSE/agama#823) - added an option which allows to define the archive destination ------------------------------------------------------------------- Tue Nov 14 15:44:15 UTC 2023 - Jorik Cronenberg <jorik.cronenberg@suse.com> - Add support for routing to the network model (gh#openSUSE/agama#824) ------------------------------------------------------------------- Mon Oct 23 14:43:59 UTC 2023 - Michal Filka <mfilka@suse.com> - Improved "agama logs store" (gh#openSUSE/agama#812) - the archive file owner is root:root - the permissions is set to r/w for the owner ------------------------------------------------------------------- Mon Oct 23 11:33:40 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Version 5 ------------------------------------------------------------------- Mon Oct 10 07:37:00 UTC 2023 - Michal Filka <mfilka@suse.com> - Improve file and directory names in "agama logs store". - Add an "agama logs list" subcommand. ------------------------------------------------------------------- Tue Sep 26 15:57:14 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Version 4 ------------------------------------------------------------------- Tue Sep 26 12:05:52 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Wait until the manager is ready before probing (gh#openSUSE/agama#771). ------------------------------------------------------------------- Mon Sep 25 11:32:53 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Add support for IPv6 network settings (gh#openSUSE/agama#761). ------------------------------------------------------------------- Mon Sep 25 10:46:53 UTC 2023 - Michal Filka <mfilka@suse.com> - CLI: added (sub)commands for handling logs. "store" subcommand is similar to what old save_y2logs did. (gh#openSUSE/agama#757) ------------------------------------------------------------------- Tue Sep 19 11:16:16 UTC 2023 - José Iván López González <jlopez@suse.com> - Adapt to new storage D-Bus API and explicitly call to probe after selecting a new product (gh#openSUSE/agama#748). ------------------------------------------------------------------- Thu Sep 14 19:44:57 UTC 2023 - Josef Reidinger <jreidinger@suse.com> - Improve questions CLI help text (gh#openSUSE/agama#754) ------------------------------------------------------------------- Thu Sep 14 10:10:37 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Use a single D-Bus service to connect to the manager and the users API (gh#openSUSE/agama#753, follow-up of gh#openSUSE/agama#729). ------------------------------------------------------------------- Wed Sep 13 09:27:22 UTC 2023 - Knut Anderssen <kanderssen@suse.com> - Allow to bind a connection to an specific interface through its name or through a set of match settings (gh#opensSUSE/agama#723). ------------------------------------------------------------------- Thu Aug 31 10:30:28 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Use a single D-Bus service to expose locale, network and questions settings (gh#openSUSE/agama#729). ------------------------------------------------------------------- Wed Aug 30 12:57:59 UTC 2023 - Josef Reidinger <jreidinger@suse.com> - Locale service: add value for UI locale (gh#openSUSE/agama#725) ------------------------------------------------------------------- Thu Aug 3 08:34:14 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Move the settings functionality to a separate package, agama-settings (gh#openSUSE/agama#666). - Make the "Settings" derive macro reusable from other crates. - Extend the "Settings" derive macro to generate code for InstallSettings and NetworkSettings. - Improve error reporting when working with the "config" subcommand. ------------------------------------------------------------------- Wed Aug 2 10:03:18 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Version 3 ------------------------------------------------------------------- Wed Jul 26 11:08:09 UTC 2023 - Josef Reidinger <jreidinger@suse.com> - CLI: add to "questions" command "answers" subcommand to set file with predefined answers - dbus-server: add "AddAnswersFile" method to Questions service (gh#openSUSE/agama#669) ------------------------------------------------------------------- Tue Jul 18 13:32:04 UTC 2023 - Josef Reidinger <jreidinger@suse.com> - Add to CLI "questions" subcommand with mode option to set interactive and non-interactive mode (gh#openSUSE/agama#668) ------------------------------------------------------------------- Mon Jul 17 13:36:56 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Fix the logic to decide which network connections to write due to a bug introduced in gh#openSUSE/agama#662 (gh#openSUSE/agama#667). ------------------------------------------------------------------- Mon Jul 17 09:16:38 UTC 2023 - Josef Reidinger <jreidinger@suse.com> - Adapt to new questions D-Bus API to allow automatic answering of questions when requested (gh#openSUSE/agama#637, reverts gh#openSUSE/agama#649 as now default option is mandatory) ------------------------------------------------------------------- Thu Jul 13 10:22:36 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Improve error reporting in the command-line interface (gh#openSUSE/agama#659 and gh#openSUSE/agama#660). ------------------------------------------------------------------- Thu Jul 13 08:56:40 UTC 2023 - José Iván López González <jlopez@suse.com> - Read the storage candidate devices and show them with "agama config show" (gh#openSUSE/agama#658). ------------------------------------------------------------------- Fri Jul 7 14:12:03 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Improve the progress reporting (gh#openSUSE/agama#653). ------------------------------------------------------------------- Thu Jul 6 09:13:47 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Improve the waiting logic and implement a retry mechanism for the "agama install" command (bsc#1213047). ------------------------------------------------------------------- Wed Jul 5 11:11:20 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Fix the questions service to handle questions with no default option (gh#openSUSE/agama#649). ------------------------------------------------------------------- Thu Jun 1 08:14:14 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Add a localization D-Bus service (gh#openSUSE/agama#533). - Add a network D-Bus service (gh#openSUSE/agama#571). ------------------------------------------------------------------- Tue May 23 11:51:26 UTC 2023 - Martin Vidner <mvidner@suse.com> - Version 2.1 ------------------------------------------------------------------- Mon May 22 12:29:20 UTC 2023 - Martin Vidner <mvidner@suse.com> - Version 2 ------------------------------------------------------------------- Thu May 11 11:00:11 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Import root authentication settings when reading a Jsonnet file (bsc#1211300, gh#openSUSE/agama#573). - Do not export the SSH public key as an empty string when it is not defined. ------------------------------------------------------------------- Fri Mar 24 14:36:36 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Version 0.2: * Add validation for software and users settings (gh#yast/agama-cli#48, gh#yast/agama-cli#51). * Better error reporting when the bus is not found (gh#yast/agama-cli#48). * Improve the progress reporting mechanism, although it is still a work in progress (gh#yast/agama-cli#50). ------------------------------------------------------------------- Wed Mar 22 09:39:29 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Add support for setting root authentication mechanisms (gh#yast/agama-cli#47). ------------------------------------------------------------------- Tue Mar 21 16:06:02 UTC 2023 - Martin Vidner <mvidner@suse.com> - Do not fall back to the system D-Bus (gh#yast/agama-cli#45). ------------------------------------------------------------------- Wed Mar 21 13:28:01 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - Use JSON as the default format (gh#yast/agama-cli#46). ------------------------------------------------------------------- Tue Mar 21 08:55:39 UTC 2023 - Josef Reidinger <jreidinger@suse.com> - Fix the path of the JSON schema (gh#yast/agama-cli#44). ------------------------------------------------------------------- Thu Mar 16 11:56:42 UTC 2023 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> - First version of the package: * Querying and setting simple values. * Adding elements to collections * Handling of auto-installation profiles. * Basic error handling - 0.1 07070100000117000081A40000000000000000000000016625710300000FB9000000000000000000000000000000000000004200000000migrate-wicked-git-0.1.git.1713729795.afeae692/package/agama.spec# # spec file for package agama # # Copyright (c) 2023-2024 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed # upon. The license for this file, and modifications and additions to the # file, is the same license as for the pristine package itself (unless the # license for the pristine package is not an Open Source License, in which # case the license is the MIT License). An "Open Source License" is a # license that conforms to the Open Source Definition (Version 1.9) # published by the Open Source Initiative. # Please submit bugfixes or comments via http://bugs.opensuse.org/ # Name: agama # This will be set by osc services, that will run after this. Version: 0 Release: 0 Summary: Agama Installer # If you know the license, put it's SPDX string here. # Alternately, you can use cargo lock2rpmprovides to help generate this. License: GPL-2.0-only Url: https://github.com/opensuse/agama Source0: agama.tar Source1: vendor.tar.zst BuildRequires: cargo-packaging BuildRequires: pkgconfig(openssl) # used in tests for dbus service BuildRequires: dbus-1-common Requires: dbus-1-common # required by agama-dbus-server integration tests BuildRequires: dbus-1-daemon BuildRequires: clang-devel BuildRequires: pkgconfig(pam) # required by autoinstallation Requires: jsonnet Requires: lshw # required by "agama logs store" Requires: bzip2 Requires: tar # required for translating the keyboards descriptions BuildRequires: xkeyboard-config-lang Requires: xkeyboard-config-lang # required for getting the list of timezones Requires: timezone BuildRequires: timezone # required for getting the languages information BuildRequires: python-langtable-data Requires: python-langtable-data # dependency on the YaST part of Agama Requires: agama-yast # conflicts with the old packages Conflicts: agama-dbus-server %description Agama is a service-based Linux installer. It is composed of an HTTP-based API, a web user interface, a command-line interface and a D-Bus service which exposes part of the YaST libraries. %package -n agama-cli # This will be set by osc services, that will run after this. Version: 0 Release: 0 Summary: Agama command-line interface License: GPL-2.0-only Url: https://github.com/opensuse/agama %description -n agama-cli Command line program to interact with the Agama installer. %prep %autosetup -a1 -n agama # Remove exec bits to prevent an issue in fedora shebang checking. Uncomment only if required. # find vendor -type f -name \*.rs -exec chmod -x '{}' \; %build %{cargo_build} %install install -D -d -m 0755 %{buildroot}%{_bindir} install -m 0755 %{_builddir}/agama/target/release/agama %{buildroot}%{_bindir}/agama install -m 0755 %{_builddir}/agama/target/release/agama-dbus-server %{buildroot}%{_bindir}/agama-dbus-server install -m 0755 %{_builddir}/agama/target/release/agama-web-server %{buildroot}%{_bindir}/agama-web-server install -D -p -m 644 %{_builddir}/agama/share/agama.pam $RPM_BUILD_ROOT%{_pam_vendordir}/agama install -D -d -m 0755 %{buildroot}%{_datadir}/agama-cli install -m 0644 %{_builddir}/agama/agama-lib/share/profile.schema.json %{buildroot}%{_datadir}/agama-cli install --directory %{buildroot}%{_datadir}/dbus-1/agama-services install -m 0644 --target-directory=%{buildroot}%{_datadir}/dbus-1/agama-services %{_builddir}/agama/share/*.service %check PATH=$PWD/share/bin:$PATH %ifarch aarch64 /usr/bin/cargo auditable test -j1 --offline --no-fail-fast %else echo $PATH %{cargo_test} %endif %files %{_bindir}/agama-dbus-server %{_bindir}/agama-web-server %{_datadir}/dbus-1/agama-services %{_pam_vendordir}/agama %files -n agama-cli %{_bindir}/agama %dir %{_datadir}/agama-cli %{_datadir}/agama-cli/profile.schema.json %changelog 07070100000118000081A40000000000000000000000016625710300000011000000000000000000000000000000000000003C00000000migrate-wicked-git-0.1.git.1713729795.afeae692/rustfmt.tomledition = "2021" 07070100000119000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003500000000migrate-wicked-git-0.1.git.1713729795.afeae692/share0707010000011A000081A40000000000000000000000016625710300000042000000000000000000000000000000000000003F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/share/agama.pam#%PAM-1.0 auth include common-auth account include common-account 0707010000011B000041ED0000000000000000000000026625710300000000000000000000000000000000000000000000003900000000migrate-wicked-git-0.1.git.1713729795.afeae692/share/bin0707010000011C000081A40000000000000000000000016625710300000123000000000000000000000000000000000000004300000000migrate-wicked-git-0.1.git.1713729795.afeae692/share/bin/README.mdThis directory contains commands that replaces real ones during CI testing. The reason is that these commands might not work in the CI environment (e.g., systemd related commands). To use these "binaries" in the tests, just set the right PATH: ``` PATH=$PWD/share/bin:$PATH cargo test ``` 0707010000011D000081ED000000000000000000000001662571030000006C000000000000000000000000000000000000004300000000migrate-wicked-git-0.1.git.1713729795.afeae692/share/bin/localectl#!/usr/bin/env sh SCRIPT=$(readlink -f "$0") DATADIR=$(dirname "$SCRIPT")/.. cat $DATADIR/localectl-$1.txt 0707010000011E000081A40000000000000000000000016625710300000E19000000000000000000000000000000000000004400000000migrate-wicked-git-0.1.git.1713729795.afeae692/share/dbus-test.conf<!-- This configuration file controls the Agama Installer message bus. It is based on /usr/share/dbus-1/session.conf but including some changes: - Type is set to "org.opensuse.Agama". - Removed the policy section for the default context. - Added a new policy section for the root user. - The local-session.conf file is not read. --> <!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> <busconfig> <type>org.opensuse.Agama</type> <!-- If we fork, keep the user's original umask to avoid affecting the behavior of child processes. --> <keep_umask/> <listen>unix:tmpdir=/tmp</listen> <!-- On Unix systems, the most secure authentication mechanism is EXTERNAL, which uses credential-passing over Unix sockets. This authentication mechanism is not available on Windows, is not suitable for use with the tcp: or nonce-tcp: transports, and will not work on obscure flavours of Unix that do not have a supported credentials-passing mechanism. On those platforms/transports, comment out the <auth> element to allow fallback to DBUS_COOKIE_SHA1. --> <auth>EXTERNAL</auth> <!-- only root can own the services --> <policy context="default"> <!-- Allow everything to be sent --> <allow send_destination="*" eavesdrop="true"/> <!-- Allow everything to be received --> <allow eavesdrop="true"/> <allow own="org.opensuse.Agama1" /> <allow own="org.opensuse.Agama.Manager1" /> <allow own="org.opensuse.Agama.Software1" /> <allow own="org.opensuse.Agama.Storage1" /> <!-- only root can send anything to the services --> <allow send_destination="org.opensuse.Agama1" /> <allow send_destination="org.opensuse.Agama.Manager1" /> <allow send_destination="org.opensuse.Agama.Software1" /> <allow send_destination="org.opensuse.Agama.Storage1" /> </policy> <include if_selinux_enabled="yes" selinux_root_relative="yes">contexts/dbus_contexts</include> <!-- For the session bus, override the default relatively-low limits with essentially infinite limits, since the bus is just running as the user anyway, using up bus resources is not something we need to worry about. In some cases, we do set the limits lower than "all available memory" if exceeding the limit is almost certainly a bug, having the bus enforce a limit is nicer than a huge memory leak. But the intent is that these limits should never be hit. --> <!-- the memory limits are 1G instead of say 4G because they can't exceed 32-bit signed int max --> <limit name="max_incoming_bytes">1000000000</limit> <limit name="max_incoming_unix_fds">250000000</limit> <limit name="max_outgoing_bytes">1000000000</limit> <limit name="max_outgoing_unix_fds">250000000</limit> <limit name="max_message_size">1000000000</limit> <!-- We do not override max_message_unix_fds here since the in-kernel limit is also relatively low --> <limit name="service_start_timeout">600000</limit> <limit name="auth_timeout">240000</limit> <limit name="pending_fd_timeout">150000</limit> <limit name="max_completed_connections">100000</limit> <limit name="max_incomplete_connections">10000</limit> <limit name="max_connections_per_user">100000</limit> <limit name="max_pending_service_starts">10000</limit> <limit name="max_names_per_connection">50000</limit> <limit name="max_match_rules_per_connection">50000</limit> <limit name="max_replies_per_connection">50000</limit> </busconfig> 0707010000011F000081A400000000000000000000000166257103000018D1000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/share/localectl-list-keymaps.txt3l ANSI-dvorak Pl02 adnw al al-plisi amiga-de amiga-us apple-a1048-sv apple-a1243-sv apple-a1243-sv-fn-reverse apple-internal-0x0253-sv apple-internal-0x0253-sv-fn-reverse applkey ara at at-mac at-nodeadkeys atari-de atari-se atari-uk-falcon atari-us az azerty ba ba-alternatequotes ba-unicode ba-unicodeus ba-us backspace bashkir be be-iso-alternate be-latin1 be-nodeadkeys be-oss be-oss_latin9 be-wang bg-cp1251 bg-cp855 bg_bds-cp1251 bg_bds-utf8 bg_pho-cp1251 bg_pho-utf8 bone br br-abnt br-abnt-alt br-abnt2 br-abnt2-old br-dvorak br-latin1-abnt2 br-latin1-us br-nativo br-nativo-epo br-nativo-us br-nodeadkeys br-thinkpad by by-cp1251 by-latin bywin-cp1251 ca ca-eng ca-fr-dvorak ca-fr-legacy ca-multix carpalx carpalx-full cf ch ch-de_mac ch-de_nodeadkeys ch-fr ch-fr_mac ch-fr_nodeadkeys ch-legacy chinese cm cm-azerty cm-dvorak cm-french cm-mmuock cm-qwerty cn cn-altgr-pinyin cn-latin1 croat ctrl cz cz-bksl cz-cp1250 cz-dvorak-ucw cz-lat2 cz-lat2-prog cz-lat2-us cz-qwerty cz-qwerty-mac cz-qwerty_bksl cz-rus cz-us-qwertz cz-winkeys cz-winkeys-qwerty de de-T3 de-deadacute de-deadgraveacute de-deadtilde de-dsb de-dsb_qwertz de-dvorak de-e1 de-e2 de-latin1 de-latin1-nodeadkeys de-mac de-mac_nodeadkeys de-mobii de-neo de-nodeadkeys de-qwerty de-ro de-ro_nodeadkeys de-tr de-us de_CH-latin1 de_alt_UTF-8 defkeymap defkeymap_V1.0 dk dk-dvorak dk-latin1 dk-mac dk-mac_nodeadkeys dk-nodeadkeys dk-winkeys dvorak dvorak-ca-fr dvorak-de dvorak-es dvorak-fr dvorak-l dvorak-la dvorak-no dvorak-programmer dvorak-r dvorak-ru dvorak-sv-a1 dvorak-sv-a5 dvorak-uk dvorak-ukp dz dz-azerty-deadkeys dz-qwerty-gb-deadkeys dz-qwerty-us-deadkeys ee ee-dvorak ee-nodeadkeys ee-us emacs emacs2 en en-latin9 epo epo-legacy es es-ast es-cat es-cp850 es-deadtilde es-dvorak es-nodeadkeys es-olpc es-winkeys et et-nodeadkeys euro euro1 euro2 fa fi fi-classic fi-kotoistus fi-mac fi-nodeadkeys fi-smi fi-winkeys fo fo-nodeadkeys fr fr-afnor fr-azerty fr-bepo fr-bepo-latin9 fr-bepo_afnor fr-bepo_latin9 fr-bre fr-dvorak fr-latin1 fr-latin9 fr-latin9_nodeadkeys fr-mac fr-nodeadkeys fr-oci fr-oss fr-oss_latin9 fr-oss_nodeadkeys fr-pc fr-us fr_CH fr_CH-latin1 gb gb-colemak gb-colemak_dh gb-dvorak gb-dvorakukp gb-extd gb-gla gb-intl gb-mac gb-mac_intl gb-pl ge ge-ergonomic ge-mess ge-ru gh gh-akan gh-avn gh-ewe gh-fula gh-ga gh-generic gh-gillbt gh-hausa gr gr-pc hr hr-alternatequotes hr-unicode hr-unicodeus hr-us hu hu-101_qwerty_comma_dead hu-101_qwerty_comma_nodead hu-101_qwerty_dot_dead hu-101_qwerty_dot_nodead hu-101_qwertz_comma_dead hu-101_qwertz_comma_nodead hu-101_qwertz_dot_dead hu-101_qwertz_dot_nodead hu-102_qwerty_comma_dead hu-102_qwerty_comma_nodead hu-102_qwerty_dot_dead hu-102_qwerty_dot_nodead hu-102_qwertz_comma_dead hu-102_qwertz_comma_nodead hu-102_qwertz_dot_dead hu-102_qwertz_dot_nodead hu-nodeadkeys hu-qwerty hu-standard hu101 id ie ie-CloGaelach ie-UnicodeExpert ie-ogam_is434 il il-heb il-phonetic il-si2 in-eng in-iipa iq-ku iq-ku_alt iq-ku_ara iq-ku_f ir ir-ku ir-ku_alt ir-ku_ara ir-ku_f is is-dvorak is-latin1 is-latin1-us is-mac is-mac_legacy it it-fur it-geo it-ibm it-intl it-mac it-nodeadkeys it-scn it-us it-winkeys it2 jp jp-OADG109A jp-dvorak jp-kana86 jp106 kazakh ke ke-kik keypad khmer koy kr kr-kr104 ky_alt_sh-UTF-8 kyrgyz kz-latin la-latin1 latam latam-colemak latam-deadtilde latam-dvorak latam-nodeadkeys lk-us lt lt-ibm lt-lekp lt-lekpa lt-ratise lt-sgs lt-std lt-us lt.baltic lt.l4 lt.std lv lv-adapted lv-apostrophe lv-ergonomic lv-fkey lv-modern lv-tilde ma-french ma-rif mac-Pl02 mac-be mac-br-abnt2 mac-cz-us-qwertz mac-de-latin1 mac-de-latin1-nodeadkeys mac-de_CH mac-dk-latin1 mac-dvorak mac-es mac-euro mac-euro2 mac-fi-latin1 mac-fr mac-fr-legacy mac-fr_CH-latin1 mac-gr mac-hu mac-it mac-jp106 mac-no-latin1 mac-pl mac-pt-latin1 mac-ru1 mac-se mac-template mac-uk mac-us md md-gag me me-latinalternatequotes me-latinunicode me-latinunicodeyz me-latinyz mk mk-cp1251 mk-utf mk0 ml ml-fr-oss ml-us-intl ml-us-mac mm mm-mnw mm-shn mod-dh-ansi-us mod-dh-ansi-us-awing mod-dh-ansi-us-fatz mod-dh-ansi-us-fatz-wide mod-dh-ansi-us-wide mod-dh-iso-uk mod-dh-iso-uk-wide mod-dh-iso-us mod-dh-iso-us-wide mod-dh-matrix-us mt mt-alt-gb mt-alt-us mt-us neo neoqwertz ng ng-hausa ng-igbo ng-yoruba nl nl-mac nl-std nl-us nl2 no no-colemak no-colemak_dh no-colemak_dh_wide no-dvorak no-latin1 no-mac no-mac_nodeadkeys no-nodeadkeys no-smi no-smi_nodeadkeys no-winkeys nz nz-mao pc110 ph ph-capewell-dvorak ph-capewell-qwerf2k6 ph-colemak ph-dvorak pl pl-csb pl-dvorak pl-dvorak_altquotes pl-dvorak_quotes pl-dvp pl-legacy pl-qwertz pl-szl pl1 pl2 pl3 pl4 pt pt-latin1 pt-latin9 pt-mac pt-mac_nodeadkeys pt-nativo pt-nativo-epo pt-nativo-us pt-nodeadkeys ro ro-latin2 ro-std ro-winkeys ro_std ro_win rs-latin rs-latinalternatequotes rs-latinunicode rs-latinunicodeyz rs-latinyz ru ru-cp1251 ru-cv_latin ru-ms ru-ruchey_en ru-yawerty ru1 ru1_win-utf ru2 ru3 ru4 ru_win ruwin_alt-CP1251 ruwin_alt-KOI8-R ruwin_alt-UTF-8 ruwin_alt_sh-UTF-8 ruwin_cplk-CP1251 ruwin_cplk-KOI8-R ruwin_cplk-UTF-8 ruwin_ct_sh-CP1251 ruwin_ct_sh-KOI8-R ruwin_ct_sh-UTF-8 ruwin_ctrl-CP1251 ruwin_ctrl-KOI8-R ruwin_ctrl-UTF-8 se se-dvorak se-fi-ir209 se-fi-lat6 se-ir209 se-lat6 se-latin1 se-mac se-nodeadkeys se-smi se-svdvorak se-us se-us_dvorak sg sg-latin1 sg-latin1-lk450 si si-alternatequotes si-us sk sk-bksl sk-prog-qwerty sk-prog-qwertz sk-qwerty sk-qwerty_bksl sk-qwertz slovene sr-cy sr-latin sun-pl sun-pl-altgraph sundvorak sunkeymap sunt4-es sunt4-fi-latin1 sunt4-no-latin1 sunt5-cz-us sunt5-de-latin1 sunt5-es sunt5-fi-latin1 sunt5-fr-latin1 sunt5-ru sunt5-uk sunt5-us-cz sunt6-uk sv-latin1 sy-ku sy-ku_alt sy-ku_f taiwanese tj_alt-UTF8 tm tm-alt tr tr-alt tr-e tr-f tr-intl tr-ku tr-ku_alt tr-ku_f tr_f-latin5 tr_q-latin5 tralt trf trq ttwin_alt-UTF-8 ttwin_cplk-UTF-8 ttwin_ct_sh-UTF-8 ttwin_ctrl-UTF-8 tw tw-indigenous tw-saisiyat ua ua-cp1251 ua-crh ua-crh_alt ua-crh_f ua-utf ua-utf-ws ua-ws uk unicode us us-acentos us-acentos-old us-alt-intl us-altgr-intl us-colemak us-colemak_dh us-colemak_dh_iso us-colemak_dh_ortho us-colemak_dh_wide us-colemak_dh_wide_iso us-dvorak us-dvorak-alt-intl us-dvorak-classic us-dvorak-intl us-dvorak-l us-dvorak-mac us-dvorak-r us-dvp us-euro us-haw us-hbs us-intl us-mac us-norman us-olpc2 us-symbolic us-workman us-workman-intl us1 uz-latin vn vn-fr vn-us wangbe wangbe2 windowkeys 07070100000120000081A40000000000000000000000016625710300000000000000000000000000000000000000000000004F00000000migrate-wicked-git-0.1.git.1713729795.afeae692/share/localectl-list-locale.txt07070100000121000081A40000000000000000000000016625710300000714000000000000000000000000000000000000005000000000migrate-wicked-git-0.1.git.1713729795.afeae692/share/localectl-list-locales.txtC.UTF-8 aa_DJ.UTF-8 af_ZA.UTF-8 an_ES.UTF-8 ar_AE.UTF-8 ar_BH.UTF-8 ar_DZ.UTF-8 ar_EG.UTF-8 ar_IQ.UTF-8 ar_JO.UTF-8 ar_KW.UTF-8 ar_LB.UTF-8 ar_LY.UTF-8 ar_MA.UTF-8 ar_OM.UTF-8 ar_QA.UTF-8 ar_SA.UTF-8 ar_SD.UTF-8 ar_SY.UTF-8 ar_TN.UTF-8 ar_YE.UTF-8 ast_ES.UTF-8 be_BY.UTF-8 bg_BG.UTF-8 bhb_IN.UTF-8 br_FR.UTF-8 bs_BA.UTF-8 ca_AD.UTF-8 ca_ES.UTF-8 ca_FR.UTF-8 ca_IT.UTF-8 cs_CZ.UTF-8 cy_GB.UTF-8 da_DK.UTF-8 de_AT.UTF-8 de_BE.UTF-8 de_CH.UTF-8 de_DE.UTF-8 de_IT.UTF-8 de_LI.UTF-8 de_LU.UTF-8 el_CY.UTF-8 el_GR.UTF-8 en_AU.UTF-8 en_BW.UTF-8 en_CA.UTF-8 en_DK.UTF-8 en_GB.UTF-8 en_HK.UTF-8 en_IE.UTF-8 en_NZ.UTF-8 en_PH.UTF-8 en_SC.UTF-8 en_SG.UTF-8 en_US.UTF-8 en_ZA.UTF-8 en_ZW.UTF-8 es_AR.UTF-8 es_BO.UTF-8 es_CL.UTF-8 es_CO.UTF-8 es_CR.UTF-8 es_DO.UTF-8 es_EC.UTF-8 es_ES.UTF-8 es_GT.UTF-8 es_HN.UTF-8 es_MX.UTF-8 es_NI.UTF-8 es_PA.UTF-8 es_PE.UTF-8 es_PR.UTF-8 es_PY.UTF-8 es_SV.UTF-8 es_US.UTF-8 es_UY.UTF-8 es_VE.UTF-8 et_EE.UTF-8 eu_ES.UTF-8 fi_FI.UTF-8 fo_FO.UTF-8 fr_BE.UTF-8 fr_CA.UTF-8 fr_CH.UTF-8 fr_FR.UTF-8 fr_LU.UTF-8 ga_IE.UTF-8 gd_GB.UTF-8 gl_ES.UTF-8 gv_GB.UTF-8 he_IL.UTF-8 hr_HR.UTF-8 hsb_DE.UTF-8 hu_HU.UTF-8 id_ID.UTF-8 is_IS.UTF-8 it_CH.UTF-8 it_IT.UTF-8 ja_JP.UTF-8 ka_GE.UTF-8 kk_KZ.UTF-8 kl_GL.UTF-8 ko_KR.UTF-8 ku_TR.UTF-8 kw_GB.UTF-8 lg_UG.UTF-8 lt_LT.UTF-8 lv_LV.UTF-8 mg_MG.UTF-8 mi_NZ.UTF-8 mk_MK.UTF-8 ms_MY.UTF-8 mt_MT.UTF-8 nb_NO.UTF-8 nl_BE.UTF-8 nl_NL.UTF-8 nn_NO.UTF-8 no_NO.UTF-8 oc_FR.UTF-8 om_KE.UTF-8 pl_PL.UTF-8 pt_BR.UTF-8 pt_PT.UTF-8 ro_RO.UTF-8 ru_RU.UTF-8 ru_UA.UTF-8 sk_SK.UTF-8 sl_SI.UTF-8 so_DJ.UTF-8 so_KE.UTF-8 so_SO.UTF-8 sq_AL.UTF-8 st_ZA.UTF-8 sv_FI.UTF-8 sv_SE.UTF-8 tcy_IN.UTF-8 tg_TJ.UTF-8 th_TH.UTF-8 tl_PH.UTF-8 tr_CY.UTF-8 tr_TR.UTF-8 uk_UA.UTF-8 uz_UZ.UTF-8 wa_BE.UTF-8 xh_ZA.UTF-8 yi_US.UTF-8 zh_CN.UTF-8 zh_HK.UTF-8 zh_SG.UTF-8 zh_TW.UTF-8 zu_ZA.UTF-8 07070100000122000081A40000000000000000000000016625710300000053000000000000000000000000000000000000005100000000migrate-wicked-git-0.1.git.1713729795.afeae692/share/org.opensuse.Agama1.service[D-BUS Service] Name=org.opensuse.Agama1 Exec=/usr/bin/agama-dbus-server User=root 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!1781 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor