diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.IPv4.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.IPv4.bus.xml
new file mode 120000
index 0000000000..7b1c091d22
--- /dev/null
+++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.IPv4.bus.xml
@@ -0,0 +1 @@
+org.opensuse.Agama.Network1.Connection.bus.xml
\ No newline at end of file
diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.Wireless.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.Wireless.bus.xml
new file mode 120000
index 0000000000..7b1c091d22
--- /dev/null
+++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.Wireless.bus.xml
@@ -0,0 +1 @@
+org.opensuse.Agama.Network1.Connection.bus.xml
\ No newline at end of file
diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.bus.xml
new file mode 100644
index 0000000000..91545a74fe
--- /dev/null
+++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.bus.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Connections.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Connections.bus.xml
new file mode 100644
index 0000000000..1987d9850c
--- /dev/null
+++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Connections.bus.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Device.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Device.bus.xml
new file mode 100644
index 0000000000..2d7d5f9708
--- /dev/null
+++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Device.bus.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Devices.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Devices.bus.xml
new file mode 100644
index 0000000000..eff3ad0179
--- /dev/null
+++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Devices.bus.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/org.opensuse.Agama.Network1.Connection.IPv4.doc.xml b/doc/dbus/org.opensuse.Agama.Network1.Connection.IPv4.doc.xml
new file mode 100644
index 0000000000..c7d36731e2
--- /dev/null
+++ b/doc/dbus/org.opensuse.Agama.Network1.Connection.IPv4.doc.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/org.opensuse.Agama.Network1.Connection.Wireless.doc.xml b/doc/dbus/org.opensuse.Agama.Network1.Connection.Wireless.doc.xml
new file mode 100644
index 0000000000..bad0fb7f53
--- /dev/null
+++ b/doc/dbus/org.opensuse.Agama.Network1.Connection.Wireless.doc.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/org.opensuse.Agama.Network1.Connection.doc.xml b/doc/dbus/org.opensuse.Agama.Network1.Connection.doc.xml
new file mode 100644
index 0000000000..20a7bf0f71
--- /dev/null
+++ b/doc/dbus/org.opensuse.Agama.Network1.Connection.doc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/org.opensuse.Agama.Network1.Connections.doc.xml b/doc/dbus/org.opensuse.Agama.Network1.Connections.doc.xml
new file mode 100644
index 0000000000..3ea5d4387f
--- /dev/null
+++ b/doc/dbus/org.opensuse.Agama.Network1.Connections.doc.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/org.opensuse.Agama.Network1.Device.doc.xml b/doc/dbus/org.opensuse.Agama.Network1.Device.doc.xml
new file mode 100644
index 0000000000..93fd13a157
--- /dev/null
+++ b/doc/dbus/org.opensuse.Agama.Network1.Device.doc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/org.opensuse.Agama.Network1.Devices.doc.xml b/doc/dbus/org.opensuse.Agama.Network1.Devices.doc.xml
new file mode 100644
index 0000000000..d11df5f3cc
--- /dev/null
+++ b/doc/dbus/org.opensuse.Agama.Network1.Devices.doc.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/network_support_design.md b/doc/network_support_design.md
index 09d3cb8a76..aac39ecfab 100644
--- a/doc/network_support_design.md
+++ b/doc/network_support_design.md
@@ -52,18 +52,19 @@ Based on the situation described above, we considered these approaches:
2. Keep the status quo, extend the web UI and rely on `nmcli` for the CLI-based installation. For
automation, we could rely on third-party tool.
-Although it might be harder, option 1 looks more consistent: you just need Agama D-Bus interface to
-perform an installation.
+Although it might be harder, option 1 looks more consistent: you can install your system just using
+Agama D-Bus interface.
## Adding our own D-Bus interface
Adding a D-Bus interface does not mean that we need to implement a full solution to set up the
-network. Actually, we can leverage some third-party tool to do the hard work. The idea is to build a
-good enough interface to support our use cases.
+network. The idea is to build a good enough API to support our use cases.
-### Why not YaST2?
+Initially, we though about adding the D-Bus API on top of [nmstate](https://nmstate.io/), although
+it covers a different use-case. After playing a bit with this idea, we decided to come up with a
+solution more aligned with our needs.
-You might be wondering, why not use YaST2 itself? Let's see some reasons:
+As an alternative, you might be wondering why not use YaST2 itself? Let's see some reasons:
* It does not implement support for reading the NetworkManager configuration.
* It is not able to talk to NetworkManager D-Bus interface. It configures NetworkManager by writing
@@ -71,32 +72,21 @@ You might be wondering, why not use YaST2 itself? Let's see some reasons:
* It is Ruby-based, so we might consider a Rust-based solution. It is not a language problem, but we
would like to reduce the memory consumption.
-### The proposal
+## The proposal
-Our proposal is to build a D-Bus service that wraps around a third-party tool that takes care of the
-hard part. We considered [Netplan](https://netplan.io/) and [nmstate](https://nmstate.io/), although
-we decided to use the latter (see [Third-party tools](#third-party-tools)).
+Agama's network service is responsible for holding the network configuration for the installer. It
+should be agnostic from the used network service, although in the short-term it will support
+NetworkManager only. Therefore, the current solution is influenced by NetworkManager itself.
-Unfortunately, those tools are missing support for some cases (e.g., wireless configuration or udev
-handling), but we can add such a support in our wrapper.
+In a first version, the API is composed of the following objects:
-## Third-party tools
+* Network devices. Each one is available as an object under `/org/opensuse/Agama/Network1/devices/*`
+ exposing the current status[^1].
+* Connections (or configurations). They are exposed as `/org/opensuse/Agama/Network1/connections/*`.
+ Depending on the type of connection, those objects implements different interfaces like
+ `org.opensuse.Agama.Network1.IPv4`, `org.opensuse.Agama.Network1.Wireless`, etc.
-Both [Netplan](https://netplan.io/) and [nmstate](https://nmstate.io/) are tools that are able to
-set up NetworkManager to meet a given configuration. Although we decided on `nmstate`, let's
-have a look to some of their characteristics:
+This API could be expanded in the future to provide a list of access points, emit signals when the
+configuration changes, provide more information about the network devices, etc.
-### nmstate
-
-- Based on devices/interfaces (the concept of connection does not exist).
-- Designed to support multiple network providers, but at this point only NetworkManager is
- supported.
-- Offered as a Rust library.
-- Bindings for several languages and a CLI.
-- Support for WiFi and VPNs is missing.
-
-### Netplan
-
-- Written in Python. That's the main reason to discard this option.
-- Designed to support multiple network providers, nowadays it supports NetworkManager and networkd.
-- It offers a rather limited D-Bus API.
+[^1]: By now it only exposes some basic data, as the current status is only needed by the web UI.
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index eecc69875a..e2b0aab33e 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -27,7 +27,9 @@ dependencies = [
name = "agama-dbus-server"
version = "0.1.0"
dependencies = [
+ "agama-lib",
"agama-locale-data",
+ "agama-network",
"anyhow",
"async-std",
"zbus",
@@ -40,7 +42,7 @@ version = "1.0.0"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
@@ -73,6 +75,18 @@ dependencies = [
"serde",
]
+[[package]]
+name = "agama-network"
+version = "0.1.0"
+dependencies = [
+ "agama-lib",
+ "async-std",
+ "parking_lot",
+ "thiserror",
+ "uuid",
+ "zbus",
+]
+
[[package]]
name = "ahash"
version = "0.8.3"
@@ -88,18 +102,67 @@ dependencies = [
[[package]]
name = "aho-corasick"
-version = "0.7.20"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+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.69"
+version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "async-attributes"
@@ -108,7 +171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
dependencies = [
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
@@ -134,9 +197,9 @@ dependencies = [
[[package]]
name = "async-executor"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b"
+checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb"
dependencies = [
"async-lock",
"async-task",
@@ -175,22 +238,22 @@ dependencies = [
[[package]]
name = "async-io"
-version = "1.12.0"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
dependencies = [
"async-lock",
"autocfg",
+ "cfg-if",
"concurrent-queue",
"futures-lite",
- "libc",
"log",
"parking",
"polling",
+ "rustix",
"slab",
"socket2",
"waker-fn",
- "windows-sys 0.42.0",
]
[[package]]
@@ -202,15 +265,33 @@ 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.2"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b015a331cc64ebd1774ba119538573603427eaace0a1950c423ab971f903796"
+checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
]
[[package]]
@@ -242,26 +323,26 @@ dependencies = [
[[package]]
name = "async-task"
-version = "4.3.0"
+version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524"
+checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae"
[[package]]
name = "async-trait"
-version = "0.1.66"
+version = "0.1.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc"
+checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
]
[[package]]
name = "atomic-waker"
-version = "1.1.0"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599"
+checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
[[package]]
name = "autocfg"
@@ -307,9 +388,9 @@ dependencies = [
[[package]]
name = "blocking"
-version = "1.3.0"
+version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8"
+checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65"
dependencies = [
"async-channel",
"async-lock",
@@ -317,13 +398,14 @@ dependencies = [
"atomic-waker",
"fastrand",
"futures-lite",
+ "log",
]
[[package]]
name = "bumpalo"
-version = "3.12.0"
+version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "bytecount"
@@ -383,61 +465,72 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.1.8"
+version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5"
+checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc"
dependencies = [
- "bitflags",
+ "clap_builder",
"clap_derive",
- "clap_lex",
- "is-terminal",
"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",
+ "clap_lex",
"strsim",
- "termcolor",
]
[[package]]
name = "clap_derive"
-version = "4.1.8"
+version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0"
+checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b"
dependencies = [
"heck",
- "proc-macro-error",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
]
[[package]]
name = "clap_lex"
-version = "0.3.2"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09"
-dependencies = [
- "os_str_bytes",
-]
+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.1.0"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e"
+checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console"
-version = "0.15.5"
+version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60"
+checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"unicode-width",
- "windows-sys 0.42.0",
+ "windows-sys 0.45.0",
]
[[package]]
@@ -451,9 +544,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
-version = "0.2.5"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
dependencies = [
"libc",
]
@@ -493,7 +586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
@@ -513,9 +606,9 @@ dependencies = [
[[package]]
name = "curl-sys"
-version = "0.4.60+curl-7.88.1"
+version = "0.4.62+curl-8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "717abe2cb465a5da6ce06617388a3980c9a2844196734bec8ccb8e575250f13f"
+checksum = "274ef7ef7c1113c7611af49ce248a700afa1171045a1aaa40137030773f993b8"
dependencies = [
"cc",
"libc",
@@ -534,39 +627,19 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
]
[[package]]
name = "digest"
-version = "0.10.6"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
-[[package]]
-name = "dirs"
-version = "4.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
-dependencies = [
- "dirs-sys",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
-dependencies = [
- "libc",
- "redox_users",
- "winapi",
-]
-
[[package]]
name = "encode_unicode"
version = "0.3.6"
@@ -575,9 +648,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "enumflags2"
-version = "0.7.5"
+version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb"
+checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2"
dependencies = [
"enumflags2_derive",
"serde",
@@ -585,24 +658,24 @@ dependencies = [
[[package]]
name = "enumflags2_derive"
-version = "0.7.4"
+version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae"
+checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
]
[[package]]
name = "errno"
-version = "0.2.8"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
dependencies = [
"errno-dragonfly",
"libc",
- "winapi",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -642,9 +715,9 @@ dependencies = [
[[package]]
name = "flate2"
-version = "1.0.25"
+version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
+checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -671,9 +744,9 @@ dependencies = [
[[package]]
name = "futures"
-version = "0.3.27"
+version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549"
+checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
dependencies = [
"futures-channel",
"futures-core",
@@ -686,9 +759,9 @@ dependencies = [
[[package]]
name = "futures-channel"
-version = "0.3.27"
+version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
dependencies = [
"futures-core",
"futures-sink",
@@ -696,15 +769,15 @@ dependencies = [
[[package]]
name = "futures-core"
-version = "0.3.27"
+version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd"
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
[[package]]
name = "futures-executor"
-version = "0.3.27"
+version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83"
+checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
dependencies = [
"futures-core",
"futures-task",
@@ -713,15 +786,15 @@ dependencies = [
[[package]]
name = "futures-io"
-version = "0.3.27"
+version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
[[package]]
name = "futures-lite"
-version = "1.12.0"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
"fastrand",
"futures-core",
@@ -734,32 +807,32 @@ dependencies = [
[[package]]
name = "futures-macro"
-version = "0.3.27"
+version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
]
[[package]]
name = "futures-sink"
-version = "0.3.27"
+version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2"
+checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
[[package]]
name = "futures-task"
-version = "0.3.27"
+version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
[[package]]
name = "futures-util"
-version = "0.3.27"
+version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
dependencies = [
"futures-channel",
"futures-core",
@@ -775,9 +848,9 @@ dependencies = [
[[package]]
name = "generic-array"
-version = "0.14.6"
+version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
@@ -785,9 +858,9 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.2.8"
+version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
"cfg-if",
"libc",
@@ -842,9 +915,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "1.9.2"
+version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
@@ -858,7 +931,7 @@ checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729"
dependencies = [
"console",
"number_prefix",
- "portable-atomic",
+ "portable-atomic 0.3.20",
"unicode-width",
]
@@ -873,24 +946,25 @@ dependencies = [
[[package]]
name = "io-lifetimes"
-version = "1.0.6"
+version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
+ "hermit-abi",
"libc",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
name = "is-terminal"
-version = "0.4.4"
+version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857"
+checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi",
"io-lifetimes",
"rustix",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -910,9 +984,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "js-sys"
-version = "0.3.61"
+version = "0.3.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790"
dependencies = [
"wasm-bindgen",
]
@@ -961,15 +1035,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.140"
+version = "0.2.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
+checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
[[package]]
name = "libz-sys"
-version = "1.1.8"
+version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
+checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db"
dependencies = [
"cc",
"libc",
@@ -979,9 +1053,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
-version = "0.1.4"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "lock_api"
@@ -1026,9 +1100,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
-version = "0.6.2"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
]
@@ -1043,7 +1117,6 @@ dependencies = [
"cfg-if",
"libc",
"memoffset",
- "pin-utils",
"static_assertions",
]
@@ -1159,11 +1232,10 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
-version = "0.9.80"
+version = "0.9.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
+checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e"
dependencies = [
- "autocfg",
"cc",
"libc",
"pkg-config",
@@ -1180,17 +1252,11 @@ dependencies = [
"pin-project-lite",
]
-[[package]]
-name = "os_str_bytes"
-version = "6.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
-
[[package]]
name = "parking"
-version = "2.0.0"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
+checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e"
[[package]]
name = "parking_lot"
@@ -1210,7 +1276,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall",
+ "redox_syscall 0.2.16",
"smallvec",
"windows-sys 0.45.0",
]
@@ -1282,15 +1348,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
-version = "0.3.26"
+version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "polling"
-version = "2.6.0"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e1f879b2998099c2d69ab9605d145d5b661195627eccc680002c4918a7fb6fa"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
dependencies = [
"autocfg",
"bitflags",
@@ -1299,14 +1365,23 @@ dependencies = [
"libc",
"log",
"pin-project-lite",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
name = "portable-atomic"
-version = "0.3.19"
+version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b"
+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"
@@ -1324,35 +1399,11 @@ dependencies = [
"toml_edit",
]
-[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
-dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
-]
-
[[package]]
name = "proc-macro2"
-version = "1.0.51"
+version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
+checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8"
dependencies = [
"unicode-ident",
]
@@ -1369,9 +1420,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.23"
+version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500"
dependencies = [
"proc-macro2",
]
@@ -1416,21 +1467,19 @@ dependencies = [
]
[[package]]
-name = "redox_users"
-version = "0.4.3"
+name = "redox_syscall"
+version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
- "getrandom",
- "redox_syscall",
- "thiserror",
+ "bitflags",
]
[[package]]
name = "regex"
-version = "1.7.1"
+version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
+checksum = "d1a59b5d8e97dee33696bf13c5ba8ab85341c002922fba050069326b9c498974"
dependencies = [
"aho-corasick",
"memchr",
@@ -1439,22 +1488,22 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.6.28"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
[[package]]
name = "rustix"
-version = "0.36.9"
+version = "0.37.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc"
+checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
- "windows-sys 0.45.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -1480,29 +1529,29 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
-version = "1.0.154"
+version = "1.0.163"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e"
+checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.154"
+version = "1.0.163"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217"
+checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
]
[[package]]
name = "serde_json"
-version = "1.0.94"
+version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
+checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
dependencies = [
"itoa",
"ryu",
@@ -1511,20 +1560,20 @@ dependencies = [
[[package]]
name = "serde_repr"
-version = "0.1.11"
+version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "395627de918015623b32e7669714206363a7fc00382bf477e72c1f7533e8eafc"
+checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
]
[[package]]
name = "serde_yaml"
-version = "0.9.19"
+version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10"
+checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c"
dependencies = [
"indexmap",
"itoa",
@@ -1544,6 +1593,25 @@ dependencies = [
"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 = "siphasher"
version = "0.3.10"
@@ -1599,52 +1667,54 @@ dependencies = [
]
[[package]]
-name = "tempfile"
-version = "3.4.0"
+name = "syn"
+version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95"
+checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01"
dependencies = [
- "cfg-if",
- "fastrand",
- "redox_syscall",
- "rustix",
- "windows-sys 0.42.0",
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
]
[[package]]
-name = "termcolor"
-version = "1.2.0"
+name = "tempfile"
+version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
dependencies = [
- "winapi-util",
+ "cfg-if",
+ "fastrand",
+ "redox_syscall 0.3.5",
+ "rustix",
+ "windows-sys 0.45.0",
]
[[package]]
name = "thiserror"
-version = "1.0.39"
+version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c"
+checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.39"
+version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e"
+checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
]
[[package]]
name = "time"
-version = "0.3.20"
+version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
+checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc"
dependencies = [
"serde",
"time-core",
@@ -1653,15 +1723,15 @@ dependencies = [
[[package]]
name = "time-core"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
-version = "0.2.8"
+version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
+checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
dependencies = [
"time-core",
]
@@ -1683,15 +1753,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml_datetime"
-version = "0.6.1"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
+checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
[[package]]
name = "toml_edit"
-version = "0.19.4"
+version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825"
+checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
dependencies = [
"indexmap",
"toml_datetime",
@@ -1712,20 +1782,20 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.23"
+version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
+checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
]
[[package]]
name = "tracing-core"
-version = "0.1.30"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
dependencies = [
"once_cell",
]
@@ -1748,15 +1818,15 @@ dependencies = [
[[package]]
name = "unicode-bidi"
-version = "0.3.11"
+version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c"
+checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
-version = "1.0.8"
+version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
[[package]]
name = "unicode-normalization"
@@ -1781,9 +1851,9 @@ checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unsafe-libyaml"
-version = "0.2.7"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c"
+checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6"
[[package]]
name = "url"
@@ -1796,11 +1866,20 @@ dependencies = [
"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.0"
+version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
+checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
+dependencies = [
+ "getrandom",
+]
[[package]]
name = "value-bag"
@@ -1838,9 +1917,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
-version = "0.2.84"
+version = "0.2.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -1848,24 +1927,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.84"
+version = "0.2.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.34"
+version = "0.4.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
+checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e"
dependencies = [
"cfg-if",
"js-sys",
@@ -1875,9 +1954,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.84"
+version = "0.2.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1885,28 +1964,28 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.84"
+version = "0.2.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.16",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.84"
+version = "0.2.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
[[package]]
name = "web-sys"
-version = "0.3.61"
+version = "0.3.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -1928,15 +2007,6 @@ 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"
@@ -1949,13 +2019,13 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "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]]
@@ -1964,92 +2034,168 @@ version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
- "windows-targets",
+ "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.42.1"
+version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "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.1"
+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 = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.42.1"
+version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
-version = "0.42.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+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.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+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.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+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.42.1"
+version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.42.1"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
+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.3.5"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee7b2c67f962bf5042bfd8b6a916178df33a26eec343ae064cb8e069f638fa6f"
+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",
+ "winapi",
+]
+
[[package]]
name = "zbus"
-version = "3.11.0"
+version = "3.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20aae5dd5b051971cd2f49f9f3b860e57b2b495ba5ba254eaec42d34ede57e97"
+checksum = "6c3d77c9966c28321f1907f0b6c5a5561189d1f7311eea6d94180c6be9daab29"
dependencies = [
"async-broadcast",
"async-executor",
"async-fs",
"async-io",
"async-lock",
+ "async-process",
"async-recursion",
"async-task",
"async-trait",
"byteorder",
"derivative",
- "dirs",
"enumflags2",
"event-listener",
"futures-core",
@@ -2067,6 +2213,7 @@ dependencies = [
"tracing",
"uds_windows",
"winapi",
+ "xdg-home",
"zbus_macros",
"zbus_names",
"zvariant",
@@ -2074,23 +2221,24 @@ dependencies = [
[[package]]
name = "zbus_macros"
-version = "3.11.0"
+version = "3.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9264b3a1bcf5503d4e0348b6e7efe1da58d4f92a913c15ed9e63b52de85faaa1"
+checksum = "f6e341d12edaff644e539ccbbf7f161601294c9a84ed3d7e015da33155b435af"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
- "syn",
+ "syn 1.0.109",
+ "winnow",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
-version = "2.5.0"
+version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3"
+checksum = "82441e6033be0a741157a72951a3e4957d519698f3a824439cc131c5ba77ac2a"
dependencies = [
"serde",
"static_assertions",
@@ -2099,9 +2247,9 @@ dependencies = [
[[package]]
name = "zvariant"
-version = "3.12.0"
+version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46fe4914a985446d6fd287019b5fceccce38303d71407d9e6e711d44954a05d8"
+checksum = "622cc473f10cef1b0d73b7b34a266be30ebdcfaea40ec297dd8cbda088f9f93c"
dependencies = [
"byteorder",
"enumflags2",
@@ -2113,24 +2261,24 @@ dependencies = [
[[package]]
name = "zvariant_derive"
-version = "3.12.0"
+version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34c20260af4b28b3275d6676c7e2a6be0d4332e8e0aba4616d34007fd84e462a"
+checksum = "5d9c1b57352c25b778257c661f3c4744b7cefb7fc09dd46909a153cce7773da2"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
-version = "1.0.0"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53b22993dbc4d128a17a3b6c92f1c63872dd67198537ee728d8b5d7c40640a8b"
+checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 1.0.109",
]
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 4143977ed6..636ad6328e 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -1,8 +1,9 @@
[workspace]
members = [
- "agama-lib",
"agama-cli",
+ "agama-dbus-server",
"agama-derive",
+ "agama-lib",
"agama-locale-data",
- "agama-dbus-server"
+ "agama-network"
]
diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml
index 6a38539f7c..b478b0d727 100644
--- a/rust/agama-dbus-server/Cargo.toml
+++ b/rust/agama-dbus-server/Cargo.toml
@@ -8,6 +8,8 @@ edition = "2021"
[dependencies]
anyhow = "1.0"
agama-locale-data = { path="../agama-locale-data" }
+agama-network = { path="../agama-network" }
+agama-lib = { path="../agama-lib" }
zbus = "3.7.0"
zbus_macros = "3.7.0"
async-std = { version = "1.12.0", features = ["attributes"]}
diff --git a/rust/agama-dbus-server/src/locale.rs b/rust/agama-dbus-server/src/locale.rs
index 06ecfa85b7..ec0bcef66e 100644
--- a/rust/agama-dbus-server/src/locale.rs
+++ b/rust/agama-dbus-server/src/locale.rs
@@ -1,9 +1,10 @@
use crate::error::Error;
+use agama_lib::connection_to;
use anyhow::Context;
use std::process::Command;
-use zbus::{dbus_interface, Connection, ConnectionBuilder};
+use zbus::{dbus_interface, Connection};
-pub struct Locale {
+pub struct LocaleService {
locales: Vec,
keymap: String,
timezone_id: String,
@@ -11,7 +12,7 @@ pub struct Locale {
}
#[dbus_interface(name = "org.opensuse.Agama.Locale1")]
-impl Locale {
+impl LocaleService {
// Can be `async` as well.
/// get labels for given locale. The first pair is english language and territory
/// and second one is localized one to target language from locale.
@@ -25,9 +26,11 @@ impl Locale {
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)
+ let language = languages
+ .find_by_id(loc_language)
.context("language for passed locale not found")?;
- let territory = territories.find_by_id(loc_territory)
+ let territory = territories
+ .find_by_id(loc_territory)
.context("territory for passed locale not found")?;
let default_ret = (
@@ -65,7 +68,9 @@ impl Locale {
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}'")))
+ return Err(zbus::fdo::Error::Failed(format!(
+ "Unsupported locale value '{loc}'"
+ )));
}
}
self.locales = locales;
@@ -99,22 +104,28 @@ impl Locale {
}
*/
- #[dbus_interface(name="ListVConsoleKeyboards")]
+ #[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")]
+ #[dbus_interface(property, name = "VConsoleKeyboard")]
fn keymap(&self) -> &str {
return &self.keymap.as_str();
}
- #[dbus_interface(property, name="VConsoleKeyboard")]
+ #[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().find(|&k| k == keyboard).is_some();
+ let exist = agama_locale_data::get_key_maps()
+ .unwrap()
+ .iter()
+ .find(|&k| k == keyboard)
+ .is_some();
if !exist {
- return Err(zbus::fdo::Error::Failed("Invalid keyboard value".to_string()))
+ return Err(zbus::fdo::Error::Failed(
+ "Invalid keyboard value".to_string(),
+ ));
}
self.keymap = keyboard.to_string();
Ok(())
@@ -134,7 +145,8 @@ impl Locale {
}
#[dbus_interface(property)]
- fn set_timezone(&mut self, timezone: &str) -> Result<(), zbus::fdo::Error> { // NOTE: cannot use crate::Error as property expect this one
+ 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(())
}
@@ -143,7 +155,12 @@ impl Locale {
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()])
+ .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")
@@ -159,10 +176,9 @@ impl Locale {
}
}
-
-impl Locale {
- fn new() -> Locale {
- Locale {
+impl LocaleService {
+ fn new() -> Self {
+ Self {
locales: vec!["en_US.UTF-8".to_string()],
keymap: "us".to_string(),
timezone_id: "America/Los_Angeles".to_string(),
@@ -170,30 +186,22 @@ impl Locale {
}
}
- pub async fn start_service() -> Result> {
- const ADDRESS : &str = "unix:path=/run/agama/bus";
+ pub async fn start(address: &str) -> Result> {
const SERVICE_NAME: &str = "org.opensuse.Agama.Locale1";
const SERVICE_PATH: &str = "/org/opensuse/Agama/Locale1";
// First connect to the Agama bus, then serve our API,
// for better error reporting.
- let conn = ConnectionBuilder::address(ADDRESS)?
- .build()
- .await
- .context(format!("Connecting to the Agama bus at {ADDRESS}"))?;
+ let connection = connection_to(address).await?;
// When serving, request the service name _after_ exposing the main object
- let locale = Locale::new();
- conn
- .object_server()
- .at(SERVICE_PATH, locale)
- .await?;
- conn
+ let locale = Self::new();
+ connection.object_server().at(SERVICE_PATH, locale).await?;
+ connection
.request_name(SERVICE_NAME)
.await
.context(format!("Requesting name {SERVICE_NAME}"))?;
- Ok(conn)
+ Ok(connection)
}
}
-
diff --git a/rust/agama-dbus-server/src/main.rs b/rust/agama-dbus-server/src/main.rs
index 39aa557235..5b0e7f419b 100644
--- a/rust/agama-dbus-server/src/main.rs
+++ b/rust/agama-dbus-server/src/main.rs
@@ -1,11 +1,16 @@
pub mod error;
pub mod locale;
+use agama_network::NetworkService;
use std::future::pending;
+const ADDRESS: &str = "unix:path=/run/agama/bus";
+
#[async_std::main]
async fn main() -> Result<(), Box> {
- let _con = crate::locale::Locale::start_service().await?;
+ // When adding more services here, the order might be important.
+ let _con = crate::locale::LocaleService::start(ADDRESS).await?;
+ NetworkService::start(ADDRESS).await?;
// Do other things or go to wait forever
pending::<()>().await;
diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-lib/src/dbus.rs
new file mode 100644
index 0000000000..863b42cb65
--- /dev/null
+++ b/rust/agama-lib/src/dbus.rs
@@ -0,0 +1,7 @@
+use 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>;
diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs
index 796039e98f..da828763c0 100644
--- a/rust/agama-lib/src/error.rs
+++ b/rust/agama-lib/src/error.rs
@@ -33,5 +33,5 @@ pub enum WrongParameter {
#[error("Unknown product '{0}'. Available products: '{1:?}'")]
UnknownProduct(String, Vec),
#[error("Wrong user parameters: '{0:?}'")]
- WrongUser(Vec)
+ WrongUser(Vec),
}
diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs
index ab085a5f7b..8240beba0e 100644
--- a/rust/agama-lib/src/lib.rs
+++ b/rust/agama-lib/src/lib.rs
@@ -7,6 +7,7 @@ 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;
@@ -15,12 +16,16 @@ pub use store::Store;
use crate::error::ServiceError;
use anyhow::Context;
+const ADDRESS: &str = "unix:path=/run/agama/bus";
+
pub async fn connection() -> Result {
- const PATH : &str = "/run/agama/bus";
- let address : String = format!("unix:path={PATH}");
- let conn = zbus::ConnectionBuilder::address(address.as_str())?
+ connection_to(ADDRESS).await
+}
+
+pub async fn connection_to(address: &str) -> Result {
+ let connection = zbus::ConnectionBuilder::address(address)?
.build()
.await
- .with_context(|| format!("Connecting to D-Bus address {}", address))?;
- Ok(conn)
+ .context(format!("Connecting to Agama bus at {ADDRESS}"))?;
+ Ok(connection)
}
diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs
index 476b052218..17301cb1fe 100644
--- a/rust/agama-lib/src/proxies.rs
+++ b/rust/agama-lib/src/proxies.rs
@@ -1,18 +1,6 @@
-//! # DBus interface proxies for: `org.opensuse.Agama*.**.*`
+//! D-Bus interface proxies for: `org.opensuse.Agama*.**.*`
//!
//! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.`.
-//!
-//!
-//! These DBus objects implements
-//! [standard DBus 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;
/// Progress1Proxy can be used also with Software and Storage object.
diff --git a/rust/agama-network/Cargo.toml b/rust/agama-network/Cargo.toml
new file mode 100644
index 0000000000..3aad2f4ce6
--- /dev/null
+++ b/rust/agama-network/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "agama-network"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+zbus = "3.12.0"
+agama-lib = { path="../agama-lib" }
+async-std = { version = "1.12.0", features = ["attributes"] }
+thiserror = "1.0.40"
+uuid = { version = "1.3.3", features = ["v4"] }
+parking_lot = "0.12.1"
diff --git a/rust/agama-network/src/action.rs b/rust/agama-network/src/action.rs
new file mode 100644
index 0000000000..7779bebc00
--- /dev/null
+++ b/rust/agama-network/src/action.rs
@@ -0,0 +1,18 @@
+use crate::model::{Connection, DeviceType};
+use uuid::Uuid;
+
+/// Networking actions, like adding, updating or removing connections.
+///
+/// These actions are meant to be processed by [crate::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(Uuid),
+ /// Apply the current configuration.
+ Apply,
+}
diff --git a/rust/agama-network/src/adapter.rs b/rust/agama-network/src/adapter.rs
new file mode 100644
index 0000000000..f3040d06e9
--- /dev/null
+++ b/rust/agama-network/src/adapter.rs
@@ -0,0 +1,8 @@
+use crate::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>;
+}
diff --git a/rust/agama-network/src/dbus.rs b/rust/agama-network/src/dbus.rs
new file mode 100644
index 0000000000..ba95f4c9ac
--- /dev/null
+++ b/rust/agama-network/src/dbus.rs
@@ -0,0 +1,11 @@
+//! 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};
diff --git a/rust/agama-network/src/dbus/interfaces.rs b/rust/agama-network/src/dbus/interfaces.rs
new file mode 100644
index 0000000000..45de5f4146
--- /dev/null
+++ b/rust/agama-network/src/dbus/interfaces.rs
@@ -0,0 +1,406 @@
+//! 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::{
+ action::Action,
+ error::NetworkStateError,
+ model::{Connection as NetworkConnection, Device as NetworkDevice, WirelessConnection},
+};
+use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
+use std::{
+ net::{AddrParseError, Ipv4Addr},
+ sync::{mpsc::Sender, Arc},
+};
+use uuid::Uuid;
+use zbus::{dbus_interface, zvariant::ObjectPath};
+
+/// 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.Agama.Network1.Devices")]
+impl Devices {
+ /// Returns the D-Bus paths of the network devices.
+ pub fn get_devices(&self) -> Vec {
+ let objects = self.objects.lock();
+ 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.Agama.Network1.Device")]
+impl Device {
+ #[dbus_interface(property)]
+ pub fn name(&self) -> &str {
+ &self.device.name
+ }
+
+ #[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.Agama.Network1.Connections")]
+impl Connections {
+ /// Returns the D-Bus paths of the network connections.
+ pub fn get_connections(&self) -> Vec {
+ let objects = self.objects.lock();
+ objects
+ .connections_paths()
+ .iter()
+ .filter_map(|c| ObjectPath::try_from(c.clone()).ok())
+ .collect()
+ }
+
+ /// Adds a new network connection.
+ ///
+ /// * `name`: connection name.
+ /// * `ty`: connection type (see [crate::model::DeviceType]).
+ pub async fn add_connection(&mut self, name: String, ty: u8) -> zbus::fdo::Result<()> {
+ let actions = self.actions.lock();
+ actions
+ .send(Action::AddConnection(name, ty.try_into()?))
+ .unwrap();
+ Ok(())
+ }
+
+ /// Removes a network connection.
+ ///
+ /// * `uuid`: connection UUID..
+ pub async fn remove_connection(&mut self, uuid: &str) -> zbus::fdo::Result<()> {
+ let actions = self.actions.lock();
+ let uuid =
+ Uuid::parse_str(uuid).map_err(|_| NetworkStateError::InvalidUuid(uuid.to_string()))?;
+ actions.send(Action::RemoveConnection(uuid)).unwrap();
+ Ok(())
+ }
+
+ /// Applies the network configuration.
+ ///
+ /// It includes adding, updating and removing connections as needed.
+ pub async fn apply(&self) -> zbus::fdo::Result<()> {
+ let actions = self.actions.lock();
+ actions.send(Action::Apply).unwrap();
+ Ok(())
+ }
+}
+
+/// D-Bus interface for a network connection
+///
+/// It offers an API to query a connection.
+pub struct Connection {
+ connection: Arc>,
+}
+
+impl Connection {
+ /// Creates a Connection interface object.
+ ///
+ /// * `connection`: connection to expose over D-Bus.
+ pub fn new(connection: Arc>) -> Self {
+ Self { connection }
+ }
+
+ /// Returns the underlying connection.
+ fn get_connection(&self) -> MutexGuard {
+ self.connection.lock()
+ }
+}
+
+#[dbus_interface(name = "org.opensuse.Agama.Network1.Connection")]
+impl Connection {
+ #[dbus_interface(property)]
+ pub fn id(&self) -> String {
+ self.get_connection().id().to_string()
+ }
+
+ #[dbus_interface(property, name = "UUID")]
+ pub fn uuid(&self) -> String {
+ self.get_connection().uuid().to_string()
+ }
+}
+
+/// D-Bus interface for IPv4 settings
+pub struct Ipv4 {
+ actions: Arc>>,
+ connection: Arc>,
+}
+
+impl Ipv4 {
+ /// Creates an IPv4 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.
+ fn get_connection(&self) -> MutexGuard {
+ self.connection.lock()
+ }
+
+ /// Updates the connection data in the NetworkSystem.
+ ///
+ /// * `connection`: Updated connection.
+ fn update_connection(
+ &self,
+ connection: MutexGuard,
+ ) -> zbus::fdo::Result<()> {
+ let actions = self.actions.lock();
+ actions
+ .send(Action::UpdateConnection(connection.clone()))
+ .unwrap();
+ Ok(())
+ }
+}
+
+#[dbus_interface(name = "org.opensuse.Agama.Network1.Connection.IPv4")]
+impl Ipv4 {
+ #[dbus_interface(property)]
+ pub fn addresses(&self) -> Vec<(String, u32)> {
+ let connection = self.get_connection();
+ connection
+ .ipv4()
+ .addresses
+ .iter()
+ .map(|(addr, prefix)| (addr.to_string(), *prefix))
+ .collect()
+ }
+
+ #[dbus_interface(property)]
+ pub fn set_addresses(&mut self, addresses: Vec<(String, u32)>) -> zbus::fdo::Result<()> {
+ let mut connection = self.get_connection();
+ addresses
+ .iter()
+ .map(|(addr, prefix)| addr.parse::().map(|a| (a, *prefix)))
+ .collect::, AddrParseError>>()
+ .and_then(|parsed| Ok(connection.ipv4_mut().addresses = parsed))
+ .map_err(|err| NetworkStateError::from(err))?;
+ self.update_connection(connection)
+ }
+
+ #[dbus_interface(property)]
+ pub fn method(&self) -> u8 {
+ let connection = self.get_connection();
+ connection.ipv4().method as u8
+ }
+
+ #[dbus_interface(property)]
+ pub fn set_method(&mut self, method: u8) -> zbus::fdo::Result<()> {
+ let mut connection = self.get_connection();
+ connection.ipv4_mut().method = method.try_into()?;
+ self.update_connection(connection)
+ }
+
+ #[dbus_interface(property)]
+ pub fn nameservers(&self) -> Vec {
+ let connection = self.get_connection();
+ connection
+ .ipv4()
+ .nameservers
+ .iter()
+ .map(|a| a.to_string())
+ .collect()
+ }
+
+ #[dbus_interface(property)]
+ pub fn set_nameservers(&mut self, addresses: Vec) -> zbus::fdo::Result<()> {
+ let mut connection = self.get_connection();
+ let ipv4 = connection.ipv4_mut();
+ addresses
+ .iter()
+ .map(|addr| addr.parse::())
+ .collect::, AddrParseError>>()
+ .and_then(|parsed| Ok(ipv4.nameservers = parsed))
+ .map_err(|err| NetworkStateError::from(err))?;
+ self.update_connection(connection)
+ }
+
+ #[dbus_interface(property)]
+ pub fn gateway(&self) -> String {
+ let connection = self.get_connection();
+ match connection.ipv4().gateway {
+ Some(addr) => addr.to_string(),
+ None => "".to_string(),
+ }
+ }
+
+ #[dbus_interface(property)]
+ pub fn set_gateway(&mut self, gateway: String) -> zbus::fdo::Result<()> {
+ let mut connection = self.get_connection();
+ let ipv4 = connection.ipv4_mut();
+ if gateway.is_empty() {
+ ipv4.gateway = None;
+ } else {
+ let parsed: Ipv4Addr = gateway
+ .parse()
+ .map_err(|err| NetworkStateError::from(err))?;
+ ipv4.gateway = Some(parsed);
+ }
+ self.update_connection(connection)
+ }
+}
+/// 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.
+ fn get_wireless(&self) -> MappedMutexGuard {
+ MutexGuard::map(self.connection.lock(), |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.
+ fn update_connection(
+ &self,
+ connection: MappedMutexGuard,
+ ) -> zbus::fdo::Result<()> {
+ let actions = self.actions.lock();
+ let connection = NetworkConnection::Wireless(connection.clone());
+ actions.send(Action::UpdateConnection(connection)).unwrap();
+ Ok(())
+ }
+}
+
+#[dbus_interface(name = "org.opensuse.Agama.Network1.Connection.Wireless")]
+impl Wireless {
+ #[dbus_interface(property, name = "SSID")]
+ pub fn ssid(&self) -> Vec {
+ let connection = self.get_wireless();
+ connection.wireless.ssid.clone()
+ }
+
+ #[dbus_interface(property, name = "SSID")]
+ pub fn set_ssid(&mut self, ssid: Vec) -> zbus::fdo::Result<()> {
+ let mut connection = self.get_wireless();
+ connection.wireless.ssid = ssid;
+ self.update_connection(connection)
+ }
+
+ #[dbus_interface(property)]
+ pub fn mode(&self) -> u8 {
+ let connection = self.get_wireless();
+ connection.wireless.mode as u8
+ }
+
+ #[dbus_interface(property)]
+ pub fn set_mode(&mut self, mode: u8) -> zbus::fdo::Result<()> {
+ let mut connection = self.get_wireless();
+ connection.wireless.mode = mode.try_into()?;
+ self.update_connection(connection)
+ }
+
+ #[dbus_interface(property)]
+ pub fn password(&self) -> String {
+ let connection = self.get_wireless();
+ connection
+ .wireless
+ .password
+ .clone()
+ .unwrap_or("".to_string())
+ }
+
+ #[dbus_interface(property)]
+ pub fn set_password(&mut self, password: String) -> zbus::fdo::Result<()> {
+ let mut connection = self.get_wireless();
+ connection.wireless.password = if password.is_empty() {
+ None
+ } else {
+ Some(password)
+ };
+ self.update_connection(connection)
+ }
+
+ #[dbus_interface(property)]
+ pub fn security(&self) -> u8 {
+ let connection = self.get_wireless();
+ connection.wireless.security as u8
+ }
+
+ #[dbus_interface(property)]
+ pub fn set_security(&mut self, security: &str) -> zbus::fdo::Result<()> {
+ let mut connection = self.get_wireless();
+ connection.wireless.security = security
+ .try_into()
+ .map_err(|_| NetworkStateError::InvalidSecurityProtocol(security.to_string()))?;
+ self.update_connection(connection)?;
+ Ok(())
+ }
+}
diff --git a/rust/agama-network/src/dbus/service.rs b/rust/agama-network/src/dbus/service.rs
new file mode 100644
index 0000000000..5ffe35d8fe
--- /dev/null
+++ b/rust/agama-network/src/dbus/service.rs
@@ -0,0 +1,39 @@
+//! Network D-Bus service.
+//!
+//! This module defines a D-Bus service which exposes Agama's network configuration.
+use crate::NetworkSystem;
+use agama_lib::connection_to;
+use std::{error::Error, thread};
+
+/// 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(address: &str) -> Result<(), Box> {
+ const SERVICE_NAME: &str = "org.opensuse.Agama.Network1";
+
+ let connection = connection_to(address).await?;
+ let mut network = NetworkSystem::from_network_manager(connection.clone())
+ .await
+ .expect("Could not read network state");
+
+ thread::spawn(move || {
+ async_std::task::block_on(async {
+ network
+ .setup()
+ .await
+ .expect("Could not set up the D-Bus tree");
+ connection
+ .request_name(SERVICE_NAME)
+ .await
+ .expect(&format!("Could not request name {SERVICE_NAME}"));
+
+ network.listen().await;
+ })
+ });
+ Ok(())
+ }
+}
diff --git a/rust/agama-network/src/dbus/tree.rs b/rust/agama-network/src/dbus/tree.rs
new file mode 100644
index 0000000000..4dc17404c8
--- /dev/null
+++ b/rust/agama-network/src/dbus/tree.rs
@@ -0,0 +1,233 @@
+use agama_lib::error::ServiceError;
+use parking_lot::Mutex;
+use uuid::Uuid;
+
+use crate::{action::Action, dbus::interfaces, model::*};
+use std::collections::HashMap;
+use std::sync::mpsc::Sender;
+use std::sync::Arc;
+
+const CONNECTIONS_PATH: &str = "/org/opensuse/Agama/Network1/connections";
+const DEVICES_PATH: &str = "/org/opensuse/Agama/Network1/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: &Vec,
+ ) -> 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: &Vec) -> 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: &Vec) -> Result<(), ServiceError> {
+ for (i, dev) in devices.iter().enumerate() {
+ let path = format!("{}/{}", DEVICES_PATH, i);
+ self.add_interface(&path, interfaces::Device::new(dev.clone()))
+ .await?;
+ let mut objects = self.objects.lock();
+ 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.
+ pub async fn add_connection(&self, conn: &Connection) -> Result<(), ServiceError> {
+ let mut objects = self.objects.lock();
+
+ let path = format!("{}/{}", CONNECTIONS_PATH, objects.connections.len());
+ let cloned = Arc::new(Mutex::new(conn.clone()));
+ self.add_interface(&path, interfaces::Connection::new(Arc::clone(&cloned)))
+ .await?;
+
+ self.add_interface(
+ &path,
+ interfaces::Ipv4::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?;
+ }
+
+ objects.register_connection(conn.uuid(), &path);
+ Ok(())
+ }
+
+ /// Removes a connection from the tree
+ ///
+ /// * `uuid`: UUID of the connection to remove.
+ pub async fn remove_connection(&mut self, uuid: Uuid) -> Result<(), ServiceError> {
+ let mut objects = self.objects.lock();
+ let Some(path) = objects.connection_path(uuid) else {
+ return Ok(())
+ };
+ self.remove_connection_on(path).await?;
+ objects.deregister_connection(uuid).unwrap();
+ Ok(())
+ }
+
+ /// Adds connections to the D-Bus tree.
+ ///
+ /// * `connections`: list of connections.
+ async fn add_connections(&self, connections: &Vec) -> Result<(), ServiceError> {
+ for conn in connections.iter() {
+ self.add_connection(conn).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();
+ 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();
+ 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.clone(), iface).await?)
+ }
+}
+
+/// Objects paths for known devices and connections
+///
+/// TODO: use zvariant::OwnedObjectPath instead of String as values.
+#[derive(Debug, Default)]
+pub struct ObjectsRegistry {
+ /// device_name (eth0) -> object_path
+ pub devices: HashMap,
+ /// uuid -> object_path
+ pub connections: HashMap,
+}
+
+impl ObjectsRegistry {
+ /// Registers a network device.
+ ///
+ /// * `name`: device name.
+ /// * `path`: object path.
+ pub fn register_device(&mut self, name: &str, path: &str) {
+ self.devices.insert(name.to_string(), path.to_string());
+ }
+
+ /// Registers a network connection.
+ ///
+ /// * `uuid`: connection UUID.
+ /// * `path`: object path.
+ pub fn register_connection(&mut self, uuid: Uuid, path: &str) {
+ self.connections.insert(uuid, path.to_string());
+ }
+
+ /// Returns the path for a connection.
+ ///
+ /// * `uuid`: connection UUID.
+ pub fn connection_path(&self, uuid: Uuid) -> Option<&str> {
+ self.connections.get(&uuid).map(|p| p.as_str())
+ }
+
+ /// Deregisters a network connection.
+ ///
+ /// * `uuid`: connection UUID.
+ pub fn deregister_connection(&mut self, uuid: Uuid) -> Option {
+ self.connections.remove(&uuid)
+ }
+
+ /// 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()
+ }
+}
diff --git a/rust/agama-network/src/error.rs b/rust/agama-network/src/error.rs
new file mode 100644
index 0000000000..baa173fba0
--- /dev/null
+++ b/rust/agama-network/src/error.rs
@@ -0,0 +1,33 @@
+//! Error types.
+use std::net::AddrParseError;
+use thiserror::Error;
+use uuid::Uuid;
+
+/// Errors that are related to the network configuration.
+#[derive(Error, Debug)]
+pub enum NetworkStateError {
+ #[error("Invalid connection name: '{0}'")]
+ UnknownConnection(Uuid),
+ #[error("Invalid connection UUID: '{0}'")]
+ InvalidUuid(String),
+ #[error("Invalid IP address")]
+ InvalidIpv4Addr(#[from] AddrParseError),
+ #[error("Invalid IP method: '{0}'")]
+ InvalidIpMethod(u8),
+ #[error("Invalid wireless mode: '{0}'")]
+ InvalidWirelessMode(u8),
+ #[error("Connection '{0}' already exists")]
+ ConnectionExists(Uuid),
+ #[error("Invalid device type: '{0}'")]
+ InvalidDeviceType(u8),
+ #[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.to_string()))
+ }
+}
diff --git a/rust/agama-network/src/lib.rs b/rust/agama-network/src/lib.rs
new file mode 100644
index 0000000000..93e8dfdc8d
--- /dev/null
+++ b/rust/agama-network/src/lib.rs
@@ -0,0 +1,53 @@
+//! 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/Agama/Network1/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/Agama/Network1/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/Agama/Network1/connections/[0-9]+`. Those objects are composed of several
+//! D-Bus interfaces depending on its type:
+//!
+//! * `org.opensuse.Agama.Network1.Connection` exposes common information across all connection
+//! types.
+//! * `org.opensuse.Agama.Network1.Connection.IPv4` includes IPv4 settings, like the configuration method
+//! (DHCP, manual, etc.), IP addresses, name servers and so on.
+//! * `org.opensuse.Agama.Network1.Connection.Wireless` exposes the configuration for wireless
+//! connections.
+//!
+//! Analogous to the devices API, there is a special `/org/opensuse/Agama/Network1/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 system::NetworkSystem;
diff --git a/rust/agama-network/src/model.rs b/rust/agama-network/src/model.rs
new file mode 100644
index 0000000000..1c124193ce
--- /dev/null
+++ b/rust/agama-network/src/model.rs
@@ -0,0 +1,441 @@
+//! 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 uuid::Uuid;
+
+use crate::error::NetworkStateError;
+use std::{fmt, net::Ipv4Addr};
+
+#[derive(Default)]
+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, uuid: Uuid) -> Option<&Connection> {
+ self.connections.iter().find(|c| c.uuid() == uuid)
+ }
+
+ /// Get connection by UUID as mutable
+ ///
+ /// * `uuid`: connection UUID
+ pub fn get_connection_mut(&mut self, uuid: Uuid) -> Option<&mut Connection> {
+ self.connections.iter_mut().find(|c| c.uuid() == uuid)
+ }
+
+ /// 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 let Some(_) = self.get_connection(conn.uuid()) {
+ 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.uuid()) else {
+ return Err(NetworkStateError::UnknownConnection(conn.uuid()));
+ };
+
+ *old_conn = conn;
+ Ok(())
+ }
+
+ /// Removes a connection from the state.
+ ///
+ /// Additionally, it registers the connection to be removed when the changes are applied.
+ pub fn remove_connection(&mut self, uuid: Uuid) -> Result<(), NetworkStateError> {
+ let Some(conn) = self.get_connection_mut(uuid) else {
+ return Err(NetworkStateError::UnknownConnection(uuid));
+ };
+
+ conn.remove();
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use uuid::Uuid;
+
+ use super::{BaseConnection, Connection, EthernetConnection, NetworkState};
+ use crate::error::NetworkStateError;
+
+ #[test]
+ fn test_add_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).unwrap();
+ let found = state.get_connection(uuid).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 uuid = Uuid::new_v4();
+ let base0 = BaseConnection {
+ uuid,
+ ..Default::default()
+ };
+ let conn0 = Connection::Ethernet(EthernetConnection { base: base0 });
+ state.add_connection(conn0).unwrap();
+
+ let base1 = BaseConnection {
+ uuid,
+ ..Default::default()
+ };
+ let conn2 = Connection::Ethernet(EthernetConnection { base: base1 });
+ state.update_connection(conn2).unwrap();
+ let found = state.get_connection(uuid).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 uuid = Uuid::new_v4();
+ let base0 = BaseConnection {
+ uuid,
+ ..Default::default()
+ };
+ let conn0 = Connection::Ethernet(EthernetConnection { base: base0 });
+ state.add_connection(conn0).unwrap();
+ state.remove_connection(uuid).unwrap();
+ let found = state.get_connection(uuid).unwrap();
+ assert!(found.is_removed());
+ }
+
+ #[test]
+ fn test_remove_unknown_connection() {
+ let mut state = NetworkState::default();
+ let error = state.remove_connection(Uuid::new_v4()).unwrap_err();
+ assert!(matches!(error, NetworkStateError::UnknownConnection(_)));
+ }
+}
+
+/// Network device
+#[derive(Debug, Clone)]
+pub struct Device {
+ pub name: String,
+ pub type_: DeviceType,
+}
+
+#[derive(Debug, PartialEq, Copy, Clone)]
+pub enum DeviceType {
+ Loopback = 0,
+ Ethernet = 1,
+ Wireless = 2,
+}
+
+impl TryFrom for DeviceType {
+ type Error = NetworkStateError;
+
+ fn try_from(value: u8) -> Result {
+ match value {
+ 0 => Ok(DeviceType::Loopback),
+ 1 => Ok(DeviceType::Ethernet),
+ 2 => Ok(DeviceType::Wireless),
+ _ => Err(NetworkStateError::InvalidDeviceType(value)),
+ }
+ }
+}
+
+/// 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: id.to_string(),
+ ..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 uuid(&self) -> Uuid {
+ self.base().uuid
+ }
+
+ pub fn ipv4(&self) -> &Ipv4Config {
+ &self.base().ipv4
+ }
+
+ pub fn ipv4_mut(&mut self) -> &mut Ipv4Config {
+ &mut self.base_mut().ipv4
+ }
+
+ pub fn remove(&mut self) {
+ self.base_mut().status = Status::Removed;
+ }
+
+ pub fn is_removed(&self) -> bool {
+ self.base().status == Status::Removed
+ }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct BaseConnection {
+ pub id: String,
+ pub uuid: Uuid,
+ pub ipv4: Ipv4Config,
+ pub status: Status,
+}
+
+impl PartialEq for BaseConnection {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id && self.uuid == other.uuid && self.ipv4 == other.ipv4
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
+pub enum Status {
+ #[default]
+ Present,
+ Removed,
+}
+
+#[derive(Debug, Default, PartialEq, Clone)]
+pub struct Ipv4Config {
+ pub method: IpMethod,
+ pub addresses: Vec<(Ipv4Addr, u32)>,
+ pub nameservers: Vec,
+ pub gateway: Option,
+}
+
+#[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)
+ }
+}
+
+// NOTE: we could use num-derive.
+impl TryFrom for IpMethod {
+ type Error = NetworkStateError;
+
+ fn try_from(value: u8) -> Result {
+ match value {
+ 0 => Ok(IpMethod::Disabled),
+ 1 => Ok(IpMethod::Auto),
+ 2 => Ok(IpMethod::Manual),
+ 3 => Ok(IpMethod::LinkLocal),
+ _ => Err(NetworkStateError::InvalidIpMethod(value)),
+ }
+ }
+}
+
+#[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: Vec,
+ pub password: Option,
+ pub security: SecurityProtocol,
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
+pub enum WirelessMode {
+ #[default]
+ Infra = 0,
+ AdHoc = 1,
+ Mesh = 2,
+ AP = 3,
+}
+
+impl TryFrom for WirelessMode {
+ type Error = NetworkStateError;
+
+ fn try_from(value: u8) -> Result {
+ match value {
+ 1 => Ok(WirelessMode::AdHoc),
+ 2 => Ok(WirelessMode::Infra),
+ 3 => Ok(WirelessMode::AP),
+ 4 => Ok(WirelessMode::Mesh),
+ _ => Err(NetworkStateError::InvalidWirelessMode(value)),
+ }
+ }
+}
+
+impl fmt::Display for WirelessMode {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let name = match &self {
+ 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(),
+ )),
+ }
+ }
+}
diff --git a/rust/agama-network/src/nm.rs b/rust/agama-network/src/nm.rs
new file mode 100644
index 0000000000..e841e0f407
--- /dev/null
+++ b/rust/agama-network/src/nm.rs
@@ -0,0 +1,15 @@
+//! 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;
diff --git a/rust/agama-network/src/nm/adapter.rs b/rust/agama-network/src/nm/adapter.rs
new file mode 100644
index 0000000000..508442e6aa
--- /dev/null
+++ b/rust/agama-network/src/nm/adapter.rs
@@ -0,0 +1,63 @@
+use crate::{
+ model::{Connection, NetworkState},
+ nm::NetworkManagerClient,
+ Adapter,
+};
+use agama_lib::error::ServiceError;
+
+/// 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 {
+ match conn {
+ Connection::Loopback(_) => false,
+ _ => true,
+ }
+ }
+}
+
+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 {
+ eprintln!("Could not remove the connection {}: {}", conn.uuid(), e);
+ }
+ } else {
+ if let Err(e) = self.client.add_or_update_connection(conn).await {
+ eprintln!("Could not add/update the connection {}: {}", conn.uuid(), e);
+ }
+ }
+ }
+ });
+ // FIXME: indicate which connections could not be written.
+ Ok(())
+ }
+}
diff --git a/rust/agama-network/src/nm/client.rs b/rust/agama-network/src/nm/client.rs
new file mode 100644
index 0000000000..cb366e167e
--- /dev/null
+++ b/rust/agama-network/src/nm/client.rs
@@ -0,0 +1,118 @@
+//! 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::model::{Connection, Device};
+use agama_lib::error::ServiceError;
+use uuid::Uuid;
+use zbus;
+
+/// 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
+ eprintln!(
+ "Unsupported device type {:?} for {}",
+ &device_type, &device_name
+ );
+ }
+ }
+
+ 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.into());
+ }
+ }
+ 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);
+ 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?;
+ } else {
+ let proxy = SettingsProxy::new(&self.connection).await?;
+ proxy.add_connection(new_conn).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(())
+ }
+
+ 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)
+ }
+}
diff --git a/rust/agama-network/src/nm/dbus.rs b/rust/agama-network/src/nm/dbus.rs
new file mode 100644
index 0000000000..79a3fbdf50
--- /dev/null
+++ b/rust/agama-network/src/nm/dbus.rs
@@ -0,0 +1,464 @@
+//! 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::model::*;
+use agama_lib::dbus::{NestedHash, OwnedNestedHash};
+use std::collections::HashMap;
+use std::net::Ipv4Addr;
+use uuid::Uuid;
+use zbus::zvariant::{self, 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 cnvert.
+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())]);
+ result.insert("ipv4", ipv4_to_dbus(conn.ipv4()));
+
+ 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.into());
+ }
+ 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<'a>(conn: &'a mut NestedHash) {
+ if let Some(ipv4) = conn.get_mut("ipv4") {
+ ipv4.remove("addresses");
+ }
+
+ if let Some(ipv6) = conn.get_mut("ipv6") {
+ ipv6.remove("addresses");
+ }
+}
+
+fn ipv4_to_dbus(ipv4: &Ipv4Config) -> HashMap<&str, zvariant::Value> {
+ let addresses: Vec> = ipv4
+ .addresses
+ .iter()
+ .map(|(addr, prefix)| {
+ HashMap::from([
+ ("address", Value::new(addr.to_string())),
+ ("prefix", Value::new(prefix)),
+ ])
+ })
+ .collect();
+ let address_data: Value = addresses.into();
+
+ let dns_data: Value = ipv4
+ .nameservers
+ .iter()
+ .map(|ns| ns.to_string().into())
+ .collect::>()
+ .into();
+
+ let mut ipv4_dbus = HashMap::from([
+ ("address-data", address_data),
+ ("dns-data", dns_data),
+ ("method", ipv4.method.to_string().into()),
+ ]);
+
+ if let Some(gateway) = ipv4.gateway {
+ ipv4_dbus.insert("gateway", gateway.to_string().into());
+ }
+ ipv4_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)),
+ ]);
+
+ 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),
+ ])
+}
+
+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(ipv4) = conn.get("ipv4") {
+ base_connection.ipv4 = ipv4_config_from_dbus(ipv4)?;
+ }
+
+ Some(base_connection)
+}
+
+fn ipv4_config_from_dbus(ipv4: &HashMap) -> Option {
+ let method: &str = ipv4.get("method")?.downcast_ref()?;
+ let address_data = ipv4.get("address-data")?;
+ let address_data = address_data.downcast_ref::()?;
+ let mut addresses: Vec<(Ipv4Addr, u32)> = 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()?;
+ addresses.push((addr_str.parse().unwrap(), *prefix))
+ }
+ let mut ipv4_config = Ipv4Config {
+ method: NmMethod(method.to_string()).try_into().ok()?,
+ addresses,
+ ..Default::default()
+ };
+
+ if let Some(dns_data) = ipv4.get("dns-data") {
+ let dns_data = dns_data.downcast_ref::()?;
+ for server in dns_data.get() {
+ let server: &str = server.downcast_ref()?;
+ ipv4_config.nameservers.push(server.parse().unwrap());
+ }
+ }
+
+ if let Some(gateway) = ipv4.get("gateway") {
+ let gateway: &str = gateway.downcast_ref()?;
+ ipv4_config.gateway = Some(gateway.parse().unwrap());
+ }
+
+ Some(ipv4_config)
+}
+
+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,
+ ..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)
+}
+
+#[cfg(test)]
+mod test {
+ use super::{
+ connection_from_dbus, connection_to_dbus, merge_dbus_connections, NestedHash,
+ OwnedNestedHash,
+ };
+ use crate::{model::*, nm::dbus::ETHERNET_KEY};
+ use std::{collections::HashMap, net::Ipv4Addr};
+ 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_data = vec![HashMap::from([
+ ("address".to_string(), Value::new("192.168.0.10")),
+ ("prefix".to_string(), Value::new(24 as u32)),
+ ])];
+
+ let ipv4_section = HashMap::from([
+ ("method".to_string(), Value::new("auto").to_owned()),
+ (
+ "address-data".to_string(),
+ Value::new(address_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 dbus_conn = HashMap::from([
+ ("connection".to_string(), connection_section),
+ ("ipv4".to_string(), ipv4_section),
+ (ETHERNET_KEY.to_string(), build_ethernet_section_from_dbus()),
+ ]);
+
+ let connection = connection_from_dbus(dbus_conn).unwrap();
+
+ assert_eq!(connection.id(), "eth0");
+ let ipv4 = connection.ipv4();
+ assert_eq!(ipv4.addresses, vec![(Ipv4Addr::new(192, 168, 0, 10), 24)]);
+ assert_eq!(ipv4.nameservers, vec![Ipv4Addr::new(192, 168, 0, 2)]);
+ assert_eq!(ipv4.method, 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, 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: "agama".as_bytes().into(),
+ ..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(),
+ ),
+ ]);
+ original.insert("connection".to_string(), connection);
+ original.insert("ipv4".to_string(), ipv4);
+
+ let base = BaseConnection {
+ id: "agama".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())
+ );
+
+ 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());
+ }
+
+ fn build_ethernet_section_from_dbus() -> HashMap {
+ HashMap::from([("auto-negotiate".to_string(), true.into())])
+ }
+
+ fn build_base_connection() -> BaseConnection {
+ let addresses = vec![(Ipv4Addr::new(192, 168, 0, 2), 24)];
+ let ipv4 = Ipv4Config {
+ addresses,
+ gateway: Some(Ipv4Addr::new(192, 168, 0, 1)),
+ ..Default::default()
+ };
+ BaseConnection {
+ id: "agama".to_string(),
+ ipv4,
+ ..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");
+ }
+}
diff --git a/rust/agama-network/src/nm/error.rs b/rust/agama-network/src/nm/error.rs
new file mode 100644
index 0000000000..0d9627ee91
--- /dev/null
+++ b/rust/agama-network/src/nm/error.rs
@@ -0,0 +1,21 @@
+//! NetworkManager error types
+use crate::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())
+ }
+}
diff --git a/rust/agama-network/src/nm/model.rs b/rust/agama-network/src/nm/model.rs
new file mode 100644
index 0000000000..0b6fb659dd
--- /dev/null
+++ b/rust/agama-network/src/nm/model.rs
@@ -0,0 +1,178 @@
+//! 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::{
+ model::{DeviceType, IpMethod, SecurityProtocol, WirelessMode},
+ nm::error::NmError,
+};
+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, Clone, Copy)]
+pub struct NmDeviceType(pub u32);
+
+impl From for u32 {
+ fn from(value: NmDeviceType) -> u32 {
+ value.0
+ }
+}
+
+impl Default for NmDeviceType {
+ fn default() -> Self {
+ NmDeviceType(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,
+}
diff --git a/rust/agama-network/src/nm/proxies.rs b/rust/agama-network/src/nm/proxies.rs
new file mode 100644
index 0000000000..32c3f0f833
--- /dev/null
+++ b/rust/agama-network/src/nm/proxies.rs
@@ -0,0 +1,585 @@
+//! 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;
+ 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>;
+ 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;
+ 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;
+ 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;
+ 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;
+ 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;
+ 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;
+}
diff --git a/rust/agama-network/src/system.rs b/rust/agama-network/src/system.rs
new file mode 100644
index 0000000000..f173b4e7cb
--- /dev/null
+++ b/rust/agama-network/src/system.rs
@@ -0,0 +1,106 @@
+use crate::dbus::Tree;
+use crate::{model::Connection, nm::NetworkManagerAdapter, Action, Adapter, NetworkState};
+use agama_lib::error::ServiceError;
+use std::error::Error;
+use std::sync::mpsc::{channel, Receiver, Sender};
+
+/// Represents the network system, wrapping a [NetworkState] 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,
+}
+
+impl NetworkSystem {
+ pub fn new(state: NetworkState, conn: zbus::Connection) -> Self {
+ let (actions_tx, actions_rx) = channel();
+ let tree = Tree::new(conn, actions_tx.clone());
+ Self {
+ state,
+ actions_tx,
+ actions_rx,
+ tree,
+ }
+ }
+
+ /// Reads the network configuration using the NetworkManager adapter.
+ ///
+ /// * `conn`: connection where self will be exposed. Another connection will be made internally
+ /// to talk with NetworkManager (which may be on a different bus even).
+ pub async fn from_network_manager(
+ conn: zbus::Connection,
+ ) -> Result> {
+ let adapter = NetworkManagerAdapter::from_system()
+ .await
+ .expect("Could not connect to NetworkManager to read the configuration.");
+ let state = adapter.read()?;
+ Ok(Self::new(state, conn))
+ }
+
+ /// Writes the network configuration to NetworkManager.
+ pub async fn to_network_manager(&mut self) -> Result<(), Box> {
+ let adapter = NetworkManagerAdapter::from_system()
+ .await
+ .expect("Could not connect to NetworkManager to write the changes.");
+ adapter.write(&self.state)?;
+ self.state = 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<(), ServiceError> {
+ self.tree
+ .set_connections(&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() {
+ 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 conn = Connection::new(name, ty);
+ self.tree.add_connection(&conn).await?;
+ self.state.add_connection(conn)?;
+ }
+ Action::UpdateConnection(conn) => {
+ self.state.update_connection(conn)?;
+ }
+ Action::RemoveConnection(uuid) => {
+ self.tree.remove_connection(uuid).await?;
+ self.state.remove_connection(uuid)?;
+ }
+ Action::Apply => {
+ self.to_network_manager().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(&self.state.connections)
+ .await?;
+ }
+ }
+
+ Ok(())
+ }
+}
diff --git a/rust/package/agama-cli.changes b/rust/package/agama-cli.changes
index 1539f2645d..fe96344e95 100644
--- a/rust/package/agama-cli.changes
+++ b/rust/package/agama-cli.changes
@@ -1,3 +1,9 @@
+-------------------------------------------------------------------
+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
diff --git a/service/share/dbus.conf b/service/share/dbus.conf
index 47a6ef1f4c..0c64fd6b2c 100644
--- a/service/share/dbus.conf
+++ b/service/share/dbus.conf
@@ -39,6 +39,7 @@
+
@@ -46,6 +47,7 @@
+