diff --git a/products.d/agama-products.changes b/products.d/agama-products.changes index ba020c407f..8558eee711 100644 --- a/products.d/agama-products.changes +++ b/products.d/agama-products.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Tue Mar 4 09:06:46 UTC 2025 - Frederic Crozat + +- Update patterns list for SLES / SLES_SAP 16. + ------------------------------------------------------------------- Thu Feb 20 13:13:18 UTC 2025 - Josef Reidinger diff --git a/products.d/sles_160.yaml b/products.d/sles_160.yaml index b7cee8e62c..937ebd11ff 100644 --- a/products.d/sles_160.yaml +++ b/products.d/sles_160.yaml @@ -65,13 +65,26 @@ software: archs: ppc mandatory_patterns: - - base_traditional + - base optional_patterns: null # no optional pattern shared user_patterns: - - kvm_host - cockpit - - sles_enhanced_base + - enhanced_base - sles_minimal_sap + - fips + - selinux + - documentation + - sw_management + - container_runtime_podman + - dhcp_dns_server + - directory_server + - file_server + - gateway_server + - kvm_server + - kvm_tools + - lamp_server + - mail_server + - printing mandatory_packages: - NetworkManager - sudo-policy-wheel-auth-self # explicit wheel group policy to conform new auth model diff --git a/products.d/sles_sap_160.yaml b/products.d/sles_sap_160.yaml index f4ff762460..7cdc5b079d 100644 --- a/products.d/sles_sap_160.yaml +++ b/products.d/sles_sap_160.yaml @@ -26,7 +26,7 @@ software: archs: ppc mandatory_patterns: - - base_traditional + - base - sles_sap_enhanced_base - sles_sap_base_sap_server optional_patterns: null # no optional pattern shared diff --git a/rust/agama-cli/src/commands.rs b/rust/agama-cli/src/commands.rs index 175fe63962..630767cbad 100644 --- a/rust/agama-cli/src/commands.rs +++ b/rust/agama-cli/src/commands.rs @@ -18,6 +18,8 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. +use std::path::PathBuf; + use crate::auth::AuthCommands; use crate::config::ConfigCommands; use crate::logs::LogsCommands; @@ -105,6 +107,8 @@ pub enum Commands { Download { /// URL pointing to file for download url: String, + /// File name + destination: PathBuf, }, /// Finish the installation rebooting the system by default. /// diff --git a/rust/agama-cli/src/lib.rs b/rust/agama-cli/src/lib.rs index dd5a43fddc..f25562c5db 100644 --- a/rust/agama-cli/src/lib.rs +++ b/rust/agama-cli/src/lib.rs @@ -19,6 +19,7 @@ // find current contact information at www.suse.com. use agama_lib::manager::FinishMethod; +use anyhow::Context; use clap::{Args, Parser}; mod auth; @@ -43,6 +44,9 @@ use logs::run as run_logs_cmd; use profile::run as run_profile_cmd; use progress::InstallerProgress; use questions::run as run_questions_cmd; +use std::fs; +use std::os::unix::fs::OpenOptionsExt; +use std::path::PathBuf; use std::{ collections::HashMap, process::{ExitCode, Termination}, @@ -198,6 +202,21 @@ async fn allowed_insecure_api(use_insecure: bool, api_url: String) -> Result Result<(), ServiceError> { + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .mode(0o400) + .open(path) + .context(format!("Cannot write the file '{}'", path.display()))?; + + match Transfer::get(&url, &mut file) { + Ok(()) => println!("File saved to {}", path.display()), + Err(e) => eprintln!("Could not retrieve the file: {e}"), + } + Ok(()) +} + pub async fn run_command(cli: Cli) -> Result<(), ServiceError> { // somehow check whether we need to ask user for self-signed certificate acceptance let api_url = cli.opts.api.trim_end_matches('/').to_string(); @@ -238,7 +257,7 @@ pub async fn run_command(cli: Cli) -> Result<(), ServiceError> { } Commands::Questions(subcommand) => run_questions_cmd(client, subcommand).await?, Commands::Logs(subcommand) => run_logs_cmd(client, subcommand).await?, - Commands::Download { url } => Transfer::get(&url, std::io::stdout())?, + Commands::Download { url, destination } => download_file(&url, &destination)?, Commands::Auth(subcommand) => { run_auth_cmd(client, subcommand).await?; } diff --git a/rust/agama-cli/src/profile.rs b/rust/agama-cli/src/profile.rs index c76be3147c..150f20324e 100644 --- a/rust/agama-cli/src/profile.rs +++ b/rust/agama-cli/src/profile.rs @@ -146,8 +146,8 @@ async fn import(url_string: String, dir: Option) -> anyhow::Result<()> fn pre_process_profile>(url_string: &str, path: P) -> anyhow::Result<()> { let work_dir = path.as_ref().parent().unwrap(); let tmp_profile_path = work_dir.join("profile.temp"); - let tmp_file = File::create(&tmp_profile_path)?; - Transfer::get(url_string, tmp_file)?; + let mut tmp_file = File::create(&tmp_profile_path)?; + Transfer::get(url_string, &mut tmp_file)?; match FileFormat::from_file(&tmp_profile_path)? { FileFormat::Jsonnet => { diff --git a/rust/agama-lib/share/examples/profile_tw.json b/rust/agama-lib/share/examples/profile_tw.json index c783a9972d..e410ced3a1 100644 --- a/rust/agama-lib/share/examples/profile_tw.json +++ b/rust/agama-lib/share/examples/profile_tw.json @@ -12,12 +12,6 @@ "id": "Tumbleweed" }, "storage": { - "guided": { - "boot": { - "configure": true, - "device": "/dev/dm-1" - } - } }, "user": { "fullName": "Jane Doe", diff --git a/rust/agama-lib/share/examples/storage/guided.json b/rust/agama-lib/share/examples/storage/guided.json deleted file mode 100644 index 573aed64d2..0000000000 --- a/rust/agama-lib/share/examples/storage/guided.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "storage": { - "guided": { - "target": { - "disk": "/dev/vdc" - }, - "boot": { - "configure": true, - "device": "/dev/vda" - }, - "encryption": { - "password": "notsecret", - "method": "luks2", - "pbkdFunction": "argon2i" - }, - "space": { - "policy": "custom", - "actions": [ - { "resize": "/dev/vda" }, - { "forceDelete": "/dev/vdb1" } - ] - }, - "volumes": [ - { - "mount": { - "path": "/", - "options": ["ro"] - }, - "filesystem": { - "btrfs": { - "snapshots": true - } - }, - "size": [1024, "5 Gib"], - "target": "default" - }, - { - "mount": { - "path": "/home" - }, - "filesystem": "xfs", - "size": { - "min": "5 GiB", - "max": "20 GiB" - }, - "target": { - "newVg": "/dev/vda" - } - }, - { - "mount": { - "path": "swap" - }, - "filesystem": "swap", - "size": "8 GiB", - "target": { - "newPartition": "/dev/vda" - } - } - ] - } - } -} diff --git a/rust/agama-lib/share/storage.schema.json b/rust/agama-lib/share/storage.schema.json index 0ddae32c14..27ec46f8e3 100644 --- a/rust/agama-lib/share/storage.schema.json +++ b/rust/agama-lib/share/storage.schema.json @@ -16,8 +16,7 @@ "description": "LVM volume groups.", "type": "array", "items": { "$ref": "#/$defs/volumeGroup" } - }, - "guided": { "$ref": "#/$defs/guided" } + } }, "$defs": { "boot": { diff --git a/rust/agama-lib/src/scripts/model.rs b/rust/agama-lib/src/scripts/model.rs index 80bc2d2569..7efb1bf987 100644 --- a/rust/agama-lib/src/scripts/model.rs +++ b/rust/agama-lib/src/scripts/model.rs @@ -65,7 +65,7 @@ impl BaseScript { match &self.source { ScriptSource::Text { body } => write!(file, "{}", &body)?, - ScriptSource::Remote { url } => Transfer::get(url, file)?, + ScriptSource::Remote { url } => Transfer::get(url, &mut file)?, }; Ok(()) diff --git a/rust/agama-lib/src/utils/transfer.rs b/rust/agama-lib/src/utils/transfer.rs index a9c9475387..b60d4db2bb 100644 --- a/rust/agama-lib/src/utils/transfer.rs +++ b/rust/agama-lib/src/utils/transfer.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2025] SUSE LLC // // All Rights Reserved. // @@ -20,21 +20,61 @@ //! File transfer API for Agama. //! -//! Implement a file transfer API which, in the future, will support Agama specific URLs. Check the +//! Implement a file transfer API which, at this point, partially supports Agama specific URLs. Check the //! YaST document about [URL handling in the //! installer](https://github.com/yast/yast-installation/blob/master/doc/url.md) for further //! information. //! -//! At this point, it only supports those schemes supported by CURL. +//! This API supports the following URLs from YaST: `device:`, `usb:`, `label:`, ! `hd:`, `dvd:` and +//! `cd:`. The support for well-known URLs (e.g., `file:`, `http:`, `https:`, ! `ftp:`, `nfs:`, +//! etc.) is implemented using CURL. +//! +//! Support for `relurl:` and `repo:` are still missing. +//! +//! ## SSL +//! +//! YaST support for HTTPS used a custom certificate which was located in +//! `/etc/sssl/clientcerts/client-cert.pem`. Agama does not use such a certificate and it only +//! relies on those that are installed in the installation media. +//! +//! ## Examples +//! Requires working localectl. +//! +//! ```no_run +//! use agama_lib::utils::Transfer; +//! Transfer::get("label://OEMDRV/autoinst.xml", &mut std::io::stdout()).unwrap(); +//! ```` use std::io::Write; -use curl::easy::Easy; use thiserror::Error; +use url::Url; + +mod file_finder; +mod file_systems; +mod handlers; + +use handlers::{DeviceHandler, GenericHandler, HdHandler, LabelHandler}; #[derive(Error, Debug)] -#[error(transparent)] -pub struct TransferError(#[from] curl::Error); +pub enum TransferError { + #[error("Could not retrieve the file: {0}")] + CurlError(#[from] curl::Error), + #[error("Could not parse the URL: {0}")] + ParseError(#[from] url::ParseError), + #[error("File not found: {0}")] + FileNotFound(String), + #[error("IO error: {0}")] + IO(#[from] std::io::Error), + #[error("Could not mount the file system {0}")] + FileSystemMount(String), + #[error("Missing file path: {0}")] + MissingPath(Url), + #[error("Missing device: {0}")] + MissingDevice(Url), + #[error("Missing file system label: {0}")] + MissingLabel(Url), +} pub type TransferResult = Result; /// File transfer API @@ -45,15 +85,13 @@ impl Transfer { /// /// * `url`: URL to get the data from. /// * `out_fd`: where to write the data. - pub fn get(url: &str, mut out_fd: impl Write) -> TransferResult<()> { - let mut handle = Easy::new(); - handle.follow_location(true)?; - handle.fail_on_error(true)?; - handle.url(url)?; - - let mut transfer = handle.transfer(); - transfer.write_function(|buf| Ok(out_fd.write(buf).unwrap()))?; - transfer.perform()?; - Ok(()) + pub fn get(url: &str, out_fd: &mut impl Write) -> TransferResult<()> { + let url = Url::parse(url)?; + match url.scheme() { + "device" | "usb" => DeviceHandler::default().get(url, out_fd), + "label" => LabelHandler::default().get(url, out_fd), + "cd" | "dvd" | "hd" => HdHandler::default().get(url, out_fd), + _ => GenericHandler::default().get(url, out_fd), + } } } diff --git a/rust/agama-lib/src/utils/transfer/file_finder.rs b/rust/agama-lib/src/utils/transfer/file_finder.rs new file mode 100644 index 0000000000..081266cb30 --- /dev/null +++ b/rust/agama-lib/src/utils/transfer/file_finder.rs @@ -0,0 +1,81 @@ +// 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. + +use std::{ + io::Write, + path::{Path, PathBuf}, +}; + +use super::{ + file_systems::{FileSystem, FileSystemsList}, + TransferError, TransferResult, +}; + +/// Finds a file in a set of file systems and copies its content. +#[derive(Default)] +pub struct FileFinder {} + +impl FileFinder { + /// Searchs for a file in the given file systems and copies its content to the given writer. + /// + /// * `file_systems`: file systems to search in. + /// * `file_name`: file name. + /// * `writer`: where to write the contents. + pub fn copy_from_file_systems( + &self, + file_systems: &FileSystemsList, + file_name: &str, + writer: &mut impl Write, + ) -> TransferResult<()> { + for fs in file_systems.to_vec().iter() { + if self.copy_from_file_system(fs, &file_name, writer).is_ok() { + return Ok(()); + } + } + Err(TransferError::FileNotFound(file_name.to_string())) + } + + /// Copies the file from the file system to the given writer. + /// + /// * `file_systems`: file systems to search in. + /// * `file_name`: file name. + /// * `writer`: where to write the contents. + pub fn copy_from_file_system( + &self, + file_system: &FileSystem, + file_name: &str, + writer: &mut impl Write, + ) -> TransferResult<()> { + println!("Searching {} in {}", &file_name, &file_system.block_device); + + file_system.ensure_mounted(|mount_point: &PathBuf| { + let file_name = file_name.strip_prefix("/").unwrap_or(file_name); + let source = mount_point.join(&file_name); + Self::copy_file(source, writer) + }) + } + + /// Reads and write the file content to the given writer. + fn copy_file>(source: P, out_fd: &mut impl Write) -> TransferResult<()> { + let mut reader = std::fs::File::open(source)?; + std::io::copy(&mut reader, out_fd)?; + Ok(()) + } +} diff --git a/rust/agama-lib/src/utils/transfer/file_systems.rs b/rust/agama-lib/src/utils/transfer/file_systems.rs new file mode 100644 index 0000000000..b03e55b6d6 --- /dev/null +++ b/rust/agama-lib/src/utils/transfer/file_systems.rs @@ -0,0 +1,341 @@ +// 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. + +/// Module to search for file systems. +use std::{path::PathBuf, process::Command}; + +use regex::Regex; + +use super::{TransferError, TransferResult}; + +/// Represents a file system from the underlying system. +/// +/// It only includes the elements that are relevant for the transfer API. +#[derive(Clone, Debug, Default)] +pub struct FileSystem { + pub block_device: String, + pub fstype: Option, + pub mount_point: Option, + pub transport: Option, + pub label: Option, +} + +impl FileSystem { + /// Whether the file system was mounted. + pub fn is_mounted(&self) -> bool { + self.mount_point.is_some() + } + + /// Kernel name of the block device containing the file system. + pub fn device(&self) -> String { + format!("/dev/{}", &self.block_device) + } + + /// Mounts the file system and runs the given function. + /// + /// It does not try to mount the file system if it is already mounted. + /// + /// * `func`: function to run. It receives the mount point. + /// + /// TODO: TransferResult and TransferError should not be visible from this + /// struct. + pub fn ensure_mounted(&self, func: F) -> TransferResult<()> + where + F: FnOnce(&PathBuf) -> TransferResult<()>, + { + const DEFAULT_MOUNT_PATH: &str = "/run/agama/mount"; + let default_mount_point = PathBuf::from(DEFAULT_MOUNT_PATH); + let mount_point = self + .mount_point + .clone() + .unwrap_or_else(|| default_mount_point); + + if !self.is_mounted() { + self.mount(&mount_point).unwrap(); + } + let result = func(&mount_point); + if !self.is_mounted() { + self.umount(&mount_point).unwrap(); + } + + result + } + + /// Whether the file system can be mounted. + /// + /// File systems that cannot be mounted are ignored. + fn can_be_mounted(&self) -> bool { + let Some(fstype) = &self.fstype else { + return false; + }; + + match fstype.as_str() { + "" | "crypto_LUKS" | "swap" => false, + _ => true, + } + } + + /// Mounts file system from the given mount point. + fn mount(&self, mount_point: &PathBuf) -> TransferResult<()> { + std::fs::create_dir_all(mount_point)?; + let output = Command::new("mount") + .args([ + "-o", + "ro", + &self.device(), + &mount_point.display().to_string(), + ]) + .output()?; + if !output.status.success() { + return Err(TransferError::FileSystemMount(self.device())); + } + Ok(()) + } + + /// Umounts file system from the given mount point. + fn umount(&self, mount_point: &PathBuf) -> TransferResult<()> { + Command::new("umount") + .arg(mount_point.display().to_string()) + .output()?; + Ok(()) + } +} + +/// Holds a list of file systems. +/// +/// It offers a set of convenience method to search within the list. +#[derive(Debug, Default)] +pub struct FileSystemsList { + file_systems: Vec, +} + +impl FileSystemsList { + /// Creates a list of file systems. + pub fn new(file_systems: Vec) -> Self { + Self { file_systems } + } + + /// Creates a list for the file systems in the underlying system. + pub fn from_system() -> Self { + let file_systems = FileSystemsReader::read_from_system(); + Self::new(file_systems) + } + + pub fn to_vec(&self) -> Vec { + self.file_systems.clone() + } + + /// Returns the file system with the given block device name. + /// + /// * `name`: block device name. + pub fn find_by_name(&self, name: &str) -> Option<&FileSystem> { + self.file_systems.iter().find(|fs| name == &fs.block_device) + } + + /// Returns the file systems with the given label name. + /// + /// * `label`: device label. + pub fn with_label(&mut self, label: &str) -> Self { + let label = Some(label.to_string()); + let file_systems = self + .file_systems + .iter() + .filter(|fs| fs.label == label) + .cloned() + .collect(); + + FileSystemsList { file_systems } + } + + /// Returns the file systems using the given transport. + /// + /// * `transport`: transport of the device (e.g., "usb"). + pub fn with_transport(&mut self, transport: &str) -> Self { + let transport = Some(transport.to_string()); + let file_systems = self + .file_systems + .iter() + .filter(|fs| fs.transport == transport) + .cloned() + .collect(); + + FileSystemsList { file_systems } + } +} + +/// Implements the logic to read the file systems from the underlying system. +/// +/// This struct relies on lsblk to find the file systems. It is extracted to +/// a separate struct to make testing easier. +struct FileSystemsReader {} + +impl FileSystemsReader { + /// Returns the file systems from the underlying system. + pub fn read_from_system() -> Vec { + let lsblk = Command::new("lsblk") + .args([ + "--output", + "KNAME,FSTYPE,MOUNTPOINTS,TRAN,LABEL", + "--pairs", + "--path", + ]) + .output() + .unwrap(); + let output = String::from_utf8_lossy(&lsblk.stdout); + Self::read_from_string(&output) + } + + /// Turns the output of lsblk into a list of file systems. + pub fn read_from_string(lsblk_string: &str) -> Vec { + let mut file_systems = vec![]; + let mut parent_transport: Option = None; + let re = + Regex::new(r#"KNAME="(.+)" FSTYPE="(.*)" MOUNTPOINTS="(.*)" TRAN="(.*)" LABEL="(.*)""#) + .unwrap(); + + for (_, [block_device, fstype, mount_points, transport, label]) in + re.captures_iter(lsblk_string).map(|c| c.extract()) + { + // Use the shorter path as the canonical mount point. + let mut mounts = mount_points.split("\\x0a").collect::>(); + mounts.sort_by(|a, b| a.len().cmp(&b.len())); + + let mut file_system = FileSystem { + block_device: block_device + .strip_prefix("/dev/") + .unwrap_or(block_device) + .to_string(), + fstype: if fstype.is_empty() { + None + } else { + Some(fstype.to_string()) + }, + mount_point: mounts.first().map(|m| PathBuf::from(m)), + transport: if transport.is_empty() { + None + } else { + Some(transport.to_string()) + }, + label: if label.is_empty() { + None + } else { + Some(label.to_string()) + }, + }; + if file_system.transport.is_none() { + file_system.transport = parent_transport.clone(); + } else { + parent_transport = file_system.transport.clone(); + } + if file_system.can_be_mounted() { + file_systems.push(file_system); + } + } + + file_systems + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::{FileSystem, FileSystemsList, FileSystemsReader}; + + fn build_file_systems() -> Vec { + let vda1 = FileSystem { + block_device: "vda1".to_string(), + fstype: Some("ext4".to_string()), + mount_point: Some(PathBuf::from("/")), + ..Default::default() + }; + let vdb1 = FileSystem { + block_device: "vdb1".to_string(), + fstype: Some("xfs".to_string()), + mount_point: Some(PathBuf::from("/home")), + ..Default::default() + }; + let usb = FileSystem { + block_device: "sr0".to_string(), + fstype: Some("vfat".to_string()), + mount_point: None, + transport: Some("usb".to_string()), + label: Some("OEMDRV".to_string()), + ..Default::default() + }; + vec![vda1, vdb1, usb] + } + + #[test] + fn test_find_file_system_by_name() { + let file_systems = build_file_systems(); + let list = FileSystemsList::new(file_systems); + let vdb1 = list.find_by_name("vdb1").unwrap(); + assert_eq!(&vdb1.block_device, "vdb1"); + } + + #[test] + fn test_find_file_system_by_label() { + let file_systems = build_file_systems(); + let mut list = FileSystemsList::new(file_systems); + let found = list.with_label("OEMDRV").to_vec(); + let usb = found.first().unwrap(); + assert_eq!(&usb.block_device, "sr0"); + } + + #[test] + fn test_find_file_system_by_transport() { + let file_systems = build_file_systems(); + let mut list = FileSystemsList::new(file_systems); + let found = list.with_transport("usb").to_vec(); + let usb = found.first().unwrap(); + assert_eq!(&usb.block_device, "sr0"); + } + + #[test] + fn test_find_all() { + let file_systems = build_file_systems(); + let finder = FileSystemsList::new(file_systems); + let all = finder.to_vec(); + assert_eq!(all.len(), 3); + } + + #[test] + fn test_parse_file_systems() { + let lsblk = r#"KNAME="sda" FSTYPE="" MOUNTPOINT="" TRAN="usb" LABEL="" +KNAME="/dev/sda1" FSTYPE="iso9660" MOUNTPOINTS="/run/media/user/agama-installer" TRAN="" LABEL="agama-installer" +KNAME="/dev/sda2" FSTYPE="vfat" MOUNTPOINTS="" TRAN="" LABEL="BOOT" +KNAME="/dev/nvme0n1" FSTYPE="" MOUNTPOINTS="" TRAN="nvme" LABEL="" +KNAME="/dev/nvme0n1p1" FSTYPE="vfat" MOUNTPOINTS="/boot/efi" TRAN="nvme" LABEL="" +KNAME="/dev/nvme0n1p2" FSTYPE="crypto_LUKS" MOUNTPOINT="" TRAN="nvme" LABEL="" +KNAME="/dev/dm-0" FSTYPE="btrfs" MOUNTPOINTS="/home\x0a/\x0a/var" TRAN="" LABEL="" +KNAME="/dev/nvme0n1p3" FSTYPE="crypto_LUKS" MOUNTPOINTS="" TRAN="nvme" LABEL="" +KNAME="/dev/dm-1" FSTYPE="swap" MOUNTPOINTS="[SWAP]" TRAN="" LABEL="" +"#; + let file_systems = FileSystemsReader::read_from_string(&lsblk); + assert_eq!(file_systems.len(), 4); + + let dm0 = file_systems + .into_iter() + .find(|fs| &fs.block_device == "dm-0") + .unwrap(); + assert_eq!(dm0.mount_point.unwrap(), PathBuf::from("/")); + } +} diff --git a/rust/agama-lib/src/utils/transfer/handlers.rs b/rust/agama-lib/src/utils/transfer/handlers.rs new file mode 100644 index 0000000000..2c62af111c --- /dev/null +++ b/rust/agama-lib/src/utils/transfer/handlers.rs @@ -0,0 +1,25 @@ +// 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. + +mod generic; +mod yast; + +pub use generic::GenericHandler; +pub use yast::{DeviceHandler, HdHandler, LabelHandler}; diff --git a/rust/agama-lib/src/utils/transfer/handlers/generic.rs b/rust/agama-lib/src/utils/transfer/handlers/generic.rs new file mode 100644 index 0000000000..b44f503e7b --- /dev/null +++ b/rust/agama-lib/src/utils/transfer/handlers/generic.rs @@ -0,0 +1,46 @@ +// 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. + +use std::io::Write; + +use curl::easy::Easy; +use url::Url; + +use crate::utils::TransferResult; + +/// Generic handler to retrieve any URL. +/// +/// It uses curl under the hood. +#[derive(Default)] +pub struct GenericHandler {} + +impl GenericHandler { + pub fn get(&self, url: Url, out_fd: &mut impl Write) -> TransferResult<()> { + let mut handle = Easy::new(); + handle.follow_location(true)?; + handle.fail_on_error(true)?; + handle.url(&url.to_string())?; + + let mut transfer = handle.transfer(); + transfer.write_function(|buf| Ok(out_fd.write(buf).unwrap()))?; + transfer.perform()?; + Ok(()) + } +} diff --git a/rust/agama-lib/src/utils/transfer/handlers/yast.rs b/rust/agama-lib/src/utils/transfer/handlers/yast.rs new file mode 100644 index 0000000000..f74b5b28b7 --- /dev/null +++ b/rust/agama-lib/src/utils/transfer/handlers/yast.rs @@ -0,0 +1,155 @@ +// 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. + +use std::io::Write; + +use url::Url; + +use crate::utils::{ + transfer::{file_finder::FileFinder, file_systems::FileSystemsList}, + TransferError, TransferResult, +}; + +/// Handler for the cd:, dvd: and hd: schemes +/// +/// It converts those schemes to a regular DeviceHandler. +#[derive(Default)] +pub struct HdHandler {} + +impl HdHandler { + pub fn get(&self, url: Url, out_fd: &mut impl Write) -> TransferResult<()> { + let device = url.query_pairs().find(|(key, _value)| key == "devices"); + + let Some((_, device_name)) = device else { + return Err(TransferError::MissingDevice(url)); + }; + let device_name = device_name.strip_prefix("/dev/").unwrap_or(&device_name); + let device_url = format!("device://{}{}", &device_name, url.path()); + let device_url = Url::parse(&device_url)?; + + DeviceHandler::default().get(device_url, out_fd) + } +} + +/// Handler for the label: scheme +#[derive(Default)] +pub struct LabelHandler {} + +impl LabelHandler { + pub fn get(&self, url: Url, out_fd: &mut impl Write) -> TransferResult<()> { + let file_name = url.path(); + if file_name.is_empty() { + return Err(TransferError::MissingPath(url)); + } + + let Some(label) = url.host_str() else { + return Err(TransferError::MissingLabel(url)); + }; + + let file_systems = FileSystemsList::from_system().with_label(label); + FileFinder::default().copy_from_file_systems(&file_systems, &file_name, out_fd) + } +} + +/// Handler to process AutoYaST-like URLs of type "device" and "usb". +/// +/// * If the URL contains a "host", it is used as the device name. +/// * If the URL does not contain a "host", it searches in all +/// known file systems. +#[derive(Default)] +pub struct DeviceHandler {} + +impl DeviceHandler { + pub fn get(&self, url: Url, out_fd: &mut impl Write) -> TransferResult<()> { + if url.path().is_empty() { + return Err(TransferError::MissingPath(url)); + } + + let mut file_systems = FileSystemsList::from_system(); + + if url.scheme() == "usb" { + file_systems = file_systems.with_transport("usb"); + } + + if let Some(host) = url.host_str() { + self.get_by_partial_names( + &mut file_systems, + &format!("{}{}", host, url.path()), + out_fd, + ) + } else { + self.get_from_any_device(&mut file_systems, url.path(), out_fd) + } + } + + /// Gets a file trying to guess the name from the device and the file itself. + /// + /// Given a URL like `device://a/b/c/d.json`, it will try: + /// + /// * device `a` and file `b/c/d.json`, + /// * device `a/b` and file `c/d.json` + /// * and device `a/b/c` and file `d.json`. + /// + /// See https://github.com/yast/yast-installation/blob/master/src/lib/transfer/file_from_url.rb#L483 + /// + /// * `file_systems`: list of file systems to search. + /// * `full_path`: full path to decompose and search for. + /// * `out_fd`: file to write to + fn get_by_partial_names( + &self, + file_systems: &mut FileSystemsList, + full_path: &str, + out_fd: &mut impl Write, + ) -> TransferResult<()> { + let mut path = full_path.to_string(); + let mut dev = "".to_string(); + let finder = FileFinder::default(); + + while let Some((device_name, file_name)) = path.split_once('/') { + dev = format!("{}/{}", dev, device_name) + .trim_start_matches('/') + .to_string(); + if let Some(file_system) = file_systems.find_by_name(&dev) { + if finder + .copy_from_file_system(&file_system, file_name, out_fd) + .is_ok() + { + return Ok(()); + } + } + path = file_name.to_string(); + } + Err(TransferError::FileNotFound(full_path.to_string())) + } + + /// Try to search in all devices. + /// + /// * `file_systems`: list of file systems to search. + /// * `file_name`: full path to decompose and search for. + /// * `out_fd`: file to write to + fn get_from_any_device( + &self, + file_systems: &mut FileSystemsList, + file_name: &str, + out_fd: &mut impl Write, + ) -> TransferResult<()> { + FileFinder::default().copy_from_file_systems(&file_systems, &file_name, out_fd) + } +} diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 49af30d0ca..5adec11d4f 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,14 @@ +------------------------------------------------------------------- +Thu Mar 6 12:51:42 UTC 2025 - Imobach Gonzalez Sosa + +- Extend agama download to support most of YaST-like URLs + (device:, usb:, label:, cd:, dvd: and hd:) (gh#agama-project/agama#2118). + +------------------------------------------------------------------- +Tue Mar 4 13:34:13 UTC 2025 - Martin Vidner + +- install and package also storage.schema.json (bsc#1238367) + ------------------------------------------------------------------- Wed Feb 26 06:52:52 UTC 2025 - José Iván López González diff --git a/rust/package/agama.spec b/rust/package/agama.spec index c9a83d1ce9..5aa84ef008 100644 --- a/rust/package/agama.spec +++ b/rust/package/agama.spec @@ -149,6 +149,7 @@ install -m 0755 %{_builddir}/agama/target/release/agama-web-server %{buildroot}% install -D -p -m 644 %{_builddir}/agama/share/agama.pam $RPM_BUILD_ROOT%{_pam_vendordir}/agama install -D -d -m 0755 %{buildroot}%{_datadir}/agama-cli install -m 0644 %{_builddir}/agama/agama-lib/share/profile.schema.json %{buildroot}%{_datadir}/agama-cli +install -m 0644 %{_builddir}/agama/agama-lib/share/storage.schema.json %{buildroot}%{_datadir}/agama-cli install -m 0644 %{_builddir}/agama/share/agama.libsonnet %{buildroot}%{_datadir}/agama-cli install --directory %{buildroot}%{_datadir}/dbus-1/agama-services install -m 0644 --target-directory=%{buildroot}%{_datadir}/dbus-1/agama-services %{_builddir}/agama/share/org.opensuse.Agama1.service @@ -217,6 +218,7 @@ echo $PATH %dir %{_datadir}/agama-cli %{_datadir}/agama-cli/agama.libsonnet %{_datadir}/agama-cli/profile.schema.json +%{_datadir}/agama-cli/storage.schema.json %{_mandir}/man1/agama*1%{?ext_man} %files -n agama-cli-bash-completion diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index d0fb3aafbe..a7d674c8b1 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -4,6 +4,12 @@ Wed Mar 5 14:50:04 UTC 2025 - Ladislav Slezák - Automatically retry package download or repository refresh before reporting an error (gh#agama-project/agama#2117) +------------------------------------------------------------------- +Wed Mar 5 09:21:03 UTC 2025 - Imobach Gonzalez Sosa + +- Enable again the signature checking for dir:/// repositories + (gh#agama-project/agama#2092). + ------------------------------------------------------------------- Wed Mar 5 08:09:28 UTC 2025 - Michal Filka @@ -11,6 +17,12 @@ Wed Mar 5 08:09:28 UTC 2025 - Michal Filka definition yaml file. It allows to control what boot strategy will be proposed by storage. Currently works only for BLS. +------------------------------------------------------------------- +Fri Feb 28 13:03:11 UTC 2025 - Imobach Gonzalez Sosa + +- Temporarily disable signature checking for dir:// repositories + (gh#agama-project/agama#2092). + ------------------------------------------------------------------- Wed Feb 26 06:52:45 UTC 2025 - José Iván López González