Skip to content

Commit

Permalink
Install from local DVD if it is present (#1372)
Browse files Browse the repository at this point in the history
## 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 <igonzalezsosa@suse.com>
  • Loading branch information
lslezak and imobachgs authored Jun 26, 2024
1 parent ce30ede commit 1a0d1e0
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 17 deletions.
10 changes: 10 additions & 0 deletions doc/yaml_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions products.d/microos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions products.d/tumbleweed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion service/lib/agama/product_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 67 additions & 2 deletions service/lib/agama/software/manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
# find current contact information at www.suse.com.

require "fileutils"
require "json"
require "yast"
require "y2packager/product"
require "y2packager/resolvable"
Expand Down Expand Up @@ -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<String>] 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)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions service/lib/agama/software/product.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class Product
# @return [Array<String>] Empty if the product requires registration.
attr_accessor :repositories

# List of disk labels used for installation repository.
#
# @return [Array<String>] Empty if the product does not support offline installation.
attr_accessor :labels

# Mandatory packages.
#
# @return [Array<String>]
Expand Down Expand Up @@ -95,6 +100,7 @@ class Product
def initialize(id)
@id = id
@repositories = []
@labels = []
@mandatory_packages = []
@optional_packages = []
@mandatory_patterns = []
Expand Down
35 changes: 21 additions & 14 deletions service/lib/agama/software/product_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]
Expand All @@ -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
),
Expand Down
2 changes: 2 additions & 0 deletions service/package/gem2rpm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions service/test/agama/software/manager_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down

0 comments on commit 1a0d1e0

Please sign in to comment.