add systemd configuration file parser/writer, start job configuration
This commit is contained in:
		@ -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})$")) }
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										173
									
								
								src/config/jobs.rs
									
									
									
									
									
										Normal 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(())
 | 
			
		||||
}
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										2
									
								
								src/tools/systemd.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
pub mod types;
 | 
			
		||||
pub mod parser;
 | 
			
		||||
							
								
								
									
										81
									
								
								src/tools/systemd/parser.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/tools/systemd/parser.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										140
									
								
								src/tools/systemd/types.rs
									
									
									
									
									
										Normal 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,
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user