diff --git a/.editorconfig b/.editorconfig
index 6367fde790..d24ed170be 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -7,3 +7,11 @@ root = true
[*.sh]
indent_style = space
indent_size = 2
+
+[*.{c,h,cxx,hxx}]
+indent_style = space
+indent_size = 2
+tab_width = 8
+
+[Makefile]
+indent_style = tab
diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml
index 29d430d677..4ffe462359 100644
--- a/.github/workflows/ci-rust.yml
+++ b/.github/workflows/ci-rust.yml
@@ -72,28 +72,43 @@ jobs:
timeout-minutes: 20
runs-on: ubuntu-latest
- defaults:
- run:
- working-directory: ./rust
+ container:
+ image: registry.opensuse.org/opensuse/tumbleweed:latest
+ options: --security-opt seccomp=unconfined
steps:
+ - name: Configure and refresh repositories
+ # disable unused repositories to have faster refresh
+ run: zypper modifyrepo -d repo-non-oss repo-openh264 repo-update && ( zypper ref || zypper ref || zypper ref )
+
+ - name: Install required packages
+ run: zypper --non-interactive install --allow-downgrade
+ clang-devel
+ gcc-c++
+ git
+ libopenssl-3-devel
+ libsuseconnect
+ libzypp-devel
+ make
+ openssl-3
+ pam-devel
+ rustup
+
+ - name: Configure git
+ run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
+
- name: Git Checkout
uses: actions/checkout@v4
+ - name: Install Rust toolchains
+ run: rustup toolchain install stable
+
- name: Rust toolchain
run: |
rustup show
cargo --version
- - name: Install packages
- run: |
- sudo apt-get update
- sudo apt-get -y install libclang-18-dev libpam0g-dev
-
- - name: Installed packages
- run: apt list --installed
-
- name: Rust cache
uses: actions/cache@v4
with:
@@ -104,44 +119,70 @@ jobs:
- name: Run clippy linter
run: cargo clippy
+ working-directory: ./rust
tests:
# the default timeout is 6 hours, that's too much if the job gets stuck
timeout-minutes: 30
runs-on: ubuntu-latest
+
+ container:
+ image: registry.opensuse.org/opensuse/tumbleweed:latest
+ options: --security-opt seccomp=unconfined
+
env:
COVERAGE: 1
- defaults:
- run:
- working-directory: ./rust
-
steps:
+ - name: Configure and refresh repositories
+ # disable unused repositories to have faster refresh
+ run: zypper modifyrepo -d repo-non-oss repo-openh264 repo-update && ( zypper ref || zypper ref || zypper ref )
+
+ - name: Install required packages
+ run: zypper --non-interactive install --allow-downgrade
+ clang-devel
+ dbus-1-daemon
+ gcc-c++
+ git
+ glibc-locale
+ golang-github-google-jsonnet
+ jq
+ libopenssl-3-devel
+ libsuseconnect
+ libzypp-devel
+ make
+ openssl-3
+ pam-devel
+ python-langtable-data
+ python3-openapi_spec_validator
+ rustup
+ timezone
+ util-linux-systemd
+ xkeyboard-config
+
+ - name: Configure git
+ run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
+
- name: Git Checkout
uses: actions/checkout@v4
+ - name: Install Rust toolchains
+ run: rustup toolchain install stable
+
- name: Rust toolchain
run: |
rustup show
cargo --version
- - name: Install packages
- run: |
- sudo apt-get update
- sudo apt-get -y install libclang-18-dev libpam0g-dev python3-langtable jsonnet
-
- name: Prepare for tests
run: |
- # the langtable data location is different in SUSE/openSUSE, create a symlink
- sudo mkdir -p /usr/share/langtable
- sudo ln -s /usr/lib/python3/dist-packages/langtable/data /usr/share/langtable/data
# create the /etc/agama.d/locales file with list of locales
- sudo mkdir /etc/agama.d
- sudo bash -c 'ls -1 -d /usr/share/i18n/locales/* | sed -e "s#/usr/share/i18n/locales/##" >/etc/agama.d/locales'
+ mkdir -p /etc/agama.d
+ ls -1 -d /usr/lib/locale/*.utf8 | sed -e "s#/usr/lib/locale/##" -e "s#utf8#UTF-8#" >/etc/agama.d/locales
- name: Installed packages
- run: apt list --installed
+ run: rpm -qa
- name: Rust cache
id: cache-tests
@@ -157,6 +198,7 @@ jobs:
# this avoids refreshing the crates index and saves few seconds
if: steps.cache-tests.outputs.cache-hit != 'true'
run: cargo install cargo-tarpaulin
+ working-directory: ./rust
- name: Run the tests
# Compile into the ./target-coverage directory because tarpaulin uses special compilation
@@ -164,11 +206,12 @@ jobs:
# The --skip-clean skips the cleanup and allows using the cached results.
# See https://github.com/xd009642/tarpaulin/discussions/772
run: cargo tarpaulin --workspace --all-targets --doc --engine llvm --out xml --target-dir target-coverage --skip-clean -- --nocapture
+ working-directory: ./rust
env:
# use the "stable" tool chain (installed by default) instead of the "nightly" default in tarpaulin
RUSTC_BOOTSTRAP: 1
RUSTUP_TOOLCHAIN: stable
- RUST_BACKTRACE: 1
+ RUST_BACKTRACE: full
RUSTFLAGS: --cfg ci
# send the code coverage for the Rust part to the coveralls.io
@@ -198,38 +241,42 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
- defaults:
- run:
- working-directory: ./rust
+ container:
+ image: registry.opensuse.org/opensuse/tumbleweed:latest
steps:
+ - name: Configure and refresh repositories
+ # disable unused repositories to have faster refresh
+ run: zypper modifyrepo -d repo-non-oss repo-openh264 repo-update && ( zypper ref || zypper ref || zypper ref )
+
+ - name: Install required packages
+ run: zypper --non-interactive install --allow-downgrade
+ clang-devel
+ gcc-c++
+ git
+ libopenssl-3-devel
+ libsuseconnect
+ libzypp-devel
+ make
+ openssl-3
+ pam-devel
+ python3-openapi_spec_validator
+ rustup
+
- name: Git Checkout
uses: actions/checkout@v4
+ - name: Install Rust toolchains
+ run: rustup toolchain install stable
+
- name: Rust toolchain
run: |
rustup show
cargo --version
- - name: Configure system
- # disable updating initramfs (the system is not booted again)
- # disable updating man db (to save some time)
- run: |
- sudo sed -i "s/yes/no/g" /etc/initramfs-tools/update-initramfs.conf
- sudo rm -f /var/lib/man-db/auto-update
-
- - name: Install packages
- run: |
- sudo apt-get update
- sudo apt-get -y install libclang-18-dev libpam0g-dev
- # uninstall the python3-jsonschema package, openapi-spec-validator wants
- # to install a newer version which would conflict with that
- sudo apt-get purge python3-jsonschema
- sudo pip install openapi-spec-validator
-
- name: Installed packages
- run: apt list --installed
+ run: rpm -qa
- name: Rust cache
uses: actions/cache@v4
@@ -241,6 +288,8 @@ jobs:
- name: Generate the OpenAPI specification
run: cargo xtask openapi
+ working-directory: ./rust
- name: Validate the OpenAPI specification
run: openapi-spec-validator out/openapi/*
+ working-directory: ./rust
diff --git a/.gitignore b/.gitignore
index ea097847f6..218e45e77f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@
/*.pot
*.mo
*.bz2
+*.o
+*.a
# Do NOT ignore .github: for git this is a no-op
# but it helps ripgrep (rg) which would otherwise ignore dotfiles and dotdirs
!/.github/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/doc/boot_arguments.md b/doc/boot_arguments.md
deleted file mode 100644
index 693510ce5b..0000000000
--- a/doc/boot_arguments.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Reading arguments from kernel command line
-
-Agama configuration can be altered through the kernel command line. It is possible to load a
-full configuration file or to change some specific values.
-
-## Loading a new configuration file
-
-It is possible to load a new configuration file specifying a URL through the `inst.config_url`
-option. Here are some examples:
-
-* `inst.config_url=http://192.168.122.1/my-agama.yaml`
-* `inst.config_url=usb:///agama.yaml`
-
-See [URL handling in the
-installer](https://github.com/yast/yast-installation/blob/master/doc/url.md) to find more details
-about the supported URLs.
-
-## Custom Installation URL Configuration
-
-You can override the default `installation_url` set in the product files [here](https://github.com/openSUSE/agama/tree/master/products.d) by passing the `inst.install_url` parameter as a boot option in the bootloader.
-This is particularly useful for any pre-production testing in openQA.
-
-**Note:** Setting this variable will impact all products.
-
-### Example Usage
-
-To specify a custom installation URLs, pass following as a parameter to kernel in the bootloader.
-You can specify multiple URLs by separating them with commas.
-
-```
-inst.install_url=https://myrepo,https://myrepo2
-```
-
-## Changing configuration values
-
-Instead of loading a full configuration file, you might be interested in adjusting just a few
-configuration values. You must specify the option name in dotted notation. A typical use-case might
-be to use your own SSL certificates:
-
-```
-inst.web.ssl=true inst.web.ssl_cert=http://192.168.122.1/mycert.pem inst.web.ssl_key=http://192.168.122.1/mycert.key
-```
-
-Changing complex options (e.g., collections) is not supported yet.
-
-## Proxy Setup
-
-Agama supports proxy setup using the `proxy=` kernel command line option like
-`proxy=http://192.168.122.1:3128` when the installation requires to use an HTTP, HTTPS or FTP
-source. The supported proxy URL format is: protocol://[user[:password]@]host[:port]
-
-When the installation system boots, the agama-proxy-setup service will read the proxy URL to be
-used from the kernel command line options or through the dracut ask prompt configuration file
-writing it to the /etc/sysconfig/proxy. After that the microOS Tools setup-systemd-proxy-env
-systemd service will make the proxy variables from that file available to all the systemd units
-writing a systemd config file with all the variables as Enviroment ones.
diff --git a/doc/dbus/bus/org.opensuse.Agama.Manager1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Manager1.bus.xml
deleted file mode 100644
index 945966dedd..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama.Manager1.bus.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama.Security.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Security.bus.xml
deleted file mode 120000
index d0feb248d1..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama.Security.bus.xml
+++ /dev/null
@@ -1 +0,0 @@
-org.opensuse.Agama.Software1.bus.xml
\ No newline at end of file
diff --git a/doc/dbus/bus/org.opensuse.Agama.Software1.Product.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Software1.Product.bus.xml
deleted file mode 100644
index 360c277ef0..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama.Software1.Product.bus.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama.Software1.Proposal.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Software1.Proposal.bus.xml
deleted file mode 100644
index b501f0d052..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama.Software1.Proposal.bus.xml
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml
deleted file mode 100644
index 959cc521b8..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml
+++ /dev/null
@@ -1,103 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama.Users1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Users1.bus.xml
deleted file mode 100644
index fcb0439338..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama.Users1.bus.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama1.Manager.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Manager.bus.xml
deleted file mode 100644
index 060c3de4d2..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama1.Manager.bus.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama1.Progress.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Progress.bus.xml
deleted file mode 120000
index d0feb248d1..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama1.Progress.bus.xml
+++ /dev/null
@@ -1 +0,0 @@
-org.opensuse.Agama.Software1.bus.xml
\ No newline at end of file
diff --git a/doc/dbus/bus/org.opensuse.Agama1.Questions.Generic.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Questions.Generic.bus.xml
deleted file mode 100644
index 0bc9fe9d10..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama1.Questions.Generic.bus.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama1.Questions.WithPassword.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Questions.WithPassword.bus.xml
deleted file mode 100644
index f9dd49994c..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama1.Questions.WithPassword.bus.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama1.Questions.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Questions.bus.xml
deleted file mode 100644
index 6a85392d5c..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama1.Questions.bus.xml
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml
deleted file mode 120000
index 9cfd11ce93..0000000000
--- a/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml
+++ /dev/null
@@ -1 +0,0 @@
-org.opensuse.Agama.Software1.Product.bus.xml
\ No newline at end of file
diff --git a/doc/dbus/bus/seed.sh b/doc/dbus/bus/seed.sh
index 664a52f6fe..d7a1fb44e7 100755
--- a/doc/dbus/bus/seed.sh
+++ b/doc/dbus/bus/seed.sh
@@ -1,6 +1,6 @@
#!/bin/bash
abusctl() {
- busctl --address=unix:path=/run/agama/bus "$@"
+ busctl --address=unix:path=/run/agama/bus "$@"
}
# a stdio filter for XML introspection,
@@ -8,11 +8,11 @@ abusctl() {
# - remove detailed introspection of _child_ nodes
# - make interfaces order deterministic by sorting them
cleanup() {
- # also remove the DTD declaration
- # otherwise xmlstarlet will complain about it not being available
- sed -e '/^ $DD.$1.bus.xml
+ abusctl tree --list $DD.${1%.*}
+ abusctl introspect --xml-interface $DD.${1%.*} $SS/${1//./\/} |
+ cleanup \
+ >$DD.$1.bus.xml
}
-look Manager1
-look Software1
-look Software1.Proposal
look Storage1
-
-abusctl introspect --xml-interface \
- ${DD}1 \
- ${SS}1/Questions \
- | cleanup \
- > ${DD}1.Questions.bus.xml
-
-abusctl call \
- ${DD}1 \
- ${SS}1/Questions \
- ${DD}1.Questions \
- New "ssassa{ss}" "org.bands.Clash" "should I stay or should I go" 2 yes no yes 0
-abusctl introspect --xml-interface \
- ${DD}1 \
- ${SS}1/Questions/0 |
- cleanup \
- >${DD}1.Questions.Generic.bus.xml
-
-abusctl call \
- ${DD}1 \
- ${SS}1/Questions \
- ${DD}1.Questions \
- NewWithPassword "ssassa{ss}" "world.MiddleEarth.Moria.gate1" "Speak friend and enter" 2 enter giveup giveup 0
-abusctl introspect --xml-interface \
- ${DD}1 \
- ${SS}1/Questions/1 |
- cleanup \
- >${DD}1.Questions.WithPassword.bus.xml
-
-abusctl introspect --xml-interface \
- ${DD}.Manager1 \
- ${SS}/Users1 \
- | cleanup \
- >${DD}.Users1.bus.xml
diff --git a/doc/dbus/org.opensuse.Agama.Security.doc.xml b/doc/dbus/org.opensuse.Agama.Security.doc.xml
deleted file mode 100644
index 6b183e682b..0000000000
--- a/doc/dbus/org.opensuse.Agama.Security.doc.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/org.opensuse.Agama.Software1.Product.doc.xml b/doc/dbus/org.opensuse.Agama.Software1.Product.doc.xml
deleted file mode 100644
index 165a67094b..0000000000
--- a/doc/dbus/org.opensuse.Agama.Software1.Product.doc.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/org.opensuse.Agama.Software1.doc.xml b/doc/dbus/org.opensuse.Agama.Software1.doc.xml
deleted file mode 100644
index 215acb0e81..0000000000
--- a/doc/dbus/org.opensuse.Agama.Software1.doc.xml
+++ /dev/null
@@ -1,137 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/org.opensuse.Agama.Users1.doc.xml b/doc/dbus/org.opensuse.Agama.Users1.doc.xml
deleted file mode 100644
index 50c3971583..0000000000
--- a/doc/dbus/org.opensuse.Agama.Users1.doc.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/org.opensuse.Agama1.Manager.doc.xml b/doc/dbus/org.opensuse.Agama1.Manager.doc.xml
deleted file mode 100644
index 13dd26b9a5..0000000000
--- a/doc/dbus/org.opensuse.Agama1.Manager.doc.xml
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/org.opensuse.Agama1.Progress.doc.xml b/doc/dbus/org.opensuse.Agama1.Progress.doc.xml
deleted file mode 100644
index 1a1a3ed0e0..0000000000
--- a/doc/dbus/org.opensuse.Agama1.Progress.doc.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/org.opensuse.Agama1.Questions.Generic.doc.xml b/doc/dbus/org.opensuse.Agama1.Questions.Generic.doc.xml
deleted file mode 100644
index a58f40cbab..0000000000
--- a/doc/dbus/org.opensuse.Agama1.Questions.Generic.doc.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/org.opensuse.Agama1.Questions.WithPassword.doc.xml b/doc/dbus/org.opensuse.Agama1.Questions.WithPassword.doc.xml
deleted file mode 100644
index 5e93a51921..0000000000
--- a/doc/dbus/org.opensuse.Agama1.Questions.WithPassword.doc.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/org.opensuse.Agama1.Questions.doc.xml b/doc/dbus/org.opensuse.Agama1.Questions.doc.xml
deleted file mode 100644
index 9e47507d8a..0000000000
--- a/doc/dbus/org.opensuse.Agama1.Questions.doc.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/org.opensuse.Agama1.Registration.doc.xml b/doc/dbus/org.opensuse.Agama1.Registration.doc.xml
deleted file mode 100644
index 750c11ee7d..0000000000
--- a/doc/dbus/org.opensuse.Agama1.Registration.doc.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus_api.md b/doc/dbus_api.md
index 7352bf2d12..7e5ae4c7a0 100644
--- a/doc/dbus_api.md
+++ b/doc/dbus_api.md
@@ -44,15 +44,6 @@ We use these resources to get more familiar with D-Bus API designing.
- network manager design https://people.freedesktop.org/~lkundrak/nm-docs/spec.html
- anakonda D-Bus API ( spread in `*_interface.py` files https://github.com/rhinstaller/anaconda/tree/master/pyanaconda/modules
-## Base Product
-
-Iface: o.o.Agama.Software1
-
-See the new-style [reference][lang-ref] ([source][lang-src]).
-
-[lang-ref]: https://opensuse.github.io/agama/dbus/ref-org.opensuse.Agama.Software1.html
-[lang-src]: dbus/org.opensuse.Agama.Software1.doc.xml
-
## `org.opensuse.Agama.Storage1` Service
Service for managing storage devices.
@@ -594,17 +585,3 @@ Summary readable a{s(uub)}
##### Signals
* `PropertiesChanged`, as standard from `org.freedesktop.DBus.Properties`.
-
-## Users
-
-See the new-style [reference][usr-ref] ([source][usr-src]).
-
-[usr-ref]: https://opensuse.github.io/agama/dbus/ref-org.opensuse.Agama.Users1.html
-[usr-src]: dbus/org.opensuse.Agama.Users1.doc.xml
-
-## Manager
-
-See the new-style [reference][mgr-ref] ([source][mgr-src]).
-
-[mgr-ref]: https://opensuse.github.io/agama/dbus/ref-org.opensuse.Agama1.Manager.html
-[mgr-src]: dbus/org.opensuse.Agama1.Manager.doc.xml
diff --git a/doc/http_api.md b/doc/http_api.md
index e394a0bc66..a7ab4e0eff 100644
--- a/doc/http_api.md
+++ b/doc/http_api.md
@@ -1,42 +1,102 @@
----
-## HTTP API: An Overview
+# HTTP API
-This document outlines the **public HTTP API**. It provides an alternative way to interact with the system, complementing the Command Line Interface (CLI) and web user interface. It's important to note that both the CLI and web UI also leverage this HTTP API for their operations.
+This document outlines the HTTP API of Agama. It provides an alternative way to interact with the system, complementing the Command Line Interface (CLI) and web user interface. It's important to note that both the CLI and web UI also leverage this HTTP API for their operations.
----
-### API Documentation
-
-Agama uses **OpenAPI** to document its HTTP API. You can generate the documentation using the following commands:
+**Note**: Agama uses *OpenAPI* to document its HTTP API. You can generate the documentation using the following commands:
```shell
(cd rust; cargo xtask openapi)
cat rust/out/openapi/*.json
```
----
-### Request and Response Body
+## Overview
+
+The API is designed around 3 main concepts: *system*, *config* and *proposal*.
+
+* *system*: represents the current status of the running system.
+* *config*: represents the configuration for installing the target system.
+* *proposal*: represents what is going to be done in the target system.
+
+The *config* contains elements that can modify the *system*, the *proposal* or both. For example, the *dasd* config changes the *system*, and the *storage* config changes the *proposal*. In other cases like *network*, the config can affect to both *system* and *proposal*.
+
+~~~
+GET /status
+GET /system
+GET /extended_config
+GET PUT PATCH /config
+GET POST PATCH /questions
+GET /proposal
+GET /issues
+POST /action
+~~~
+
+### GET /status
+
+Reports the status of the installation. It contains the installation state (*configuring*, *installing*, *finished*) and the active progresses.
+
+### GET /system
+
+Returns a JSON with the info of the system (storage devices, network connections, current localization, etc).
+
+### GET /extended_config
+
+Returns the *extended config* JSON.
+
+There is a distinction between *extended config* and *config*:
+
+* The *config* is the config explicitly set by the clients.
+* The *extended config* is the config used for calculating the proposal and it is built by merging the the *config* with the default *extended config*. The default *extended config* is built from the *system info* and the *product info*.
+
+For example, if only the *locale* was configured by the user, then the *config* has no *keymap* property. Nevertheless, the *extended config* would have a *keymap* with the value from the default *extended config*.
+
+### GET PUT /config
+
+Reads or replaces the *config*. In case of patching, the given config is merged into the current *extended config*.
+
+### PATCH /config
+
+Applies changes in the *config*. There is an own patch document:
+
+~~~json
+{
+ "update": {
+ "l10n": {
+ "keymap": "es"
+ }
+ }
+}
+~~~
+
+The given config from the *update* key is merged into current *extended config*.
+
+The patch document could be extended in the future with more options, for example for resetting some parts of the config.
+
+See https://datatracker.ietf.org/doc/html/rfc5789#section-2
+
+### POST /action
+
+Allows performing actions that cannot be done as side effect of applying a config. For example, start the installation, reload the system, etc. The *actions schema* defines the possible actions, parameters, etc.
-The Agama HTTP API uses **JSON** as its request and response body format. The schema for this JSON is thoroughly documented within the OpenAPI specification.
+#### Example: reload the system
----
-### Configuration-Based API
+In some cases, clients need to request a system reload. For example, if you create a RAID device using the terminal, then you need to reload the system in order to see the new device. In the future, reloading the system could be automatically done (e.g., by listening udisk D-Bus). For now, reloading has to be manually requested.
-For automated installations, the system provides two primary API endpoints for managing configurations across various modules:
+~~~
+POST /action { "reloadSystem": "storage" }
+~~~
-* **GET `/api/${module}/config`**: Use this endpoint to **export** the current system configuration for a specific module.
-* **PUT `/api/${module}/config`**: Use this endpoint to **load** an unattended installation profile for a specific module. In some cases, the loaded configuration is also immediately applied; further details are available below.
+#### Example: change the system localization
----
-### Future Enhancements: PATCH `/api/${module}/config` for Targeted Modifications
+Sometimes we need to directly modify the system without changing the config. For example, switching the locale of the running system (UI language).
-Following internal discussions, we plan to introduce a **PATCH `/api/${module}/config`** endpoint. This new endpoint will enable more granular modifications and applications of configurations, and it will replace the existing HTTP API methods used for modifications. While not strictly required, the structure of the PATCH request will likely mirror the configuration's existing layout.
+~~~
+POST /action { "configureL10n": { language: "es_ES" } }
+~~~
-This enhancement will allow you to modify specific parts of the configuration. In some cases, these changes can be applied immediately without needing a full installation. This is especially useful for technologies that require configuration to be applied *before* an installation begins, such as:
+#### Example: start installation
-* Network settings
-* System registration
-* iSCSI
-* DASD
-* zFCP
+The installation can be started by calling the proper action.
-The key advantage of this PATCH approach is its ability to minimize **race conditions** and to more easily keep the configuration manipulation API closely aligned with the core configuration API. By only modifying the necessary parts of the configuration, it reduces conflicts, which is particularly helpful in scenarios like rapid clicks within the web user interface.
+~~~
+POST /action "install"
+~~~
diff --git a/live/src/agama-installer.changes b/live/src/agama-installer.changes
index 8e66eb7845..dde7ba49a0 100644
--- a/live/src/agama-installer.changes
+++ b/live/src/agama-installer.changes
@@ -1,3 +1,8 @@
+-------------------------------------------------------------------
+Fri Jan 9 14:44:01 UTC 2026 - Imobach Gonzalez Sosa
+
+- Version 19.pre
+
-------------------------------------------------------------------
Tue Jan 6 16:21:35 UTC 2026 - Ladislav Slezák
diff --git a/live/src/agama-installer.kiwi b/live/src/agama-installer.kiwi
index 5cf042dde7..7cb2438eca 100644
--- a/live/src/agama-installer.kiwi
+++ b/live/src/agama-installer.kiwi
@@ -18,7 +18,7 @@
- 18.0.0
+ 19.pre.0.0
zypper
en_US
us
diff --git a/live/src/config.sh b/live/src/config.sh
index 970ca53bf4..bacbac0ed6 100644
--- a/live/src/config.sh
+++ b/live/src/config.sh
@@ -61,7 +61,6 @@ systemctl enable agama-web-server.service
systemctl enable agama-dbus-monitor.service
systemctl enable agama-autoinstall.service
systemctl enable agama-hostname.service
-systemctl enable agama-proxy-setup.service
systemctl enable agama-certificate-issue.path
systemctl enable agama-certificate-wait.service
systemctl enable agama-cmdline-process.service
diff --git a/products.d/agama-products.changes b/products.d/agama-products.changes
index b322aaa5b4..246032426f 100644
--- a/products.d/agama-products.changes
+++ b/products.d/agama-products.changes
@@ -1,3 +1,9 @@
+-------------------------------------------------------------------
+Fri Jan 9 14:44:00 UTC 2026 - Imobach Gonzalez Sosa
+
+- Version 19.pre
+- Do not offer or pre-install YaST2.
+
-------------------------------------------------------------------
Tue Dec 2 12:40:33 UTC 2025 - Dominique Leuenberger
diff --git a/products.d/tumbleweed.yaml b/products.d/tumbleweed.yaml
index 0a6d8aec5d..6a134c5282 100644
--- a/products.d/tumbleweed.yaml
+++ b/products.d/tumbleweed.yaml
@@ -134,6 +134,8 @@ software:
- apparmor
mandatory_packages:
- NetworkManager
+ # TODO: dynamically propose kernel in agama code
+ - kernel-default
- openSUSE-repos-Tumbleweed
- sudo-policy-wheel-auth-self # explicit wheel group policy to conform new auth model
optional_packages: null
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 97c1405370..9b323fd071 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 3
-[[package]]
-name = "addr2line"
-version = "0.24.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
-dependencies = [
- "gimli",
-]
-
[[package]]
name = "adler2"
version = "2.0.0"
@@ -22,9 +13,11 @@ name = "agama-autoinstall"
version = "0.1.0"
dependencies = [
"agama-lib",
+ "agama-transfer",
+ "agama-utils",
"anyhow",
"tempfile",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
"tokio",
"url",
]
@@ -34,12 +27,14 @@ name = "agama-cli"
version = "1.0.0"
dependencies = [
"agama-lib",
+ "agama-transfer",
+ "agama-utils",
"anyhow",
"async-trait",
"chrono",
"clap",
"console",
- "fluent-uri",
+ "fluent-uri 0.3.2",
"home",
"indicatif",
"inquire",
@@ -47,24 +42,78 @@ dependencies = [
"reqwest",
"serde_json",
"tempfile",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
"tokio",
"url",
]
+[[package]]
+name = "agama-files"
+version = "0.1.0"
+dependencies = [
+ "agama-software",
+ "agama-utils",
+ "async-trait",
+ "serde_json",
+ "tempfile",
+ "test-context",
+ "thiserror 2.0.17",
+ "tokio",
+ "tokio-test",
+ "tracing",
+]
+
+[[package]]
+name = "agama-hostname"
+version = "0.1.0"
+dependencies = [
+ "agama-utils",
+ "anyhow",
+ "async-trait",
+ "tempfile",
+ "test-context",
+ "thiserror 2.0.17",
+ "tokio",
+ "tokio-stream",
+ "tokio-test",
+ "tracing",
+ "zbus",
+]
+
+[[package]]
+name = "agama-l10n"
+version = "0.1.0"
+dependencies = [
+ "agama-locale-data",
+ "agama-utils",
+ "anyhow",
+ "async-trait",
+ "gettext-rs",
+ "regex",
+ "test-context",
+ "thiserror 2.0.17",
+ "tokio",
+ "tokio-stream",
+ "tokio-test",
+ "tracing",
+ "zbus",
+]
+
[[package]]
name = "agama-lib"
version = "1.0.0"
dependencies = [
+ "agama-l10n",
"agama-locale-data",
"agama-network",
+ "agama-transfer",
"agama-utils",
"anyhow",
"async-trait",
"chrono",
"curl",
"env_logger",
- "fluent-uri",
+ "fluent-uri 0.3.2",
"fs_extra",
"futures-util",
"home",
@@ -81,31 +130,57 @@ dependencies = [
"serde_with",
"strum",
"tempfile",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
"tokio",
"tokio-native-tls",
"tokio-stream",
"tokio-tungstenite 0.26.2",
+ "tracing",
"url",
"utoipa",
"uuid",
"zbus",
+ "zypp-agama",
]
[[package]]
name = "agama-locale-data"
version = "0.1.0"
dependencies = [
- "anyhow",
"chrono-tz",
"flate2",
"quick-xml",
"regex",
"serde",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
"utoipa",
]
+[[package]]
+name = "agama-manager"
+version = "0.1.0"
+dependencies = [
+ "agama-files",
+ "agama-hostname",
+ "agama-l10n",
+ "agama-network",
+ "agama-software",
+ "agama-storage",
+ "agama-utils",
+ "async-trait",
+ "gettext-rs",
+ "merge",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "test-context",
+ "thiserror 2.0.17",
+ "tokio",
+ "tokio-test",
+ "tracing",
+ "zbus",
+]
+
[[package]]
name = "agama-network"
version = "0.1.0"
@@ -121,7 +196,7 @@ dependencies = [
"serde",
"serde_with",
"strum",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
"tokio",
"tokio-stream",
"tokio-test",
@@ -135,18 +210,25 @@ dependencies = [
name = "agama-server"
version = "0.1.0"
dependencies = [
+ "agama-l10n",
"agama-lib",
"agama-locale-data",
+ "agama-manager",
+ "agama-network",
+ "agama-software",
+ "agama-transfer",
"agama-utils",
"anyhow",
"async-trait",
"axum",
"axum-extra",
+ "bindgen 0.69.5",
"clap",
"config",
"futures-util",
"gethostname",
"gettext-rs",
+ "glob",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
@@ -160,9 +242,12 @@ dependencies = [
"serde",
"serde_json",
"serde_with",
+ "serde_yaml",
+ "strum",
"subprocess",
"tempfile",
- "thiserror 2.0.12",
+ "test-context",
+ "thiserror 2.0.17",
"tokio",
"tokio-openssl",
"tokio-stream",
@@ -177,14 +262,91 @@ dependencies = [
"utoipa",
"uuid",
"zbus",
+ "zypp-agama",
+]
+
+[[package]]
+name = "agama-software"
+version = "0.1.0"
+dependencies = [
+ "agama-locale-data",
+ "agama-utils",
+ "async-trait",
+ "camino",
+ "gettext-rs",
+ "glob",
+ "regex",
+ "serde",
+ "serde_with",
+ "serde_yaml",
+ "strum",
+ "suseconnect-agama",
+ "tempfile",
+ "thiserror 2.0.17",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+ "utoipa",
+ "zypp-agama",
+]
+
+[[package]]
+name = "agama-storage"
+version = "0.1.0"
+dependencies = [
+ "agama-utils",
+ "async-trait",
+ "serde",
+ "serde_json",
+ "test-context",
+ "thiserror 2.0.17",
+ "tokio",
+ "tokio-stream",
+ "tokio-test",
+ "zbus",
+]
+
+[[package]]
+name = "agama-transfer"
+version = "0.1.0"
+dependencies = [
+ "curl",
+ "regex",
+ "thiserror 2.0.17",
+ "url",
]
[[package]]
name = "agama-utils"
version = "0.1.0"
dependencies = [
+ "agama-locale-data",
+ "agama-transfer",
+ "async-trait",
+ "cidr",
+ "fluent-uri 0.4.1",
+ "fs-err",
+ "gettext-rs",
+ "macaddr",
+ "merge",
+ "regex",
+ "serde",
"serde_json",
+ "serde_with",
+ "serde_yaml",
+ "strum",
+ "tempfile",
+ "test-context",
+ "thiserror 2.0.17",
+ "tokio",
+ "tokio-stream",
+ "tokio-test",
+ "tracing",
+ "url",
"utoipa",
+ "uuid",
"zbus",
"zvariant",
]
@@ -294,9 +456,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.98"
+version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "arraydeque"
@@ -555,9 +717,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
-version = "0.1.88"
+version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
@@ -584,6 +746,7 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core",
+ "axum-macros",
"base64 0.22.1",
"bytes",
"futures-util",
@@ -660,18 +823,14 @@ dependencies = [
]
[[package]]
-name = "backtrace"
-version = "0.3.74"
+name = "axum-macros"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
dependencies = [
- "addr2line",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
- "windows-targets 0.52.6",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
]
[[package]]
@@ -709,10 +868,33 @@ dependencies = [
"itertools 0.12.1",
"lazy_static",
"lazycell",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 1.1.0",
+ "shlex",
+ "syn 2.0.101",
+ "which",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.72.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
+dependencies = [
+ "bitflags 2.9.0",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.12.1",
+ "log",
+ "prettyplease",
"proc-macro2",
"quote",
"regex",
- "rustc-hash",
+ "rustc-hash 2.1.1",
"shlex",
"syn 2.0.101",
]
@@ -792,9 +974,9 @@ dependencies = [
[[package]]
name = "borrow-or-share"
-version = "0.2.2"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32"
+checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
[[package]]
name = "brotli"
@@ -841,6 +1023,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+[[package]]
+name = "camino"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
+
[[package]]
name = "cc"
version = "1.2.20"
@@ -883,7 +1071,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
- "windows-link",
+ "windows-link 0.1.1",
]
[[package]]
@@ -924,6 +1112,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
+ "libloading",
]
[[package]]
@@ -1183,24 +1372,24 @@ dependencies = [
[[package]]
name = "curl"
-version = "0.4.47"
+version = "0.4.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265"
+checksum = "79fc3b6dd0b87ba36e565715bf9a2ced221311db47bd18011676f24a6066edbc"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"schannel",
- "socket2",
- "windows-sys 0.52.0",
+ "socket2 0.6.0",
+ "windows-sys 0.59.0",
]
[[package]]
name = "curl-sys"
-version = "0.4.80+curl-8.12.1"
+version = "0.4.84+curl-8.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55f7df2eac63200c3ab25bde3b2268ef2ee56af3d238e76d61f01c3c49bff734"
+checksum = "abc4294dc41b882eaff37973c2ec3ae203d0091341ee68fbadd1d06e0c18a73b"
dependencies = [
"cc",
"libc",
@@ -1208,14 +1397,14 @@ dependencies = [
"openssl-sys",
"pkg-config",
"vcpkg",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
name = "darling"
-version = "0.20.11"
+version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core",
"darling_macro",
@@ -1223,9 +1412,9 @@ dependencies = [
[[package]]
name = "darling_core"
-version = "0.20.11"
+version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
@@ -1237,9 +1426,9 @@ dependencies = [
[[package]]
name = "darling_macro"
-version = "0.20.11"
+version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"quote",
@@ -1468,8 +1657,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
@@ -1505,6 +1694,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "fluent-uri"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
+dependencies = [
+ "borrow-or-share",
+ "ref-cast",
+ "serde",
+]
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1534,9 +1734,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
-version = "1.2.1"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
@@ -1551,12 +1751,36 @@ dependencies = [
"num",
]
+[[package]]
+name = "fs-err"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62d91fd049c123429b018c47887d3f75a265540dd3c30ba9cb7bae9197edb03a"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -1564,6 +1788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
+ "futures-sink",
]
[[package]]
@@ -1572,6 +1797,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
[[package]]
name = "futures-io"
version = "0.3.31"
@@ -1620,10 +1856,13 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
+ "futures-channel",
"futures-core",
+ "futures-io",
"futures-macro",
"futures-sink",
"futures-task",
+ "memchr",
"pin-project-lite",
"pin-utils",
"slab",
@@ -1685,9 +1924,9 @@ dependencies = [
[[package]]
name = "gettext-rs"
-version = "0.7.2"
+version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a44e92f7dc08430aca7ed55de161253a22276dfd69c5526e5c5e95d1f7cf338a"
+checksum = "5d5857dc1b7f0fee86961de833f434e29494d72af102ce5355738c0664222bdf"
dependencies = [
"gettext-sys",
"locale_config",
@@ -1703,12 +1942,6 @@ dependencies = [
"temp-dir",
]
-[[package]]
-name = "gimli"
-version = "0.31.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
-
[[package]]
name = "glob"
version = "0.3.2"
@@ -1954,7 +2187,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
- "socket2",
+ "socket2 0.5.9",
"tokio",
"tower-service",
"tracing",
@@ -2029,7 +2262,7 @@ dependencies = [
"hyper 1.6.0",
"libc",
"pin-project-lite",
- "socket2",
+ "socket2 0.5.9",
"tokio",
"tower-service",
"tracing",
@@ -2185,9 +2418,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
-version = "1.0.3"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
@@ -2255,17 +2488,6 @@ dependencies = [
"unicode-width 0.1.14",
]
-[[package]]
-name = "io-uring"
-version = "0.7.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
-dependencies = [
- "bitflags 2.9.0",
- "cfg-if",
- "libc",
-]
-
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -2367,7 +2589,7 @@ dependencies = [
"percent-encoding",
"referencing",
"regex",
- "regex-syntax 0.8.5",
+ "regex-syntax",
"serde",
"serde_json",
"uuid-simd",
@@ -2411,7 +2633,7 @@ dependencies = [
"petgraph",
"pico-args",
"regex",
- "regex-syntax 0.8.5",
+ "regex-syntax",
"string_cache",
"term",
"tiny-keccak",
@@ -2425,7 +2647,7 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553"
dependencies = [
- "regex-automata 0.4.9",
+ "regex-automata",
]
[[package]]
@@ -2452,6 +2674,16 @@ version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link 0.2.1",
+]
+
[[package]]
name = "libredox"
version = "0.1.3"
@@ -2476,7 +2708,7 @@ dependencies = [
"once_cell",
"serde",
"sha2",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
"uuid",
]
@@ -2568,11 +2800,11 @@ dependencies = [
[[package]]
name = "matchers"
-version = "0.1.0"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
- "regex-automata 0.1.10",
+ "regex-automata",
]
[[package]]
@@ -2596,6 +2828,28 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "merge"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e520ba58faea3487f75df198b1d079644ec226ea3b0507d002c6fa4b8cf93a"
+dependencies = [
+ "merge_derive",
+ "num-traits",
+]
+
+[[package]]
+name = "merge_derive"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c8f8ce6efff81cbc83caf4af0905c46e58cb46892f63ad3835e81b47eaf7968"
+dependencies = [
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
[[package]]
name = "mime"
version = "0.3.17"
@@ -2746,12 +3000,11 @@ dependencies = [
[[package]]
name = "nu-ansi-term"
-version = "0.46.0"
+version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "overload",
- "winapi",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2874,15 +3127,6 @@ dependencies = [
"objc",
]
-[[package]]
-name = "object"
-version = "0.36.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
-dependencies = [
- "memchr",
-]
-
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -2959,12 +3203,6 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
-[[package]]
-name = "overload"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
-
[[package]]
name = "pam"
version = "0.8.0"
@@ -2994,7 +3232,7 @@ version = "1.0.0-alpha5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b"
dependencies = [
- "bindgen",
+ "bindgen 0.69.5",
"libc",
]
@@ -3054,9 +3292,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
-version = "2.3.1"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
@@ -3065,7 +3303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
dependencies = [
"memchr",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
"ucd-trie",
]
@@ -3257,6 +3495,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+[[package]]
+name = "prettyplease"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.101",
+]
+
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
@@ -3266,6 +3514,28 @@ dependencies = [
"toml_edit",
]
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.95"
@@ -3422,7 +3692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
dependencies = [
"ahash",
- "fluent-uri",
+ "fluent-uri 0.3.2",
"once_cell",
"parking_lot",
"percent-encoding",
@@ -3431,42 +3701,27 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.11.1"
+version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
name = "regex-automata"
-version = "0.1.10"
+version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.5",
+ "regex-syntax",
]
-[[package]]
-name = "regex-syntax"
-version = "0.6.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -3563,16 +3818,16 @@ dependencies = [
]
[[package]]
-name = "rustc-demangle"
-version = "0.1.24"
+name = "rustc-hash"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
-version = "1.1.0"
+version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
@@ -3669,6 +3924,30 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -3715,18 +3994,28 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]]
name = "serde"
-version = "1.0.219"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.219"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -3735,14 +4024,15 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.140"
+version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
+ "serde_core",
]
[[package]]
@@ -3799,17 +4089,18 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.12.0"
+version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
+checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.9.0",
- "serde",
- "serde_derive",
+ "schemars 0.9.0",
+ "schemars 1.1.0",
+ "serde_core",
"serde_json",
"serde_with_macros",
"time",
@@ -3817,9 +4108,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.12.0"
+version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
+checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
dependencies = [
"darling",
"proc-macro2",
@@ -3827,6 +4118,19 @@ dependencies = [
"syn 2.0.101",
]
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap 2.9.0",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
[[package]]
name = "sha1"
version = "0.10.6"
@@ -3908,7 +4212,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
"time",
]
@@ -3943,6 +4247,16 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "socket2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "spin"
version = "0.9.8"
@@ -3981,9 +4295,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
-version = "0.27.1"
+version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
+checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
@@ -4017,6 +4331,27 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+[[package]]
+name = "suseconnect-agama"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "suseconnect-agama-sys",
+ "tempfile",
+ "thiserror 2.0.17",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+]
+
+[[package]]
+name = "suseconnect-agama-sys"
+version = "0.1.0"
+dependencies = [
+ "bindgen 0.72.1",
+]
+
[[package]]
name = "syn"
version = "1.0.109"
@@ -4088,15 +4423,15 @@ checksum = "bc1ee6eef34f12f765cb94725905c6312b6610ab2b0940889cfe58dae7bc3c72"
[[package]]
name = "tempfile"
-version = "3.20.0"
+version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.5",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -4120,6 +4455,27 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "test-context"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb69cce03e432993e2dc1f93f7899b952300fcb6dc44191a1b830b60b8c3c8aa"
+dependencies = [
+ "futures",
+ "test-context-macros",
+]
+
+[[package]]
+name = "test-context-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97e0639209021e54dbe19cafabfc0b5574b078c37358945e6d473eabe39bb974"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -4131,11 +4487,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.12"
+version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
- "thiserror-impl 2.0.12",
+ "thiserror-impl 2.0.17",
]
[[package]]
@@ -4151,9 +4507,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
-version = "2.0.12"
+version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
@@ -4222,29 +4578,26 @@ dependencies = [
[[package]]
name = "tokio"
-version = "1.46.0"
+version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4"
+checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
- "backtrace",
"bytes",
- "io-uring",
"libc",
"mio 1.0.3",
"pin-project-lite",
"signal-hook-registry",
- "slab",
- "socket2",
+ "socket2 0.6.0",
"tokio-macros",
"tracing",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
-version = "2.5.0"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
@@ -4436,9 +4789,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
-version = "0.1.41"
+version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [
"log",
"pin-project-lite",
@@ -4448,9 +4801,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.28"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -4459,9 +4812,9 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.33"
+version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [
"once_cell",
"valuable",
@@ -4491,14 +4844,14 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.19"
+version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
- "regex",
+ "regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
@@ -4551,7 +4904,7 @@ dependencies = [
"native-tls",
"rand 0.9.1",
"sha1",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
"utf-8",
]
@@ -4614,6 +4967,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -4622,9 +4981,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
-version = "2.5.4"
+version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
@@ -4870,6 +5229,18 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix 0.38.44",
+]
+
[[package]]
name = "winapi"
version = "0.3.9"
@@ -4909,7 +5280,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
- "windows-link",
+ "windows-link 0.1.1",
"windows-result",
"windows-strings 0.4.0",
]
@@ -4942,6 +5313,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
[[package]]
name = "windows-registry"
version = "0.4.0"
@@ -4959,7 +5336,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
]
[[package]]
@@ -4968,7 +5345,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
]
[[package]]
@@ -4977,7 +5354,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
]
[[package]]
@@ -5007,6 +5384,15 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
[[package]]
name = "windows-targets"
version = "0.48.5"
@@ -5271,9 +5657,9 @@ dependencies = [
[[package]]
name = "zbus"
-version = "5.7.1"
+version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68"
+checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91"
dependencies = [
"async-broadcast",
"async-executor",
@@ -5296,7 +5682,8 @@ dependencies = [
"tokio",
"tracing",
"uds_windows",
- "windows-sys 0.59.0",
+ "uuid",
+ "windows-sys 0.61.2",
"winnow",
"zbus_macros",
"zbus_names",
@@ -5305,9 +5692,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
-version = "5.7.1"
+version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a"
+checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -5459,3 +5846,20 @@ dependencies = [
"syn 2.0.101",
"winnow",
]
+
+[[package]]
+name = "zypp-agama"
+version = "0.1.0"
+dependencies = [
+ "tracing",
+ "url",
+ "zypp-agama-sys",
+]
+
+[[package]]
+name = "zypp-agama-sys"
+version = "0.1.0"
+dependencies = [
+ "bindgen 0.72.1",
+ "tracing",
+]
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 4b93ca5f00..c440a2347e 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -2,12 +2,23 @@
members = [
"agama-autoinstall",
"agama-cli",
- "agama-server",
+ "agama-files",
+ "agama-hostname",
+ "agama-l10n",
"agama-lib",
"agama-locale-data",
+ "agama-manager",
"agama-network",
+ "agama-server",
+ "agama-software",
+ "agama-storage",
+ "agama-transfer",
"agama-utils",
+ "suseconnect-agama",
+ "suseconnect-agama/suseconnect-agama-sys",
"xtask",
+ "zypp-agama",
+ "zypp-agama/zypp-agama-sys",
]
resolver = "2"
diff --git a/rust/agama-autoinstall/Cargo.toml b/rust/agama-autoinstall/Cargo.toml
index 48df1a7cc0..ae668af56a 100644
--- a/rust/agama-autoinstall/Cargo.toml
+++ b/rust/agama-autoinstall/Cargo.toml
@@ -6,6 +6,8 @@ edition.workspace = true
[dependencies]
agama-lib = { path = "../agama-lib" }
+agama-utils = { path = "../agama-utils" }
+agama-transfer = { path = "../agama-transfer" }
anyhow = { version = "1.0.98" }
tempfile = "3.20.0"
thiserror = "2.0.12"
diff --git a/rust/agama-autoinstall/src/lib.rs b/rust/agama-autoinstall/src/lib.rs
index 64af4837ff..01f6a9efc3 100644
--- a/rust/agama-autoinstall/src/lib.rs
+++ b/rust/agama-autoinstall/src/lib.rs
@@ -18,9 +18,6 @@
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.
-mod kernel_cmdline;
-pub use kernel_cmdline::KernelCmdline;
-
mod loader;
pub use loader::ConfigLoader;
diff --git a/rust/agama-autoinstall/src/main.rs b/rust/agama-autoinstall/src/main.rs
index d666e8a7fd..861f2c05e8 100644
--- a/rust/agama-autoinstall/src/main.rs
+++ b/rust/agama-autoinstall/src/main.rs
@@ -20,15 +20,15 @@
use std::str::FromStr;
-use agama_autoinstall::{ConfigAutoLoader, KernelCmdline, ScriptsRunner};
+use agama_autoinstall::{ConfigAutoLoader, ScriptsRunner};
use agama_lib::{
auth::AuthToken,
http::BaseHTTPClient,
manager::{FinishMethod, ManagerHTTPClient},
};
+use agama_utils::kernel_cmdline::KernelCmdline;
use anyhow::anyhow;
-const CMDLINE_FILE: &str = "/run/agama/cmdline.d/agama.conf";
const API_URL: &str = "http://localhost/api";
pub fn build_base_client() -> anyhow::Result {
@@ -43,7 +43,7 @@ pub fn insecure_from(cmdline: &KernelCmdline, key: &str) -> bool {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
- let args = KernelCmdline::parse_file(CMDLINE_FILE)?;
+ let args = KernelCmdline::parse()?;
let http = build_base_client()?;
let manager_client = ManagerHTTPClient::new(http.clone());
diff --git a/rust/agama-autoinstall/src/questions.rs b/rust/agama-autoinstall/src/questions.rs
index 22113e929c..c91e3f1aba 100644
--- a/rust/agama-autoinstall/src/questions.rs
+++ b/rust/agama-autoinstall/src/questions.rs
@@ -20,15 +20,8 @@
//! This module offers a mechanism to ask questions to users.
-use std::collections::HashMap;
-
-use agama_lib::{
- http::BaseHTTPClient,
- questions::{
- http_client::HTTPClient as QuestionsHTTPClient,
- model::{GenericQuestion, Question},
- },
-};
+use agama_lib::{http::BaseHTTPClient, questions::http_client::HTTPClient as QuestionsHTTPClient};
+use agama_utils::api::question::QuestionSpec;
pub struct UserQuestions {
questions: QuestionsHTTPClient,
@@ -43,24 +36,13 @@ impl UserQuestions {
/// Asks the user whether to retry loading the profile.
pub async fn should_retry(&self, text: &str, error: &str) -> anyhow::Result {
- let data = HashMap::from([("error".to_string(), error.to_string())]);
- let generic = GenericQuestion {
- id: None,
- class: "load.retry".to_string(),
- text: text.to_string(),
- options: vec!["Yes".to_string(), "No".to_string()],
- default_option: "No".to_string(),
- data,
- };
- let question = Question {
- generic,
- with_password: None,
- };
+ let question = QuestionSpec::new(text, "load.retry")
+ .with_actions(&[("yes", "Yes"), ("no", "No")])
+ .with_default_action("no")
+ .with_data(&[("error", error)]);
+
let question = self.questions.create_question(&question).await?;
- let answer = self
- .questions
- .get_answer(question.generic.id.unwrap())
- .await?;
- Ok(answer.generic.answer == "Yes")
+ let answer = self.questions.get_answer(question.id).await?;
+ Ok(answer.action == "yes")
}
}
diff --git a/rust/agama-autoinstall/src/scripts.rs b/rust/agama-autoinstall/src/scripts.rs
index 6058c61c6b..bdda0cdde3 100644
--- a/rust/agama-autoinstall/src/scripts.rs
+++ b/rust/agama-autoinstall/src/scripts.rs
@@ -19,14 +19,15 @@
// find current contact information at www.suse.com.
use std::{
- fs::{self, create_dir_all, File},
+ fs::{self, create_dir_all},
io::Write,
os::unix::fs::OpenOptionsExt,
path::{Path, PathBuf},
- process::Output,
};
-use agama_lib::{http::BaseHTTPClient, utils::Transfer};
+use agama_lib::http::BaseHTTPClient;
+use agama_transfer::Transfer;
+use agama_utils::command::{create_log_file, run_with_retry};
use anyhow::anyhow;
use url::Url;
@@ -60,6 +61,8 @@ impl ScriptsRunner {
/// It downloads the script from the given URL to the runner directory.
/// It saves the stdout, stderr and exit code to separate files.
///
+ /// It will retry if it cannot run the script.
+ ///
/// * url: script URL, supporting agama-specific schemes.
pub async fn run(&mut self, url: &str) -> anyhow::Result<()> {
create_dir_all(&self.path)?;
@@ -69,8 +72,17 @@ impl ScriptsRunner {
let path = self.path.join(&file_name);
self.save_script(url, &path).await?;
- let output = std::process::Command::new(&path).output()?;
- self.save_logs(&path, output)?;
+ let stdout_file = create_log_file(&path.with_extension("stdout"))?;
+ let stderr_file = create_log_file(&path.with_extension("stderr"))?;
+
+ let mut command = tokio::process::Command::new(&path);
+ command.stdout(stdout_file).stderr(stderr_file);
+ let output = run_with_retry(command).await?;
+
+ if let Some(code) = output.status.code() {
+ let mut file = create_log_file(&path.with_extension("exit"))?;
+ write!(&mut file, "{}", code)?;
+ }
Ok(())
}
@@ -96,44 +108,23 @@ impl ScriptsRunner {
}
async fn save_script(&self, url: &str, path: &PathBuf) -> anyhow::Result<()> {
- let mut file = Self::create_file(&path, 0o700)?;
+ let mut file = fs::OpenOptions::new()
+ .create(true)
+ .write(true)
+ .truncate(true)
+ .mode(0o700)
+ .open(&path)?;
+
while let Err(error) = Transfer::get(url, &mut file, self.insecure) {
- eprintln!("Could not load configuration from {url}: {error}");
+ eprintln!("Could not load the script from {url}: {error}");
if !self.should_retry(&url, &error.to_string()).await? {
return Err(anyhow!(error));
}
}
+ file.sync_all()?;
Ok(())
}
- fn save_logs(&self, path: &Path, output: Output) -> anyhow::Result<()> {
- if !output.stdout.is_empty() {
- let mut file = Self::create_file(&path.with_extension("stdout"), 0o600)?;
- file.write_all(&output.stdout)?;
- }
-
- if !output.stderr.is_empty() {
- let mut file = Self::create_file(&path.with_extension("stderr"), 0o600)?;
- file.write_all(&output.stderr)?;
- }
-
- if let Some(code) = output.status.code() {
- let mut file = Self::create_file(&path.with_extension("exit"), 0o600)?;
- write!(&mut file, "{}", code)?;
- }
-
- Ok(())
- }
-
- fn create_file(path: &Path, perms: u32) -> std::io::Result {
- fs::OpenOptions::new()
- .create(true)
- .truncate(true)
- .write(true)
- .mode(perms)
- .open(path)
- }
-
async fn should_retry(&self, url: &str, error: &str) -> anyhow::Result {
let msg = format!(
r#"
diff --git a/rust/agama-cli/Cargo.toml b/rust/agama-cli/Cargo.toml
index 9daa10479d..4b1d87b9cc 100644
--- a/rust/agama-cli/Cargo.toml
+++ b/rust/agama-cli/Cargo.toml
@@ -8,6 +8,8 @@ edition = "2021"
[dependencies]
clap = { version = "4.5.19", features = ["derive", "wrap_help"] }
agama-lib = { path = "../agama-lib" }
+agama-utils = { path = "../agama-utils" }
+agama-transfer = { path = "../agama-transfer" }
serde_json = "1.0.128"
indicatif = "0.17.8"
thiserror = "2.0.12"
diff --git a/rust/agama-cli/src/cli_input.rs b/rust/agama-cli/src/cli_input.rs
index 61fa559d6b..fbe570e6a9 100644
--- a/rust/agama-cli/src/cli_input.rs
+++ b/rust/agama-cli/src/cli_input.rs
@@ -18,7 +18,7 @@
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.
-use agama_lib::utils::Transfer;
+use agama_transfer::Transfer;
use anyhow::Context;
use std::{
collections::HashMap,
diff --git a/rust/agama-cli/src/config.rs b/rust/agama-cli/src/config.rs
index 291ea08f0e..416e68bdb8 100644
--- a/rust/agama-cli/src/config.rs
+++ b/rust/agama-cli/src/config.rs
@@ -18,22 +18,27 @@
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.
-use std::{io::Write, path::PathBuf, process::Command};
+use std::{io::Write, path::PathBuf, process::Command, time::Duration};
-use agama_lib::profile::ProfileHTTPClient;
use agama_lib::{
- context::InstallationContext, http::BaseHTTPClient, install_settings::InstallSettings,
- profile::ProfileValidator, profile::ValidationOutcome, utils::FileFormat,
- Store as SettingsStore,
+ context::InstallationContext,
+ http::BaseHTTPClient,
+ monitor::MonitorClient,
+ profile::{ProfileHTTPClient, ProfileValidator, ValidationOutcome},
+ utils::FileFormat,
};
+use agama_utils::api;
use anyhow::{anyhow, Context};
use clap::Subcommand;
use console::style;
use fluent_uri::Uri;
+use serde_json::json;
use tempfile::Builder;
+use tokio::time::sleep;
use crate::{
- api_url, build_clients, cli_input::CliInput, cli_output::CliOutput, show_progress, GlobalOpts,
+ api_url, build_clients, build_http_client, cli_input::CliInput, cli_output::CliOutput,
+ show_progress, GlobalOpts,
};
const DEFAULT_EDITOR: &str = "/usr/bin/vi";
@@ -60,7 +65,7 @@ pub enum ConfigCommands {
/// Validate a profile using JSON Schema
///
- /// Schema is available at /usr/share/agama-cli/profile.schema.json
+ /// Schema is available at /usr/share/agama/schema/profile.schema.json
/// Note: validation is always done as part of all other "agama config" commands.
Validate {
/// JSON file, URL or path or `-` for standard input
@@ -108,67 +113,71 @@ pub async fn run(subcommand: ConfigCommands, opts: GlobalOpts) -> anyhow::Result
match subcommand {
ConfigCommands::Show { output } => {
- let (http_client, _monitor) = build_clients(api_url, opts.insecure).await?;
- let store = SettingsStore::new(http_client.clone()).await?;
- let model = store.load().await?;
- let json = serde_json::to_string_pretty(&model)?;
+ let http_client = build_http_client(api_url, opts.insecure, true).await?;
+ let response: api::Config = http_client.get("/v2/config").await?;
+ let json = serde_json::to_string_pretty(&response)?;
let destination = output.unwrap_or(CliOutput::Stdout);
destination.write(&json)?;
eprintln!();
validate(&http_client, CliInput::Full(json.clone()), false).await?;
- Ok(())
}
ConfigCommands::Load { url_or_path } => {
let (http_client, monitor) = build_clients(api_url, opts.insecure).await?;
- let store = SettingsStore::new(http_client.clone()).await?;
let url_or_path = url_or_path.unwrap_or(CliInput::Stdin);
let contents = url_or_path.read_to_string(opts.insecure)?;
let valid = validate(&http_client, CliInput::Full(contents.clone()), false).await?;
- if matches!(valid, ValidationOutcome::Valid) {
- let result =
- InstallSettings::from_json(&contents, &InstallationContext::from_env()?)?;
- tokio::spawn(async move {
- show_progress(monitor, true).await;
- });
- store.store(&result).await?;
+ if !matches!(valid, ValidationOutcome::Valid) {
+ return Ok(());
}
- Ok(())
+ let model: api::Config = serde_json::from_str(&contents)?;
+ patch_config(&http_client, &model).await?;
+
+ monitor_progress(monitor).await?;
}
ConfigCommands::Validate { url_or_path, local } => {
let _ = if !local {
- let (http_client, _monitor) = build_clients(api_url, opts.insecure).await?;
+ let http_client = build_http_client(api_url, opts.insecure, true).await?;
validate(&http_client, url_or_path, false).await
} else {
validate_local(url_or_path, opts.insecure)
};
-
- Ok(())
}
ConfigCommands::Generate { url_or_path } => {
- let (http_client, _monitor) = build_clients(api_url, opts.insecure).await?;
+ let http_client = build_http_client(api_url, opts.insecure, true).await?;
let url_or_path = url_or_path.unwrap_or(CliInput::Stdin);
- generate(&http_client, url_or_path, opts.insecure).await
+ generate(&http_client, url_or_path, opts.insecure).await?;
}
ConfigCommands::Edit { editor } => {
let (http_client, monitor) = build_clients(api_url, opts.insecure).await?;
- let store = SettingsStore::new(http_client.clone()).await?;
- let model = store.load().await?;
+ let response: api::Config = http_client.get("/v2/config").await?;
let editor = editor
.or_else(|| std::env::var("EDITOR").ok())
.unwrap_or(DEFAULT_EDITOR.to_string());
- let result = edit(&http_client, &model, &editor).await?;
- tokio::spawn(async move {
- show_progress(monitor, true).await;
- });
- store.store(&result).await?;
- Ok(())
+ let result = edit(&http_client, &response, &editor).await?;
+ patch_config(&http_client, &result).await?;
+
+ monitor_progress(monitor).await?;
}
}
+
+ Ok(())
+}
+
+async fn patch_config(
+ http_client: &BaseHTTPClient,
+ model: &api::Config,
+) -> Result<(), anyhow::Error> {
+ let model_json = json!(model);
+ let patch = api::Patch {
+ update: Some(model_json),
+ };
+ http_client.patch_void("/v2/config", &patch).await?;
+ Ok(())
}
/// Validates a JSON profile with locally available tools only
@@ -244,7 +253,7 @@ async fn generate(
url_or_path: CliInput,
insecure: bool,
) -> anyhow::Result<()> {
- let context = match &url_or_path {
+ let _context = match &url_or_path {
CliInput::Stdin | CliInput::Full(_) => InstallationContext::from_env()?,
CliInput::Url(url_str) => InstallationContext::from_url_str(url_str)?,
CliInput::Path(pathbuf) => InstallationContext::from_file(pathbuf.as_path())?,
@@ -288,8 +297,8 @@ async fn generate(
return Ok(());
}
- // resolves relative URL references
- let model = InstallSettings::from_json(&profile_json, &context)?;
+ // TODO: resolves relative URL references
+ let model: api::Config = serde_json::from_str(&profile_json)?;
let config_json = serde_json::to_string_pretty(&model)?;
println!("{}", &config_json);
@@ -344,9 +353,9 @@ async fn from_json_or_jsonnet(
/// * `editor`: editor command.
async fn edit(
http_client: &BaseHTTPClient,
- model: &InstallSettings,
+ model: &api::Config,
editor: &str,
-) -> anyhow::Result {
+) -> anyhow::Result {
let content = serde_json::to_string_pretty(model)?;
let mut file = Builder::new().suffix(".json").tempfile()?;
let path = PathBuf::from(file.path());
@@ -361,10 +370,7 @@ async fn edit(
let contents =
std::fs::read_to_string(&path).context(format!("Reading from file {:?}", path))?;
validate(&http_client, CliInput::Full(contents), false).await?;
- return Ok(InstallSettings::from_file(
- path,
- &InstallationContext::from_env()?,
- )?);
+ return Ok(serde_json::from_str(&content)?);
}
Err(anyhow!(
@@ -385,3 +391,15 @@ fn editor_command(command: &str) -> Command {
command.args(parts.collect::>());
command
}
+
+async fn monitor_progress(monitor: MonitorClient) -> anyhow::Result<()> {
+ // wait a bit to settle it down and avoid quick actions blinking
+ sleep(Duration::from_secs(1)).await;
+
+ let task = tokio::spawn(async move {
+ show_progress(monitor, true).await;
+ });
+ let _ = task.await?;
+
+ Ok(())
+}
diff --git a/rust/agama-cli/src/lib.rs b/rust/agama-cli/src/lib.rs
index e473c397bf..7b0d6a28fe 100644
--- a/rust/agama-cli/src/lib.rs
+++ b/rust/agama-cli/src/lib.rs
@@ -18,14 +18,35 @@
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.
-use agama_lib::auth::AuthToken;
-use agama_lib::context::InstallationContext;
-use agama_lib::manager::{FinishMethod, ManagerHTTPClient};
-use agama_lib::monitor::{Monitor, MonitorClient};
+use std::{
+ fs,
+ os::unix::fs::OpenOptionsExt,
+ path::PathBuf,
+ process::{ExitCode, Termination},
+ time::Duration,
+};
+
+use agama_lib::{
+ auth::AuthToken,
+ context::InstallationContext,
+ error::ServiceError,
+ http::{BaseHTTPClient, WebSocketClient},
+ manager::{FinishMethod, ManagerHTTPClient},
+ monitor::{Monitor, MonitorClient},
+};
+use agama_transfer::Transfer;
+use agama_utils::api::{self, status::Stage, IssueWithScope};
use anyhow::Context;
-use auth_tokens_file::AuthTokensFile;
use clap::{Args, Parser};
use fluent_uri::UriRef;
+use tokio::time::sleep;
+use url::Url;
+
+use crate::{
+ auth::run as run_auth_cmd, auth_tokens_file::AuthTokensFile, commands::Commands,
+ config::run as run_config_cmd, error::CliError, events::run as run_events_cmd,
+ logs::run as run_logs_cmd, progress::ProgressMonitor, questions::run as run_questions_cmd,
+};
mod auth;
mod auth_tokens_file;
@@ -39,26 +60,6 @@ mod logs;
mod progress;
mod questions;
-use crate::error::CliError;
-use agama_lib::http::{BaseHTTPClient, WebSocketClient};
-use agama_lib::{error::ServiceError, utils::Transfer};
-use auth::run as run_auth_cmd;
-use commands::Commands;
-use config::run as run_config_cmd;
-use events::run as run_events_cmd;
-use logs::run as run_logs_cmd;
-use progress::ProgressMonitor;
-use questions::run as run_questions_cmd;
-use std::fs;
-use std::os::unix::fs::OpenOptionsExt;
-use std::path::PathBuf;
-use std::{
- process::{ExitCode, Termination},
- thread::sleep,
- time::Duration,
-};
-use url::Url;
-
/// Agama's CLI global options
#[derive(Args, Clone)]
pub struct GlobalOpts {
@@ -107,40 +108,27 @@ async fn probe(manager: ManagerHTTPClient, monitor: MonitorClient) -> anyhow::Re
/// Before starting, it makes sure that the manager is idle.
///
/// * `manager`: the manager client.
-async fn install(
- manager: ManagerHTTPClient,
- monitor: MonitorClient,
- max_attempts: usize,
-) -> anyhow::Result<()> {
+async fn install(http_client: BaseHTTPClient, monitor: MonitorClient) -> anyhow::Result<()> {
wait_until_idle(monitor.clone()).await?;
- let status = manager.status().await?;
- if !status.can_install {
+ let status = monitor.get_status().await?;
+ // TODO: own client for issues?
+ let issues: Vec = http_client.get("/v2/issues").await?;
+ if status.stage != Stage::Configuring {
+ return Err(CliError::Installation)?;
+ }
+ if !issues.is_empty() {
return Err(CliError::Validation)?;
}
+ let action = api::Action::Install;
+ http_client.post_void("/v2/action", &action).await?;
+
+ // wait a bit before start monitoring
+ sleep(Duration::from_secs(1)).await;
let progress = tokio::spawn(async {
show_progress(monitor, true).await;
});
- // Try to start the installation up to max_attempts times.
- let mut attempts = 1;
- loop {
- match manager.install().await {
- Ok(()) => break,
- Err(e) => {
- eprintln!(
- "Could not start the installation process: {e}. Attempt {}/{}.",
- attempts, max_attempts
- );
- }
- }
- if attempts == max_attempts {
- eprintln!("Giving up.");
- return Err(CliError::Installation)?;
- }
- attempts += 1;
- sleep(Duration::from_secs(1));
- }
let _ = progress.await;
Ok(())
}
@@ -167,7 +155,7 @@ async fn finish(
async fn wait_until_idle(monitor: MonitorClient) -> anyhow::Result<()> {
// FIXME: implement something like "wait_until_idle" in the monitor?
let status = monitor.get_status().await?;
- if status.installer_status.is_busy {
+ if !status.progresses.is_empty() {
eprintln!("The Agama service is busy. Waiting for it to be available...");
show_progress(monitor.clone(), true).await;
}
@@ -305,9 +293,8 @@ pub async fn run_command(cli: Cli) -> anyhow::Result<()> {
}
Commands::Install => {
let (client, monitor) = build_clients(api_url, cli.opts.insecure).await?;
- let manager = ManagerHTTPClient::new(client.clone());
let _ = wait_until_idle(monitor.clone()).await;
- install(manager, monitor, 3).await?
+ install(client, monitor).await?
}
Commands::Finish { method } => {
let (client, monitor) = build_clients(api_url, cli.opts.insecure).await?;
diff --git a/rust/agama-cli/src/progress.rs b/rust/agama-cli/src/progress.rs
index 7752d35824..d9b42ba9a3 100644
--- a/rust/agama-cli/src/progress.rs
+++ b/rust/agama-cli/src/progress.rs
@@ -18,22 +18,15 @@
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.
-use agama_lib::{
- monitor::{MonitorClient, MonitorStatus},
- progress::Progress,
-};
-use console::style;
+use agama_lib::monitor::MonitorClient;
+use agama_utils::api::{self, Scope};
use indicatif::{ProgressBar, ProgressStyle};
-use std::time::Duration;
-
-const MANAGER_PROGRESS_OBJECT_PATH: &str = "/org/opensuse/Agama/Manager1";
-const SOFTWARE_PROGRESS_OBJECT_PATH: &str = "/org/opensuse/Agama/Software1";
+use std::{collections::HashMap, time::Duration};
/// Displays the progress on the terminal.
+#[derive(Debug)]
pub struct ProgressMonitor {
monitor: MonitorClient,
- bar: Option,
- current_step: u32,
running: bool,
stop_on_idle: bool,
}
@@ -45,9 +38,7 @@ impl ProgressMonitor {
pub fn new(monitor: MonitorClient) -> Self {
Self {
monitor,
- bar: None,
- current_step: 0,
- running: false,
+ running: true,
stop_on_idle: true,
}
}
@@ -62,13 +53,43 @@ impl ProgressMonitor {
pub async fn run(&mut self) -> anyhow::Result<()> {
let mut updates = self.monitor.subscribe();
let status = self.monitor.get_status().await?;
- self.update(status).await;
+ self.update(&status).await;
+ if !self.running {
+ return Ok(());
+ }
+ let multibar = indicatif::MultiProgress::new();
+ let mut bars: HashMap = HashMap::new();
+ multibar.println("Installaton Tasks:")?;
loop {
if let Ok(status) = updates.recv().await {
- if !self.update(status).await {
+ if !self.update(&status).await {
return Ok(());
}
+
+ let mut active_scopes = vec![];
+ for progress in status.progresses {
+ active_scopes.push(progress.scope);
+ let bar = bars.entry(progress.scope).or_insert_with(|| {
+ let style = ProgressStyle::with_template("{spinner:.green} {msg}").unwrap();
+ let new_bar = ProgressBar::new(progress.size as u64).with_style(style);
+ new_bar.enable_steady_tick(Duration::from_millis(120));
+ multibar.add(new_bar)
+ });
+ bar.set_message(progress.step);
+ bar.set_position(progress.index as u64);
+ }
+ // and finish all that no longer have progress
+ let mut to_remove = vec![];
+ for (scope, bar) in &bars {
+ if !active_scopes.contains(&scope) {
+ bar.finish_with_message("done");
+ to_remove.push(scope.clone());
+ }
+ }
+ for scope in to_remove {
+ bars.remove(&scope);
+ }
}
}
}
@@ -76,63 +97,19 @@ impl ProgressMonitor {
/// Updates the progress.
///
/// It returns true if the monitor should continue.
- async fn update(&mut self, status: MonitorStatus) -> bool {
- if status.progress.get(MANAGER_PROGRESS_OBJECT_PATH).is_none() && self.running {
+ async fn update(&mut self, status: &api::Status) -> bool {
+ if status.progresses.is_empty() && self.running {
self.finish();
if self.stop_on_idle {
return false;
}
}
- if let Some(progress) = status.progress.get(MANAGER_PROGRESS_OBJECT_PATH) {
- self.running = true;
- if self.current_step != progress.current_step {
- self.update_main(&progress).await;
- self.current_step = progress.current_step;
- }
- }
-
- match status.progress.get(SOFTWARE_PROGRESS_OBJECT_PATH) {
- Some(progress) => self.update_bar(progress),
- None => self.remove_bar(),
- }
-
true
}
- /// Updates the main bar.
- async fn update_main(&mut self, progress: &Progress) {
- let counter = format!("[{}/{}]", &progress.current_step, &progress.max_steps);
-
- println!(
- "{} {}",
- style(&counter).bold().green(),
- &progress.current_title
- );
- }
-
- fn update_bar(&mut self, progress: &Progress) {
- let bar = self.bar.get_or_insert_with(|| {
- let style = ProgressStyle::with_template("{spinner:.green} {msg}").unwrap();
- let bar = ProgressBar::new(0).with_style(style);
- bar.enable_steady_tick(Duration::from_millis(120));
- bar
- });
-
- bar.set_length(progress.max_steps.into());
- bar.set_position(progress.current_step.into());
- bar.set_message(progress.current_title.to_owned());
- }
-
- fn remove_bar(&mut self) {
- _ = self.bar.take()
- }
-
/// Stops the representation.
fn finish(&mut self) {
self.running = false;
- if let Some(bar) = self.bar.take() {
- bar.finish_with_message("Done");
- }
}
}
diff --git a/rust/agama-cli/src/questions.rs b/rust/agama-cli/src/questions.rs
index 3cde3e6839..f15f018b42 100644
--- a/rust/agama-cli/src/questions.rs
+++ b/rust/agama-cli/src/questions.rs
@@ -18,10 +18,10 @@
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.
-use agama_lib::{
- connection, http::BaseHTTPClient, proxies::questions::QuestionsProxy,
- questions::http_client::HTTPClient,
-};
+use std::{fs::File, io::BufReader};
+
+use agama_lib::{http::BaseHTTPClient, questions::http_client::HTTPClient};
+use agama_utils::api::question::{AnswerRule, Policy, QuestionSpec};
use anyhow::anyhow;
use clap::{Args, Subcommand, ValueEnum};
@@ -37,7 +37,7 @@ pub enum QuestionsCommands {
/// mode or change the answer in automatic mode.
///
/// Please check Agama documentation for more details and examples:
- /// https://github.com/openSUSE/agama/blob/master/doc/questions.md
+ /// https://github.com/openSUSE/agama/blob/master/doc/questions.
Answers {
/// Path to a file containing the answers in JSON format.
path: String,
@@ -62,53 +62,47 @@ pub enum Modes {
NonInteractive,
}
-async fn set_mode(proxy: QuestionsProxy<'_>, value: Modes) -> anyhow::Result<()> {
- proxy
- .set_interactive(value == Modes::Interactive)
- .await
- .map_err(|e| e.into())
+async fn set_mode(client: HTTPClient, value: Modes) -> anyhow::Result<()> {
+ let policy = match value {
+ Modes::Interactive => Policy::User,
+ Modes::NonInteractive => Policy::Auto,
+ };
+
+ client.set_mode(policy).await?;
+ Ok(())
}
-async fn set_answers(proxy: QuestionsProxy<'_>, path: String) -> anyhow::Result<()> {
- proxy
- .add_answer_file(path.as_str())
- .await
- .map_err(|e| e.into())
+async fn set_answers(client: HTTPClient, path: &str) -> anyhow::Result<()> {
+ let file = File::open(&path)?;
+ let reader = BufReader::new(file);
+ let rules: Vec = serde_json::from_reader(reader)?;
+ client.set_answers(rules).await?;
+ Ok(())
}
-async fn list_questions(client: BaseHTTPClient) -> anyhow::Result<()> {
- let client = HTTPClient::new(client);
- let questions = client.list_questions().await?;
- // FIXME: if performance is bad, we can skip converting json from http to struct and then
- // serialize it, but it won't be pretty string
+async fn list_questions(client: HTTPClient) -> anyhow::Result<()> {
+ let questions = client.get_questions().await?;
let questions_json = serde_json::to_string_pretty(&questions)?;
println!("{}", questions_json);
Ok(())
}
-async fn ask_question(client: BaseHTTPClient) -> anyhow::Result<()> {
- let client = HTTPClient::new(client);
- let question = serde_json::from_reader(std::io::stdin())?;
-
- let created_question = client.create_question(&question).await?;
- let Some(id) = created_question.generic.id else {
- return Err(anyhow!("The created question does not have an ID"));
- };
- let answer = client.get_answer(id).await?;
+async fn ask_question(client: HTTPClient) -> anyhow::Result<()> {
+ let spec: QuestionSpec = serde_json::from_reader(std::io::stdin())?;
+ let question = client.create_question(&spec).await?;
+ let answer = client.get_answer(question.id).await?;
let answer_json = serde_json::to_string_pretty(&answer).map_err(|e| anyhow!(e.to_string()))?;
println!("{}", answer_json);
- client.delete_question(id).await?;
+ client.delete_question(question.id).await?;
Ok(())
}
pub async fn run(client: BaseHTTPClient, subcommand: QuestionsCommands) -> anyhow::Result<()> {
- let connection = connection().await?;
- let proxy = QuestionsProxy::new(&connection).await?;
-
+ let client = HTTPClient::new(client);
match subcommand {
- QuestionsCommands::Mode(value) => set_mode(proxy, value.value).await,
- QuestionsCommands::Answers { path } => set_answers(proxy, path).await,
+ QuestionsCommands::Mode(value) => set_mode(client, value.value).await,
+ QuestionsCommands::Answers { path } => set_answers(client, &path).await,
QuestionsCommands::List => list_questions(client).await,
QuestionsCommands::Ask => ask_question(client).await,
}
diff --git a/rust/agama-files/Cargo.toml b/rust/agama-files/Cargo.toml
new file mode 100644
index 0000000000..ce812bd900
--- /dev/null
+++ b/rust/agama-files/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "agama-files"
+version = "0.1.0"
+rust-version.workspace = true
+edition.workspace = true
+
+[dependencies]
+agama-software = { version = "0.1.0", path = "../agama-software" }
+agama-utils = { path = "../agama-utils" }
+async-trait = "0.1.89"
+tempfile = "3.23.0"
+thiserror = "2.0.17"
+tokio = { version = "1.48.0", features = ["sync"] }
+tracing = "0.1.41"
+
+[dev-dependencies]
+tokio-test = "0.4.4"
+test-context = "0.4.1"
+serde_json = "1.0.145"
diff --git a/rust/agama-files/src/lib.rs b/rust/agama-files/src/lib.rs
new file mode 100644
index 0000000000..941fce548f
--- /dev/null
+++ b/rust/agama-files/src/lib.rs
@@ -0,0 +1,157 @@
+// Copyright (c) [2025] SUSE LLC
+//
+// All Rights Reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 2 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, contact SUSE LLC.
+//
+// To contact SUSE LLC about this file by physical or electronic mail, you may
+// find current contact information at www.suse.com.
+
+//! This crate implements the support for handling files and scripts in Agama.
+
+pub mod service;
+pub use service::{Service, Starter};
+
+pub mod message;
+mod runner;
+pub use runner::ScriptsRunner;
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use agama_software::test_utils::start_service as start_software_service;
+ use agama_utils::{
+ actor::Handler,
+ api::{
+ event,
+ files::{scripts::ScriptsGroup, Config},
+ Event,
+ },
+ issue, progress, question,
+ };
+ use tempfile::TempDir;
+ use test_context::{test_context, AsyncTestContext};
+ use tokio::sync::broadcast;
+
+ use crate::{message, service::Error, Service};
+
+ struct Context {
+ handler: Handler,
+ tmp_dir: TempDir,
+ events_rx: event::Receiver,
+ }
+
+ impl AsyncTestContext for Context {
+ async fn setup() -> Context {
+ // Set the PATH
+ let old_path = std::env::var("PATH").unwrap();
+ let bin_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../share/bin");
+ std::env::set_var("PATH", format!("{}:{}", &bin_dir.display(), &old_path));
+
+ // Set up the chroot
+ let tmp_dir = TempDir::with_prefix("test").unwrap();
+ std::fs::create_dir_all(tmp_dir.path().join("usr/bin")).unwrap();
+ std::fs::copy("/usr/bin/install", tmp_dir.path().join("usr/bin/install")).unwrap();
+
+ // Set up the service
+ let (events_tx, events_rx) = broadcast::channel::(16);
+ let issues = issue::Service::starter(events_tx.clone()).start();
+ let progress = progress::Service::starter(events_tx.clone()).start();
+ let questions = question::start(events_tx.clone()).await.unwrap();
+ let software = start_software_service(
+ events_tx.clone(),
+ issues,
+ progress.clone(),
+ questions.clone(),
+ )
+ .await;
+ let handler = Service::starter(progress, questions, software)
+ .with_scripts_workdir(tmp_dir.path())
+ .with_install_dir(tmp_dir.path())
+ .start()
+ .await
+ .unwrap();
+ Context {
+ handler,
+ tmp_dir,
+ events_rx,
+ }
+ }
+ }
+
+ #[test_context(Context)]
+ #[tokio::test]
+ async fn test_add_and_run_scripts(ctx: &mut Context) -> Result<(), Error> {
+ let test_file_1 = ctx.tmp_dir.path().join("file-1.txt");
+ let test_file_2 = ctx.tmp_dir.path().join("file-2.txt");
+
+ let pre_script_json = format!(
+ "{{ \"name\": \"pre.sh\", \"content\": \"#!/usr/bin/bash\\nset -x\\ntouch {}\" }}",
+ test_file_1.to_str().unwrap()
+ );
+
+ let init_script_json = format!(
+ "{{ \"name\": \"init.sh\", \"content\": \"#!/usr/bin/bash\\ntouch {}\" }}",
+ test_file_2.to_str().unwrap()
+ );
+
+ let config = format!(
+ "{{ \"scripts\": {{ \"pre\": [{}], \"init\": [{}] }} }}",
+ pre_script_json, init_script_json
+ );
+
+ let config: Config = serde_json::from_str(&config).unwrap();
+ ctx.handler
+ .call(message::SetConfig::with(config))
+ .await
+ .unwrap();
+
+ ctx.handler
+ .call(message::RunScripts::new(ScriptsGroup::Pre))
+ .await
+ .unwrap();
+
+ // Wait until the scripts are executed.
+ while let Ok(event) = ctx.events_rx.recv().await {
+ if matches!(event, Event::ProgressFinished { scope: _ }) {
+ break;
+ }
+ }
+ // Check that only the pre-script ran
+ assert!(std::fs::exists(&test_file_1).unwrap());
+ assert!(!std::fs::exists(&test_file_2).unwrap());
+ Ok(())
+ }
+
+ #[test_context(Context)]
+ #[tokio::test]
+ async fn test_add_and_write_files(ctx: &mut Context) -> Result<(), Error> {
+ let config =
+ r#"{ "files": [{ "destination": "/etc/README.md", "content": "Some text" }] }"#;
+
+ let config: Config = serde_json::from_str(&config).unwrap();
+ ctx.handler
+ .call(message::SetConfig::with(config))
+ .await
+ .unwrap();
+
+ ctx.handler.call(message::WriteFiles).await.unwrap();
+
+ // Check that the file exists
+ let expected_path = ctx.tmp_dir.path().join("etc/README.md");
+ assert!(std::fs::exists(&expected_path).unwrap());
+ Ok(())
+ }
+}
diff --git a/rust/agama-files/src/message.rs b/rust/agama-files/src/message.rs
new file mode 100644
index 0000000000..54d24ad160
--- /dev/null
+++ b/rust/agama-files/src/message.rs
@@ -0,0 +1,67 @@
+// Copyright (c) [2025] SUSE LLC
+//
+// All Rights Reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 2 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, contact SUSE LLC.
+//
+// To contact SUSE LLC about this file by physical or electronic mail, you may
+// find current contact information at www.suse.com.
+
+use agama_utils::{
+ actor::Message,
+ api::files::{scripts::ScriptsGroup, Config},
+};
+
+#[derive(Clone)]
+pub struct SetConfig {
+ pub config: Option,
+}
+
+impl Message for SetConfig {
+ type Reply = ();
+}
+
+impl SetConfig {
+ pub fn new(config: Option) -> Self {
+ Self { config }
+ }
+
+ pub fn with(config: Config) -> Self {
+ Self {
+ config: Some(config),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct RunScripts {
+ pub group: ScriptsGroup,
+}
+
+impl RunScripts {
+ pub fn new(group: ScriptsGroup) -> Self {
+ RunScripts { group }
+ }
+}
+
+impl Message for RunScripts {
+ type Reply = ();
+}
+
+#[derive(Clone)]
+pub struct WriteFiles;
+
+impl Message for WriteFiles {
+ type Reply = ();
+}
diff --git a/rust/agama-files/src/runner.rs b/rust/agama-files/src/runner.rs
new file mode 100644
index 0000000000..37bb58a19d
--- /dev/null
+++ b/rust/agama-files/src/runner.rs
@@ -0,0 +1,462 @@
+// Copyright (c) [2024-2025] SUSE LLC
+//
+// All Rights Reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 2 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, contact SUSE LLC.
+//
+// To contact SUSE LLC about this file by physical or electronic mail, you may
+// find current contact information at www.suse.com.
+
+use std::{
+ fs::{self, File},
+ io::{self, Read, Seek, SeekFrom, Write},
+ os::unix::fs::symlink,
+ path::{Path, PathBuf},
+ process::ExitStatus,
+};
+
+use agama_utils::{
+ actor::Handler,
+ api::{files::Script, question::QuestionSpec, Scope},
+ command::{create_log_file, run_with_retry},
+ progress,
+ question::{self, ask_question},
+};
+use tokio::process;
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+ #[error("The script failed")]
+ Script { status: ExitStatus, stderr: String },
+ #[error(transparent)]
+ Question(#[from] question::AskError),
+}
+
+// Relative path to the resolv.conf file.
+const RESOLV_CONF_PATH: &str = "etc/resolv.conf";
+// Relative path to the NetworkManager resolv.conf file.
+const NM_RESOLV_CONF_PATH: &str = "run/NetworkManager/resolv.conf";
+
+/// Implements the logic to run a script.
+///
+/// It takes care of running the script, reporting errors (and asking whether to retry) and write
+/// the logs.
+pub struct ScriptsRunner {
+ progress: Handler,
+ questions: Handler,
+ install_dir: PathBuf,
+ workdir: PathBuf,
+}
+
+impl ScriptsRunner {
+ /// Creates a new runner.
+ ///
+ /// * `install_dir`: directory where the system is being installed. It is relevant for
+ /// chrooted scripts.
+ /// * `workdir`: scripts work directory.
+ /// * `progress`: handler to report the progress.
+ /// * `questions`: handler to interact with the user.
+ pub fn new>(
+ install_dir: P,
+ workdir: P,
+ progress: Handler,
+ questions: Handler,
+ ) -> Self {
+ Self {
+ progress,
+ questions,
+ install_dir: install_dir.as_ref().to_path_buf(),
+ workdir: workdir.as_ref().to_path_buf(),
+ }
+ }
+
+ /// Runs the given scripts.
+ ///
+ /// It runs each script. If something goes wrong, it reports the problem to the user through
+ /// the questions mechanism.
+ ///
+ /// * `scripts`: scripts to run.
+ pub async fn run(&self, scripts: &[&Script]) -> Result<(), Error> {
+ self.start_progress(scripts);
+
+ let mut resolv_linked = false;
+ if scripts.iter().any(|s| s.chroot()) {
+ resolv_linked = self.link_resolv()?;
+ }
+
+ for script in scripts {
+ _ = self
+ .progress
+ .cast(progress::message::Next::new(Scope::Files));
+ self.run_script(script).await?;
+ }
+
+ if resolv_linked {
+ self.unlink_resolv();
+ }
+
+ _ = self
+ .progress
+ .cast(progress::message::Finish::new(Scope::Files));
+ Ok(())
+ }
+
+ /// Runs the script.
+ ///
+ /// If the script fails, it asks the user whether it should try again.
+ async fn run_script(&self, script: &Script) -> Result<(), Error> {
+ loop {
+ let path = self
+ .workdir
+ .join(script.group().to_string())
+ .join(script.name());
+
+ let Err(error) = self.run_command(&path, script.chroot()).await else {
+ return Ok(());
+ };
+
+ if !self.should_retry(&script, error).await? {
+ return Ok(());
+ }
+ }
+ }
+
+ /// Asks the user whether it should try to run the script again.
+ async fn should_retry(&self, script: &Script, error: Error) -> Result {
+ let text = format!(
+ "Running the script '{}' failed. Do you want to try again?",
+ script.name()
+ );
+ let mut question = QuestionSpec::new(&text, "scripts.retry").with_yes_no_actions();
+
+ if let Error::Script { status, stderr } = error {
+ let exit_status = status
+ .code()
+ .map(|c| c.to_string())
+ .unwrap_or("unknown".to_string());
+ question = question.with_data(&[
+ ("name", script.name()),
+ ("stderr", &stderr),
+ ("exit_status", &exit_status),
+ ]);
+ }
+
+ let answer = ask_question(&self.questions, question).await?;
+ return Ok(answer.action == "Yes");
+ }
+
+ /// Runs the script at the given path.
+ ///
+ /// * `path`: script's path.
+ /// * `chroot`: whether to run the script in a chroot.
+ async fn run_command>(&self, path: P, chroot: bool) -> Result<(), Error> {
+ const STDERR_SIZE: u64 = 512;
+
+ let path = path.as_ref();
+ let stdout_file = path.with_extension("stdout");
+ let stderr_file = path.with_extension("stderr");
+
+ let mut command = if chroot {
+ let mut command = process::Command::new("chroot");
+ command.args([&self.install_dir, path]);
+ command
+ } else {
+ process::Command::new(path)
+ };
+
+ command
+ .stdout(create_log_file(&stdout_file)?)
+ .stderr(create_log_file(&stderr_file)?);
+
+ let output = run_with_retry(command)
+ .await
+ .inspect_err(|e| println!("Error executing the script: {e}"))?;
+
+ if let Some(code) = output.status.code() {
+ let mut file = create_log_file(&path.with_extension("exit"))?;
+ write!(&mut file, "{}", code)?;
+ }
+
+ if !output.status.success() {
+ let stderr = Self::read_n_last_bytes(&stderr_file, STDERR_SIZE)?;
+ return Err(Error::Script {
+ status: output.status,
+ stderr,
+ });
+ }
+
+ Ok(())
+ }
+
+ /// Ancillary function to start the progress.
+ fn start_progress(&self, scripts: &[&Script]) {
+ let steps: Vec<_> = scripts
+ .iter()
+ .map(|s| format!("Running user script '{}'", s.name()))
+ .collect();
+ let progress_action = progress::message::StartWithSteps::new(Scope::Files, steps);
+ _ = self.progress.cast(progress_action);
+ }
+
+ /// Reads the last n bytes of the file and returns them as a string.
+ fn read_n_last_bytes(path: &Path, n_bytes: u64) -> io::Result {
+ let mut file = File::open(path)?;
+ let file_size = file.metadata()?.len();
+ let offset = file_size.saturating_sub(n_bytes);
+ file.seek(SeekFrom::Start(offset))?;
+ let bytes_to_read = (file_size - offset) as usize;
+ let mut buffer = Vec::with_capacity(bytes_to_read);
+ _ = file.read_to_end(&mut buffer)?;
+ let string = String::from_utf8_lossy(&buffer);
+ Ok(string.into_owned())
+ }
+
+ /// Make sures that the resolv.conf is linked and returns true if any action was needed.
+ ///
+ /// It returns false if the resolv.conf was already linked and no action was required.
+ fn link_resolv(&self) -> Result {
+ let original = self.install_dir.join(NM_RESOLV_CONF_PATH);
+ let link = self.resolv_link_path();
+
+ if fs::exists(&link)? || !fs::exists(&original)? {
+ return Ok(false);
+ }
+
+ // It assumes that the directory of the resolv.conf (/etc) exists.
+ symlink(original.as_path(), link.as_path())?;
+ Ok(true)
+ }
+
+ /// Removes the resolv.conf file from the chroot.
+ fn unlink_resolv(&self) {
+ let link = self.resolv_link_path();
+ if let Err(error) = fs::remove_file(link) {
+ tracing::warn!("Could not remove the resolv.conf link: {error}");
+ }
+ }
+
+ fn resolv_link_path(&self) -> PathBuf {
+ self.install_dir.join(RESOLV_CONF_PATH)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use agama_utils::{
+ api::{
+ event,
+ files::{BaseScript, FileSource, PostScript},
+ question::Answer,
+ Event,
+ },
+ question::test_utils::wait_for_question,
+ };
+ use tempfile::TempDir;
+ use test_context::{test_context, AsyncTestContext};
+ use tokio::sync::broadcast;
+
+ use super::*;
+
+ struct Context {
+ // runner: ScriptsRunner,
+ install_dir: PathBuf,
+ workdir: PathBuf,
+ progress: Handler,
+ questions: Handler,
+ events_rx: event::Receiver,
+ tmp_dir: TempDir,
+ }
+
+ impl AsyncTestContext for Context {
+ async fn setup() -> Context {
+ // Set the PATH
+ let old_path = std::env::var("PATH").unwrap();
+ let bin_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../share/bin");
+ std::env::set_var("PATH", format!("{}:{}", &bin_dir.display(), &old_path));
+
+ let tmp_dir = TempDir::with_prefix("scripts-").expect("a temporary directory");
+
+ let (events_tx, events_rx) = broadcast::channel::(16);
+ let install_dir = tmp_dir.path().join("mnt");
+ let workdir = tmp_dir.path().join("scripts");
+ let questions = question::start(events_tx.clone()).await.unwrap();
+ let progress = progress::Service::starter(events_tx.clone()).start();
+
+ Context {
+ events_rx,
+ install_dir,
+ workdir,
+ progress,
+ questions,
+ // runner,
+ tmp_dir,
+ }
+ }
+ }
+
+ impl Context {
+ pub fn runner(&self) -> ScriptsRunner {
+ ScriptsRunner::new(
+ self.install_dir.clone(),
+ self.workdir.clone(),
+ self.progress.clone(),
+ self.questions.clone(),
+ )
+ }
+
+ pub fn setup_script(&self, content: &str, chroot: bool) -> Script {
+ let base = BaseScript {
+ name: "test.sh".to_string(),
+ source: FileSource::Text {
+ content: content.to_string(),
+ },
+ };
+ let script = Script::Post(PostScript {
+ base,
+ chroot: Some(chroot),
+ });
+ script
+ .write(&self.workdir)
+ .expect("Could not write the script");
+ script
+ }
+
+ // Set up a fake chroot.
+ pub fn setup_chroot(&self) -> std::io::Result<()> {
+ let nm_dir = self.install_dir.join("run/NetworkManager");
+ fs::create_dir_all(&nm_dir)?;
+ fs::create_dir_all(self.install_dir.join("etc"))?;
+
+ let mut file = File::create(nm_dir.join("resolv.conf"))?;
+ file.write_all(b"nameserver 127.0.0.1\n")?;
+
+ Ok(())
+ }
+
+ // Return the content of a script result file.
+ pub fn result_content(&self, script_type: &str, name: &str) -> String {
+ let path = &self.workdir.join(script_type).join(name);
+ let body: Vec = std::fs::read(path).unwrap();
+ String::from_utf8(body).unwrap()
+ }
+ }
+
+ #[test_context(Context)]
+ #[tokio::test]
+ async fn test_run_scripts_success(ctx: &mut Context) -> Result<(), Error> {
+ let file = ctx.tmp_dir.path().join("file-1.txt");
+ let content = format!(
+ "#!/usr/bin/bash\necho hello\necho error >&2\ntouch {}",
+ file.display()
+ );
+ let script = ctx.setup_script(&content, false);
+ let scripts = vec![&script];
+
+ let runner = ctx.runner();
+ runner.run(&scripts).await.unwrap();
+
+ assert_eq!(ctx.result_content("post", "test.stdout"), "hello\n");
+ assert_eq!(ctx.result_content("post", "test.stderr"), "error\n");
+ assert_eq!(ctx.result_content("post", "test.exit"), "0");
+
+ assert!(std::fs::exists(file).unwrap());
+ Ok(())
+ }
+
+ #[test_context(Context)]
+ #[tokio::test]
+ async fn test_chrooted_script(ctx: &mut Context) -> Result<(), Error> {
+ ctx.setup_chroot()?;
+
+ // Ideally, the script should check the existence of /etc/resolv.conf.
+ // However, it does not run on a real chroot (see share/bin/chroot),
+ // so it needs the whole path.
+ let file = ctx.install_dir.join("etc/resolv.conf");
+ let content = format!("#!/usr/bin/bash\ntest -h {} && echo exists", file.display());
+ let script = ctx.setup_script(&content, true);
+
+ let runner = ctx.runner();
+ let scripts = vec![&script];
+ runner.run(&scripts).await.unwrap();
+
+ // It runs successfully because the resolv.conf link exists.
+ assert_eq!(ctx.result_content("post", "test.stdout"), "exists\n");
+
+ Ok(())
+ }
+
+ #[test_context(Context)]
+ #[tokio::test]
+ async fn test_run_scripts_retry(ctx: &mut Context) -> Result<(), Error> {
+ let file = ctx.tmp_dir.path().join("file-1.txt");
+ let content = format!(
+ "#!/usr/bin/bash\necho \"hello\"\necho \"line\" >>{}\nagama-unknown\n",
+ file.display()
+ );
+ let script = ctx.setup_script(&content, false);
+
+ let runner = ctx.runner();
+ tokio::task::spawn(async move {
+ let scripts = vec![&script];
+ _ = runner.run(&scripts).await;
+ });
+
+ // Retry
+ let id = wait_for_question(&mut ctx.events_rx)
+ .await
+ .expect("Did not receive a question");
+ _ = ctx.questions.cast(question::message::Answer {
+ id,
+ answer: Answer::new("Yes"),
+ });
+
+ // Check the question content
+ let questions = ctx
+ .questions
+ .call(question::message::Get)
+ .await
+ .expect("Could not get the questions");
+ let question = questions.first().unwrap();
+ assert_eq!(question.spec.data.get("name"), Some(&"test.sh".to_string()));
+ assert_eq!(
+ question.spec.data.get("exit_status"),
+ Some(&"127".to_string())
+ );
+ let stderr = question.spec.data.get("stderr").unwrap();
+ assert!(stderr.contains("agama-unknown"));
+
+ // Do not retry
+ let id = wait_for_question(&mut ctx.events_rx)
+ .await
+ .expect("Did not receive a question");
+ _ = ctx.questions.cast(question::message::Answer {
+ id,
+ answer: Answer::new("No"),
+ });
+
+ // Check the generated files
+ let path = &ctx.workdir.join("post").join("test.stderr");
+ let body: Vec = std::fs::read(path).unwrap();
+ let body = String::from_utf8(body).unwrap();
+ assert!(body.contains("agama-unknown"));
+
+ let body: Vec = std::fs::read(&file).unwrap();
+ let body = String::from_utf8(body).unwrap();
+ assert_eq!("line\nline\n", body);
+
+ Ok(())
+ }
+}
diff --git a/rust/agama-files/src/service.rs b/rust/agama-files/src/service.rs
new file mode 100644
index 0000000000..62393fb9b4
--- /dev/null
+++ b/rust/agama-files/src/service.rs
@@ -0,0 +1,223 @@
+// Copyright (c) [2025] SUSE LLC
+//
+// All Rights Reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 2 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, contact SUSE LLC.
+//
+// To contact SUSE LLC about this file by physical or electronic mail, you may
+// find current contact information at www.suse.com.
+
+use std::{
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+
+use agama_software::{self as software, Resolvable, ResolvableType};
+use agama_utils::{
+ actor::{self, Actor, Handler, MessageHandler},
+ api::files::{
+ scripts::{self, ScriptsRepository},
+ user_file, ScriptsConfig, UserFile,
+ },
+ progress, question,
+};
+use async_trait::async_trait;
+use tokio::sync::Mutex;
+
+use crate::{message, ScriptsRunner};
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error(transparent)]
+ Files(#[from] user_file::Error),
+ #[error(transparent)]
+ Scripts(#[from] scripts::Error),
+ #[error(transparent)]
+ Software(#[from] software::service::Error),
+ #[error(transparent)]
+ Actor(#[from] actor::Error),
+}
+
+const DEFAULT_SCRIPTS_DIR: &str = "/run/agama/scripts";
+const DEFAULT_INSTALL_DIR: &str = "/mnt";
+
+/// Builds and spawns the files service.
+///
+/// This structs allows to build a files service.
+pub struct Starter {
+ scripts_workdir: PathBuf,
+ install_dir: PathBuf,
+ software: Handler,
+ progress: Handler,
+ questions: Handler,
+}
+
+impl Starter {
+ /// Creates a new starter.
+ ///
+ /// * `events`: channel to emit the [localization-specific events](crate::Event).
+ pub fn new(
+ progress: Handler,
+ questions: Handler,
+ software: Handler,
+ ) -> Self {
+ Self {
+ software,
+ progress,
+ questions,
+ scripts_workdir: PathBuf::from(DEFAULT_SCRIPTS_DIR),
+ install_dir: PathBuf::from(DEFAULT_INSTALL_DIR),
+ }
+ }
+
+ /// Starts the service and returns the handler to communicate with it.
+ pub async fn start(self) -> Result, Error> {
+ let scripts = ScriptsRepository::new(self.scripts_workdir);
+ let service = Service {
+ progress: self.progress,
+ questions: self.questions,
+ software: self.software,
+ scripts: Arc::new(Mutex::new(scripts)),
+ files: vec![],
+ install_dir: self.install_dir,
+ };
+ let handler = actor::spawn(service);
+ Ok(handler)
+ }
+
+ pub fn with_scripts_workdir>(mut self, workdir: P) -> Self {
+ self.scripts_workdir = PathBuf::from(workdir.as_ref());
+ self
+ }
+
+ pub fn with_install_dir>(mut self, install_dir: P) -> Self {
+ self.install_dir = PathBuf::from(install_dir.as_ref());
+ self
+ }
+}
+
+pub struct Service {
+ software: Handler,
+ progress: Handler,
+ questions: Handler,
+ scripts: Arc>,
+ files: Vec,
+ install_dir: PathBuf,
+}
+
+impl Service {
+ pub fn starter(
+ progress: Handler,
+ questions: Handler,
+ software: Handler,
+ ) -> Starter {
+ Starter::new(progress, questions, software)
+ }
+
+ pub async fn clear_scripts(&mut self) -> Result<(), Error> {
+ let mut repo = self.scripts.lock().await;
+ repo.clear()?;
+ Ok(())
+ }
+
+ pub async fn add_scripts(&mut self, config: ScriptsConfig) -> Result<(), Error> {
+ let mut repo = self.scripts.lock().await;
+ if let Some(scripts) = config.pre {
+ for pre in scripts {
+ repo.add(pre.into())?;
+ }
+ }
+
+ if let Some(scripts) = config.post_partitioning {
+ for post in scripts {
+ repo.add(post.into())?;
+ }
+ }
+
+ if let Some(scripts) = config.post {
+ for post in scripts {
+ repo.add(post.into())?;
+ }
+ }
+
+ let mut packages = vec![];
+ if let Some(scripts) = config.init {
+ for init in scripts {
+ repo.add(init.into())?;
+ }
+ packages.push(Resolvable::new("agama-scripts", ResolvableType::Package));
+ }
+ _ = self
+ .software
+ .call(agama_software::message::SetResolvables::new(
+ "agama-scripts".to_string(),
+ packages,
+ ))
+ .await?;
+ Ok(())
+ }
+}
+
+impl Actor for Service {
+ type Error = Error;
+}
+
+#[async_trait]
+impl MessageHandler for Service {
+ async fn handle(&mut self, message: message::SetConfig) -> Result<(), Error> {
+ let config = message.config.unwrap_or_default();
+
+ self.clear_scripts().await?;
+ if let Some(scripts) = config.scripts {
+ self.add_scripts(scripts.clone()).await?;
+ }
+
+ if let Some(files) = config.files {
+ self.files = files;
+ } else {
+ self.files.clear();
+ }
+
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl MessageHandler for Service {
+ async fn handle(&mut self, message: message::RunScripts) -> Result<(), Error> {
+ let scripts = self.scripts.clone();
+ let install_dir = self.install_dir.clone();
+ let progress = self.progress.clone();
+ let questions = self.questions.clone();
+
+ tokio::task::spawn(async move {
+ let scripts = scripts.lock().await;
+ let workdir = scripts.workdir.clone();
+ let to_run = scripts.by_group(message.group).clone();
+ let runner = ScriptsRunner::new(install_dir, workdir, progress, questions);
+ runner.run(&to_run).await.unwrap();
+ });
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl MessageHandler for Service {
+ async fn handle(&mut self, _message: message::WriteFiles) -> Result<(), Error> {
+ for file in &self.files {
+ file.write(&self.install_dir).await?;
+ }
+ Ok(())
+ }
+}
diff --git a/rust/agama-hostname/Cargo.toml b/rust/agama-hostname/Cargo.toml
new file mode 100644
index 0000000000..187eefbf54
--- /dev/null
+++ b/rust/agama-hostname/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "agama-hostname"
+version = "0.1.0"
+rust-version.workspace = true
+edition.workspace = true
+
+[dependencies]
+agama-utils = { version = "0.1.0", path = "../agama-utils" }
+anyhow = "1.0.100"
+async-trait = "0.1.89"
+tempfile = "3.23.0"
+thiserror = "2.0.17"
+tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "sync"] }
+tokio-stream = "0.1.17"
+tokio-test = "0.4.4"
+tracing = "0.1.43"
+zbus = "5.12.0"
+
+[dev-dependencies]
+tempfile = "3.23.0"
+test-context = "0.4.1"
diff --git a/rust/agama-hostname/src/dbus.rs b/rust/agama-hostname/src/dbus.rs
new file mode 100644
index 0000000000..5c4630d61f
--- /dev/null
+++ b/rust/agama-hostname/src/dbus.rs
@@ -0,0 +1,155 @@
+//! # D-Bus interface proxy for: `org.freedesktop.hostname1`
+//!
+//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data.
+//! Source: `Interface '/org/freedesktop/hostname1' from service 'org.freedesktop.hostname1' on system bus`.
+//!
+//! You may prefer to adapt it, instead of using it verbatim.
+//!
+//! More information can be found in the [Writing a client proxy] section of the zbus
+//! documentation.
+//!
+//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
+//! following zbus API can be used:
+//!
+//! * [`zbus::fdo::PeerProxy`]
+//! * [`zbus::fdo::IntrospectableProxy`]
+//! * [`zbus::fdo::PropertiesProxy`]
+//!
+//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
+//!
+//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
+//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
+use zbus::proxy;
+#[proxy(
+ interface = "org.freedesktop.hostname1",
+ default_service = "org.freedesktop.hostname1",
+ default_path = "/org/freedesktop/hostname1"
+)]
+pub trait Hostname1 {
+ /// Describe method
+ fn describe(&self) -> zbus::Result;
+
+ /// GetHardwareSerial method
+ fn get_hardware_serial(&self) -> zbus::Result;
+
+ /// GetProductUUID method
+ #[zbus(name = "GetProductUUID")]
+ fn get_product_uuid(&self, interactive: bool) -> zbus::Result>;
+
+ /// SetChassis method
+ fn set_chassis(&self, chassis: &str, interactive: bool) -> zbus::Result<()>;
+
+ /// SetDeployment method
+ fn set_deployment(&self, deployment: &str, interactive: bool) -> zbus::Result<()>;
+
+ /// SetHostname method
+ fn set_hostname(&self, hostname: &str, interactive: bool) -> zbus::Result<()>;
+
+ /// SetIconName method
+ fn set_icon_name(&self, icon: &str, interactive: bool) -> zbus::Result<()>;
+
+ /// SetLocation method
+ fn set_location(&self, location: &str, interactive: bool) -> zbus::Result<()>;
+
+ /// SetPrettyHostname method
+ fn set_pretty_hostname(&self, hostname: &str, interactive: bool) -> zbus::Result<()>;
+
+ /// SetStaticHostname method
+ fn set_static_hostname(&self, hostname: &str, interactive: bool) -> zbus::Result<()>;
+
+ /// BootID property
+ #[zbus(property, name = "BootID")]
+ fn boot_id(&self) -> zbus::Result>;
+
+ /// Chassis property
+ #[zbus(property)]
+ fn chassis(&self) -> zbus::Result;
+
+ /// DefaultHostname property
+ #[zbus(property)]
+ fn default_hostname(&self) -> zbus::Result;
+
+ /// Deployment property
+ #[zbus(property)]
+ fn deployment(&self) -> zbus::Result;
+
+ /// FirmwareDate property
+ #[zbus(property)]
+ fn firmware_date(&self) -> zbus::Result;
+
+ /// FirmwareVendor property
+ #[zbus(property)]
+ fn firmware_vendor(&self) -> zbus::Result;
+
+ /// FirmwareVersion property
+ #[zbus(property)]
+ fn firmware_version(&self) -> zbus::Result;
+
+ /// HardwareModel property
+ #[zbus(property)]
+ fn hardware_model(&self) -> zbus::Result;
+
+ /// HardwareVendor property
+ #[zbus(property)]
+ fn hardware_vendor(&self) -> zbus::Result;
+
+ /// HomeURL property
+ #[zbus(property, name = "HomeURL")]
+ fn home_url(&self) -> zbus::Result;
+
+ /// Hostname property
+ #[zbus(property)]
+ fn hostname(&self) -> zbus::Result;
+
+ /// HostnameSource property
+ #[zbus(property)]
+ fn hostname_source(&self) -> zbus::Result;
+
+ /// IconName property
+ #[zbus(property)]
+ fn icon_name(&self) -> zbus::Result;
+
+ /// KernelName property
+ #[zbus(property)]
+ fn kernel_name(&self) -> zbus::Result;
+
+ /// KernelRelease property
+ #[zbus(property)]
+ fn kernel_release(&self) -> zbus::Result;
+
+ /// KernelVersion property
+ #[zbus(property)]
+ fn kernel_version(&self) -> zbus::Result;
+
+ /// Location property
+ #[zbus(property)]
+ fn location(&self) -> zbus::Result;
+
+ /// MachineID property
+ #[zbus(property, name = "MachineID")]
+ fn machine_id(&self) -> zbus::Result>;
+
+ /// OperatingSystemCPEName property
+ #[zbus(property, name = "OperatingSystemCPEName")]
+ fn operating_system_cpename(&self) -> zbus::Result;
+
+ /// OperatingSystemPrettyName property
+ #[zbus(property)]
+ fn operating_system_pretty_name(&self) -> zbus::Result;
+
+ /// OperatingSystemSupportEnd property
+ #[zbus(property)]
+ fn operating_system_support_end(&self) -> zbus::Result;
+
+ /// PrettyHostname property
+ #[zbus(property)]
+ fn pretty_hostname(&self) -> zbus::Result;
+
+ /// StaticHostname property
+ #[zbus(property)]
+ fn static_hostname(&self) -> zbus::Result;
+
+ /// VSockCID property
+ #[zbus(property, name = "VSockCID")]
+ fn vsock_cid(&self) -> zbus::Result;
+}
diff --git a/rust/agama-hostname/src/lib.rs b/rust/agama-hostname/src/lib.rs
new file mode 100644
index 0000000000..a4a09b8c4f
--- /dev/null
+++ b/rust/agama-hostname/src/lib.rs
@@ -0,0 +1,114 @@
+// Copyright (c) [2025] SUSE LLC
+//
+// All Rights Reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 2 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, contact SUSE LLC.
+//
+// To contact SUSE LLC about this file by physical or electronic mail, you may
+// find current contact information at www.suse.com.
+
+//! This crate implements the support for hostname handling in Agama.
+//! It takes care of setting the hostname for Agama itself and copying it
+//! to the target system in case of an static one.
+//!
+//! * The [Proposal] struct that describes how the system will look like after
+//! the installation.
+//! * The [SystemInfo] which includes information about the system
+//! where Agama is running.
+//! * An [specific event type](Event) for hostname-related events.
+//!
+//! The service can be started by calling the [start_service] function, which
+//! returns a [agama_utils::actors::ActorHandler] to interact with the system.
+
+pub mod service;
+pub use service::{Service, Starter};
+
+mod dbus;
+pub mod message;
+mod model;
+pub use model::{Model, ModelAdapter};
+mod monitor;
+pub mod test_utils;
+
+#[cfg(test)]
+mod tests {
+ use crate::{message, service::Service, test_utils::start_service};
+
+ use agama_utils::{
+ actor::Handler,
+ api::{self, event::Event, scope::Scope},
+ issue,
+ };
+ use test_context::{test_context, AsyncTestContext};
+ use tokio::sync::broadcast;
+
+ struct Context {
+ events_rx: broadcast::Receiver,
+ handler: Handler,
+ issues: Handler,
+ }
+
+ impl AsyncTestContext for Context {
+ async fn setup() -> Context {
+ let (events_tx, events_rx) = broadcast::channel::(16);
+ let issues = issue::Service::starter(events_tx.clone()).start();
+
+ let handler = start_service(events_tx, issues.clone()).await;
+
+ Self {
+ events_rx,
+ handler,
+ issues,
+ }
+ }
+ }
+
+ #[test_context(Context)]
+ #[tokio::test]
+ async fn test_get_and_set_config(ctx: &mut Context) -> Result<(), Box> {
+ let mut config = ctx.handler.call(message::GetConfig).await.unwrap();
+ assert_eq!(config.r#static, Some("test-hostname".to_string()));
+ config.r#static = Some("".to_string());
+ config.hostname = Some("test".to_string());
+
+ ctx.handler.call(message::SetConfig::with(config)).await?;
+
+ let updated = ctx.handler.call(message::GetConfig).await?;
+ assert_eq!(
+ &updated,
+ &api::hostname::Config {
+ r#static: Some("".to_string()),
+ hostname: Some("test".to_string())
+ }
+ );
+
+ let proposal = ctx.handler.call(message::GetProposal).await?;
+ assert!(proposal.is_some());
+
+ let event = ctx
+ .events_rx
+ .recv()
+ .await
+ .expect("Did not receive the event");
+
+ assert!(matches!(
+ event,
+ Event::ProposalChanged {
+ scope: Scope::Hostname
+ }
+ ));
+
+ Ok(())
+ }
+}
diff --git a/rust/agama-hostname/src/message.rs b/rust/agama-hostname/src/message.rs
new file mode 100644
index 0000000000..94caa29bd0
--- /dev/null
+++ b/rust/agama-hostname/src/message.rs
@@ -0,0 +1,99 @@
+// Copyright (c) [2025] SUSE LLC
+//
+// All Rights Reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 2 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, contact SUSE LLC.
+//
+// To contact SUSE LLC about this file by physical or electronic mail, you may
+// find current contact information at www.suse.com.
+
+use agama_utils::{
+ actor::Message,
+ api::hostname::{Config, Proposal, SystemInfo},
+};
+
+#[derive(Clone)]
+pub struct GetSystem;
+
+impl Message for GetSystem {
+ type Reply = SystemInfo;
+}
+
+pub struct SetSystem {
+ pub config: T,
+}
+
+impl Message for SetSystem {
+ type Reply = ();
+}
+
+impl SetSystem {
+ pub fn new(config: T) -> Self {
+ Self { config }
+ }
+}
+
+pub struct GetConfig;
+
+impl Message for GetConfig {
+ type Reply = Config;
+}
+
+pub struct SetConfig {
+ pub config: Option,
+}
+
+impl Message for SetConfig {
+ type Reply = ();
+}
+
+impl SetConfig {
+ pub fn new(config: Option) -> Self {
+ Self { config }
+ }
+
+ pub fn with(config: T) -> Self {
+ Self {
+ config: Some(config),
+ }
+ }
+}
+
+pub struct GetProposal;
+
+impl Message for GetProposal {
+ type Reply = Option;
+}
+
+pub struct UpdateHostname {
+ pub name: String,
+}
+
+impl Message for UpdateHostname {
+ type Reply = ();
+}
+
+pub struct UpdateStaticHostname {
+ pub name: String,
+}
+
+impl Message for UpdateStaticHostname {
+ type Reply = ();
+}
+
+pub struct Install;
+
+impl Message for Install {
+ type Reply = ();
+}
diff --git a/rust/agama-hostname/src/model.rs b/rust/agama-hostname/src/model.rs
new file mode 100644
index 0000000000..919c506d4e
--- /dev/null
+++ b/rust/agama-hostname/src/model.rs
@@ -0,0 +1,184 @@
+// Copyright (c) [2025] SUSE LLC
+//
+// All Rights Reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 2 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, contact SUSE LLC.
+//
+// To contact SUSE LLC about this file by physical or electronic mail, you may
+// find current contact information at www.suse.com.
+
+use crate::service;
+use agama_utils::api::hostname::SystemInfo;
+use std::{fs, path::PathBuf, process::Command};
+
+/// Abstract the hostname-related configuration from the underlying system.
+///
+/// It offers an API to query and set the transient or static hostname of a
+/// system. This trait can be implemented to replace the real system during
+/// tests.
+pub trait ModelAdapter: Send + 'static {
+ /// Reads the system info.
+ fn system_info(&self) -> Result {
+ let name = self.static_hostname()?;
+
+ Ok(SystemInfo {
+ r#static: (!name.is_empty()).then(|| name),
+ hostname: self.hostname()?,
+ })
+ }
+
+ /// Current system hostname.
+ fn hostname(&self) -> Result;
+
+ /// Current system static hostname.
+ fn static_hostname(&self) -> Result;
+
+ /// Change the system static hostname.
+ fn set_static_hostname(&mut self, name: String) -> Result<(), service::Error>;
+
+ /// Change the system hostname
+ fn set_hostname(&mut self, name: String) -> Result<(), service::Error>;
+
+ /// Apply the changes to target system. It is expected to be called almost
+ /// at the end of the installation.
+ fn install(&self) -> Result<(), service::Error>;
+
+ // Target directory to copy the static hostname at the end of the installation
+ fn static_target_dir(&self) -> &str;
+}
+
+/// [ModelAdapter] implementation for systemd-based systems.
+pub struct Model;
+
+impl ModelAdapter for Model {
+ fn static_hostname(&self) -> Result {
+ let mut cmd = Command::new("hostnamectl");
+ cmd.args(["hostname", "--static"]);
+ tracing::info!("{:?}", &cmd);
+ let output = cmd.output()?;
+ tracing::info!("{:?}", &output);
+
+ let output = String::from_utf8_lossy(&output.stdout).trim().to_string();
+
+ Ok(output)
+ }
+
+ fn hostname(&self) -> Result {
+ let output = Command::new("hostnamectl")
+ .args(["hostname", "--transient"])
+ .output()?;
+ let output = String::from_utf8_lossy(&output.stdout).trim().to_string();
+
+ Ok(output)
+ }
+
+ fn set_static_hostname(&mut self, name: String) -> Result<(), service::Error> {
+ Command::new("hostnamectl")
+ .args(["set-hostname", "--static", name.as_str()])
+ .output()?;
+
+ Ok(())
+ }
+
+ fn set_hostname(&mut self, name: String) -> Result<(), service::Error> {
+ Command::new("hostnamectl")
+ .args(["set-hostname", "--transient", name.as_str()])
+ .output()?;
+ Ok(())
+ }
+
+ fn static_target_dir(&self) -> &str {
+ "/mnt"
+ }
+
+ /// Copy the static hostname to the target system
+ fn install(&self) -> Result<(), service::Error> {
+ const HOSTNAME_PATH: &str = "/etc/hostname";
+ let from = PathBuf::from(HOSTNAME_PATH);
+ if from.exists() {
+ let to = PathBuf::from(self.static_target_dir()).join(HOSTNAME_PATH);
+ fs::create_dir_all(to.parent().unwrap())?;
+ fs::copy(from, to)?;
+ }
+ Ok(())
+ }
+}
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use tempfile::tempdir;
+
+ #[derive(Clone)]
+ pub struct TestModel {
+ pub source_dir: PathBuf,
+ pub target_dir: PathBuf,
+ }
+
+ impl ModelAdapter for TestModel {
+ fn hostname(&self) -> Result {
+ Ok("test-hostname".to_string())
+ }
+
+ fn static_hostname(&self) -> Result {
+ let path = self.source_dir.join("etc/hostname");
+ fs::read_to_string(path).map_err(service::Error::from)
+ }
+
+ fn set_static_hostname(&mut self, name: String) -> Result<(), service::Error> {
+ let path = self.source_dir.join("etc/hostname");
+ fs::write(path, name).map_err(service::Error::from)
+ }
+
+ fn set_hostname(&mut self, _name: String) -> Result<(), service::Error> {
+ Ok(())
+ }
+
+ fn install(&self) -> Result<(), service::Error> {
+ let from = self.source_dir.join("etc/hostname");
+ if from.exists() {
+ let to = self.target_dir.join("etc/hostname");
+ fs::create_dir_all(to.parent().unwrap())?;
+ fs::copy(from, to)?;
+ }
+ Ok(())
+ }
+
+ fn static_target_dir(&self) -> &str {
+ self.target_dir.to_str().unwrap()
+ }
+ }
+
+ #[test]
+ fn test_install() -> Result<(), service::Error> {
+ let temp_source = tempdir()?;
+ let temp_target = tempdir()?;
+ let hostname_path = temp_source.path().join("etc");
+ fs::create_dir_all(&hostname_path)?;
+ fs::write(hostname_path.join("hostname"), "test-hostname")?;
+
+ let model = TestModel {
+ source_dir: temp_source.path().to_path_buf(),
+ target_dir: temp_target.path().to_path_buf(),
+ };
+
+ model.install()?;
+
+ let installed_hostname_path = temp_target.path().join("etc/hostname");
+ assert!(fs::exists(&installed_hostname_path)?);
+ let content = fs::read_to_string(installed_hostname_path)?;
+ assert_eq!(content, "test-hostname");
+
+ Ok(())
+ }
+}
diff --git a/rust/agama-hostname/src/monitor.rs b/rust/agama-hostname/src/monitor.rs
new file mode 100644
index 0000000000..eb58990a35
--- /dev/null
+++ b/rust/agama-hostname/src/monitor.rs
@@ -0,0 +1,90 @@
+// Copyright (c) [2025] SUSE LLC
+//
+// All Rights Reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 2 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, contact SUSE LLC.
+//
+// To contact SUSE LLC about this file by physical or electronic mail, you may
+// find current contact information at www.suse.com.
+
+use crate::{message, service::Service};
+use agama_utils::{
+ actor::Handler,
+ dbus::{get_property, to_owned_hash},
+};
+use tokio_stream::StreamExt;
+use zbus::fdo::{PropertiesChangedStream, PropertiesProxy};
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error(transparent)]
+ DBus(#[from] zbus::Error),
+}
+
+pub struct Monitor {
+ handler: Handler,
+ stream: PropertiesChangedStream,
+}
+
+// Monitors the DBUS hostname service notifying the static or transient system hostname change
+// when them occurs
+//
+/// * `handler`: service handler to be monitorized.
+impl Monitor {
+ pub async fn new(handler: Handler) -> Result {
+ let dbus = zbus::Connection::system().await?;
+ let proxy = PropertiesProxy::builder(&dbus)
+ .path("/org/freedesktop/hostname1")?
+ .destination("org.freedesktop.hostname1")?
+ .build()
+ .await?;
+ let stream = proxy
+ .receive_properties_changed()
+ .await
+ .map_err(Error::DBus)?;
+ Ok(Self { handler, stream })
+ }
+
+ pub async fn run(&mut self) {
+ while let Some(changes) = self.stream.next().await {
+ let Ok(args) = changes.args() else {
+ continue;
+ };
+
+ let changes = args.changed_properties();
+ let Ok(changes) = to_owned_hash(changes) else {
+ continue;
+ };
+
+ if let Ok(name) = get_property::(&changes, "Hostname") {
+ let _ = self.handler.call(message::UpdateHostname { name }).await;
+ }
+ if let Ok(name) = get_property::(&changes, "StaticHostname") {
+ let _ = self
+ .handler
+ .call(message::UpdateStaticHostname { name })
+ .await;
+ }
+ }
+ }
+}
+
+/// Spawns a Tokio task for the monitor.
+///
+/// * `monitor`: monitor to spawn.
+pub fn spawn(mut monitor: Monitor) {
+ tokio::spawn(async move {
+ monitor.run().await;
+ });
+}
diff --git a/rust/agama-hostname/src/service.rs b/rust/agama-hostname/src/service.rs
new file mode 100644
index 0000000000..f9c337d408
--- /dev/null
+++ b/rust/agama-hostname/src/service.rs
@@ -0,0 +1,276 @@
+// Copyright (c) [2025] SUSE LLC
+//
+// All Rights Reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 2 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, contact SUSE LLC.
+//
+// To contact SUSE LLC about this file by physical or electronic mail, you may
+// find current contact information at www.suse.com.
+
+use crate::monitor::Monitor;
+use crate::{message, monitor};
+use crate::{Model, ModelAdapter};
+use agama_utils::{
+ actor::{self, Actor, Handler, MessageHandler},
+ api::{
+ self,
+ event::{self, Event},
+ hostname::{Proposal, SystemInfo},
+ Issue, Scope,
+ },
+ issue,
+};
+use async_trait::async_trait;
+use tokio::sync::broadcast;
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("Invalid hostname")]
+ InvalidHostname,
+ #[error(transparent)]
+ Event(#[from] broadcast::error::SendError),
+ #[error(transparent)]
+ IssueService(#[from] issue::service::Error),
+ #[error(transparent)]
+ Actor(#[from] actor::Error),
+ #[error(transparent)]
+ IO(#[from] std::io::Error),
+ #[error(transparent)]
+ Generic(#[from] anyhow::Error),
+ #[error("There is no proposal for hostname")]
+ MissingProposal,
+}
+
+/// Builds and spawns the hostname service.
+///
+/// This struct allows to build a hostname service. It allows replacing
+/// the "model" for a custom one.
+///
+/// It spawns two Tokio tasks:
+///
+/// - The main service, which is reponsible for holding and applying the configuration.
+/// - A monitor which checks for changes in the underlying system (e.g., changing the hostname)
+/// and signals the main service accordingly.
+/// - It depends on the issues service to keep the installation issues.
+pub struct Starter {
+ model: Option>,
+ issues: Handler,
+ events: event::Sender,
+}
+
+impl Starter {
+ /// Creates a new starter.
+ ///
+ /// * `events`: channel to emit the [hostname-specific events](crate::Event).
+ /// * `issues`: handler to the issues service.
+ pub fn new(events: event::Sender, issues: Handler) -> Self {
+ Self {
+ model: None,
+ events,
+ issues,
+ }
+ }
+
+ /// Uses the given model.
+ ///
+ /// By default, the hostname service relies on systemd. However, it might be useful
+ /// to replace it in some scenarios (e.g., when testing).
+ ///
+ /// * `model`: model to use. It must implement the [ModelAdapter] trait.
+ pub fn with_model(mut self, model: T) -> Self {
+ self.model = Some(Box::new(model));
+ self
+ }
+
+ /// Starts the service and returns a handler to communicate with it.
+ ///
+ /// The service uses a separate monitor to listen to system configuration
+ /// changes.
+ pub async fn start(self) -> Result, Error> {
+ let model = match self.model {
+ Some(model) => model,
+ None => Box::new(Model),
+ };
+
+ let config = model.system_info()?;
+
+ let service = Service {
+ config,
+ model,
+ issues: self.issues,
+ events: self.events,
+ };
+ let handler = actor::spawn(service);
+ Self::start_monitor(handler.clone()).await;
+ Ok(handler)
+ }
+
+ pub async fn start_monitor(handler: Handler) {
+ match Monitor::new(handler.clone()).await {
+ Ok(monitor) => monitor::spawn(monitor),
+ Err(error) => {
+ tracing::error!(
+ "Could not launch the hostname monitor, therefore changes from systemd will be ignored. \
+ The original error was {error}"
+ );
+ }
+ }
+ }
+}
+
+/// Hostname service.
+///
+/// It is responsible for handling the hostname part of the installation:
+///
+/// * Reads the static and transient hostname
+/// * Keeps track of the hostname settings of the underlying system (the installer).
+/// * Persist the static hostname at the end of the installation.
+pub struct Service {
+ config: SystemInfo,
+ model: Box,
+ issues: Handler,
+ events: event::Sender,
+}
+
+impl Service {
+ pub fn starter(events: event::Sender, issues: Handler) -> Starter {
+ Starter::new(events, issues)
+ }
+
+ fn get_proposal(&self) -> Option {
+ if !self.find_issues().is_empty() {
+ return None;
+ }
+
+ Some(Proposal {
+ hostname: self.config.hostname.clone(),
+ r#static: self.config.r#static.clone(),
+ })
+ }
+
+ /// Returns configuration issues.
+ ///
+ /// It returns issues if the hostname are too long
+ fn find_issues(&self) -> Vec {
+ // TODO: add length checks
+ vec![]
+ }
+}
+
+impl Actor for Service {
+ type Error = Error;
+}
+
+#[async_trait]
+impl MessageHandler for Service {
+ async fn handle(&mut self, _message: message::GetSystem) -> Result {
+ Ok(self.config.clone())
+ }
+}
+
+#[async_trait]
+impl MessageHandler for Service {
+ async fn handle(
+ &mut self,
+ _message: message::GetConfig,
+ ) -> Result {
+ Ok(api::hostname::Config {
+ r#static: self.config.r#static.clone(),
+ hostname: Some(self.config.hostname.clone()),
+ })
+ }
+}
+
+#[async_trait]
+impl MessageHandler> for Service {
+ async fn handle(
+ &mut self,
+ message: message::SetConfig,
+ ) -> Result<(), Error> {
+ let current = self.config.clone();
+
+ if let Some(config) = &message.config {
+ self.config.r#static = config.r#static.clone();
+ self.model
+ .set_static_hostname(config.r#static.clone().unwrap_or_default())?;
+ if let Some(name) = &config.r#static {
+ self.config.hostname = name.clone();
+ }
+
+ if let Some(name) = &config.hostname {
+ // If static hostname is set the transient is basically the same
+ if self.config.r#static.clone().unwrap_or_default().is_empty() {
+ self.config.hostname = name.clone();
+ self.model.set_hostname(name.clone())?
+ }
+ }
+ } else {
+ return Ok(());
+ }
+
+ if current == self.config {
+ return Ok(());
+ }
+
+ let issues = self.find_issues();
+ self.issues
+ .cast(issue::message::Set::new(Scope::Hostname, issues))?;
+ self.events.send(Event::ProposalChanged {
+ scope: Scope::Hostname,
+ })?;
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl MessageHandler for Service {
+ async fn handle(&mut self, _message: message::GetProposal) -> Result