src/config/jobs.rs: use SectionConfig for jobs
This commit is contained in:
parent
c681885227
commit
b4900286ce
@ -394,11 +394,9 @@ pub async fn pull_store(
|
|||||||
"remote-store": {
|
"remote-store": {
|
||||||
schema: DATASTORE_SCHEMA,
|
schema: DATASTORE_SCHEMA,
|
||||||
},
|
},
|
||||||
delete: {
|
"remove-vanished": {
|
||||||
description: "Delete vanished backups. This remove the local copy if the remote backup was deleted.",
|
schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
|
||||||
type: Boolean,
|
|
||||||
optional: true,
|
optional: true,
|
||||||
default: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -416,7 +414,7 @@ async fn pull (
|
|||||||
store: String,
|
store: String,
|
||||||
remote: String,
|
remote: String,
|
||||||
remote_store: String,
|
remote_store: String,
|
||||||
delete: Option<bool>,
|
remove_vanished: Option<bool>,
|
||||||
_info: &ApiMethod,
|
_info: &ApiMethod,
|
||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
@ -427,7 +425,7 @@ async fn pull (
|
|||||||
user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_BACKUP, false)?;
|
user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_BACKUP, false)?;
|
||||||
user_info.check_privs(&username, &["remote", &remote, &remote_store], PRIV_REMOTE_READ, false)?;
|
user_info.check_privs(&username, &["remote", &remote, &remote_store], PRIV_REMOTE_READ, false)?;
|
||||||
|
|
||||||
let delete = delete.unwrap_or(true);
|
let delete = remove_vanished.unwrap_or(true);
|
||||||
|
|
||||||
if delete {
|
if delete {
|
||||||
user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_PRUNE, false)?;
|
user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_PRUNE, false)?;
|
||||||
|
@ -303,6 +303,17 @@ pub const REMOTE_ID_SCHEMA: Schema = StringSchema::new("Remote ID.")
|
|||||||
.max_length(32)
|
.max_length(32)
|
||||||
.schema();
|
.schema();
|
||||||
|
|
||||||
|
pub const JOB_ID_SCHEMA: Schema = StringSchema::new("Job ID.")
|
||||||
|
.format(&PROXMOX_SAFE_ID_FORMAT)
|
||||||
|
.min_length(3)
|
||||||
|
.max_length(32)
|
||||||
|
.schema();
|
||||||
|
|
||||||
|
pub const REMOVE_VANISHED_BACKUPS_SCHEMA: Schema = BooleanSchema::new(
|
||||||
|
"Delete vanished backups. This remove the local copy if the remote backup was deleted.")
|
||||||
|
.default(true)
|
||||||
|
.schema();
|
||||||
|
|
||||||
pub const SINGLE_LINE_COMMENT_SCHEMA: Schema = StringSchema::new("Comment (single line).")
|
pub const SINGLE_LINE_COMMENT_SCHEMA: Schema = StringSchema::new("Comment (single line).")
|
||||||
.format(&SINGLE_LINE_COMMENT_FORMAT)
|
.format(&SINGLE_LINE_COMMENT_FORMAT)
|
||||||
.schema();
|
.schema();
|
||||||
|
@ -1,160 +1,125 @@
|
|||||||
use anyhow::{bail, Error};
|
use anyhow::{bail, Error};
|
||||||
use regex::Regex;
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use proxmox::api::section_config::SectionConfigData;
|
use proxmox::api::{
|
||||||
|
api,
|
||||||
use crate::PROXMOX_SAFE_ID_REGEX_STR;
|
schema::*,
|
||||||
use crate::tools::systemd::config::*;
|
section_config::{
|
||||||
use crate::tools::systemd::types::*;
|
SectionConfig,
|
||||||
|
SectionConfigData,
|
||||||
const SYSTEMD_CONFIG_DIR: &str = "/etc/systemd/system";
|
SectionConfigPlugin,
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum JobType {
|
|
||||||
GarbageCollection,
|
|
||||||
Prune,
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
use proxmox::tools::{fs::replace_file, fs::CreateOptions};
|
||||||
pub struct CalenderTimeSpec {
|
|
||||||
pub hour: u8, // 0-23
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
use crate::api2::types::*;
|
||||||
pub struct JobListEntry {
|
|
||||||
job_type: JobType,
|
|
||||||
id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_jobs() -> Result<Vec<JobListEntry>, Error> {
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref PBS_JOB_REGEX: Regex = Regex::new(
|
static ref CONFIG: SectionConfig = init();
|
||||||
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)? {
|
#[api(
|
||||||
let entry = entry?;
|
properties: {
|
||||||
let file_type = match entry.file_type() {
|
id: {
|
||||||
Some(file_type) => file_type,
|
schema: JOB_ID_SCHEMA,
|
||||||
None => bail!("unable to detect file type"),
|
},
|
||||||
};
|
store: {
|
||||||
if file_type != nix::dir::Type::File { continue; };
|
schema: DATASTORE_SCHEMA,
|
||||||
|
},
|
||||||
let file_name = entry.file_name().to_bytes();
|
remote: {
|
||||||
if file_name == b"." || file_name == b".." { continue; };
|
schema: REMOTE_ID_SCHEMA,
|
||||||
|
},
|
||||||
let name = match std::str::from_utf8(file_name) {
|
"remote-store": {
|
||||||
Ok(name) => name,
|
schema: DATASTORE_SCHEMA,
|
||||||
Err(_) => continue,
|
},
|
||||||
};
|
"remove-vanished": {
|
||||||
let caps = match PBS_JOB_REGEX.captures(name) {
|
schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
|
||||||
Some(caps) => caps,
|
optional: true,
|
||||||
None => continue,
|
},
|
||||||
};
|
comment: {
|
||||||
|
optional: true,
|
||||||
// fixme: read config data ?
|
schema: SINGLE_LINE_COMMENT_SCHEMA,
|
||||||
//let config = parse_systemd_timer(&format!("{}/{}", SYSTEMD_CONFIG_DIR, name))?;
|
},
|
||||||
|
schedule: {
|
||||||
match (&caps[1], &caps[2]) {
|
optional: true,
|
||||||
("gc", store) => {
|
schema: GC_SCHEDULE_SCHEMA,
|
||||||
list.push(JobListEntry {
|
},
|
||||||
job_type: JobType::GarbageCollection,
|
|
||||||
id: store.to_string(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
("prune", store) => {
|
)]
|
||||||
list.push(JobListEntry {
|
#[serde(rename_all="kebab-case")]
|
||||||
job_type: JobType::Prune,
|
#[derive(Serialize,Deserialize)]
|
||||||
id: store.to_string(),
|
/// Pull Job
|
||||||
});
|
pub struct PullJobConfig {
|
||||||
|
pub id: String,
|
||||||
|
pub store: String,
|
||||||
|
pub remote: String,
|
||||||
|
pub remote_store: String,
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub remove_vanished: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub comment: Option<String>,
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub schedule: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init() -> SectionConfig {
|
||||||
|
let obj_schema = match PullJobConfig::API_SCHEMA {
|
||||||
|
Schema::Object(ref obj_schema) => obj_schema,
|
||||||
_ => unreachable!(),
|
_ => 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 plugin = SectionConfigPlugin::new("pull".to_string(), Some(String::from("id")), obj_schema);
|
||||||
let service = SystemdServiceSection {
|
let mut config = SectionConfig::new(&JOB_ID_SCHEMA);
|
||||||
Type: Some(ServiceStartup::Oneshot),
|
config.register_plugin(plugin);
|
||||||
ExecStart: Some(vec![cmd]),
|
|
||||||
..Default::default()
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JOB_CFG_FILENAME: &str = "/etc/proxmox-backup/job.cfg";
|
||||||
|
pub const JOB_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.job.lck";
|
||||||
|
|
||||||
|
pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
|
||||||
|
let content = match std::fs::read_to_string(JOB_CFG_FILENAME) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(err) => {
|
||||||
|
if err.kind() == std::io::ErrorKind::NotFound {
|
||||||
|
String::from("")
|
||||||
|
} else {
|
||||||
|
bail!("unable to read '{}' - {}", JOB_CFG_FILENAME, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let service_config = new_systemd_service_config(&unit, &service)?;
|
let digest = openssl::sha::sha256(content.as_bytes());
|
||||||
|
let data = CONFIG.parse(JOB_CFG_FILENAME, &content)?;
|
||||||
|
Ok((data, digest))
|
||||||
|
}
|
||||||
|
|
||||||
let timer = SystemdTimerSection {
|
pub fn save_config(config: &SectionConfigData) -> Result<(), Error> {
|
||||||
OnCalendar: Some(vec![format!("{}:00", schedule.hour)]),
|
let raw = CONFIG.write(JOB_CFG_FILENAME, &config)?;
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let install = SystemdInstallSection {
|
let backup_user = crate::backup::backup_user()?;
|
||||||
WantedBy: Some(vec![String::from("timers.target")]),
|
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
|
||||||
..Default::default()
|
// 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);
|
||||||
|
|
||||||
let timer_config = new_systemd_timer_config(&unit, &timer, &install)?;
|
replace_file(JOB_CFG_FILENAME, raw.as_bytes(), options)?;
|
||||||
|
|
||||||
let basename = format!("{}/pbs-gc-{}", SYSTEMD_CONFIG_DIR, store);
|
|
||||||
let timer_fn = format!("{}.timer", basename);
|
|
||||||
let service_fn = format!("{}.service", basename);
|
|
||||||
|
|
||||||
save_systemd_service(&service_fn, &service_config)?;
|
|
||||||
save_systemd_timer(&timer_fn, &timer_config)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shell completion helper
|
||||||
|
pub fn complete_job_id(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||||
|
match config() {
|
||||||
|
Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(),
|
||||||
|
Err(_) => return vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user