]> git.proxmox.com Git - proxmox-backup.git/blob - pbs-api-types/src/jobs.rs
601d86f90e9a3fc282178c72fd92aa54d1a6dbb1
[proxmox-backup.git] / pbs-api-types / src / jobs.rs
1 use anyhow::format_err;
2 use std::str::FromStr;
3
4 use regex::Regex;
5 use serde::{Deserialize, Serialize};
6
7 use proxmox_schema::*;
8
9 use crate::{
10 Userid, Authid, REMOTE_ID_SCHEMA, DRIVE_NAME_SCHEMA, MEDIA_POOL_NAME_SCHEMA,
11 SINGLE_LINE_COMMENT_SCHEMA, PROXMOX_SAFE_ID_FORMAT, DATASTORE_SCHEMA,
12 BACKUP_GROUP_SCHEMA, BACKUP_TYPE_SCHEMA,
13 };
14
15 const_regex!{
16
17 /// Regex for verification jobs 'DATASTORE:ACTUAL_JOB_ID'
18 pub VERIFICATION_JOB_WORKER_ID_REGEX = concat!(r"^(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):");
19 /// Regex for sync jobs 'REMOTE:REMOTE_DATASTORE:LOCAL_DATASTORE:ACTUAL_JOB_ID'
20 pub SYNC_JOB_WORKER_ID_REGEX = concat!(r"^(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):");
21 }
22
23 pub const JOB_ID_SCHEMA: Schema = StringSchema::new("Job ID.")
24 .format(&PROXMOX_SAFE_ID_FORMAT)
25 .min_length(3)
26 .max_length(32)
27 .schema();
28
29 pub const SYNC_SCHEDULE_SCHEMA: Schema = StringSchema::new(
30 "Run sync job at specified schedule.")
31 .format(&ApiStringFormat::VerifyFn(proxmox_time::verify_calendar_event))
32 .type_text("<calendar-event>")
33 .schema();
34
35 pub const GC_SCHEDULE_SCHEMA: Schema = StringSchema::new(
36 "Run garbage collection job at specified schedule.")
37 .format(&ApiStringFormat::VerifyFn(proxmox_time::verify_calendar_event))
38 .type_text("<calendar-event>")
39 .schema();
40
41 pub const PRUNE_SCHEDULE_SCHEMA: Schema = StringSchema::new(
42 "Run prune job at specified schedule.")
43 .format(&ApiStringFormat::VerifyFn(proxmox_time::verify_calendar_event))
44 .type_text("<calendar-event>")
45 .schema();
46
47 pub const VERIFICATION_SCHEDULE_SCHEMA: Schema = StringSchema::new(
48 "Run verify job at specified schedule.")
49 .format(&ApiStringFormat::VerifyFn(proxmox_time::verify_calendar_event))
50 .type_text("<calendar-event>")
51 .schema();
52
53 pub const REMOVE_VANISHED_BACKUPS_SCHEMA: Schema = BooleanSchema::new(
54 "Delete vanished backups. This remove the local copy if the remote backup was deleted.")
55 .default(false)
56 .schema();
57
58 #[api(
59 properties: {
60 "next-run": {
61 description: "Estimated time of the next run (UNIX epoch).",
62 optional: true,
63 type: Integer,
64 },
65 "last-run-state": {
66 description: "Result of the last run.",
67 optional: true,
68 type: String,
69 },
70 "last-run-upid": {
71 description: "Task UPID of the last run.",
72 optional: true,
73 type: String,
74 },
75 "last-run-endtime": {
76 description: "Endtime of the last run.",
77 optional: true,
78 type: Integer,
79 },
80 }
81 )]
82 #[derive(Serialize,Deserialize,Default)]
83 #[serde(rename_all="kebab-case")]
84 /// Job Scheduling Status
85 pub struct JobScheduleStatus {
86 #[serde(skip_serializing_if="Option::is_none")]
87 pub next_run: Option<i64>,
88 #[serde(skip_serializing_if="Option::is_none")]
89 pub last_run_state: Option<String>,
90 #[serde(skip_serializing_if="Option::is_none")]
91 pub last_run_upid: Option<String>,
92 #[serde(skip_serializing_if="Option::is_none")]
93 pub last_run_endtime: Option<i64>,
94 }
95
96 #[api()]
97 #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
98 #[serde(rename_all = "lowercase")]
99 /// When do we send notifications
100 pub enum Notify {
101 /// Never send notification
102 Never,
103 /// Send notifications for failed and successful jobs
104 Always,
105 /// Send notifications for failed jobs only
106 Error,
107 }
108
109 #[api(
110 properties: {
111 gc: {
112 type: Notify,
113 optional: true,
114 },
115 verify: {
116 type: Notify,
117 optional: true,
118 },
119 sync: {
120 type: Notify,
121 optional: true,
122 },
123 },
124 )]
125 #[derive(Debug, Serialize, Deserialize)]
126 /// Datastore notify settings
127 pub struct DatastoreNotify {
128 /// Garbage collection settings
129 pub gc: Option<Notify>,
130 /// Verify job setting
131 pub verify: Option<Notify>,
132 /// Sync job setting
133 pub sync: Option<Notify>,
134 }
135
136 pub const DATASTORE_NOTIFY_STRING_SCHEMA: Schema = StringSchema::new(
137 "Datastore notification setting")
138 .format(&ApiStringFormat::PropertyString(&DatastoreNotify::API_SCHEMA))
139 .schema();
140
141 pub const IGNORE_VERIFIED_BACKUPS_SCHEMA: Schema = BooleanSchema::new(
142 "Do not verify backups that are already verified if their verification is not outdated.")
143 .default(true)
144 .schema();
145
146 pub const VERIFICATION_OUTDATED_AFTER_SCHEMA: Schema = IntegerSchema::new(
147 "Days after that a verification becomes outdated")
148 .minimum(1)
149 .schema();
150
151 #[api(
152 properties: {
153 id: {
154 schema: JOB_ID_SCHEMA,
155 },
156 store: {
157 schema: DATASTORE_SCHEMA,
158 },
159 "ignore-verified": {
160 optional: true,
161 schema: IGNORE_VERIFIED_BACKUPS_SCHEMA,
162 },
163 "outdated-after": {
164 optional: true,
165 schema: VERIFICATION_OUTDATED_AFTER_SCHEMA,
166 },
167 comment: {
168 optional: true,
169 schema: SINGLE_LINE_COMMENT_SCHEMA,
170 },
171 schedule: {
172 optional: true,
173 schema: VERIFICATION_SCHEDULE_SCHEMA,
174 },
175 }
176 )]
177 #[derive(Serialize,Deserialize,Updater)]
178 #[serde(rename_all="kebab-case")]
179 /// Verification Job
180 pub struct VerificationJobConfig {
181 /// unique ID to address this job
182 #[updater(skip)]
183 pub id: String,
184 /// the datastore ID this verificaiton job affects
185 pub store: String,
186 #[serde(skip_serializing_if="Option::is_none")]
187 /// if not set to false, check the age of the last snapshot verification to filter
188 /// out recent ones, depending on 'outdated_after' configuration.
189 pub ignore_verified: Option<bool>,
190 #[serde(skip_serializing_if="Option::is_none")]
191 /// Reverify snapshots after X days, never if 0. Ignored if 'ignore_verified' is false.
192 pub outdated_after: Option<i64>,
193 #[serde(skip_serializing_if="Option::is_none")]
194 pub comment: Option<String>,
195 #[serde(skip_serializing_if="Option::is_none")]
196 /// when to schedule this job in calendar event notation
197 pub schedule: Option<String>,
198 }
199
200 #[api(
201 properties: {
202 config: {
203 type: VerificationJobConfig,
204 },
205 status: {
206 type: JobScheduleStatus,
207 },
208 },
209 )]
210 #[derive(Serialize,Deserialize)]
211 #[serde(rename_all="kebab-case")]
212 /// Status of Verification Job
213 pub struct VerificationJobStatus {
214 #[serde(flatten)]
215 pub config: VerificationJobConfig,
216 #[serde(flatten)]
217 pub status: JobScheduleStatus,
218 }
219
220 #[api(
221 properties: {
222 store: {
223 schema: DATASTORE_SCHEMA,
224 },
225 pool: {
226 schema: MEDIA_POOL_NAME_SCHEMA,
227 },
228 drive: {
229 schema: DRIVE_NAME_SCHEMA,
230 },
231 "eject-media": {
232 description: "Eject media upon job completion.",
233 type: bool,
234 optional: true,
235 },
236 "export-media-set": {
237 description: "Export media set upon job completion.",
238 type: bool,
239 optional: true,
240 },
241 "latest-only": {
242 description: "Backup latest snapshots only.",
243 type: bool,
244 optional: true,
245 },
246 "notify-user": {
247 optional: true,
248 type: Userid,
249 },
250 groups: {
251 schema: GROUP_FILTER_LIST_SCHEMA,
252 optional: true,
253 },
254 }
255 )]
256 #[derive(Serialize,Deserialize,Clone,Updater)]
257 #[serde(rename_all="kebab-case")]
258 /// Tape Backup Job Setup
259 pub struct TapeBackupJobSetup {
260 pub store: String,
261 pub pool: String,
262 pub drive: String,
263 #[serde(skip_serializing_if="Option::is_none")]
264 pub eject_media: Option<bool>,
265 #[serde(skip_serializing_if="Option::is_none")]
266 pub export_media_set: Option<bool>,
267 #[serde(skip_serializing_if="Option::is_none")]
268 pub latest_only: Option<bool>,
269 /// Send job email notification to this user
270 #[serde(skip_serializing_if="Option::is_none")]
271 pub notify_user: Option<Userid>,
272 #[serde(skip_serializing_if="Option::is_none")]
273 pub groups: Option<Vec<GroupFilter>>,
274 }
275
276 #[api(
277 properties: {
278 id: {
279 schema: JOB_ID_SCHEMA,
280 },
281 setup: {
282 type: TapeBackupJobSetup,
283 },
284 comment: {
285 optional: true,
286 schema: SINGLE_LINE_COMMENT_SCHEMA,
287 },
288 schedule: {
289 optional: true,
290 schema: SYNC_SCHEDULE_SCHEMA,
291 },
292 }
293 )]
294 #[derive(Serialize,Deserialize,Clone,Updater)]
295 #[serde(rename_all="kebab-case")]
296 /// Tape Backup Job
297 pub struct TapeBackupJobConfig {
298 #[updater(skip)]
299 pub id: String,
300 #[serde(flatten)]
301 pub setup: TapeBackupJobSetup,
302 #[serde(skip_serializing_if="Option::is_none")]
303 pub comment: Option<String>,
304 #[serde(skip_serializing_if="Option::is_none")]
305 pub schedule: Option<String>,
306 }
307
308 #[api(
309 properties: {
310 config: {
311 type: TapeBackupJobConfig,
312 },
313 status: {
314 type: JobScheduleStatus,
315 },
316 },
317 )]
318 #[derive(Serialize,Deserialize)]
319 #[serde(rename_all="kebab-case")]
320 /// Status of Tape Backup Job
321 pub struct TapeBackupJobStatus {
322 #[serde(flatten)]
323 pub config: TapeBackupJobConfig,
324 #[serde(flatten)]
325 pub status: JobScheduleStatus,
326 /// Next tape used (best guess)
327 #[serde(skip_serializing_if="Option::is_none")]
328 pub next_media_label: Option<String>,
329 }
330
331 #[derive(Clone, Debug)]
332 /// Filter for matching `BackupGroup`s, for use with `BackupGroup::filter`.
333 pub enum GroupFilter {
334 /// BackupGroup type - either `vm`, `ct`, or `host`.
335 BackupType(String),
336 /// Full identifier of BackupGroup, including type
337 Group(String),
338 /// A regular expression matched against the full identifier of the BackupGroup
339 Regex(Regex),
340 }
341
342 impl std::str::FromStr for GroupFilter {
343 type Err = anyhow::Error;
344
345 fn from_str(s: &str) -> Result<Self, Self::Err> {
346 match s.split_once(":") {
347 Some(("group", value)) => parse_simple_value(value, &BACKUP_GROUP_SCHEMA).map(|_| GroupFilter::Group(value.to_string())),
348 Some(("type", value)) => parse_simple_value(value, &BACKUP_TYPE_SCHEMA).map(|_| GroupFilter::BackupType(value.to_string())),
349 Some(("regex", value)) => Ok(GroupFilter::Regex(Regex::new(value)?)),
350 Some((ty, _value)) => Err(format_err!("expected 'group', 'type' or 'regex' prefix, got '{}'", ty)),
351 None => Err(format_err!("input doesn't match expected format '<group:GROUP||type:<vm|ct|host>|regex:REGEX>'")),
352 }.map_err(|err| format_err!("'{}' - {}", s, err))
353 }
354 }
355
356 // used for serializing below, caution!
357 impl std::fmt::Display for GroupFilter {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 match self {
360 GroupFilter::BackupType(backup_type) => write!(f, "type:{}", backup_type),
361 GroupFilter::Group(backup_group) => write!(f, "group:{}", backup_group),
362 GroupFilter::Regex(regex) => write!(f, "regex:{}", regex.as_str()),
363 }
364 }
365 }
366
367 proxmox::forward_deserialize_to_from_str!(GroupFilter);
368 proxmox::forward_serialize_to_display!(GroupFilter);
369
370 fn verify_group_filter(input: &str) -> Result<(), anyhow::Error> {
371 GroupFilter::from_str(input).map(|_| ())
372 }
373
374 pub const GROUP_FILTER_SCHEMA: Schema = StringSchema::new(
375 "Group filter based on group identifier ('group:GROUP'), group type ('type:<vm|ct|host>'), or regex ('regex:RE').")
376 .format(&ApiStringFormat::VerifyFn(verify_group_filter))
377 .type_text("<type:<vm|ct|host>|group:GROUP|regex:RE>")
378 .schema();
379
380 pub const GROUP_FILTER_LIST_SCHEMA: Schema = ArraySchema::new("List of group filters.", &GROUP_FILTER_SCHEMA).schema();
381
382 #[api(
383 properties: {
384 id: {
385 schema: JOB_ID_SCHEMA,
386 },
387 store: {
388 schema: DATASTORE_SCHEMA,
389 },
390 "owner": {
391 type: Authid,
392 optional: true,
393 },
394 remote: {
395 schema: REMOTE_ID_SCHEMA,
396 },
397 "remote-store": {
398 schema: DATASTORE_SCHEMA,
399 },
400 "remove-vanished": {
401 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
402 optional: true,
403 },
404 comment: {
405 optional: true,
406 schema: SINGLE_LINE_COMMENT_SCHEMA,
407 },
408 schedule: {
409 optional: true,
410 schema: SYNC_SCHEDULE_SCHEMA,
411 },
412 groups: {
413 schema: GROUP_FILTER_LIST_SCHEMA,
414 optional: true,
415 },
416 }
417 )]
418 #[derive(Serialize,Deserialize,Clone,Updater)]
419 #[serde(rename_all="kebab-case")]
420 /// Sync Job
421 pub struct SyncJobConfig {
422 #[updater(skip)]
423 pub id: String,
424 pub store: String,
425 #[serde(skip_serializing_if="Option::is_none")]
426 pub owner: Option<Authid>,
427 pub remote: String,
428 pub remote_store: String,
429 #[serde(skip_serializing_if="Option::is_none")]
430 pub remove_vanished: Option<bool>,
431 #[serde(skip_serializing_if="Option::is_none")]
432 pub comment: Option<String>,
433 #[serde(skip_serializing_if="Option::is_none")]
434 pub schedule: Option<String>,
435 #[serde(skip_serializing_if="Option::is_none")]
436 pub groups: Option<Vec<GroupFilter>>,
437 }
438
439 #[api(
440 properties: {
441 config: {
442 type: SyncJobConfig,
443 },
444 status: {
445 type: JobScheduleStatus,
446 },
447 },
448 )]
449
450 #[derive(Serialize,Deserialize)]
451 #[serde(rename_all="kebab-case")]
452 /// Status of Sync Job
453 pub struct SyncJobStatus {
454 #[serde(flatten)]
455 pub config: SyncJobConfig,
456 #[serde(flatten)]
457 pub status: JobScheduleStatus,
458 }