diff --git a/.github/workflows/rust.yml b/.github/workflows/ci-build.yml similarity index 100% rename from .github/workflows/rust.yml rename to .github/workflows/ci-build.yml diff --git a/.github/workflows/ci-clippy.yml b/.github/workflows/ci-clippy.yml new file mode 100644 index 0000000..9424fa9 --- /dev/null +++ b/.github/workflows/ci-clippy.yml @@ -0,0 +1,14 @@ +on: + push: + branches: + - "master" + pull_request: + +name: Sanity check +jobs: + clippy_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: rustup component add clippy + - run: cargo clippy -- -Dwarnings diff --git a/Cargo.toml b/Cargo.toml index a48197e..76ee9a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ clap_complete = "4.3.2" colored = "2.0.4" exitcode = "1.1.2" log = "0.4.20" +walkdir = "2.3.3" [profile.release] diff --git a/README.md b/README.md index 684a422..83df883 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,236 @@ # Limopack -**Li**nux **mo**dule **pack**age (helper) is a package helper to remove -unnecessary kernel modules on embedded or minimalistic setup, where -there is no expectations to put an existing disk into a different -hardware and expect it to boot. +1. [What is it?](#intro) +2. [Building](#building) +3. [Use Cases](#uc) + 1. [Determine Current Modules](#current-modules) + 2. [Freezing Modules](#freeze) + 2. [Flush Unnecessary Modules](#flush) +4. [Usage In Packaging](#pkg) +5. [Step-by-step Packaging](#sbs) + 1. [Step 1: The Kernel](#step-1) + 2. [Step 2: The Modules ](#step-2) + 3. [Step 3: Defining Vital Modules](#step-3) + 4. [Step 4: Defining Dynamic Modules](#step-4) +6. [Limitations](#limitations) -## Use Case -Create a very tiny Linux kernel setup from the mainline Linux kerne, +## What is it? + +**Li**nux **mo**dule **pack**age (helper) is a packaging helper for the +situation, where only required Linux kernel modules are needed to be +present on a system. + +The `limopack` is here to help you remove unnecessary kernel modules +on an embedded or any minimalistic setup, where there is no expectations +to pull an existing disk with a system and put it into a different hardware, +expecting it to successfully boot. Such behaviour is expected for the +mainline general purpose consumer system, but embedded. + +## Building + +`limopack` is written in Rust and is bult in a usual way on a standard +Rust-enabled environemnt, version 1.66 or higher. You would need the following: + +- Rust compiler `rustc` +- Cargo +- GNU make 4.3 or higher + +To build `limopack` simply checkout this repo, and run inside it: + + make [release] + + +## Use Cases +Create a very tiny Linux kernel setup from the mainline Linux kernel, where only necessary modules are present on the media, but achieveing this without intervening into an existing kernel maintenance framework or modifying upstream packaging. -## Principle -The Linmodpak works by installing a mainline kernel with all supported -modules and then remove those, that are not needed. +Below are the main use cases for the `limopack` helper. +### Determine Current Modules + +The `limopack` can (and should) determine which your modules are currently required +to be loaded next time your system boots and all their dependencies. To know this, +simply run module checker: + + limopack --list + +This command will return you a list of all loaded modules in your system, denoted +as a relative path to the current kernel, expecting the following prefix for the +complete path: + + /lib/modules// + +This command will only print-out a list of all currently required modules and their +relative paths. + +### Freezing Modules + +In order to flush unneeded modules, all required ones needs to be "frozen" or denoted +as those that are needed. There are two kinds of needed modules: + +- Static. This is a permanent, immutable list of modules that stays permanently on the system. +- Dynamic, on demand. These modules may be removed as long as they are no longer requested by +any other software component. + +All modules are listed in `/lib/modules//modules.active` file. It has the +following format: + + module_name: + +Type can be either `S` for "static" and an a non-zero integer for "dynamic". The integer +denotes how many package references is in the system. With each package installation +this reference is increased by one, and with each package removal/purge it is decreased +by one. If the reference is thus decreased to zero, module is considered no longer needed +and therefore is removed from this list. + +#### Add a Module + +To add a module to the list, you need to know its _loaded name_ (which is different from +a _file name_), and it is done as follows: + + limopack --use=hci_nokia --install + +This will add `hci_nokia` kernel module as a dynamic module, increasing the reference by one. +Repeating this command will _update_ `hci-nokia` dynamic module state, increasting the +reference by one, thus denoting there are two software components installed, requiring this +module to be present on a system. + + +#### Remove a Module + +To remove a module from the list, you need to use the same _loaded module name_ as follows: + + limopack --use=hci_nokia --remove + +If you added this module twice, first time it will decrease the reference count by one, +and the second time it will remove the module completely from the list. + +#### Add a Static Module + +To add `hci-nokia` kernel module as static, you need to add `--static` flag: + + limopack --use=hci_nokia --install --static + +Or short version: + + limopack --use=hci_nokia -is + +In this case `hci-nokia` module will be permanently added to the system and `limopack` +no longer will be able to remove it. An attempt to its removal will be logged as a warning +that such module is skipped. + +#### Add All Necessary Modules + +To add all vital modules that are currently loaded in the system, simply omit `--use` flag: + + limopack -i + +The `limopack` will extract all current modules, find them on the disk and will register +all of them as static (in this case `--static` makes no influence). + +### Flush Unnecessary Modules + +Once modules are set, one needs to remove unnecessary modules from the system. However +this operation require a package name that needs to be hidden from the system as known, +even though its contents keeps being installed. This is a package, which contains +all the modules of the kernel: + + limopack --pkname=linux-modules-5.19.0-50-generic --apply + +This command will do the following: + +- Remove any mentioning of a package `linux-modules-5.19.0-50-generic` from the system, +so it will look like such package is not even installed. +- Remove all the modules and their dependencies, those are not mentioned in the active list. + +This particular use-case can fit to an embedded image provisioning for "vacuuming" unnecessary +modules, using a package pattern or similar. For instance, installing such package will +install pre-set active static modules and flush all others. Such use-case is often popular +for one-time image provisioning, which is not supposed to be changed afterwards. + +## Usage In Packaging + +In general, the setup supposed to be as follows: + +1. Mainline Linux kernel package, containing no packages +2. A sub-package of that mainline Linux kernel, containing only packages for it + +Without this requirement live dynamic tracking and/or updates are impossible. + +## Step-by-step Packaging + +### Step 1: The Kernel +We would need a kernel package, which contains the kernel itself, its config, all the +`/boot/*` parts etc. However this package should **not** contain any modules. + +### Step 2: The Modules +As a second step, we need a kernel _modules_ package, which could be a sub-package of the +kernel package, except it contains everything else but the kernel itself. This package may +contain all possible kernel modules or only essential ones etc. + +Additionally, there might be more unlimited number of sub-packages, containing 3rd party +modules or any other additional modules, as long as they are installed to the same root +tree, yielding to `/lib/modules//kernel` path. + +### Step 3: Defining Vital Modules + +Now we need to define all vital modules that are still required to be on the system, +no matter what. This should be done in a transient package, which is not really installed +on the system, but only brings some elements and disappears from it. + +At this point of time there is only one way of doing it: to have a live system running +with all modules installed and then determine from _live_ system which modules are actually loaded. +This is also current limitation of the `limopack`, unfortunately. + +These modules as a list can be saved to `/lib/modules/ + +This is a separate step, and it is done per any other possible software component, which is +after extra installation of any additional modules, as well as their purge. + +In general, when you are packaging whatever your software, which requires some other additional +module, you should do the following: + +- Require a package, which contains that module, e.g. `linux-5.19.50-my-modules`. When your +package is installed, and so is that package as well, installed all extra modules somewhere +to `/lib/modules/ +The `limopack` is only a helper utility and currently works only on Debian family distributions. +It is intended to track required kernel modules and therefore help to install or remove them +on demand. This means that the Linux module state on the machine does not depend on the mainline +kernel update mechanisms and to reference a software component is a burden of that software +component. + +Currently there is no way to determine which modules are vital for the system beforehand. +This is only possible to first provision full installation and examine it. -## Limitations -The Limodpak is only a helper, which is used to track used modules and -or install/remove them on demand. This means that the Linux module -state on the machine does not depend on the mainline kernel update -mechanisms. +The current design of `limopack` at least as of today has no tracking of any additional data created +on the disk outside of package manager, thus lacks tracking of those files. diff --git a/doc/limopack.8 b/doc/limopack.8 index ba0a6a2..dd6b31d 100644 --- a/doc/limopack.8 +++ b/doc/limopack.8 @@ -8,21 +8,7 @@ (Helper) .SH SYNOPSIS .PP -Usage of the limopack as follows: -.IP -.nf -\f[C] -USAGE: - limopack [OPTIONS] - -OPTIONS: - -d, --debug Set to debug mode - -e, --tree Display module dependency tree - -h, --help Print help information - -u, --use Specify comma-separated list of kernel modules to be used - -v, --version Get current version -\f[R] -.fi +Usage of the limopack as follows: \f[C]limopack [OPTIONS]\f[R] .SH DESCRIPTION .PP The \f[B]limopack\f[R] is a Linux Kernel modules packaging helper, which @@ -58,30 +44,51 @@ When a new update comes, modules package is then brought back for update. .SS OPTIONS .TP --d, \[en]debug -Set to debug mode +-u, \[en]use +Specify comma-separated list of kernel modules to be processed. +For example +you can specify \f[B]\[en]use=module1,module2,module3\f[R] etc. +.TP +-s, \[en]static +Use specified modules as static (i.e.\ stays permanently) .TP -e, \[en]tree -Display module dependency tree +Display module dependency tree. .TP --h, \[en]help -Print help information +-l, \[en]list +Display in a sorted flat list format all modules that will +be used. +This includes all dependencies and already marked +and existing modules. +.TP +-p, \[en]pkname +Specify a package name, which needs to be un-registered +from the package manager database in order to be visible to the system +as +non-existing, so the system can bring it again for an update or +installation. .TP -i, \[en]install -Add to the list of used modules, those are specified by \f[C]--use\f[R] -option. +Mark specified modules as needed for the system. .TP --s, \[en]shrink -Remove unused kernel modules (vacuum) from the media. +-r, \[en]remove +Remove specified modules as no longer needed for the system, +so they can be purged from the disk. +This operation only marks +the modules to be removed, but does not actually removes them. .TP --u, \[en]use -Specify comma-separated list of kernel module to be used. -Kernel module can have a partial path to it (without -\f[C]/lib/modules//kernel\f[R] as a prefix), and also can have -only names or with \f[C].ko\f[R] extension. +-a, \[en]apply +Apply the changes, vacuuming all unneded/unregisterd (non-marked) +kernel modules, those are still exist on a disk, but always unused. +\f[I]NOTE: this option can be only used alone, as it commits the +changes\f[R] .TP +-d, \[en]debug +Set debug mode for more verbose output. -v, \[en]version -Get current version +Get current version. +-h, \[en]help +Print help .SH FILES .TP \f[I]/usr/bin/limopack\f[R] diff --git a/doc/limopack.8.md b/doc/limopack.8.md index 1e9dd0a..35a28d2 100644 --- a/doc/limopack.8.md +++ b/doc/limopack.8.md @@ -8,17 +8,7 @@ NAME SYNOPSIS ======== -Usage of the limopack as follows: - - USAGE: - limopack [OPTIONS] - - OPTIONS: - -d, --debug Set to debug mode - -e, --tree Display module dependency tree - -h, --help Print help information - -u, --use Specify comma-separated list of kernel modules to be used - -v, --version Get current version +Usage of the limopack as follows: `limopack [OPTIONS]` DESCRIPTION =========== @@ -54,35 +44,56 @@ When a new update comes, modules package is then brought back for update. OPTIONS ------- --d, --debug +-u, --use -: Set to debug mode +: Specify comma-separated list of kernel modules to be processed. For example +: you can specify **--use=module1,module2,module3** etc. + +-s, --static + +: Use specified modules as static (i.e. stays permanently) -e, --tree -: Display module dependency tree +: Display module dependency tree. --h, --help +-l, --list -: Print help information +: Display in a sorted flat list format all modules that will +: be used. This includes all dependencies and already marked +: and existing modules. + +-p, --pkname + +: Specify a package name, which needs to be un-registered +: from the package manager database in order to be visible to the system as +: non-existing, so the system can bring it again for an update or installation. -i, --install -: Add to the list of used modules, those are specified by `--use` option. +: Mark specified modules as needed for the system. --s, --shrink +-r, --remove -: Remove unused kernel modules (vacuum) from the media. +: Remove specified modules as no longer needed for the system, +: so they can be purged from the disk. This operation only marks +: the modules to be removed, but does not actually removes them. --u, --use +-a, --apply -: Specify comma-separated list of kernel module to be used. Kernel - module can have a partial path to it (without `/lib/modules//kernel` - as a prefix), and also can have only names or with `.ko` extension. +: Apply the changes, vacuuming all unneded/unregisterd (non-marked) +: kernel modules, those are still exist on a disk, but always unused. +: *NOTE: this option can be only used alone, as it commits the changes* +-d, --debug + +: Set debug mode for more verbose output. -v, --version -: Get current version +: Get current version. +-h, --help + +: Print help FILES ===== diff --git a/src/actions.rs b/src/actions.rs index da98ab0..cfa57bd 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,8 +1,8 @@ -use std::io::ErrorKind; - -use crate::mdb::modlist; -use crate::mtree::kerman::kman::get_kernel_infos; use crate::mtree::moddeps::ktree::KModuleTree; +use crate::{mdb::modlist, pakmod}; +use crate::{mtree::kerman::kman::get_kernel_infos, pakmod::rmpak::PackMod}; + +use std::io::ErrorKind; /// Show module dependency tree. /// @@ -27,20 +27,24 @@ pub fn do_tree(debug: &bool, modules: &[String]) { /// List dependencies from all specified modules /// in a flat sorted format -pub fn do_list(debug: &bool, modules: &[String]) { +pub fn do_list(debug: &bool, modules: &[String]) -> Vec { + let mut out: Vec = Vec::default(); for ki in get_kernel_infos(debug) { let kmtree: KModuleTree<'_> = KModuleTree::new(&ki); for m in kmtree.merge_specified_deps(modules) { - println!("{m}"); + out.push(m); } } + + out.sort(); + out } /// Add or remove kernel modules fn _add_remove(debug: &bool, add: bool, is_static: bool, modules: &mut Vec) -> Result<(), std::io::Error> { for ki in get_kernel_infos(debug) { let kmtree: KModuleTree<'_> = KModuleTree::new(&ki); - let rml: Result, std::io::Error> = modlist::ModList::new(&ki); + let rml: Result, std::io::Error> = modlist::ModList::new(&ki, debug); if rml.is_err() { return Err(rml.err().unwrap()); @@ -91,12 +95,49 @@ pub fn do_remove(debug: &bool, modules: &[String]) -> Result<(), std::io::Error> /// Commit changes on the disk. This will permanently remove unused kernel modules /// from the disk. pub fn do_commit(debug: &bool) -> Result<(), std::io::Error> { - for ki in get_kernel_infos(debug) {} + for ki in get_kernel_infos(debug) { + match modlist::ModList::new(&ki, debug) { + Ok(ml) => { + let mut diff_mods: Vec = vec![]; + let idx_mods = ki.get_deps_for_flatten(&ml.get_modules()); + let disk_mods = ki.get_disk_modules(); + + for dmod in &disk_mods { + if !idx_mods.contains(dmod) { + diff_mods.push(dmod.to_owned()); + } + } + + log::info!("Modules on disk: {}, indexed: {}, to remove: {}", disk_mods.len(), idx_mods.len(), diff_mods.len()); + match ml.commit(&diff_mods) { + Ok(_) => ml.vacuum_dirs()?, + Err(err) => { + return Err(std::io::Error::new(err.kind(), format!("Unable to commit changes to the disk: {}", err))) + } + } + } + + Err(err) => { + return Err(std::io::Error::new( + err.kind(), + format!("Error while getting module list on kernel \"{}\": {}", ki.version, err), + )); + } + } + } Ok(()) } /// Unregister specified package from the package manager database. /// Yuck!... -pub fn do_unregister_pkg(debug: &bool, pkgname: String) -> Result<(), std::io::Error> { - Ok(()) +pub fn do_unregister_pkg(debug: &bool, pkgname: &String) -> Result<(), std::io::Error> { + if *debug { + log::debug!("Unregistering {} package", pkgname); + } + + let mut pmod = pakmod::dpkgmod::DpkgMod::new(debug); + match pmod.remove_package(pkgname.to_string()) { + Ok(_) => pmod.save(), + Err(err) => Err(err), + } } diff --git a/src/clidef.rs b/src/clidef.rs index 787295a..6212afb 100644 --- a/src/clidef.rs +++ b/src/clidef.rs @@ -12,7 +12,8 @@ pub fn cli(version: &'static str) -> Command { Command::new("limopack") .version(version) - .about("[Li]nux [Mo]dule [Pack]age Helper") + .about(format!("{}{} {}{} {}{}", "Li".bold().underline(), "nux", "Mo".bold().underline(), "dule", "Pack".bold().underline(), + "age Helper - is a packaging utility to manage unused kernel modules on mainline kernels")) // Config .arg( Arg::new("use") @@ -71,7 +72,8 @@ pub fn cli(version: &'static str) -> Command { Arg::new("apply") .short('a') .long("apply") - .conflicts_with_all(["use", "static", "tree", "list", "pkname", "install", "remove"]) + .conflicts_with_all(["use", "static", "tree", "list", "install", "remove"]) + .requires("pkname") .action(ArgAction::SetTrue) .help(format!( "{}{}", diff --git a/src/main.rs b/src/main.rs index 4307d1c..7ebc6a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,11 @@ mod clidef; mod logger; mod mdb; mod mtree; +mod pakmod; +mod sysutils; use clap::Error; -use std::env; +use std::{env, io::ErrorKind, process}; static VERSION: &str = "0.1"; static LOGGER: logger::STDOUTLogger = logger::STDOUTLogger; @@ -18,6 +20,7 @@ fn init(debug: &bool) -> Result<(), log::SetLoggerError> { fn if_err(res: Result<(), std::io::Error>) { if res.is_err() { log::error!("{}", res.err().unwrap().to_string()); + process::exit(exitcode::OSERR); } } @@ -38,6 +41,11 @@ fn main() -> Result<(), Error> { init(&debug).unwrap(); + // Check if user has required access + if params.get_flag("install") || params.get_flag("remove") || params.get_flag("apply") { + if_err(sysutils::user_is_root()); + } + let modlist = params.get_one::("use"); let modules: Vec = if modlist.is_some() { params.get_many::("use").unwrap().collect::>().iter().map(|x| x.to_string()).collect() @@ -54,11 +62,24 @@ fn main() -> Result<(), Error> { } else if params.get_flag("tree") { actions::do_tree(&debug, &modules); } else if params.get_flag("list") { - actions::do_list(&debug, &modules); + for modname in actions::do_list(&debug, &modules) { + println!("{}", modname); + } } else if params.get_flag("install") { if_err(actions::do_add(&debug, is_static, &modules)); } else if params.get_flag("remove") { if_err(actions::do_remove(&debug, &modules)); + } else if params.get_flag("apply") { + match params.get_one::("pkname") { + Some(pkname) => { + if pkname.is_empty() { + if_err(Err(std::io::Error::new(ErrorKind::InvalidInput, "Package name is not specified"))) + } + if_err(actions::do_unregister_pkg(&debug, pkname)); + if_err(actions::do_commit(&debug)) + } + None => todo!(), + } } else { cli.print_help().unwrap(); } diff --git a/src/mdb/modlist.rs b/src/mdb/modlist.rs index 2aefbb7..cb823ad 100644 --- a/src/mdb/modlist.rs +++ b/src/mdb/modlist.rs @@ -1,11 +1,17 @@ use crate::mtree::kerman::kman; use crate::mtree::kerman::kman::KernelInfo; use colored::Colorize; -use exitcode::{self}; -use std::io::{self, Error}; -use std::io::{BufRead, Write}; use std::path::PathBuf; -use std::{collections::HashMap, fs::File, io::ErrorKind, path::Path, process}; +use std::{collections::HashMap, fs::File, io::ErrorKind, path::Path}; +use std::{ + fs, + io::{self}, +}; +use std::{ + io::{BufRead, Write}, + vec, +}; +use walkdir::WalkDir; /// Module tracker /// Used modules are stored a plain-text file in /lib/modules//modules.active @@ -48,12 +54,13 @@ pub struct ModList<'a> { // - any positive value is a counter for the references modlist: HashMap, kinfo: &'a KernelInfo<'a>, + debug: &'a bool, } impl<'a> ModList<'a> { /// Constructor - pub fn new(kinfo: &'a KernelInfo) -> Result { - let mut modlist = ModList { modlist: HashMap::default(), kinfo }; + pub fn new(kinfo: &'a KernelInfo, debug: &'a bool) -> Result { + let mut modlist = ModList { modlist: HashMap::default(), kinfo, debug }; let loaded = modlist.load(); if loaded.is_err() { @@ -151,6 +158,14 @@ impl<'a> ModList<'a> { self.write() } + /// Get indexed modules + pub fn get_modules(&self) -> Vec { + let mut out: Vec = self.modlist.keys().map(|s| s.to_owned()).collect(); + out.sort(); + + out + } + /// Remove a module from the tree. /// /// Note, it does not removes a module from the list iff there are no more counters @@ -190,8 +205,76 @@ impl<'a> ModList<'a> { } /// Apply changes on a disk: remove from the media unused modules - pub fn commit(&self) -> Result<(), std::io::Error> { - log::info!("Applying changes"); - self.write().map_err(|e| Error::new(e.kind(), format!("Error while saving data about used modules: {}", e))) + pub fn commit(&self, modules: &[String]) -> Result<(), std::io::Error> { + log::info!("Applying changes to {} modules", modules.len()); + let mut skipped = 0; + let mut removed = 0; + + for modpath in modules { + let modpath = &self.kinfo.get_kernel_path().join(modpath); + let s_modpath = modpath.to_owned().into_os_string().into_string().unwrap(); + if *self.debug { + log::debug!("Removing kernel module: {}", s_modpath); + } + + if modpath.exists() { + fs::remove_file(modpath)?; + removed += 1; + } else { + if *self.debug { + log::debug!("Skipping kernel module: {}", s_modpath); + } + skipped += 1; + } + } + + log::info!( + "Removed: {}, skipped (do not exist on the media): {}", + removed.to_string().bright_yellow(), + skipped.to_string().bright_yellow() + ); + Ok(()) + } + + /// Removes all empty sub/directories from the kernel's relative directory. + pub fn vacuum_dirs(&self) -> Result<(), std::io::Error> { + log::info!("Vacuuming modules space"); + let mut removed = 0; + let mut paths: Vec<_> = vec![]; + + // Get directories, but do not remove them just yet + for e in WalkDir::new(self.kinfo.get_kernel_path().join("kernel")).into_iter().flatten() { + if e.file_type().is_dir() { + paths.push(e.path().to_owned()); + } + } + + // Erase empty dirs, if any (if dir is not empty it won't be deleted) + // This is a pretty crude way, as it cycles until no more directories deleted + // The fs::remove_dir() will fail to remove a non-empty directory, + // efficiently removing only empty ones. This way several walks will eventually + // remove all subdirs with empty subdirs in them. Maybe in a future + // could be a better algorithm, but this just works fast enough and does the job. :-) + let mut cycle_removed = 0; + loop { + for p in &paths { + if let Ok(()) = fs::remove_dir(p) { + cycle_removed += 1; + removed += 1; + } + } + + if cycle_removed == 0 { + break; + } else { + cycle_removed = 0; + } + } + + if removed > 0 { + log::info!("Removed {} empty directories", removed); + } + + Ok(()) } } diff --git a/src/mtree/kerman.rs b/src/mtree/kerman.rs index 35721a5..33f20c9 100644 --- a/src/mtree/kerman.rs +++ b/src/mtree/kerman.rs @@ -1,10 +1,14 @@ pub mod kman { - use std::collections::{HashMap, HashSet}; use std::fs::{read_dir, read_to_string}; use std::path::{Path, PathBuf}; + use std::{ + collections::{HashMap, HashSet}, + process::Command, + }; pub static MOD_D: &str = "/lib/modules"; pub static MOD_DEP_F: &str = "modules.dep"; + pub static MOD_INFO_EXE: &str = "/usr/sbin/modinfo"; /// Metadata about the kernel and details about it #[derive(Debug, Clone)] @@ -48,6 +52,11 @@ pub mod kman { self } + /// Return current kernel info root path. + pub fn get_kernel_path(&self) -> PathBuf { + PathBuf::from(MOD_D).join(&self.version) + } + /// Load module dependencies /// Skip if there is no /lib/modules/ = vec![]; if !moddeps.is_empty() { - deplist = moddeps.split(' ').into_iter().map(|x| x.to_owned()).collect(); + deplist = moddeps.split(' ').map(|x| x.to_owned()).collect(); if *self.debug { log::debug!("Found {} dependencies for {}", deplist.len(), modpath); } @@ -90,6 +99,9 @@ pub mod kman { /// Find a full path to a module /// Example: "sunrpc.ko" will be resolved as "kernel/net/sunrpc/sunrpc.ko" + /// + /// Some modules are named differently on the disk than in the memory. + /// In this case they are tried to be resolved via external "modinfo". fn expand_module_name<'a>(&'a self, name: &'a String) -> &String { let mut m_name: String; if !name.ends_with(".ko") { @@ -105,12 +117,35 @@ pub mod kman { } for fmodname in self.deplist.keys() { - if fmodname.ends_with(&m_name) { + // Eliminate to a minimum 3rd fallback via modinfo by trying replacing underscore with a minus. + // This not always works, because some modules called mixed. + let mm_name = m_name.replace('_', "-"); + if fmodname.ends_with(&m_name) || fmodname.ends_with(&mm_name) { return fmodname; } } } + let out = Command::new(MOD_INFO_EXE).arg(name).output(); + match out { + Ok(_) => match String::from_utf8(out.unwrap().stdout) { + Ok(data) => { + for line in data.lines().map(|el| el.replace(' ', "")) { + if line.starts_with("filename:/") && line.contains("/kernel/") { + let t_modname = format!("kernel/{}", line.split("/kernel/").collect::>()[1]); + for fmodname in self.deplist.keys() { + if *fmodname == t_modname { + return fmodname; + } + } + } + } + } + Err(err) => log::error!("Unable to get info about module \"{name}\": {}", err), + }, + Err(_) => log::error!("Module {name} not found on the disk"), + } + name } @@ -138,6 +173,7 @@ pub mod kman { for kmodname in names { let r_kmodname = self.expand_module_name(kmodname); if !r_kmodname.contains('/') { + log::warn!("Module not found on a disk: {}", r_kmodname); continue; } @@ -154,6 +190,33 @@ pub mod kman { mod_tree } + + /// Same as `get_deps_for`, except returns flattened list + /// for all modules with their dependencies. + pub fn get_deps_for_flatten(&self, names: &[String]) -> Vec { + let mut buff: HashSet = HashSet::default(); + for (mname, mdeps) in &self.get_deps_for(names) { + buff.insert(mname.to_owned()); + buff.extend(mdeps.to_owned()); + } + + buff.iter().map(|x| x.to_owned()).collect() + } + + /// Get all found modules + pub fn get_disk_modules(&self) -> Vec { + let mut buff: HashSet = HashSet::default(); + + for (modname, moddeps) in &self.deplist { + buff.insert(modname.to_owned()); + buff.extend(moddeps.to_owned()); + } + + let mut mods: Vec = buff.iter().map(|x| x.to_string()).collect(); + mods.sort(); + + mods + } } /// Get the list of existing kernels in the system. diff --git a/src/mtree/moddeps.rs b/src/mtree/moddeps.rs index 4770682..dd62a12 100644 --- a/src/mtree/moddeps.rs +++ b/src/mtree/moddeps.rs @@ -1,7 +1,7 @@ pub mod ktree { use crate::mdb::modules::modinfo; use crate::mtree::kerman::kman::KernelInfo; - use std::collections::HashMap; + use std::collections::{HashMap, HashSet}; pub struct KModuleTree<'kinfo> { kernel: &'kinfo KernelInfo<'kinfo>, @@ -36,20 +36,19 @@ pub mod ktree { /// Same as a snapshot `get_loaded()` except it is merges /// all the dependencies into one list for an actual operations. #[allow(dead_code)] - pub fn merge_loaded_deps(&self) -> Vec { + pub fn merge_loaded_deps(&self) -> HashSet { self.merge_specified_deps(&self.get_loaded_modules()) } /// Same as `get_specified` method, except it merges /// all the dependencies into one list for an actual operations. - pub fn merge_specified_deps(&self, modules: &[String]) -> Vec { - let mut deps: Vec = vec![]; + pub fn merge_specified_deps(&self, modules: &[String]) -> HashSet { + let mut deps = HashSet::default(); for (module, data) in self.get_specified_deps(modules) { deps.extend(data); - deps.push(module); + deps.insert(module); } - deps.sort(); deps } } diff --git a/src/pakmod/dpkgmod.rs b/src/pakmod/dpkgmod.rs new file mode 100644 index 0000000..1ca01ec --- /dev/null +++ b/src/pakmod/dpkgmod.rs @@ -0,0 +1,120 @@ +use std::{ + fs::{self, OpenOptions}, + io::{Error, ErrorKind, Write}, +}; + +use colored::Colorize; + +/// This module is designed to remove a package from /var/lib/dpkg/status from being +/// mentioned there. Such operation is required to install a package and physically +/// keep its data on a media, modify its content to further reuse and allow it to be +/// updated by a standard package manager means. +/// +use super::rmpak::PackMod; + +#[derive(Clone)] +pub struct DpkgMod<'a> { + packages: Vec, + status_path: String, + debug: &'a bool, +} + +impl<'a> DpkgMod<'a> { + pub fn new(debug: &'a bool) -> Self { + DpkgMod { packages: vec![], status_path: "/var/lib/dpkg/status".to_string(), debug }.load() + } + + /// Remove field from a string. + /// Fields are first keywords, following ":" colon. + fn chop_field(&self, line: String) -> String { + match line.split_once(':') { + Some(data) => data.1.trim().to_string(), + None => String::from(""), + } + } + + /// Load package status + fn load(&mut self) -> Self { + if let Ok(data) = fs::read_to_string(&self.status_path) { + let _ = &self.packages.extend(data.split("\n\n").map(|el| el.to_string()).collect::>()); + } + + self.to_owned() + } + + /// Returns true if a current chunk corresponds to a given package name + fn is_package(&self, name: String, data: String) -> bool { + let dls: Vec = data.split('\n').map(|x| x.to_string()).collect(); + name == self.chop_field(dls[0].to_owned()) + } +} + +impl PackMod for DpkgMod<'_> { + /// Remove package from the index. This still keeps only the state of the modpack, + /// but does not writes anything to the disk. + fn remove_package(&mut self, pn: String) -> Result<(), Error> { + let mut buff: Vec = vec![]; + log::info!("Looking for \"{}\" package...", pn.bright_yellow()); + let mut found = false; + for p in &self.packages { + if !self.is_package(pn.to_owned(), p.to_owned()) { + buff.push(p.to_owned()); + } else { + log::info!("Altering package manager database for \"{}\"", pn.bright_yellow()); + found = true; + } + } + + self.packages = Vec::new(); + self.packages.extend(buff); + + if !found { + return Err(Error::new( + ErrorKind::NotFound, + format!("Package \"{}\" was not found in the database", pn.bright_yellow()), + )); + } + + Ok(()) + } + + /// Save the current state to the disk. + fn save(&self) -> Result<(), Error> { + log::info!("Save changes to the dpkg database"); + if *self.debug { + log::debug!("Backing up \"{}\" before modification", self.status_path.to_owned().bright_yellow()); + } + + let status_backup_path = format!("{}.limopack.bkp", &self.status_path); + fs::copy(&self.status_path, &status_backup_path)?; + + if let Ok(mut fptr) = OpenOptions::new().create(true).write(true).truncate(true).open(&self.status_path) { + let p_idx = self.packages.len() - 1; + for (idx, pinfo) in self.packages.iter().enumerate() { + match fptr.write_all(format!("{}{}", pinfo, if idx < p_idx { "\n\n" } else { "" }).as_bytes()) { + Ok(_) => {} + Err(err) => { + log::error!( + "Unable to write \"{}\": \"{}\"", + &self.status_path.bright_yellow(), + err.to_string().bright_red() + ); + // Badaboom. We probably screwed it all up and now need to restore the backup. Hopefully. + if *self.debug { + log::debug!("Restoring \"{}\"", self.status_path.to_owned().bright_yellow()); + } + fs::copy(&status_backup_path, &self.status_path)?; + + if *self.debug { + log::debug!("Removing backup at \"{}\"", &status_backup_path.to_owned().bright_yellow()); + } + + fs::remove_file(&status_backup_path)?; + } + } + } + } + + Ok(()) + } +} diff --git a/src/pakmod/mod.rs b/src/pakmod/mod.rs new file mode 100644 index 0000000..77fa828 --- /dev/null +++ b/src/pakmod/mod.rs @@ -0,0 +1,2 @@ +pub mod dpkgmod; +pub mod rmpak; diff --git a/src/pakmod/rmpak.rs b/src/pakmod/rmpak.rs new file mode 100644 index 0000000..c0fe4df --- /dev/null +++ b/src/pakmod/rmpak.rs @@ -0,0 +1,6 @@ +use std::io::Error; + +pub trait PackMod { + fn remove_package(&mut self, name: String) -> Result<(), Error>; + fn save(&self) -> Result<(), Error>; +} diff --git a/src/sysutils.rs b/src/sysutils.rs new file mode 100644 index 0000000..76bc70d --- /dev/null +++ b/src/sysutils.rs @@ -0,0 +1,25 @@ +use std::io::{Error, ErrorKind}; + +#[link(name = "c")] +extern "C" { + fn geteuid() -> u32; + fn getegid() -> u32; +} + +/// Returns true if the specified UID matches +fn is_uid(uid: i8) -> bool { + unsafe { geteuid() == uid.try_into().unwrap() } +} + +// Returns true if the specified GID matches +fn is_gid(gid: i8) -> bool { + unsafe { getegid() == gid.try_into().unwrap() } +} + +/// Returns no error if user is root +pub fn user_is_root() -> Result<(), Error> { + if !is_uid(0) || !is_gid(0) { + return Err(Error::new(ErrorKind::PermissionDenied, "User requires root privileges")); + } + Ok(()) +}