From 4c01cb824eba5131105a247a4b92da7272370278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 23 Feb 2023 15:48:48 +0000 Subject: [PATCH 01/25] [service] Add a script to import an AutoYaST profile --- service/bin/autoyast | 99 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100755 service/bin/autoyast diff --git a/service/bin/autoyast b/service/bin/autoyast new file mode 100755 index 0000000000..db4200e84b --- /dev/null +++ b/service/bin/autoyast @@ -0,0 +1,99 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Copyright (c) [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 "yast" +require "dinstaller/dbus/clients/software" +require "dinstaller/dbus/clients/storage" +require "dinstaller/cmdline_args" + +module DInstaller + # Sets up the AutoYaST + class AutoyastImporter + def initialize(profile_url) + @profile_url = profile_url + end + + def import + import_yast + profile = find_profile + puts "Importing profile #{profile.inspect}" + import_profile(profile) + end + + private + + attr_reader :profile_url + + def find_profile + Yast::AutoinstConfig.ParseCmdLine(profile_url) + Yast::ProfileLocation.Process + Yast::Profile.ReadXML(Yast::AutoinstConfig.xml_tmpfile) + Yast::Profile.current + end + + # @param profile [ProfileHash] + def import_profile(profile) + import_software(profile["software"] || {}) + import_partitioning(profile["partitioning"] || []) + end + + # @param drives [Array] Array of drives in the AutoYaST partitioning section + def import_partitioning(drives) + devices = drives.each_with_object([]) do |d, all| + next unless d["device"] + + all << d["device"] + end + + storage_client.calculate(devices) + end + + # @param profile [Hash] Software section from the AutoYaST profile + def import_software(profile) + product = profile.fetch("products", []).first + return unless product + + software_client.select_product(product) + end + + def import_yast + Yast.import "AutoinstConfig" + Yast.import "ProfileLocation" + Yast.import "Profile" + end + + # @return [DInstaller::DBus::Clients::Storage] + def storage_client + @storage_client ||= DBus::Clients::Storage.new + end + + # @return [DInstaller::DBus::Clients::Software] + def software_client + @software_client ||= DBus::Clients::Software.new + end + end +end + +url = ARGV[0] +url ||= "file:/autoinst.xml" +importer = DInstaller::AutoyastImporter.new(url) +importer.import From 6c2a7a4afca5dcf919307a1e6cebdefa62c214b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 24 Feb 2023 06:50:41 +0000 Subject: [PATCH 02/25] [service] Do not import but convert to an Agama profile --- service/bin/autoyast | 53 ++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/service/bin/autoyast b/service/bin/autoyast index db4200e84b..fdca010df7 100755 --- a/service/bin/autoyast +++ b/service/bin/autoyast @@ -21,22 +21,20 @@ # find current contact information at www.suse.com. require "yast" -require "dinstaller/dbus/clients/software" -require "dinstaller/dbus/clients/storage" -require "dinstaller/cmdline_args" +require "json" module DInstaller - # Sets up the AutoYaST + # Converts an AutoYaST profile to D-Installer class AutoyastImporter def initialize(profile_url) @profile_url = profile_url end - def import + # @return [Hash] D-Installer profile + def export import_yast profile = find_profile - puts "Importing profile #{profile.inspect}" - import_profile(profile) + export_profile(profile) end private @@ -51,28 +49,32 @@ module DInstaller end # @param profile [ProfileHash] - def import_profile(profile) - import_software(profile["software"] || {}) - import_partitioning(profile["partitioning"] || []) + # @return [Hash] D-Installer profile + def export_profile(profile) + { + "software" => export_software(profile["software"] || {}), + "storage" => export_storage(profile["partitioning"] || []) + } end # @param drives [Array] Array of drives in the AutoYaST partitioning section - def import_partitioning(drives) + def export_storage(drives) devices = drives.each_with_object([]) do |d, all| next unless d["device"] all << d["device"] end + return {} if devices.empty? - storage_client.calculate(devices) + { "devices" => devices } end # @param profile [Hash] Software section from the AutoYaST profile - def import_software(profile) + def export_software(profile) product = profile.fetch("products", []).first - return unless product + return {} unless product - software_client.select_product(product) + { "product" => product } end def import_yast @@ -80,20 +82,17 @@ module DInstaller Yast.import "ProfileLocation" Yast.import "Profile" end - - # @return [DInstaller::DBus::Clients::Storage] - def storage_client - @storage_client ||= DBus::Clients::Storage.new - end - - # @return [DInstaller::DBus::Clients::Software] - def software_client - @software_client ||= DBus::Clients::Software.new - end end end url = ARGV[0] url ||= "file:/autoinst.xml" -importer = DInstaller::AutoyastImporter.new(url) -importer.import +begin + importer = DInstaller::AutoyastImporter.new(url) + profile = importer.export.to_json + puts profile +rescue RuntimeError => e + warn "Could not load a profile from #{url}" + warn "Error: #{e}" + exit 1 +end From 7ab66de66fe3d77547a5ae62ebfaee1f8eb10870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 31 Jan 2024 10:00:18 +0000 Subject: [PATCH 03/25] [service] Move the AutoYaST importer to lib and add tests --- service/bin/autoyast | 98 ---------------- service/lib/agama/autoyast/converter.rb | 105 ++++++++++++++++++ service/test/agama/autoyast/converter_test.rb | 66 +++++++++++ service/test/fixtures/profiles/simple.xml | 21 ++++ 4 files changed, 192 insertions(+), 98 deletions(-) delete mode 100755 service/bin/autoyast create mode 100755 service/lib/agama/autoyast/converter.rb create mode 100644 service/test/agama/autoyast/converter_test.rb create mode 100644 service/test/fixtures/profiles/simple.xml diff --git a/service/bin/autoyast b/service/bin/autoyast deleted file mode 100755 index fdca010df7..0000000000 --- a/service/bin/autoyast +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Copyright (c) [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 "yast" -require "json" - -module DInstaller - # Converts an AutoYaST profile to D-Installer - class AutoyastImporter - def initialize(profile_url) - @profile_url = profile_url - end - - # @return [Hash] D-Installer profile - def export - import_yast - profile = find_profile - export_profile(profile) - end - - private - - attr_reader :profile_url - - def find_profile - Yast::AutoinstConfig.ParseCmdLine(profile_url) - Yast::ProfileLocation.Process - Yast::Profile.ReadXML(Yast::AutoinstConfig.xml_tmpfile) - Yast::Profile.current - end - - # @param profile [ProfileHash] - # @return [Hash] D-Installer profile - def export_profile(profile) - { - "software" => export_software(profile["software"] || {}), - "storage" => export_storage(profile["partitioning"] || []) - } - end - - # @param drives [Array] Array of drives in the AutoYaST partitioning section - def export_storage(drives) - devices = drives.each_with_object([]) do |d, all| - next unless d["device"] - - all << d["device"] - end - return {} if devices.empty? - - { "devices" => devices } - end - - # @param profile [Hash] Software section from the AutoYaST profile - def export_software(profile) - product = profile.fetch("products", []).first - return {} unless product - - { "product" => product } - end - - def import_yast - Yast.import "AutoinstConfig" - Yast.import "ProfileLocation" - Yast.import "Profile" - end - end -end - -url = ARGV[0] -url ||= "file:/autoinst.xml" -begin - importer = DInstaller::AutoyastImporter.new(url) - profile = importer.export.to_json - puts profile -rescue RuntimeError => e - warn "Could not load a profile from #{url}" - warn "Error: #{e}" - exit 1 -end diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb new file mode 100755 index 0000000000..c06c39126d --- /dev/null +++ b/service/lib/agama/autoyast/converter.rb @@ -0,0 +1,105 @@ +#!/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 "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 = find_profile + File.write(path.join("autoinst.json"), export_profile(profile).to_json) + end + + private + + attr_reader :profile_url + + def find_profile + Yast::AutoinstConfig.ParseCmdLine(profile_url) + Yast::ProfileLocation.Process + Yast::Profile.ReadXML(Yast::AutoinstConfig.xml_tmpfile) + Yast::Profile.current + end + + # @param profile [ProfileHash] + # @return [Hash] D-Installer profile + def export_profile(profile) + { + "software" => export_software(profile["software"] || {}), + "storage" => export_storage(profile["partitioning"] || []) + } + end + + # @param drives [Array] Array of drives in the AutoYaST partitioning section + def export_storage(drives) + 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("products", []).first + return {} unless product + + { "product" => product } + end + + def import_yast + Yast.import "AutoinstConfig" + Yast.import "ProfileLocation" + Yast.import "Profile" + end + end + end +end diff --git a/service/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb new file mode 100644 index 0000000000..9dfb3db826 --- /dev/null +++ b/service/test/agama/autoyast/converter_test.rb @@ -0,0 +1,66 @@ +# 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" + +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(:result) do + content = File.read(File.join(workdir, "autoinst.json")) + JSON.parse(content) + end + + before do + Yast.import "Installation" + allow(Yast::Installation).to receive(:sourcedir).and_return(File.join(tmpdir, "mount")) + 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 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 + end +end diff --git a/service/test/fixtures/profiles/simple.xml b/service/test/fixtures/profiles/simple.xml new file mode 100644 index 0000000000..c4869dc8c8 --- /dev/null +++ b/service/test/fixtures/profiles/simple.xml @@ -0,0 +1,21 @@ + + + + + + Tumbleweed + + + + + en_US + en_US + + + + + /dev/vda + all + + + From 2d5fc269e88d31ca7883cc98362f3092d199a4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 31 Jan 2024 16:07:01 +0000 Subject: [PATCH 04/25] [service] Add a D-Bus method to convert an AutoYaST profile --- service/lib/agama/dbus/manager.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/service/lib/agama/dbus/manager.rb b/service/lib/agama/dbus/manager.rb index d1f129d18e..0e6caa4450 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 @@ -62,6 +63,7 @@ def initialize(backend, logger) dbus_method(:CanInstall, "out result:b") { can_install? } dbus_method(:CollectLogs, "out tarball_filesystem_path:s") { collect_logs } dbus_method(:Finish, "") { finish_phase } + dbus_method(:ConvertProfile, "in url:s, in dir:s") { |url, dir| convert_profile(url, dir) } dbus_reader :installation_phases, "aa{sv}" dbus_reader :current_installation_phase, "u" dbus_reader :iguana_backend, "b" @@ -101,6 +103,16 @@ def finish_phase backend.finish_installation end + # Converts an AutoYaST profile into an Agama one. + # + # @param url [String] URL to download the profile from. + # @param directory [String] Directory to write the profile and its associated files (e.g., + # scripts). + def convert_profile(url, directory) + converter = AutoYaST::Converter.new(url) + converter.to_agama(directory) + end + # Description of all possible installation phase values # # @return [Array] From 470145f9d305e8313b2c91c4628b35e81a8cad85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 31 Jan 2024 16:14:19 +0000 Subject: [PATCH 05/25] [rust] Refactor the code to download a profile --- rust/Cargo.lock | 1 + rust/agama-cli/src/profile.rs | 11 ++++++-- rust/agama-lib/Cargo.toml | 1 + rust/agama-lib/src/profile.rs | 49 ++++++++++++++++++++--------------- 4 files changed, 39 insertions(+), 23 deletions(-) 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..64b997b015 100644 --- a/rust/agama-lib/src/profile.rs +++ b/rust/agama-lib/src/profile.rs @@ -4,29 +4,36 @@ 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 std::{fs, io, io::Write, path::Path, process::Command}; use tempfile::tempdir; +use url::Url; -/// 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(()) +/// 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 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)?) + } } #[derive(Debug)] From d3938d54e6032f589766c9770a35ac3afdd15067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 2 Feb 2024 17:21:50 +0000 Subject: [PATCH 06/25] [service] Add support for pre-script to the AutoYaST converter --- service/lib/agama/autoyast/converter.rb | 51 +++++++++++++++++-- service/test/agama/autoyast/converter_test.rb | 22 ++++++++ service/test/fixtures/profiles/invalid.xml | 19 +++++++ .../test/fixtures/profiles/pre-scripts.xml | 35 +++++++++++++ 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 service/test/fixtures/profiles/invalid.xml create mode 100644 service/test/fixtures/profiles/pre-scripts.xml diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index c06c39126d..5c0001911f 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -21,6 +21,8 @@ # find current contact information at www.suse.com. require "yast" +require "autoinstall/script_runner" +require "autoinstall/script" require "json" require "fileutils" require "pathname" @@ -51,7 +53,7 @@ def to_agama(dir) path = Pathname(dir) FileUtils.mkdir_p(path) import_yast - profile = find_profile + profile = read_profile File.write(path.join("autoinst.json"), export_profile(profile).to_json) end @@ -59,13 +61,53 @@ def to_agama(dir) attr_reader :profile_url - def find_profile + 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 - Yast::Profile.ReadXML(Yast::AutoinstConfig.xml_tmpfile) + + # 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 + # @param profile [ProfileHash] # @return [Hash] D-Installer profile def export_profile(profile) @@ -97,8 +139,9 @@ def export_software(profile) def import_yast Yast.import "AutoinstConfig" - Yast.import "ProfileLocation" + Yast.import "AutoinstScripts" Yast.import "Profile" + Yast.import "ProfileLocation" end end end diff --git a/service/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb index 9dfb3db826..b32b4aa49b 100644 --- a/service/test/agama/autoyast/converter_test.rb +++ b/service/test/agama/autoyast/converter_test.rb @@ -49,6 +49,20 @@ end describe "#to_agama" do + context "when some pre-script is defined" do + let(:profile_name) { "pre-scripts.xml" } + + before do + expect(Yast::AutoinstConfig).to receive(:scripts_dir) + .and_return(File.join(tmpdir, "scripts")) + 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) @@ -63,4 +77,12 @@ end end end + + context "when an invalid profile is given" do + let(:profile_name) { "invalid.xml" } + + xit "reports the problem" do + subject.to_agama(workdir) + end + end end diff --git a/service/test/fixtures/profiles/invalid.xml b/service/test/fixtures/profiles/invalid.xml new file mode 100644 index 0000000000..4fc82b80b4 --- /dev/null +++ b/service/test/fixtures/profiles/invalid.xml @@ -0,0 +1,19 @@ + + + + + Tumbleweed + + + + en_US + en_US + + + + + /dev/vda + all + + + 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 + + + + + + + + + From 28ff59804a20a7d8575a9d5c30086344f2cb5136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 2 Feb 2024 17:24:01 +0000 Subject: [PATCH 07/25] [service] Add an agama-autoyast executable * A pre-script may potentially block the main D-Bus process, so it seems better to run it outside of the Ruby D-Bus service. --- service/agama.gemspec | 2 +- service/bin/agama-autoyast | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100755 service/bin/agama-autoyast 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..63e6f48dfd --- /dev/null +++ b/service/bin/agama-autoyast @@ -0,0 +1,49 @@ +#!/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 +$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 From 9fb1f789fe03dc743691656902d579d41add0037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 2 Feb 2024 17:28:29 +0000 Subject: [PATCH 08/25] [rust] Use agama-autoyast to process AutoYaST profiles --- rust/agama-lib/src/profile.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/rust/agama-lib/src/profile.rs b/rust/agama-lib/src/profile.rs index 64b997b015..0cba11db91 100644 --- a/rust/agama-lib/src/profile.rs +++ b/rust/agama-lib/src/profile.rs @@ -5,7 +5,7 @@ use jsonschema::JSONSchema; use log::info; use serde_json; use std::{fs, io, io::Write, path::Path, process::Command}; -use tempfile::tempdir; +use tempfile::{tempdir, TempDir}; use url::Url; /// Downloads a profile for a given location. @@ -20,6 +20,15 @@ impl ProfileReader { } 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(); @@ -34,6 +43,19 @@ impl ProfileReader { } 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)] From f2e7cad59c2f1e9e96e03f325fcde9003f881ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 2 Feb 2024 18:46:06 +0000 Subject: [PATCH 09/25] [service] Add dependency on yast2-schema * It is used during the AutoYaST conversion. --- service/package/gem2rpm.yml | 1 + setup-services.sh | 1 + 2 files changed, 2 insertions(+) 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/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 \ From 48df2608c4ee2e7dc40bee0937e8cfe22ec56fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 6 Feb 2024 06:37:55 +0000 Subject: [PATCH 10/25] [service] Replace Yast2::Popup with an Agama specific class * It uses the questions service to display the messages. * By now it is just a PoC that needs to be extended. --- service/bin/agama-autoyast | 1 + service/lib/yast2/popup.rb | 67 +++++++++++++++++++ service/test/agama/autoyast/converter_test.rb | 7 +- 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 service/lib/yast2/popup.rb diff --git a/service/bin/agama-autoyast b/service/bin/agama-autoyast index 63e6f48dfd..1df1e6eaed 100755 --- a/service/bin/agama-autoyast +++ b/service/bin/agama-autoyast @@ -22,6 +22,7 @@ # 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 diff --git a/service/lib/yast2/popup.rb b/service/lib/yast2/popup.rb new file mode 100644 index 0000000000..d633740d26 --- /dev/null +++ b/service/lib/yast2/popup.rb @@ -0,0 +1,67 @@ +# 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 + 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 diff --git a/service/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb index b32b4aa49b..8feb44615f 100644 --- a/service/test/agama/autoyast/converter_test.rb +++ b/service/test/agama/autoyast/converter_test.rb @@ -23,6 +23,7 @@ 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) } @@ -35,6 +36,7 @@ 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")) end @@ -53,7 +55,7 @@ let(:profile_name) { "pre-scripts.xml" } before do - expect(Yast::AutoinstConfig).to receive(:scripts_dir) + allow(Yast::AutoinstConfig).to receive(:scripts_dir) .and_return(File.join(tmpdir, "scripts")) end @@ -81,7 +83,8 @@ context "when an invalid profile is given" do let(:profile_name) { "invalid.xml" } - xit "reports the problem" do + it "reports the problem" do + expect(Yast2::Popup).to receive(:show) subject.to_agama(workdir) end end From a105391949a914ae57719c80a610c642fb581924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 6 Feb 2024 06:39:13 +0000 Subject: [PATCH 11/25] [service] Do not crash when software/products is not an array --- service/lib/agama/autoyast/converter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index 5c0001911f..bf84f4ea8e 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -131,7 +131,7 @@ def export_storage(drives) # @param profile [Hash] Software section from the AutoYaST profile def export_software(profile) - product = profile.fetch("products", []).first + product = profile.fetch_as_array("products").first return {} unless product { "product" => product } From be386a521f9d341024ad36db7da89cf333416471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 6 Feb 2024 16:13:28 +0000 Subject: [PATCH 12/25] [doc] Document the AutoYaST support --- doc/autoyast.md | 218 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 doc/autoyast.md diff --git a/doc/autoyast.md b/doc/autoyast.md new file mode 100644 index 0000000000..e980a61637 --- /dev/null +++ b/doc/autoyast.md @@ -0,0 +1,218 @@ +# 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: + +* Import these sections as given because they are handled by the YaST code in Agama. +* Extract the root authentication data from the profile and try to infer which is the first user. + +### `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 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` From 65f9b1df2b36345c45a0a87175a5bd190dfdd21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 06:47:09 +0000 Subject: [PATCH 13/25] [service] Temporarily disable some RuboCop rules --- service/lib/yast2/popup.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/service/lib/yast2/popup.rb b/service/lib/yast2/popup.rb index d633740d26..ec1af2a5b3 100644 --- a/service/lib/yast2/popup.rb +++ b/service/lib/yast2/popup.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# # Copyright (c) [2024] SUSE LLC # # All Rights Reserved. @@ -24,6 +27,8 @@ 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) @@ -65,3 +70,5 @@ def questions_client end end end +# rubocop:enable Metrics/ParameterLists +# rubocop:enable Lint/UnusedMethodArgument From 45399ade2205a0e8fdad89f342827d1c08ac1012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 10:22:49 +0000 Subject: [PATCH 14/25] [service] Fix tests to not write in /tmp/profile --- service/test/agama/autoyast/converter_test.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/service/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb index 8feb44615f..0c23d2f2b9 100644 --- a/service/test/agama/autoyast/converter_test.rb +++ b/service/test/agama/autoyast/converter_test.rb @@ -39,6 +39,12 @@ 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")) end after do @@ -53,10 +59,18 @@ 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 From 3116c9c30974016fcbcc236de5ea3cd4ad6bf85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 10:26:32 +0000 Subject: [PATCH 15/25] [service] Imports AutoYaST root settings --- service/lib/agama/autoyast/converter.rb | 22 +++++++++++++++++-- service/test/agama/autoyast/converter_test.rb | 9 ++++++++ service/test/fixtures/profiles/simple.xml | 18 +++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index bf84f4ea8e..313c149acf 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -23,6 +23,8 @@ require "yast" require "autoinstall/script_runner" require "autoinstall/script" +require "y2users/config" +require "y2users/autoinst/reader" require "json" require "fileutils" require "pathname" @@ -112,8 +114,9 @@ def tmp_profile_path # @return [Hash] D-Installer profile def export_profile(profile) { - "software" => export_software(profile["software"] || {}), - "storage" => export_storage(profile["partitioning"] || []) + "software" => export_software(profile.fetch_as_hash("software")), + "storage" => export_storage(profile.fetch_as_array("partitioning")), + "root" => export_root("users" => profile.fetch_as_array("users")) } end @@ -137,6 +140,21 @@ def export_software(profile) { "product" => product } end + # @param profile [Hash] Users section from the AutoYaST profile + def export_root(profile) + reader = Y2Users::Autoinst::Reader.new(profile) + result = reader.read + return {} unless result.issues.empty? + + root = result.config.users.find { |u| u.name == "root" } + return {} unless root + + hsh = { "password" => root.password.value.to_s } + public_key = root.authorized_keys.first + hsh["sshPublicKey"] = public_key if public_key + hsh + end + def import_yast Yast.import "AutoinstConfig" Yast.import "AutoinstScripts" diff --git a/service/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb index 0c23d2f2b9..8cd8b4f0ec 100644 --- a/service/test/agama/autoyast/converter_test.rb +++ b/service/test/agama/autoyast/converter_test.rb @@ -92,6 +92,15 @@ 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 + expect(Yast2::Popup).to_not receive(:show) + subject.to_agama(workdir) + expect(result["root"]).to include("password" => "nots3cr3t", + "sshPublicKey" => "ssh-rsa ...") + end + end end context "when an invalid profile is given" do diff --git a/service/test/fixtures/profiles/simple.xml b/service/test/fixtures/profiles/simple.xml index c4869dc8c8..9925278556 100644 --- a/service/test/fixtures/profiles/simple.xml +++ b/service/test/fixtures/profiles/simple.xml @@ -18,4 +18,22 @@ all + + + + root + false + nots3cr3t + + ssh-rsa ... + + + + jane + 1000 + Jane Doe + false + 12345678 + + From b7dab3c246f6b9b724e2364c7a9714975a0e2cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 10:29:29 +0000 Subject: [PATCH 16/25] [auto] Skip AutoYaST XML validation --- autoinstallation/scripts/auto.sh | 3 +++ 1 file changed, 3 insertions(+) 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) From e0e395a86a6111762407a47b211842a682053daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 10:31:17 +0000 Subject: [PATCH 17/25] [service] Add a comment --- service/lib/agama/autoyast/converter.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index 313c149acf..4bcfe8c049 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -122,6 +122,7 @@ def export_profile(profile) # @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"] From 3dc6b713df7fd73b4aeb5d62259252e3fc578f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 11:05:39 +0000 Subject: [PATCH 18/25] [service] Imports AutoYaST software patterns --- service/lib/agama/autoyast/converter.rb | 3 ++- service/test/fixtures/profiles/simple.xml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index 4bcfe8c049..9ee4ebdae6 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -136,9 +136,10 @@ def export_storage(drives) # @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 } + { "product" => product, "patterns" => patterns } end # @param profile [Hash] Users section from the AutoYaST profile diff --git a/service/test/fixtures/profiles/simple.xml b/service/test/fixtures/profiles/simple.xml index 9925278556..1a0fd55eb7 100644 --- a/service/test/fixtures/profiles/simple.xml +++ b/service/test/fixtures/profiles/simple.xml @@ -5,6 +5,9 @@ Tumbleweed + + enhanced_base + From 74117b2e649ce6d848445f6db5d0f65559e66fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 11:18:25 +0000 Subject: [PATCH 19/25] [service] Extract the users logic from the AutoYaST converter --- service/lib/agama/autoyast/converter.rb | 21 ++---- service/lib/agama/autoyast/users_converter.rb | 74 +++++++++++++++++++ service/test/agama/autoyast/converter_test.rb | 8 ++ 3 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 service/lib/agama/autoyast/users_converter.rb diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index 9ee4ebdae6..97a39a18fa 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -23,8 +23,7 @@ require "yast" require "autoinstall/script_runner" require "autoinstall/script" -require "y2users/config" -require "y2users/autoinst/reader" +require "agama/autoyast/users_converter" require "json" require "fileutils" require "pathname" @@ -110,13 +109,14 @@ def tmp_profile_path ) end - # @param profile [ProfileHash] # @return [Hash] D-Installer 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" => export_root("users" => profile.fetch_as_array("users")) + "root" => users.root, + "user" => users.user } end @@ -144,17 +144,8 @@ def export_software(profile) # @param profile [Hash] Users section from the AutoYaST profile def export_root(profile) - reader = Y2Users::Autoinst::Reader.new(profile) - result = reader.read - return {} unless result.issues.empty? - - root = result.config.users.find { |u| u.name == "root" } - return {} unless root - - hsh = { "password" => root.password.value.to_s } - public_key = root.authorized_keys.first - hsh["sshPublicKey"] = public_key if public_key - hsh + users = Agama::AutoYaST::UsersConverter.new(profile) + users.root end def import_yast 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/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb index 8cd8b4f0ec..84d3662b9b 100644 --- a/service/test/agama/autoyast/converter_test.rb +++ b/service/test/agama/autoyast/converter_test.rb @@ -101,6 +101,14 @@ "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 From 24019f41bf226d3599d19e2320e1900bf29cfb03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 11:30:58 +0000 Subject: [PATCH 20/25] [service] Removed unneeded expect --- service/test/agama/autoyast/converter_test.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/service/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb index 84d3662b9b..f5911d1454 100644 --- a/service/test/agama/autoyast/converter_test.rb +++ b/service/test/agama/autoyast/converter_test.rb @@ -95,7 +95,6 @@ context "when the root password and/or public SSH key are set" do it "exports the root password and/or public SSH key" do - expect(Yast2::Popup).to_not receive(:show) subject.to_agama(workdir) expect(result["root"]).to include("password" => "nots3cr3t", "sshPublicKey" => "ssh-rsa ...") From aa71027bfd8c0f3f7ebf96f85fd28e40dec12461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 11:32:58 +0000 Subject: [PATCH 21/25] [doc] Update the AutoYaST support document --- doc/autoyast.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/autoyast.md b/doc/autoyast.md index e980a61637..3a8c4da592 100644 --- a/doc/autoyast.md +++ b/doc/autoyast.md @@ -95,8 +95,9 @@ Regarding users, Agama only allows defining the first user and setting the root 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: -* Import these sections as given because they are handled by the YaST code in Agama. * 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` From 62a9781ffa218cfaa554e30ae4121a1b76866544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 11:34:25 +0000 Subject: [PATCH 22/25] [service] Drop the ConvertProfile D-Bus method --- service/lib/agama/dbus/manager.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/service/lib/agama/dbus/manager.rb b/service/lib/agama/dbus/manager.rb index 0e6caa4450..61b4df984f 100644 --- a/service/lib/agama/dbus/manager.rb +++ b/service/lib/agama/dbus/manager.rb @@ -63,7 +63,6 @@ def initialize(backend, logger) dbus_method(:CanInstall, "out result:b") { can_install? } dbus_method(:CollectLogs, "out tarball_filesystem_path:s") { collect_logs } dbus_method(:Finish, "") { finish_phase } - dbus_method(:ConvertProfile, "in url:s, in dir:s") { |url, dir| convert_profile(url, dir) } dbus_reader :installation_phases, "aa{sv}" dbus_reader :current_installation_phase, "u" dbus_reader :iguana_backend, "b" @@ -103,16 +102,6 @@ def finish_phase backend.finish_installation end - # Converts an AutoYaST profile into an Agama one. - # - # @param url [String] URL to download the profile from. - # @param directory [String] Directory to write the profile and its associated files (e.g., - # scripts). - def convert_profile(url, directory) - converter = AutoYaST::Converter.new(url) - converter.to_agama(directory) - end - # Description of all possible installation phase values # # @return [Array] From 329be93bfa4b019910b60e82948ab5bc1cd2be16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 11:41:40 +0000 Subject: [PATCH 23/25] [service] Mock the profile validation --- service/test/agama/autoyast/converter_test.rb | 12 ++++++++++++ service/test/fixtures/profiles/invalid.xml | 19 ------------------- 2 files changed, 12 insertions(+), 19 deletions(-) delete mode 100644 service/test/fixtures/profiles/invalid.xml diff --git a/service/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb index f5911d1454..bc6de397fc 100644 --- a/service/test/agama/autoyast/converter_test.rb +++ b/service/test/agama/autoyast/converter_test.rb @@ -30,6 +30,15 @@ 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) @@ -45,6 +54,7 @@ .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 @@ -111,6 +121,8 @@ 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 diff --git a/service/test/fixtures/profiles/invalid.xml b/service/test/fixtures/profiles/invalid.xml deleted file mode 100644 index 4fc82b80b4..0000000000 --- a/service/test/fixtures/profiles/invalid.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - Tumbleweed - - - - en_US - en_US - - - - - /dev/vda - all - - - From a872ca1b12a0c179c61a26a8d22599f3f2717c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 11:50:54 +0000 Subject: [PATCH 24/25] Update changes files --- rust/package/agama-cli.changes | 6 ++++++ service/package/rubygem-agama.changes | 6 ++++++ 2 files changed, 12 insertions(+) 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/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 From 39caf46fd7a7d9cab8264d05de8268adafdee386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Feb 2024 12:18:00 +0000 Subject: [PATCH 25/25] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Knut Alejandro Anderssen González --- doc/autoyast.md | 2 +- service/lib/agama/autoyast/converter.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/autoyast.md b/doc/autoyast.md index 3a8c4da592..198c60ae98 100644 --- a/doc/autoyast.md +++ b/doc/autoyast.md @@ -119,7 +119,7 @@ things simple. ### `partitioning` By far, the most complex part of an AutoYaST profile. We can import the AutoYaST `partitioning` -section as is because the partitioning is handled by the same code in Agama and AutoyaST. +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. diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index 97a39a18fa..ad7bdd0680 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -109,7 +109,7 @@ def tmp_profile_path ) end - # @return [Hash] D-Installer profile + # @return [Hash] Agama profile def export_profile(profile) users = Agama::AutoYaST::UsersConverter.new(profile) {