07070100000000000081A4000000000000000000000001653660D700000083000000000000000000000000000000000000001100000000agama/.gitignore# Generated by Cargo # will have compiled files and executables /target/ # These are backup files generated by rustfmt **/*.rs.bk 07070100000001000081A4000000000000000000000001653660D70000F5E9000000000000000000000000000000000000001100000000agama/Cargo.lock# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[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-std", "clap", "console", "convert_case", "fs_extra", "indicatif", "log", "nix 0.27.1", "serde", "serde_json", "serde_yaml", "tempdir", "thiserror", ] [[package]] name = "agama-dbus-server" version = "0.1.0" dependencies = [ "agama-lib", "agama-locale-data", "anyhow", "async-std", "cidr", "futures", "log", "serde", "serde_yaml", "simplelog", "systemd-journal-logger", "thiserror", "uuid", "zbus", "zbus_macros", ] [[package]] name = "agama-derive" version = "1.0.0" dependencies = [ "proc-macro2", "quote", "syn 2.0.26", ] [[package]] name = "agama-lib" version = "1.0.0" dependencies = [ "agama-settings", "anyhow", "async-std", "cidr", "curl", "futures", "futures-util", "jsonschema", "log", "serde", "serde_json", "tempfile", "thiserror", "zbus", ] [[package]] name = "agama-locale-data" version = "0.1.0" dependencies = [ "anyhow", "chrono-tz", "flate2", "quick-xml", "regex", "serde", ] [[package]] name = "agama-settings" version = "1.0.0" dependencies = [ "agama-derive", "thiserror", ] [[package]] name = "ahash" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", "getrandom", "once_cell", "serde", "version_check", ] [[package]] name = "aho-corasick" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is-terminal", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" [[package]] name = "anstyle-parse" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ "windows-sys 0.48.0", ] [[package]] name = "anstyle-wincon" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" dependencies = [ "anstyle", "windows-sys 0.48.0", ] [[package]] name = "anyhow" version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "async-attributes" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" dependencies = [ "quote", "syn 1.0.109", ] [[package]] name = "async-broadcast" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" dependencies = [ "event-listener", "futures-core", ] [[package]] name = "async-channel" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" dependencies = [ "concurrent-queue", "event-listener", "futures-core", ] [[package]] name = "async-executor" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" dependencies = [ "async-lock", "async-task", "concurrent-queue", "fastrand", "futures-lite", "slab", ] [[package]] name = "async-fs" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" dependencies = [ "async-lock", "autocfg", "blocking", "futures-lite", ] [[package]] name = "async-global-executor" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" dependencies = [ "async-channel", "async-executor", "async-io", "async-lock", "blocking", "futures-lite", "once_cell", ] [[package]] name = "async-io" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock", "autocfg", "cfg-if", "concurrent-queue", "futures-lite", "log", "parking", "polling", "rustix", "slab", "socket2", "waker-fn", ] [[package]] name = "async-lock" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" dependencies = [ "event-listener", ] [[package]] name = "async-process" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" dependencies = [ "async-io", "async-lock", "autocfg", "blocking", "cfg-if", "event-listener", "futures-lite", "rustix", "signal-hook", "windows-sys 0.48.0", ] [[package]] name = "async-recursion" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", "syn 2.0.26", ] [[package]] name = "async-std" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" dependencies = [ "async-attributes", "async-channel", "async-global-executor", "async-io", "async-lock", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", "futures-lite", "gloo-timers", "kv-log-macro", "log", "memchr", "once_cell", "pin-project-lite", "pin-utils", "slab", "wasm-bindgen-futures", ] [[package]] name = "async-task" version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "async-trait" version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", "syn 2.0.26", ] [[package]] name = "atomic-waker" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[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.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[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.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" dependencies = [ "async-channel", "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", "log", ] [[package]] name = "bumpalo" version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytecount" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[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.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "chrono-tz" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9cc2b23599e6d7479755f3594285efb3f74a1bdca7a7374948bc831e23a552" dependencies = [ "chrono", "chrono-tz-build", "phf", ] [[package]] name = "chrono-tz-build" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751" 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 = "clap" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" dependencies = [ "clap_builder", "clap_derive", "once_cell", ] [[package]] name = "clap_builder" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" dependencies = [ "anstream", "anstyle", "bitflags 1.3.2", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_derive" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" dependencies = [ "heck", "proc-macro2", "quote", "syn 2.0.26", ] [[package]] name = "clap_lex" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[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.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", "windows-sys 0.45.0", ] [[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 = "cpufeatures" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-utils" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if", ] [[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 = "ctor" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ "quote", "syn 1.0.109", ] [[package]] name = "curl" version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", "socket2", "winapi", ] [[package]] name = "curl-sys" version = "0.4.62+curl-8.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "274ef7ef7c1113c7611af49ce248a700afa1171045a1aaa40137030773f993b8" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", "winapi", ] [[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 = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "enumflags2" version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", "syn 2.0.26", ] [[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", "windows-sys 0.48.0", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[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 = "flate2" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "form_urlencoded" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 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 = "fuchsia-cprng" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "futures" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-lite" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ "fastrand", "futures-core", "futures-io", "memchr", "parking", "pin-project-lite", "waker-fn", ] [[package]] name = "futures-macro" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", "syn 2.0.26", ] [[package]] name = "futures-sink" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "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.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gloo-timers" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" dependencies = [ "futures-channel", "futures-core", "js-sys", "wasm-bindgen", ] [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[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 = "idna" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 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", ] [[package]] name = "indexmap" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", "hashbrown 0.14.0", ] [[package]] name = "indicatif" version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" dependencies = [ "console", "number_prefix", "portable-atomic 0.3.20", "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 = "is-terminal" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi", "io-lifetimes", "rustix", "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 = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] [[package]] name = "jsonschema" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ca9e2b45609132ae2214d50482c03aeee78826cd6fd53a8940915b81acedf16" dependencies = [ "ahash", "anyhow", "base64", "bytecount", "fancy-regex", "fraction", "iso8601", "itoa", "lazy_static", "memchr", "num-cmp", "parking_lot", "percent-encoding", "regex", "serde", "serde_json", "time", "url", "uuid", ] [[package]] name = "kv-log-macro" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" dependencies = [ "log", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[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.2", "nom", "once_cell", "serde", "sha2", "thiserror", "uuid", ] [[package]] name = "libz-sys" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", "value-bag", ] [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "nix" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", "memoffset", "static_assertions", ] [[package]] name = "nix" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ "bitflags 2.4.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 = "num" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" dependencies = [ "num-bigint", "num-complex", "num-integer", "num-iter", "num-rational", "num-traits", ] [[package]] name = "num-bigint" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" 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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" dependencies = [ "num-traits", ] [[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-iter" version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" 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.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "num_threads" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ "libc", ] [[package]] name = "number_prefix" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "once_cell" version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[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.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[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 = "parking" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" [[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.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", "redox_syscall 0.2.16", "smallvec", "windows-sys 0.45.0", ] [[package]] name = "parse-zoneinfo" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" dependencies = [ "regex", ] [[package]] name = "percent-encoding" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "phf" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" dependencies = [ "phf_generator", "phf_shared", ] [[package]] name = "phf_generator" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" dependencies = [ "phf_shared", "rand 0.8.5", ] [[package]] name = "phf_shared" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[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 = "portable-atomic" version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e30165d31df606f5726b090ec7592c308a0eaf61721ff64c9a3018e344a8753e" dependencies = [ "portable-atomic 1.3.2", ] [[package]] name = "portable-atomic" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc59d1bcc64fc5d021d67521f818db868368028108d37f0e98d74e33f68297b5" [[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", ] [[package]] name = "proc-macro2" version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 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.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" dependencies = [ "fuchsia-cprng", "libc", "rand_core 0.3.1", "rdrand", "winapi", ] [[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 0.6.4", ] [[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 0.6.4", ] [[package]] name = "rand_core" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" dependencies = [ "rand_core 0.4.2", ] [[package]] name = "rand_core" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rdrand" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" dependencies = [ "rand_core 0.3.1", ] [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1a59b5d8e97dee33696bf13c5ba8ab85341c002922fba050069326b9c498974" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi", ] [[package]] name = "rustix" version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys", "windows-sys 0.48.0", ] [[package]] name = "ryu" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "schannel" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" dependencies = [ "windows-sys 0.42.0", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", "syn 2.0.26", ] [[package]] name = "serde_json" version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_repr" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", "syn 2.0.26", ] [[package]] name = "serde_yaml" version = "0.9.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5f51e3fdb5b9cdd1577e1cb7a733474191b1aca6a72c2e50913241632c1180" dependencies = [ "indexmap 2.0.0", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "sha1" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" dependencies = [ "libc", "signal-hook-registry", ] [[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 = "simplelog" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" dependencies = [ "log", "termcolor", "time", ] [[package]] name = "siphasher" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "slab" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", ] [[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 = "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.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[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 = "tempdir" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" dependencies = [ "rand 0.4.6", "remove_dir_all", ] [[package]] name = "tempfile" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", "rustix", "windows-sys 0.45.0", ] [[package]] name = "termcolor" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] [[package]] name = "terminal_size" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ "rustix", "windows-sys 0.48.0", ] [[package]] name = "thiserror" version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ "proc-macro2", "quote", "syn 2.0.26", ] [[package]] name = "time" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ "itoa", "libc", "num_threads", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] [[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 = "toml_datetime" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" [[package]] name = "toml_edit" version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ "indexmap 1.9.3", "toml_datetime", "winnow", ] [[package]] name = "tracing" version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", "syn 2.0.26", ] [[package]] name = "tracing-core" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", ] [[package]] name = "typenum" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "uds_windows" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" dependencies = [ "tempfile", "winapi", ] [[package]] name = "unicode-bidi" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unsafe-libyaml" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" [[package]] name = "url" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" dependencies = [ "getrandom", "serde", ] [[package]] name = "value-bag" version = "1.0.0-alpha.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" dependencies = [ "ctor", "version_check", ] [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[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.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.26", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", "syn 2.0.26", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "web-sys" version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" 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.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 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-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[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.0", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", "windows_i686_gnu 0.48.0", "windows_i686_msvc 0.48.0", "windows_x86_64_gnu 0.48.0", "windows_x86_64_gnullvm 0.48.0", "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" dependencies = [ "memchr", ] [[package]] name = "xdg-home" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" dependencies = [ "nix 0.26.2", "winapi", ] [[package]] name = "zbus" version = "3.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c3d77c9966c28321f1907f0b6c5a5561189d1f7311eea6d94180c6be9daab29" dependencies = [ "async-broadcast", "async-executor", "async-fs", "async-io", "async-lock", "async-process", "async-recursion", "async-task", "async-trait", "byteorder", "derivative", "enumflags2", "event-listener", "futures-core", "futures-sink", "futures-util", "hex", "nix 0.26.2", "once_cell", "ordered-stream", "rand 0.8.5", "serde", "serde_repr", "sha1", "static_assertions", "tracing", "uds_windows", "winapi", "xdg-home", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "3.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e341d12edaff644e539ccbbf7f161601294c9a84ed3d7e015da33155b435af" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", "syn 1.0.109", "winnow", "zvariant_utils", ] [[package]] name = "zbus_names" version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82441e6033be0a741157a72951a3e4957d519698f3a824439cc131c5ba77ac2a" dependencies = [ "serde", "static_assertions", "zvariant", ] [[package]] name = "zvariant" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622cc473f10cef1b0d73b7b34a266be30ebdcfaea40ec297dd8cbda088f9f93c" dependencies = [ "byteorder", "enumflags2", "libc", "serde", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d9c1b57352c25b778257c661f3c4744b7cefb7fc09dd46909a153cce7773da2" 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", ] 07070100000002000081A4000000000000000000000001653660D70000008C000000000000000000000000000000000000001100000000agama/Cargo.toml[workspace] members = [ "agama-cli", "agama-dbus-server", "agama-derive", "agama-lib", "agama-locale-data", "agama-settings" ] 07070100000003000081A4000000000000000000000001653660D7000046AC000000000000000000000000000000000000000E00000000agama/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. Copyright (C) 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. , 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. 07070100000004000081A4000000000000000000000001653660D7000010E5000000000000000000000000000000000000001000000000agama/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 ``. 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 07070100000005000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001000000000agama/agama-cli07070100000006000081A4000000000000000000000001653660D7000002F3000000000000000000000000000000000000001B00000000agama/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" async-std = { version ="1.12.0", features = ["attributes"] } 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 tempdir = "0.3" fs_extra = "1.3.0" nix = { version = "0.27.1", features = ["user"] } [[bin]] name = "agama" path = "src/main.rs" 07070100000007000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001400000000agama/agama-cli/doc07070100000008000081A4000000000000000000000001653660D7000028AE000000000000000000000000000000000000001F00000000agama/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 `). 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 `. 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 Loads installation config from a YAML file, keeping the rest of the config as it is. $ agama config show [] Prints the current installation config in YAML format. If a is given, then it only prints the content for the given key. $ agama config set = ... Sets a config value for the given key. $ agama config unset Removes the current value for the given key. $ agama config reset [] Sets the default value for the given . If no key is given, then the whole config is reset. $ agama config add [=] ... 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 [=] ... 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 [] 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 [
] 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 `` represents a YAML key from the config file (e.g., `root.ssh_key`) and `` 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=. ~~~ ### 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 # Load a config file and apply the configuration dinstallerctl language available # List available languages for the installation dinstallerctl language selected [...] # Select the languages to install in the target system dinstallerctl rootuser clear # Clear root configuration dinstallerctl rootuser password [] # Set the root password dinstallerctl rootuser ssh_key [] # Set the SSH key for root dinstallerctl software available_products # List available products for the installation dinstallerctl software selected_product [] # 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 [...] # Select devices for the installation dinstallerctl user clear # Clear the user configuration dinstallerctl user set # 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 07070100000009000081A4000000000000000000000001653660D700000B51000000000000000000000000000000000000002B00000000agama/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 ```sh # https://build.opensuse.org/package/show/systemsmanagement:Agama:Staging/agama-testing CIMAGE=registry.opensuse.org/systemsmanagement/agama/staging/containers/opensuse/agama-testing:latest # rename this if you test multiple things CNAME=agama # the '?' here will report a shell error # if you accidentally paste a command without setting the variable first echo ${CNAME?} test -f service/agama.gemspec || echo "You should run this from a checkout of agama" # destroy the previous instance podman stop ${CNAME?} podman rm ${CNAME?} mkdir -p ./mnt/log-yast2 # needed? mkdir -p ./mnt/run-agama # only needed for D-Bus access from outside, unused now # Update our image podman pull ${CIMAGE?} podman run --name ${CNAME?} \ --privileged --detach --ipc=host \ -v .:/checkout -v ./mnt/run-agama:/run/agama -v ./mnt/log-yast2:/var/log/YaST2 \ ${CIMAGE?} # shortcut for the following CEXEC="podman exec ${CNAME?} bash -c" ${CEXEC?} "cd /checkout && ./setup-service.sh" # Optional: explicit service start using a separate log file ${CEXEC?} "cd /checkout/service && (bundle exec bin/agamactl > service.log 2>&1 &)" # Now the CLI is in the same repo, just symlink it ${CEXEC?} "ln -sfv /checkout/./rust/target/debug/agama /usr/bin/agama" # Optional: Play! ${CEXEC?} "agama -f yaml config show" # Optional: show logs of autostarted services ${CEXEC?} "journalctl --since=-5min" # Optional: show logs of explicitly started services ${CEXEC?} "cat /checkout/service/service.log" # Optional: Interactive shell in the container podman exec --tty --interactive ${CNAME?} bash ``` 0707010000000A000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001400000000agama/agama-cli/src0707010000000B000081A4000000000000000000000001653660D7000004ED000000000000000000000000000000000000002000000000agama/agama-cli/src/commands.rsuse 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, }, /// 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), } 0707010000000C000081A4000000000000000000000001653660D700001180000000000000000000000000000000000000001E00000000agama/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 }, /// Set one or many installation settings Set { /// key-value pairs (e.g., user.name="Jane Doe") values: Vec, }, /// 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), Set(HashMap), 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 { 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) -> Result, 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::::new(); let empty_hash = HashMap::::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> { if let Some((name, _)) = key.split_once('.') { return Ok(Scope::from_str(name)?); } Err(Box::new(CliError::InvalidKeyName(key.to_string()))) } 0707010000000D000081A4000000000000000000000001653660D700000185000000000000000000000000000000000000001D00000000agama/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), } 0707010000000E000081A4000000000000000000000001653660D70000292B000000000000000000000000000000000000001C00000000agama/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::path::{Path, PathBuf}; use std::process::Command; use tempdir::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, }, /// 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 } => { // feed internal options structure by what was received from user // for now we always use / add defaults if any let options = LogOptions { verbose, ..Default::default() }; Ok(store(options)?) } LogsCommands::List => { list(LogOptions::default()); Ok(()) } } } const DEFAULT_COMMANDS: [(&str, &str); 3] = [ // (, ) ("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: // (, ) const DEFAULT_COMPRESSION: (&str, &str) = ("bzip2", "tar.bz2"); const DEFAULT_TMP_DIR: &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, commands: Vec<(String, String)>, verbose: bool, } 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, } } } // 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 //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(format!("{}", file_name)) } fn store(&self) -> Result<(), io::Error> { let cmd_parts = self.cmd.split_whitespace().collect::>(); 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: &Vec, tmp_dir: &TempDir) -> Vec> { let mut log_sources: Vec> = 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: &Vec<(String, String)>, tmp_dir: &TempDir, ) -> Vec> { let mut log_sources: Vec> = 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::>(); let res = Command::new(cmd_parts[0]) .args(cmd_parts[1..].iter()) .status()?; if res.success() { Ok(()) } else { Err(io::Error::new( io::ErrorKind::Other, "Cannot create tar archive", )) } } // 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 result = format!("{}.{}", DEFAULT_RESULT, 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::new(DEFAULT_TMP_DIR)?; 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 showln(true, format!("Storing result in: \"{}\"", 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 //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, format!("{}", res).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!(); } } 0707010000000F000081A4000000000000000000000001653660D70000121A000000000000000000000000000000000000001C00000000agama/agama-cli/src/main.rsuse clap::Parser; 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 async_std::task::{self, block_on}; 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 = task::spawn(async move { 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 = task::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 task::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> { 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?; block_on(wait_for_services(&manager))?; block_on(run_config_cmd(subcommand, cli.format)) } Commands::Probe => { let manager = build_manager().await?; block_on(wait_for_services(&manager))?; block_on(probe()) } Commands::Profile(subcommand) => Ok(run_profile_cmd(subcommand)?), Commands::Install => { let manager = build_manager().await?; block_on(install(&manager, 3)) } Commands::Questions(subcommand) => block_on(run_questions_cmd(subcommand)), Commands::Logs(subcommand) => block_on(run_logs_cmd(subcommand)), _ => 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) } } #[async_std::main] async fn main() -> CliResult { let cli = Cli::parse(); if let Err(error) = run_command(cli).await { eprintln!("{:?}", error); return CliResult::Error; } CliResult::Ok } 07070100000010000081A4000000000000000000000001653660D700000718000000000000000000000000000000000000002000000000agama/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(content: T, writer: W, format: Format) -> anyhow::Result<()> where T: serde::Serialize + Debug, W: Write, { let printer: Box> = 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 { fn print(self: Box) -> anyhow::Result<()>; } pub struct JsonPrinter { content: T, writer: W, } impl Printer for JsonPrinter { fn print(mut self: Box) -> anyhow::Result<()> { let json = serde_json::to_string(&self.content)?; Ok(writeln!(self.writer, "{}", json)?) } } pub struct TextPrinter { content: T, writer: W, } impl Printer for TextPrinter { fn print(mut self: Box) -> anyhow::Result<()> { Ok(writeln!(self.writer, "{:?}", &self.content)?) } } pub struct YamlPrinter { content: T, writer: W, } impl Printer for YamlPrinter { fn print(self: Box) -> anyhow::Result<()> { Ok(serde_yaml::to_writer(self.writer, &self.content)?) } } 07070100000011000081A4000000000000000000000001653660D70000063D000000000000000000000000000000000000001F00000000agama/agama-cli/src/profile.rsuse agama_lib::profile::{download, ProfileEvaluator, 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 validate(path: String) -> anyhow::Result<()> { let validator = ProfileValidator::default_schema()?; let path = Path::new(&path); let result = validator .validate_file(path) .context("Could not validate the profile")?; 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 } => Ok(download(&url)?), ProfileCommands::Validate { path } => validate(path), ProfileCommands::Evaluate { path } => evaluate(path), } } 07070100000012000081A4000000000000000000000001653660D7000006BB000000000000000000000000000000000000002000000000agama/agama-cli/src/progress.rsuse agama_lib::progress::{Progress, ProgressPresenter}; use console::style; use indicatif::{ProgressBar, ProgressStyle}; use std::time::Duration; /// Reports the installer progress through the terminal pub struct InstallerProgress { bar: Option, } 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()); } } impl ProgressPresenter for InstallerProgress { fn start(&mut self, progress: &Progress) { if !progress.finished { self.update_main(progress); } } fn update_main(&mut self, progress: &Progress) { let counter = format!("[{}/{}]", &progress.current_step, &progress.max_steps); println!( "{} {}", style(&counter).bold().green(), &progress.current_title ); } 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); } } fn finish(&mut self) { if let Some(bar) = self.bar.take() { bar.finish_and_clear(); } } } 07070100000013000081A4000000000000000000000001653660D7000007E9000000000000000000000000000000000000002100000000agama/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, } } 07070100000014000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001800000000agama/agama-dbus-server07070100000015000081A4000000000000000000000001653660D70000029B000000000000000000000000000000000000002300000000agama/agama-dbus-server/Cargo.toml[package] name = "agama-dbus-server" 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" 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 = "3.7.0" zbus_macros = "3.7.0" async-std = { version = "1.12.0", features = ["attributes"]} uuid = { version = "1.3.4", features = ["v4"] } thiserror = "1.0.40" serde = { version = "1.0.152", features = ["derive"] } serde_yaml = "0.9.24" futures = "0.3.28" cidr = { version = "0.2.2", features = ["serde"] } 07070100000016000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001C00000000agama/agama-dbus-server/src07070100000017000081A4000000000000000000000001653660D70000023E000000000000000000000000000000000000002500000000agama/agama-dbus-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 = Result; impl From for Error { fn from(e: anyhow::Error) -> Self { // {:#} includes causes Self::Anyhow(format!("{:#}", e)) } } 07070100000018000081A4000000000000000000000001653660D700000043000000000000000000000000000000000000002300000000agama/agama-dbus-server/src/lib.rspub mod error; pub mod locale; pub mod network; pub mod questions; 07070100000019000081A4000000000000000000000001653660D700002041000000000000000000000000000000000000002600000000agama/agama-dbus-server/src/locale.rsuse crate::error::Error; use anyhow::Context; use std::{fs::read_dir, process::Command}; use zbus::{dbus_interface, Connection}; pub struct Locale { locales: Vec, keymap: String, timezone_id: String, supported_locales: Vec, ui_locale: String, } #[dbus_interface(name = "org.opensuse.Agama1.Locale")] impl Locale { /// Get labels for locales. The first pair is english language and territory /// and second one is localized one to target language from locale. /// // Can be `async` as well. // NOTE: check how often it is used and if often, it can be easily cached fn labels_for_locales(&self) -> Result, Error> { const DEFAULT_LANG: &str = "en"; let mut res = Vec::with_capacity(self.supported_locales.len()); let languages = agama_locale_data::get_languages()?; let territories = agama_locale_data::get_territories()?; for locale in self.supported_locales.as_slice() { let (loc_language, loc_territory) = agama_locale_data::parse_locale(locale.as_str())?; let language = languages .find_by_id(loc_language) .context("language for passed locale not found")?; let territory = territories .find_by_id(loc_territory) .context("territory for passed locale not found")?; let default_ret = ( language .names .name_for(DEFAULT_LANG) .context("missing default translation for language")?, territory .names .name_for(DEFAULT_LANG) .context("missing default translation for territory")?, ); let localized_ret = ( language .names .name_for(language.id.as_str()) .context("missing native label for language")?, territory .names .name_for(language.id.as_str()) .context("missing native label for territory")?, ); res.push((default_ret, localized_ret)); } Ok(res) } #[dbus_interface(property)] fn locales(&self) -> Vec { self.locales.to_owned() } #[dbus_interface(property)] fn set_locales(&mut self, locales: Vec) -> zbus::fdo::Result<()> { for loc in &locales { if !self.supported_locales.contains(loc) { return Err(zbus::fdo::Error::Failed(format!( "Unsupported locale value '{loc}'" ))); } } self.locales = locales; Ok(()) } #[dbus_interface(property)] fn supported_locales(&self) -> Vec { self.supported_locales.to_owned() } #[dbus_interface(property)] fn set_supported_locales(&mut self, locales: Vec) -> Result<(), zbus::fdo::Error> { self.supported_locales = locales; // TODO: handle if current selected locale contain something that is no longer supported Ok(()) } #[dbus_interface(property, name = "UILocale")] fn ui_locale(&self) -> &str { &self.ui_locale } #[dbus_interface(property, name = "UILocale")] fn set_ui_locale(&mut self, locale: &str) { self.ui_locale = locale.to_string(); } /// Gets list of locales available on system. /// /// # Examples /// /// ``` /// use agama_dbus_server::locale::Locale; /// let locale = Locale::new(); /// assert!(locale.list_ui_locales().unwrap().len() > 0); /// ``` #[dbus_interface(name = "ListUILocales")] pub fn list_ui_locales(&self) -> Result, Error> { // english is always available ui localization let mut result = vec!["en".to_string()]; const DIR: &str = "/usr/share/YaST2/locale/"; let entries = read_dir(DIR); if entries.is_err() { // if dir is not there act like if it is empty return Ok(result); } for entry in entries.unwrap() { let entry = entry.context("Failed to read entry in YaST2 locale dir")?; let name = entry .file_name() .to_str() .context("Non valid UTF entry found in YaST2 locale dir")? .to_string(); result.push(name) } Ok(result) } /* support only keymaps for console for now fn list_x11_keyboards(&self) -> Result, Error> { let keyboards = agama_locale_data::get_xkeyboards()?; let ret = keyboards .keyboard.iter() .map(|k| (k.id.clone(), k.description.clone())) .collect(); Ok(ret) } fn set_x11_keyboard(&mut self, keyboard: &str) { self.keyboard_id = keyboard.to_string(); } */ #[dbus_interface(name = "ListVConsoleKeyboards")] fn list_keyboards(&self) -> Result, Error> { let res = agama_locale_data::get_key_maps()?; Ok(res) } #[dbus_interface(property, name = "VConsoleKeyboard")] fn keymap(&self) -> &str { self.keymap.as_str() } #[dbus_interface(property, name = "VConsoleKeyboard")] fn set_keymap(&mut self, keyboard: &str) -> Result<(), zbus::fdo::Error> { let exist = agama_locale_data::get_key_maps() .unwrap() .iter() .any(|k| k == keyboard); if !exist { return Err(zbus::fdo::Error::Failed( "Invalid keyboard value".to_string(), )); } self.keymap = keyboard.to_string(); Ok(()) } fn list_timezones(&self, locale: &str) -> Result, Error> { let timezones = agama_locale_data::get_timezones(); let localized = agama_locale_data::get_timezone_parts()?.localize_timezones(locale, &timezones); let ret = timezones.into_iter().zip(localized.into_iter()).collect(); Ok(ret) } #[dbus_interface(property)] fn timezone(&self) -> &str { self.timezone_id.as_str() } #[dbus_interface(property)] fn set_timezone(&mut self, timezone: &str) -> Result<(), zbus::fdo::Error> { // NOTE: cannot use crate::Error as property expect this one self.timezone_id = timezone.to_string(); 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, "--locale", self.locales.first().context("missing locale")?.as_str(), ]) .status() .context("Failed to execute systemd-firstboot")?; Command::new("/usr/bin/systemd-firstboot") .args(["root", ROOT, "--keymap", self.keymap.as_str()]) .status() .context("Failed to execute systemd-firstboot")?; Command::new("/usr/bin/systemd-firstboot") .args(["root", ROOT, "--timezone", self.timezone_id.as_str()]) .status() .context("Failed to execute systemd-firstboot")?; Ok(()) } } impl Locale { pub fn new() -> Self { Self { locales: vec!["en_US.UTF-8".to_string()], keymap: "us".to_string(), timezone_id: "America/Los_Angeles".to_string(), supported_locales: vec!["en_US.UTF-8".to_string()], ui_locale: "en".to_string(), } } } impl Default for Locale { fn default() -> Self { Self::new() } } pub async fn export_dbus_objects( connection: &Connection, ) -> Result<(), Box> { const PATH: &str = "/org/opensuse/Agama1/Locale"; // When serving, request the service name _after_ exposing the main object let locale = Locale::new(); connection.object_server().at(PATH, locale).await?; Ok(()) } 0707010000001A000081A4000000000000000000000001653660D700000730000000000000000000000000000000000000002400000000agama/agama-dbus-server/src/main.rsuse agama_dbus_server::{locale, network, questions}; use agama_lib::connection_to; use anyhow::Context; use log::LevelFilter; use std::future::pending; const ADDRESS: &str = "unix:path=/run/agama/bus"; const SERVICE_NAME: &str = "org.opensuse.Agama1"; #[async_std::main] async fn main() -> Result<(), Box> { // 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"); locale::export_dbus_objects(&connection).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(()) } 0707010000001B000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000002400000000agama/agama-dbus-server/src/network0707010000001C000081A4000000000000000000000001653660D700000A35000000000000000000000000000000000000002700000000agama/agama-dbus-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; 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> { let adapter = NetworkManagerAdapter::from_system() .await .expect("Could not connect to NetworkManager to read the configuration."); NetworkService::start(&connection, adapter).await } 0707010000001D000081A4000000000000000000000001653660D70000028E000000000000000000000000000000000000002E00000000agama/agama-dbus-server/src/network/action.rsuse crate::network::model::Connection; use agama_lib::network::types::DeviceType; /// 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), /// Update a connection (replacing the old one). UpdateConnection(Connection), /// Remove the connection with the given Uuid. RemoveConnection(String), /// Apply the current configuration. Apply, } 0707010000001E000081A4000000000000000000000001653660D70000011B000000000000000000000000000000000000002F00000000agama/agama-dbus-server/src/network/adapter.rsuse crate::network::NetworkState; use std::error::Error; /// A trait for the ability to read/write from/to a network service pub trait Adapter { fn read(&self) -> Result>; fn write(&self, network: &NetworkState) -> Result<(), Box>; } 0707010000001F000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000002900000000agama/agama-dbus-server/src/network/dbus07070100000020000081A4000000000000000000000001653660D70000011B000000000000000000000000000000000000002C00000000agama/agama-dbus-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::{ObjectsRegistry, Tree}; 07070100000021000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000003400000000agama/agama-dbus-server/src/network/dbus/interfaces07070100000022000081A4000000000000000000000001653660D700003A2C000000000000000000000000000000000000003700000000agama/agama-dbus-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). use super::ObjectsRegistry; use crate::network::{ action::Action, error::NetworkStateError, model::{Connection as NetworkConnection, Device as NetworkDevice, WirelessConnection}, }; use agama_lib::network::types::SSID; use async_std::{channel::Sender, sync::Arc}; use futures::lock::{MappedMutexGuard, Mutex, MutexGuard}; use zbus::{ dbus_interface, zvariant::{ObjectPath, OwnedObjectPath}, SignalContext, }; mod ip_config; pub use ip_config::Ip; /// D-Bus interface for the network devices collection /// /// It offers an API to query the devices collection. pub struct Devices { objects: Arc>, } impl Devices { /// Creates a Devices interface object. /// /// * `objects`: Objects paths registry. pub fn new(objects: Arc>) -> Self { Self { objects } } } #[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) -> Vec { let objects = self.objects.lock().await; objects .devices_paths() .iter() .filter_map(|c| ObjectPath::try_from(c.clone()).ok()) .collect() } } /// 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 } } /// D-Bus interface for the set of connections. /// /// It offers an API to query the connections collection. pub struct Connections { actions: Arc>>, objects: Arc>, } impl Connections { /// Creates a Connections interface object. /// /// * `objects`: Objects paths registry. pub fn new(objects: Arc>, actions: Sender) -> Self { Self { objects, 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) -> Vec { let objects = self.objects.lock().await; objects .connections_paths() .iter() .filter_map(|c| ObjectPath::try_from(c.clone()).ok()) .collect() } /// 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::fdo::Result<()> { let actions = self.actions.lock().await; actions .send(Action::AddConnection(id.clone(), ty.try_into()?)) .await .unwrap(); Ok(()) } /// Returns the D-Bus path of the network connection. /// /// * `id`: connection ID. pub async fn get_connection(&self, id: &str) -> zbus::fdo::Result { let objects = self.objects.lock().await; match objects.connection_path(id) { Some(path) => Ok(path.into()), None => Err(NetworkStateError::UnknownConnection(id.to_string()).into()), } } /// Removes a network connection. /// /// * `uuid`: connection UUID.. pub async fn remove_connection(&mut self, id: &str) -> zbus::fdo::Result<()> { let actions = self.actions.lock().await; actions .send(Action::RemoveConnection(id.to_string())) .await .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; actions.send(Action::Apply).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>>, connection: Arc>, } impl Connection { /// Creates a Connection interface object. /// /// * `actions`: sending-half of a channel to send actions. /// * `connection`: connection to expose over D-Bus. pub fn new(actions: Sender, connection: Arc>) -> Self { Self { actions: Arc::new(Mutex::new(actions)), connection, } } /// Returns the underlying connection. async fn get_connection(&self) -> MutexGuard { self.connection.lock().await } /// Updates the connection data in the NetworkSystem. /// /// * `connection`: Updated connection. async fn update_connection<'a>( &self, connection: MutexGuard<'a, NetworkConnection>, ) -> zbus::fdo::Result<()> { let actions = self.actions.lock().await; actions .send(Action::UpdateConnection(connection.clone())) .await .unwrap(); Ok(()) } } #[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) -> String { self.get_connection().await.id().to_string() } #[dbus_interface(property)] pub async fn interface(&self) -> String { self.get_connection().await.interface().to_string() } #[dbus_interface(property)] pub async fn set_interface(&mut self, name: &str) -> zbus::fdo::Result<()> { let mut connection = self.get_connection().await; connection.set_interface(name); self.update_connection(connection).await } } /// D-Bus interface for Match settings pub struct Match { actions: Arc>>, connection: Arc>, } impl Match { /// Creates a Match Settings interface object. /// /// * `actions`: sending-half of a channel to send actions. /// * `connection`: connection to expose over D-Bus. pub fn new(actions: Sender, connection: Arc>) -> Self { Self { actions: Arc::new(Mutex::new(actions)), connection, } } /// Returns the underlying connection. async fn get_connection(&self) -> MutexGuard { self.connection.lock().await } /// Updates the connection data in the NetworkSystem. /// /// * `connection`: Updated connection. async fn update_connection<'a>( &self, connection: MutexGuard<'a, NetworkConnection>, ) -> zbus::fdo::Result<()> { let actions = self.actions.lock().await; actions .send(Action::UpdateConnection(connection.clone())) .await .unwrap(); Ok(()) } } #[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) -> Vec { let connection = self.get_connection().await; connection.match_config().driver.clone() } #[dbus_interface(property)] pub async fn set_driver(&mut self, driver: Vec) -> zbus::fdo::Result<()> { let mut connection = self.get_connection().await; let config = connection.match_config_mut(); config.driver = driver; self.update_connection(connection).await } /// List of paths to match agains the ID_PATH udev property of devices. #[dbus_interface(property)] pub async fn path(&self) -> Vec { let connection = self.get_connection().await; connection.match_config().path.clone() } #[dbus_interface(property)] pub async fn set_path(&mut self, path: Vec) -> zbus::fdo::Result<()> { let mut connection = self.get_connection().await; let config = connection.match_config_mut(); config.path = path; self.update_connection(connection).await } /// List of interface names to match. #[dbus_interface(property)] pub async fn interface(&self) -> Vec { let connection = self.get_connection().await; connection.match_config().interface.clone() } #[dbus_interface(property)] pub async fn set_interface(&mut self, interface: Vec) -> zbus::fdo::Result<()> { let mut connection = self.get_connection().await; let config = connection.match_config_mut(); config.interface = interface; self.update_connection(connection).await } /// List of kernel options to match. #[dbus_interface(property)] pub async fn kernel(&self) -> Vec { let connection = self.get_connection().await; connection.match_config().kernel.clone() } #[dbus_interface(property)] pub async fn set_kernel(&mut self, kernel: Vec) -> zbus::fdo::Result<()> { let mut connection = self.get_connection().await; let config = connection.match_config_mut(); config.kernel = kernel; self.update_connection(connection).await } } /// D-Bus interface for wireless settings pub struct Wireless { actions: Arc>>, connection: Arc>, } impl Wireless { /// Creates a Wireless interface object. /// /// * `actions`: sending-half of a channel to send actions. /// * `connection`: connection to expose over D-Bus. pub fn new(actions: Sender, connection: Arc>) -> Self { Self { actions: Arc::new(Mutex::new(actions)), connection, } } /// Gets the wireless connection. /// /// Beware that it crashes when it is not a wireless connection. async fn get_wireless(&self) -> MappedMutexGuard { MutexGuard::map(self.connection.lock().await, |c| match c { NetworkConnection::Wireless(config) => config, _ => panic!("Not a wireless network. This is most probably a bug."), }) } /// Updates the connection data in the NetworkSystem. /// /// * `connection`: Updated connection. async fn update_connection<'a>( &self, connection: MappedMutexGuard<'a, NetworkConnection, WirelessConnection>, ) -> zbus::fdo::Result<()> { let actions = self.actions.lock().await; let connection = NetworkConnection::Wireless(connection.clone()); actions .send(Action::UpdateConnection(connection)) .await .unwrap(); Ok(()) } } #[dbus_interface(name = "org.opensuse.Agama1.Network.Connection.Wireless")] impl Wireless { /// Network SSID. #[dbus_interface(property, name = "SSID")] pub async fn ssid(&self) -> Vec { let connection = self.get_wireless().await; connection.wireless.ssid.clone().into() } #[dbus_interface(property, name = "SSID")] pub async fn set_ssid(&mut self, ssid: Vec) -> zbus::fdo::Result<()> { let mut connection = self.get_wireless().await; connection.wireless.ssid = SSID(ssid); self.update_connection(connection).await } /// Wireless connection mode. /// /// Possible values: "unknown", "adhoc", "infrastructure", "ap" or "mesh". /// /// See [crate::network::model::WirelessMode]. #[dbus_interface(property)] pub async fn mode(&self) -> String { let connection = self.get_wireless().await; connection.wireless.mode.to_string() } #[dbus_interface(property)] pub async fn set_mode(&mut self, mode: &str) -> zbus::fdo::Result<()> { let mut connection = self.get_wireless().await; connection.wireless.mode = mode.try_into()?; self.update_connection(connection).await } /// Password to connect to the wireless network. #[dbus_interface(property)] pub async fn password(&self) -> String { let connection = self.get_wireless().await; connection .wireless .password .clone() .unwrap_or("".to_string()) } #[dbus_interface(property)] pub async fn set_password(&mut self, password: String) -> zbus::fdo::Result<()> { let mut connection = self.get_wireless().await; connection.wireless.password = if password.is_empty() { None } else { Some(password) }; self.update_connection(connection).await } /// 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) -> String { let connection = self.get_wireless().await; connection.wireless.security.to_string() } #[dbus_interface(property)] pub async fn set_security(&mut self, security: &str) -> zbus::fdo::Result<()> { let mut connection = self.get_wireless().await; connection.wireless.security = security .try_into() .map_err(|_| NetworkStateError::InvalidSecurityProtocol(security.to_string()))?; self.update_connection(connection).await?; Ok(()) } } 07070100000023000081A4000000000000000000000001653660D700001C8B000000000000000000000000000000000000004100000000agama/agama-dbus-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` struct. use crate::network::{ action::Action, model::{Connection as NetworkConnection, IpConfig, IpMethod}, }; use async_std::{channel::Sender, sync::Arc}; use cidr::IpInet; use futures::lock::{MappedMutexGuard, Mutex, MutexGuard}; use std::net::IpAddr; use zbus::dbus_interface; /// D-Bus interface for IPv4 and IPv6 settings pub struct Ip { actions: Arc>>, connection: Arc>, } impl Ip { /// Creates an IP interface object. /// /// * `actions`: sending-half of a channel to send actions. /// * `connection`: connection to expose over D-Bus. pub fn new(actions: Sender, connection: Arc>) -> Self { Self { actions: Arc::new(Mutex::new(actions)), connection, } } /// Returns the underlying connection. async fn get_connection(&self) -> MutexGuard { self.connection.lock().await } /// Updates the connection data in the NetworkSystem. /// /// * `connection`: Updated connection. async fn update_connection<'a>( &self, connection: MutexGuard<'a, NetworkConnection>, ) -> zbus::fdo::Result<()> { let actions = self.actions.lock().await; actions .send(Action::UpdateConnection(connection.clone())) .await .unwrap(); Ok(()) } } impl Ip { /// Returns the IpConfig struct. async fn get_ip_config(&self) -> MappedMutexGuard { MutexGuard::map(self.get_connection().await, |c| c.ip_config_mut()) } /// Updates the IpConfig struct. /// /// * `func`: function to update the configuration. async fn update_config(&self, func: F) -> zbus::fdo::Result<()> where F: Fn(&mut IpConfig), { let mut connection = self.get_connection().await; func(connection.ip_config_mut()); self.update_connection(connection).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) -> Vec { let ip_config = self.get_ip_config().await; ip_config.addresses.iter().map(|a| a.to_string()).collect() } #[dbus_interface(property)] pub async fn set_addresses(&mut self, addresses: Vec) -> zbus::fdo::Result<()> { let addresses = helpers::parse_addresses::(addresses); self.update_config(|ip| ip.addresses = addresses.clone()) .await } /// IPv4 configuration method. /// /// Possible values: "disabled", "auto", "manual" or "link-local". /// /// See [crate::network::model::IpMethod]. #[dbus_interface(property)] pub async fn method4(&self) -> String { let ip_config = self.get_ip_config().await; ip_config.method4.to_string() } #[dbus_interface(property)] pub async fn set_method4(&mut self, method: &str) -> zbus::fdo::Result<()> { let method: IpMethod = method.parse()?; self.update_config(|ip| ip.method4 = method).await } /// IPv6 configuration method. /// /// Possible values: "disabled", "auto", "manual" or "link-local". /// /// See [crate::network::model::IpMethod]. #[dbus_interface(property)] pub async fn method6(&self) -> String { let ip_config = self.get_ip_config().await; ip_config.method6.to_string() } #[dbus_interface(property)] pub async fn set_method6(&mut self, method: &str) -> zbus::fdo::Result<()> { let method: IpMethod = method.parse()?; self.update_config(|ip| ip.method6 = method).await } /// Name server addresses. #[dbus_interface(property)] pub async fn nameservers(&self) -> Vec { let ip_config = self.get_ip_config().await; ip_config .nameservers .iter() .map(IpAddr::to_string) .collect() } #[dbus_interface(property)] pub async fn set_nameservers(&mut self, addresses: Vec) -> zbus::fdo::Result<()> { let addresses = helpers::parse_addresses::(addresses); self.update_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) -> String { let ip_config = self.get_ip_config().await; match ip_config.gateway4 { Some(ref address) => address.to_string(), None => "".to_string(), } } #[dbus_interface(property)] pub async fn set_gateway4(&mut self, gateway: String) -> zbus::fdo::Result<()> { let gateway = helpers::parse_gateway(gateway)?; self.update_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) -> String { let ip_config = self.get_ip_config().await; match ip_config.gateway6 { Some(ref address) => address.to_string(), None => "".to_string(), } } #[dbus_interface(property)] pub async fn set_gateway6(&mut self, gateway: String) -> zbus::fdo::Result<()> { let gateway = helpers::parse_gateway(gateway)?; self.update_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(addresses: Vec) -> Vec where T: FromStr, ::Err: Display, { addresses .into_iter() .filter_map(|ip| match ip.parse::() { 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(gateway: String) -> Result, NetworkStateError> where T: FromStr, ::Err: Debug + Display, { if gateway.is_empty() { Ok(None) } else { let parsed = gateway .parse() .map_err(|_| NetworkStateError::InvalidIpAddr(gateway))?; Ok(Some(parsed)) } } } 07070100000024000081A4000000000000000000000001653660D7000003DA000000000000000000000000000000000000003400000000agama/agama-dbus-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 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( connection: &Connection, adapter: T, ) -> Result<(), Box> { let connection = connection.clone(); let mut network = NetworkSystem::new(connection.clone(), adapter); async_std::task::spawn(async move { network .setup() .await .expect("Could not set up the D-Bus tree"); network.listen().await; }); Ok(()) } } 07070100000025000081A4000000000000000000000001653660D700002529000000000000000000000000000000000000003100000000agama/agama-dbus-server/src/network/dbus/tree.rsuse agama_lib::error::ServiceError; use futures::lock::Mutex; use zbus::zvariant::{ObjectPath, OwnedObjectPath}; use crate::network::{action::Action, dbus::interfaces, model::*}; use async_std::{channel::Sender, sync::Arc}; use log; use std::collections::HashMap; 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: Sender, objects: Arc>, } 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: Sender) -> 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( &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?; let mut objects = self.objects.lock().await; objects.register_device(&dev.name, path); } self.add_interface( DEVICES_PATH, interfaces::Devices::new(Arc::clone(&self.objects)), ) .await?; Ok(()) } /// Adds a connection to the D-Bus tree. /// /// * `connection`: connection to add. /// * `notify`: whether to notify the added connection pub async fn add_connection( &self, conn: &mut Connection, notify: bool, ) -> Result<(), ServiceError> { let mut objects = self.objects.lock().await; let orig_id = conn.id().to_owned(); let (id, path) = objects.register_connection(conn); if id != conn.id() { conn.set_id(&id) } log::info!("Publishing network connection '{}'", id); let cloned = Arc::new(Mutex::new(conn.clone())); self.add_interface( &path, interfaces::Connection::new(self.actions.clone(), Arc::clone(&cloned)), ) .await?; self.add_interface( &path, interfaces::Ip::new(self.actions.clone(), Arc::clone(&cloned)), ) .await?; self.add_interface( &path, interfaces::Match::new(self.actions.clone(), Arc::clone(&cloned)), ) .await?; if let Connection::Wireless(_) = conn { self.add_interface( &path, interfaces::Wireless::new(self.actions.clone(), Arc::clone(&cloned)), ) .await?; } if notify { self.notify_connection_added(&orig_id, &path).await?; } Ok(()) } /// Removes a connection from the tree /// /// * `id`: connection ID. pub async fn remove_connection(&mut self, id: &str) -> Result<(), ServiceError> { let mut objects = self.objects.lock().await; let Some(path) = objects.connection_path(id) else { return Ok(()); }; self.remove_connection_on(path.as_str()).await?; objects.deregister_connection(id).unwrap(); Ok(()) } /// Adds connections to the D-Bus tree. /// /// * `connections`: list of connections. async fn add_connections(&self, connections: &mut [Connection]) -> Result<(), ServiceError> { for conn in connections.iter_mut() { self.add_connection(conn, false).await?; } self.add_interface( CONNECTIONS_PATH, interfaces::Connections::new(Arc::clone(&self.objects), self.actions.clone()), ) .await?; Ok(()) } /// Clears all the connections from the tree. async fn remove_connections(&self) -> Result<(), ServiceError> { let mut objects = self.objects.lock().await; for path in objects.connections.values() { self.remove_connection_on(path.as_str()).await?; } 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(); let mut objects = self.objects.lock().await; for path in objects.devices.values() { object_server .remove::(path.as_str()) .await?; } 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::(path).await; object_server.remove::(path).await?; object_server .remove::(path) .await?; Ok(()) } async fn add_interface(&self, path: &str, iface: T) -> Result where T: zbus::Interface, { let object_server = self.connection.object_server(); Ok(object_server.at(path, iface).await?) } /// Notify that a new connection has been added async fn notify_connection_added( &self, id: &str, path: &ObjectPath<'_>, ) -> Result<(), ServiceError> { let object_server = self.connection.object_server(); let iface_ref = object_server .interface::<_, interfaces::Connections>(CONNECTIONS_PATH) .await?; Ok(interfaces::Connections::connection_added(iface_ref.signal_context(), id, path).await?) } } /// Objects paths for known devices and connections /// /// Connections are indexed by its Id, which is expected to be unique. #[derive(Debug, Default)] pub struct ObjectsRegistry { /// device_name (eth0) -> object_path devices: HashMap, /// id -> object_path connections: HashMap, } 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. /// /// * `conn`: network connection. pub fn register_connection(&mut self, conn: &Connection) -> (String, ObjectPath) { let path = format!("{}/{}", CONNECTIONS_PATH, self.connections.len()); let path = ObjectPath::try_from(path).unwrap(); let mut id = conn.id().to_string(); if self.connection_path(&id).is_some() { id = self.propose_id(&id); }; self.connections.insert(id.clone(), path.clone().into()); (id, path) } /// Returns the path for a connection. /// /// * `id`: connection ID. pub fn connection_path(&self, id: &str) -> Option { self.connections.get(id).map(|p| p.as_ref()) } /// Deregisters a network connection. /// /// * `id`: connection ID. pub fn deregister_connection(&mut self, id: &str) -> Option { self.connections.remove(id) } /// Returns all devices paths. pub fn devices_paths(&self) -> Vec { self.devices.values().map(|p| p.to_string()).collect() } /// Returns all connection paths. pub fn connections_paths(&self) -> Vec { self.connections.values().map(|p| p.to_string()).collect() } /// Proposes a connection ID. /// /// * `id`: original connection ID. fn propose_id(&self, id: &str) -> String { let prefix = format!("{}-", id); let filtered: Vec<_> = self .connections .keys() .filter_map(|i| i.strip_prefix(&prefix).and_then(|n| n.parse::().ok())) .collect(); let index = filtered.into_iter().max().unwrap_or(0); format!("{}{}", prefix, index + 1) } } 07070100000026000081A4000000000000000000000001653660D7000003BE000000000000000000000000000000000000002D00000000agama/agama-dbus-server/src/network/error.rs//! Error types. use thiserror::Error; use uuid::Uuid; /// Errors that are related to the network configuration. #[derive(Error, Debug)] pub enum NetworkStateError { #[error("Unknown connection with ID: '{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(Uuid), #[error("Invalid security wireless protocol: '{0}'")] InvalidSecurityProtocol(String), #[error("Adapter error: '{0}'")] AdapterError(String), } impl From for zbus::fdo::Error { fn from(value: NetworkStateError) -> zbus::fdo::Error { zbus::fdo::Error::Failed(format!("Network error: {value}")) } } 07070100000027000081A4000000000000000000000001653660D7000039A8000000000000000000000000000000000000002D00000000agama/agama-dbus-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::{DeviceType, SSID}; use cidr::IpInet; use std::{ default::Default, fmt, net::IpAddr, str::{self, FromStr}, }; use thiserror::Error; use uuid::Uuid; #[derive(Default, Clone)] pub struct NetworkState { pub devices: Vec, pub connections: Vec, } 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, connections: Vec) -> 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(&self, id: &str) -> Option<&Connection> { self.connections.iter().find(|c| c.id() == id) } /// Get connection by UUID as mutable /// /// * `uuid`: connection UUID pub fn get_connection_mut(&mut self, id: &str) -> Option<&mut Connection> { self.connections.iter_mut().find(|c| c.id() == id) } /// 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.uuid())); } 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().to_string())); }; *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, id: &str) -> Result<(), NetworkStateError> { let Some(conn) = self.get_connection_mut(id) else { return Err(NetworkStateError::UnknownConnection(id.to_string())); }; conn.remove(); Ok(()) } } #[cfg(test)] mod tests { use uuid::Uuid; use super::*; use crate::network::error::NetworkStateError; #[test] fn test_add_connection() { let mut state = NetworkState::default(); let uuid = Uuid::new_v4(); let base = BaseConnection { id: "eth0".to_string(), uuid, ..Default::default() }; let conn0 = Connection::Ethernet(EthernetConnection { base }); 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 uuid = Uuid::new_v4(); let base = BaseConnection { uuid, ..Default::default() }; let conn0 = Connection::Ethernet(EthernetConnection { base }); 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 base0 = BaseConnection { id: "eth0".to_string(), uuid: Uuid::new_v4(), ..Default::default() }; let conn0 = Connection::Ethernet(EthernetConnection { base: base0 }); state.add_connection(conn0).unwrap(); let uuid = Uuid::new_v4(); let base1 = BaseConnection { id: "eth0".to_string(), uuid, ..Default::default() }; let conn2 = Connection::Ethernet(EthernetConnection { base: base1 }); state.update_connection(conn2).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 uuid = Uuid::new_v4(); let base = BaseConnection { uuid, ..Default::default() }; let conn0 = Connection::Ethernet(EthernetConnection { base }); let error = state.update_connection(conn0).unwrap_err(); assert!(matches!(error, NetworkStateError::UnknownConnection(_))); } #[test] fn test_remove_connection() { let mut state = NetworkState::default(); let id = "eth0".to_string(); let uuid = Uuid::new_v4(); let base0 = BaseConnection { id, uuid, ..Default::default() }; let conn0 = Connection::Ethernet(EthernetConnection { base: base0 }); state.add_connection(conn0).unwrap(); state.remove_connection("eth0").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("eth0").unwrap_err(); assert!(matches!(error, NetworkStateError::UnknownConnection(_))); } #[test] fn test_is_loopback() { let base = BaseConnection { id: "eth0".to_string(), ..Default::default() }; let conn = Connection::Ethernet(EthernetConnection { base }); assert!(!conn.is_loopback()); let base = BaseConnection { id: "lo".to_string(), ..Default::default() }; let conn = Connection::Loopback(LoopbackConnection { base }); assert!(conn.is_loopback()); } } /// Network device #[derive(Debug, Clone)] pub struct Device { pub name: String, pub type_: DeviceType, } /// Represents an available network connection #[derive(Debug, PartialEq, Clone)] pub enum Connection { Ethernet(EthernetConnection), Wireless(WirelessConnection), Loopback(LoopbackConnection), } impl Connection { pub fn new(id: String, device_type: DeviceType) -> Self { let base = BaseConnection { id, ..Default::default() }; match device_type { DeviceType::Wireless => Connection::Wireless(WirelessConnection { base, ..Default::default() }), DeviceType::Loopback => Connection::Loopback(LoopbackConnection { base }), DeviceType::Ethernet => Connection::Ethernet(EthernetConnection { base }), } } /// TODO: implement a macro to reduce the amount of repetitive code. The same applies to /// the base_mut function. pub fn base(&self) -> &BaseConnection { match &self { Connection::Ethernet(conn) => &conn.base, Connection::Wireless(conn) => &conn.base, Connection::Loopback(conn) => &conn.base, } } pub fn base_mut(&mut self) -> &mut BaseConnection { match self { Connection::Ethernet(conn) => &mut conn.base, Connection::Wireless(conn) => &mut conn.base, Connection::Loopback(conn) => &mut conn.base, } } pub fn id(&self) -> &str { self.base().id.as_str() } pub fn set_id(&mut self, id: &str) { self.base_mut().id = id.to_string() } pub fn interface(&self) -> &str { self.base().interface.as_str() } pub fn set_interface(&mut self, interface: &str) { self.base_mut().interface = interface.to_string() } pub fn uuid(&self) -> Uuid { self.base().uuid } /// FIXME: rename to ip_config pub fn ip_config(&self) -> &IpConfig { &self.base().ip_config } pub fn ip_config_mut(&mut self) -> &mut IpConfig { &mut self.base_mut().ip_config } pub fn match_config(&self) -> &MatchConfig { &self.base().match_config } pub fn match_config_mut(&mut self) -> &mut MatchConfig { &mut self.base_mut().match_config } pub fn remove(&mut self) { self.base_mut().status = Status::Removed; } pub fn is_removed(&self) -> bool { self.base().status == Status::Removed } /// Determines whether it is a loopback interface. pub fn is_loopback(&self) -> bool { matches!(self, Connection::Loopback(_)) } } #[derive(Debug, Default, Clone)] pub struct BaseConnection { pub id: String, pub uuid: Uuid, pub ip_config: IpConfig, pub status: Status, pub interface: String, pub match_config: MatchConfig, } impl PartialEq for BaseConnection { fn eq(&self, other: &Self) -> bool { self.id == other.id && self.uuid == other.uuid && self.ip_config == other.ip_config } } #[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum Status { #[default] Present, Removed, } #[derive(Default, Debug, PartialEq, Clone)] pub struct IpConfig { pub method4: IpMethod, pub method6: IpMethod, pub addresses: Vec, pub nameservers: Vec, pub gateway4: Option, pub gateway6: Option, } #[derive(Debug, Default, PartialEq, Clone)] pub struct MatchConfig { pub driver: Vec, pub interface: Vec, pub path: Vec, pub kernel: Vec, } #[derive(Debug, Error)] #[error("Unknown IP configuration method name: {0}")] pub struct UnknownIpMethod(String); #[derive(Debug, Default, Copy, Clone, PartialEq)] pub enum IpMethod { #[default] Disabled = 0, Auto = 1, Manual = 2, LinkLocal = 3, } impl fmt::Display for IpMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match &self { IpMethod::Disabled => "disabled", IpMethod::Auto => "auto", IpMethod::Manual => "manual", IpMethod::LinkLocal => "link-local", }; write!(f, "{}", name) } } impl FromStr for IpMethod { type Err = UnknownIpMethod; fn from_str(s: &str) -> Result { match s { "disabled" => Ok(IpMethod::Disabled), "auto" => Ok(IpMethod::Auto), "manual" => Ok(IpMethod::Manual), "link-local" => Ok(IpMethod::LinkLocal), _ => Err(UnknownIpMethod(s.to_string())), } } } impl From for zbus::fdo::Error { fn from(value: UnknownIpMethod) -> zbus::fdo::Error { zbus::fdo::Error::Failed(value.to_string()) } } #[derive(Debug, Default, PartialEq, Clone)] pub struct EthernetConnection { pub base: BaseConnection, } #[derive(Debug, Default, PartialEq, Clone)] pub struct WirelessConnection { pub base: BaseConnection, pub wireless: WirelessConfig, } #[derive(Debug, Default, PartialEq, Clone)] pub struct LoopbackConnection { pub base: BaseConnection, } #[derive(Debug, Default, PartialEq, Clone)] pub struct WirelessConfig { pub mode: WirelessMode, pub ssid: SSID, pub password: Option, pub security: SecurityProtocol, } #[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 { 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 { 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(), )), } } } 07070100000028000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000002700000000agama/agama-dbus-server/src/network/nm07070100000029000081A4000000000000000000000001653660D7000001DE000000000000000000000000000000000000002A00000000agama/agama-dbus-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; 0707010000002A000081A4000000000000000000000001653660D7000007FF000000000000000000000000000000000000003200000000agama/agama-dbus-server/src/network/nm/adapter.rsuse crate::network::{ model::{Connection, NetworkState}, nm::NetworkManagerClient, Adapter, }; use agama_lib::error::ServiceError; 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, 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() } } impl<'a> Adapter for NetworkManagerAdapter<'a> { fn read(&self) -> Result> { async_std::task::block_on(async { let devices = self.client.devices().await?; let connections = self.client.connections().await?; Ok(NetworkState::new(devices, connections)) }) } fn write(&self, network: &NetworkState) -> Result<(), Box> { // By now, traits do not support async functions. Using `task::block_on` allows // to use 'await'. async_std::task::block_on(async { for conn in &network.connections { if !Self::is_writable(conn) { continue; } if conn.is_removed() { if let Err(e) = self.client.remove_connection(conn.uuid()).await { log::error!("Could not remove the connection {}: {}", conn.id(), e); } } else if let Err(e) = self.client.add_or_update_connection(conn).await { log::error!("Could not add/update the connection {}: {}", conn.id(), e); } } }); // FIXME: indicate which connections could not be written. Ok(()) } } 0707010000002B000081A4000000000000000000000001653660D7000013D3000000000000000000000000000000000000003100000000agama/agama-dbus-server/src/network/nm/client.rs//! NetworkManager client. use super::dbus::{connection_from_dbus, connection_to_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, 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, ServiceError> { Ok(Self { nm_proxy: NetworkManagerProxy::new(&connection).await?, connection, }) } /// Returns the list of network devices. pub async fn devices(&self) -> Result, 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, ServiceError> { let proxy = SettingsProxy::new(&self.connection).await?; let paths = proxy.list_connections().await?; let mut connections: Vec = 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?; // TODO: log an error if a connection is not found if let Some(connection) = connection_from_dbus(settings) { connections.push(connection); } } 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) -> Result<(), ServiceError> { let new_conn = connection_to_dbus(conn); 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?; proxy.add_connection(new_conn).await? }; self.activate_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(()) } /// 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(()) } async fn get_connection_proxy(&self, uuid: Uuid) -> Result { 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) } } 0707010000002C000081A4000000000000000000000001653660D700006350000000000000000000000000000000000000002F00000000agama/agama-dbus-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::SSID, }; use cidr::IpInet; use std::{collections::HashMap, net::IpAddr}; use uuid::Uuid; use zbus::zvariant::{self, OwnedValue, Value}; const ETHERNET_KEY: &str = "802-3-ethernet"; const WIRELESS_KEY: &str = "802-11-wireless"; const WIRELESS_SECURITY_KEY: &str = "802-11-wireless-security"; const LOOPBACK_KEY: &str = "loopback"; /// Converts a connection struct into a HashMap that can be sent over D-Bus. /// /// * `conn`: Connection to convert. pub fn connection_to_dbus(conn: &Connection) -> NestedHash { let mut result = NestedHash::new(); let mut connection_dbus = HashMap::from([ ("id", conn.id().into()), ("type", ETHERNET_KEY.into()), ("interface-name", conn.interface().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 let Connection::Wireless(wireless) = conn { connection_dbus.insert("type", "802-11-wireless".into()); let wireless_dbus = wireless_config_to_dbus(wireless); for (k, v) in wireless_dbus { result.insert(k, v); } } 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 { let base = base_connection_from_dbus(&conn)?; if let Some(wireless_config) = wireless_config_from_dbus(&conn) { return Some(Connection::Wireless(WirelessConnection { base, wireless: wireless_config, })); } if conn.get(LOOPBACK_KEY).is_some() { return Some(Connection::Loopback(LoopbackConnection { base })); }; if conn.get(ETHERNET_KEY).is_some() { return Some(Connection::Ethernet(EthernetConnection { base })); }; 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. fn cleanup_dbus_connection(conn: &mut NestedHash) { if let Some(connection) = conn.get_mut("connection") { if connection .get("interface-name") .is_some_and(|v| is_empty_value(&v)) { connection.remove("interface-name"); } } if let Some(ipv4) = conn.get_mut("ipv4") { ipv4.remove("addresses"); ipv4.remove("dns"); } if let Some(ipv6) = conn.get_mut("ipv6") { ipv6.remove("addresses"); ipv6.remove("dns"); } } fn ip_config_to_ipv4_dbus(ip_config: &IpConfig) -> HashMap<&str, zvariant::Value> { let addresses: Vec> = 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::>() .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(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> = 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::>() .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(gateway) = &ip_config.gateway6 { ipv6_dbus.insert("gateway", gateway.to_string().into()); } ipv6_dbus } fn wireless_config_to_dbus(conn: &WirelessConnection) -> NestedHash { let config = &conn.wireless; let wireless: HashMap<&str, zvariant::Value> = HashMap::from([ ("mode", Value::new(config.mode.to_string())), ("ssid", Value::new(config.ssid.to_vec())), ]); 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()); } NestedHash::from([ ("802-11-wireless", wireless), ("802-11-wireless-security", security), ]) } /// 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 .iter() .cloned() .collect::>() .into(); let kernels: Value = match_config .kernel .iter() .cloned() .collect::>() .into(); let paths: Value = match_config.path.iter().cloned().collect::>().into(); let interfaces: Value = match_config .interface .iter() .cloned() .collect::>() .into(); HashMap::from([ ("driver", drivers), ("kernel-command-line", kernels), ("path", paths), ("interface-name", interfaces), ]) } fn base_connection_from_dbus(conn: &OwnedNestedHash) -> Option { 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 = BaseConnection { id: id.to_string(), uuid, ..Default::default() }; if let Some(interface) = connection.get("interface-name") { let interface: &str = interface.downcast_ref()?; base_connection.interface = interface.to_string(); } if let Some(match_config) = conn.get("match") { base_connection.match_config = match_config_from_dbus(match_config)?; } base_connection.ip_config = ip_config_from_dbus(&conn)?; Some(base_connection) } fn match_config_from_dbus( match_config: &HashMap, ) -> Option { let mut match_conf = MatchConfig::default(); if let Some(drivers) = match_config.get("driver") { let drivers = drivers.downcast_ref::()?; 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::()?; 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::()?; 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::()?; 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 { 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(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(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> { let address_data = address_data.downcast_ref::()?; let mut addresses: Vec = vec![]; for addr in address_data.get() { let dict = addr.downcast_ref::()?; let map = >>::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 nameservers_from_dbus(dns_data: &OwnedValue) -> Option> { let dns_data = dns_data.downcast_ref::()?; let mut servers: Vec = 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 { 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 = ssid .get() .iter() .map(|u| *u.downcast_ref::().unwrap()) .collect(); let mut wireless_config = WirelessConfig { mode: NmWirelessMode(mode.to_string()).try_into().ok()?, ssid: SSID(ssid), ..Default::default() }; 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()?; } Some(wireless_config) } /// 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::() { 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::ETHERNET_KEY}; use agama_lib::network::types::SSID; use std::{collections::HashMap, net::IpAddr}; use uuid::Uuid; use zbus::zvariant::{self, 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 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(), ), ]); let address_v6_data = vec![HashMap::from([ ("address".to_string(), Value::new("::ffff:c0a8:10a")), ("prefix".to_string(), Value::new(24_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(), ), ]); 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!( 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::().unwrap(), "::ffff:c0a8:102".parse::().unwrap() ] ); assert_eq!(ip_config.method4, IpMethod::Auto); assert_eq!(ip_config.method6, IpMethod::Auto); } #[test] fn test_connection_from_dbus_missing_connection() { let dbus_conn: HashMap> = 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(), ), ]); let security_section = HashMap::from([("key-mgmt".to_string(), Value::new("wpa-psk").to_owned())]); let dbus_conn = HashMap::from([ ("connection".to_string(), connection_section), ("802-11-wireless".to_string(), wireless_section), ("802-11-wireless-security".to_string(), security_section), ]); let connection = connection_from_dbus(dbus_conn).unwrap(); assert!(matches!(connection, Connection::Wireless(_))); if let Connection::Wireless(connection) = connection { assert_eq!(connection.wireless.ssid, SSID(vec![97, 103, 97, 109, 97])); assert_eq!(connection.wireless.mode, WirelessMode::Infra); assert_eq!(connection.wireless.security, SecurityProtocol::WPA2) } } #[test] fn test_dbus_from_wireless_connection() { let config = WirelessConfig { mode: WirelessMode::Infra, security: SecurityProtocol::WPA2, ssid: SSID(vec![97, 103, 97, 109, 97]), ..Default::default() }; let wireless = WirelessConnection { base: build_base_connection(), wireless: config, ..Default::default() }; let wireless = Connection::Wireless(wireless); let wireless_dbus = connection_to_dbus(&wireless); let wireless = wireless_dbus.get("802-11-wireless").unwrap(); let mode: &str = wireless.get("mode").unwrap().downcast_ref().unwrap(); assert_eq!(mode, "infrastructure"); let ssid: &zvariant::Array = wireless.get("ssid").unwrap().downcast_ref().unwrap(); let ssid: Vec = ssid .get() .iter() .map(|u| *u.downcast_ref::().unwrap()) .collect(); assert_eq!(ssid, "agama".as_bytes()); let security = wireless_dbus.get("802-11-wireless-security").unwrap(); let key_mgmt: &str = security.get("key-mgmt").unwrap().downcast_ref().unwrap(); assert_eq!(key_mgmt, "wpa-psk"); } #[test] fn test_dbus_from_ethernet_connection() { let ethernet = build_ethernet_connection(); let ethernet_dbus = connection_to_dbus(ðernet); 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 base = BaseConnection { id: "agama".to_string(), interface: "eth0".to_string(), ..Default::default() }; let ethernet = EthernetConnection { base, ..Default::default() }; let updated = Connection::Ethernet(ethernet); let updated = connection_to_dbus(&updated); 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()) ); assert_eq!( *ipv4.get("gateway").unwrap(), Value::new("192.168.1.1".to_string()) ); assert!(ipv4.get("addresses").is_none()); let ipv6 = merged.get("ipv6").unwrap(); assert_eq!( *ipv6.get("method").unwrap(), Value::new("disabled".to_string()) ); assert_eq!( *ipv6.get("gateway").unwrap(), Value::new("::ffff:c0a8:101".to_string()) ); } #[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(), ), ]); original.insert("connection".to_string(), connection); let mut updated = Connection::Ethernet(EthernetConnection::default()); updated.set_interface(""); let updated = connection_to_dbus(&updated); let merged = merge_dbus_connections(&original, &updated); let connection = merged.get("connection").unwrap(); assert_eq!(connection.get("interface-name"), None); } fn build_ethernet_section_from_dbus() -> HashMap { HashMap::from([("auto-negotiate".to_string(), true.into())]) } fn build_base_connection() -> BaseConnection { 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()), ..Default::default() }; BaseConnection { id: "agama".to_string(), ip_config, ..Default::default() } } fn build_ethernet_connection() -> Connection { let ethernet = EthernetConnection { base: build_base_connection(), }; Connection::Ethernet(ethernet) } 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 ipv4_dbus = conn_dbus.get("ipv4").unwrap(); let gateway: &str = ipv4_dbus.get("gateway").unwrap().downcast_ref().unwrap(); assert_eq!(gateway, "192.168.0.1"); } } 0707010000002D000081A4000000000000000000000001653660D700000283000000000000000000000000000000000000003000000000agama/agama-dbus-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 for NetworkStateError { fn from(value: NmError) -> NetworkStateError { NetworkStateError::AdapterError(value.to_string()) } } 0707010000002E000081A4000000000000000000000001653660D7000012EF000000000000000000000000000000000000003000000000agama/agama-dbus-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::{IpMethod, SecurityProtocol, WirelessMode}, nm::error::NmError, }; use agama_lib::network::types::DeviceType; use std::fmt; #[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 for WirelessMode { type Error = NmError; fn try_from(value: NmWirelessMode) -> Result { 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 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 for DeviceType { type Error = NmError; fn try_from(value: NmDeviceType) -> Result { match value { NmDeviceType(0) => Ok(DeviceType::Loopback), NmDeviceType(1) => Ok(DeviceType::Ethernet), NmDeviceType(2) => Ok(DeviceType::Wireless), 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 for SecurityProtocol { type Error = NmError; fn try_from(value: NmKeyManagement) -> Result { 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 for IpMethod { type Error = NmError; fn try_from(value: NmMethod) -> Result { match value.as_str() { "auto" => Ok(IpMethod::Auto), "manual" => Ok(IpMethod::Manual), "disabled" => Ok(IpMethod::Disabled), "link-local" => Ok(IpMethod::LinkLocal), _ => 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, pub gateway: Option, pub method: NmMethod, } 0707010000002F000081A4000000000000000000000001653660D70000480F000000000000000000000000000000000000003200000000agama/agama-dbus-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 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; /// 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, )>; /// CheckConnectivity method fn check_connectivity(&self) -> zbus::Result; /// 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; /// CheckpointDestroy method fn checkpoint_destroy(&self, checkpoint: &zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; /// CheckpointRollback method fn checkpoint_rollback( &self, checkpoint: &zbus::zvariant::ObjectPath<'_>, ) -> zbus::Result>; /// 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>; /// GetDeviceByIpIface method fn get_device_by_ip_iface(&self, iface: &str) -> zbus::Result; /// GetDevices method fn get_devices(&self) -> zbus::Result>; /// GetLogging method fn get_logging(&self) -> zbus::Result<(String, String)>; /// GetPermissions method fn get_permissions(&self) -> zbus::Result>; /// 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; /// ActiveConnections property #[dbus_proxy(property)] fn active_connections(&self) -> zbus::Result>; /// AllDevices property #[dbus_proxy(property)] fn all_devices(&self) -> zbus::Result>; /// Capabilities property #[dbus_proxy(property)] fn capabilities(&self) -> zbus::Result>; /// Checkpoints property #[dbus_proxy(property)] fn checkpoints(&self) -> zbus::Result>; /// Connectivity property #[dbus_proxy(property)] fn connectivity(&self) -> zbus::Result; /// ConnectivityCheckAvailable property #[dbus_proxy(property)] fn connectivity_check_available(&self) -> zbus::Result; /// ConnectivityCheckEnabled property #[dbus_proxy(property)] fn connectivity_check_enabled(&self) -> zbus::Result; #[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; /// Devices property #[dbus_proxy(property)] fn devices(&self) -> zbus::Result>; /// GlobalDnsConfiguration property #[dbus_proxy(property)] fn global_dns_configuration( &self, ) -> zbus::Result>; #[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; /// NetworkingEnabled property #[dbus_proxy(property)] fn networking_enabled(&self) -> zbus::Result; /// PrimaryConnection property #[dbus_proxy(property)] fn primary_connection(&self) -> zbus::Result; /// PrimaryConnectionType property #[dbus_proxy(property)] fn primary_connection_type(&self) -> zbus::Result; /// RadioFlags property #[dbus_proxy(property)] fn radio_flags(&self) -> zbus::Result; /// Startup property #[dbus_proxy(property)] fn startup(&self) -> zbus::Result; /// State property #[dbus_proxy(property)] fn state(&self) -> zbus::Result; /// Version property #[dbus_proxy(property)] fn version(&self) -> zbus::Result; /// VersionInfo property #[dbus_proxy(property)] fn version_info(&self) -> zbus::Result>; /// WimaxEnabled property #[dbus_proxy(property)] fn wimax_enabled(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_wimax_enabled(&self, value: bool) -> zbus::Result<()>; /// WimaxHardwareEnabled property #[dbus_proxy(property)] fn wimax_hardware_enabled(&self) -> zbus::Result; /// WirelessEnabled property #[dbus_proxy(property)] fn wireless_enabled(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_wireless_enabled(&self, value: bool) -> zbus::Result<()>; /// WirelessHardwareEnabled property #[dbus_proxy(property)] fn wireless_hardware_enabled(&self) -> zbus::Result; /// WwanEnabled property #[dbus_proxy(property)] fn wwan_enabled(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_wwan_enabled(&self, value: bool) -> zbus::Result<()>; /// WwanHardwareEnabled property #[dbus_proxy(property)] fn wwan_hardware_enabled(&self) -> zbus::Result; } /// # 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<( std::collections::HashMap< String, std::collections::HashMap, >, 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; /// Autoconnect property #[dbus_proxy(property)] fn autoconnect(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_autoconnect(&self, value: bool) -> zbus::Result<()>; /// AvailableConnections property #[dbus_proxy(property)] fn available_connections(&self) -> zbus::Result>; /// Capabilities property #[dbus_proxy(property)] fn capabilities(&self) -> zbus::Result; /// DeviceType property #[dbus_proxy(property)] fn device_type(&self) -> zbus::Result; /// Dhcp4Config property #[dbus_proxy(property)] fn dhcp4_config(&self) -> zbus::Result; /// Dhcp6Config property #[dbus_proxy(property)] fn dhcp6_config(&self) -> zbus::Result; /// Driver property #[dbus_proxy(property)] fn driver(&self) -> zbus::Result; /// DriverVersion property #[dbus_proxy(property)] fn driver_version(&self) -> zbus::Result; /// FirmwareMissing property #[dbus_proxy(property)] fn firmware_missing(&self) -> zbus::Result; /// FirmwareVersion property #[dbus_proxy(property)] fn firmware_version(&self) -> zbus::Result; /// HwAddress property #[dbus_proxy(property)] fn hw_address(&self) -> zbus::Result; /// Interface property #[dbus_proxy(property)] fn interface(&self) -> zbus::Result; /// InterfaceFlags property #[dbus_proxy(property)] fn interface_flags(&self) -> zbus::Result; /// Ip4Address property #[dbus_proxy(property)] fn ip4_address(&self) -> zbus::Result; /// Ip4Config property #[dbus_proxy(property)] fn ip4_config(&self) -> zbus::Result; /// Ip4Connectivity property #[dbus_proxy(property)] fn ip4_connectivity(&self) -> zbus::Result; /// Ip6Config property #[dbus_proxy(property)] fn ip6_config(&self) -> zbus::Result; /// Ip6Connectivity property #[dbus_proxy(property)] fn ip6_connectivity(&self) -> zbus::Result; /// IpInterface property #[dbus_proxy(property)] fn ip_interface(&self) -> zbus::Result; /// LldpNeighbors property #[dbus_proxy(property)] fn lldp_neighbors( &self, ) -> zbus::Result>>; /// Managed property #[dbus_proxy(property)] fn managed(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_managed(&self, value: bool) -> zbus::Result<()>; /// Metered property #[dbus_proxy(property)] fn metered(&self) -> zbus::Result; /// Mtu property #[dbus_proxy(property)] fn mtu(&self) -> zbus::Result; /// NmPluginMissing property #[dbus_proxy(property)] fn nm_plugin_missing(&self) -> zbus::Result; /// Path property #[dbus_proxy(property)] fn path(&self) -> zbus::Result; /// PhysicalPortId property #[dbus_proxy(property)] fn physical_port_id(&self) -> zbus::Result; /// Ports property #[dbus_proxy(property)] fn ports(&self) -> zbus::Result>; /// Real property #[dbus_proxy(property)] fn real(&self) -> zbus::Result; /// State property #[dbus_proxy(property)] fn state(&self) -> zbus::Result; /// StateReason property #[dbus_proxy(property)] fn state_reason(&self) -> zbus::Result<(u32, u32)>; /// Udi property #[dbus_proxy(property)] fn udi(&self) -> zbus::Result; } /// # 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; /// 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, )>; /// AddConnectionUnsaved method fn add_connection_unsaved( &self, connection: std::collections::HashMap< &str, std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, >, ) -> zbus::Result; /// GetConnectionByUuid method fn get_connection_by_uuid(&self, uuid: &str) -> zbus::Result; /// ListConnections method fn list_connections(&self) -> zbus::Result>; /// LoadConnections method fn load_connections(&self, filenames: &[&str]) -> zbus::Result<(bool, Vec)>; /// ReloadConnections method fn reload_connections(&self) -> zbus::Result; /// 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; /// Connections property #[dbus_proxy(property)] fn connections(&self) -> zbus::Result>; /// Hostname property #[dbus_proxy(property)] fn hostname(&self) -> zbus::Result; } /// # 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, >, >; /// GetSettings method fn get_settings( &self, ) -> zbus::Result< std::collections::HashMap< String, std::collections::HashMap, >, >; /// 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>; /// 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; /// Flags property #[dbus_proxy(property)] fn flags(&self) -> zbus::Result; /// Unsaved property #[dbus_proxy(property)] fn unsaved(&self) -> zbus::Result; } 07070100000030000081A4000000000000000000000001653660D700000C6B000000000000000000000000000000000000002E00000000agama/agama-dbus-server/src/network/system.rsuse crate::network::{dbus::Tree, model::Connection, Action, Adapter, NetworkState}; use async_std::channel::{unbounded, Receiver, Sender}; use std::error::Error; /// Represents the network system using holding the state and setting up the D-Bus tree. pub struct NetworkSystem { /// Network state pub state: NetworkState, /// Side of the channel to send actions. actions_tx: Sender, actions_rx: Receiver, tree: Tree, /// Adapter to read/write the network state. adapter: T, } impl NetworkSystem { pub fn new(conn: zbus::Connection, adapter: T) -> Self { let (actions_tx, actions_rx) = unbounded(); let tree = Tree::new(conn, actions_tx.clone()); Self { state: NetworkState::default(), actions_tx, actions_rx, tree, adapter, } } /// Writes the network configuration. pub async fn write(&mut self) -> Result<(), Box> { self.adapter.write(&self.state)?; self.state = self.adapter.read()?; Ok(()) } /// Returns a clone of the [Sender](https://doc.rust-lang.org/std/sync/mpsc/struct.Sender.html) to execute /// [actions](Action). pub fn actions_tx(&self) -> Sender { self.actions_tx.clone() } /// Populates the D-Bus tree with the known devices and connections. pub async fn setup(&mut self) -> Result<(), Box> { self.state = self.adapter.read()?; self.tree .set_connections(&mut self.state.connections) .await?; self.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 Ok(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> { match action { Action::AddConnection(name, ty) => { let mut conn = Connection::new(name, ty); self.tree.add_connection(&mut conn, true).await?; self.state.add_connection(conn)?; } Action::UpdateConnection(conn) => { self.state.update_connection(conn)?; } Action::RemoveConnection(id) => { self.tree.remove_connection(&id).await?; self.state.remove_connection(&id)?; } Action::Apply => { self.write().await?; // 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. self.tree .set_connections(&mut self.state.connections) .await?; } } Ok(()) } } 07070100000031000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000002600000000agama/agama-dbus-server/src/questions07070100000032000081A4000000000000000000000001653660D700002C31000000000000000000000000000000000000002900000000agama/agama-dbus-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 { 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 { 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; /// 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, Option); } /// 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 { Some(question.default_option.clone()) } fn answer_with_password(&self, question: &WithPassword) -> (Option, Option) { (Some(question.base.default_option.clone()), None) } } pub struct Questions { questions: HashMap, connection: Connection, last_id: u32, answer_strategies: Vec>, } #[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, ) -> Result { 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, ) -> Result { 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::(question.clone()) .await?; } QuestionType::BaseWithPassword => { self.connection .object_server() .remove::(question.clone()) .await?; self.connection .object_server() .remove::(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> { 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(()) } 07070100000033000081A4000000000000000000000001653660D7000026BB000000000000000000000000000000000000003100000000agama/agama-dbus-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, pub text: Option, /// A matching GenericQuestion can have other data fields too pub data: Option>, /// 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, } 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, } impl Answers { pub fn new_from_file(path: &str) -> anyhow::Result { 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 { let answer = self.find_answer(question); answer.map(|answer| answer.answer.clone()) } fn answer_with_password( &self, question: &agama_lib::questions::WithPassword, ) -> (Option, Option) { // 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); } } 07070100000034000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001E00000000agama/agama-dbus-server/tests07070100000035000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000002500000000agama/agama-dbus-server/tests/common07070100000036000081A4000000000000000000000001653660D700000DAF000000000000000000000000000000000000002C00000000agama/agama-dbus-server/tests/common/mod.rsuse futures::stream::StreamExt; use std::error::Error; use std::process::{Child, Command}; use std::time::Duration; 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 { 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 DBusServer { 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) -> DBusServer { 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"); self.wait(500).await; let connection = agama_lib::connection_to(&self.address).await.unwrap(); DBusServer { address: self.address, extra: Started { child, connection }, } } } impl DBusServer { /// Waits until the D-Bus daemon is ready. // TODO: implement proper waiting instead of just using a sleep async fn wait(&self, millis: u64) { let wait_time = Duration::from_millis(millis); async_std::task::sleep(wait_time).await; } } impl DBusServer { pub fn connection(&self) -> zbus::Connection { self.extra.connection.clone() } pub async fn request_name(&mut self) -> Result<(), Box> { let connection = self.connection(); let mut stream = NameOwnerChangedStream::for_connection(&connection).await?; let cloned = connection.clone(); async_std::task::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> { 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; } } } } 07070100000037000081A4000000000000000000000001653660D700000FAD000000000000000000000000000000000000002900000000agama/agama-dbus-server/tests/network.rsmod common; use self::common::DBusServer; use agama_dbus_server::network::{ self, model::{self, IpMethod}, Adapter, NetworkService, NetworkState, }; use agama_lib::network::{settings, types::DeviceType, NetworkClient}; use async_std::test; use cidr::IpInet; #[derive(Default)] pub struct NetworkTestAdapter(network::NetworkState); impl Adapter for NetworkTestAdapter { fn read(&self) -> Result> { Ok(self.0.clone()) } fn write(&self, _network: &network::NetworkState) -> Result<(), Box> { unimplemented!("Not used in tests"); } } #[test] async fn test_read_connections() { 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); let _service = NetworkService::start(&server.connection(), adapter) .await .unwrap(); server.request_name().await.unwrap(); let client = NetworkClient::new(server.connection()).await.unwrap(); let conns = client.connections().await.unwrap(); 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); } #[test] async fn test_add_connection() { let mut server = DBusServer::new().start().await; let adapter = NetworkTestAdapter(NetworkState::default()); let _service = NetworkService::start(&server.connection(), adapter) .await .unwrap(); server.request_name().await.unwrap(); let client = NetworkClient::new(server.connection().clone()) .await .unwrap(); let addresses: Vec = vec![ "192.168.0.2/24".parse().unwrap(), "::ffff:c0a8:7ac7/64".parse().unwrap(), ]; let wlan0 = settings::NetworkConnection { id: "wlan0".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.unwrap(); let conns = client.connections().await.unwrap(); assert_eq!(conns.len(), 1); let conn = conns.first().unwrap(); assert_eq!(conn.id, "wlan0"); assert_eq!(conn.device_type(), DeviceType::Wireless); assert_eq!(&conn.addresses, &addresses); let method4 = conn.method4.as_ref().unwrap(); assert_eq!(method4, &IpMethod::Auto.to_string()); let method6 = conn.method6.as_ref().unwrap(); assert_eq!(method6, &IpMethod::Disabled.to_string()); } #[test] async fn test_update_connection() { 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); let _service = NetworkService::start(&server.connection(), adapter) .await .unwrap(); server.request_name().await.unwrap(); let client = NetworkClient::new(server.connection()).await.unwrap(); let mut dbus_eth0 = client.get_connection("eth0").await.unwrap(); dbus_eth0.interface = Some("eth0".to_string()); client.add_or_update_connection(&dbus_eth0).await.unwrap(); let dbus_eth0 = client.get_connection("eth0").await.unwrap(); assert_eq!(dbus_eth0.interface, Some("eth0".to_string())); } 07070100000038000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001300000000agama/agama-derive07070100000039000081A4000000000000000000000001653660D7000000FE000000000000000000000000000000000000001E00000000agama/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" 0707010000003A000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001700000000agama/agama-derive/src0707010000003B000081A4000000000000000000000001653660D70000279E000000000000000000000000000000000000001E00000000agama/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, //! enabled: Option //! } //! //! #[derive(Default, Settings)] //! struct InstallSettings { //! #[settings(nested, alias = "first_user")] //! user: Option, //! reboot: Option //! product: Option, //! #[settings(collection)] //! packages: Vec //! } //! //! ## 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 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, } 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); 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 { &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: &Vec<&SettingField>) -> Vec { nested_fields .iter() .map(|f| { let aliases = f.aliases.clone(); if aliases.is_empty() { quote! {} } else { quote! { #(| #aliases)* } } }) .collect() } 0707010000003C000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001000000000agama/agama-lib0707010000003D000081A4000000000000000000000001653660D70000026B000000000000000000000000000000000000001B00000000agama/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-std = "1.12.0" cidr = { version = "0.2.2", features = ["serde"] } curl = { version = "0.4.44", features = ["protocol-ftp"] } futures = "0.3.27" futures-util = "0.3.27" 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" zbus = "3.7.0" 0707010000003E000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001600000000agama/agama-lib/share0707010000003F000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001F00000000agama/agama-lib/share/examples07070100000040000081A4000000000000000000000001653660D700000325000000000000000000000000000000000000002C00000000agama/agama-lib/share/examples/profile.json{ "localization": { "keyboard": "en_US", "language": "en_US" }, "software": { "product": "ALP-Dolomite" }, "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" ] } ] } } 07070100000041000081A4000000000000000000000001653660D7000006CA000000000000000000000000000000000000002F00000000agama/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' } }, { 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"] } } ] } } 07070100000042000081A4000000000000000000000001653660D700001B33000000000000000000000000000000000000002A00000000agama/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": { "product": { "description": "Product identifier", "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" }, "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" ] } } }, "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" } } }, "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" } } } } } 07070100000043000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001400000000agama/agama-lib/src07070100000044000081A4000000000000000000000001653660D700000124000000000000000000000000000000000000001C00000000agama/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>; 07070100000045000081A4000000000000000000000001653660D7000003FB000000000000000000000000000000000000001D00000000agama/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")] DBus(#[from] zbus::Error), #[error("Could not connect to Agama bus at '{0}'")] DBusConnectionError(String, #[source] zbus::Error), #[error("Unknown product '{0}'. Available products: '{1:?}'")] UnknownProduct(String, Vec), // 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), } #[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), } 07070100000046000081A4000000000000000000000001653660D700000A3F000000000000000000000000000000000000002800000000agama/agama-lib/src/install_settings.rs//! Configuration settings handling //! //! This module implements the mechanisms to load and store the installation settings. use crate::{ network::NetworkSettings, 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, } impl Scope { /// Returns known scopes /// // TODO: we can rely on strum so we do not forget to add them pub fn all() -> [Scope; 4] { [ Scope::Network, Scope::Software, Scope::Storage, Scope::Users, ] } } impl FromStr for Scope { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "users" => Ok(Self::Users), "software" => Ok(Self::Software), "storage" => Ok(Self::Storage), "network" => Ok(Self::Network), _ => 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, #[serde(default)] #[settings(nested)] pub software: Option, #[serde(default)] #[settings(nested)] pub storage: Option, #[serde(default)] #[settings(nested)] pub network: Option, } impl InstallSettings { pub fn defined_scopes(&self) -> Vec { 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); } scopes } } 07070100000047000081A4000000000000000000000001653660D700000819000000000000000000000000000000000000001B00000000agama/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 manager; pub mod network; 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 { connection_to(ADDRESS).await } pub async fn connection_to(address: &str) -> Result { let connection = zbus::ConnectionBuilder::address(address)? .build() .await .map_err(|e| ServiceError::DBusConnectionError(address.to_string(), e))?; Ok(connection) } 07070100000048000081A4000000000000000000000001653660D700000859000000000000000000000000000000000000001F00000000agama/agama-lib/src/manager.rsuse crate::error::ServiceError; use crate::proxies::ServiceStatusProxy; use crate::{ progress::Progress, proxies::{ManagerProxy, ProgressProxy}, }; use futures_util::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> { 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, 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 { Ok(self.manager_proxy.can_install().await?) } pub async fn progress(&self) -> zbus::Result { 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(()) } } 07070100000049000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001C00000000agama/agama-lib/src/network0707010000004A000081A4000000000000000000000001653660D7000000DF000000000000000000000000000000000000001F00000000agama/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; 0707010000004B000081A4000000000000000000000001653660D70000266E000000000000000000000000000000000000002600000000agama/agama-lib/src/network/client.rsuse super::proxies::{ConnectionProxy, ConnectionsProxy, IPProxy, MatchProxy, WirelessProxy}; use super::settings::{MatchSettings, NetworkConnection, WirelessSettings}; use super::types::SSID; use crate::error::ServiceError; use async_std::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>, } impl<'a> NetworkClient<'a> { pub async fn new(connection: Connection) -> Result, ServiceError> { Ok(Self { connections_proxy: ConnectionsProxy::new(&connection).await?, connection, }) } pub async fn get_connection(&self, id: &str) -> Result { let path = self.connections_proxy.get_connection(id).await?; Ok(self.connection_from(path.as_str()).await?) } /// Returns an array of network connections pub async fn connections(&self) -> Result, 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(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 NetworkConnection for the given connection path /// /// * `path`: the connections path to get the config from async fn connection_from(&self, path: &str) -> Result { 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 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, ..Default::default() }) } /// 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 { 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 { 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(&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 { 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(&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?; let interface = conn.interface.as_deref().unwrap_or(""); proxy.set_interface(interface).await?; self.update_ip_settings(path, conn).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 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(()) } } 0707010000004C000081A4000000000000000000000001653660D700001323000000000000000000000000000000000000002700000000agama/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.Connections", default_service = "org.opensuse.Agama1", default_path = "/org/opensuse/Agama1/Network/connections" )] trait Connections { /// Add a new network connection. /// /// `name`: connection name. /// `ty`: connection type. fn add_connection(&self, name: &str, ty: u8) -> zbus::Result<()>; /// Apply method fn apply(&self) -> zbus::Result<()>; /// Gets a connection D-Bus path by its ID /// /// * `id`: connection ID. fn get_connection(&self, id: &str) -> zbus::Result; /// GetConnections method fn get_connections(&self) -> zbus::Result>; /// RemoveConnection method fn remove_connection(&self, uuid: &str) -> zbus::Result<()>; /// ConnectionAdded signal #[dbus_proxy(signal)] fn connection_added(&self, id: &str, path: &str) -> 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; #[dbus_proxy(property)] fn set_mode(&self, value: &str) -> zbus::Result<()>; /// Password property #[dbus_proxy(property)] fn password(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_password(&self, value: &str) -> zbus::Result<()>; /// SSID property #[dbus_proxy(property, name = "SSID")] fn ssid(&self) -> zbus::Result>; #[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; #[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; #[dbus_proxy(property)] fn interface(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_interface(&self, interface: &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>; #[dbus_proxy(property)] fn set_addresses(&self, value: &[&str]) -> zbus::Result<()>; /// Gateway4 property #[dbus_proxy(property)] fn gateway4(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_gateway4(&self, value: &str) -> zbus::Result<()>; /// Gateway6 property #[dbus_proxy(property)] fn gateway6(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_gateway6(&self, value: &str) -> zbus::Result<()>; /// Method4 property #[dbus_proxy(property)] fn method4(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_method4(&self, value: &str) -> zbus::Result<()>; /// Method6 property #[dbus_proxy(property)] fn method6(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_method6(&self, value: &str) -> zbus::Result<()>; /// Nameservers property #[dbus_proxy(property)] fn nameservers(&self) -> zbus::Result>; #[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>; /// Interface property #[dbus_proxy(property)] fn interface(&self) -> zbus::Result>; /// Path property #[dbus_proxy(property)] fn path(&self) -> zbus::Result>; #[dbus_proxy(property)] fn set_path(&self, value: &[&str]) -> zbus::Result<()>; /// Path property #[dbus_proxy(property)] fn kernel(&self) -> zbus::Result>; } 0707010000004D000081A4000000000000000000000001653660D7000014DB000000000000000000000000000000000000002800000000agama/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, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct MatchSettings { #[serde(skip_serializing_if = "Vec::is_empty", default)] pub driver: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub path: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub kernel: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub interface: Vec, } 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, Default, Serialize, Deserialize)] pub struct NetworkConnection { pub id: String, #[serde(skip_serializing_if = "Option::is_none")] pub method4: Option, #[serde(skip_serializing_if = "Option::is_none")] pub gateway4: Option, #[serde(skip_serializing_if = "Option::is_none")] pub method6: Option, #[serde(skip_serializing_if = "Option::is_none")] pub gateway6: Option, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub addresses: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub nameservers: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub wireless: Option, #[serde(skip_serializing_if = "Option::is_none")] pub interface: Option, #[serde(skip_serializing_if = "Option::is_none")] pub match_settings: Option, } 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 { DeviceType::Ethernet } } } impl TryFrom for NetworkConnection { type Error = ConversionError; fn try_from(value: SettingObject) -> Result { 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() }; assert_eq!(wlan.device_type(), DeviceType::Wireless); } #[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())); } } 0707010000004E000081A4000000000000000000000001653660D7000003DF000000000000000000000000000000000000002500000000agama/agama-lib/src/network/store.rsuse 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, ServiceError> { Ok(Self { network_client: NetworkClient::new(connection).await?, }) } // TODO: read the settings from the service pub async fn load(&self) -> Result { let connections = self.network_client.connections().await?; Ok(NetworkSettings { connections }) } pub async fn store(&self, settings: &NetworkSettings) -> Result<(), ServiceError> { for conn in &settings.connections { self.network_client.add_or_update_connection(conn).await?; } self.network_client.apply().await?; Ok(()) } } 0707010000004F000081A4000000000000000000000001653660D70000076F000000000000000000000000000000000000002500000000agama/agama-lib/src/network/types.rsuse serde::{Deserialize, Serialize}; use std::{fmt, str}; use thiserror::Error; use zbus; #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] pub struct SSID(pub Vec); impl SSID { pub fn to_vec(&self) -> &Vec { &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 for Vec { fn from(value: SSID) -> Self { value.0 } } #[derive(Debug, PartialEq, Copy, Clone)] pub enum DeviceType { Loopback = 0, Ethernet = 1, Wireless = 2, } #[derive(Debug, Error, PartialEq)] #[error("Invalid device type: {0}")] pub struct InvalidDeviceType(u8); impl TryFrom for DeviceType { type Error = InvalidDeviceType; fn try_from(value: u8) -> Result { match value { 0 => Ok(DeviceType::Loopback), 1 => Ok(DeviceType::Ethernet), 2 => Ok(DeviceType::Wireless), _ => Err(InvalidDeviceType(value)), } } } impl From 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))); } } 07070100000050000081A4000000000000000000000001653660D700001295000000000000000000000000000000000000001F00000000agama/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::{stdout, Write}, path::Path, process::Command, }; use tempfile::tempdir; /// Downloads a file and writes it to the stdout() /// /// TODO: move this code to a struct /// TODO: add support for YaST-specific URLs /// TODO: do not write to stdout, but to something implementing the Write trait /// TODO: retry the download if it fails pub fn download(url: &str) -> Result<(), ProfileError> { let mut easy = Easy::new(); easy.url(url)?; easy.write_function(|data| { stdout().write_all(data).unwrap(); Ok(data.len()) })?; easy.perform()?; Ok(()) } #[derive(Debug)] pub enum ValidationResult { Valid, NotValid(Vec), } /// 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 { 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.to_str().unwrap()); Self::new(path) } pub fn new(schema_path: &Path) -> Result { let contents = fs::read_to_string(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 { let contents = fs::read_to_string(profile_path)?; self.validate_str(&contents) } pub fn validate_str(&self, profile: &str) -> Result { let contents = serde_json::from_str(profile)?; let result = self.schema.validate(&contents); if let Err(errors) = result { let messages: Vec = errors.map(|e| format!("{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(()) } } 07070100000051000081A4000000000000000000000001653660D70000170F000000000000000000000000000000000000002000000000agama/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_std::task::block_on; //! # use zbus; //! //! // Custom presenter //! struct SimplePresenter {} //! //! impl SimplePresenter { //! fn report_progress(&self, progress: &Progress) { //! println!("{}/{} {}", &progress.current_step, &progress.max_steps, &progress.current_title); //! } //! } //! //! impl ProgressPresenter for SimplePresenter { //! fn start(&mut self, progress: &Progress) { //! println!("Starting..."); //! self.report_progress(progress); //! } //! //! fn update_main(&mut self, progress: &Progress) { //! self.report_progress(progress); //! } //! //! fn update_detail(&mut self, progress: &Progress) { //! self.report_progress(progress); //! } //! //! fn finish(&mut self) { //! println!("Done"); //! } //! } //! //! let connection = block_on(zbus::Connection::system()).unwrap(); //! let mut monitor = block_on(ProgressMonitor::new(connection)).unwrap(); //! monitor.run(SimplePresenter {}); //! ``` use crate::error::ServiceError; use crate::proxies::ProgressProxy; use futures::stream::{SelectAll, StreamExt}; use futures_util::{future::try_join3, Stream}; use zbus::Connection; /// Represents the progress for an Agama service. #[derive(Default, Debug)] 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 { let ((current_step, current_title), max_steps, finished) = try_join3(proxy.current_step(), proxy.total_steps(), proxy.finished()).await?; Ok(Self { current_step, current_title, max_steps, 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, 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); 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(); return Ok(()); } else { presenter.update_main(&progress); } } "/org/opensuse/Agama/Software1" => { presenter.update_detail(&self.detail_progress().await) } _ => eprintln!("Unknown"), }; } Ok(()) } /// Proxy that reports the progress. async fn main_progress(&self) -> Progress { Progress::from_proxy(&self.manager_proxy).await.unwrap() } /// Proxy that reports the progress detail. async fn detail_progress(&self) -> Progress { Progress::from_proxy(&self.software_proxy).await.unwrap() } /// 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) -> SelectAll + '_> { let mut streams = SelectAll::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(); let tagged = stream.map(move |_| path); streams.push(tagged); } streams } } /// Presents the progress to the user. pub trait ProgressPresenter { /// Starts the progress reporting. /// /// * `progress`: current main progress. fn start(&mut self, progress: &Progress); /// Updates the progress. /// /// * `progress`: current progress. fn update_main(&mut self, progress: &Progress); /// Updates the progress detail. /// /// * `progress`: current progress detail. fn update_detail(&mut self, progress: &Progress); /// Finishes the progress reporting. fn finish(&mut self); } 07070100000052000081A4000000000000000000000001653660D700001200000000000000000000000000000000000000001F00000000agama/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; /// TotalSteps property #[dbus_proxy(property)] fn total_steps(&self) -> zbus::Result; } #[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>>; /// Current property #[dbus_proxy(property)] fn current(&self) -> zbus::Result; } #[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; /// CollectLogs method fn collect_logs(&self, user: &str) -> zbus::Result; /// 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>; /// CurrentInstallationPhase property #[dbus_proxy(property)] fn current_installation_phase(&self) -> zbus::Result; /// InstallationPhases property #[dbus_proxy(property)] fn installation_phases( &self, ) -> zbus::Result>>; } #[dbus_proxy(interface = "org.opensuse.Agama1.Locale", assume_defaults = true)] trait Locale { /// Commit method fn commit(&self) -> zbus::Result<()>; /// LabelsForLocales method fn labels_for_locales(&self) -> zbus::Result>; /// ListTimezones method fn list_timezones(&self, locale: &str) -> zbus::Result>; /// ListVConsoleKeyboards method #[dbus_proxy(name = "ListVConsoleKeyboards")] fn list_vconsole_keyboards(&self) -> zbus::Result>; /// Locales property #[dbus_proxy(property)] fn locales(&self) -> zbus::Result>; fn set_locales(&self, value: &[&str]) -> zbus::Result<()>; /// SupportedLocales property #[dbus_proxy(property)] fn supported_locales(&self) -> zbus::Result>; fn set_supported_locales(&self, value: &[&str]) -> zbus::Result<()>; /// Timezone property #[dbus_proxy(property)] fn timezone(&self) -> zbus::Result; fn set_timezone(&self, value: &str) -> zbus::Result<()>; /// VConsoleKeyboard property #[dbus_proxy(property, name = "VConsoleKeyboard")] fn vconsole_keyboard(&self) -> zbus::Result; fn set_vconsole_keyboard(&self, value: &str) -> zbus::Result<()>; } #[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; /// NewWithPassword method fn new_with_password( &self, class: &str, text: &str, options: &[&str], default_option: &str, data: std::collections::HashMap<&str, &str>, ) -> zbus::Result; /// Interactive property #[dbus_proxy(property)] fn interactive(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_interactive(&self, value: bool) -> zbus::Result<()>; } 07070100000053000081A4000000000000000000000001653660D700000C30000000000000000000000000000000000000002100000000agama/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, /// 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, /// Confirmed answer. If empty then not answered yet. pub answer: String, } impl GenericQuestion { pub fn new( id: u32, class: String, text: String, options: Vec, default_option: String, data: HashMap, ) -> 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, /// another_mixin: Option /// } /// ``` /// /// 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, } } } 07070100000054000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001D00000000agama/agama-lib/src/software07070100000055000081A4000000000000000000000001653660D7000000D0000000000000000000000000000000000000002000000000agama/agama-lib/src/software.rs//! Implements support for handling the software settings mod client; mod proxies; mod settings; mod store; pub use client::SoftwareClient; pub use settings::SoftwareSettings; pub use store::SoftwareStore; 07070100000056000081A4000000000000000000000001653660D700000726000000000000000000000000000000000000002700000000agama/agama-lib/src/software/client.rsuse super::proxies::Software1Proxy; use crate::error::ServiceError; use serde::Serialize; use zbus::Connection; /// Represents a software product #[derive(Debug, Serialize)] 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 pub struct SoftwareClient<'a> { software_proxy: Software1Proxy<'a>, } impl<'a> SoftwareClient<'a> { pub async fn new(connection: Connection) -> Result, ServiceError> { Ok(Self { software_proxy: Software1Proxy::new(&connection).await?, }) } /// Returns the available products pub async fn products(&self) -> Result, ServiceError> { let products: Vec = self .software_proxy .available_base_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 selected product to install pub async fn product(&self) -> Result { Ok(self.software_proxy.selected_base_product().await?) } /// Selects the product to install pub async fn select_product(&self, product_id: &str) -> Result<(), ServiceError> { Ok(self.software_proxy.select_product(product_id).await?) } } 07070100000057000081A4000000000000000000000001653660D70000093D000000000000000000000000000000000000002800000000agama/agama-lib/src/software/proxies.rs//! D-Bus interface proxies for: `org.opensuse.Agama.Software1.*` //! //! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.`. use zbus::dbus_proxy; #[dbus_proxy( interface = "org.opensuse.Agama.Software1", default_service = "org.opensuse.Agama.Software1", default_path = "/org/opensuse/Agama/Software1" )] trait Software1 { /// 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; /// Probe method fn probe(&self) -> zbus::Result<()>; /// Propose method fn propose(&self) -> zbus::Result<()>; /// ProvisionsSelected method fn provisions_selected(&self, provisions: &[&str]) -> zbus::Result>; /// SelectProduct method fn select_product(&self, product_id: &str) -> zbus::Result<()>; /// UsedDiskSpace method fn used_disk_space(&self) -> zbus::Result; /// AvailableBaseProducts property #[dbus_proxy(property)] fn available_base_products( &self, ) -> zbus::Result< Vec<( String, String, std::collections::HashMap, )>, >; /// SelectedBaseProduct property #[dbus_proxy(property)] fn selected_base_product(&self) -> zbus::Result; } #[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>; /// 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<()>; } 07070100000058000081A4000000000000000000000001653660D70000017F000000000000000000000000000000000000002900000000agama/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 { /// ID of the product to install (e.g., "ALP", "Tumbleweed", etc.) pub product: Option, } 07070100000059000081A4000000000000000000000001653660D7000005CF000000000000000000000000000000000000002600000000agama/agama-lib/src/software/store.rs//! Implements the store for the storage settings. use super::{SoftwareClient, SoftwareSettings}; use crate::error::ServiceError; use crate::manager::ManagerClient; use zbus::Connection; /// Loads and stores the software settings from/to the D-Bus service. pub struct SoftwareStore<'a> { software_client: SoftwareClient<'a>, manager_client: ManagerClient<'a>, } impl<'a> SoftwareStore<'a> { pub async fn new(connection: Connection) -> Result, ServiceError> { Ok(Self { software_client: SoftwareClient::new(connection.clone()).await?, manager_client: ManagerClient::new(connection).await?, }) } pub async fn load(&self) -> Result { let product = self.software_client.product().await?; Ok(SoftwareSettings { product: Some(product), }) } pub async fn store(&self, settings: &SoftwareSettings) -> Result<(), ServiceError> { if let Some(product) = &settings.product { let products = self.software_client.products().await?; let ids: Vec = products.into_iter().map(|p| p.id).collect(); if ids.contains(product) { self.software_client.select_product(product).await?; self.manager_client.probe().await?; } else { return Err(ServiceError::UnknownProduct(product.clone(), ids)); } } Ok(()) } } 0707010000005A000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001C00000000agama/agama-lib/src/storage0707010000005B000081A4000000000000000000000001653660D7000000CC000000000000000000000000000000000000001F00000000agama/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; 0707010000005C000081A4000000000000000000000001653660D7000012A3000000000000000000000000000000000000002600000000agama/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::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, 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, 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, ServiceError> { let devices: Vec<_> = self .calculator_proxy .available_devices() .await? .into_iter() .map(|path| self.storage_device(path)) .collect(); return 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 { 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, 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, 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, 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(&self, value: Result) -> Result, 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 { 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?) } } 0707010000005D000081A4000000000000000000000001653660D700000F12000000000000000000000000000000000000002700000000agama/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; } #[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; /// DefaultVolume method fn default_volume( &self, mount_path: &str, ) -> zbus::Result>; /// AvailableDevices property #[dbus_proxy(property)] fn available_devices(&self) -> zbus::Result>; /// ProductMountPoints property #[dbus_proxy(property)] fn product_mount_points(&self) -> zbus::Result>; /// Result property #[dbus_proxy(property)] fn result(&self) -> zbus::Result; } #[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>>; /// BootDevice property #[dbus_proxy(property)] fn boot_device(&self) -> zbus::Result; /// EncryptionMethod property #[dbus_proxy(property)] fn encryption_method(&self) -> zbus::Result; /// EncryptionPBKDFunction property #[dbus_proxy(property, name = "EncryptionPBKDFunction")] fn encryption_pbkdfunction(&self) -> zbus::Result; /// EncryptionPassword property #[dbus_proxy(property)] fn encryption_password(&self) -> zbus::Result; /// LVM property #[dbus_proxy(property, name = "LVM")] fn lvm(&self) -> zbus::Result; /// SpacePolicy property #[dbus_proxy(property)] fn space_policy(&self) -> zbus::Result; /// SystemVGDevices property #[dbus_proxy(property, name = "SystemVGDevices")] fn system_vg_devices(&self) -> zbus::Result>>; /// Volumes property #[dbus_proxy(property)] fn volumes( &self, ) -> zbus::Result>>; } #[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; /// Name property #[dbus_proxy(property)] fn name(&self) -> zbus::Result; /// Size property #[dbus_proxy(property)] fn size(&self) -> zbus::Result; /// Systems property #[dbus_proxy(property)] fn systems(&self) -> zbus::Result>; /// UdevIds property #[dbus_proxy(property)] fn udev_ids(&self) -> zbus::Result>; /// UdevPaths property #[dbus_proxy(property)] fn udev_paths(&self) -> zbus::Result>; } 0707010000005E000081A4000000000000000000000001653660D70000021A000000000000000000000000000000000000002800000000agama/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, /// Encryption password for the storage devices (in clear text) pub encryption_password: Option, /// Boot device to use in the installation pub boot_device: Option, } 0707010000005F000081A4000000000000000000000001653660D700000473000000000000000000000000000000000000002500000000agama/agama-lib/src/storage/store.rs//! Implements the store for the storage settings. use super::{StorageClient, StorageSettings}; use crate::error::ServiceError; use std::default::Default; 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, ServiceError> { Ok(Self { storage_client: StorageClient::new(connection).await?, }) } pub async fn load(&self) -> Result { 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, ..Default::default() }) } pub async fn store(&self, settings: &StorageSettings) -> Result<(), ServiceError> { self.storage_client.calculate(settings).await?; Ok(()) } } 07070100000060000081A4000000000000000000000001653660D700000A2A000000000000000000000000000000000000001D00000000agama/agama-lib/src/store.rs//! Load/store the settings from/to the D-Bus services. use crate::error::ServiceError; use crate::install_settings::{InstallSettings, Scope}; use crate::{ network::NetworkStore, 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>, software: SoftwareStore<'a>, storage: StorageStore<'a>, } impl<'a> Store<'a> { pub async fn new(connection: Connection) -> Result, ServiceError> { Ok(Self { users: UsersStore::new(connection.clone()).await?, network: NetworkStore::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>) -> Result { 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?); } // 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?; } 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(()) } } 07070100000061000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001A00000000agama/agama-lib/src/users07070100000062000081A4000000000000000000000001653660D7000000F7000000000000000000000000000000000000001D00000000agama/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; 07070100000063000081A4000000000000000000000001653660D700001022000000000000000000000000000000000000002400000000agama/agama-lib/src/users/client.rs//! Implements a client to access Agama's users service. use super::proxies::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, } impl FirstUser { pub fn from_dbus( dbus_data: zbus::Result<( String, String, String, bool, std::collections::HashMap, )>, ) -> zbus::Result { 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> { 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::from_dbus(self.users_proxy.first_user().await) } /// SetRootPassword method pub async fn set_root_password( &self, value: &str, encrypted: bool, ) -> Result { 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 { 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 { self.users_proxy.root_sshkey().await } /// SetRootSSHKey method pub async fn set_root_sshkey(&self, value: &str) -> Result { 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)> { 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 } } 07070100000064000081A4000000000000000000000001653660D700000663000000000000000000000000000000000000002500000000agama/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; #[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; /// RemoveRootPassword method fn remove_root_password(&self) -> zbus::Result; /// 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)>; /// SetRootPassword method fn set_root_password(&self, value: &str, encrypted: bool) -> zbus::Result; /// SetRootSSHKey method #[dbus_proxy(name = "SetRootSSHKey")] fn set_root_sshkey(&self, value: &str) -> zbus::Result; /// Write method fn write(&self) -> zbus::Result; /// FirstUser property #[dbus_proxy(property)] fn first_user( &self, ) -> zbus::Result<( String, String, String, bool, std::collections::HashMap, )>; /// RootPasswordSet property #[dbus_proxy(property)] fn root_password_set(&self) -> zbus::Result; /// RootSSHKey property #[dbus_proxy(property, name = "RootSSHKey")] fn root_sshkey(&self) -> zbus::Result; } 07070100000065000081A4000000000000000000000001653660D7000009AF000000000000000000000000000000000000002600000000agama/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, #[settings(nested)] pub root: Option, } /// 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, /// First user's username pub user_name: Option, /// First user's password (in clear text) pub password: Option, /// Whether auto-login should enabled or not pub autologin: Option, } /// 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, /// Root SSH public key pub ssh_public_key: Option, } #[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") } } 07070100000066000081A4000000000000000000000001653660D700000A84000000000000000000000000000000000000002300000000agama/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, ServiceError> { Ok(Self { users_client: UsersClient::new(connection).await?, }) } pub async fn load(&self) -> Result { 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(()) } } 07070100000067000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001800000000agama/agama-locale-data07070100000068000081A4000000000000000000000001653660D70000016F000000000000000000000000000000000000002300000000agama/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" 07070100000069000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001C00000000agama/agama-locale-data/src0707010000006A000081A4000000000000000000000001653660D70000102B000000000000000000000000000000000000003400000000agama/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", ]; 0707010000006B000081A4000000000000000000000001653660D700000229000000000000000000000000000000000000002800000000agama/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, } impl Languages { pub fn find_by_id(&self, id: &str) -> Option<&Language> { self.language.iter().find(|t| t.id == id) } } 0707010000006C000081A4000000000000000000000001653660D7000017A8000000000000000000000000000000000000002300000000agama/agama-locale-data/src/lib.rsuse anyhow::Context; use flate2::bufread::GzDecoder; use quick_xml::de::Deserializer; use regex::Regex; use serde::Deserialize; use std::fs::File; use std::io::BufRead; use std::io::BufReader; use std::process::Command; pub mod deprecated_timezones; pub mod language; pub mod localization; pub mod ranked; pub mod territory; pub mod timezone_part; pub mod xkeyboard; fn file_reader(file_path: &str) -> anyhow::Result { 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 { 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 /// let key_maps = agama_locale_data::get_key_maps().unwrap(); /// assert!(key_maps.contains(&"us".to_string())) /// ``` pub fn get_key_maps() -> anyhow::Result> { const BINARY: &str = "/usr/bin/localectl"; let output = Command::new(BINARY) .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 = output.split('\n').map(|l| l.trim().to_string()).collect(); Ok(ret) } /// Parses given locale to language and territory part /// /// /// ## Examples /// /// ``` /// let result = agama_locale_data::parse_locale("en_US.UTF-8").unwrap(); /// assert_eq!(result.0, "en"); /// assert_eq!(result.1, "US") /// ``` pub fn parse_locale(locale: &str) -> anyhow::Result<(&str, &str)> { let locale_regexp: Regex = Regex::new(r"^([[:alpha:]]+)_([[:alpha:]]+)").unwrap(); let captures = locale_regexp .captures(locale) .context("Failed to parse locale")?; Ok(( captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), )) } /// Returns struct which contain list of known languages pub fn get_languages() -> anyhow::Result { 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 { 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 { 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) } /// Gets list of non-deprecated timezones pub fn get_timezones() -> Vec { 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 keyboards"); assert_eq!(first.id, "aa") } #[test] fn test_get_territories() { let result = get_territories().unwrap(); let first = result.territory.first().expect("no keyboards"); 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 keyboards"); 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".to_string()); assert_eq!(asmera, None); let asmara = result.iter().find(|&t| *t == "Africa/Asmara".to_string()); 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.into_iter()).collect(); } } 0707010000006D000081A4000000000000000000000001653660D70000020E000000000000000000000000000000000000002C00000000agama/agama-locale-data/src/localization.rsuse serde::Deserialize; #[derive(Debug, Deserialize)] pub struct Localization { pub name: Vec, } impl Localization { pub fn name_for(&self, language: &str) -> Option { 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, } 0707010000006E000081A4000000000000000000000001653660D7000003CD000000000000000000000000000000000000002600000000agama/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, } #[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, } #[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, } 0707010000006F000081A4000000000000000000000001653660D7000001E6000000000000000000000000000000000000002900000000agama/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, } impl Territories { pub fn find_by_id(&self, id: &str) -> Option<&Territory> { self.territory.iter().find(|t| t.id == id) } } 07070100000070000081A4000000000000000000000001653660D700000915000000000000000000000000000000000000002D00000000agama/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, } impl TimezoneIdParts { /// 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 { let mapping = self.construct_mapping(language); timezones .iter() .map(|tz| self.translate_timezone(&mapping, tz)) .collect() } fn construct_mapping(&self, language: &str) -> HashMap { let mut res: HashMap = 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, timezone: &str) -> String { timezone .split('/') .map(|tzp| { mapping .get(&tzp.to_string()) .unwrap_or_else(|| panic!("Unknown timezone part {tzp}")) .to_owned() }) .collect::>() .join("/") } } 07070100000071000081A4000000000000000000000001653660D700000231000000000000000000000000000000000000002900000000agama/agama-locale-data/src/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, pub languages: RankedLanguages, pub territories: RankedTerritories, } #[derive(Debug, Deserialize)] pub struct XKeyboards { pub keyboard: Vec, } 07070100000072000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001500000000agama/agama-settings07070100000073000081A4000000000000000000000001653660D7000000F6000000000000000000000000000000000000002000000000agama/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" 07070100000074000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001900000000agama/agama-settings/src07070100000075000081A4000000000000000000000001653660D700000301000000000000000000000000000000000000002200000000agama/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), } } } 07070100000076000081A4000000000000000000000001653660D700000969000000000000000000000000000000000000002000000000agama/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, //! enabled: Option //! } //! //! #[derive(Default, Settings)] //! struct InstallSettings { //! #[settings(nested)] //! user: Option, //! reboot: Option //! } //! //! 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; 07070100000077000081A4000000000000000000000001653660D700001158000000000000000000000000000000000000002500000000agama/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); 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> for SettingObject { fn from(value: HashMap) -> SettingObject { let mut hash: HashMap = HashMap::new(); for (k, v) in value { hash.insert(k, SettingValue(v)); } SettingObject(hash) } } impl TryFrom for bool { type Error = ConversionError; fn try_from(value: SettingValue) -> Result { 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 for Option { type Error = ConversionError; fn try_from(value: SettingValue) -> Result { Ok(Some(value.try_into()?)) } } impl TryFrom for String { type Error = ConversionError; fn try_from(value: SettingValue) -> Result { Ok(value.0) } } impl TryFrom for Option { type Error = ConversionError; fn try_from(value: SettingValue) -> Result { 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 = 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"); } } 07070100000078000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000001B00000000agama/agama-settings/tests07070100000079000081A4000000000000000000000001653660D700000A6E000000000000000000000000000000000000002700000000agama/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, #[settings(collection)] patterns: Vec, #[settings(nested)] network: Option, } /// Software patterns #[derive(Debug, Clone)] pub struct Pattern { id: String, } #[derive(Default, Debug, Settings)] pub struct Network { enabled: Option, ssid: Option, } /// TODO: deriving this trait could be easy. impl TryFrom for Pattern { type Error = ConversionError; fn try_from(value: SettingObject) -> Result { 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); } 0707010000007A000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000000E00000000agama/package0707010000007B000081A4000000000000000000000001653660D700000439000000000000000000000000000000000000001700000000agama/package/_service https://github.com/openSUSE/agama.git @PARENT_TAG@+@TAG_OFFSET@ v(.*) git master rust enable package/agama-cli.changes package/agama-cli.spec agama/rust zst true agama/rust agama.obsinfo agama agama 0707010000007C000081A4000000000000000000000001653660D700002062000000000000000000000000000000000000002000000000agama/package/agama-cli.changes------------------------------------------------------------------- Mon Oct 23 11:33:40 UTC 2023 - Imobach Gonzalez Sosa - Version 5 ------------------------------------------------------------------- Mon Oct 10 07:37:00 UTC 2023 - Michal Filka - 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 - Version 4 ------------------------------------------------------------------- Tue Sep 26 12:05:52 UTC 2023 - Imobach Gonzalez Sosa - Wait until the manager is ready before probing (gh#openSUSE/agama#771). ------------------------------------------------------------------- Mon Sep 25 11:32:53 UTC 2023 - Imobach Gonzalez Sosa - Add support for IPv6 network settings (gh#openSUSE/agama#761). ------------------------------------------------------------------- Mon Sep 25 10:46:53 UTC 2023 - Michal Filka - 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 - 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 - Improve questions CLI help text (gh#openSUSE/agama#754) ------------------------------------------------------------------- Thu Sep 14 10:10:37 UTC 2023 - Imobach Gonzalez Sosa - 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 - 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 - 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 - Locale service: add value for UI locale (gh#openSUSE/agama#725) ------------------------------------------------------------------- Thu Aug 3 08:34:14 UTC 2023 - Imobach Gonzalez Sosa - 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 - Version 3 ------------------------------------------------------------------- Wed Jul 26 11:08:09 UTC 2023 - Josef Reidinger - 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 - 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 - 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 - 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 - 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 - 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 - Improve the progress reporting (gh#openSUSE/agama#653). ------------------------------------------------------------------- Thu Jul 6 09:13:47 UTC 2023 - Imobach Gonzalez Sosa - 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 - 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 - 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 - Version 2.1 ------------------------------------------------------------------- Mon May 22 12:29:20 UTC 2023 - Martin Vidner - Version 2 ------------------------------------------------------------------- Thu May 11 11:00:11 UTC 2023 - Imobach Gonzalez Sosa - 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 - 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 - Add support for setting root authentication mechanisms (gh#yast/agama-cli#47). ------------------------------------------------------------------- Tue Mar 21 16:06:02 UTC 2023 - Martin Vidner - 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 - Use JSON as the default format (gh#yast/agama-cli#46). ------------------------------------------------------------------- Tue Mar 21 08:55:39 UTC 2023 - Josef Reidinger - Fix the path of the JSON schema (gh#yast/agama-cli#44). ------------------------------------------------------------------- Thu Mar 16 11:56:42 UTC 2023 - Imobach Gonzalez Sosa - First version of the package: * Querying and setting simple values. * Adding elements to collections * Handling of auto-installation profiles. * Basic error handling - 0.1 0707010000007D000081A4000000000000000000000001653660D700000C87000000000000000000000000000000000000001D00000000agama/package/agama-cli.spec# # spec file for package agama-cli # # Copyright (c) 2023 SUSE LINUX GmbH, Nuernberg, Germany. # # 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-cli # This will be set by osc services, that will run after this. Version: 0 Release: 0 Summary: Agama command line interface # 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 # Generated by the cargo_vendor OBS service Source2: cargo_config BuildRequires: cargo-packaging BuildRequires: pkgconfig(openssl) # used in tests for dbus service BuildRequires: python-langtable-data BuildRequires: dbus-1-common # required by agama-dbus-server integration tests BuildRequires: dbus-1-daemon Requires: jsonnet Requires: lshw # required by "agama logs store" Requires: bzip2 Requires: tar %description Command line program to interact with the agama service. %package -n agama-dbus-server # This will be set by osc services, that will run after this. Version: 0 Release: 0 Summary: Agama Rust D-Bus service License: GPL-2.0-only Url: https://github.com/opensuse/agama Requires: python-langtable-data Requires: dbus-1-common %description -n agama-dbus-server DBus service for agama project. It provides so far localization service. %prep %autosetup -a1 -n agama mkdir .cargo cp %{SOURCE2} .cargo/config # 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 -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 %{cargo_test} %files %{_bindir}/agama %dir %{_datadir}/agama-cli %{_datadir}/agama-cli/profile.schema.json %files -n agama-dbus-server %{_bindir}/agama-dbus-server %{_datadir}/dbus-1/agama-services %changelog 0707010000007E000041ED000000000000000000000002653660D700000000000000000000000000000000000000000000000C00000000agama/share0707010000007F000081A4000000000000000000000001653660D700000E19000000000000000000000000000000000000001B00000000agama/share/dbus-test.conf org.opensuse.Agama unix:tmpdir=/tmp EXTERNAL contexts/dbus_contexts 1000000000 250000000 1000000000 250000000 1000000000 600000 240000 150000 100000 10000 100000 10000 50000 50000 50000 07070100000080000081A4000000000000000000000001653660D700000053000000000000000000000000000000000000002800000000agama/share/org.opensuse.Agama1.service[D-BUS Service] Name=org.opensuse.Agama1 Exec=/usr/bin/agama-dbus-server User=root 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!867 blocks