/* * SPDX-FileCopyrightText: 2021 - 2024 StorPool * SPDX-License-Identifier: BSD-2-Clause */ //! Detect the OS distribution and version. #![warn(missing_docs)] // We do not want to expose the whole of the autogenerated data module. #![allow(clippy::pub_use)] use std::clone::Clone; use std::collections::HashMap; use std::fs; use std::io::{Error as IoError, ErrorKind}; use regex::RegexBuilder; use serde_derive::{Deserialize, Serialize}; use thiserror::Error; use yai::YAIError; mod data; pub mod yai; #[cfg(test)] pub mod tests; pub use data::VariantKind; /// An error that occurred while determining the Linux variant. #[derive(Debug, Error)] #[non_exhaustive] pub enum VariantError { /// An invalid variant name was specified. #[error("Unknown variant '{0}'")] BadVariant(String), /// A file to be examined could not be read. #[error("Checking for {0}: could not read {1}")] FileRead(String, String, #[source] IoError), /// Unexpected error parsing the /etc/os-release file. #[error("Could not parse the /etc/os-release file")] OsRelease(#[source] YAIError), /// None of the variants matched. #[error("Could not detect the current host's build variant")] UnknownVariant, /// Something went really, really wrong. #[error("Internal sp-variant error: {0}")] Internal(String), } /// The version of the variant definition format data. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[non_exhaustive] pub struct VariantFormatVersion { /// The version major number. pub major: u32, /// The version minor number. pub minor: u32, } /// The internal format of the variant definition format data. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[non_exhaustive] pub struct VariantFormat { /// The version of the metadata format. pub version: VariantFormatVersion, } #[derive(Debug, Serialize, Deserialize)] struct VariantFormatTop { format: VariantFormat, } /// Check whether this host is running this particular OS variant. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct Detect { /// The name of the file to read. pub filename: String, /// The regular expression pattern to look for in the file. pub regex: String, /// The "ID" field in the /etc/os-release file. pub os_id: String, /// The regular expression pattern for the "VERSION_ID" os-release field. pub os_version_regex: String, } /// The aspects of the StorPool operation supported for this build variant. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct Supported { /// Is there a StorPool third-party packages repository? pub repo: bool, } /// Debian package repository data. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct DebRepo { /// The distribution codename (e.g. "buster"). pub codename: String, /// The distribution vendor ("debian", "ubuntu", etc.). pub vendor: String, /// The APT sources list file to copy to /etc/apt/sources.list.d/. pub sources: String, /// The GnuPG keyring file to copy to /usr/share/keyrings/. pub keyring: String, /// OS packages that need to be installed before `apt-get update` is run. pub req_packages: Vec, } /// Yum/DNF package repository data. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct YumRepo { /// The *.repo file to copy to /etc/yum.repos.d/. pub yumdef: String, /// The keyring file to copy to /etc/pki/rpm-gpg/. pub keyring: String, } /// OS package repository data. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(untagged)] #[non_exhaustive] pub enum Repo { /// Debian/Ubuntu repository data. Deb(DebRepo), /// CentOS/Oracle repository data. Yum(YumRepo), } /// StorPool builder data. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct Builder { /// The builder name. pub alias: String, /// The base Docker image that the builder is generated from. pub base_image: String, /// The branch used by the sp-pkg tool to specify the variant. pub branch: String, /// The base kernel OS package. pub kernel_package: String, /// The name of the locale to use for clean UTF-8 output. pub utf8_locale: String, } /// A single StorPool build variant with all its options. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct Variant { /// Which variant is that? #[serde(rename = "name")] pub kind: VariantKind, /// The human-readable description of the variant. pub descr: String, /// The OS "family" that this distribution belongs to. pub family: String, /// The name of the variant that this one is based on. pub parent: String, /// The ways to check whether we are running this variant. pub detect: Detect, /// The aspects of StorPool operation supported for this build variant. pub supported: Supported, /// The OS commands to execute for particular purposes. pub commands: HashMap>>, /// The minimum Python version that we can depend on. pub min_sys_python: String, /// The StorPool repository files to install. pub repo: Repo, /// The names of the packages to be used for this variant. pub package: HashMap, /// The name of the directory to install systemd unit files to. pub systemd_lib: String, /// The filename extension of the OS packages ("deb", "rpm", etc.). pub file_ext: String, /// The type of initramfs-generating tools. pub initramfs_flavor: String, /// The data specific to the StorPool builder containers. pub builder: Builder, } /// The internal variant format data: all build variants, some more info. #[derive(Debug, Serialize, Deserialize)] pub struct VariantDefTop { format: VariantFormat, order: Vec, variants: HashMap, version: String, } /// Get the list of StorPool variants from the internal `data` module. #[inline] #[must_use] pub fn build_variants() -> &'static VariantDefTop { data::get_variants() } /// Detect the variant that this host is currently running. /// /// # Errors /// Propagates any errors from [`detect_from()`]. #[inline] pub fn detect() -> Result { detect_from(build_variants()).cloned() } /// Detect the current host's variant from the supplied data. /// /// # Errors /// May return a [`VariantError`], either "unknown variant" or a wrapper around /// an underlying error condition: /// - any `os-release` parse errors from [`crate::yai::parse()`] other than "file not found" /// - I/O errors from reading the distribution-specific version files (e.g. `/etc/redhat-release`) #[allow(clippy::missing_inline_in_public_items)] pub fn detect_from(variants: &VariantDefTop) -> Result<&Variant, VariantError> { match yai::parse("/etc/os-release") { Ok(data) => { if let Some(os_id) = data.get("ID") { if let Some(version_id) = data.get("VERSION_ID") { for kind in &variants.order { let var = &variants.variants.get(kind).ok_or_else(|| { VariantError::Internal(format!( "Internal error: unknown variant {kind} in the order", kind = kind.as_ref() )) })?; if var.detect.os_id != *os_id { continue; } let re_ver = RegexBuilder::new(&var.detect.os_version_regex) .ignore_whitespace(true) .build() .map_err(|err| { VariantError::Internal(format!( "Internal error: {kind}: could not parse '{regex}': {err}", kind = kind.as_ref(), regex = var.detect.regex )) })?; if re_ver.is_match(version_id) { return Ok(var); } } } } // Fall through to the PRETTY_NAME processing. } Err(YAIError::FileRead(io_err)) if io_err.kind() == ErrorKind::NotFound => (), Err(err) => return Err(VariantError::OsRelease(err)), } for kind in &variants.order { let var = &variants.variants.get(kind).ok_or_else(|| { VariantError::Internal(format!( "Internal error: unknown variant {kind} in the order", kind = kind.as_ref() )) })?; let re_line = RegexBuilder::new(&var.detect.regex) .ignore_whitespace(true) .build() .map_err(|err| { VariantError::Internal(format!( "Internal error: {kind}: could not parse '{regex}': {err}", kind = kind.as_ref(), regex = var.detect.regex )) })?; match fs::read(&var.detect.filename) { Ok(file_bytes) => { if let Ok(contents) = String::from_utf8(file_bytes) { { if contents.lines().any(|line| re_line.is_match(line)) { return Ok(var); } } } } Err(err) => { if err.kind() != ErrorKind::NotFound { return Err(VariantError::FileRead( var.kind.as_ref().to_owned(), var.detect.filename.clone(), err, )); } } }; } Err(VariantError::UnknownVariant) } /// Get the variant with the specified name from the supplied data. /// /// # Errors /// - [`VariantKind`] name parse errors, e.g. invalid name /// - an internal error if there is no data about a recognized variant name #[inline] pub fn get_from<'defs>( variants: &'defs VariantDefTop, name: &str, ) -> Result<&'defs Variant, VariantError> { let kind: VariantKind = name.parse()?; variants .variants .get(&kind) .ok_or_else(|| VariantError::Internal(format!("No data for the {name} variant"))) } /// Get the variant with the specified builder alias from the supplied data. /// /// # Errors /// May fail if the argument does not specify a recognized variant builder alias. #[inline] pub fn get_by_alias_from<'defs>( variants: &'defs VariantDefTop, alias: &str, ) -> Result<&'defs Variant, VariantError> { variants .variants .values() .find(|var| var.builder.alias == alias) .ok_or_else(|| VariantError::Internal(format!("No variant with the {alias} alias"))) } /// Get information about all variants. #[inline] #[must_use] pub fn get_all_variants() -> &'static HashMap { get_all_variants_from(build_variants()) } /// Get information about all variants defined in the specified structure. #[inline] #[must_use] pub const fn get_all_variants_from(variants: &VariantDefTop) -> &HashMap { &variants.variants } /// Get information about all variants in the order of inheritance between them. #[inline] pub fn get_all_variants_in_order() -> impl Iterator { get_all_variants_in_order_from(build_variants()) } /// Get information about all variants defined in the specified structure in order. /// /// # Panics /// May panic if the variants data is inconsistent and the variants order array /// includes a [`VariantKind`] that is not present in the actual hashmap. /// This should hopefully never ever happen, and there is a unit test for that. #[inline] #[allow(clippy::indexing_slicing)] pub fn get_all_variants_in_order_from(variants: &VariantDefTop) -> impl Iterator { variants.order.iter().map(|kind| &variants.variants[kind]) } /// Get the metadata format version of the variant data. #[inline] #[must_use] pub fn get_format_version() -> (u32, u32) { get_format_version_from(build_variants()) } /// Get the metadata format version of the supplied variant data structure. #[inline] #[must_use] pub const fn get_format_version_from(variants: &VariantDefTop) -> (u32, u32) { (variants.format.version.major, variants.format.version.minor) } /// Get the program version from the variant data. #[inline] #[must_use] pub fn get_program_version() -> &'static str { get_program_version_from(build_variants()) } /// Get the program version from the supplied variant data structure. #[inline] #[must_use] pub fn get_program_version_from(variants: &VariantDefTop) -> &str { &variants.version }