Implement transactional operations
This commit is contained in:
parent
866b5b5164
commit
f4f135da71
5 changed files with 190 additions and 33 deletions
|
|
@ -7,7 +7,7 @@ use crate::{
|
||||||
Kind, OperatingSystem,
|
Kind, OperatingSystem,
|
||||||
pkg::{self, Package, PackagerVariant, Packages},
|
pkg::{self, Package, PackagerVariant, Packages},
|
||||||
},
|
},
|
||||||
run::{Command, executor::read},
|
run::{Command, Transaction, TransactionCommand, executor::read},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const DEBIAN: OperatingSystem = OperatingSystem {
|
pub const DEBIAN: OperatingSystem = OperatingSystem {
|
||||||
|
|
@ -25,12 +25,20 @@ pub struct Apt {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Packages for Apt {
|
impl Packages for Apt {
|
||||||
fn install(&self, packages: &[Package], config: &Configuration) -> Result<(), pkg::Error> {
|
fn install(
|
||||||
super::debian::Apt::haul("install", packages, config)
|
&self,
|
||||||
|
packages: &[Package],
|
||||||
|
config: &Configuration,
|
||||||
|
) -> Result<Transaction, pkg::Error> {
|
||||||
|
super::debian::Apt::haul(&Operation::Install, packages, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uninstall(&self, packages: &[Package], config: &Configuration) -> Result<(), pkg::Error> {
|
fn uninstall(
|
||||||
super::debian::Apt::haul("remove", packages, config)
|
&self,
|
||||||
|
packages: &[Package],
|
||||||
|
config: &Configuration,
|
||||||
|
) -> Result<Transaction, pkg::Error> {
|
||||||
|
super::debian::Apt::haul(&Operation::Uninstall, packages, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manual(&self) -> Result<Vec<Package>, pkg::Error> {
|
fn manual(&self) -> Result<Vec<Package>, pkg::Error> {
|
||||||
|
|
@ -145,20 +153,64 @@ impl Packages for Apt {
|
||||||
|
|
||||||
impl Apt {
|
impl Apt {
|
||||||
fn haul(
|
fn haul(
|
||||||
subcommand: &str,
|
operation: &Operation,
|
||||||
packages: &[Package],
|
packages: &[Package],
|
||||||
config: &Configuration,
|
config: &Configuration,
|
||||||
) -> Result<(), pkg::Error> {
|
) -> Result<Transaction, pkg::Error> {
|
||||||
if packages.is_empty() {
|
if packages.is_empty() {
|
||||||
println!("Package selection is empty: Nothing to {subcommand}");
|
println!("Package selection is empty: Nothing to {operation}");
|
||||||
return Ok(());
|
return Ok(Transaction::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let args: Vec<&str> = iter::once(subcommand)
|
// TODO This works as it is stated and is interesting as part of the
|
||||||
|
// PoC, but doesn't really make sense to install something that wasn't
|
||||||
|
// installed in the first place as the "rollback" of a failed uninstall
|
||||||
|
let rollback_operation = match operation {
|
||||||
|
Operation::Install => Operation::Uninstall,
|
||||||
|
Operation::Uninstall => Operation::Install,
|
||||||
|
};
|
||||||
|
|
||||||
|
let run_args: Vec<&str> = iter::once(operation.into())
|
||||||
.chain(packages.iter().map(|p| p.into()))
|
.chain(packages.iter().map(|p| p.into()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let command = Command::new("apt", &args).escalate(config)?;
|
let rollback_args: Vec<&str> = iter::once(rollback_operation.into())
|
||||||
Ok(crate::run::executor::spawn(&command)?)
|
.chain(packages.iter().map(|p| p.into()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let run = Command::new("apt", &run_args).escalate(config)?;
|
||||||
|
let rollback = Command::new("apt", &rollback_args).escalate(config)?;
|
||||||
|
let transaction_command = TransactionCommand::new(run, rollback);
|
||||||
|
Ok(Transaction::single(&transaction_command))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Operation {
|
||||||
|
Install,
|
||||||
|
Uninstall,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> From<Operation> for &'s str {
|
||||||
|
fn from(operation: Operation) -> &'s str {
|
||||||
|
match operation {
|
||||||
|
Operation::Install => "install",
|
||||||
|
Operation::Uninstall => "remove",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> From<&'s Operation> for &'s str {
|
||||||
|
fn from(operation: &Operation) -> &str {
|
||||||
|
match *operation {
|
||||||
|
Operation::Install => "install",
|
||||||
|
Operation::Uninstall => "remove",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Operation {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
let s: &str = self.into();
|
||||||
|
write!(f, "{s}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,16 @@ use core::{
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{conf::Configuration, os::debian, run};
|
use crate::{
|
||||||
|
conf::Configuration,
|
||||||
|
os::debian,
|
||||||
|
run::{self, Transaction},
|
||||||
|
};
|
||||||
|
|
||||||
pub trait Packages: Clone + Default + Debug + PartialEq + Eq {
|
pub trait Packages: Clone + Default + Debug + PartialEq + Eq {
|
||||||
fn install(&self, packages: &[Package], config: &Configuration) -> Result<(), Error>;
|
fn install(&self, packages: &[Package], config: &Configuration) -> Result<Transaction, Error>;
|
||||||
fn uninstall(&self, packages: &[Package], config: &Configuration) -> Result<(), Error>;
|
fn uninstall(&self, packages: &[Package], config: &Configuration)
|
||||||
|
-> Result<Transaction, Error>;
|
||||||
fn manual(&self) -> Result<Vec<Package>, Error>;
|
fn manual(&self) -> Result<Vec<Package>, Error>;
|
||||||
fn automatic(&self) -> Result<Vec<Package>, Error>;
|
fn automatic(&self) -> Result<Vec<Package>, Error>;
|
||||||
fn variant(&self) -> &PackagerVariant;
|
fn variant(&self) -> &PackagerVariant;
|
||||||
|
|
@ -23,14 +28,18 @@ pub enum Packager {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Packages for Packager {
|
impl Packages for Packager {
|
||||||
fn install(&self, packages: &[Package], config: &Configuration) -> Result<(), Error> {
|
fn install(&self, packages: &[Package], config: &Configuration) -> Result<Transaction, Error> {
|
||||||
match self {
|
match self {
|
||||||
Packager::Apt(p) => p.install(packages, config),
|
Packager::Apt(p) => p.install(packages, config),
|
||||||
Packager::Unknown => Error::unknown_packager(&format!("install {packages:?}")),
|
Packager::Unknown => Error::unknown_packager(&format!("install {packages:?}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uninstall(&self, packages: &[Package], config: &Configuration) -> Result<(), Error> {
|
fn uninstall(
|
||||||
|
&self,
|
||||||
|
packages: &[Package],
|
||||||
|
config: &Configuration,
|
||||||
|
) -> Result<Transaction, Error> {
|
||||||
match self {
|
match self {
|
||||||
Packager::Apt(p) => p.uninstall(packages, config),
|
Packager::Apt(p) => p.uninstall(packages, config),
|
||||||
Packager::Unknown => Error::unknown_packager(&format!("uninstall {packages:?}")),
|
Packager::Unknown => Error::unknown_packager(&format!("uninstall {packages:?}")),
|
||||||
|
|
|
||||||
58
src/run.rs
58
src/run.rs
|
|
@ -74,6 +74,7 @@ pub enum TaskKind {
|
||||||
pub struct Command {
|
pub struct Command {
|
||||||
pub base: String,
|
pub base: String,
|
||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
|
escalated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
|
|
@ -104,6 +105,7 @@ impl Command {
|
||||||
|
|
||||||
Ok(Command {
|
Ok(Command {
|
||||||
base: config.su_command.command().clone().base,
|
base: config.su_command.command().clone().base,
|
||||||
|
escalated: true,
|
||||||
args,
|
args,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -112,17 +114,69 @@ impl Command {
|
||||||
Command {
|
Command {
|
||||||
base: base.to_string(),
|
base: base.to_string(),
|
||||||
args: args.iter().map(|e| e.to_string()).collect(),
|
args: args.iter().map(|e| e.to_string()).collect(),
|
||||||
|
escalated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn escalated(&self) -> bool {
|
||||||
|
self.escalated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Transaction {
|
||||||
|
commands: Vec<TransactionCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transaction {
|
||||||
|
pub fn single(command: &TransactionCommand) -> Transaction {
|
||||||
|
Transaction {
|
||||||
|
commands: vec![command.clone()],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
||||||
|
pub struct TransactionCommand {
|
||||||
|
run: Command,
|
||||||
|
rollback: Command,
|
||||||
|
status: TransactionCommandStatus,
|
||||||
|
errors: Option<Vec<executor::Error>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransactionCommand {
|
||||||
|
pub const fn new(run: Command, rollback: Command) -> TransactionCommand {
|
||||||
|
TransactionCommand {
|
||||||
|
run,
|
||||||
|
rollback,
|
||||||
|
status: TransactionCommandStatus::Pending,
|
||||||
|
errors: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_error(&mut self, error: &executor::Error) {
|
||||||
|
self.errors.get_or_insert_with(Vec::new).push(error.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
||||||
|
pub enum TransactionCommandStatus {
|
||||||
|
#[default]
|
||||||
|
Pending,
|
||||||
|
Success,
|
||||||
|
PendingRollback,
|
||||||
|
Rolledback,
|
||||||
|
FailedRollback,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub kind: ErrorKind,
|
pub kind: ErrorKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ErrorKind {
|
pub enum ErrorKind {
|
||||||
BadSuCommandConfig,
|
BadSuCommandConfig,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,41 @@
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use crate::{os::pkg::Package, run::Command};
|
use crate::{
|
||||||
|
log::elog,
|
||||||
|
os::pkg::Package,
|
||||||
|
run::{Command, Transaction, TransactionCommandStatus},
|
||||||
|
};
|
||||||
|
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
|
|
||||||
pub fn print(message: &str) -> Result<(), Error> {
|
// TODO Should this be a method of Transaction instead?
|
||||||
println!("{message}");
|
pub fn commit(transaction: &mut Transaction) -> Result<(), Error> {
|
||||||
Ok(())
|
elog(&format!("Committing transaction: {transaction:#?}"));
|
||||||
}
|
for command in &mut transaction.commands {
|
||||||
|
if let Err(error) = spawn(&command.run) {
|
||||||
pub fn print_packages(packages: Vec<Package>) -> Result<(), Error> {
|
command.status = TransactionCommandStatus::PendingRollback;
|
||||||
for package in packages {
|
command.push_error(&error);
|
||||||
print(&format!("{package}"))?;
|
if let Err(rollback_error) = spawn(&command.rollback) {
|
||||||
|
command.status = TransactionCommandStatus::FailedRollback;
|
||||||
|
command.push_error(&rollback_error);
|
||||||
|
elog(&format!("Failed rollback of command {:#?}", &command));
|
||||||
|
return Err(rollback_error);
|
||||||
|
} else {
|
||||||
|
command.status = TransactionCommandStatus::Rolledback;
|
||||||
|
elog(&format!("Successfully rolled back command {:#?}", &command));
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
command.status = TransactionCommandStatus::Success;
|
||||||
|
elog(&format!("Successfully ran command {:#?}", &command));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elog("Transaction committed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn(command: &Command) -> Result<(), Error> {
|
pub(super) fn spawn(command: &Command) -> Result<(), Error> {
|
||||||
if let Ok(mut child) = process::Command::new(&command.base)
|
if let Ok(mut child) = process::Command::new(&command.base)
|
||||||
.args(&command.args)
|
.args(&command.args)
|
||||||
.spawn()
|
.spawn()
|
||||||
|
|
@ -44,6 +63,13 @@ pub fn spawn(command: &Command) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(command: &Command) -> Result<String, Error> {
|
pub fn read(command: &Command) -> Result<String, Error> {
|
||||||
|
if command.escalated() {
|
||||||
|
return Err(Error {
|
||||||
|
message: "Read function is strictly rootless".to_string(),
|
||||||
|
kind: ErrorKind::RootlessReadOnly,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(output) = process::Command::new(&command.base)
|
if let Ok(output) = process::Command::new(&command.base)
|
||||||
.args(&command.args)
|
.args(&command.args)
|
||||||
.output()
|
.output()
|
||||||
|
|
@ -68,15 +94,29 @@ pub fn read(command: &Command) -> Result<String, Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub fn print(message: &str) -> Result<(), Error> {
|
||||||
|
println!("{message}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn print_packages(packages: Vec<Package>) -> Result<(), Error> {
|
||||||
|
for package in packages {
|
||||||
|
print(&format!("{package}"))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub kind: ErrorKind,
|
pub kind: ErrorKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
||||||
pub enum ErrorKind {
|
pub enum ErrorKind {
|
||||||
CommandNotFound,
|
CommandNotFound,
|
||||||
|
RootlessReadOnly,
|
||||||
|
#[default]
|
||||||
Unknown,
|
Unknown,
|
||||||
FailedSpawn,
|
FailedSpawn,
|
||||||
ChildExit,
|
ChildExit,
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,19 @@ pub fn fulfill(state: &State) -> Result<(), Error> {
|
||||||
TaskKind::Help => executor::print("<long help>")?,
|
TaskKind::Help => executor::print("<long help>")?,
|
||||||
TaskKind::PackageInstall => {
|
TaskKind::PackageInstall => {
|
||||||
let packages: Vec<Package> = task.parameters.iter().map(|s| s.into()).collect();
|
let packages: Vec<Package> = task.parameters.iter().map(|s| s.into()).collect();
|
||||||
state
|
let mut transaction = state
|
||||||
.os()
|
.os()
|
||||||
.packager()
|
.packager()
|
||||||
.install(&packages, state.configuration())?;
|
.install(&packages, state.configuration())?;
|
||||||
|
executor::commit(&mut transaction)?;
|
||||||
}
|
}
|
||||||
TaskKind::PackageUninstall => {
|
TaskKind::PackageUninstall => {
|
||||||
let packages: Vec<Package> = task.parameters.iter().map(|s| s.into()).collect();
|
let packages: Vec<Package> = task.parameters.iter().map(|s| s.into()).collect();
|
||||||
state
|
let mut transaction = state
|
||||||
.os()
|
.os()
|
||||||
.packager()
|
.packager()
|
||||||
.uninstall(&packages, state.configuration())?;
|
.uninstall(&packages, state.configuration())?;
|
||||||
|
executor::commit(&mut transaction)?;
|
||||||
}
|
}
|
||||||
TaskKind::PackageListAuto => {
|
TaskKind::PackageListAuto => {
|
||||||
match state.os().packager().automatic() {
|
match state.os().packager().automatic() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue