Skip to main content

shared/models/
system_backup_policy.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4};
5use compact_str::ToCompactString;
6use garde::Validate;
7use serde::{Deserialize, Serialize};
8use sqlx::{Row, postgres::PgRow};
9use std::{
10    collections::BTreeMap,
11    str::FromStr,
12    sync::{Arc, LazyLock},
13};
14use utoipa::ToSchema;
15
16#[derive(Serialize, Deserialize, Clone)]
17pub struct SystemBackupPolicy {
18    pub uuid: uuid::Uuid,
19    pub backup_configuration: Option<Fetchable<super::backup_configuration::BackupConfiguration>>,
20
21    pub name: compact_str::CompactString,
22    pub description: Option<compact_str::CompactString>,
23
24    pub enabled: bool,
25    pub cron: croner::Cron,
26    pub retention_count: Option<i32>,
27    pub retention_days: Option<i32>,
28    pub parallelism: i32,
29
30    pub triggered: Option<chrono::NaiveDateTime>,
31    pub created: chrono::NaiveDateTime,
32
33    extension_data: super::ModelExtensionData,
34}
35
36impl BaseModel for SystemBackupPolicy {
37    const NAME: &'static str = "system_backup_policy";
38
39    fn get_extension_list() -> &'static super::ModelExtensionList {
40        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
41            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
42
43        &EXTENSIONS
44    }
45
46    fn get_extension_data(&self) -> &super::ModelExtensionData {
47        &self.extension_data
48    }
49
50    #[inline]
51    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
52        let prefix = prefix.unwrap_or_default();
53
54        BTreeMap::from([
55            (
56                "system_backup_policies.uuid",
57                compact_str::format_compact!("{prefix}uuid"),
58            ),
59            (
60                "system_backup_policies.backup_configuration_uuid",
61                compact_str::format_compact!("{prefix}backup_configuration_uuid"),
62            ),
63            (
64                "system_backup_policies.name",
65                compact_str::format_compact!("{prefix}name"),
66            ),
67            (
68                "system_backup_policies.description",
69                compact_str::format_compact!("{prefix}description"),
70            ),
71            (
72                "system_backup_policies.enabled",
73                compact_str::format_compact!("{prefix}enabled"),
74            ),
75            (
76                "system_backup_policies.cron",
77                compact_str::format_compact!("{prefix}cron"),
78            ),
79            (
80                "system_backup_policies.retention_count",
81                compact_str::format_compact!("{prefix}retention_count"),
82            ),
83            (
84                "system_backup_policies.retention_days",
85                compact_str::format_compact!("{prefix}retention_days"),
86            ),
87            (
88                "system_backup_policies.parallelism",
89                compact_str::format_compact!("{prefix}parallelism"),
90            ),
91            (
92                "system_backup_policies.triggered",
93                compact_str::format_compact!("{prefix}triggered"),
94            ),
95            (
96                "system_backup_policies.created",
97                compact_str::format_compact!("{prefix}created"),
98            ),
99        ])
100    }
101
102    #[inline]
103    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
104        let prefix = prefix.unwrap_or_default();
105
106        let cron: String = row.try_get(compact_str::format_compact!("{prefix}cron").as_str())?;
107
108        Ok(Self {
109            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
110            backup_configuration:
111                super::backup_configuration::BackupConfiguration::get_fetchable_from_row(
112                    row,
113                    compact_str::format_compact!("{prefix}backup_configuration_uuid"),
114                ),
115            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
116            description: row
117                .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
118            enabled: row.try_get(compact_str::format_compact!("{prefix}enabled").as_str())?,
119            cron: croner::Cron::from_str(&cron)
120                .map_err(|err| crate::database::DatabaseError::Any(anyhow::Error::new(err)))?,
121            retention_count: row
122                .try_get(compact_str::format_compact!("{prefix}retention_count").as_str())?,
123            retention_days: row
124                .try_get(compact_str::format_compact!("{prefix}retention_days").as_str())?,
125            parallelism: row
126                .try_get(compact_str::format_compact!("{prefix}parallelism").as_str())?,
127            triggered: row.try_get(compact_str::format_compact!("{prefix}triggered").as_str())?,
128            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
129            extension_data: Self::map_extensions(prefix, row)?,
130        })
131    }
132}
133
134pub struct SystemBackupPolicyDueCandidate {
135    pub server_uuid: uuid::Uuid,
136    pub node_uuid: uuid::Uuid,
137    pub last_attempt: Option<chrono::NaiveDateTime>,
138    /// The point from which the policy started covering this server, used as the cron anchor
139    /// when the server has no `last_attempt` yet.
140    pub coverage_start: chrono::NaiveDateTime,
141}
142
143impl SystemBackupPolicy {
144    pub async fn all_with_pagination(
145        database: &crate::database::Database,
146        page: i64,
147        per_page: i64,
148        search: Option<&str>,
149    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
150        let offset = (page - 1) * per_page;
151
152        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
153            r#"
154            SELECT {}, COUNT(*) OVER() AS total_count
155            FROM system_backup_policies
156            WHERE $1 IS NULL OR system_backup_policies.name ILIKE '%' || $1 || '%'
157            ORDER BY system_backup_policies.created
158            LIMIT $2 OFFSET $3
159            "#,
160            Self::columns_sql(None)
161        )))
162        .bind(search)
163        .bind(per_page)
164        .bind(offset)
165        .fetch_all(database.read())
166        .await?;
167
168        Ok(super::Pagination {
169            total: rows
170                .first()
171                .map_or(Ok(0), |row| row.try_get("total_count"))?,
172            per_page,
173            page,
174            data: rows
175                .into_iter()
176                .map(|row| Self::map(None, &row))
177                .try_collect_vec()?,
178        })
179    }
180
181    pub async fn all_schedulable(
182        database: &crate::database::Database,
183    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
184        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
185            r#"
186            SELECT {}
187            FROM system_backup_policies
188            WHERE system_backup_policies.enabled OR system_backup_policies.triggered IS NOT NULL
189            ORDER BY system_backup_policies.created
190            "#,
191            Self::columns_sql(None)
192        )))
193        .fetch_all(database.read())
194        .await?;
195
196        rows.into_iter()
197            .map(|row| Self::map(None, &row))
198            .try_collect_vec()
199    }
200
201    pub async fn trigger(
202        &mut self,
203        database: &crate::database::Database,
204    ) -> Result<(), sqlx::Error> {
205        let triggered = sqlx::query_scalar!(
206            "UPDATE system_backup_policies
207            SET triggered = NOW()
208            WHERE system_backup_policies.uuid = $1
209            RETURNING system_backup_policies.triggered",
210            self.uuid,
211        )
212        .fetch_one(database.write())
213        .await?;
214
215        self.triggered = triggered;
216
217        Ok(())
218    }
219
220    pub async fn clear_trigger(
221        &self,
222        database: &crate::database::Database,
223        triggered: chrono::NaiveDateTime,
224    ) -> Result<(), sqlx::Error> {
225        sqlx::query!(
226            "UPDATE system_backup_policies
227            SET triggered = NULL
228            WHERE system_backup_policies.uuid = $1 AND system_backup_policies.triggered = $2",
229            self.uuid,
230            triggered,
231        )
232        .execute(database.write())
233        .await?;
234
235        Ok(())
236    }
237
238    pub async fn due_candidates(
239        &self,
240        database: &crate::database::Database,
241        limit: i64,
242    ) -> Result<Vec<SystemBackupPolicyDueCandidate>, sqlx::Error> {
243        let rows = sqlx::query!(
244            r#"
245            SELECT
246                candidates.server_uuid AS server_uuid,
247                candidates.node_uuid AS node_uuid,
248                candidates.last_attempt AS "last_attempt?",
249                candidates.coverage_start AS "coverage_start!"
250            FROM (
251                SELECT
252                    servers.uuid AS server_uuid,
253                    servers.node_uuid AS node_uuid,
254                    (
255                        SELECT MAX(server_backups.created)
256                        FROM server_backups
257                        WHERE
258                            server_backups.server_uuid = servers.uuid
259                            AND server_backups.system_backup_policy_uuid = $1
260                    ) AS last_attempt,
261                    GREATEST(servers.created, coverage.attached) AS coverage_start
262                FROM servers
263                JOIN nodes ON nodes.uuid = servers.node_uuid
264                LEFT JOIN LATERAL (
265                    SELECT MIN(scopes.created) AS attached
266                    FROM (
267                        SELECT system_backup_policy_nodes.created
268                        FROM system_backup_policy_nodes
269                        WHERE
270                            system_backup_policy_nodes.system_backup_policy_uuid = $1
271                            AND system_backup_policy_nodes.node_uuid = servers.node_uuid
272                        UNION ALL
273                        SELECT system_backup_policy_locations.created
274                        FROM system_backup_policy_locations
275                        WHERE
276                            system_backup_policy_locations.system_backup_policy_uuid = $1
277                            AND system_backup_policy_locations.location_uuid = nodes.location_uuid
278                        UNION ALL
279                        SELECT system_backup_policy_servers.created
280                        FROM system_backup_policy_servers
281                        WHERE
282                            system_backup_policy_servers.system_backup_policy_uuid = $1
283                            AND system_backup_policy_servers.server_uuid = servers.uuid
284                    ) AS scopes
285                ) AS coverage ON TRUE
286                WHERE
287                    servers.destination_node_uuid IS NULL
288                    AND servers.status IS NULL
289                    AND coverage.attached IS NOT NULL
290            ) AS candidates
291            ORDER BY COALESCE(candidates.last_attempt, candidates.coverage_start) ASC
292            LIMIT $2
293            "#,
294            self.uuid,
295            limit,
296        )
297        .fetch_all(database.read())
298        .await?;
299
300        Ok(rows
301            .into_iter()
302            .map(|row| SystemBackupPolicyDueCandidate {
303                server_uuid: row.server_uuid,
304                node_uuid: row.node_uuid,
305                last_attempt: row.last_attempt,
306                coverage_start: row.coverage_start,
307            })
308            .collect())
309    }
310
311    /// (total_nodes, total_locations, total_servers, total_backups)
312    async fn attachment_counts(
313        &self,
314        database: &crate::database::Database,
315    ) -> Result<(i64, i64, i64, i64), crate::database::DatabaseError> {
316        let row = sqlx::query!(
317            r#"
318            SELECT
319                (
320                    SELECT COUNT(*)
321                    FROM system_backup_policy_nodes
322                    WHERE system_backup_policy_nodes.system_backup_policy_uuid = $1
323                ) AS "total_nodes!",
324                (
325                    SELECT COUNT(*)
326                    FROM system_backup_policy_locations
327                    WHERE system_backup_policy_locations.system_backup_policy_uuid = $1
328                ) AS "total_locations!",
329                (
330                    SELECT COUNT(*)
331                    FROM system_backup_policy_servers
332                    WHERE system_backup_policy_servers.system_backup_policy_uuid = $1
333                ) AS "total_servers!",
334                (
335                    SELECT COUNT(*)
336                    FROM server_backups
337                    WHERE
338                        server_backups.system_backup_policy_uuid = $1
339                        AND server_backups.deleted IS NULL
340                ) AS "total_backups!"
341            "#,
342            self.uuid,
343        )
344        .fetch_one(database.read())
345        .await?;
346
347        Ok((
348            row.total_nodes,
349            row.total_locations,
350            row.total_servers,
351            row.total_backups,
352        ))
353    }
354}
355
356#[async_trait::async_trait]
357impl IntoAdminApiObject for SystemBackupPolicy {
358    type AdminApiObject = AdminApiSystemBackupPolicy;
359    type ExtraArgs<'a> = ();
360
361    async fn into_admin_api_object<'a>(
362        self,
363        state: &crate::State,
364        _args: Self::ExtraArgs<'a>,
365    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
366        let (total_nodes, total_locations, total_servers, total_backups) =
367            self.attachment_counts(&state.database).await?;
368
369        let api_object = AdminApiSystemBackupPolicy::init_hooks(&self, state).await?;
370
371        let api_object = finish_extendible!(
372            AdminApiSystemBackupPolicy {
373                uuid: self.uuid,
374                backup_configuration: match self.backup_configuration {
375                    Some(backup_configuration) => Some(
376                        backup_configuration
377                            .fetch_cached(&state.database)
378                            .await?
379                            .into_admin_api_object(state, ())
380                            .await?,
381                    ),
382                    None => None,
383                },
384                name: self.name,
385                description: self.description,
386                enabled: self.enabled,
387                cron: self.cron,
388                retention_count: self.retention_count,
389                retention_days: self.retention_days,
390                parallelism: self.parallelism,
391                triggered: self.triggered.map(|dt| dt.and_utc()),
392                total_nodes,
393                total_locations,
394                total_servers,
395                total_backups,
396                created: self.created.and_utc(),
397            },
398            api_object,
399            state
400        )?;
401
402        Ok(api_object)
403    }
404}
405
406#[async_trait::async_trait]
407impl ByUuid for SystemBackupPolicy {
408    async fn by_uuid(
409        database: &crate::database::Database,
410        uuid: uuid::Uuid,
411    ) -> Result<Self, crate::database::DatabaseError> {
412        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
413            r#"
414            SELECT {}
415            FROM system_backup_policies
416            WHERE system_backup_policies.uuid = $1
417            "#,
418            Self::columns_sql(None)
419        )))
420        .bind(uuid)
421        .fetch_one(database.read())
422        .await?;
423
424        Self::map(None, &row)
425    }
426
427    async fn by_uuid_with_transaction(
428        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
429        uuid: uuid::Uuid,
430    ) -> Result<Self, crate::database::DatabaseError> {
431        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
432            r#"
433            SELECT {}
434            FROM system_backup_policies
435            WHERE system_backup_policies.uuid = $1
436            "#,
437            Self::columns_sql(None)
438        )))
439        .bind(uuid)
440        .fetch_one(&mut **transaction)
441        .await?;
442
443        Self::map(None, &row)
444    }
445}
446
447#[derive(ToSchema, Deserialize, Validate)]
448pub struct CreateSystemBackupPolicyOptions {
449    #[garde(length(chars, min = 1, max = 255))]
450    #[schema(min_length = 1, max_length = 255)]
451    pub name: compact_str::CompactString,
452    #[garde(length(chars, min = 1, max = 1024))]
453    #[schema(min_length = 1, max_length = 1024)]
454    pub description: Option<compact_str::CompactString>,
455    #[garde(skip)]
456    pub backup_configuration_uuid: Option<uuid::Uuid>,
457    #[garde(skip)]
458    pub enabled: bool,
459    #[garde(skip)]
460    #[schema(value_type = String, example = "0 0 0 * * *")]
461    pub cron: croner::Cron,
462    #[garde(inner(range(min = 1)))]
463    #[schema(minimum = 1)]
464    pub retention_count: Option<i32>,
465    #[garde(inner(range(min = 1)))]
466    #[schema(minimum = 1)]
467    pub retention_days: Option<i32>,
468    #[garde(range(min = 1, max = 100))]
469    #[schema(minimum = 1, maximum = 100)]
470    pub parallelism: i32,
471}
472
473#[async_trait::async_trait]
474impl CreatableModel for SystemBackupPolicy {
475    type CreateOptions<'a> = CreateSystemBackupPolicyOptions;
476    type CreateResult = Self;
477
478    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
479        static CREATE_LISTENERS: LazyLock<CreateListenerList<SystemBackupPolicy>> =
480            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
481
482        &CREATE_LISTENERS
483    }
484
485    async fn create_with_transaction(
486        state: &crate::State,
487        mut options: Self::CreateOptions<'_>,
488        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
489    ) -> Result<Self, crate::database::DatabaseError> {
490        options.validate()?;
491
492        if let Some(backup_configuration_uuid) = options.backup_configuration_uuid {
493            super::backup_configuration::BackupConfiguration::by_uuid_optional_cached(
494                &state.database,
495                backup_configuration_uuid,
496            )
497            .await?
498            .ok_or(crate::database::InvalidRelationError(
499                "backup_configuration",
500            ))?;
501        }
502
503        let mut query_builder = InsertQueryBuilder::new("system_backup_policies");
504
505        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
506
507        query_builder
508            .set("name", &options.name)
509            .set("description", &options.description)
510            .set(
511                "backup_configuration_uuid",
512                options.backup_configuration_uuid,
513            )
514            .set("enabled", options.enabled)
515            .set("cron", options.cron.to_compact_string())
516            .set("retention_count", options.retention_count)
517            .set("retention_days", options.retention_days)
518            .set("parallelism", options.parallelism);
519
520        let row = query_builder
521            .returning(&Self::columns_sql(None))
522            .fetch_one(&mut **transaction)
523            .await?;
524        let mut policy = Self::map(None, &row)?;
525
526        Self::run_after_create_handlers(&mut policy, &options, state, transaction).await?;
527
528        Ok(policy)
529    }
530}
531
532#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
533pub struct UpdateSystemBackupPolicyOptions {
534    #[garde(length(chars, min = 1, max = 255))]
535    #[schema(min_length = 1, max_length = 255)]
536    pub name: Option<compact_str::CompactString>,
537    #[garde(length(chars, min = 1, max = 1024))]
538    #[schema(min_length = 1, max_length = 1024)]
539    #[serde(
540        default,
541        skip_serializing_if = "Option::is_none",
542        with = "::serde_with::rust::double_option"
543    )]
544    pub description: Option<Option<compact_str::CompactString>>,
545    #[garde(skip)]
546    #[serde(
547        default,
548        skip_serializing_if = "Option::is_none",
549        with = "::serde_with::rust::double_option"
550    )]
551    pub backup_configuration_uuid: Option<Option<uuid::Uuid>>,
552    #[garde(skip)]
553    pub enabled: Option<bool>,
554    #[garde(skip)]
555    #[schema(value_type = Option<String>, example = "0 0 0 * * *")]
556    pub cron: Option<croner::Cron>,
557    #[garde(inner(range(min = 1)))]
558    #[schema(minimum = 1)]
559    #[serde(
560        default,
561        skip_serializing_if = "Option::is_none",
562        with = "::serde_with::rust::double_option"
563    )]
564    pub retention_count: Option<Option<i32>>,
565    #[garde(inner(range(min = 1)))]
566    #[schema(minimum = 1)]
567    #[serde(
568        default,
569        skip_serializing_if = "Option::is_none",
570        with = "::serde_with::rust::double_option"
571    )]
572    pub retention_days: Option<Option<i32>>,
573    #[garde(inner(range(min = 1, max = 100)))]
574    #[schema(minimum = 1, maximum = 100)]
575    pub parallelism: Option<i32>,
576}
577
578#[async_trait::async_trait]
579impl UpdatableModel for SystemBackupPolicy {
580    type UpdateOptions = UpdateSystemBackupPolicyOptions;
581
582    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
583        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<SystemBackupPolicy>> =
584            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
585
586        &UPDATE_LISTENERS
587    }
588
589    async fn update_with_transaction(
590        &mut self,
591        state: &crate::State,
592        mut options: Self::UpdateOptions,
593        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
594    ) -> Result<(), crate::database::DatabaseError> {
595        options.validate()?;
596
597        if let Some(Some(backup_configuration_uuid)) = options.backup_configuration_uuid {
598            super::backup_configuration::BackupConfiguration::by_uuid_optional_cached(
599                &state.database,
600                backup_configuration_uuid,
601            )
602            .await?
603            .ok_or(crate::database::InvalidRelationError(
604                "backup_configuration",
605            ))?;
606        }
607
608        let mut query_builder = UpdateQueryBuilder::new("system_backup_policies");
609
610        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
611            .await?;
612
613        query_builder
614            .set("name", options.name.as_ref())
615            .set(
616                "description",
617                options.description.as_ref().map(|d| d.as_ref()),
618            )
619            .set(
620                "backup_configuration_uuid",
621                options.backup_configuration_uuid,
622            )
623            .set("enabled", options.enabled)
624            .set(
625                "cron",
626                options.cron.as_ref().map(|cron| cron.to_compact_string()),
627            )
628            .set("retention_count", options.retention_count)
629            .set("retention_days", options.retention_days)
630            .set("parallelism", options.parallelism)
631            .where_eq("uuid", self.uuid);
632
633        query_builder.execute(&mut **transaction).await?;
634
635        if let Some(name) = options.name {
636            self.name = name;
637        }
638        if let Some(description) = options.description {
639            self.description = description;
640        }
641        if let Some(backup_configuration_uuid) = options.backup_configuration_uuid {
642            self.backup_configuration = backup_configuration_uuid
643                .map(super::backup_configuration::BackupConfiguration::get_fetchable);
644        }
645        if let Some(enabled) = options.enabled {
646            self.enabled = enabled;
647        }
648        if let Some(cron) = options.cron {
649            self.cron = cron;
650        }
651        if let Some(retention_count) = options.retention_count {
652            self.retention_count = retention_count;
653        }
654        if let Some(retention_days) = options.retention_days {
655            self.retention_days = retention_days;
656        }
657        if let Some(parallelism) = options.parallelism {
658            self.parallelism = parallelism;
659        }
660
661        self.run_after_update_handlers(state, transaction).await?;
662
663        Ok(())
664    }
665}
666
667#[async_trait::async_trait]
668impl DeletableModel for SystemBackupPolicy {
669    type DeleteOptions = ();
670
671    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
672        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<SystemBackupPolicy>> =
673            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
674
675        &DELETE_LISTENERS
676    }
677
678    async fn delete_with_transaction(
679        &self,
680        state: &crate::State,
681        options: Self::DeleteOptions,
682        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
683    ) -> Result<(), anyhow::Error> {
684        self.run_delete_handlers(&options, state, transaction)
685            .await?;
686
687        sqlx::query!(
688            "DELETE FROM system_backup_policies
689            WHERE system_backup_policies.uuid = $1",
690            self.uuid,
691        )
692        .execute(&mut **transaction)
693        .await?;
694
695        self.run_after_delete_handlers(&options, state, transaction)
696            .await?;
697
698        Ok(())
699    }
700}
701
702#[schema_extension_derive::extendible]
703#[init_args(SystemBackupPolicy, crate::State)]
704#[hook_args(crate::State)]
705#[derive(ToSchema, Serialize)]
706#[schema(title = "SystemBackupPolicy")]
707pub struct AdminApiSystemBackupPolicy {
708    pub uuid: uuid::Uuid,
709    pub backup_configuration: Option<super::backup_configuration::AdminApiBackupConfiguration>,
710
711    pub name: compact_str::CompactString,
712    pub description: Option<compact_str::CompactString>,
713
714    pub enabled: bool,
715    #[schema(value_type = String, example = "0 0 0 * * *")]
716    pub cron: croner::Cron,
717    pub retention_count: Option<i32>,
718    pub retention_days: Option<i32>,
719    pub parallelism: i32,
720
721    pub triggered: Option<chrono::DateTime<chrono::Utc>>,
722
723    pub total_nodes: i64,
724    pub total_locations: i64,
725    pub total_servers: i64,
726    pub total_backups: i64,
727
728    pub created: chrono::DateTime<chrono::Utc>,
729}