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