From 1a0d1e0f324a256494e7766242f0653732506bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Wed, 26 Jun 2024 07:41:05 +0200 Subject: [PATCH] Install from local DVD if it is present (#1372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem - Local DVD cannot be used for installation, offline installation is not possible - We have a [special branch](https://github.com/openSUSE/agama/compare/install_from_2nd_DVD?expand=1) with this feature and build special ISO but I think it should work out of box with upstream Agama - Sooner or later we will need to support installation from the local media anyway ## Solution - A disk label defines the installation repository name for each product - If such disk label is found then that disk is used as the installation repository - If no label is found in the system it uses the online repositories - If multiple labels are found then the DVD device is preferred ## Testing - Tested manually, when the Tumbleweed DVD is present it is automatically used instead of the online repositories - When the product is switched to MicroOS and the DVD is not present it uses the online repositories --------- Co-authored-by: Imobach González Sosa --- doc/yaml_config.md | 10 +++ products.d/microos.yaml | 10 +++ products.d/tumbleweed.yaml | 10 +++ service/lib/agama/product_reader.rb | 2 +- service/lib/agama/software/manager.rb | 69 ++++++++++++++++++- service/lib/agama/software/product.rb | 6 ++ service/lib/agama/software/product_builder.rb | 35 ++++++---- service/package/gem2rpm.yml | 2 + service/test/agama/software/manager_test.rb | 16 +++++ 9 files changed, 143 insertions(+), 17 deletions(-) diff --git a/doc/yaml_config.md b/doc/yaml_config.md index 70210e1395..5bab27c3a7 100644 --- a/doc/yaml_config.md +++ b/doc/yaml_config.md @@ -30,6 +30,16 @@ Array of url for installation repositories. Map can be used instead of string. In such case map should contain url and archs keys. Archs key is used to limit usage of repository on matching hardware architectures. +#### installation\_labels + +Array of disk labels used for finding the local installation repository. Instead +of array of strings it is possible to use an array of maps with `label` and +`archs` keys where `archs` is a string with comma separated list of supported +hardware architectures. + +If the matching disk label is not found then the online installation repository +from the `installation_repositories` section is used. + #### mandatory\_patterns Array of patterns that have to be selected. diff --git a/products.d/microos.yaml b/products.d/microos.yaml index 85289b42e2..07cbd3bfb8 100644 --- a/products.d/microos.yaml +++ b/products.d/microos.yaml @@ -98,6 +98,16 @@ software: archs: s390 - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/oss/ archs: ppc + # device labels for offline installation media + installation_labels: + - label: openSUSE-MicroOS-DVD-x86_64 + archs: x86_64 + - label: openSUSE-MicroOS-DVD-aarch64 + archs: aarch64 + - label: openSUSE-MicroOS-DVD-s390x + archs: s390 + - label: openSUSE-MicroOS-DVD-ppc64le + archs: ppc mandatory_patterns: - microos_base - microos_base_zypper diff --git a/products.d/tumbleweed.yaml b/products.d/tumbleweed.yaml index ca7fade155..d66e79f703 100644 --- a/products.d/tumbleweed.yaml +++ b/products.d/tumbleweed.yaml @@ -91,6 +91,16 @@ software: archs: s390 - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/oss/ archs: ppc + # device labels for offline installation media + installation_labels: + - label: openSUSE-Tumbleweed-DVD-x86_64 + archs: x86_64 + - label: openSUSE-Tumbleweed-DVD-aarch64 + archs: aarch64 + - label: openSUSE-Tumbleweed-DVD-s390x + archs: s390 + - label: openSUSE-Tumbleweed-DVD-ppc64le + archs: ppc mandatory_patterns: - enhanced_base # only pattern that is shared among all roles on TW optional_patterns: null # no optional pattern shared diff --git a/service/lib/agama/product_reader.rb b/service/lib/agama/product_reader.rb index 453e9a23b6..96acc58ef3 100644 --- a/service/lib/agama/product_reader.rb +++ b/service/lib/agama/product_reader.rb @@ -49,7 +49,7 @@ def initialize(logger: nil) def load_products glob = File.join(default_path, "*.{yaml,yml}") Dir.glob(glob).each_with_object([]) do |path, result| - products = YAML.safe_load_file(path) + products = YAML.safe_load(File.read(path)) products = [products] unless products.is_a?(Array) result.concat(products) end diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb index 3a8ef6734e..c7c56f5b7c 100644 --- a/service/lib/agama/software/manager.rb +++ b/service/lib/agama/software/manager.rb @@ -20,6 +20,7 @@ # find current contact information at www.suse.com. require "fileutils" +require "json" require "yast" require "y2packager/product" require "y2packager/resolvable" @@ -420,9 +421,66 @@ def import_gpg_keys end def add_base_repos + # NOTE: support multiple labels/installation media? + label = product.labels.first + + if label + logger.info "Installation repository label: #{label.inspect}" + # we cannot use the simple /dev/disk/by-label/* device file as there + # might be multiple devices with the same label + device = installation_device(label) + if device + logger.info "Installation device: #{device}" + repositories.add("hd:/?device=" + device) + return + end + end + + # disk label not found or not configured, use the online repositories product.repositories.each { |url| repositories.add(url) } end + # find all devices with the required disk label + # @return [Array] returns list of devices, e.g. `["/dev/sr1"]`, + # returns empty list if there is no device with the required label + def disks_with_label(label) + data = list_disks + disks = data.fetch("blockdevices", []).map do |device| + device["kname"] if device["label"] == label + end + disks.compact! + logger.info "Disks with the installation label: #{disks.inspect}" + disks + end + + # get list of disks, returns parsed data from the `lsblk` call + # @return [Hash] parsed data + def list_disks + # we need only the kernel device name and the label + output = `lsblk --paths --json --output kname,label` + JSON.parse(output) + rescue StandardError => e + logger.error "ERROR: Cannot read disk devices: #{e}" + {} + end + + # find the installation device with the required label + # @return [String,nil] Device name (`/dev/sr1`) or `nil` if not found + def installation_device(label) + disks = disks_with_label(label) + + # multiple installation media? + if disks.size > 1 + # prefer optical media (/dev/srX) to disk so the disk can be used as + # the installation target + optical = disks.find { |d| d.match(/\A\/dev\/sr[0-9]+\z/) } + optical || disks.first + else + # none or just one disk + disks.first + end + end + # Adds resolvables for selected product def select_resolvables proposal.set_resolvables("agama", :pattern, product.mandatory_patterns) @@ -538,16 +596,23 @@ def copy_zypp_to_target FileUtils.copy(glob_credentials, target_dir) end + # Is any local repository (CD/DVD, disk) currently used? + # @return [Boolean] true if any local repository is used + def local_repo? + Agama::Software::Repository.all.any?(&:local?) + end + # update the zypp repositories for the new product, either delete them # or keep them untouched # @param new_product [Agama::Software::Product] the new selected product def update_repositories(new_product) # reuse the repositories when they are the same as for the previously - # selected product + # selected product and no local repository is currently used + # (local repositories are usually product specific) # TODO: what about registered products? # TODO: allow a partial match? i.e. keep the same repositories, delete # additional repositories and add missing ones - if product&.repositories&.sort == new_product.repositories.sort + if product&.repositories&.sort == new_product.repositories.sort && !local_repo? # the same repositories, we just needed to reset the package selection Yast::Pkg.PkgReset() else diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb index 9e1179ce3a..5ea4e1fd69 100644 --- a/service/lib/agama/software/product.rb +++ b/service/lib/agama/software/product.rb @@ -53,6 +53,11 @@ class Product # @return [Array] Empty if the product requires registration. attr_accessor :repositories + # List of disk labels used for installation repository. + # + # @return [Array] Empty if the product does not support offline installation. + attr_accessor :labels + # Mandatory packages. # # @return [Array] @@ -95,6 +100,7 @@ class Product def initialize(id) @id = id @repositories = [] + @labels = [] @mandatory_packages = [] @optional_packages = [] @mandatory_patterns = [] diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb index c048be304e..465e2107fd 100644 --- a/service/lib/agama/software/product_builder.rb +++ b/service/lib/agama/software/product_builder.rb @@ -36,20 +36,7 @@ def initialize(config) def build config.products.map do |id, attrs| data = product_data_from_config(id) - - Agama::Software::Product.new(id).tap do |product| - product.display_name = attrs["name"] - product.description = attrs["description"] - product.name = data[:name] - product.version = data[:version] - product.repositories = data[:repositories] - product.mandatory_packages = data[:mandatory_packages] - product.optional_packages = data[:optional_packages] - product.mandatory_patterns = data[:mandatory_patterns] - product.optional_patterns = data[:optional_patterns] - product.user_patterns = data[:user_patterns] - product.translations = attrs["translations"] || {} - end + create_product(id, data, attrs) end end @@ -58,6 +45,23 @@ def build # @return [Agama::Config] attr_reader :config + def create_product(id, data, attrs) + Agama::Software::Product.new(id).tap do |product| + product.display_name = attrs["name"] + product.description = attrs["description"] + product.name = data[:name] + product.version = data[:version] + product.repositories = data[:repositories] + product.labels = data[:labels] + product.mandatory_packages = data[:mandatory_packages] + product.optional_packages = data[:optional_packages] + product.mandatory_patterns = data[:mandatory_patterns] + product.optional_patterns = data[:optional_patterns] + product.user_patterns = data[:user_patterns] + product.translations = attrs["translations"] || {} + end + end + # Data from config, filtering by arch. # # @param id [String] @@ -66,6 +70,9 @@ def product_data_from_config(id) { name: config.products.dig(id, "software", "base_product"), version: config.products.dig(id, "software", "version"), + labels: config.arch_elements_from( + id, "software", "installation_labels", property: :label + ), repositories: config.arch_elements_from( id, "software", "installation_repositories", property: :url ), diff --git a/service/package/gem2rpm.yml b/service/package/gem2rpm.yml index 7fab722649..c95995ae40 100644 --- a/service/package/gem2rpm.yml +++ b/service/package/gem2rpm.yml @@ -74,6 +74,8 @@ Requires: udftools Requires: xfsprogs Requires: yast2-schema + # lsblk + Requires: util-linux-systemd :filelist: "%{_datadir}/dbus-1/agama.conf\n %dir %{_datadir}/dbus-1/agama-services\n %{_datadir}/dbus-1/agama-services/org.opensuse.Agama*.service\n diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb index d121571a54..34d6458b7c 100644 --- a/service/test/agama/software/manager_test.rb +++ b/service/test/agama/software/manager_test.rb @@ -215,6 +215,7 @@ describe "#probe" do before do subject.select_product("Tumbleweed") + allow(subject).to receive(:list_disks).and_return({}) end it "creates a packages proposal" do @@ -228,6 +229,21 @@ subject.probe end + it "uses the offline medium if available" do + device = "/dev/sr1" + expect(subject).to receive(:list_disks).and_return({ + "blockdevices" => [ + { + "kname" => device, + "label" => "openSUSE-Tumbleweed-DVD-x86_64" + } + ] + }) + + expect(repositories).to receive(:add).with("hd:/?device=" + device) + subject.probe + end + include_examples "software issues", "probe" end