add systemd configuration file parser/writer, start job configuration

This commit is contained in:
Dietmar Maurer 2020-05-12 13:07:49 +02:00
parent 7f5a27d302
commit f486e9e50e
7 changed files with 399 additions and 0 deletions

View File

@ -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})$")) }

View File

@ -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
///

173
src/config/jobs.rs Normal file
View File

@ -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<Vec<JobListEntry>, 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<SectionConfigData, Error> {
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<SectionConfigData, Error> {
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(())
}

View File

@ -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::*;

2
src/tools/systemd.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod types;
pub mod parser;

View File

@ -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<SectionConfigData, Error> {
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(())
}

140
src/tools/systemd/types.rs Normal file
View File

@ -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<Vec<String>>,
/// 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<bool>,
}
#[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<ServiceStartup>,
#[serde(skip_serializing_if="Option::is_none")]
pub ExecStart: Option<Vec<String>>,
}
#[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<bool>,
}
#[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<Vec<String>>,
#[serde(skip_serializing_if="Option::is_none")]
pub Also: Option<Vec<String>>,
/// DefaultInstance for template unit.
#[serde(skip_serializing_if="Option::is_none")]
pub DefaultInstance: Option<String>,
#[serde(skip_serializing_if="Option::is_none")]
pub WantedBy: Option<Vec<String>>,
#[serde(skip_serializing_if="Option::is_none")]
pub RequiredBy: Option<Vec<String>>,
}
#[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,
}