From e21a294078b6a730213df722b677ae616bf802c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 4 Feb 2025 15:48:44 +0000 Subject: [PATCH 01/26] refactor(ruby): split profile fetching and conversion --- service/bin/agama-autoyast | 9 +- service/lib/agama/autoyast/converter.rb | 76 +--------- service/lib/agama/autoyast/profile_fetcher.rb | 107 ++++++++++++++ service/test/agama/autoyast/converter_test.rb | 119 ++-------------- .../agama/autoyast/profile_fetcher_test.rb | 132 ++++++++++++++++++ 5 files changed, 261 insertions(+), 182 deletions(-) create mode 100644 service/lib/agama/autoyast/profile_fetcher.rb create mode 100644 service/test/agama/autoyast/profile_fetcher_test.rb diff --git a/service/bin/agama-autoyast b/service/bin/agama-autoyast index 1df1e6eaed..7017728ee5 100755 --- a/service/bin/agama-autoyast +++ b/service/bin/agama-autoyast @@ -34,6 +34,7 @@ Dir.chdir(__dir__) do require "bundler/setup" end require "agama/autoyast/converter" +require "agama/autoyast/profile_fetcher" if ARGV.length != 2 warn "Usage: #{$PROGRAM_NAME} URL DIRECTORY" @@ -42,9 +43,13 @@ end begin url, directory = ARGV - converter = Agama::AutoYaST::Converter.new(url) - converter.to_agama(directory) + profile = Agama::AutoYaST::ProfileFetcher.new(url) rescue RuntimeError => e warn "Could not load the profile from #{url}: #{e}" exit 2 end + +converter = Agama::AutoYaST::Converter.new +agama_config = converter.to_agama(profile) +FileUtils.mkdir_p(directory) +File.write(File.join(directory, "autoinst.json"), agama_config.to_json) diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index b2cad924e6..5435e3dd58 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -50,73 +50,6 @@ module AutoYaST # TODO: handle invalid profiles (YAST_SKIP_XML_VALIDATION). # TODO: capture reported errors (e.g., via the Report.Error function). class Converter - # @param profile_url [String] Profile URL - def initialize(profile_url) - @profile_url = profile_url - end - - # Converts the profile into a set of files that Agama can process. - # - # @param dir [Pathname,String] Directory to write the profile. - def to_agama(dir) - path = Pathname(dir) - FileUtils.mkdir_p(path) - import_yast - profile = read_profile - File.write(path.join("autoinst.json"), export_profile(profile).to_json) - end - - private - - attr_reader :profile_url - - def copy_profile; end - - # @return [Hash] AutoYaST profile - def read_profile - FileUtils.mkdir_p(Yast::AutoinstConfig.profile_dir) - - # fetch the profile - Yast::AutoinstConfig.ParseCmdLine(profile_url) - Yast::ProfileLocation.Process - - # put the profile in the tmp directory - FileUtils.cp( - Yast::AutoinstConfig.xml_tmpfile, - tmp_profile_path - ) - - loop do - Yast::Profile.ReadXML(tmp_profile_path) - run_pre_scripts - break unless File.exist?(Yast::AutoinstConfig.modified_profile) - - FileUtils.cp(Yast::AutoinstConfig.modified_profile, tmp_profile_path) - FileUtils.rm(Yast::AutoinstConfig.modified_profile) - end - - Yast::Profile.current - end - - def run_pre_scripts - pre_scripts = Yast::Profile.current.fetch_as_hash("scripts") - .fetch_as_array("pre-scripts") - .map { |h| Y2Autoinstallation::PreScript.new(h) } - script_runner = Y2Autoinstall::ScriptRunner.new - - pre_scripts.each do |script| - script.create_script_file - script_runner.run(script) - end - end - - def tmp_profile_path - @tmp_profile_path ||= File.join( - Yast::AutoinstConfig.profile_dir, - "autoinst.xml" - ) - end - # Sections which have a corresponding reader. The reader is expected to be # named in Pascal case and adding "Reader" as suffix (e.g., "L10nReader"). SECTIONS = ["l10n", "product", "root", "scripts", "software", "storage", "user"].freeze @@ -126,7 +59,7 @@ def tmp_profile_path # It goes through the list of READERS and merges the results of all of them. # # @return [Hash] Agama profile - def export_profile(profile) + def to_agama(profile) SECTIONS.reduce({}) do |result, section| require "agama/autoyast/#{section}_reader" klass = "#{section}_reader".split("_").map(&:capitalize).join @@ -134,13 +67,6 @@ def export_profile(profile) result.merge(reader.read) end end - - def import_yast - Yast.import "AutoinstConfig" - Yast.import "AutoinstScripts" - Yast.import "Profile" - Yast.import "ProfileLocation" - end end end end diff --git a/service/lib/agama/autoyast/profile_fetcher.rb b/service/lib/agama/autoyast/profile_fetcher.rb new file mode 100644 index 0000000000..1a2bb96dde --- /dev/null +++ b/service/lib/agama/autoyast/profile_fetcher.rb @@ -0,0 +1,107 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Copyright (c) [2025] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" + +# :nodoc: +module Agama + module AutoYaST + # Retrieves an AutoYaST profile into an Agama one. + # + # It supports AutoYaST dynamic profiles features: pre-scripts, rules/classes and ERB. + # + # It generates an AutoYaST profile that can be converted into an Agama configuration using the + # {Agama::AutoYaST::Converter} class. + class ProfileFetcher + # @param profile_url [String] Profile URL + def initialize(profile_url) + @profile_url = profile_url + end + + # Converts the profile into a set of files that Agama can process. + # + # @param dir [Pathname,String] Directory to write the profile. + def fetch + import_yast + read_profile + 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::ProfileHash.new(Yast::Profile.current) + end + + def run_pre_scripts + pre_scripts = Yast::Profile.current.fetch_as_hash("scripts") + .fetch_as_array("pre-scripts") + .map { |h| Y2Autoinstallation::PreScript.new(h) } + script_runner = Y2Autoinstall::ScriptRunner.new + + pre_scripts.each do |script| + script.create_script_file + script_runner.run(script) + end + end + + def tmp_profile_path + @tmp_profile_path ||= File.join( + Yast::AutoinstConfig.profile_dir, + "autoinst.xml" + ) + end + + def import_yast + Yast.import "AutoinstConfig" + Yast.import "AutoinstScripts" + Yast.import "Profile" + Yast.import "ProfileLocation" + end + end + end +end diff --git a/service/test/agama/autoyast/converter_test.rb b/service/test/agama/autoyast/converter_test.rb index 44256439c5..57ab318ee8 100644 --- a/service/test/agama/autoyast/converter_test.rb +++ b/service/test/agama/autoyast/converter_test.rb @@ -20,120 +20,40 @@ # find current contact information at www.suse.com. require_relative "../../test_helper" +require "yast" require "agama/autoyast/converter" require "json" require "tmpdir" require "autoinstall/xml_checks" require "y2storage" +Yast.import "Profile" + describe Agama::AutoYaST::Converter do - let(:profile) { File.join(FIXTURES_PATH, "profiles", profile_name) } - let(:profile_name) { "simple.xml" } - let(:workdir) { Dir.mktmpdir } - let(:tmpdir) { Dir.mktmpdir } - let(:xml_validator) do - instance_double( - Y2Autoinstallation::XmlValidator, - valid?: xml_valid?, - errors: xml_errors - ) - end - let(:xml_valid?) { true } - let(:xml_errors) { [] } - let(:result) do - content = File.read(File.join(workdir, "autoinst.json")) - JSON.parse(content) - end - let(:storage_manager) do - instance_double( - Y2Storage::StorageManager, - probed: storage_probed, - probed_disk_analyzer: disk_analyzer + let(:profile) do + Yast::Profile.ReadXML( + File.join(FIXTURES_PATH, "profiles", profile_name) ) - end - let(:storage_probed) do - instance_double(Y2Storage::Devicegraph, disks: []) - end - let(:disk_analyzer) do - instance_double(Y2Storage::DiskAnalyzer, windows_partitions: [], linux_partitions: []) - end - - before do - stub_const("Y2Autoinstallation::XmlChecks::ERRORS_PATH", File.join(tmpdir, "errors")) - Yast.import "Installation" - allow(Yast::Installation).to receive(:sourcedir).and_return(File.join(tmpdir, "mount")) - allow(Yast::AutoinstConfig).to receive(:scripts_dir) - .and_return(File.join(tmpdir, "scripts")) - allow(Yast::AutoinstConfig).to receive(:profile_dir) - .and_return(File.join(tmpdir, "profile")) - allow(Yast::AutoinstConfig).to receive(:modified_profile) - .and_return(File.join(tmpdir, "profile", "modified.xml")) - allow(Y2Autoinstallation::XmlValidator).to receive(:new).and_return(xml_validator) - allow(Y2Storage::StorageManager).to receive(:instance).and_return(storage_manager) + Yast::Profile.current end - after do - FileUtils.remove_entry(workdir) - FileUtils.remove_entry(tmpdir) - end + let(:profile_name) { "simple.xml" } subject do - described_class.new("file://#{profile}") + described_class.new end describe "#to_agama" do - context "when some pre-script is defined" do - let(:profile_name) { "pre-scripts.xml" } - let(:profile) { File.join(tmpdir, profile_name) } - - before do - allow(Yast::AutoinstConfig).to receive(:scripts_dir) - .and_return(File.join(tmpdir, "scripts")) - allow(Yast::AutoinstConfig).to receive(:profile_dir) - .and_return(File.join(tmpdir, "profile")) - - # Adapt the script to use the new tmp directory - profile_content = File.read(File.join(FIXTURES_PATH, "profiles", profile_name)) - profile_content.gsub!("/tmp/profile/", "#{tmpdir}/profile/") - File.write(profile, profile_content) - end - - it "runs the script" do - subject.to_agama(workdir) - expect(result["product"]).to include("id" => "Tumbleweed") - end - end - - context "when the profile contains some ERB" do - let(:profile_name) { "simple.xml.erb" } - - it "evaluates the ERB code" do - subject.to_agama(workdir) - expect(result["l10n"]).to include( - "languages" => ["en_US.UTF-8", "es_ES.UTF-8"] - ) - end - end - - context "when the profile uses rules" do - let(:profile_name) { "profile/" } - - it "evaluates the rules" do - subject.to_agama(workdir) - expect(result["product"]).to include("id" => "Tumbleweed") - end - end - context "when a product is selected" do it "exports the selected product" do - subject.to_agama(workdir) + result = subject.to_agama(profile) expect(result["product"]).to include("id" => "Tumbleweed") end end context "when partitioning is defined" do it "exports the drives information" do - subject.to_agama(workdir) + result = subject.to_agama(profile) expect(result["legacyAutoyastStorage"]).to include({ "device" => "/dev/vda", "use" => "all" @@ -143,7 +63,7 @@ context "when the root password and/or public SSH key are set" do it "exports the root password and/or public SSH key" do - subject.to_agama(workdir) + result = subject.to_agama(profile) expect(result["root"]).to include("password" => "nots3cr3t", "sshPublicKey" => "ssh-rsa ...") end @@ -151,14 +71,14 @@ context "when a non-system user is defined" do it "exports the user information" do - subject.to_agama(workdir) + result = subject.to_agama(profile) expect(result["user"]).to include("userName" => "jane", "password" => "12345678", "fullName" => "Jane Doe") end end it "exports l10n settings" do - subject.to_agama(workdir) + result = subject.to_agama(profile) expect(result["l10n"]).to include( "languages" => ["en_US.UTF-8"], "timezone" => "Atlantic/Canary", @@ -166,15 +86,4 @@ ) end end - - context "when an invalid profile is given" do - let(:xml_valid?) { false } - let(:xml_errors) { ["Some validation error"] } - let(:profile_name) { "invalid.xml" } - - it "reports the problem" do - expect(Yast2::Popup).to receive(:show) - subject.to_agama(workdir) - end - end end diff --git a/service/test/agama/autoyast/profile_fetcher_test.rb b/service/test/agama/autoyast/profile_fetcher_test.rb new file mode 100644 index 0000000000..cf67e5c45e --- /dev/null +++ b/service/test/agama/autoyast/profile_fetcher_test.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +# Copyright (c) [2025] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "agama/autoyast/profile_fetcher" +require "json" +require "tmpdir" +require "autoinstall/xml_checks" +require "y2storage" + +describe Agama::AutoYaST::ProfileFetcher do + let(:profile) { File.join(FIXTURES_PATH, "profiles", profile_name) } + let(:profile_name) { "simple.xml" } + let(:tmpdir) { Dir.mktmpdir } + let(:xml_validator) do + instance_double( + Y2Autoinstallation::XmlValidator, + valid?: xml_valid?, + errors: xml_errors + ) + end + let(:xml_valid?) { true } + let(:xml_errors) { [] } + let(:storage_manager) do + instance_double( + Y2Storage::StorageManager, + probed: storage_probed, + probed_disk_analyzer: disk_analyzer + ) + end + let(:storage_probed) do + instance_double(Y2Storage::Devicegraph, disks: []) + end + let(:disk_analyzer) do + instance_double(Y2Storage::DiskAnalyzer, windows_partitions: [], linux_partitions: []) + end + + before do + stub_const("Y2Autoinstallation::XmlChecks::ERRORS_PATH", File.join(tmpdir, "errors")) + Yast.import "Installation" + allow(Yast::Installation).to receive(:sourcedir).and_return(File.join(tmpdir, "mount")) + allow(Yast::AutoinstConfig).to receive(:scripts_dir) + .and_return(File.join(tmpdir, "scripts")) + allow(Yast::AutoinstConfig).to receive(:profile_dir) + .and_return(File.join(tmpdir, "profile")) + allow(Yast::AutoinstConfig).to receive(:modified_profile) + .and_return(File.join(tmpdir, "profile", "modified.xml")) + allow(Y2Autoinstallation::XmlValidator).to receive(:new).and_return(xml_validator) + allow(Y2Storage::StorageManager).to receive(:instance).and_return(storage_manager) + end + + after do + FileUtils.remove_entry(tmpdir) + end + + subject do + described_class.new("file://#{profile}") + end + + describe "#fetch" do + context "when some pre-script is defined" do + let(:profile_name) { "pre-scripts.xml" } + let(:profile) { File.join(tmpdir, profile_name) } + + before do + allow(Yast::AutoinstConfig).to receive(:scripts_dir) + .and_return(File.join(tmpdir, "scripts")) + allow(Yast::AutoinstConfig).to receive(:profile_dir) + .and_return(File.join(tmpdir, "profile")) + + # Adapt the script to use the new tmp directory + profile_content = File.read(File.join(FIXTURES_PATH, "profiles", profile_name)) + profile_content.gsub!("/tmp/profile/", "#{tmpdir}/profile/") + File.write(profile, profile_content) + end + + it "runs the script" do + result = subject.fetch + expect(result["software"]).to include("products" => ["Tumbleweed"]) + end + end + + context "when the profile contains some ERB" do + let(:profile_name) { "simple.xml.erb" } + + it "evaluates the ERB code" do + result = subject.fetch + expect(result["language"]).to include( + "language" => "en_US" + ) + end + end + + context "when the profile uses rules" do + let(:profile_name) { "profile/" } + + it "evaluates the rules" do + result = subject.fetch + expect(result["software"]).to include("products" => ["Tumbleweed"]) + end + end + end + + context "when an invalid profile is given" do + let(:xml_valid?) { false } + let(:xml_errors) { ["Some validation error"] } + let(:profile_name) { "invalid.xml" } + + it "reports the problem" do + expect(Yast2::Popup).to receive(:show) + subject.fetch + end + end +end From 25d328a0bcbff31986eb1bd08ad1a634e17bbb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 4 Feb 2025 12:13:17 +0000 Subject: [PATCH 02/26] feat(ruby): find unsupported elements in the AutoYaST profile --- service/lib/agama/autoyast/profile_checker.rb | 58 +++++++++ .../lib/agama/autoyast/profile_description.rb | 70 +++++++++++ service/share/autoyast-compat.json | 111 ++++++++++++++++++ .../agama/autoyast/profile_checker_test.rb | 80 +++++++++++++ .../autoyast/profile_description_test.rb | 37 ++++++ 5 files changed, 356 insertions(+) create mode 100644 service/lib/agama/autoyast/profile_checker.rb create mode 100644 service/lib/agama/autoyast/profile_description.rb create mode 100644 service/share/autoyast-compat.json create mode 100644 service/test/agama/autoyast/profile_checker_test.rb create mode 100644 service/test/agama/autoyast/profile_description_test.rb diff --git a/service/lib/agama/autoyast/profile_checker.rb b/service/lib/agama/autoyast/profile_checker.rb new file mode 100644 index 0000000000..fca0861f8f --- /dev/null +++ b/service/lib/agama/autoyast/profile_checker.rb @@ -0,0 +1,58 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Copyright (c) [2025] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/autoyast/profile_description" + +module Agama + module AutoYaST + # This class checks an AutoYaST profile and determines which unsupported elements are used. + class ProfileChecker + def find_unsupported(profile) + description = ProfileDescription.load + elements = elements_from(profile) + + elements.map do |e| + normalized_key = e.gsub(/\[\d+\]/, "[]") + description.find_element(normalized_key) + end.compact + end + + private + + def elements_from(profile, parent = "") + return [] unless profile.is_a?(Hash) + + profile.map do |k, v| + current = parent.empty? ? k : "#{parent}.#{k}" + + children = if v.is_a?(Array) + v.map.with_index { |e, i| elements_from(e, "#{parent}.#{k}[#{i}]") } + else + elements_from(v, k) + end + + [current, *children] + end.flatten + end + end + end +end diff --git a/service/lib/agama/autoyast/profile_description.rb b/service/lib/agama/autoyast/profile_description.rb new file mode 100644 index 0000000000..4189adaa98 --- /dev/null +++ b/service/lib/agama/autoyast/profile_description.rb @@ -0,0 +1,70 @@ +#!/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 "json" + +module Agama + module AutoYaST + # Describes an AutoYaST element and its support level in Agama + class ProfileElement + # @return [String] Element key. + attr_reader :key + # @return [Symbol] Support level (:no or :planned). + attr_reader :support + # @return [String] How to handle the case where the attribute is needed. + attr_reader :advice + + def initialize(key, support) + @key = key + @support = support + end + end + + # Describes the AutoYaST profile format. + # + # At this point, it only includes the information of the unsupported sections. + class ProfileDescription + attr_reader :elements + + DESCRIPTION_PATH = File.expand_path("#{__dir__}/../../../share/autoyast-compat.json") + + class << self + def load(path = DESCRIPTION_PATH) + json = JSON.load_file(path) + elements = json.map do |e| + ProfileElement.new(e["key"], e["support"].to_sym) + end + new(elements) + end + end + + def initialize(elements) + @elements = elements + end + + def find_element(key) + section = key.split(".").first + elements.find { |e| [key, section].include?(e.key) } + end + end + end +end diff --git a/service/share/autoyast-compat.json b/service/share/autoyast-compat.json new file mode 100644 index 0000000000..24ffd3349e --- /dev/null +++ b/service/share/autoyast-compat.json @@ -0,0 +1,111 @@ +[ + { + "key": "networking.backend", + "support": "no" + }, + { + "key": "services-manager", + "support": "planned", + "advice": "You can use a post-installation script to handle these cases." + }, + { + "key": "iscsi-client", + "support": "planned" + }, + { + "key": "networking.dhcp_options", + "support": "no" + }, + { + "key": "networking.dns", + "support": "no" + }, + { + "key": "networking.keep_install_network", + "support": "no" + }, + { + "key": "networking.managed", + "support": "no" + }, + { + "key": "networking.modules", + "support": "no" + }, + { + "key": "networking.net-udev", + "support": "no" + }, + { + "key": "networking.routing", + "support": "no" + }, + { + "key": "networking.s390-devices", + "support": "no" + }, + { + "key": "networking.setup_before_proposal", + "support": "no" + }, + { + "key": "networking.strict_IP_check_timeout", + "support": "no" + }, + { + "key": "networking.virt_bridge_proposal", + "support": "no" + }, + { + "key": "scripts.pre-script.feedback", + "support": "no" + }, + { + "key": "scripts.pre-script[].rerun", + "support": "no" + }, + { + "key": "scripts.pre-script[].intepreter", + "support": "no" + }, + { + "key": "keyboard.capslock", + "support": "no" + }, + { + "key": "keyboard.delay", + "support": "no" + }, + { + "key": "keyboard.discaps", + "support": "no" + }, + { + "key": "keyboard.numlock", + "support": "no" + }, + { + "key": "keyboard.rate", + "support": "no" + }, + { + "key": "keyboard.scrlock", + "support": "no" + }, + { + "key": "keyboard.tty", + "support": "no" + }, + { + "key": "scripts.pre-script.feedback", + "support": "no" + }, + { + "key": "scripts.pre-script.rerun", + "support": "no" + }, + { + "key": "scripts.pre-script.intepreter", + "support": "no" + } +] diff --git a/service/test/agama/autoyast/profile_checker_test.rb b/service/test/agama/autoyast/profile_checker_test.rb new file mode 100644 index 0000000000..1070dd8523 --- /dev/null +++ b/service/test/agama/autoyast/profile_checker_test.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# Copyright (c) [2025] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "agama/autoyast/profile_checker" + +describe Agama::AutoYaST::ProfileChecker do + describe "#find_unsupported" do + context "when no unsupported elements are included" do + let(:profile) do + { "software" => {} } + end + + it "returns an empty array" do + expect(subject.find_unsupported(profile)).to eq([]) + end + end + + context "when an unsupported section is included" do + let(:profile) do + { "iscsi-client" => {} } + end + + it "returns an array with the unsupported element" do + expect(subject.find_unsupported(profile)).to contain_exactly( + an_object_having_attributes(key: "iscsi-client") + ) + end + end + + context "when an unsupported element is included" do + let(:profile) do + { "networking" => { "backend" => "wicked" } } + end + + it "returns an array with the unsupported element" do + expect(subject.find_unsupported(profile)).to contain_exactly( + an_object_having_attributes(key: "networking.backend") + ) + end + end + + context "when an unsupported nested element is included" do + let(:profile) do + { + "scripts" => { + "pre-script" => [ + { "location" => "http://example.net/pre-script.sh", + "rerun" => true } + ] + } + } + end + + it "returns an array with the unsupported element" do + expect(subject.find_unsupported(profile)).to contain_exactly( + an_object_having_attributes(key: "scripts.pre-script[].rerun") + ) + end + end + end +end diff --git a/service/test/agama/autoyast/profile_description_test.rb b/service/test/agama/autoyast/profile_description_test.rb new file mode 100644 index 0000000000..d4748dbff4 --- /dev/null +++ b/service/test/agama/autoyast/profile_description_test.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Copyright (c) [2025] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "agama/autoyast/profile_description" + +describe Agama::AutoYaST::ProfileDescription do + let(:subject) { described_class.load } + + describe "#find_element" do + context "when the element exists" do + it "returns the element data" do + element = subject.find_element("networking.backend") + expect(element.key).to eq("networking.backend") + expect(element.support).to eq(:no) + end + end + end +end From 84915a29c5fb0f4f17fee5f521ed226187e3b2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 5 Feb 2025 13:10:46 +0000 Subject: [PATCH 03/26] feat(ruby): report AutoYaST unsupported elements --- service/bin/agama-autoyast | 24 ++--- .../lib/agama/autoyast/profile_reporter.rb | 74 ++++++++++++++ service/lib/agama/commands/agama_autoyast.rb | 98 +++++++++++++++++++ 3 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 service/lib/agama/autoyast/profile_reporter.rb create mode 100644 service/lib/agama/commands/agama_autoyast.rb diff --git a/service/bin/agama-autoyast b/service/bin/agama-autoyast index 7017728ee5..4281076925 100755 --- a/service/bin/agama-autoyast +++ b/service/bin/agama-autoyast @@ -33,8 +33,8 @@ require "rubygems" Dir.chdir(__dir__) do require "bundler/setup" end -require "agama/autoyast/converter" -require "agama/autoyast/profile_fetcher" + +require "agama/commands/agama_autoyast" if ARGV.length != 2 warn "Usage: #{$PROGRAM_NAME} URL DIRECTORY" @@ -43,13 +43,15 @@ end begin url, directory = ARGV - profile = Agama::AutoYaST::ProfileFetcher.new(url) -rescue RuntimeError => e - warn "Could not load the profile from #{url}: #{e}" - exit 2 + result = Agama::Commands::AgamaAutoYaST.new(url, directory).run + if !result + warn "Did not convert the profile (canceled by the user)." + exit 2 + end +rescue Agama::Commands::CouldNotFetchProfile + warn "Could not fetch the AutoYaST profile." + exit 3 +rescue Agama::Commands::CouldNotWriteAgamaConfig + warn "Could not write the Agama configuration." + exit 4 end - -converter = Agama::AutoYaST::Converter.new -agama_config = converter.to_agama(profile) -FileUtils.mkdir_p(directory) -File.write(File.join(directory, "autoinst.json"), agama_config.to_json) diff --git a/service/lib/agama/autoyast/profile_reporter.rb b/service/lib/agama/autoyast/profile_reporter.rb new file mode 100644 index 0000000000..cd480eedf5 --- /dev/null +++ b/service/lib/agama/autoyast/profile_reporter.rb @@ -0,0 +1,74 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Copyright (c) [2025] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" + +# :nodoc: +module Agama + module AutoYaST + class ProfileReporter + include Yast::I18n + + # Constructor + # + # @param questions_client [Agama::DBus::Clients::Questions] + # @param logger [Logger] + def initialize(questions_client, logger) + textdomain "agama" + + @questions_client = questions_client + @logger = logger + end + + # Reports the problems and decide whether to continue or not. + # + # @param elements [Array] List of unsupported elements. + def report(elements) + keys = elements.map { |e| e.key }.join(", ") + unsupported = elements.select { |e| e.support == :no } + planned = elements.select { |e| e.support == :planned } + + message = format( + _("Some AutoYaST elements are not supported."), keys: keys + ) + question = Agama::Question.new( + qclass: "autoyast.unsupported", + text: message, + options: [:Continue, :Abort], + default_option: :Continue, + data: { + "planned" => planned.map(&:key).join(","), + "unsupported" => unsupported.map(&:key).join(",") + } + ) + + questions_client.ask(question) do |question_client| + question_client.answer == :Continue + end + end + + private + + attr_reader :questions_client, :logger + end + end +end diff --git a/service/lib/agama/commands/agama_autoyast.rb b/service/lib/agama/commands/agama_autoyast.rb new file mode 100644 index 0000000000..410f52f65e --- /dev/null +++ b/service/lib/agama/commands/agama_autoyast.rb @@ -0,0 +1,98 @@ +#!/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 "agama/autoyast/converter" +require "agama/autoyast/profile_fetcher" +require "agama/autoyast/profile_reporter" +require "agama/autoyast/profile_checker" +require "agama/dbus/clients/questions" + +module Agama + module Commands + class CouldNotFetchProfile < StandardError; end + class CouldNotWriteAgamaConfig < StandardError; end + + # Command to convert an AutoYaST profile to an Agama configuration. + # + # It fetches the profile, checks for unsupported elements and converts it to an Agama + # configuration file. + class AgamaAutoYaST + def initialize(url, directory) + @url = url + @directory = directory + @logger = Logger.new($stdout) + end + + # Run the command fetching, checking, converting and writing the Agama configuration. + def run + profile = fetch_profile + unsupported = check_profile(profile) + return false unless report_unsupported(unsupported) + + write_agama_config(profile) + end + + private + + attr_reader :url, :directory, :logger + + # Fetch the AutoYaST profile from the given URL. + def fetch_profile + Agama::AutoYaST::ProfileFetcher.new(url).fetch + rescue RuntimeError + raise CouldNotFetchProfile + end + + # Check the profile for unsupported + def check_profile(profile) + checker = Agama::AutoYaST::ProfileChecker.new + checker.find_unsupported(profile) + end + + def report_unsupported(elements) + return true if elements.empty? + + reporter = Agama::AutoYaST::ProfileReporter.new(questions_client, logger) + reporter.report(elements) + end + + def write_agama_config(profile) + converter = Agama::AutoYaST::Converter.new + agama_config = converter.to_agama(profile) + FileUtils.mkdir_p(directory) + File.write(File.join(directory, "autoinst.json"), agama_config.to_json) + rescue StandardError + raise CouldNotWriteAgamaConfig + end + + def questions_client + @questions_client ||= Agama::DBus::Clients::Questions.new(logger: logger) + end + end + + if ARGV.length != 2 + warn "Usage: #{$PROGRAM_NAME} URL DIRECTORY" + exit 1 + end + end +end From 682381b760294f498912bebfa483c27a1ea9131e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 6 Feb 2025 10:12:27 +0000 Subject: [PATCH 04/26] feat: extend the AutoYaST profile definition * Improve the structure of the data, adding proper support for nested elements. --- service/Rakefile | 31 ++ service/lib/agama/autoyast/profile_checker.rb | 12 +- .../lib/agama/autoyast/profile_description.rb | 83 ++++- service/lib/tasks/autoyast.rb | 107 ++++++ service/share/autoyast-compat.json | 325 +++++++++++++----- .../agama/autoyast/profile_checker_test.rb | 4 +- 6 files changed, 459 insertions(+), 103 deletions(-) create mode 100644 service/Rakefile create mode 100644 service/lib/tasks/autoyast.rb diff --git a/service/Rakefile b/service/Rakefile new file mode 100644 index 0000000000..498e831fdd --- /dev/null +++ b/service/Rakefile @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Copyright (c) [2025] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +$LOAD_PATH.unshift File.expand_path("lib", __dir__) +require_relative "lib/tasks/autoyast" + +desc "Documentation tasks" +namespace :doc do + desc "Generate the AutoYaST compatibility documentation" + task :autoyast_compat do + puts Agama::Tasks::AutoYaSTCompatGenerator.new.generate + end +end diff --git a/service/lib/agama/autoyast/profile_checker.rb b/service/lib/agama/autoyast/profile_checker.rb index fca0861f8f..63009bf96a 100644 --- a/service/lib/agama/autoyast/profile_checker.rb +++ b/service/lib/agama/autoyast/profile_checker.rb @@ -25,19 +25,29 @@ module Agama module AutoYaST # This class checks an AutoYaST profile and determines which unsupported elements are used. + # + # It does not report unknown elements. class ProfileChecker + # Finds unsupported profile elements. + # + # @param profile [Yast::ProfileHash] AutoYaST profile to check + # @return [Array] List of unsupported elements def find_unsupported(profile) description = ProfileDescription.load elements = elements_from(profile) elements.map do |e| normalized_key = e.gsub(/\[\d+\]/, "[]") - description.find_element(normalized_key) + element = description.find_element(normalized_key) + element unless element&.supported? end.compact end private + # Returns the elements from the profile + # + # @return [Array] List of element IDs (e.g., "networking.backend") def elements_from(profile, parent = "") return [] unless profile.is_a?(Hash) diff --git a/service/lib/agama/autoyast/profile_description.rb b/service/lib/agama/autoyast/profile_description.rb index 4189adaa98..7bbcdc7069 100644 --- a/service/lib/agama/autoyast/profile_description.rb +++ b/service/lib/agama/autoyast/profile_description.rb @@ -30,40 +30,101 @@ class ProfileElement attr_reader :key # @return [Symbol] Support level (:no or :planned). attr_reader :support - # @return [String] How to handle the case where the attribute is needed. - attr_reader :advice + # @return [String] Additional information about the element. + attr_reader :notes + # @return [String] Agama equivalent attribute + attr_reader :agama + # @return [Array] Children elements. + attr_reader :children - def initialize(key, support) + class << self + def from_json(json, parent = nil) + support = json.key?("children") ? :yes : json["support"].to_sym + key = parent ? "#{parent}.#{json["key"]}" : json["key"] + children = json.fetch("children", []).map do |json_element| + ProfileElement.from_json(json_element, key) + end + ProfileElement.new(key, support, json["agama"], json["notes"], children) + end + end + + def initialize(key, support, agama, hint, children = []) @key = key @support = support + @hint = hint + @agama = agama + @children = children + end + + # Whether the element is supported. + # + # @return [Boolean] + def supported? + @support == :yes + end + + # Whether it is a top level element. + # + # @return [Boolean] + def top_level? + !key.include?(".") + end + + # Short key name. + # + # @return [String] + def short_key + key.split(".").last end end # Describes the AutoYaST profile format. - # - # At this point, it only includes the information of the unsupported sections. class ProfileDescription attr_reader :elements - DESCRIPTION_PATH = File.expand_path("#{__dir__}/../../../share/autoyast-compat.json") + DEFAULT_PATH = File.expand_path("#{__dir__}/../../../share/autoyast-compat.json") class << self - def load(path = DESCRIPTION_PATH) + # Load the AutoYaST profile definition. + # + # @param path [String] Path of the profile definition. + # @return [ProfileDescription] + def load(path = DEFAULT_PATH) json = JSON.load_file(path) - elements = json.map do |e| - ProfileElement.new(e["key"], e["support"].to_sym) + elements = json.map do |json_element| + ProfileElement.from_json(json_element) end new(elements) end end + # Constructor + # + # @param elements [Array] List of profile elements def initialize(elements) @elements = elements + @index = create_index(elements) end + # Find an element by its key + # + # @param key [String] Element key (e.g., "networking.base") def find_element(key) - section = key.split(".").first - elements.find { |e| [key, section].include?(e.key) } + element = @index.dig(*key.split(".")) + element.is_a?(ProfileElement) ? element : nil + end + + private + + # Creates an index to make searching for an element faster and easier. + def create_index(elements) + elements.each_with_object({}) do |e, index| + index[e.short_key] = if e.children.empty? + e + else + create_index(e.children) + end + end end end end diff --git a/service/lib/tasks/autoyast.rb b/service/lib/tasks/autoyast.rb new file mode 100644 index 0000000000..cd6b7437eb --- /dev/null +++ b/service/lib/tasks/autoyast.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/autoyast/profile_description" + +module Agama + module Tasks + # Generates the AutoYaST compatibility reference + # + # The document describes which elements are supported, unsupported or planned to be supported. + # It uses markdown to make it easier to integrate the document in Agama's website. + class AutoYaSTCompatGenerator + attr_reader :description + + def initialize + @description = Agama::AutoYaST::ProfileDescription.load + end + + # Generates the document in Markdown format + def generate + lines = ["# AutoYaST compatibility reference"] + + top_level = description.elements.select(&:top_level?) + + top_level.each do |e| + lines.concat(section(e)) + end + + lines.join("\n") + end + + private + + def section(element, level = 1) + title = "#" * (level + 1) + lines = ["#{title} #{element.key}"] + + lines << "" + lines << notes_for(element) + lines << "" + + scalar, complex = element.children.partition do |e| + e.children.empty? + end + + lines.concat(elements_table(scalar)) + + complex.each_with_object(lines) do |e, all| + all.concat(section(e, level + 1)) + end + end + + # Generates a table describing the support level of the elements. + # + # @param elements [Array] Elements to describe. + def elements_table(elements) + return [] if elements.empty? + + lines = [ + "| AutoYaST | Supported | Agama | Notes |", + "|----------|-----------|-------|-------|" + ] + elements.each do |e| + lines << "| #{e.short_key} | #{e.support} | #{e.agama} | #{e.notes} |" + end + lines << "" + lines + end + + # Generates the notes for a given element. + # + # @param element [ProfileElement] Profile element to generate the notes for + def notes_for(element) + content = case element.support + when :yes + "This section is supported." + when :no + "This section is not supported." + when :planned + "There are plans to support this section in the future." + else + "Support for this element is still undecided." + end + + element.notes ? "#{content} #{element.notes}" : content + end + end + end +end diff --git a/service/share/autoyast-compat.json b/service/share/autoyast-compat.json index 24ffd3349e..145020d117 100644 --- a/service/share/autoyast-compat.json +++ b/service/share/autoyast-compat.json @@ -1,111 +1,258 @@ [ + { "key": "add-on", "support": "no" }, + { "key": "audit-laf", "support": "no" }, + { "key": "auth-client", "support": "no" }, + { "key": "configuration_management", "support": "no" }, + { "key": "deploy_image", "support": "no" }, + { "key": "dhcp-server", "support": "no" }, + { "key": "dns-server", "support": "no" }, + { "key": "fcoe-client", "support": "no" }, + { "key": "files", "support": "planned" }, + { "key": "firstboot", "support": "no" }, + { "key": "ftp-server", "support": "no" }, + { "key": "general", "support": "no" }, + { "key": "groups", "support": "no" }, + { "key": "host", "support": "no" }, + { "key": "http-server", "support": "no" }, { - "key": "networking.backend", - "support": "no" + "key": "keyboard", + "children": [ + { "key": "keymap", "support": "yes", "agama": "localization.keyboard" }, + { "key": "capslock", "support": "no" }, + { "key": "delay", "support": "no" }, + { "key": "discaps", "support": "no" }, + { "key": "numlock", "support": "no" }, + { "key": "rate", "support": "no" }, + { "key": "scrlock", "support": "no" }, + { "key": "tty", "support": "no" } + ] }, { - "key": "services-manager", - "support": "planned", - "advice": "You can use a post-installation script to handle these cases." - }, - { - "key": "iscsi-client", - "support": "planned" - }, - { - "key": "networking.dhcp_options", - "support": "no" - }, - { - "key": "networking.dns", - "support": "no" - }, - { - "key": "networking.keep_install_network", - "support": "no" - }, - { - "key": "networking.managed", - "support": "no" - }, - { - "key": "networking.modules", - "support": "no" - }, - { - "key": "networking.net-udev", - "support": "no" - }, - { - "key": "networking.routing", - "support": "no" - }, - { - "key": "networking.s390-devices", - "support": "no" + "key": "language", + "children": [ + { "key": "language", "support": "yes", "agama": "localization.language" }, + { "key": "languages", "support": "no" } + ] }, { - "key": "networking.setup_before_proposal", - "support": "no" + "key": "networking", + "children": [ + { "key": "backend", "support": "no" }, + { "key": "dhcp_options", "support": "no" }, + { "key": "dns", "support": "no" }, + { "key": "keep_install_network", "support": "no" }, + { "key": "managed", "support": "no" }, + { "key": "modules", "support": "no" }, + { "key": "net-udev", "support": "no" }, + { "key": "routing", "support": "no" }, + { "key": "s390-devices", "support": "no" }, + { "key": "setup_before_proposal", "support": "no" }, + { "key": "strict_IP_check_timeout", "support": "no" }, + { "key": "virt_bridge_proposal", "support": "no" } + ] }, { - "key": "networking.strict_IP_check_timeout", - "support": "no" - }, - { - "key": "networking.virt_bridge_proposal", - "support": "no" - }, - { - "key": "scripts.pre-script.feedback", - "support": "no" - }, - { - "key": "scripts.pre-script[].rerun", - "support": "no" - }, - { - "key": "scripts.pre-script[].intepreter", - "support": "no" - }, - { - "key": "keyboard.capslock", - "support": "no" - }, - { - "key": "keyboard.delay", - "support": "no" - }, - { - "key": "keyboard.discaps", - "support": "no" - }, - { - "key": "keyboard.numlock", - "support": "no" + "key": "services-manager", + "support": "planned", + "notes": "You can use a post-installation script to handle these cases." }, { - "key": "keyboard.rate", - "support": "no" + "key": "scripts", + "children": [ + { + "key": "pre-scripts[]", + "children": [ + { + "key": "filename", + "support": "yes", + "agama": "scripts.pre[].name" + }, + { "key": "location", "support": "yes", "agama": "scripts.pre[].url" }, + { "key": "source", "support": "yes", "agama": "scripts.pre[].body" }, + { + "key": "interpreter", + "support": "no", + "notes": "Use the shebang line in your scripts." + }, + { "key": "feedback", "support": "no" }, + { "key": "feedback_type", "support": "no" }, + { "key": "debug", "support": "no" }, + { "key": "notification", "support": "no" }, + { "key": "param-list", "support": "no" }, + { "key": "rerun", "support": "no" } + ] + }, + { + "key": "chroot-scripts[]", + "agama": "scripts.post[]", + "children": [ + { + "key": "filename", + "support": "yes", + "agama": "scripts.chroot[].name" + }, + { + "key": "location", + "support": "yes", + "agama": "scripts.chroot[].url" + }, + { + "key": "source", + "support": "yes", + "agama": "scripts.chroot[].body" + }, + { + "key": "interpreter", + "support": "no", + "notes": "Use the shebang line in your scripts." + }, + { "key": "feedback", "support": "no" }, + { "key": "feedback_type", "support": "no" }, + { "key": "debug", "support": "no" }, + { "key": "notification", "support": "no" }, + { "key": "param-list", "support": "no" }, + { "key": "rerun", "support": "no" } + ] + }, + { + "key": "post-scripts[]", + "agama": "scripts.init[]", + "children": [ + { + "key": "filename", + "support": "yes", + "agama": "scripts.init[].name" + }, + { + "key": "location", + "support": "yes", + "agama": "scripts.init[].url" + }, + { "key": "source", "support": "yes", "agama": "scripts.init[].body" }, + { + "key": "interpreter", + "support": "no", + "notes": "Use the shebang line in your scripts." + }, + { "key": "feedback", "support": "no" }, + { "key": "feedback_type", "support": "no" }, + { "key": "debug", "support": "no" }, + { "key": "notification", "support": "no" }, + { "key": "param-list", "support": "no" }, + { "key": "rerun", "support": "no" } + ] + }, + { + "key": "init-scripts[]", + "agama": "scripts.init[]", + "children": [ + { + "key": "filename", + "support": "yes", + "agama": "scripts.init[].name" + }, + { + "key": "location", + "support": "yes", + "agama": "scripts.init[].url" + }, + { "key": "source", "support": "yes", "agama": "scripts.init[].body" }, + { "key": "rerun", "support": "no" } + ] + } + ] }, + { "key": "mail", "support": "no" }, + { "key": "nfs", "support": "no" }, + { "key": "nfs_server", "support": "no" }, + { "key": "nis", "support": "no" }, + { "key": "nis_server", "support": "no" }, + { "key": "ntp-client", "support": "no" }, + { "key": "printer", "support": "no" }, { - "key": "keyboard.scrlock", - "support": "no" + "key": "proxy", + "support": "planned", + "notes": "Set the proxy using the kernels' command line" }, + { "key": "report", "support": "no" }, + { "key": "samba-client", "support": "no" }, { - "key": "keyboard.tty", - "support": "no" + "key": "software", + "children": [ + { "key": "do_online_update", "support": "no", "notes": "No 2nd stage" }, + { "key": "install_recommended", "support": "no" }, + { "key": "instsource", "support": "no" }, + { "key": "kernel", "support": "no" }, + { "key": "packages[]", "support": "planned" }, + { "key": "post-packages[]", "support": "no" }, + { "key": "patterns[]", "support": "yes", "agama": "software.patterns[]" }, + { "key": "products[]", "support": "yes", "agama": "software.id" }, + { "key": "remove-packages[]", "support": "no" }, + { "key": "remove-patterns[]", "support": "no" }, + { "key": "remove-products[]", "support": "no" } + ] }, + { "key": "sound", "support": "no" }, + { "key": "squid", "support": "no" }, + { "key": "ssh_import", "support": "no" }, { - "key": "scripts.pre-script.feedback", - "support": "no" + "key": "suse_register", + "children": [ + { + "key": "do_registration", + "support": "yes", + "notes": "The while section is ignored if \"false\"" + }, + { + "key": "email", + "support": "yes", + "agama": "product.registrationEmail" + }, + { "key": "install_updates", "support": "no" }, + { + "key": "reg_code", + "support": "yes", + "agama": "product.registrationCode" + }, + { "key": "reg_server", "support": "planned" }, + { "key": "reg_server_cert", "support": "planned" }, + { "key": "reg_server_cert_fingerprint", "support": "planned" }, + { "key": "reg_server_cert_fingerprint_type", "support": "planned" }, + { "key": "addons", "support": "planned" }, + { "key": "slp_discovery", "support": "planned" } + ] }, + { "key": "sysconfig", "support": "no" }, + { "key": "tftp-server", "support": "no" }, { - "key": "scripts.pre-script.rerun", - "support": "no" + "key": "timezone", + "children": [ + { "key": "timezone", "support": "yes", "agama": "localization.timezone" }, + { "key": "hwclock", "support": "no" } + ] }, + { "key": "upgrade", "support": "no" }, + { "key": "iscsi-client", "support": "planned" }, { - "key": "scripts.pre-script.intepreter", - "support": "no" + "key": "users[]", + "support": "yes", + "notes": "Only the root and the first user are considered.", + "children": [ + { "key": "username", "support": "yes", "agama": "user.userName" }, + { "key": "fullName", "support": "yes", "agama": "user.fullName" }, + { "key": "password", "support": "yes", "agama": "user.password" }, + { + "key": "encrypted", + "support": "yes", + "agama": "user.hashedPassword", + "notes": "If set to true, it uses \"hashedPassword\" instead of \"password\"" + }, + { + "key": "authorized_keys", + "support": "yes", + "agama": "root.sshPublicKey", + "notes": "It only considers one password and only for the root user." + } + ] } ] diff --git a/service/test/agama/autoyast/profile_checker_test.rb b/service/test/agama/autoyast/profile_checker_test.rb index 1070dd8523..4da56bc623 100644 --- a/service/test/agama/autoyast/profile_checker_test.rb +++ b/service/test/agama/autoyast/profile_checker_test.rb @@ -62,7 +62,7 @@ let(:profile) do { "scripts" => { - "pre-script" => [ + "pre-scripts" => [ { "location" => "http://example.net/pre-script.sh", "rerun" => true } ] @@ -72,7 +72,7 @@ it "returns an array with the unsupported element" do expect(subject.find_unsupported(profile)).to contain_exactly( - an_object_having_attributes(key: "scripts.pre-script[].rerun") + an_object_having_attributes(key: "scripts.pre-scripts[].rerun") ) end end From f8abcb6e210d323b7a95fc61cb9aa9be8dd5cd09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 6 Feb 2025 12:00:25 +0000 Subject: [PATCH 05/26] feat(ruby): improve support level calculation --- .../lib/agama/autoyast/profile_description.rb | 33 +++++++++++++++---- .../lib/agama/autoyast/profile_reporter.rb | 2 +- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/service/lib/agama/autoyast/profile_description.rb b/service/lib/agama/autoyast/profile_description.rb index 7bbcdc7069..fad91c800c 100644 --- a/service/lib/agama/autoyast/profile_description.rb +++ b/service/lib/agama/autoyast/profile_description.rb @@ -28,8 +28,6 @@ module AutoYaST class ProfileElement # @return [String] Element key. attr_reader :key - # @return [Symbol] Support level (:no or :planned). - attr_reader :support # @return [String] Additional information about the element. attr_reader :notes # @return [String] Agama equivalent attribute @@ -38,8 +36,9 @@ class ProfileElement attr_reader :children class << self + # Builds a ProfileElement from its JSON description. def from_json(json, parent = nil) - support = json.key?("children") ? :yes : json["support"].to_sym + support = json.key?("children") ? :yes : json["support"]&.to_sym key = parent ? "#{parent}.#{json["key"]}" : json["key"] children = json.fetch("children", []).map do |json_element| ProfileElement.from_json(json_element, key) @@ -48,10 +47,17 @@ def from_json(json, parent = nil) end end - def initialize(key, support, agama, hint, children = []) + # Constructor + # + # @param key [String] Element key. + # @param support [String, nil] Support level. + # @param notes [String] Additional information about the element. + # @param agama [String] Agama equivalent attribute + # @param children [Array] Children elements. + def initialize(key, support, agama, notes, children = []) @key = key @support = support - @hint = hint + @notes = notes @agama = agama @children = children end @@ -60,7 +66,22 @@ def initialize(key, support, agama, hint, children = []) # # @return [Boolean] def supported? - @support == :yes + support == :yes + end + + # Returns the support level. + # + # If it was not specified when building the object, it infers it from its children. If the + # element has no children, it returns :no. + def support + return @support if @support + + nested = children.map(&:support).uniq + return nested.first if nested.size == 1 + return :partial if nested.include?(:yes) || nested.include?(:partial) + return :planned if nested.include?(:planned) + + :no end # Whether it is a top level element. diff --git a/service/lib/agama/autoyast/profile_reporter.rb b/service/lib/agama/autoyast/profile_reporter.rb index cd480eedf5..d7339d184f 100644 --- a/service/lib/agama/autoyast/profile_reporter.rb +++ b/service/lib/agama/autoyast/profile_reporter.rb @@ -43,7 +43,7 @@ def initialize(questions_client, logger) # # @param elements [Array] List of unsupported elements. def report(elements) - keys = elements.map { |e| e.key }.join(", ") + keys = elements.map(&:key).join(", ") unsupported = elements.select { |e| e.support == :no } planned = elements.select { |e| e.support == :planned } From a1e8ff30bf3653be6e5b8368faecc7961f147229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 6 Feb 2025 12:04:08 +0000 Subject: [PATCH 06/26] feat(ruby): add network elements to AutoYaST compatibility --- service/lib/tasks/autoyast.rb | 5 +- service/share/autoyast-compat.json | 134 ++++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/service/lib/tasks/autoyast.rb b/service/lib/tasks/autoyast.rb index cd6b7437eb..e4cea89393 100644 --- a/service/lib/tasks/autoyast.rb +++ b/service/lib/tasks/autoyast.rb @@ -79,7 +79,8 @@ def elements_table(elements) "|----------|-----------|-------|-------|" ] elements.each do |e| - lines << "| #{e.short_key} | #{e.support} | #{e.agama} | #{e.notes} |" + agama_key = e.agama ? "`#{e.agama}`" : "" + lines << "| `#{e.short_key}` | #{e.support} | #{agama_key} | #{e.notes} |" end lines << "" lines @@ -96,6 +97,8 @@ def notes_for(element) "This section is not supported." when :planned "There are plans to support this section in the future." + when :partial + "There is partial support for this section." else "Support for this element is still undecided." end diff --git a/service/share/autoyast-compat.json b/service/share/autoyast-compat.json index 145020d117..97762dcde2 100644 --- a/service/share/autoyast-compat.json +++ b/service/share/autoyast-compat.json @@ -37,9 +37,18 @@ { "key": "networking", "children": [ - { "key": "backend", "support": "no" }, + { + "key": "backend", + "support": "no", + "notes": "Only NetworkManager is supported." + }, { "key": "dhcp_options", "support": "no" }, { "key": "dns", "support": "no" }, + { + "key": "ipv6", + "support": "yes", + "notes": "It affects `method4` and `method6`." + }, { "key": "keep_install_network", "support": "no" }, { "key": "managed", "support": "no" }, { "key": "modules", "support": "no" }, @@ -48,7 +57,128 @@ { "key": "s390-devices", "support": "no" }, { "key": "setup_before_proposal", "support": "no" }, { "key": "strict_IP_check_timeout", "support": "no" }, - { "key": "virt_bridge_proposal", "support": "no" } + { "key": "virt_bridge_proposal", "support": "no" }, + { + "key": "interfaces[]", + "agama": "connections", + "notes": "It corresponds to Agama `connections`, but the format is not exactly the same.", + "children": [ + { "key": "device", "agama": "interface", "support": "yes" }, + { "key": "name", "agama": "id", "support": "yes" }, + { "key": "description", "support": "no" }, + { + "key": "bootproto", + "agama": "method6", + "notes": "Different set of values." + }, + { + "key": "startmode", + "support": "no", + "notes": "Do not set up network connections you won't use." + }, + { + "key": "lladdr", + "support": "yes", + "agama": "macAddress" + }, + { + "key": "ifplugd_priority", + "support": "no", + "notes": "Not relevant (no ifplugd support)." + }, + { "key": "usercontrol", "support": "no" }, + { "key": "dhclient_set_hostname", "support": "no" }, + { + "key": "ipaddr", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "prefixlen", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "netmask", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "aliases", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "broadcast", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "network", + "support": "yes", + "agama": "network.connections[].address[]" + }, + { + "key": "mtu", + "support": "no" + }, + { + "key": "ethtool_options", + "support": "no" + }, + { + "key": "wireless", + "support": "yes", + "agama": "wireless", + "notes": "It uses a different format." + }, + { "key": "dhclient_set_down_link", "support": "no" }, + { "key": "dhclient_set_default_route", "support": "no" }, + { "key": "zone", "support": "no" }, + { "key": "firewall", "support": "no" }, + { "key": "bonding_master", "support": "planned" }, + { "key": "bonding_module_opts", "support": "planned" }, + { "key": "bonding_slave0", "support": "planned" }, + { "key": "bonding_slave1", "support": "planned" }, + { "key": "bridge", "support": "planned" }, + { "key": "bridge_forwarddelay", "support": "planned" }, + { "key": "bridge_ports", "support": "planned" }, + { "key": "bridge_stp", "support": "planned" }, + { "key": "vlan_id", "support": "planned" }, + { "key": "wireless_auth_mode", "support": "yes" }, + { "key": "wireless_ap", "support": "no" }, + { "key": "wireless_bitrate", "support": "no" }, + { "key": "wireless_ca_cert", "support": "no" }, + { "key": "wireless_channel", "support": "no" }, + { "key": "wireless_client_cert", "support": "no" }, + { "key": "wireless_client_key", "support": "no" }, + { "key": "wireless_client_key_password", "support": "no" }, + { "key": "wireless_default_key", "support": "no" }, + { "key": "wireless_eap_auth", "support": "no" }, + { "key": "wireless_eap_mode", "support": "no" }, + { "key": "wireless_essid", "support": "yes", "agama": "ssid" }, + { "key": "wireless_frequency", "support": "no" }, + { "key": "wireless_key", "support": "no" }, + { "key": "wireless_key_0", "support": "no" }, + { "key": "wireless_key_1", "support": "no" }, + { "key": "wireless_key_2", "support": "no" }, + { "key": "wireless_key_3", "support": "no" }, + { "key": "wireless_key_length", "support": "no" }, + { "key": "wireless_mode", "support": "yes", "agama": "mode" }, + { "key": "wireless_nick", "support": "no" }, + { "key": "wireless_nwid", "support": "no" }, + { "key": "wireless_peap_version", "support": "no" }, + { "key": "wireless_power", "support": "no" }, + { "key": "wireless_wpa_anonid", "support": "no" }, + { "key": "wireless_wpa_identity", "support": "no" }, + { + "key": "wireless_wpa_password", + "support": "yes", + "agama": "password" + }, + { "key": "wireless_wpa_psk", "support": "yes", "agama": "password" } + ] + } ] }, { From bc0fbd48528e64b467c739621efe803a970a4123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 6 Feb 2025 12:16:03 +0000 Subject: [PATCH 07/26] fix(ruby): make RuboCop happy --- service/lib/agama/autoyast/profile_reporter.rb | 2 +- service/lib/agama/commands/agama_autoyast.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/service/lib/agama/autoyast/profile_reporter.rb b/service/lib/agama/autoyast/profile_reporter.rb index d7339d184f..3bc510be20 100644 --- a/service/lib/agama/autoyast/profile_reporter.rb +++ b/service/lib/agama/autoyast/profile_reporter.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2025] SUSE LLC @@ -25,6 +24,7 @@ # :nodoc: module Agama module AutoYaST + # Reports the problems found by the {ProfileChecker} using the questions client. class ProfileReporter include Yast::I18n diff --git a/service/lib/agama/commands/agama_autoyast.rb b/service/lib/agama/commands/agama_autoyast.rb index 410f52f65e..305df11a5e 100644 --- a/service/lib/agama/commands/agama_autoyast.rb +++ b/service/lib/agama/commands/agama_autoyast.rb @@ -1,8 +1,7 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # -# Copyright (c) [2024] SUSE LLC +# Copyright (c) [2025] SUSE LLC # # All Rights Reserved. # @@ -28,6 +27,7 @@ require "agama/dbus/clients/questions" module Agama + # :nodoc: module Commands class CouldNotFetchProfile < StandardError; end class CouldNotWriteAgamaConfig < StandardError; end From 1e8ecafd7bc6c9db2f401b14a1bc4320a0c9b706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 6 Feb 2025 12:17:48 +0000 Subject: [PATCH 08/26] chore(ruby): remove useless shebang lines --- service/lib/agama/autoyast/bond_reader.rb | 1 - .../lib/agama/autoyast/connections_reader.rb | 1 - service/lib/agama/autoyast/converter.rb | 1 - service/lib/agama/autoyast/l10n_reader.rb | 1 - service/lib/agama/autoyast/network_reader.rb | 1 - service/lib/agama/autoyast/product_reader.rb | 1 - service/lib/agama/autoyast/profile_checker.rb | 1 - .../lib/agama/autoyast/profile_description.rb | 1 - service/lib/agama/autoyast/profile_fetcher.rb | 1 - service/lib/agama/commands/commands.rb | 31 +++++++++++++++++++ 10 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 service/lib/agama/commands/commands.rb diff --git a/service/lib/agama/autoyast/bond_reader.rb b/service/lib/agama/autoyast/bond_reader.rb index 3bb4669a9f..edeaf59746 100755 --- a/service/lib/agama/autoyast/bond_reader.rb +++ b/service/lib/agama/autoyast/bond_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/connections_reader.rb b/service/lib/agama/autoyast/connections_reader.rb index 4dfb615ed1..bcdd4ae62e 100755 --- a/service/lib/agama/autoyast/connections_reader.rb +++ b/service/lib/agama/autoyast/connections_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index 5435e3dd58..f1e95c7eea 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/l10n_reader.rb b/service/lib/agama/autoyast/l10n_reader.rb index 771db35cdf..80f232b0b6 100755 --- a/service/lib/agama/autoyast/l10n_reader.rb +++ b/service/lib/agama/autoyast/l10n_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/network_reader.rb b/service/lib/agama/autoyast/network_reader.rb index 04756e4026..4aae25d5cc 100755 --- a/service/lib/agama/autoyast/network_reader.rb +++ b/service/lib/agama/autoyast/network_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/product_reader.rb b/service/lib/agama/autoyast/product_reader.rb index 06a9de3394..96ddcccb09 100755 --- a/service/lib/agama/autoyast/product_reader.rb +++ b/service/lib/agama/autoyast/product_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/profile_checker.rb b/service/lib/agama/autoyast/profile_checker.rb index 63009bf96a..fab9370a76 100644 --- a/service/lib/agama/autoyast/profile_checker.rb +++ b/service/lib/agama/autoyast/profile_checker.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2025] SUSE LLC diff --git a/service/lib/agama/autoyast/profile_description.rb b/service/lib/agama/autoyast/profile_description.rb index fad91c800c..c7dbc5ab69 100644 --- a/service/lib/agama/autoyast/profile_description.rb +++ b/service/lib/agama/autoyast/profile_description.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/profile_fetcher.rb b/service/lib/agama/autoyast/profile_fetcher.rb index 1a2bb96dde..aa9a162804 100644 --- a/service/lib/agama/autoyast/profile_fetcher.rb +++ b/service/lib/agama/autoyast/profile_fetcher.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2025] SUSE LLC diff --git a/service/lib/agama/commands/commands.rb b/service/lib/agama/commands/commands.rb new file mode 100644 index 0000000000..14c42ddd24 --- /dev/null +++ b/service/lib/agama/commands/commands.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# +# Copyright (c) [2025] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +module Agama + # Module which contains the Agama commands. + # + # TODO: move all the commands to this namespace. + module Commands + end +end + +require "agama/autoyast" From 56fd289cb193c233842f29bcc06802e0b9a9292d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 6 Feb 2025 13:36:19 +0000 Subject: [PATCH 09/26] fix(ruby): rake task to the top-level Rakefile --- Rakefile | 11 +++++++++++ service/Rakefile | 31 ------------------------------- 2 files changed, 11 insertions(+), 31 deletions(-) delete mode 100644 service/Rakefile diff --git a/Rakefile b/Rakefile index 48cad8f7d0..0e4685d4a8 100644 --- a/Rakefile +++ b/Rakefile @@ -19,9 +19,12 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +$LOAD_PATH.unshift File.expand_path("service/lib", __dir__) + require "shellwords" require "fileutils" require "yast/rake" +require_relative "service/lib/tasks/autoyast" # Infers the gem name from the source code # @@ -209,3 +212,11 @@ if ENV["YUPDATE_FORCE"] == "1" || File.exist?("/.packages.initrd") || live_iso? end end end + +desc "Documentation tasks" +namespace :doc do + desc "Generate the AutoYaST compatibility documentation" + task :autoyast_compat do + puts Agama::Tasks::AutoYaSTCompatGenerator.new.generate + end +end diff --git a/service/Rakefile b/service/Rakefile deleted file mode 100644 index 498e831fdd..0000000000 --- a/service/Rakefile +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2025] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -$LOAD_PATH.unshift File.expand_path("lib", __dir__) -require_relative "lib/tasks/autoyast" - -desc "Documentation tasks" -namespace :doc do - desc "Generate the AutoYaST compatibility documentation" - task :autoyast_compat do - puts Agama::Tasks::AutoYaSTCompatGenerator.new.generate - end -end From d5ec2b62dd255324dd03af199238802fd4da833f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 6 Feb 2025 16:30:24 +0000 Subject: [PATCH 10/26] fix(ruby): fix handling of unknown AutoYaST elements --- service/lib/agama/autoyast/profile_description.rb | 4 +++- service/test/agama/autoyast/profile_description_test.rb | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/service/lib/agama/autoyast/profile_description.rb b/service/lib/agama/autoyast/profile_description.rb index c7dbc5ab69..68f00cc969 100644 --- a/service/lib/agama/autoyast/profile_description.rb +++ b/service/lib/agama/autoyast/profile_description.rb @@ -131,7 +131,9 @@ def initialize(elements) # @param key [String] Element key (e.g., "networking.base") def find_element(key) element = @index.dig(*key.split(".")) - element.is_a?(ProfileElement) ? element : nil + element if element.is_a?(ProfileElement) + rescue TypeError + nil end private diff --git a/service/test/agama/autoyast/profile_description_test.rb b/service/test/agama/autoyast/profile_description_test.rb index d4748dbff4..3ec905c8b7 100644 --- a/service/test/agama/autoyast/profile_description_test.rb +++ b/service/test/agama/autoyast/profile_description_test.rb @@ -33,5 +33,11 @@ expect(element.support).to eq(:no) end end + + context "when the element is unknown" do + it "returns nil" do + expect(subject.find_element("iscsi-client.dummy")).to be_nil + end + end end end From b621c12cef17c20fda3312c3153292036ea5034f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 6 Feb 2025 16:42:48 +0000 Subject: [PATCH 11/26] feat(web): add an specific question to handle AutoYaST unsupported elements --- web/src/components/questions/Questions.tsx | 5 + .../questions/UnsupportedAutoYaST.test.tsx | 55 +++++++++++ .../questions/UnsupportedAutoYaST.tsx | 91 +++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 web/src/components/questions/UnsupportedAutoYaST.test.tsx create mode 100644 web/src/components/questions/UnsupportedAutoYaST.tsx diff --git a/web/src/components/questions/Questions.tsx b/web/src/components/questions/Questions.tsx index d96b409f97..a8cbb47706 100644 --- a/web/src/components/questions/Questions.tsx +++ b/web/src/components/questions/Questions.tsx @@ -24,6 +24,7 @@ import React from "react"; import GenericQuestion from "~/components/questions/GenericQuestion"; import QuestionWithPassword from "~/components/questions/QuestionWithPassword"; import LuksActivationQuestion from "~/components/questions/LuksActivationQuestion"; +import UnsupportedAutoYaST from "~/components/questions/UnsupportedAutoYaST"; import { useQuestions, useQuestionsConfig, useQuestionsChanges } from "~/queries/questions"; import { AnswerCallback, QuestionType } from "~/types/questions"; @@ -53,5 +54,9 @@ export default function Questions(): React.ReactNode { QuestionComponent = LuksActivationQuestion; } + if (currentQuestion.class === "autoyast.unsupported") { + QuestionComponent = UnsupportedAutoYaST; + } + return ; } diff --git a/web/src/components/questions/UnsupportedAutoYaST.test.tsx b/web/src/components/questions/UnsupportedAutoYaST.test.tsx new file mode 100644 index 0000000000..55c028a307 --- /dev/null +++ b/web/src/components/questions/UnsupportedAutoYaST.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { AnswerCallback, Question } from "~/types/questions"; +import UnsupportedAutoYaST from "~/components/questions/UnsupportedAutoYaST"; +import { plainRender } from "~/test-utils"; + +const question: Question = { + id: 1, + class: "autoyast.unsupported", + text: "Some elements from the AutoYaST profile are not supported.", + options: ["abort", "continue"], + defaultOption: "continue", + data: { + unsupported: "dns-server", + planned: "iscsi-client", + }, +}; + +const answerFn: AnswerCallback = jest.fn(); + +describe("UnsupportedAutoYaST", () => { + it("mentions the planned elements", () => { + plainRender(); + + expect(screen.getByText(/there are plans to support them: iscsi-client/)).toBeInTheDocument(); + }); + + it("mentions the unsupported elements", () => { + plainRender(); + + expect(screen.getByText(/no plans to support them: dns-server/)).toBeInTheDocument(); + }); +}); diff --git a/web/src/components/questions/UnsupportedAutoYaST.tsx b/web/src/components/questions/UnsupportedAutoYaST.tsx new file mode 100644 index 0000000000..c6c2e8a77f --- /dev/null +++ b/web/src/components/questions/UnsupportedAutoYaST.tsx @@ -0,0 +1,91 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { Text } from "@patternfly/react-core"; +import { AnswerCallback, Question } from "~/types/questions"; +import { Popup } from "~/components/core"; +import { _ } from "~/i18n"; +import QuestionActions from "~/components/questions/QuestionActions"; +import { sprintf } from "sprintf-js"; + +const UnsupportedElementsText = ({ data }: { data: { [key: string]: string } }) => { + if (data.unsupported.length === 0) { + return undefined; + } + + const elements = data.unsupported.split(","); + + return ( + + {sprintf( + _("These elements are not supported and there are no plans to support them: %s."), + elements.join(", "), + )} + + ); +}; + +const PlannedElementsText = ({ data }: { data: { [key: string]: string } }) => { + if (data.planned.length === 0) { + return undefined; + } + + const elements = data.planned.split(","); + + return ( + + {sprintf( + _("These elements are not supported but there are plans to support them: %s."), + elements.join(", "), + )} + + ); +}; + +export default function UnsupportedAutoYaST({ + question, + answerCallback, +}: { + question: Question; + answerCallback: AnswerCallback; +}) { + const actionCallback = (option: string) => { + question.answer = option; + answerCallback(question); + }; + + return ( + + {question.text} + + + + + + + ); +} From dcdab3452b77be97a7db7ce868db6f29c93d0f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 7 Feb 2025 06:45:04 +0000 Subject: [PATCH 12/26] chore(ruby): drop ProfileFetcher#copy_files (unused) --- service/lib/agama/autoyast/profile_fetcher.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/service/lib/agama/autoyast/profile_fetcher.rb b/service/lib/agama/autoyast/profile_fetcher.rb index aa9a162804..7c80bc3ff9 100644 --- a/service/lib/agama/autoyast/profile_fetcher.rb +++ b/service/lib/agama/autoyast/profile_fetcher.rb @@ -48,8 +48,6 @@ def fetch attr_reader :profile_url - def copy_profile; end - # @return [Hash] AutoYaST profile def read_profile FileUtils.mkdir_p(Yast::AutoinstConfig.profile_dir) From fb9039f8a2c9163f0c93933b487f1265005e9d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 7 Feb 2025 06:46:39 +0000 Subject: [PATCH 13/26] fix(ruby): do not read the profile if it was not fetched --- service/lib/agama/autoyast/profile_fetcher.rb | 2 +- .../test/agama/autoyast/profile_fetcher_test.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/service/lib/agama/autoyast/profile_fetcher.rb b/service/lib/agama/autoyast/profile_fetcher.rb index 7c80bc3ff9..be3dc3da8b 100644 --- a/service/lib/agama/autoyast/profile_fetcher.rb +++ b/service/lib/agama/autoyast/profile_fetcher.rb @@ -54,7 +54,7 @@ def read_profile # fetch the profile Yast::AutoinstConfig.ParseCmdLine(profile_url) - Yast::ProfileLocation.Process + return unless Yast::ProfileLocation.Process # put the profile in the tmp directory FileUtils.cp( diff --git a/service/test/agama/autoyast/profile_fetcher_test.rb b/service/test/agama/autoyast/profile_fetcher_test.rb index cf67e5c45e..15357d60bc 100644 --- a/service/test/agama/autoyast/profile_fetcher_test.rb +++ b/service/test/agama/autoyast/profile_fetcher_test.rb @@ -26,6 +26,8 @@ require "autoinstall/xml_checks" require "y2storage" +Yast.import "ProfileLocation" + describe Agama::AutoYaST::ProfileFetcher do let(:profile) { File.join(FIXTURES_PATH, "profiles", profile_name) } let(:profile_name) { "simple.xml" } @@ -117,6 +119,21 @@ expect(result["software"]).to include("products" => ["Tumbleweed"]) end end + + context "when it cannot download the profile" do + before do + allow(Yast::ProfileLocation).to receive(:Process).and_return(false) + end + + it "returns nil" do + expect(subject.fetch).to be_nil + end + + it "does not process the profile" do + expect(Yast::Profile).to_not receive(:ReadXML) + subject.fetch + end + end end context "when an invalid profile is given" do From f33c81e441b7779fd75c3aec473ac87d8831ddae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 7 Feb 2025 07:31:03 +0000 Subject: [PATCH 14/26] fix(ruby): use a slash (/) for AutoYaST element path separator --- service/lib/agama/autoyast/profile_checker.rb | 8 +++++--- service/lib/agama/autoyast/profile_description.rb | 12 +++++++----- service/test/agama/autoyast/profile_checker_test.rb | 4 ++-- .../test/agama/autoyast/profile_description_test.rb | 6 +++--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/service/lib/agama/autoyast/profile_checker.rb b/service/lib/agama/autoyast/profile_checker.rb index fab9370a76..a4d87324a8 100644 --- a/service/lib/agama/autoyast/profile_checker.rb +++ b/service/lib/agama/autoyast/profile_checker.rb @@ -46,15 +46,17 @@ def find_unsupported(profile) # Returns the elements from the profile # - # @return [Array] List of element IDs (e.g., "networking.backend") + # @return [Array] List of element IDs (e.g., "networking/backend") def elements_from(profile, parent = "") return [] unless profile.is_a?(Hash) profile.map do |k, v| - current = parent.empty? ? k : "#{parent}.#{k}" + current = parent.empty? ? k : "#{parent}#{ProfileDescription::SEPARATOR}#{k}" children = if v.is_a?(Array) - v.map.with_index { |e, i| elements_from(e, "#{parent}.#{k}[#{i}]") } + v.map.with_index do |e, i| + elements_from(e, "#{parent}#{ProfileDescription::SEPARATOR}#{k}[#{i}]") + end else elements_from(v, k) end diff --git a/service/lib/agama/autoyast/profile_description.rb b/service/lib/agama/autoyast/profile_description.rb index 68f00cc969..59ff144d2a 100644 --- a/service/lib/agama/autoyast/profile_description.rb +++ b/service/lib/agama/autoyast/profile_description.rb @@ -38,7 +38,7 @@ class << self # Builds a ProfileElement from its JSON description. def from_json(json, parent = nil) support = json.key?("children") ? :yes : json["support"]&.to_sym - key = parent ? "#{parent}.#{json["key"]}" : json["key"] + key = parent ? "#{parent}#{ProfileDescription::SEPARATOR}#{json["key"]}" : json["key"] children = json.fetch("children", []).map do |json_element| ProfileElement.from_json(json_element, key) end @@ -87,19 +87,21 @@ def support # # @return [Boolean] def top_level? - !key.include?(".") + !key.include?(ProfileDescription::SEPARATOR) end # Short key name. # # @return [String] def short_key - key.split(".").last + key.split(ProfileDescription::SEPARATOR).last end end # Describes the AutoYaST profile format. class ProfileDescription + SEPARATOR = "/" + attr_reader :elements DEFAULT_PATH = File.expand_path("#{__dir__}/../../../share/autoyast-compat.json") @@ -128,9 +130,9 @@ def initialize(elements) # Find an element by its key # - # @param key [String] Element key (e.g., "networking.base") + # @param key [String] Element key (e.g., "networking/base") def find_element(key) - element = @index.dig(*key.split(".")) + element = @index.dig(*key.split(SEPARATOR)) element if element.is_a?(ProfileElement) rescue TypeError nil diff --git a/service/test/agama/autoyast/profile_checker_test.rb b/service/test/agama/autoyast/profile_checker_test.rb index 4da56bc623..aeee0ec21b 100644 --- a/service/test/agama/autoyast/profile_checker_test.rb +++ b/service/test/agama/autoyast/profile_checker_test.rb @@ -53,7 +53,7 @@ it "returns an array with the unsupported element" do expect(subject.find_unsupported(profile)).to contain_exactly( - an_object_having_attributes(key: "networking.backend") + an_object_having_attributes(key: "networking/backend") ) end end @@ -72,7 +72,7 @@ it "returns an array with the unsupported element" do expect(subject.find_unsupported(profile)).to contain_exactly( - an_object_having_attributes(key: "scripts.pre-scripts[].rerun") + an_object_having_attributes(key: "scripts/pre-scripts[]/rerun") ) end end diff --git a/service/test/agama/autoyast/profile_description_test.rb b/service/test/agama/autoyast/profile_description_test.rb index 3ec905c8b7..4b8af078c1 100644 --- a/service/test/agama/autoyast/profile_description_test.rb +++ b/service/test/agama/autoyast/profile_description_test.rb @@ -28,15 +28,15 @@ describe "#find_element" do context "when the element exists" do it "returns the element data" do - element = subject.find_element("networking.backend") - expect(element.key).to eq("networking.backend") + element = subject.find_element("networking/backend") + expect(element.key).to eq("networking/backend") expect(element.support).to eq(:no) end end context "when the element is unknown" do it "returns nil" do - expect(subject.find_element("iscsi-client.dummy")).to be_nil + expect(subject.find_element("iscsi-client/dummy")).to be_nil end end end From 5b14f30956a19fcec96f5bd94f1a7d5998f832f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 7 Feb 2025 13:18:19 +0000 Subject: [PATCH 15/26] feat(web): improve UnsupportedAutoYaST component --- .../questions/UnsupportedAutoYaST.test.tsx | 40 +++++++++- .../questions/UnsupportedAutoYaST.tsx | 79 +++++++++++-------- 2 files changed, 82 insertions(+), 37 deletions(-) diff --git a/web/src/components/questions/UnsupportedAutoYaST.test.tsx b/web/src/components/questions/UnsupportedAutoYaST.test.tsx index 55c028a307..1f9845b9ee 100644 --- a/web/src/components/questions/UnsupportedAutoYaST.test.tsx +++ b/web/src/components/questions/UnsupportedAutoYaST.test.tsx @@ -21,7 +21,7 @@ */ import React from "react"; -import { screen } from "@testing-library/react"; +import { screen, within } from "@testing-library/react"; import { AnswerCallback, Question } from "~/types/questions"; import UnsupportedAutoYaST from "~/components/questions/UnsupportedAutoYaST"; import { plainRender } from "~/test-utils"; @@ -38,18 +38,50 @@ const question: Question = { }, }; +let mockQuestion = question; + const answerFn: AnswerCallback = jest.fn(); describe("UnsupportedAutoYaST", () => { + beforeEach(() => { + mockQuestion = { ...question }; + }); + it("mentions the planned elements", () => { plainRender(); - expect(screen.getByText(/there are plans to support them: iscsi-client/)).toBeInTheDocument(); + const list = screen.getByRole("region", { name: "Not implemented yet (1)" }); + within(list).getByText("iscsi-client"); }); - it("mentions the unsupported elements", () => { + it("mentions the unsuported elements", () => { plainRender(); - expect(screen.getByText(/no plans to support them: dns-server/)).toBeInTheDocument(); + const list = screen.getByRole("region", { name: "Not supported (1)" }); + within(list).getByText("dns-server"); + }); + + describe("when there are no unsupported (but planned) elements", () => { + beforeEach(() => { + mockQuestion = { ...question, data: {} }; + }); + + it('does not render the "Not implemented yet" list', () => { + plainRender(); + + expect(screen.queryByRole("region", { name: /Not implemented/ })).not.toBeInTheDocument(); + }); + }); + + describe("when there are no unsupported (but planned) elements", () => { + beforeEach(() => { + mockQuestion = { ...question, data: {} }; + }); + + it('does not render the "Not supported" list', () => { + plainRender(); + + expect(screen.queryByRole("region", { name: /Not supported/ })).not.toBeInTheDocument(); + }); }); }); diff --git a/web/src/components/questions/UnsupportedAutoYaST.tsx b/web/src/components/questions/UnsupportedAutoYaST.tsx index c6c2e8a77f..7f1202d7f2 100644 --- a/web/src/components/questions/UnsupportedAutoYaST.tsx +++ b/web/src/components/questions/UnsupportedAutoYaST.tsx @@ -21,44 +21,38 @@ */ import React from "react"; -import { Text } from "@patternfly/react-core"; +import { Flex, Grid, GridItem, Text } from "@patternfly/react-core"; import { AnswerCallback, Question } from "~/types/questions"; -import { Popup } from "~/components/core"; +import { Page, Popup } from "~/components/core"; import { _ } from "~/i18n"; import QuestionActions from "~/components/questions/QuestionActions"; import { sprintf } from "sprintf-js"; -const UnsupportedElementsText = ({ data }: { data: { [key: string]: string } }) => { - if (data.unsupported.length === 0) { - return undefined; - } - - const elements = data.unsupported.split(","); - - return ( - - {sprintf( - _("These elements are not supported and there are no plans to support them: %s."), - elements.join(", "), - )} - - ); -}; - -const PlannedElementsText = ({ data }: { data: { [key: string]: string } }) => { - if (data.planned.length === 0) { +const UnsupportedElements = ({ + elements, + title, + description, +}: { + elements: string[]; + title: string; + description: string; +}) => { + if (elements.length === 0) { return undefined; } - const elements = data.planned.split(","); - return ( - - {sprintf( - _("These elements are not supported but there are plans to support them: %s."), - elements.join(", "), - )} - + + + + {elements.map((e: string, i: number) => ( + + {e} + + ))} + + + ); }; @@ -74,11 +68,30 @@ export default function UnsupportedAutoYaST({ answerCallback(question); }; + const planned = question.data.planned?.split(",") || []; + const unsupported = question.data.unsupported?.split(",") || []; + return ( - - {question.text} - - + + {_("Some elements found in the AutoYaST profile are not supported.")} + + + + Date: Fri, 7 Feb 2025 13:51:26 +0000 Subject: [PATCH 16/26] chore(ruby): allow disabling the AutoYaST check --- service/lib/agama/autoyast/profile_reporter.rb | 2 +- service/lib/agama/commands/agama_autoyast.rb | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/service/lib/agama/autoyast/profile_reporter.rb b/service/lib/agama/autoyast/profile_reporter.rb index 3bc510be20..37511d4619 100644 --- a/service/lib/agama/autoyast/profile_reporter.rb +++ b/service/lib/agama/autoyast/profile_reporter.rb @@ -48,7 +48,7 @@ def report(elements) planned = elements.select { |e| e.support == :planned } message = format( - _("Some AutoYaST elements are not supported."), keys: keys + _("Found unsupported elements in the AutoYaST profile: %{keys}."), keys: keys ) question = Agama::Question.new( qclass: "autoyast.unsupported", diff --git a/service/lib/agama/commands/agama_autoyast.rb b/service/lib/agama/commands/agama_autoyast.rb index 305df11a5e..3990ea3d19 100644 --- a/service/lib/agama/commands/agama_autoyast.rb +++ b/service/lib/agama/commands/agama_autoyast.rb @@ -24,6 +24,7 @@ require "agama/autoyast/profile_fetcher" require "agama/autoyast/profile_reporter" require "agama/autoyast/profile_checker" +require "agama/cmdline_args" require "agama/dbus/clients/questions" module Agama @@ -66,11 +67,13 @@ def fetch_profile # Check the profile for unsupported def check_profile(profile) checker = Agama::AutoYaST::ProfileChecker.new - checker.find_unsupported(profile) + elements = checker.find_unsupported(profile) + logger.info "Found unsupported AutoYaST elements: #{elements.map(&:key)}" + elements end def report_unsupported(elements) - return true if elements.empty? + return true if elements.empty? || !report? reporter = Agama::AutoYaST::ProfileReporter.new(questions_client, logger) reporter.report(elements) @@ -88,6 +91,12 @@ def write_agama_config(profile) def questions_client @questions_client ||= Agama::DBus::Clients::Questions.new(logger: logger) end + + # Whether the report is enabled or not. + def report? + cmdline = CmdlineArgs.read_from("/proc/cmdline") + cmdline.data.fetch("ay_check", "1") != "0" + end end if ARGV.length != 2 From 744c297dbf9a4971f92064ceab3986a9d51cfb75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 7 Feb 2025 14:39:16 +0000 Subject: [PATCH 17/26] fix(ruby): fix ProfileFetcher documentation --- service/lib/agama/autoyast/profile_fetcher.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/service/lib/agama/autoyast/profile_fetcher.rb b/service/lib/agama/autoyast/profile_fetcher.rb index be3dc3da8b..dc64a350e8 100644 --- a/service/lib/agama/autoyast/profile_fetcher.rb +++ b/service/lib/agama/autoyast/profile_fetcher.rb @@ -37,8 +37,6 @@ def initialize(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 fetch import_yast read_profile @@ -48,7 +46,7 @@ def fetch attr_reader :profile_url - # @return [Hash] AutoYaST profile + # @return [Hash, nil] AutoYaST profile def read_profile FileUtils.mkdir_p(Yast::AutoinstConfig.profile_dir) From 9ee3b4a9657361e137cb87956d9cd7287c2b7ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 7 Feb 2025 14:44:03 +0000 Subject: [PATCH 18/26] docs: update changes files --- service/package/rubygem-agama-yast.changes | 6 ++++++ web/package/agama-web-ui.changes | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 234b7cb787..8630cb260a 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Fri Feb 7 14:42:31 UTC 2025 - Imobach Gonzalez Sosa + +- Report unsupported AutoYaST elements + (gh#agama-project/agama#1976). + ------------------------------------------------------------------- Wed Jan 29 16:28:32 UTC 2025 - Josef Reidinger diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index d5a49a67c5..5892389417 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Fri Feb 7 14:43:08 UTC 2025 - Imobach Gonzalez Sosa + +- Report unsupported AutoYaST elements + (gh#agama-project/agama#1976). + ------------------------------------------------------------------- Fri Jan 24 09:34:24 UTC 2025 - Imobach Gonzalez Sosa From 679594041e4ae5fbb300cac38853025179728c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 7 Feb 2025 14:46:42 +0000 Subject: [PATCH 19/26] fix(ruby): fix location of commands.rb --- service/lib/agama/{commands => }/commands.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename service/lib/agama/{commands => }/commands.rb (100%) diff --git a/service/lib/agama/commands/commands.rb b/service/lib/agama/commands.rb similarity index 100% rename from service/lib/agama/commands/commands.rb rename to service/lib/agama/commands.rb From eb8ead1b9f566a88a06347de092aa1ee5c8479a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 7 Feb 2025 16:15:32 +0000 Subject: [PATCH 20/26] fix(ruby): add unit tests for the AgamaAutoYaST command --- service/lib/agama/commands/agama_autoyast.rb | 8 +- .../agama/commands/agama_autoyast_test.rb | 97 +++++++++++++++++++ 2 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 service/test/agama/commands/agama_autoyast_test.rb diff --git a/service/lib/agama/commands/agama_autoyast.rb b/service/lib/agama/commands/agama_autoyast.rb index 3990ea3d19..11e9685d21 100644 --- a/service/lib/agama/commands/agama_autoyast.rb +++ b/service/lib/agama/commands/agama_autoyast.rb @@ -37,6 +37,9 @@ class CouldNotWriteAgamaConfig < StandardError; end # # It fetches the profile, checks for unsupported elements and converts it to an Agama # configuration file. + # + # @param url [String] URL of the AutoYaST profile + # @param dir [String] Directory to write the converted profile class AgamaAutoYaST def initialize(url, directory) @url = url @@ -98,10 +101,5 @@ def report? cmdline.data.fetch("ay_check", "1") != "0" end end - - if ARGV.length != 2 - warn "Usage: #{$PROGRAM_NAME} URL DIRECTORY" - exit 1 - end end end diff --git a/service/test/agama/commands/agama_autoyast_test.rb b/service/test/agama/commands/agama_autoyast_test.rb new file mode 100644 index 0000000000..eb0c843c69 --- /dev/null +++ b/service/test/agama/commands/agama_autoyast_test.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +# Copyright (c) [2025] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "agama/commands/agama_autoyast" +require "yast" +Yast.import "Profile" + +describe Agama::Commands::AgamaAutoYaST do + subject { described_class.new(url, tmpdir) } + + let(:fetcher) { instance_double(Agama::AutoYaST::ProfileFetcher) } + let(:checker) { Agama::AutoYaST::ProfileChecker.new } + let(:reporter) { instance_double(Agama::AutoYaST::ProfileReporter, report: true) } + let(:questions) { instance_double(Agama::DBus::Clients::Questions) } + let(:profile) do + Yast::ProfileHash.new({ "software" => { "products" => ["openSUSE"] } }) + end + let(:url) { "http://example.net/autoyast.xml" } + let(:tmpdir) { Dir.mktmpdir } + let(:cmdline_args) { Agama::CmdlineArgs.new({}) } + + before do + allow(Agama::AutoYaST::ProfileFetcher).to receive(:new).with(url).and_return(fetcher) + allow(Agama::AutoYaST::ProfileChecker).to receive(:new).and_return(checker) + allow(Agama::AutoYaST::ProfileReporter).to receive(:new).and_return(reporter) + allow(Agama::DBus::Clients::Questions).to receive(:new).and_return(questions) + allow(Agama::CmdlineArgs).to receive(:read_from).and_return(cmdline_args) + allow(fetcher).to receive(:fetch).and_return(profile) + end + + after do + FileUtils.remove_entry(tmpdir) + end + + describe "#run" do + it "checks for unsupported elements" do + expect(checker).to receive(:find_unsupported).with(profile) + .and_call_original + subject.run + end + + it "writes the Agama equivalent to the given directory" do + subject.run + autoinst = File.read(File.join(tmpdir, "autoinst.json")) + expect(autoinst).to include("openSUSE") + end + + context "when the profile includes unsupported elements" do + let(:profile) do + Yast::ProfileHash.new({ "networking" => { "backend" => "wicked" } }) + end + + it "reports them to the user" do + expect(reporter).to receive(:report).and_return(true) + subject.run + end + + context "but the error reporting is disabled" do + let(:cmdline_args) { Agama::CmdlineArgs.new({ "ay_check" => "0" }) } + + it "does not report the errors" do + expect(reporter).to_not receive(:report) + subject.run + end + end + end + + context "when the profile could not be fetched" do + before do + allow(fetcher).to receive(:fetch).and_raise("Could not fetch the profile") + end + + it "raises a CouldNotFetchProfile exception" do + expect { subject.run }.to raise_exception(Agama::Commands::CouldNotFetchProfile) + end + end + end +end From be389d400b21e50384bdc721cd87ddc503ecabc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 7 Feb 2025 16:42:39 +0000 Subject: [PATCH 21/26] fix(web): improve wording in the UnsupportedAutoYaST component Co-authored-by: Martin Vidner --- web/src/components/questions/UnsupportedAutoYaST.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/questions/UnsupportedAutoYaST.tsx b/web/src/components/questions/UnsupportedAutoYaST.tsx index 7f1202d7f2..23766bea8e 100644 --- a/web/src/components/questions/UnsupportedAutoYaST.tsx +++ b/web/src/components/questions/UnsupportedAutoYaST.tsx @@ -72,7 +72,7 @@ export default function UnsupportedAutoYaST({ const unsupported = question.data.unsupported?.split(",") || []; return ( - + {_("Some elements found in the AutoYaST profile are not supported.")} Date: Fri, 7 Feb 2025 16:55:26 +0000 Subject: [PATCH 22/26] docs: describe the autoyast.unsupported question --- doc/questions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/questions.md b/doc/questions.md index 7ef8ca3bfb..523d7755f1 100644 --- a/doc/questions.md +++ b/doc/questions.md @@ -63,9 +63,11 @@ Sensitive answers or params will be replaced, so the user has to explicitly spec | class | description | possible answers | available data | notes | |--- |--- |--- |--- |--- | +| `autoyast.unsupported` | When there are unsupported elements in an AutoYaST profile | `Abort` `Continue` | `planned` elements to be supported in the future, `unsupported` unsupported elements | | | `software.medium_error` | When there is issue with access to medium | `Retry` `Skip` | `url` with url where failed access happen | | | `software.unsigned_file` | When file from repository is not digitally signed. If it should be used | `Yes` `No` | `filename` with name of file | | | `software.import_gpg` | When signature is sign with unknown GPG key | `Trust` `Skip` | `id` of key `name` of key and `fingerprint` of key | | | `storage.activate_multipath` | When it looks like system has multipath and if it should be activated | `yes` `no` | | Here it is used lower case. It should be unified. | | `storage.commit_error` | When some storage actions failed and if it should continue | `yes` `no` | | Also here it is lowercase | | `storage.luks_activation` | When LUKS encrypted device is detected and it needs password to probe it | `skip` `decrypt` | `device` name, `label` of device, `size` of device and `attempt` the number of attempt | Answer contain additional field password that has to be filled if answer is `decrypt`. Attempt data can be used to limit passing wrong password. | + From 9044b34a96e9ee93d4531f928f85348cbf29db1e Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 7 Feb 2025 21:19:16 +0100 Subject: [PATCH 23/26] autoyast compat: declare "bootproto" unsupported and remove 'support: yes' from users[], it is implicit on all items with 'children' --- service/share/autoyast-compat.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/share/autoyast-compat.json b/service/share/autoyast-compat.json index 97762dcde2..49f8b56c99 100644 --- a/service/share/autoyast-compat.json +++ b/service/share/autoyast-compat.json @@ -68,6 +68,7 @@ { "key": "description", "support": "no" }, { "key": "bootproto", + "support": "no", "agama": "method6", "notes": "Different set of values." }, @@ -365,7 +366,6 @@ { "key": "iscsi-client", "support": "planned" }, { "key": "users[]", - "support": "yes", "notes": "Only the root and the first user are considered.", "children": [ { "key": "username", "support": "yes", "agama": "user.userName" }, From 3fefbf59dacf6ca9a8f97649590681d6b21f22ef Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 7 Feb 2025 21:41:34 +0100 Subject: [PATCH 24/26] Added JSON schema for autoyast-compat.json zypper install python3-jsonschema.rpm jsonschema -i autoyast-compat.json autoyast-compat.schema.json TODO: check in CI, maybe make `agama profile validate` a bit more generic --- service/share/autoyast-compat.schema.json | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 service/share/autoyast-compat.schema.json diff --git a/service/share/autoyast-compat.schema.json b/service/share/autoyast-compat.schema.json new file mode 100644 index 0000000000..adafe72989 --- /dev/null +++ b/service/share/autoyast-compat.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "$id": "https://github.com/openSUSE/agama/blob/master/service/share/autoyast-compat.schema.json", + "title": "AutoYaST profile compatibility description", + "description": "For elements of AutoYaST profiles, describes whether Agama will understand them", + "type": "array", + "items": { + "$ref": "#/definitions/profileElement" + }, + "definitions": { + "profileElement": { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "support": { + "type": "string", + "enum": ["yes", "no", "planned"] + }, + "notes": { + "type": "string" + }, + "agama": { + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/profileElement" + } + } + }, + "required": [ + "key" + ], + "oneOf": [ + { "required": ["support"] }, + { "required": ["children"] } + ] + } + } +} From 1086efcbacd765a8eea02b777851ffb2fcf8fe45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 10 Feb 2025 16:35:06 +0000 Subject: [PATCH 25/26] feat(web): explain how to disable AutoYaST checks --- web/src/components/questions/UnsupportedAutoYaST.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/components/questions/UnsupportedAutoYaST.tsx b/web/src/components/questions/UnsupportedAutoYaST.tsx index 23766bea8e..24f63b8f21 100644 --- a/web/src/components/questions/UnsupportedAutoYaST.tsx +++ b/web/src/components/questions/UnsupportedAutoYaST.tsx @@ -73,7 +73,7 @@ export default function UnsupportedAutoYaST({ return ( - {_("Some elements found in the AutoYaST profile are not supported.")} + {_("Some of the elements in your AutoYaST profile are not supported.")} + + {_( + 'If you want to disable this check, please specify "agama.ay_check=0" at kernel\'s command-line', + )} + Date: Mon, 10 Feb 2025 16:39:13 +0000 Subject: [PATCH 26/26] fix(ruby): disable AutoYaST XML validation in agama-autoyast --- service/bin/agama-autoyast | 3 +++ 1 file changed, 3 insertions(+) diff --git a/service/bin/agama-autoyast b/service/bin/agama-autoyast index 4281076925..a28e5857ae 100755 --- a/service/bin/agama-autoyast +++ b/service/bin/agama-autoyast @@ -28,6 +28,9 @@ $LOAD_PATH.unshift File.expand_path("../lib", __dir__) # Set the PATH to a known value ENV["PATH"] = "/sbin:/usr/sbin:/usr/bin:/bin" +# Disable AutoYaST XML validation. It will be enabled in the future. +ENV["YAST_SKIP_XML_VALIDATION"] = "1" + require "rubygems" # find Gemfile when D-Bus activates a git checkout Dir.chdir(__dir__) do