diff --git a/src/api2/types.rs b/src/api2/types.rs index a9c92de8..d26195c1 100644 --- a/src/api2/types.rs +++ b/src/api2/types.rs @@ -27,6 +27,7 @@ macro_rules! DNS_NAME { () => (concat!(r"(?:", DNS_LABEL!() , r"\.)*", DNS_LABEL macro_rules! USER_NAME_REGEX_STR { () => (r"(?:[^\s:/[:cntrl:]]+)") } macro_rules! GROUP_NAME_REGEX_STR { () => (USER_NAME_REGEX_STR!()) } +#[macro_export] macro_rules! PROXMOX_SAFE_ID_REGEX_STR { () => (r"(?:[A-Za-z0-9_][A-Za-z0-9._\-]*)") } macro_rules! CIDR_V4_REGEX_STR { () => (concat!(r"(?:", IPV4RE!(), r"/\d{1,2})$")) } diff --git a/src/config.rs b/src/config.rs index a9dc1afd..d852e0ca 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,6 +21,7 @@ pub mod user; pub mod acl; pub mod cached_user_info; pub mod network; +pub mod jobs; /// Check configuration directory permissions /// diff --git a/src/config/jobs.rs b/src/config/jobs.rs new file mode 100644 index 00000000..7011d354 --- /dev/null +++ b/src/config/jobs.rs @@ -0,0 +1,173 @@ +use anyhow::{bail, Error}; +use regex::Regex; +use lazy_static::lazy_static; + +use proxmox::api::section_config::SectionConfigData; +use proxmox::tools::{fs::replace_file, fs::CreateOptions}; + +use crate::PROXMOX_SAFE_ID_REGEX_STR; +use crate::tools::systemd::parser::*; +use crate::tools::systemd::types::*; + +const SYSTEMD_CONFIG_DIR: &str = "/etc/systemd/system"; + +#[derive(Debug)] +pub enum JobType { + GarbageCollection, + Prune, +} + +#[derive(Debug)] +pub struct CalenderTimeSpec { + pub hour: u8, // 0-23 +} + +#[derive(Debug)] +pub struct JobListEntry { + job_type: JobType, + id: String, +} + +pub fn list_jobs() -> Result, Error> { + + lazy_static!{ + static ref PBS_JOB_REGEX: Regex = Regex::new( + concat!(r"^pbs-(gc|prune)-(", PROXMOX_SAFE_ID_REGEX_STR!(), ").timer$") + ).unwrap(); + } + + let mut list = Vec::new(); + + for entry in crate::tools::fs::read_subdir(libc::AT_FDCWD, SYSTEMD_CONFIG_DIR)? { + let entry = entry?; + let file_type = match entry.file_type() { + Some(file_type) => file_type, + None => bail!("unable to detect file type"), + }; + if file_type != nix::dir::Type::File { continue; }; + + let file_name = entry.file_name().to_bytes(); + if file_name == b"." || file_name == b".." { continue; }; + + let name = match std::str::from_utf8(file_name) { + Ok(name) => name, + Err(_) => continue, + }; + let caps = match PBS_JOB_REGEX.captures(name) { + Some(caps) => caps, + None => continue, + }; + + // fixme: read config data ? + //let config = parse_systemd_config(&format!("{}/{}", SYSTEMD_CONFIG_DIR, name))?; + + match (&caps[1], &caps[2]) { + ("gc", store) => { + list.push(JobListEntry { + job_type: JobType::GarbageCollection, + id: store.to_string(), + }); + } + ("prune", store) => { + list.push(JobListEntry { + job_type: JobType::Prune, + id: store.to_string(), + }); + } + _ => unreachable!(), + } + } + + Ok(list) +} + +pub fn new_systemd_service_config( + unit: &SystemdUnitSection, + service: &SystemdServiceSection, +) -> Result { + + let mut config = SectionConfigData::new(); + config.set_data("Unit", "Unit", unit)?; + config.record_order("Unit"); + config.set_data("Service", "Service", service)?; + config.record_order("Service"); + + Ok(config) +} + +pub fn new_systemd_timer_config( + unit: &SystemdUnitSection, + timer: &SystemdTimerSection, + install: &SystemdInstallSection, +) -> Result { + + let mut config = SectionConfigData::new(); + config.set_data("Unit", "Unit", unit)?; + config.record_order("Unit"); + config.set_data("Timer", "Timer", timer)?; + config.record_order("Timer"); + config.set_data("Install", "Install", install)?; + config.record_order("Install"); + + Ok(config) +} + +pub fn create_garbage_collection_job( + schedule: CalenderTimeSpec, + store: &str, +) -> Result<(), Error> { + + if schedule.hour > 23 { + bail!("inavlid time spec: hour > 23"); + } + + let description = format!("Proxmox Backup Server Garbage Collection Job '{}'", store); + + let unit = SystemdUnitSection { + Description: description.clone(), + ConditionACPower: Some(true), + ..Default::default() + }; + + let cmd = format!("/usr/sbin/proxmox-backup-manager garbage-collection start {} --output-format json", store); + let service = SystemdServiceSection { + Type: Some(ServiceStartup::Oneshot), + ExecStart: Some(vec![cmd]), + ..Default::default() + }; + + let service_config = new_systemd_service_config(&unit, &service)?; + + let timer = SystemdTimerSection { + OnCalendar: Some(vec![format!("{}:00", schedule.hour)]), + ..Default::default() + }; + + let install = SystemdInstallSection { + WantedBy: Some(vec![String::from("timers.target")]), + ..Default::default() + }; + + let timer_config = new_systemd_timer_config(&unit, &timer, &install)?; + + let basename = format!("{}/pbs-gc-{}", SYSTEMD_CONFIG_DIR, store); + let timer_fn = format!("{}.timer", basename); + let timer_config = CONFIG.write(&timer_fn, &timer_config)?; + + let service_fn = format!("{}.service", basename); + let service_config = CONFIG.write(&service_fn, &service_config)?; + + let backup_user = crate::backup::backup_user()?; + let mode = nix::sys::stat::Mode::from_bits_truncate(0o0750); + // set the correct owner/group/permissions while saving file + // owner(rw) = root, group(r)= backup + let options = CreateOptions::new() + .perm(mode) + .owner(nix::unistd::ROOT) + .group(backup_user.gid); + + replace_file(service_fn, service_config.as_bytes(), options.clone())?; + replace_file(timer_fn, timer_config.as_bytes(), options)?; + + Ok(()) +} diff --git a/src/tools.rs b/src/tools.rs index 17f867bf..bbee59bd 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -31,6 +31,7 @@ pub mod lru_cache; pub mod runtime; pub mod ticket; pub mod timer; +pub mod systemd; mod wrapped_reader_stream; pub use wrapped_reader_stream::*; diff --git a/src/tools/systemd.rs b/src/tools/systemd.rs new file mode 100644 index 00000000..557bac75 --- /dev/null +++ b/src/tools/systemd.rs @@ -0,0 +1,2 @@ +pub mod types; +pub mod parser; diff --git a/src/tools/systemd/parser.rs b/src/tools/systemd/parser.rs new file mode 100644 index 00000000..f18d63ef --- /dev/null +++ b/src/tools/systemd/parser.rs @@ -0,0 +1,81 @@ +use anyhow::Error; +use lazy_static::lazy_static; + +use super::types::*; + +use proxmox::api::{ + schema::*, + section_config::{ + SectionConfig, + SectionConfigData, + SectionConfigPlugin, + } +}; + +use proxmox::tools::{fs::replace_file, fs::CreateOptions}; + + +lazy_static! { + pub static ref CONFIG: SectionConfig = init(); +} + +fn init() -> SectionConfig { + + let mut config = SectionConfig::with_systemd_syntax(&SYSTEMD_SECTION_NAME_SCHEMA); + + match SystemdUnitSection::API_SCHEMA { + Schema::Object(ref obj_schema) => { + let plugin = SectionConfigPlugin::new("Unit".to_string(), obj_schema); + config.register_plugin(plugin); + } + _ => unreachable!(), + }; + match SystemdInstallSection::API_SCHEMA { + Schema::Object(ref obj_schema) => { + let plugin = SectionConfigPlugin::new("Install".to_string(), obj_schema); + config.register_plugin(plugin); + } + _ => unreachable!(), + }; + match SystemdServiceSection::API_SCHEMA { + Schema::Object(ref obj_schema) => { + let plugin = SectionConfigPlugin::new("Service".to_string(), obj_schema); + config.register_plugin(plugin); + } + _ => unreachable!(), + }; + match SystemdTimerSection::API_SCHEMA { + Schema::Object(ref obj_schema) => { + let plugin = SectionConfigPlugin::new("Timer".to_string(), obj_schema); + config.register_plugin(plugin); + } + _ => unreachable!(), + }; + + config +} + +pub fn parse_systemd_config(filename: &str) -> Result { + + let raw = proxmox::tools::fs::file_get_contents(filename)?; + let input = String::from_utf8(raw)?; + + let data = CONFIG.parse(filename, &input)?; + + Ok(data) +} + + +pub fn save_systemd_config(filename: &str, config: &SectionConfigData) -> Result<(), Error> { + let raw = CONFIG.write(filename, &config)?; + + let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644); + // set the correct owner/group/permissions while saving file, owner(rw) = root + let options = CreateOptions::new() + .perm(mode) + .owner(nix::unistd::ROOT); + + replace_file(filename, raw.as_bytes(), options)?; + + Ok(()) +} diff --git a/src/tools/systemd/types.rs b/src/tools/systemd/types.rs new file mode 100644 index 00000000..c0d4b139 --- /dev/null +++ b/src/tools/systemd/types.rs @@ -0,0 +1,140 @@ +use serde::{Serialize, Deserialize}; + +use proxmox::api::{ api, schema::* }; +use crate::api2::types::SINGLE_LINE_COMMENT_FORMAT; + +pub const SYSTEMD_SECTION_NAME_SCHEMA: Schema = StringSchema::new( + "Section name") + .format(&ApiStringFormat::Enum(&[ + EnumEntry::new("Unit", "Unit"), + EnumEntry::new("Timer", "Timer"), + EnumEntry::new("Install", "Install"), + EnumEntry::new("Service", "Service")])) + .schema(); + +pub const SYSTEMD_STRING_SCHEMA: Schema = + StringSchema::new("Systemd configuration value.") + .format(&SINGLE_LINE_COMMENT_FORMAT) + .schema(); + +pub const SYSTEMD_STRING_ARRAY_SCHEMA: Schema = ArraySchema::new( + "Array of Strings", &SYSTEMD_STRING_SCHEMA) + .schema(); + +#[api( + properties: { + "OnCalendar": { + type: Array, + optional: true, + items: { + description: "Calendar event expression.", + type: String, + }, + }, + }, +)] +#[derive(Serialize, Deserialize, Default)] +#[allow(non_snake_case)] +/// Systemd Timer Section +pub struct SystemdTimerSection { + /// Calender event list. + #[serde(skip_serializing_if="Option::is_none")] + pub OnCalendar: Option>, + /// If true, the time when the service unit was last triggered is stored on disk. + #[serde(skip_serializing_if="Option::is_none")] + pub Persistent: Option, +} + +#[api( + properties: { + "Type": { + type: ServiceStartup, + optional: true, + }, + "ExecStart": { + schema: SYSTEMD_STRING_ARRAY_SCHEMA, + optional: true, + }, + } +)] +#[derive(Serialize, Deserialize, Default)] +#[allow(non_snake_case)] +/// Systemd Service Section +pub struct SystemdServiceSection { + /// The process start-up type for this service unit. + #[serde(skip_serializing_if="Option::is_none")] + pub Type: Option, + #[serde(skip_serializing_if="Option::is_none")] + pub ExecStart: Option>, +} + +#[api()] +#[derive(Serialize, Deserialize, Default)] +#[allow(non_snake_case)] +/// Systemd Unit Section +pub struct SystemdUnitSection { + /// A human readable name for the unit. + pub Description: String, + /// Check whether the system has AC power. + #[serde(skip_serializing_if="Option::is_none")] + pub ConditionACPower: Option, +} + +#[api( + properties: { + "Alias": { + schema: SYSTEMD_STRING_ARRAY_SCHEMA, + optional: true, + }, + "Also": { + schema: SYSTEMD_STRING_ARRAY_SCHEMA, + optional: true, + }, + "DefaultInstance": { + optional: true, + }, + "WantedBy": { + schema: SYSTEMD_STRING_ARRAY_SCHEMA, + optional: true, + }, + "RequiredBy": { + schema: SYSTEMD_STRING_ARRAY_SCHEMA, + optional: true, + }, + }, +)] +#[derive(Serialize, Deserialize, Default)] +#[allow(non_snake_case)] +/// Systemd Install Section +pub struct SystemdInstallSection { + #[serde(skip_serializing_if="Option::is_none")] + pub Alias: Option>, + #[serde(skip_serializing_if="Option::is_none")] + pub Also: Option>, + /// DefaultInstance for template unit. + #[serde(skip_serializing_if="Option::is_none")] + pub DefaultInstance: Option, + #[serde(skip_serializing_if="Option::is_none")] + pub WantedBy: Option>, + #[serde(skip_serializing_if="Option::is_none")] + pub RequiredBy: Option>, +} + +#[api()] +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +/// Node Power command type. +pub enum ServiceStartup { + /// Simple fork + Simple, + /// Like fork, but wait until exec succeeds + Exec, + /// Fork daemon + Forking, + /// Like 'simple', but consider the unit up after the process exits. + Oneshot, + /// Like 'simple', but use DBUS to synchronize startup. + Dbus, + /// Like 'simple', but use sd_notify to synchronize startup. + Notify, +}