1use garde::Validate;
2use serde::{Deserialize, Serialize};
3use utoipa::ToSchema;
4
5#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
6#[serde(transparent)]
7pub struct Config(pub serde_json::Value);
8
9impl Config {
10 pub fn view<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
11 T::deserialize(&self.0)
12 }
13}
14
15#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
16#[serde(tag = "type", rename_all = "snake_case")]
17pub enum QueryValue {
18 Null,
19 Text { value: String, truncated: bool },
20 Binary { value: String, truncated: bool },
21}
22
23#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
24#[serde(tag = "type", rename_all = "snake_case")]
25#[non_exhaustive]
26pub enum ServerSelector {
27 Uuids {
28 uuids: std::collections::HashSet<uuid::Uuid>,
29 },
30 All,
31}
32
33#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
34#[serde(tag = "adapter")]
35#[non_exhaustive]
36pub enum BackupTestTarget {
37 #[serde(rename = "wings")]
38 Wings,
39 #[serde(rename = "s3")]
40 S3 {
41 configuration: super::S3TestConfiguration,
42 },
43 #[serde(rename = "ddup-bak")]
44 DdupBak,
45 #[serde(rename = "btrfs")]
46 Btrfs,
47 #[serde(rename = "zfs")]
48 Zfs,
49 #[serde(rename = "restic")]
50 Restic {
51 configuration: Option<super::ResticBackupConfiguration>,
52 },
53 #[serde(rename = "proxmox-backup-server")]
54 ProxmoxBackupServer {
55 configuration: super::PbsRepositoryConfiguration,
56 },
57 #[serde(rename = "kopia")]
58 Kopia {
59 configuration: super::KopiaBackupConfiguration,
60 },
61}
62
63impl std::fmt::Display for super::StreamableArchiveFormat {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 write!(
66 f,
67 "{}",
68 match self {
69 super::StreamableArchiveFormat::Tar => "tar",
70 super::StreamableArchiveFormat::TarGz => "tar_gz",
71 super::StreamableArchiveFormat::TarXz => "tar_xz",
72 super::StreamableArchiveFormat::TarLzip => "tar_lzip",
73 super::StreamableArchiveFormat::TarBz2 => "tar_bz2",
74 super::StreamableArchiveFormat::TarLz4 => "tar_lz4",
75 super::StreamableArchiveFormat::TarZstd => "tar_zstd",
76 super::StreamableArchiveFormat::Itaf => "itaf",
77 super::StreamableArchiveFormat::ItafGz => "itaf_gz",
78 super::StreamableArchiveFormat::ItafXz => "itaf_xz",
79 super::StreamableArchiveFormat::ItafLzip => "itaf_lzip",
80 super::StreamableArchiveFormat::ItafBz2 => "itaf_bz2",
81 super::StreamableArchiveFormat::ItafLz4 => "itaf_lz4",
82 super::StreamableArchiveFormat::ItafZstd => "itaf_zstd",
83 super::StreamableArchiveFormat::Zip => "zip",
84 }
85 )
86 }
87}
88
89#[allow(clippy::derivable_impls)]
90impl Default for super::StreamableArchiveFormat {
91 #[inline]
92 fn default() -> Self {
93 super::StreamableArchiveFormat::TarGz
94 }
95}
96
97impl std::fmt::Display for super::BackupAdapter {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 write!(
100 f,
101 "{}",
102 match self {
103 super::BackupAdapter::Wings => "wings",
104 super::BackupAdapter::S3 => "s3",
105 super::BackupAdapter::DdupBak => "ddup-bak",
106 super::BackupAdapter::Btrfs => "btrfs",
107 super::BackupAdapter::Zfs => "zfs",
108 super::BackupAdapter::Restic => "restic",
109 super::BackupAdapter::ProxmoxBackupServer => "proxmox-backup-server",
110 super::BackupAdapter::Kopia => "kopia",
111 }
112 )
113 }
114}
115
116#[derive(Debug, ToSchema, Deserialize, Serialize, Clone, Copy)]
117#[serde(rename_all = "lowercase")]
118#[non_exhaustive]
119pub enum Algorithm {
120 Md5,
121 Crc32,
122 Sha1,
123 Sha224,
124 Sha256,
125 Sha384,
126 Sha512,
127 Curseforge,
128}
129
130impl std::fmt::Display for Algorithm {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 write!(
133 f,
134 "{}",
135 match self {
136 Algorithm::Md5 => "md5",
137 Algorithm::Crc32 => "crc32",
138 Algorithm::Sha1 => "sha1",
139 Algorithm::Sha224 => "sha224",
140 Algorithm::Sha256 => "sha256",
141 Algorithm::Sha384 => "sha384",
142 Algorithm::Sha512 => "sha512",
143 Algorithm::Curseforge => "curseforge",
144 }
145 )
146 }
147}
148
149#[derive(Debug, Default, ToSchema, Deserialize, Serialize, Clone, Copy)]
150#[serde(rename_all = "snake_case")]
151#[non_exhaustive]
152pub enum DirectorySortingMode {
153 #[default]
154 NameAsc,
155 NameDesc,
156 SizeAsc,
157 SizeDesc,
158 PhysicalSizeAsc,
159 PhysicalSizeDesc,
160 ModifiedAsc,
161 ModifiedDesc,
162 CreatedAsc,
163 CreatedDesc,
164}
165
166impl std::fmt::Display for DirectorySortingMode {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 write!(
169 f,
170 "{}",
171 match self {
172 DirectorySortingMode::NameAsc => "name_asc",
173 DirectorySortingMode::NameDesc => "name_desc",
174 DirectorySortingMode::SizeAsc => "size_asc",
175 DirectorySortingMode::SizeDesc => "size_desc",
176 DirectorySortingMode::PhysicalSizeAsc => "physical_size_asc",
177 DirectorySortingMode::PhysicalSizeDesc => "physical_size_desc",
178 DirectorySortingMode::ModifiedAsc => "modified_asc",
179 DirectorySortingMode::ModifiedDesc => "modified_desc",
180 DirectorySortingMode::CreatedAsc => "created_asc",
181 DirectorySortingMode::CreatedDesc => "created_desc",
182 }
183 )
184 }
185}
186
187#[derive(ToSchema, Debug, Clone, Copy, Deserialize, Serialize)]
188#[serde(rename_all = "lowercase")]
189pub enum ServerBackupStatus {
190 Starting,
191 Finished,
192 Failed,
193}
194
195#[derive(Debug, ToSchema, Deserialize, Serialize, Clone, Copy)]
196#[serde(rename_all = "snake_case")]
197#[non_exhaustive]
198pub enum Game {
199 MinecraftJava,
200}
201
202impl std::fmt::Display for Game {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 write!(
205 f,
206 "{}",
207 match self {
208 Game::MinecraftJava => "minecraft_java",
209 }
210 )
211 }
212}
213
214#[derive(ToSchema, Clone, Deserialize, Serialize, Validate)]
215pub struct ScheduleVariable {
216 #[garde(length(chars, min = 1, max = 255))]
217 #[schema(min_length = 1, max_length = 255)]
218 pub variable: compact_str::CompactString,
219}
220
221#[derive(ToSchema, Clone, Deserialize, Serialize, Validate)]
222#[serde(untagged)]
223pub enum ScheduleDynamicParameter {
224 Raw(#[garde(length(chars, min = 1, max = 16384))] compact_str::CompactString),
225 Variable(#[garde(dive)] ScheduleVariable),
226}
227
228#[derive(ToSchema, Deserialize, Serialize, Clone, Copy)]
229#[serde(rename_all = "snake_case")]
230pub enum ScheduleHttpMethod {
231 Get,
232 Post,
233 Put,
234 Patch,
235 Delete,
236 Head,
237}
238
239#[derive(ToSchema, Clone, Deserialize, Serialize, Validate)]
240pub struct ScheduleHttpHeader {
241 #[garde(length(chars, min = 1, max = 128))]
242 #[schema(min_length = 1, max_length = 128)]
243 pub name: compact_str::CompactString,
244 #[garde(dive)]
245 pub value: ScheduleDynamicParameter,
246}
247
248#[derive(ToSchema, Deserialize, Serialize)]
249pub struct ScheduleAction {
250 pub uuid: uuid::Uuid,
251
252 #[serde(flatten)]
253 pub inner: ScheduleActionInner,
254}
255
256#[derive(ToSchema, Validate, Deserialize, Serialize, Clone)]
257#[serde(rename_all = "snake_case", tag = "mode")]
258#[non_exhaustive]
259pub enum ScheduleBackupSelector {
260 Latest {
261 #[garde(skip)]
262 #[serde(default)]
263 backup_group_uuid: Option<uuid::Uuid>,
264 },
265 Oldest {
266 #[garde(skip)]
267 #[serde(default)]
268 backup_group_uuid: Option<uuid::Uuid>,
269 },
270 Uuid {
271 #[garde(dive)]
272 uuid: ScheduleDynamicParameter,
273 },
274 Name {
275 #[garde(dive)]
276 name: ScheduleDynamicParameter,
277 #[garde(skip)]
278 #[serde(default)]
279 backup_group_uuid: Option<uuid::Uuid>,
280 #[garde(skip)]
281 #[serde(default)]
282 oldest: bool,
283 },
284}
285
286#[derive(ToSchema, Validate, Deserialize, Serialize, Clone)]
287#[serde(rename_all = "snake_case", tag = "type")]
288#[non_exhaustive]
289pub enum ScheduleActionInner {
290 Sleep {
291 #[garde(range(min = 1, max = 24 * 60 * 60 * 1000))]
292 #[schema(minimum = 1, maximum = 86400000)]
293 duration: u64,
294 },
295 Ensure {
296 #[garde(dive, custom(ScheduleCondition::validate_nesting))]
297 condition: ScheduleCondition,
298 },
299 If {
300 #[garde(dive, custom(ScheduleCondition::validate_nesting))]
301 condition: ScheduleCondition,
302 },
303 ElseIf {
304 #[garde(dive, custom(ScheduleCondition::validate_nesting))]
305 condition: ScheduleCondition,
306 },
307 Else,
308 EndIf,
309 Exit {
310 #[garde(skip)]
311 successful: bool,
312 },
313 WaitForState {
314 #[garde(skip)]
315 ignore_failure: bool,
316
317 #[garde(skip)]
318 state: super::ServerState,
319 #[garde(range(min = 1, max = 24 * 60 * 60 * 1000))]
320 #[schema(minimum = 1, maximum = 86400000)]
321 timeout: u64,
322 },
323 Format {
324 #[garde(length(chars, min = 1, max = 16384))]
325 #[schema(min_length = 1, max_length = 16384)]
326 format: String,
327 #[garde(dive)]
328 output_into: ScheduleVariable,
329 },
330 MatchRegex {
331 #[garde(dive)]
332 input: ScheduleDynamicParameter,
333
334 #[garde(skip)]
335 #[serde(with = "serde_regex")]
336 #[schema(value_type = String, format = "regex")]
337 regex: regex::Regex,
338
339 #[garde(skip)]
340 output_into: Vec<Option<ScheduleVariable>>,
341 },
342 WaitForConsoleLine {
343 #[garde(skip)]
344 ignore_failure: bool,
345
346 #[garde(dive)]
347 contains: ScheduleDynamicParameter,
348 #[garde(skip)]
349 #[serde(default)]
350 case_insensitive: bool,
351 #[garde(range(min = 1, max = 24 * 60 * 60 * 1000))]
352 #[schema(minimum = 1, maximum = 86400000)]
353 timeout: u64,
354
355 #[garde(dive)]
356 output_into: Option<ScheduleVariable>,
357 },
358 SendPower {
359 #[garde(skip)]
360 ignore_failure: bool,
361
362 #[garde(skip)]
363 action: super::ServerPowerAction,
364 },
365 SendCommand {
366 #[garde(skip)]
367 ignore_failure: bool,
368
369 #[garde(dive)]
370 command: ScheduleDynamicParameter,
371 },
372 CreateBackup {
373 #[garde(skip)]
374 ignore_failure: bool,
375 #[garde(skip)]
376 foreground: bool,
377
378 #[garde(dive)]
379 name: Option<ScheduleDynamicParameter>,
380 #[garde(skip)]
381 #[serde(default)]
382 backup_group_uuid: Option<uuid::Uuid>,
383 #[garde(skip)]
384 ignored_files: Vec<compact_str::CompactString>,
385
386 #[garde(dive)]
387 #[serde(default)]
388 output_into: Option<ScheduleVariable>,
389 },
390 RestoreBackup {
391 #[garde(skip)]
392 ignore_failure: bool,
393 #[garde(skip)]
394 truncate_directory: bool,
395 #[garde(skip)]
396 #[serde(default)]
397 restore_startup: bool,
398
399 #[garde(dive)]
400 backup: ScheduleBackupSelector,
401 },
402 DeleteBackup {
403 #[garde(skip)]
404 #[serde(default)]
405 ignore_failure: bool,
406
407 #[garde(dive)]
408 backup: ScheduleBackupSelector,
409 },
410 MoveBackup {
411 #[garde(skip)]
412 #[serde(default)]
413 ignore_failure: bool,
414
415 #[garde(dive)]
416 backup: ScheduleBackupSelector,
417 #[garde(skip)]
418 #[serde(default)]
419 backup_group_uuid: Option<uuid::Uuid>,
420 },
421 CreateDatabaseBackup {
422 #[garde(skip)]
423 ignore_failure: bool,
424 #[garde(skip)]
425 foreground: bool,
426
427 #[garde(dive)]
428 name: Option<ScheduleDynamicParameter>,
429 #[garde(skip)]
430 database_instance_uuid: uuid::Uuid,
431 #[garde(skip)]
432 #[serde(default)]
433 backup_group_uuid: Option<uuid::Uuid>,
434
435 #[garde(dive)]
436 #[serde(default)]
437 output_into: Option<ScheduleVariable>,
438 },
439 DeleteDatabaseBackup {
440 #[garde(skip)]
441 #[serde(default)]
442 ignore_failure: bool,
443
444 #[garde(dive)]
445 backup: ScheduleBackupSelector,
446 #[garde(skip)]
447 #[serde(default)]
448 database_instance_uuid: Option<uuid::Uuid>,
449 },
450 MoveDatabaseBackup {
451 #[garde(skip)]
452 #[serde(default)]
453 ignore_failure: bool,
454
455 #[garde(dive)]
456 backup: ScheduleBackupSelector,
457 #[garde(skip)]
458 #[serde(default)]
459 database_instance_uuid: Option<uuid::Uuid>,
460 #[garde(skip)]
461 #[serde(default)]
462 backup_group_uuid: Option<uuid::Uuid>,
463 },
464 RestoreDatabaseBackup {
465 #[garde(skip)]
466 #[serde(default)]
467 ignore_failure: bool,
468
469 #[garde(dive)]
470 backup: ScheduleBackupSelector,
471 #[garde(skip)]
472 #[serde(default)]
473 source_database_instance_uuid: Option<uuid::Uuid>,
474 #[garde(skip)]
475 #[serde(default)]
476 database_instance_uuid: Option<uuid::Uuid>,
477 },
478 CreateDirectory {
479 #[garde(skip)]
480 ignore_failure: bool,
481
482 #[garde(dive)]
483 root: ScheduleDynamicParameter,
484 #[garde(dive)]
485 name: ScheduleDynamicParameter,
486 },
487 WriteFile {
488 #[garde(skip)]
489 ignore_failure: bool,
490 #[garde(skip)]
491 append: bool,
492
493 #[garde(dive)]
494 file: ScheduleDynamicParameter,
495 #[garde(dive)]
496 content: ScheduleDynamicParameter,
497 },
498 CopyFile {
499 #[garde(skip)]
500 ignore_failure: bool,
501 #[garde(skip)]
502 foreground: bool,
503
504 #[garde(dive)]
505 file: ScheduleDynamicParameter,
506 #[garde(dive)]
507 destination: ScheduleDynamicParameter,
508 },
509 DeleteFiles {
510 #[garde(skip)]
511 #[serde(default)]
512 ignore_failure: bool,
513 #[garde(skip)]
514 #[serde(default)]
515 trash: bool,
516
517 #[garde(dive)]
518 root: ScheduleDynamicParameter,
519 #[garde(skip)]
520 files: Vec<compact_str::CompactString>,
521 },
522 RenameFiles {
523 #[garde(skip)]
524 #[serde(default)]
525 ignore_failure: bool,
526
527 #[garde(dive)]
528 root: ScheduleDynamicParameter,
529 #[garde(skip)]
530 #[schema(inline)]
531 files: Vec<super::servers_server_files_rename::put::RequestBodyFiles>,
532 },
533 CompressFiles {
534 #[garde(skip)]
535 ignore_failure: bool,
536 #[garde(skip)]
537 foreground: bool,
538
539 #[garde(dive)]
540 root: ScheduleDynamicParameter,
541 #[garde(skip)]
542 files: Vec<compact_str::CompactString>,
543 #[garde(skip)]
544 format: super::ArchiveFormat,
545 #[garde(dive)]
546 name: ScheduleDynamicParameter,
547 },
548 DecompressFile {
549 #[garde(skip)]
550 ignore_failure: bool,
551 #[garde(skip)]
552 foreground: bool,
553
554 #[garde(dive)]
555 root: ScheduleDynamicParameter,
556 #[garde(dive)]
557 file: ScheduleDynamicParameter,
558 },
559 PullFile {
560 #[garde(skip)]
561 ignore_failure: bool,
562 #[garde(skip)]
563 foreground: bool,
564
565 #[garde(dive)]
566 root: ScheduleDynamicParameter,
567 #[garde(dive)]
568 url: ScheduleDynamicParameter,
569 #[garde(dive)]
570 #[serde(default)]
571 file_name: Option<ScheduleDynamicParameter>,
572 #[garde(skip)]
573 #[serde(default)]
574 use_header: bool,
575 },
576 UpdateStartupVariable {
577 #[garde(skip)]
578 ignore_failure: bool,
579
580 #[garde(dive)]
581 env_variable: ScheduleDynamicParameter,
582 #[garde(dive)]
583 value: ScheduleDynamicParameter,
584 },
585 UpdateStartupCommand {
586 #[garde(skip)]
587 ignore_failure: bool,
588
589 #[garde(dive)]
590 command: ScheduleDynamicParameter,
591 },
592 UpdateStartupDockerImage {
593 #[garde(skip)]
594 ignore_failure: bool,
595
596 #[garde(dive)]
597 image: ScheduleDynamicParameter,
598 },
599 HttpRequest {
600 #[garde(skip)]
601 ignore_failure: bool,
602
603 #[garde(skip)]
604 method: ScheduleHttpMethod,
605 #[garde(skip)]
606 #[schema(value_type = String, format = "uri")]
607 url: reqwest::Url,
608 #[garde(length(max = 32), dive)]
609 #[serde(default)]
610 headers: Vec<ScheduleHttpHeader>,
611 #[garde(dive)]
612 #[serde(default)]
613 body: Option<ScheduleDynamicParameter>,
614 #[garde(range(min = 1, max = 60 * 1000))]
615 #[schema(minimum = 1, maximum = 60000)]
616 timeout: u64,
617 #[garde(skip)]
618 #[serde(default)]
619 ignore_error_status: bool,
620
621 #[garde(dive)]
622 #[serde(default)]
623 output_status_into: Option<ScheduleVariable>,
624 #[garde(dive)]
625 #[serde(default)]
626 output_body_into: Option<ScheduleVariable>,
627 },
628}
629
630impl ScheduleActionInner {
631 pub fn permissions(&self) -> &'static [&'static str] {
632 match self {
633 ScheduleActionInner::Sleep { .. } => &[],
634 ScheduleActionInner::Ensure { .. } => &[],
635 ScheduleActionInner::If { .. } => &[],
636 ScheduleActionInner::ElseIf { .. } => &[],
637 ScheduleActionInner::Else => &[],
638 ScheduleActionInner::EndIf => &[],
639 ScheduleActionInner::Exit { .. } => &[],
640 ScheduleActionInner::WaitForState { .. } => &[],
641 ScheduleActionInner::Format { .. } => &[],
642 ScheduleActionInner::MatchRegex { .. } => &[],
643 ScheduleActionInner::WaitForConsoleLine { .. } => &["control.read-console"],
644 ScheduleActionInner::SendPower { action, .. } => match action {
645 super::ServerPowerAction::Start => &["control.start"],
646 super::ServerPowerAction::Stop => &["control.stop"],
647 super::ServerPowerAction::Restart => &["control.restart"],
648 super::ServerPowerAction::Kill => &["control.stop"],
649 },
650 ScheduleActionInner::SendCommand { .. } => &["control.console"],
651 ScheduleActionInner::CreateBackup { .. } => &["backups.create"],
652 ScheduleActionInner::RestoreBackup { .. } => &["backups.restore"],
653 ScheduleActionInner::DeleteBackup { .. } => &["backups.delete"],
654 ScheduleActionInner::MoveBackup { .. } => &["backups.update"],
655 ScheduleActionInner::CreateDatabaseBackup { .. } => {
656 &["backups.create", "database-instances.read"]
657 }
658 ScheduleActionInner::DeleteDatabaseBackup { .. } => {
659 &["backups.delete", "database-instances.read"]
660 }
661 ScheduleActionInner::MoveDatabaseBackup { .. } => {
662 &["backups.update", "database-instances.read"]
663 }
664 ScheduleActionInner::RestoreDatabaseBackup { .. } => {
665 &["backups.restore", "database-instances.read"]
666 }
667 ScheduleActionInner::CreateDirectory { .. } => &["files.create"],
668 ScheduleActionInner::WriteFile { .. } => &["files.update"],
669 ScheduleActionInner::CopyFile { .. } => &["files.update"],
670 ScheduleActionInner::DeleteFiles { .. } => &["files.delete"],
671 ScheduleActionInner::RenameFiles { .. } => &["files.update"],
672 ScheduleActionInner::CompressFiles { .. } => &["files.archive"],
673 ScheduleActionInner::DecompressFile { .. } => &["files.archive"],
674 ScheduleActionInner::PullFile { .. } => &["files.create"],
675 ScheduleActionInner::UpdateStartupVariable { .. } => &["startup.update"],
676 ScheduleActionInner::UpdateStartupCommand { .. } => &["startup.command"],
677 ScheduleActionInner::UpdateStartupDockerImage { .. } => &["startup.docker-image"],
678 ScheduleActionInner::HttpRequest { .. } => &[],
679 }
680 }
681}
682
683#[derive(ToSchema, Validate, Deserialize, Serialize, Clone)]
684#[serde(rename_all = "snake_case", tag = "type")]
685#[non_exhaustive]
686pub enum ScheduleTrigger {
687 Cron {
688 #[garde(skip)]
689 #[schema(value_type = String, example = "* * * * * *")]
690 schedule: Box<croner::Cron>,
691 },
692 PowerAction {
693 #[garde(skip)]
694 action: super::ServerPowerAction,
695 },
696 ServerState {
697 #[garde(skip)]
698 state: super::ServerState,
699 },
700 BackupStatus {
701 #[garde(skip)]
702 status: ServerBackupStatus,
703 },
704 DatabaseBackupStatus {
705 #[garde(skip)]
706 status: ServerBackupStatus,
707 },
708 ScheduleCompletion {
709 #[garde(skip)]
710 schedule: uuid::Uuid,
711 #[garde(skip)]
712 successful: bool,
713 },
714 ResourceUsage {
715 #[garde(skip)]
716 metric: ScheduleResourceMetric,
717 #[garde(skip)]
718 comparator: ScheduleConditionComparator,
719 #[garde(range(min = 0.0))]
720 value: f64,
721 #[garde(range(min = 0, max = 24 * 60 * 60))]
722 #[schema(minimum = 0, maximum = 86400)]
723 #[serde(default)]
724 for_seconds: u64,
725 },
726 ConsoleLine {
727 #[garde(length(chars, min = 1, max = 1024))]
728 #[schema(min_length = 1, max_length = 1024)]
729 contains: compact_str::CompactString,
730 #[garde(skip)]
731 #[serde(default)]
732 case_insensitive: bool,
733 #[garde(dive)]
734 output_into: Option<ScheduleVariable>,
735 },
736 Crash,
737}
738
739impl ScheduleTrigger {
740 pub fn permissions(&self) -> &'static [&'static str] {
741 match self {
742 ScheduleTrigger::Cron { .. } => &[],
743 ScheduleTrigger::PowerAction { .. } => &[],
744 ScheduleTrigger::ServerState { .. } => &[],
745 ScheduleTrigger::BackupStatus { .. } => &[],
746 ScheduleTrigger::DatabaseBackupStatus { .. } => &[],
747 ScheduleTrigger::ScheduleCompletion { .. } => &[],
748 ScheduleTrigger::ResourceUsage { .. } => &[],
749 ScheduleTrigger::ConsoleLine { .. } => &["control.read-console"],
750 ScheduleTrigger::Crash => &[],
751 }
752 }
753}
754
755#[derive(ToSchema, Deserialize, Serialize, Clone, Copy)]
756#[serde(rename_all = "snake_case")]
757pub enum ScheduleConditionComparator {
758 SmallerThan,
759 SmallerThanOrEquals,
760 Equal,
761 GreaterThan,
762 GreaterThanOrEquals,
763}
764
765#[derive(ToSchema, Deserialize, Serialize, Clone, Copy)]
766#[serde(rename_all = "snake_case")]
767pub enum ScheduleResourceMetric {
768 Cpu,
769 Memory,
770 Disk,
771}
772
773#[derive(ToSchema, Deserialize, Serialize, Clone, Validate)]
774#[serde(rename_all = "snake_case", tag = "type")]
775#[schema(no_recursion)]
776pub enum ScheduleCondition {
777 None,
778 And {
779 #[garde(dive)]
780 conditions: Vec<ScheduleCondition>,
781 },
782 Or {
783 #[garde(dive)]
784 conditions: Vec<ScheduleCondition>,
785 },
786 Not {
787 #[garde(dive)]
788 condition: Box<ScheduleCondition>,
789 },
790 ServerState {
791 #[garde(skip)]
792 state: super::ServerState,
793 },
794 Uptime {
795 #[garde(skip)]
796 comparator: ScheduleConditionComparator,
797 #[garde(skip)]
798 value: u64,
799 },
800 ResourceUsage {
801 #[garde(skip)]
802 metric: ScheduleResourceMetric,
803 #[garde(skip)]
804 comparator: ScheduleConditionComparator,
805 #[garde(range(min = 0.0))]
806 value: f64,
807 },
808 FileExists {
809 #[garde(length(chars, min = 1, max = 255))]
810 #[schema(min_length = 1, max_length = 255)]
811 file: String,
812 },
813 VariableExists {
814 #[garde(dive)]
815 variable: ScheduleVariable,
816 },
817 VariableEquals {
818 #[garde(dive)]
819 variable: ScheduleVariable,
820 #[garde(dive)]
821 equals: ScheduleDynamicParameter,
822 },
823 VariableContains {
824 #[garde(dive)]
825 variable: ScheduleVariable,
826 #[garde(dive)]
827 contains: ScheduleDynamicParameter,
828 },
829 VariableStartsWith {
830 #[garde(dive)]
831 variable: ScheduleVariable,
832 #[garde(dive)]
833 starts_with: ScheduleDynamicParameter,
834 },
835 VariableEndsWith {
836 #[garde(dive)]
837 variable: ScheduleVariable,
838 #[garde(dive)]
839 ends_with: ScheduleDynamicParameter,
840 },
841}
842
843impl ScheduleCondition {
844 pub const MAX_NESTING_DEPTH: usize = 3;
845
846 fn nested_within_limit(&self, depth: usize) -> bool {
847 match self {
848 ScheduleCondition::And { conditions } | ScheduleCondition::Or { conditions } => {
849 depth < Self::MAX_NESTING_DEPTH
850 && conditions.iter().all(|c| c.nested_within_limit(depth + 1))
851 }
852 ScheduleCondition::Not { condition } => {
853 depth < Self::MAX_NESTING_DEPTH && condition.nested_within_limit(depth + 1)
854 }
855 _ => true,
856 }
857 }
858
859 pub fn validate_nesting(value: &Self, _context: &()) -> garde::Result {
860 if value.nested_within_limit(0) {
861 Ok(())
862 } else {
863 Err(garde::Error::new(format!(
864 "condition may not nest groups more than {} levels deep",
865 Self::MAX_NESTING_DEPTH
866 )))
867 }
868 }
869
870 pub fn validate_optional_nesting(value: &Option<Self>, context: &()) -> garde::Result {
871 match value {
872 Some(condition) => Self::validate_nesting(condition, context),
873 None => Ok(()),
874 }
875 }
876}
877
878pub const FORBIDDEN_CONFIG_PATHS: &[&str] = &[
880 "uuid",
881 "token",
882 "token_id",
883 "remote",
884 "remote_headers",
885 "system.root_directory",
886 "system.log_directory",
887 "system.data",
888 "system.diffs_directory",
889 "system.vmount_directory",
890 "system.archive_directory",
891 "system.backup_directory",
892 "system.tmp_directory",
893 "system.passwd.directory",
894 "system.backups.restic.repository",
895 "system.backups.restic.password_file",
896 "system.backups.mounting.path",
897 "system.username",
898 "system.user",
899 "system.passwd",
900 "docker.socket",
901 "tundra.data_directory",
902 "tundra.binary",
903 "tundra.image",
904 "tundra.source_image",
905 "allowed_mounts",
906 "allowed_devices",
907 "ignore_panel_config_updates",
908 "ignore_panel_wings_upgrades",
909 "api.host",
910 "api.port",
911 "api.ssl",
912 "api.trusted_proxies",
913 "api.disable_remote_download",
914 "api.remote_download_blocked_cidrs",
915 "api.schedule.steps.http_request",
916];
917
918pub const SECRET_CONFIG_PATHS: &[&str] = &[
920 "token",
921 "token_id",
922 "remote_headers.*",
923 "docker.registries.*.password",
924 "docker.log_config.config.*",
925 "system.backups.restic.environment.*",
926];
927
928pub fn strip_config_paths(value: &mut serde_json::Value) -> Vec<&'static str> {
929 let mut stripped = Vec::new();
930
931 for path in FORBIDDEN_CONFIG_PATHS {
932 let mut cursor = &mut *value;
933 let mut parts = path.split('.').peekable();
934
935 while let Some(part) = parts.next() {
936 let serde_json::Value::Object(map) = cursor else {
937 break;
938 };
939
940 if parts.peek().is_none() {
941 if map.remove(part).is_some() {
942 stripped.push(*path);
943 }
944 break;
945 }
946
947 if map.get(part).is_some_and(|next| !next.is_object()) {
948 map.remove(part);
949 stripped.push(*path);
950 break;
951 }
952
953 match map.get_mut(part) {
954 Some(next) => cursor = next,
955 None => break,
956 }
957 }
958 }
959
960 stripped
961}
962
963#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
966pub struct TundraStatus {
967 pub supported: bool,
969 pub connected: bool,
970 pub epoch: Option<u64>,
971}
972
973#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
978pub struct TundraMetrics {
979 pub node: TundraNodeMetrics,
980 pub peers: Vec<TundraPeerMetrics>,
981}
982
983#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
984pub struct TundraNodeMetrics {
985 pub uptime_secs: u64,
986 pub epoch: u64,
987 pub remote_link: compact_str::CompactString,
989 pub frontends: u64,
990 pub snapshots_applied: u64,
991 pub local_flows_open: u64,
992 pub local_drops: u64,
993 pub frozen_flows: u64,
994 pub peers_connected: u64,
995}
996
997#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
998pub struct TundraPeerMetrics {
999 pub uuid: uuid::Uuid,
1000 pub name: compact_str::CompactString,
1001 pub role: compact_str::CompactString,
1003 pub remote_addr: compact_str::CompactString,
1004 pub established_secs: u64,
1005
1006 pub path: TundraPeerPathMetrics,
1007 pub relay: TundraPeerRelayMetrics,
1008 pub flows: TundraPeerFlowMetrics,
1009 pub drops: TundraPeerDropMetrics,
1010}
1011
1012#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
1013pub struct TundraPeerPathMetrics {
1014 pub rtt_ms: f64,
1015 pub current_mtu: u16,
1016 pub lost_packets: u64,
1017 pub congestion_events: u64,
1018}
1019
1020#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
1021pub struct TundraPeerRelayMetrics {
1022 pub stream_bytes_in: u64,
1023 pub stream_bytes_out: u64,
1024 pub datagram_bytes_in: u64,
1025 pub datagram_bytes_out: u64,
1026 pub streams_open: u64,
1027 pub streams_total: u64,
1028}
1029
1030#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
1031pub struct TundraPeerFlowMetrics {
1032 pub open: u64,
1033 pub opened_total: u64,
1034 pub tcp_open: u64,
1035}
1036
1037#[derive(Debug, ToSchema, Deserialize, Serialize, Clone)]
1038pub struct TundraPeerDropMetrics {
1039 pub send_buffer_full: u64,
1040 pub unknown_flow: u64,
1041 pub frag_timeout: u64,
1042 pub frag_limit: u64,
1043 pub oversize: u64,
1044 pub malformed: u64,
1045}
1046
1047impl super::client::WingsClient {
1048 async fn tundra_json<T: serde::de::DeserializeOwned>(
1049 &self,
1050 method: reqwest::Method,
1051 endpoint: &str,
1052 ) -> Result<T, super::client::ApiHttpError> {
1053 let response = self
1054 .request_raw(method, endpoint)
1055 .header("Accept", "application/json")
1056 .send()
1057 .await
1058 .map_err(super::client::ApiHttpError::Reqwest)?;
1059
1060 let status = response.status();
1061 if !status.is_success() {
1062 return Err(super::client::ApiHttpError::Http(
1063 status,
1064 response
1065 .json::<super::ApiError>()
1066 .await
1067 .unwrap_or_else(|err| super::ApiError {
1068 error: err.to_string().into(),
1069 }),
1070 ));
1071 }
1072
1073 response
1074 .json()
1075 .await
1076 .map_err(super::client::ApiHttpError::Reqwest)
1077 }
1078
1079 pub async fn get_tundra(&self) -> Result<TundraStatus, super::client::ApiHttpError> {
1080 self.tundra_json(reqwest::Method::GET, "/api/tundra").await
1081 }
1082
1083 pub async fn post_tundra_sync(&self) -> Result<(), super::client::ApiHttpError> {
1085 self.tundra_json::<serde::de::IgnoredAny>(reqwest::Method::POST, "/api/tundra/sync")
1086 .await?;
1087
1088 Ok(())
1089 }
1090
1091 pub async fn post_tundra_rotate(&self) -> Result<(), super::client::ApiHttpError> {
1093 self.tundra_json::<serde::de::IgnoredAny>(reqwest::Method::POST, "/api/tundra/rotate")
1094 .await?;
1095
1096 Ok(())
1097 }
1098
1099 pub async fn get_tundra_metrics(&self) -> Result<TundraMetrics, super::client::ApiHttpError> {
1100 self.tundra_json(reqwest::Method::GET, "/api/tundra/metrics")
1101 .await
1102 }
1103}
1104
1105#[cfg(test)]
1106mod tests {
1107 use super::*;
1108
1109 fn encode(value: &impl Serialize) -> Vec<u8> {
1110 let mut bytes = Vec::new();
1111 let mut serializer = rmp_serde::Serializer::new(&mut bytes)
1112 .with_struct_map()
1113 .with_human_readable();
1114 value.serialize(&mut serializer).unwrap();
1115
1116 bytes
1117 }
1118
1119 #[test]
1120 fn search_content_matches_preserve_older_node_omission() {
1121 for (payload, expected_present) in [
1122 (serde_json::json!({ "results": [] }), false),
1123 (
1124 serde_json::json!({ "results": [], "content_matches": [] }),
1125 true,
1126 ),
1127 ] {
1128 let response: crate::servers_server_files_search::post::Response =
1129 rmp_serde::from_slice(&encode(&payload)).unwrap();
1130
1131 assert!(response.results.is_empty());
1132 assert_eq!(response.content_matches.is_some(), expected_present);
1133 }
1134 }
1135
1136 #[test]
1137 fn file_content_response_preserves_text_and_nullable_line_bounds() {
1138 let payload = serde_json::json!({
1139 "results": [],
1140 "content_matches": [{
1141 "file": "test.txt",
1142 "truncated": false,
1143 "blocks": [{
1144 "start_line": 3,
1145 "end_line": 4,
1146 "content": "é\r\nHello\r\n",
1147 "matches": [{ "start_byte": 4, "end_byte": 9 }],
1148 }],
1149 }],
1150 });
1151 let response: crate::servers_server_files_search::post::Response =
1152 rmp_serde::from_slice(&encode(&payload)).unwrap();
1153 assert_eq!(serde_json::to_value(response).unwrap(), payload);
1154
1155 for payload in [
1156 serde_json::json!({
1157 "start_line": null, "end_line": null, "content": "", "eof": true,
1158 }),
1159 serde_json::json!({
1160 "start_line": 3, "end_line": 4, "content": "é\r\nHello\r\n", "eof": false,
1161 }),
1162 ] {
1163 let response: crate::servers_server_files_lines::get::Response =
1164 rmp_serde::from_slice(&encode(&payload)).unwrap();
1165 assert_eq!(serde_json::to_value(response).unwrap(), payload);
1166 }
1167 }
1168
1169 #[test]
1174 fn overlay_matches_flat_body() {
1175 #[derive(Serialize)]
1176 struct Inner {
1177 root: compact_str::CompactString,
1178 files: Vec<compact_str::CompactString>,
1179 nested: Vec<Nested>,
1180 foreground: Option<bool>,
1181 }
1182
1183 #[derive(Serialize)]
1184 struct Nested {
1185 #[serde(rename = "toName")]
1186 to_name: compact_str::CompactString,
1187 }
1188
1189 #[derive(Serialize)]
1190 struct Overlay<'a> {
1191 #[serde(flatten)]
1192 inner: &'a Inner,
1193 ignored: &'a Vec<compact_str::CompactString>,
1194 }
1195
1196 #[derive(Serialize)]
1197 struct Flat {
1198 root: compact_str::CompactString,
1199 files: Vec<compact_str::CompactString>,
1200 nested: Vec<Nested>,
1201 foreground: Option<bool>,
1202 ignored: Vec<compact_str::CompactString>,
1203 }
1204
1205 let ignored = vec!["*.log".into(), "secret/**".into()];
1206 let inner = Inner {
1207 root: "/".into(),
1208 files: vec!["a.txt".into()],
1209 nested: vec![Nested {
1210 to_name: "b.txt".into(),
1211 }],
1212 foreground: Some(true),
1213 };
1214
1215 assert_eq!(
1216 encode(&Overlay {
1217 inner: &inner,
1218 ignored: &ignored,
1219 }),
1220 encode(&Flat {
1221 root: inner.root.clone(),
1222 files: inner.files.clone(),
1223 nested: vec![Nested {
1224 to_name: "b.txt".into(),
1225 }],
1226 foreground: inner.foreground,
1227 ignored,
1228 })
1229 );
1230 }
1231}