]> git.proxmox.com Git - proxmox-backup.git/blob - pbs-api-types/src/jobs.rs
api: add GroupFilter(List) type
[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(true)
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 }
251 )]
252 #[derive(Serialize,Deserialize,Clone,Updater)]
253 #[serde(rename_all="kebab-case")]
254 /// Tape Backup Job Setup
255 pub struct TapeBackupJobSetup {
256 pub store: String,
257 pub pool: String,
258 pub drive: String,
259 #[serde(skip_serializing_if="Option::is_none")]
260 pub eject_media: Option<bool>,
261 #[serde(skip_serializing_if="Option::is_none")]
262 pub export_media_set: Option<bool>,
263 #[serde(skip_serializing_if="Option::is_none")]
264 pub latest_only: Option<bool>,
265 /// Send job email notification to this user
266 #[serde(skip_serializing_if="Option::is_none")]
267 pub notify_user: Option<Userid>,
268 }
269
270 #[api(
271 properties: {
272 id: {
273 schema: JOB_ID_SCHEMA,
274 },
275 setup: {
276 type: TapeBackupJobSetup,
277 },
278 comment: {
279 optional: true,
280 schema: SINGLE_LINE_COMMENT_SCHEMA,
281 },
282 schedule: {
283 optional: true,
284 schema: SYNC_SCHEDULE_SCHEMA,
285 },
286 }
287 )]
288 #[derive(Serialize,Deserialize,Clone,Updater)]
289 #[serde(rename_all="kebab-case")]
290 /// Tape Backup Job
291 pub struct TapeBackupJobConfig {
292 #[updater(skip)]
293 pub id: String,
294 #[serde(flatten)]
295 pub setup: TapeBackupJobSetup,
296 #[serde(skip_serializing_if="Option::is_none")]
297 pub comment: Option<String>,
298 #[serde(skip_serializing_if="Option::is_none")]
299 pub schedule: Option<String>,
300 }
301
302 #[api(
303 properties: {
304 config: {
305 type: TapeBackupJobConfig,
306 },
307 status: {
308 type: JobScheduleStatus,
309 },
310 },
311 )]
312 #[derive(Serialize,Deserialize)]
313 #[serde(rename_all="kebab-case")]
314 /// Status of Tape Backup Job
315 pub struct TapeBackupJobStatus {
316 #[serde(flatten)]
317 pub config: TapeBackupJobConfig,
318 #[serde(flatten)]
319 pub status: JobScheduleStatus,
320 /// Next tape used (best guess)
321 #[serde(skip_serializing_if="Option::is_none")]
322 pub next_media_label: Option<String>,
323 }
324
325 #[derive(Clone, Debug)]
326 /// Filter for matching `BackupGroup`s, for use with `BackupGroup::filter`.
327 pub enum GroupFilter {
328 /// BackupGroup type - either `vm`, `ct`, or `host`.
329 BackupType(String),
330 /// Full identifier of BackupGroup, including type
331 Group(String),
332 /// A regular expression matched against the full identifier of the BackupGroup
333 Regex(Regex),
334 }
335
336 impl std::str::FromStr for GroupFilter {
337 type Err = anyhow::Error;
338
339 fn from_str(s: &str) -> Result<Self, Self::Err> {
340 match s.split_once(":") {
341 Some(("group", value)) => parse_simple_value(value, &BACKUP_GROUP_SCHEMA).map(|_| GroupFilter::Group(value.to_string())),
342 Some(("type", value)) => parse_simple_value(value, &BACKUP_TYPE_SCHEMA).map(|_| GroupFilter::BackupType(value.to_string())),
343 Some(("regex", value)) => Ok(GroupFilter::Regex(Regex::new(value)?)),
344 Some((ty, _value)) => Err(format_err!("expected 'group', 'type' or 'regex' prefix, got '{}'", ty)),
345 None => Err(format_err!("input doesn't match expected format '<group:GROUP||type:<vm|ct|host>|regex:REGEX>'")),
346 }.map_err(|err| format_err!("'{}' - {}", s, err))
347 }
348 }
349
350 // used for serializing below, caution!
351 impl std::fmt::Display for GroupFilter {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 match self {
354 GroupFilter::BackupType(backup_type) => write!(f, "type:{}", backup_type),
355 GroupFilter::Group(backup_group) => write!(f, "group:{}", backup_group),
356 GroupFilter::Regex(regex) => write!(f, "regex:{}", regex.as_str()),
357 }
358 }
359 }
360
361 proxmox::forward_deserialize_to_from_str!(GroupFilter);
362 proxmox::forward_serialize_to_display!(GroupFilter);
363
364 fn verify_group_filter(input: &str) -> Result<(), anyhow::Error> {
365 GroupFilter::from_str(input).map(|_| ())
366 }
367
368 pub const GROUP_FILTER_SCHEMA: Schema = StringSchema::new(
369 "Group filter based on group identifier ('group:GROUP'), group type ('type:<vm|ct|host>'), or regex ('regex:RE').")
370 .format(&ApiStringFormat::VerifyFn(verify_group_filter))
371 .type_text("<type:<vm|ct|host>|group:GROUP|regex:RE>")
372 .schema();
373
374 pub const GROUP_FILTER_LIST_SCHEMA: Schema = ArraySchema::new("List of group filters.", &GROUP_FILTER_SCHEMA).schema();
375
376 #[api(
377 properties: {
378 id: {
379 schema: JOB_ID_SCHEMA,
380 },
381 store: {
382 schema: DATASTORE_SCHEMA,
383 },
384 "owner": {
385 type: Authid,
386 optional: true,
387 },
388 remote: {
389 schema: REMOTE_ID_SCHEMA,
390 },
391 "remote-store": {
392 schema: DATASTORE_SCHEMA,
393 },
394 "remove-vanished": {
395 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
396 optional: true,
397 },
398 comment: {
399 optional: true,
400 schema: SINGLE_LINE_COMMENT_SCHEMA,
401 },
402 schedule: {
403 optional: true,
404 schema: SYNC_SCHEDULE_SCHEMA,
405 },
406 }
407 )]
408 #[derive(Serialize,Deserialize,Clone,Updater)]
409 #[serde(rename_all="kebab-case")]
410 /// Sync Job
411 pub struct SyncJobConfig {
412 #[updater(skip)]
413 pub id: String,
414 pub store: String,
415 #[serde(skip_serializing_if="Option::is_none")]
416 pub owner: Option<Authid>,
417 pub remote: String,
418 pub remote_store: String,
419 #[serde(skip_serializing_if="Option::is_none")]
420 pub remove_vanished: Option<bool>,
421 #[serde(skip_serializing_if="Option::is_none")]
422 pub comment: Option<String>,
423 #[serde(skip_serializing_if="Option::is_none")]
424 pub schedule: Option<String>,
425 }
426
427 #[api(
428 properties: {
429 config: {
430 type: SyncJobConfig,
431 },
432 status: {
433 type: JobScheduleStatus,
434 },
435 },
436 )]
437
438 #[derive(Serialize,Deserialize)]
439 #[serde(rename_all="kebab-case")]
440 /// Status of Sync Job
441 pub struct SyncJobStatus {
442 #[serde(flatten)]
443 pub config: SyncJobConfig,
444 #[serde(flatten)]
445 pub status: JobScheduleStatus,
446 }