diff --git a/autoinstallation/scripts/auto.sh b/autoinstallation/scripts/auto.sh index 4b3f841e04..639ba948e9 100755 --- a/autoinstallation/scripts/auto.sh +++ b/autoinstallation/scripts/auto.sh @@ -1,6 +1,9 @@ #!/usr/bin/sh set -ex +# Temporarily skip the AutoYaST XML validation +export YAST_SKIP_XML_VALIDATION=1 + if [ -z "$1" ] then url=$(awk -F 'agama.auto=' '{sub(/ .*$/, "", $2); print $2}' < /proc/cmdline) diff --git a/doc/autoyast.md b/doc/autoyast.md new file mode 100644 index 0000000000..198c60ae98 --- /dev/null +++ b/doc/autoyast.md @@ -0,0 +1,219 @@ +# AutoYaST Support + +Agama offers a mechanism to perform [unattended installations](../autoinstallation/). However, we +would like AutoYaST users to be able to use their AutoYaST profiles in Agama. This document +describes how Agama could support, to some extent, such profiles. + +Bear in mind that this document is just a draft and our plans could change once we start working +on the implementation. + +## What to support + +We want to point out that Agama and AutoYaST have different features. Agama is focused on the +installation and delegates further configuration to other tools. From this point of view, it is +clear that many of the sections you can find in an AutoYaST profile will not have an Agama +counterpart. + +Nevertheless, we want to cover: + +* Dynamic profiles, including rules/classes, ERB templates, pre-installation scripts and even "ask +lists". See [Dynamic profiles](#dynamic-profiles). +* Compatibility (partial or full) for the following sections: `networking`, `partitioning`, +`language`, `timezone`, `keyboard`, `software`, `scripts`, `users`, `iscsi-client`, `proxy` and +`suse_register`. See [Supported sections](#supported-sections). + +We still need to decide how to handle other sections like `firewall`, `bootloader`, `report`, +`general` or even some elements from `security` or `kdump`. + +Finally, we plan to "ignore" many other sections (e.g., all *-server elements) and sysconfig-like +elements. See [Unsupported sections](#unsupported-sections). + +## Dynamic profiles + +Many AutoYaST users rely on its dynamic capabilities to build adaptable profiles that they can use +to install different systems. For that reason, we need Agama to support these features: + +* [Rules and classes][rules-classes]. +* [Embedded Ruby (ERB)][erb]. +* [Pre-installation scripts][pre-scripts]. +* [Ask lists](). + +The most realistic way to support those features in the mid-term is to use the AutoYaST code with +some adaptations. The [import-autoyast-profiles branch][autoyast-branch] contains a proof-of-concept +that supports rules/classes, ERB and pre-installation scripts. If you are interested, you can give +it a try: + +``` +cd service +sudo bundle exec bin/agama-autoyast \ + file:///$PWD/test/fixtures/profiles/invalid.xml /tmp/output +cat /tmp/output/autoinst.json +``` + +You can even use the `agama-cli`: + +``` +cd rust +cargo build +sudo PATH=$PWD/../service/bin:$PATH ./target/debug/agama profile download \ + file:///$PWD/../service/test/fixtures/profiles/pre-scripts.xml +``` + +About "ask lists", there might need more work. Fortunately, the code to [parse][ask-list-reader] and +[run][ask-list-runner] the process are there but we need to adapt the [user +interface][ask-list-dialog], which is not trivial. + +[rules-classes]: https://doc.opensuse.org/documentation/leap/autoyast/html/book-autoyast/rulesandclass.html +[erb]: https://doc.opensuse.org/documentation/leap/autoyast/html/book-autoyast/erb-templates.html +[pre-scripts]: https://doc.opensuse.org/documentation/leap/autoyast/html/book-autoyast/cha-configuration-installation-options.html#pre-install-scripts +[ask-lists]: https://doc.opensuse.org/documentation/leap/autoyast/html/book-autoyast/cha-configuration-installation-options.html#CreateProfile-Ask +[autoyast-branch]: https://github.com/openSUSE/agama/tree/import-autoyast-profiles +[ask-list-reader]: https://github.com/yast/yast-autoinstallation/blob/c2dc34560df4ba890688a0c84caec94cc2718f14/src/lib/autoinstall/ask/profile_reader.rb#L29 +[ask-list-runner]: https://github.com/yast/yast-autoinstallation/blob/c2dc34560df4ba890688a0c84caec94cc2718f14/src/lib/autoinstall/ask/runner.rb#L50 +[ask-list-dialog]: https://github.com/yast/yast-autoinstallation/blob/c2dc34560df4ba890688a0c84caec94cc2718f14/src/lib/autoinstall/ask/dialog.rb#L23 + +## Supported sections + +### `dasd` and `iscsi-client` + +Support for iSCSI and DASD devices is missing in Agama profiles. Let's work on that when adding the +`partitioning` section equivalent. + +### `general` + +AutoYaST `general` section contains a set of elements that, for some reason, did not find a better +place. Most of those options will be ignored by Agama (e.g., `cio_ignore`, `mode`, `proposals`, +etc.). However, we might need to add support for a handful of them. + +Agama should process the `ask-list` section (see [Supported sections](#supported-sections)), +`signature-handling` (to deal with packages signatures) and, most probably, `storage` too (e.g., +affects the proposal). + +### `groups` and `users` + +Regarding users, Agama only allows defining the first user and setting the root authentication +mechanism (password and/or SSH public key). However, AutoYaST allows to specify a list of users and +groups plus some authentication settings. We have at least two options here: + +* Extract the root authentication data from the profile and try to infer which is the first user. +This behavior is already implemented. +* Import these sections as given because they are handled by the YaST code in Agama. + +### `keyboard`, `language` and `timezone` + +These sections are rather simple, but we need to do some mapping between AutoYaST and Agama values. +Additionally, the `hwclock` element is not present in Agama. + +### `networking` + +The `networking` section in AutoYaST is composed of several sections: `dns`, `interfaces`, +`net-udev`, `routing` and `s390-devices`. Additionally, other elements like `ipv6` or +`keep_install_network` might need some level of support. + +At this point, Agama only supports defining a list of connections that could correspond with the +AutoYaST interfaces list. We might need to extend Agama to support `dns`, `net-udev`, etc. + +About `keep_install_network` and `setup_before_proposal`, we should not implement them to keep +things simple. + +### `partitioning` + +By far, the most complex part of an AutoYaST profile. We can import the AutoYaST `partitioning` +section as it is because the partitioning is handled by the same code in Agama and AutoyaST. + +However, we must implement a mechanism to convert to/from both profile types. + +### `proxy` + +To use a proxy in Agama, you set the `proxy` in the [kernel's command line][cmdline]. In AutoYaST, +you can specify the proxy in the profile apart from the command line. + +Although we need to support the same use case, we should avoid introducing a `proxy` section unless +it is strictly required. + +[cmdline]: https://github.com/openSUSE/agama/blob/a105391949a914ae57719c80a610c642fb581924/service/lib/agama/proxy_setup.rb#L31 + +### `report` + +The AutoYaST `report` section defines which kind of messages to report (errors, warnings, +information and yes/no messages) and whether the installation should stop on any of them. Agama does +not have an equivalent mechanism. Moreover, it is arguable whether it is a good idea to base on the +type of message to stop the installation. A more fine-grained control over the situations that +should stop the installation would be better. As an example, consider the `signature-handling` +section. + +### `scripts` + +The only way to use scripts in Agama is to write your own autoinstallation script. Unlike AutoYaST, +you cannot embed the script within the Jsonnet-based profile. This is relevant from the +implementation point of view because we might need to extract AutoYaST scripts and put them in some +place for Agama to run them. + +Apart from that, AutoYaST considers five kind of scripts: `pre`, `post-partitioning`, `chroot`, +`post`, and `init`. The last two are expected to run after the first boot, where Agama is not +present anymore. + +If we want to support `post` or `init` scripts, we need to copy them to the installed system and run +them through a systemd service. + +### `software` + +The `software` section is composed of several lists: + +* A list of products to install, although a single value is expected. +* A list of patterns to install, a list of patterns to install in the 2nd stage and a list of +patterns to remove. +* A list of packages to install, a list of packages to install in the 2nd stage and a list of +packages to remove. + +Additionally, it is possible to force the installation of a specific kernel (`kernel`), perform +an online update at the end of the installation (`do_online_update`) or enable/disable the +installation of recommended packages (`install_recommended`). + +Only the product and the list of products or patterns are available for Agama. We might consider +adding support for the packages list and the `install_recommended` setting, although none are in the +web UI. + +### `suse_register` + +Basic support for registering in the SUSE Customer Center is already in place, although +there is no way to select the list of add-ons. + +It is arguable whether we should offer a `install_updates` element instead of just installing them +(which is the use case for not installing them?). + +About the `slp_discoverty` element, Agama does not support [SLP] at all? + +[SLP]: https://documentation.suse.com/sles/15-SP5/single-html/SLES-administration/#cha-slp + +## Unsupported sections + +* `FCoE` +* `add-on` +* `audit-laf` +* `auth-client` +* `configuration_management` +* `deploy_image` +* `dhcp-server` +* `dns-server` +* `files` +* `firstboot` +* `ftp-server` +* `groups` +* `host` +* `http-server` +* `mail` +* `nfs` +* `nfs_server` +* `nis` +* `nis_server` +* `ntp-client` +* `printer` +* `samba-client` +* `services-manager` +* `sound` +* `squid` +* `ssh_import` +* `sysconfig` +* `tftp-server` +* `upgrade` diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a1a1a8c5d2..6a83c511f9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -92,6 +92,7 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "url", "zbus", ] diff --git a/rust/agama-cli/src/profile.rs b/rust/agama-cli/src/profile.rs index 8d837ece57..3c76619486 100644 --- a/rust/agama-cli/src/profile.rs +++ b/rust/agama-cli/src/profile.rs @@ -1,4 +1,4 @@ -use agama_lib::profile::{download, ProfileEvaluator, ProfileValidator, ValidationResult}; +use agama_lib::profile::{ProfileEvaluator, ProfileReader, ProfileValidator, ValidationResult}; use anyhow::Context; use clap::Subcommand; use std::path::Path; @@ -15,6 +15,13 @@ pub enum ProfileCommands { Evaluate { path: String }, } +fn download(url: &str) -> anyhow::Result<()> { + let reader = ProfileReader::new(url)?; + let contents = reader.read()?; + print!("{}", contents); + Ok(()) +} + fn validate(path: String) -> anyhow::Result<()> { let validator = ProfileValidator::default_schema()?; let path = Path::new(&path); @@ -45,7 +52,7 @@ fn evaluate(path: String) -> anyhow::Result<()> { pub fn run(subcommand: ProfileCommands) -> anyhow::Result<()> { match subcommand { - ProfileCommands::Download { url } => Ok(download(&url)?), + ProfileCommands::Download { url } => download(&url), ProfileCommands::Validate { path } => validate(path), ProfileCommands::Evaluate { path } => evaluate(path), } diff --git a/rust/agama-lib/Cargo.toml b/rust/agama-lib/Cargo.toml index 8fab6b830c..bd386984a0 100644 --- a/rust/agama-lib/Cargo.toml +++ b/rust/agama-lib/Cargo.toml @@ -19,4 +19,5 @@ tempfile = "3.4.0" thiserror = "1.0.39" tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1.14" +url = "2.5.0" zbus = { version = "3", default-features = false, features = ["tokio"] } diff --git a/rust/agama-lib/src/profile.rs b/rust/agama-lib/src/profile.rs index dab1326583..0cba11db91 100644 --- a/rust/agama-lib/src/profile.rs +++ b/rust/agama-lib/src/profile.rs @@ -4,29 +4,58 @@ use curl::easy::Easy; use jsonschema::JSONSchema; use log::info; use serde_json; -use std::{ - fs, io, - io::{stdout, Write}, - path::Path, - process::Command, -}; -use tempfile::tempdir; - -/// Downloads a file and writes it to the stdout() -/// -/// TODO: move this code to a struct -/// TODO: add support for YaST-specific URLs -/// TODO: do not write to stdout, but to something implementing the Write trait -/// TODO: retry the download if it fails -pub fn download(url: &str) -> Result<(), ProfileError> { - let mut easy = Easy::new(); - easy.url(url)?; - easy.write_function(|data| { - stdout().write_all(data).unwrap(); - Ok(data.len()) - })?; - easy.perform()?; - Ok(()) +use std::{fs, io, io::Write, path::Path, process::Command}; +use tempfile::{tempdir, TempDir}; +use url::Url; + +/// Downloads a profile for a given location. +pub struct ProfileReader { + url: Url, +} + +impl ProfileReader { + pub fn new(url: &str) -> anyhow::Result { + let url = Url::parse(url)?; + Ok(Self { url }) + } + + pub fn read(&self) -> anyhow::Result { + let path = self.url.path(); + if path.ends_with(".xml") || path.ends_with(".erb") || path.ends_with('/') { + self.read_from_autoyast() + } else { + self.read_from_url() + } + } + + fn read_from_url(&self) -> anyhow::Result { + let mut buf = Vec::new(); + { + let mut handle = Easy::new(); + handle.url(self.url.as_str())?; + + let mut transfer = handle.transfer(); + transfer.write_function(|data| { + buf.extend(data); + Ok(data.len()) + })?; + transfer.perform().unwrap(); + } + Ok(String::from_utf8(buf)?) + } + + fn read_from_autoyast(&self) -> anyhow::Result { + const TMP_DIR_PREFIX: &str = "autoyast"; + const AUTOINST_JSON: &str = "autoinst.json"; + + let tmp_dir = TempDir::with_prefix(TMP_DIR_PREFIX)?; + Command::new("agama-autoyast") + .args([self.url.as_str(), &tmp_dir.path().to_string_lossy()]) + .status()?; + + let autoinst_json = tmp_dir.path().join(AUTOINST_JSON); + Ok(fs::read_to_string(autoinst_json)?) + } } #[derive(Debug)] diff --git a/rust/package/agama-cli.changes b/rust/package/agama-cli.changes index 4ef0bfa60e..ef6dccebab 100644 --- a/rust/package/agama-cli.changes +++ b/rust/package/agama-cli.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Feb 7 11:49:02 UTC 2024 - Imobach Gonzalez Sosa + +- Add preliminary support to import AutoYaST profiles + (gh#openSUSE/agama#1029). + ------------------------------------------------------------------- Mon Jan 29 15:53:56 UTC 2024 - Imobach Gonzalez Sosa diff --git a/service/agama.gemspec b/service/agama.gemspec index 8cb877f6ba..62dbdba14f 100644 --- a/service/agama.gemspec +++ b/service/agama.gemspec @@ -40,7 +40,7 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/openSUSE/agama" spec.license = "GPL-2.0-only" spec.files = Dir["lib/**/*.rb", "bin/*", "share/*", "conf.d/*"] - spec.executables = ["agamactl", "agama-proxy-setup"] + spec.executables = ["agamactl", "agama-proxy-setup", "agama-autoyast"] spec.metadata = { "rubygems_mfa_required" => "true" } spec.required_ruby_version = ">= 2.5.0" diff --git a/service/bin/agama-autoyast b/service/bin/agama-autoyast new file mode 100755 index 0000000000..1df1e6eaed --- /dev/null +++ b/service/bin/agama-autoyast @@ -0,0 +1,50 @@ +#!/usr/bin/env ruby +# 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. + +# TEMPORARY overwrite of Y2DIR to use DBus for communication with dependent yast modules +require "yast" +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) + +# Set the PATH to a known value +ENV["PATH"] = "/sbin:/usr/sbin:/usr/bin:/bin" + +require "rubygems" +# find Gemfile when D-Bus activates a git checkout +Dir.chdir(__dir__) do + require "bundler/setup" +end +require "agama/autoyast/converter" + +if ARGV.length != 2 + warn "Usage: #{$PROGRAM_NAME} URL DIRECTORY" + exit 1 +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 +end diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb new file mode 100755 index 0000000000..ad7bdd0680 --- /dev/null +++ b/service/lib/agama/autoyast/converter.rb @@ -0,0 +1,159 @@ +#!/usr/bin/env ruby +# 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 "yast" +require "autoinstall/script_runner" +require "autoinstall/script" +require "agama/autoyast/users_converter" +require "json" +require "fileutils" +require "pathname" + +# :nodoc: +module Agama + module AutoYaST + # Converts an AutoYaST profile into an Agama one. + # + # It is expected that many of the AutoYaST options are ignored because Agama does not have the + # same features. + # + # The output might include, apart from the JSON Agama profile, a set of scripts (not implemented + # yet). + # + # 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 + + # @return [Hash] Agama profile + def export_profile(profile) + users = Agama::AutoYaST::UsersConverter.new(profile) + { + "software" => export_software(profile.fetch_as_hash("software")), + "storage" => export_storage(profile.fetch_as_array("partitioning")), + "root" => users.root, + "user" => users.user + } + end + + # @param drives [Array] Array of drives in the AutoYaST partitioning section + def export_storage(drives) + # TODO: rely on AutoinstProfile classes + devices = drives.each_with_object([]) do |d, all| + next unless d["device"] + + all << d["device"] + end + return {} if devices.empty? + + { "bootDevice" => devices.first } + end + + # @param profile [Hash] Software section from the AutoYaST profile + def export_software(profile) + product = profile.fetch_as_array("products").first + patterns = profile.fetch_as_array("patterns") + return {} unless product + + { "product" => product, "patterns" => patterns } + end + + # @param profile [Hash] Users section from the AutoYaST profile + def export_root(profile) + users = Agama::AutoYaST::UsersConverter.new(profile) + users.root + 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/users_converter.rb b/service/lib/agama/autoyast/users_converter.rb new file mode 100644 index 0000000000..412826a6e3 --- /dev/null +++ b/service/lib/agama/autoyast/users_converter.rb @@ -0,0 +1,74 @@ +#!/usr/bin/env ruby +# 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 "yast" +require "y2users/config" +require "y2users/autoinst/reader" + +# :nodoc: +module Agama + module AutoYaST + # Extracts the users information from an AutoYaST profile. + class UsersConverter + # @param profile [ProfileHash] AutoYaST profile + def initialize(profile) + @profile = profile + end + + # @return [Hash] Agama "root" section + def root + root_user = config.users.find { |u| u.name == "root" } + return {} unless root_user + + hsh = { "password" => root_user.password.value.to_s } + public_key = root_user.authorized_keys.first + hsh["sshPublicKey"] = public_key if public_key + hsh + end + + # @return [Hash] Agama "user" section + def user + user = config.users.find { |u| !u.system? && !u.root? } + return {} unless user + + { + "userName" => user.name, + "fullName" => user.gecos.first.to_s, + "password" => user.password.value.to_s + } + end + + private + + attr_reader :profile + + # @return [Y2Users::Config] Users configuration + def config + return @config if @config + + reader = Y2Users::Autoinst::Reader.new(profile) + result = reader.read + @config = result.config + end + end + end +end diff --git a/service/lib/agama/dbus/manager.rb b/service/lib/agama/dbus/manager.rb index d1f129d18e..61b4df984f 100644 --- a/service/lib/agama/dbus/manager.rb +++ b/service/lib/agama/dbus/manager.rb @@ -25,6 +25,7 @@ require "agama/dbus/with_service_status" require "agama/dbus/interfaces/progress" require "agama/dbus/interfaces/service_status" +require "agama/autoyast/converter" module Agama module DBus diff --git a/service/lib/yast2/popup.rb b/service/lib/yast2/popup.rb new file mode 100644 index 0000000000..ec1af2a5b3 --- /dev/null +++ b/service/lib/yast2/popup.rb @@ -0,0 +1,74 @@ +# 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 "yast" +require "agama/dbus/clients/questions" + +module Yast2 + # Replacement to the Yast2::Popup class to work with Agama. + class Popup + class << self + # rubocop:disable Metrics/ParameterLists + # rubocop:disable Lint/UnusedMethodArgument + def show(message, details: "", headline: "", timeout: 0, focus: nil, buttons: :ok, + richtext: false, style: :notice) + + question = Agama::Question.new( + qclass: "popup", + text: message, + options: generate_options(buttons), + default_option: focus + ) + questions_client.ask(question) + end + + private + + # FIXME: inject the logger + def logger + @logger = Logger.new($stdout) + end + + def generate_options(buttons) + case buttons + when :ok + [:ok] + when :continue_cancel + [:continue, :cancel] + when :yes_no + [:yes, :no] + else + raise ArgumentError, "Invalid value #{buttons.inspect} for buttons" + end + end + + # Returns the client to ask questions + # + # @return [Agama::DBus::Clients::Questions] + def questions_client + @questions_client ||= Agama::DBus::Clients::Questions.new(logger: logger) + end + end + end +end +# rubocop:enable Metrics/ParameterLists +# rubocop:enable Lint/UnusedMethodArgument diff --git a/service/package/gem2rpm.yml b/service/package/gem2rpm.yml index 9b4095401d..142916e9d2 100644 --- a/service/package/gem2rpm.yml +++ b/service/package/gem2rpm.yml @@ -73,6 +73,7 @@ Requires: snapper Requires: udftools Requires: xfsprogs + Requires: yast2-schema :filelist: "%{_datadir}/dbus-1/agama.conf\n %dir %{_datadir}/dbus-1/agama-services\n %{_datadir}/dbus-1/agama-services/org.opensuse.Agama*.service\n diff --git a/service/package/rubygem-agama.changes b/service/package/rubygem-agama.changes index edefa801fc..4b2c8a6496 100644 --- a/service/package/rubygem-agama.changes +++ b/service/package/rubygem-agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Feb 7 11:49:02 UTC 2024 - Imobach Gonzalez Sosa + +- Add preliminary support to import AutoYaST profiles + (gh#openSUSE/agama#1029). + ------------------------------------------------------------------- Thu Feb 1 13:08:39 UTC 2024 - Josef Reidinger diff --git a/service/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb new file mode 100644 index 0000000000..bc6de397fc --- /dev/null +++ b/service/test/agama/autoyast/converter_test.rb @@ -0,0 +1,133 @@ +# 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_relative "../../test_helper" +require "agama/autoyast/converter" +require "json" +require "tmpdir" +require "autoinstall/xml_checks" + +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 + + 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) + end + + after do + FileUtils.remove_entry(workdir) + FileUtils.remove_entry(tmpdir) + end + + subject do + described_class.new("file://#{profile}") + 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["software"]).to include("product" => "Tumbleweed") + end + end + + context "when a product is selected" do + it "exports the selected product" do + subject.to_agama(workdir) + expect(result["software"]).to include("product" => "Tumbleweed") + end + end + + context "when a storage device is selected" do + it "exports the device" do + subject.to_agama(workdir) + expect(result["storage"]).to include("bootDevice" => "/dev/vda") + end + end + + 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) + expect(result["root"]).to include("password" => "nots3cr3t", + "sshPublicKey" => "ssh-rsa ...") + end + end + + context "when a non-system user is defined" do + it "exports the user information" do + subject.to_agama(workdir) + expect(result["user"]).to include("userName" => "jane", + "password" => "12345678", "fullName" => "Jane Doe") + 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.to_agama(workdir) + end + end +end diff --git a/service/test/fixtures/profiles/pre-scripts.xml b/service/test/fixtures/profiles/pre-scripts.xml new file mode 100644 index 0000000000..cca64b7df8 --- /dev/null +++ b/service/test/fixtures/profiles/pre-scripts.xml @@ -0,0 +1,35 @@ + + + + + + + __PRODUCT__ + + + + + + false + linux + root + + + + + + + + + diff --git a/service/test/fixtures/profiles/simple.xml b/service/test/fixtures/profiles/simple.xml new file mode 100644 index 0000000000..1a0fd55eb7 --- /dev/null +++ b/service/test/fixtures/profiles/simple.xml @@ -0,0 +1,42 @@ + + + + + + Tumbleweed + + + enhanced_base + + + + + en_US + en_US + + + + + /dev/vda + all + + + + + + root + false + nots3cr3t + + ssh-rsa ... + + + + jane + 1000 + Jane Doe + false + 12345678 + + + diff --git a/setup-services.sh b/setup-services.sh index 29abd1da43..43fab94e83 100755 --- a/setup-services.sh +++ b/setup-services.sh @@ -58,6 +58,7 @@ $SUDO zypper --non-interactive --gpg-auto-import-keys install \ yast2-iscsi-client \ yast2-network \ yast2-proxy \ + yast2-schema \ yast2-storage-ng \ yast2-users \ bcache-tools \