Skip to main content

shared/models/
system_backup_policy.rs

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