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 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 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}