1use crate::{
2 State,
3 models::{InsertQueryBuilder, UpdateQueryBuilder},
4 prelude::*,
5 response::DisplayError,
6};
7use compact_str::ToCompactString;
8use garde::Validate;
9use indexmap::IndexMap;
10use serde::{Deserialize, Serialize};
11use sqlx::{Row, postgres::PgRow, prelude::Type};
12use std::{
13 collections::{BTreeMap, HashMap},
14 sync::{Arc, LazyLock},
15};
16use utoipa::ToSchema;
17
18mod events;
19pub use events::ServerEvent;
20
21pub mod firewall;
22
23pub type GetServer = crate::extract::ConsumingExtension<Server>;
24pub type GetServerActivityLogger = crate::extract::ConsumingExtension<ServerActivityLogger>;
25
26fn is_path_ignored(
30 overrides: &ignore::overrides::Override,
31 path: impl AsRef<std::path::Path>,
32 is_dir: bool,
33) -> bool {
34 let path = crate::cap::CapFilesystem::resolve_path(path.as_ref());
35
36 if path == std::path::Path::new("/")
37 || path == std::path::Path::new("")
38 || path == std::path::Path::new(".")
39 {
40 return false;
41 }
42
43 overrides.matched(path, is_dir).is_whitelist()
44}
45
46#[derive(Clone)]
47pub struct ServerActivityLogger {
48 pub state: State,
49 pub server_uuid: uuid::Uuid,
50 pub user_uuid: uuid::Uuid,
51 pub impersonator_uuid: Option<uuid::Uuid>,
52 pub user_admin: bool,
53 pub user_owner: bool,
54 pub user_subuser: bool,
55 pub api_key_uuid: Option<uuid::Uuid>,
56 pub ip: std::net::IpAddr,
57}
58
59impl ServerActivityLogger {
60 pub async fn log(&self, event: impl Into<compact_str::CompactString>, data: serde_json::Value) {
61 let settings = match self.state.settings.get().await {
62 Ok(settings) => settings,
63 Err(_) => return,
64 };
65
66 if !settings.activity.server_log_admin_activity
67 && self.user_admin
68 && !self.user_owner
69 && !self.user_subuser
70 {
71 return;
72 }
73 drop(settings);
74
75 let options = super::server_activity::CreateServerActivityOptions {
76 server_uuid: self.server_uuid,
77 user_uuid: Some(self.user_uuid),
78 impersonator_uuid: self.impersonator_uuid,
79 api_key_uuid: self.api_key_uuid,
80 schedule_uuid: None,
81 event: event.into(),
82 ip: Some(self.ip.into()),
83 data,
84 created: None,
85 };
86 if let Err(err) = super::server_activity::ServerActivity::create(&self.state, options).await
87 {
88 tracing::warn!(
89 user = %self.user_uuid,
90 "failed to log server activity: {:#?}",
91 err
92 );
93 }
94 }
95}
96
97#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
98#[serde(rename_all = "snake_case")]
99#[sqlx(type_name = "server_status", rename_all = "SCREAMING_SNAKE_CASE")]
100pub enum ServerStatus {
101 Installing,
102 InstallFailed,
103 RestoringBackup,
104 BackupRestoreFailed,
105}
106
107#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
108#[serde(rename_all = "snake_case")]
109#[sqlx(
110 type_name = "server_auto_start_behavior",
111 rename_all = "SCREAMING_SNAKE_CASE"
112)]
113pub enum ServerAutoStartBehavior {
114 Always,
115 UnlessStopped,
116 Never,
117}
118
119impl From<ServerAutoStartBehavior> for wings_api::ServerAutoStartBehavior {
120 fn from(value: ServerAutoStartBehavior) -> Self {
121 match value {
122 ServerAutoStartBehavior::Always => Self::Always,
123 ServerAutoStartBehavior::UnlessStopped => Self::UnlessStopped,
124 ServerAutoStartBehavior::Never => Self::Never,
125 }
126 }
127}
128
129pub const MAX_TRANSFER_MULTIPLEX_CHANNELS: u64 = 16;
130
131pub struct ServerTransferOptions {
132 pub destination_node: super::node::Node,
133
134 pub allocation_uuid: Option<uuid::Uuid>,
135 pub allocation_uuids: Vec<uuid::Uuid>,
136
137 pub backups: Vec<uuid::Uuid>,
138 pub delete_source_backups: bool,
139 pub archive_format: wings_api::TransferArchiveFormat,
140 pub compression_level: Option<wings_api::CompressionLevel>,
141 pub multiplex_channels: u64,
142}
143
144#[derive(Serialize, Deserialize, Clone)]
145pub struct Server {
146 pub uuid: uuid::Uuid,
147 pub uuid_short: i32,
148 pub external_id: Option<compact_str::CompactString>,
149 pub allocation: Option<super::server_allocation::ServerAllocation>,
150 pub destination_allocation_uuid: Option<uuid::Uuid>,
151 pub node: Fetchable<super::node::Node>,
152 pub destination_node: Option<Fetchable<super::node::Node>>,
153 pub owner: super::user::User,
154 pub egg: Box<super::nest_egg::NestEgg>,
155 pub nest: Box<super::nest::Nest>,
156 pub backup_configuration: Option<Fetchable<super::backup_configuration::BackupConfiguration>>,
157
158 pub status: Option<ServerStatus>,
159 pub suspended: bool,
160
161 pub name: compact_str::CompactString,
162 pub description: Option<compact_str::CompactString>,
163
164 pub memory: i64,
165 pub memory_overhead: i64,
166 pub swap: i64,
167 pub disk: i64,
168 pub io_weight: Option<i16>,
169 pub cpu: i32,
170 pub pinned_cpus: Vec<i16>,
171
172 pub startup: compact_str::CompactString,
173 pub image: compact_str::CompactString,
174 pub auto_kill: wings_api::ServerConfigurationAutoKill,
175 pub auto_start_behavior: ServerAutoStartBehavior,
176 pub timezone: Option<compact_str::CompactString>,
177
178 pub hugepages_passthrough_enabled: bool,
179 pub kvm_passthrough_enabled: bool,
180
181 pub allocation_limit: i32,
182 pub database_limit: i32,
183 pub backup_limit: i32,
184 pub schedule_limit: i32,
185
186 pub subuser_permissions: Option<Arc<Vec<compact_str::CompactString>>>,
187 pub subuser_ignored_files: Option<Vec<compact_str::CompactString>>,
188 #[serde(skip_serializing, skip_deserializing)]
189 subuser_ignored_files_overrides: Option<Box<ignore::overrides::Override>>,
190
191 pub created: chrono::NaiveDateTime,
192
193 extension_data: super::ModelExtensionData,
194}
195
196impl BaseModel for Server {
197 const NAME: &'static str = "server";
198
199 fn get_extension_list() -> &'static super::ModelExtensionList {
200 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
201 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
202
203 &EXTENSIONS
204 }
205
206 fn get_extension_data(&self) -> &super::ModelExtensionData {
207 &self.extension_data
208 }
209
210 #[inline]
211 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
212 let prefix = prefix.unwrap_or_default();
213
214 let mut columns = BTreeMap::from([
215 ("servers.uuid", compact_str::format_compact!("{prefix}uuid")),
216 (
217 "servers.uuid_short",
218 compact_str::format_compact!("{prefix}uuid_short"),
219 ),
220 (
221 "servers.external_id",
222 compact_str::format_compact!("{prefix}external_id"),
223 ),
224 (
225 "servers.destination_allocation_uuid",
226 compact_str::format_compact!("{prefix}destination_allocation_uuid"),
227 ),
228 (
229 "servers.node_uuid",
230 compact_str::format_compact!("{prefix}node_uuid"),
231 ),
232 (
233 "servers.destination_node_uuid",
234 compact_str::format_compact!("{prefix}destination_node_uuid"),
235 ),
236 (
237 "servers.backup_configuration_uuid",
238 compact_str::format_compact!("{prefix}backup_configuration_uuid"),
239 ),
240 (
241 "servers.status",
242 compact_str::format_compact!("{prefix}status"),
243 ),
244 (
245 "servers.suspended",
246 compact_str::format_compact!("{prefix}suspended"),
247 ),
248 ("servers.name", compact_str::format_compact!("{prefix}name")),
249 (
250 "servers.description",
251 compact_str::format_compact!("{prefix}description"),
252 ),
253 (
254 "servers.memory",
255 compact_str::format_compact!("{prefix}memory"),
256 ),
257 (
258 "servers.memory_overhead",
259 compact_str::format_compact!("{prefix}memory_overhead"),
260 ),
261 ("servers.swap", compact_str::format_compact!("{prefix}swap")),
262 ("servers.disk", compact_str::format_compact!("{prefix}disk")),
263 (
264 "servers.io_weight",
265 compact_str::format_compact!("{prefix}io_weight"),
266 ),
267 ("servers.cpu", compact_str::format_compact!("{prefix}cpu")),
268 (
269 "servers.pinned_cpus",
270 compact_str::format_compact!("{prefix}pinned_cpus"),
271 ),
272 (
273 "servers.startup",
274 compact_str::format_compact!("{prefix}startup"),
275 ),
276 (
277 "servers.image",
278 compact_str::format_compact!("{prefix}image"),
279 ),
280 (
281 "servers.auto_kill",
282 compact_str::format_compact!("{prefix}auto_kill"),
283 ),
284 (
285 "servers.auto_start_behavior",
286 compact_str::format_compact!("{prefix}auto_start_behavior"),
287 ),
288 (
289 "servers.timezone",
290 compact_str::format_compact!("{prefix}timezone"),
291 ),
292 (
293 "servers.hugepages_passthrough_enabled",
294 compact_str::format_compact!("{prefix}hugepages_passthrough_enabled"),
295 ),
296 (
297 "servers.kvm_passthrough_enabled",
298 compact_str::format_compact!("{prefix}kvm_passthrough_enabled"),
299 ),
300 (
301 "servers.allocation_limit",
302 compact_str::format_compact!("{prefix}allocation_limit"),
303 ),
304 (
305 "servers.database_limit",
306 compact_str::format_compact!("{prefix}database_limit"),
307 ),
308 (
309 "servers.backup_limit",
310 compact_str::format_compact!("{prefix}backup_limit"),
311 ),
312 (
313 "servers.schedule_limit",
314 compact_str::format_compact!("{prefix}schedule_limit"),
315 ),
316 (
317 "servers.created",
318 compact_str::format_compact!("{prefix}created"),
319 ),
320 ]);
321
322 columns.extend(super::server_allocation::ServerAllocation::base_columns(
323 Some("allocation_"),
324 ));
325 columns.extend(super::user::User::base_columns(Some("owner_")));
326 columns.extend(super::nest_egg::NestEgg::base_columns(Some("egg_")));
327 columns.extend(super::nest::Nest::base_columns(Some("nest_")));
328
329 columns
330 }
331
332 #[inline]
333 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
334 let prefix = prefix.unwrap_or_default();
335
336 Ok(Self {
337 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
338 uuid_short: row.try_get(compact_str::format_compact!("{prefix}uuid_short").as_str())?,
339 external_id: row
340 .try_get(compact_str::format_compact!("{prefix}external_id").as_str())?,
341 allocation: if row
342 .try_get::<uuid::Uuid, _>(
343 compact_str::format_compact!("{prefix}allocation_uuid").as_str(),
344 )
345 .is_ok()
346 {
347 Some(super::server_allocation::ServerAllocation::map(
348 Some("allocation_"),
349 row,
350 )?)
351 } else {
352 None
353 },
354 destination_allocation_uuid: row
355 .try_get::<uuid::Uuid, _>(
356 compact_str::format_compact!("{prefix}destination_allocation_uuid").as_str(),
357 )
358 .ok(),
359 node: super::node::Node::get_fetchable(
360 row.try_get(compact_str::format_compact!("{prefix}node_uuid").as_str())?,
361 ),
362 destination_node: super::node::Node::get_fetchable_from_row(
363 row,
364 compact_str::format_compact!("{prefix}destination_node_uuid"),
365 ),
366 owner: super::user::User::map(Some("owner_"), row)?,
367 egg: Box::new(super::nest_egg::NestEgg::map(Some("egg_"), row)?),
368 nest: Box::new(super::nest::Nest::map(Some("nest_"), row)?),
369 backup_configuration:
370 super::backup_configuration::BackupConfiguration::get_fetchable_from_row(
371 row,
372 compact_str::format_compact!("{prefix}backup_configuration_uuid"),
373 ),
374 status: row.try_get(compact_str::format_compact!("{prefix}status").as_str())?,
375 suspended: row.try_get(compact_str::format_compact!("{prefix}suspended").as_str())?,
376 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
377 description: row
378 .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
379 memory: row.try_get(compact_str::format_compact!("{prefix}memory").as_str())?,
380 memory_overhead: row
381 .try_get(compact_str::format_compact!("{prefix}memory_overhead").as_str())?,
382 swap: row.try_get(compact_str::format_compact!("{prefix}swap").as_str())?,
383 disk: row.try_get(compact_str::format_compact!("{prefix}disk").as_str())?,
384 io_weight: row.try_get(compact_str::format_compact!("{prefix}io_weight").as_str())?,
385 cpu: row.try_get(compact_str::format_compact!("{prefix}cpu").as_str())?,
386 pinned_cpus: row
387 .try_get(compact_str::format_compact!("{prefix}pinned_cpus").as_str())?,
388 startup: row.try_get(compact_str::format_compact!("{prefix}startup").as_str())?,
389 image: row.try_get(compact_str::format_compact!("{prefix}image").as_str())?,
390 auto_kill: serde_json::from_value(row.try_get::<serde_json::Value, _>(
391 compact_str::format_compact!("{prefix}auto_kill").as_str(),
392 )?)?,
393 auto_start_behavior: row
394 .try_get(compact_str::format_compact!("{prefix}auto_start_behavior").as_str())?,
395 timezone: row.try_get(compact_str::format_compact!("{prefix}timezone").as_str())?,
396 hugepages_passthrough_enabled: row.try_get(
397 compact_str::format_compact!("{prefix}hugepages_passthrough_enabled").as_str(),
398 )?,
399 kvm_passthrough_enabled: row.try_get(
400 compact_str::format_compact!("{prefix}kvm_passthrough_enabled").as_str(),
401 )?,
402 allocation_limit: row
403 .try_get(compact_str::format_compact!("{prefix}allocation_limit").as_str())?,
404 database_limit: row
405 .try_get(compact_str::format_compact!("{prefix}database_limit").as_str())?,
406 backup_limit: row
407 .try_get(compact_str::format_compact!("{prefix}backup_limit").as_str())?,
408 schedule_limit: row
409 .try_get(compact_str::format_compact!("{prefix}schedule_limit").as_str())?,
410 subuser_permissions: row
411 .try_get::<Vec<compact_str::CompactString>, _>("permissions")
412 .map(Arc::new)
413 .ok(),
414 subuser_ignored_files: row
415 .try_get::<Vec<compact_str::CompactString>, _>("ignored_files")
416 .ok(),
417 subuser_ignored_files_overrides: None,
418 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
419 extension_data: Self::map_extensions(prefix, row)?,
420 })
421 }
422
423 fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
424 vec![compact_str::format_compact!(
425 "{}::{}",
426 Self::NAME,
427 self.uuid
428 )]
429 }
430}
431
432#[async_trait::async_trait]
433impl ResolvableModel for Server {
434 type Fingerprint = i32;
435
436 fn uuid(&self) -> uuid::Uuid {
437 self.uuid
438 }
439
440 fn fingerprint(&self) -> Self::Fingerprint {
441 self.uuid_short
442 }
443
444 async fn resolve(
445 database: &crate::database::Database,
446 identifier: &str,
447 ) -> Result<Option<Self>, anyhow::Error> {
448 let Ok(uuid_short) = u32::from_str_radix(identifier, 16) else {
449 return Ok(None);
450 };
451
452 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
453 r#"
454 SELECT {}
455 FROM servers
456 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
457 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
458 JOIN users ON users.uuid = servers.owner_uuid
459 LEFT JOIN roles ON roles.uuid = users.role_uuid
460 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
461 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
462 WHERE servers.uuid_short = $1
463 "#,
464 Self::columns_sql(None)
465 )))
466 .bind(uuid_short as i32)
467 .fetch_optional(database.read())
468 .await?;
469
470 Ok(row.try_map(|row| Self::map(None, &row))?)
471 }
472}
473
474impl Server {
475 pub fn unavailable_reason(&self) -> Option<&'static str> {
478 if self.suspended {
479 Some("server is suspended")
480 } else if self.destination_node.is_some() {
481 Some("server is being transferred")
482 } else {
483 match self.status? {
484 ServerStatus::Installing => Some("server is currently installing"),
485 ServerStatus::InstallFailed => Some("server has failed its installation process"),
486 ServerStatus::RestoringBackup => Some("server is restoring from a backup"),
487 ServerStatus::BackupRestoreFailed => Some("server has failed to restore a backup"),
488 }
489 }
490 }
491
492 pub async fn by_node_uuid_uuid(
493 database: &crate::database::Database,
494 node_uuid: uuid::Uuid,
495 uuid: uuid::Uuid,
496 ) -> Result<Option<Self>, crate::database::DatabaseError> {
497 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
498 r#"
499 SELECT {}
500 FROM servers
501 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
502 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
503 JOIN users ON users.uuid = servers.owner_uuid
504 LEFT JOIN roles ON roles.uuid = users.role_uuid
505 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
506 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
507 WHERE (servers.node_uuid = $1 OR servers.destination_node_uuid = $1) AND servers.uuid = $2
508 "#,
509 Self::columns_sql(None)
510 )))
511 .bind(node_uuid)
512 .bind(uuid)
513 .fetch_optional(database.read())
514 .await?;
515
516 row.try_map(|row| Self::map(None, &row))
517 }
518
519 pub async fn by_external_id(
520 database: &crate::database::Database,
521 external_id: &str,
522 ) -> Result<Option<Self>, crate::database::DatabaseError> {
523 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
524 r#"
525 SELECT {}
526 FROM servers
527 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
528 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
529 JOIN users ON users.uuid = servers.owner_uuid
530 LEFT JOIN roles ON roles.uuid = users.role_uuid
531 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
532 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
533 WHERE servers.external_id = $1
534 "#,
535 Self::columns_sql(None)
536 )))
537 .bind(external_id)
538 .fetch_optional(database.read())
539 .await?;
540
541 row.try_map(|row| Self::map(None, &row))
542 }
543
544 pub async fn by_identifier(
545 database: &crate::database::Database,
546 identifier: &str,
547 ) -> Result<Option<Self>, crate::database::DatabaseError> {
548 let query = format!(
549 r#"
550 SELECT {}
551 FROM servers
552 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
553 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
554 JOIN users ON users.uuid = servers.owner_uuid
555 LEFT JOIN roles ON roles.uuid = users.role_uuid
556 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
557 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
558 WHERE servers.{} = $1
559 "#,
560 Self::columns_sql(None),
561 match identifier.len() {
562 8 => "uuid_short",
563 36 => "uuid",
564 _ => return Ok(None),
565 }
566 );
567
568 let mut row = sqlx::query(sqlx::AssertSqlSafe(query));
569 row = match identifier.len() {
570 8 => row.bind(u32::from_str_radix(identifier, 16).map_err(anyhow::Error::new)? as i32),
571 36 => row.bind(uuid::Uuid::parse_str(identifier).map_err(anyhow::Error::new)?),
572 _ => return Ok(None),
573 };
574 let row = row.fetch_optional(database.read()).await?;
575
576 row.try_map(|row| Self::map(None, &row))
577 }
578
579 pub async fn by_user_identifier(
583 database: &crate::database::Database,
584 user: &super::user::User,
585 identifier: &str,
586 ) -> Result<Option<Self>, anyhow::Error> {
587 let server = match identifier.len() {
588 8 => Self::resolve_cached(database, identifier).await?,
589 36 => match uuid::Uuid::parse_str(identifier) {
590 Ok(uuid) => Self::by_uuid_optional_cached(database, uuid).await?,
591 Err(_) => None,
592 },
593 _ => None,
594 };
595
596 let Some(mut server) = server else {
597 return Ok(None);
598 };
599
600 if server.owner.uuid == user.uuid {
601 return Ok(Some(server));
602 }
603
604 if let Some((permissions, ignored_files)) =
605 super::server_subuser::ServerSubuser::permissions_by_server_uuid_user_uuid(
606 database,
607 server.uuid,
608 user.uuid,
609 )
610 .await?
611 {
612 server.subuser_permissions = Some(Arc::new(permissions));
613 server.subuser_ignored_files = Some(ignored_files);
614
615 return Ok(Some(server));
616 }
617
618 let admin_bypass = user.role.as_ref().map_or(user.admin, |role| {
619 role.admin_permissions.iter().any(|p| p == "servers.read")
620 });
621
622 Ok(admin_bypass.then_some(server))
623 }
624
625 pub async fn by_user_uuids(
626 database: &crate::database::Database,
627 user: &super::user::User,
628 uuids: &[uuid::Uuid],
629 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
630 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
631 r#"
632 SELECT {}, server_subusers.permissions, server_subusers.ignored_files
633 FROM servers
634 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
635 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
636 JOIN users ON users.uuid = servers.owner_uuid
637 LEFT JOIN roles ON roles.uuid = users.role_uuid
638 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
639 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
640 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
641 WHERE servers.uuid = ANY($3) AND (servers.owner_uuid = $1 OR server_subusers.user_uuid = $1 OR $2)
642 ORDER BY array_position($3, servers.uuid)
643 "#,
644 Self::columns_sql(None)
645 )))
646 .bind(user.uuid)
647 .bind(
648 user.role.as_ref().map_or(user.admin, |r| {
649 r.admin_permissions.iter().any(|p| p == "servers.read")
650 }),
651 )
652 .bind(uuids)
653 .fetch_all(database.read())
654 .await?;
655
656 rows.into_iter()
657 .map(|row| Self::map(None, &row))
658 .try_collect_vec()
659 }
660
661 pub async fn by_owner_uuid_with_pagination(
662 database: &crate::database::Database,
663 owner_uuid: uuid::Uuid,
664 page: i64,
665 per_page: i64,
666 search: Option<&str>,
667 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
668 let offset = (page - 1) * per_page;
669
670 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
671 r#"
672 SELECT {}, COUNT(*) OVER() AS total_count
673 FROM servers
674 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
675 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
676 JOIN users ON users.uuid = servers.owner_uuid
677 LEFT JOIN roles ON roles.uuid = users.role_uuid
678 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
679 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
680 WHERE servers.owner_uuid = $1 AND ($2 IS NULL OR servers.name ILIKE '%' || $2 || '%')
681 ORDER BY servers.created
682 LIMIT $3 OFFSET $4
683 "#,
684 Self::columns_sql(None)
685 )))
686 .bind(owner_uuid)
687 .bind(search)
688 .bind(per_page)
689 .bind(offset)
690 .fetch_all(database.read())
691 .await?;
692
693 Ok(super::Pagination {
694 total: rows
695 .first()
696 .map_or(Ok(0), |row| row.try_get("total_count"))?,
697 per_page,
698 page,
699 data: rows
700 .into_iter()
701 .map(|row| Self::map(None, &row))
702 .try_collect_vec()?,
703 })
704 }
705
706 pub async fn by_user_uuid_server_order_with_pagination(
707 database: &crate::database::Database,
708 user: &super::user::User,
709 server_order: &[uuid::Uuid],
710 page: i64,
711 per_page: i64,
712 search: Option<&str>,
713 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
714 let offset = (page - 1) * per_page;
715
716 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
717 r#"
718 SELECT {}, COUNT(*) OVER() AS total_count
719 FROM servers
720 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
721 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
722 JOIN users ON users.uuid = servers.owner_uuid
723 LEFT JOIN roles ON roles.uuid = users.role_uuid
724 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
725 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
726 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
727 WHERE servers.uuid = ANY($2)
728 AND (servers.owner_uuid = $1 OR server_subusers.user_uuid = $1 OR $6)
729 AND ($3 IS NULL OR servers.name ILIKE '%' || $3 || '%' OR users.username ILIKE '%' || $3 || '%' OR users.email ILIKE '%' || $3 || '%')
730 ORDER BY array_position($2, servers.uuid), servers.created
731 LIMIT $4 OFFSET $5
732 "#,
733 Self::columns_sql(None)
734 )))
735 .bind(user.uuid)
736 .bind(server_order)
737 .bind(search)
738 .bind(per_page)
739 .bind(offset)
740 .bind(
741 user.role.as_ref().map_or(user.admin, |r| {
742 r.admin_permissions.iter().any(|p| p == "servers.read")
743 }),
744 )
745 .fetch_all(database.read())
746 .await?;
747
748 Ok(super::Pagination {
749 total: rows
750 .first()
751 .map_or(Ok(0), |row| row.try_get("total_count"))?,
752 per_page,
753 page,
754 data: rows
755 .into_iter()
756 .map(|row| Self::map(None, &row))
757 .try_collect_vec()?,
758 })
759 }
760
761 pub async fn by_user_uuid_with_pagination(
762 database: &crate::database::Database,
763 user_uuid: uuid::Uuid,
764 page: i64,
765 per_page: i64,
766 search: Option<&str>,
767 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
768 let offset = (page - 1) * per_page;
769
770 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
771 r#"
772 SELECT DISTINCT ON (servers.uuid, servers.created) {}, server_subusers.permissions, server_subusers.ignored_files, COUNT(*) OVER() AS total_count
773 FROM servers
774 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
775 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
776 JOIN users ON users.uuid = servers.owner_uuid
777 LEFT JOIN roles ON roles.uuid = users.role_uuid
778 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
779 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
780 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
781 WHERE
782 (servers.owner_uuid = $1 OR server_subusers.user_uuid = $1)
783 AND ($2 IS NULL OR servers.name ILIKE '%' || $2 || '%' OR users.username ILIKE '%' || $2 || '%' OR users.email ILIKE '%' || $2 || '%')
784 ORDER BY servers.created
785 LIMIT $3 OFFSET $4
786 "#,
787 Self::columns_sql(None)
788 )))
789 .bind(user_uuid)
790 .bind(search)
791 .bind(per_page)
792 .bind(offset)
793 .fetch_all(database.read())
794 .await?;
795
796 Ok(super::Pagination {
797 total: rows
798 .first()
799 .map_or(Ok(0), |row| row.try_get("total_count"))?,
800 per_page,
801 page,
802 data: rows
803 .into_iter()
804 .map(|row| Self::map(None, &row))
805 .try_collect_vec()?,
806 })
807 }
808
809 pub async fn all_uuids_by_node_uuid_user_uuid(
810 database: &crate::database::Database,
811 node_uuid: uuid::Uuid,
812 user_uuid: uuid::Uuid,
813 ) -> Result<Vec<uuid::Uuid>, crate::database::DatabaseError> {
814 let rows = sqlx::query(
815 r#"
816 SELECT DISTINCT ON (servers.uuid, servers.created) servers.uuid
817 FROM servers
818 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $2
819 WHERE servers.node_uuid = $1 AND (servers.owner_uuid = $2 OR server_subusers.user_uuid = $2)
820 ORDER BY servers.created
821 "#
822 )
823 .bind(node_uuid)
824 .bind(user_uuid)
825 .fetch_all(database.read())
826 .await?;
827
828 Ok(rows
829 .into_iter()
830 .map(|row| row.get::<uuid::Uuid, _>("uuid"))
831 .collect())
832 }
833
834 pub async fn by_not_user_uuid_with_pagination(
835 database: &crate::database::Database,
836 user_uuid: uuid::Uuid,
837 page: i64,
838 per_page: i64,
839 search: Option<&str>,
840 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
841 let offset = (page - 1) * per_page;
842
843 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
844 r#"
845 SELECT DISTINCT ON (servers.uuid, servers.created) {}, server_subusers.permissions, server_subusers.ignored_files, COUNT(*) OVER() AS total_count
846 FROM servers
847 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
848 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
849 JOIN users ON users.uuid = servers.owner_uuid
850 LEFT JOIN roles ON roles.uuid = users.role_uuid
851 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
852 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
853 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
854 WHERE
855 servers.owner_uuid != $1 AND (server_subusers.user_uuid IS NULL OR server_subusers.user_uuid != $1)
856 AND ($2 IS NULL OR servers.name ILIKE '%' || $2 || '%' OR users.username ILIKE '%' || $2 || '%' OR users.email ILIKE '%' || $2 || '%')
857 ORDER BY servers.created
858 LIMIT $3 OFFSET $4
859 "#,
860 Self::columns_sql(None)
861 )))
862 .bind(user_uuid)
863 .bind(search)
864 .bind(per_page)
865 .bind(offset)
866 .fetch_all(database.read())
867 .await?;
868
869 Ok(super::Pagination {
870 total: rows
871 .first()
872 .map_or(Ok(0), |row| row.try_get("total_count"))?,
873 per_page,
874 page,
875 data: rows
876 .into_iter()
877 .map(|row| Self::map(None, &row))
878 .try_collect_vec()?,
879 })
880 }
881
882 pub async fn tunnel_available_by_user_uuid_with_pagination(
885 database: &crate::database::Database,
886 user_uuid: uuid::Uuid,
887 server_uuid: uuid::Uuid,
888 page: i64,
889 per_page: i64,
890 search: Option<&str>,
891 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
892 let offset = (page - 1) * per_page;
893
894 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
895 r#"
896 SELECT DISTINCT ON (servers.uuid, servers.created) {}, server_subusers.permissions, server_subusers.ignored_files, COUNT(*) OVER() AS total_count
897 FROM server_tunnels
898 JOIN servers ON servers.uuid = server_tunnels.server_uuid
899 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
900 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
901 JOIN users ON users.uuid = servers.owner_uuid
902 LEFT JOIN roles ON roles.uuid = users.role_uuid
903 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
904 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
905 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
906 WHERE
907 (servers.owner_uuid = $1 OR server_subusers.user_uuid = $1)
908 AND servers.uuid != $2
909 AND NOT servers.suspended
910 AND servers.destination_node_uuid IS NULL
911 AND servers.status IS NULL
912 AND ($3 IS NULL OR servers.name ILIKE '%' || $3 || '%' OR users.username ILIKE '%' || $3 || '%' OR users.email ILIKE '%' || $3 || '%')
913 ORDER BY servers.created
914 LIMIT $4 OFFSET $5
915 "#,
916 Self::columns_sql(None)
917 )))
918 .bind(user_uuid)
919 .bind(server_uuid)
920 .bind(search)
921 .bind(per_page)
922 .bind(offset)
923 .fetch_all(database.read())
924 .await?;
925
926 Ok(super::Pagination {
927 total: rows
928 .first()
929 .map_or(Ok(0), |row| row.try_get("total_count"))?,
930 per_page,
931 page,
932 data: rows
933 .into_iter()
934 .map(|row| Self::map(None, &row))
935 .try_collect_vec()?,
936 })
937 }
938
939 pub async fn tunnel_available_by_not_user_uuid_with_pagination(
940 database: &crate::database::Database,
941 user_uuid: uuid::Uuid,
942 server_uuid: uuid::Uuid,
943 page: i64,
944 per_page: i64,
945 search: Option<&str>,
946 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
947 let offset = (page - 1) * per_page;
948
949 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
950 r#"
951 SELECT DISTINCT ON (servers.uuid, servers.created) {}, server_subusers.permissions, server_subusers.ignored_files, COUNT(*) OVER() AS total_count
952 FROM server_tunnels
953 JOIN servers ON servers.uuid = server_tunnels.server_uuid
954 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
955 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
956 JOIN users ON users.uuid = servers.owner_uuid
957 LEFT JOIN roles ON roles.uuid = users.role_uuid
958 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
959 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
960 LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
961 WHERE
962 servers.owner_uuid != $1 AND (server_subusers.user_uuid IS NULL OR server_subusers.user_uuid != $1)
963 AND servers.uuid != $2
964 AND NOT servers.suspended
965 AND servers.destination_node_uuid IS NULL
966 AND servers.status IS NULL
967 AND ($3 IS NULL OR servers.name ILIKE '%' || $3 || '%' OR users.username ILIKE '%' || $3 || '%' OR users.email ILIKE '%' || $3 || '%')
968 ORDER BY servers.created
969 LIMIT $4 OFFSET $5
970 "#,
971 Self::columns_sql(None)
972 )))
973 .bind(user_uuid)
974 .bind(server_uuid)
975 .bind(search)
976 .bind(per_page)
977 .bind(offset)
978 .fetch_all(database.read())
979 .await?;
980
981 Ok(super::Pagination {
982 total: rows
983 .first()
984 .map_or(Ok(0), |row| row.try_get("total_count"))?,
985 per_page,
986 page,
987 data: rows
988 .into_iter()
989 .map(|row| Self::map(None, &row))
990 .try_collect_vec()?,
991 })
992 }
993
994 pub async fn by_node_uuid_with_pagination(
995 database: &crate::database::Database,
996 node_uuid: uuid::Uuid,
997 page: i64,
998 per_page: i64,
999 search: Option<&str>,
1000 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
1001 let offset = (page - 1) * per_page;
1002
1003 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
1004 r#"
1005 SELECT {}, COUNT(*) OVER() AS total_count
1006 FROM servers
1007 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
1008 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1009 JOIN users ON users.uuid = servers.owner_uuid
1010 LEFT JOIN roles ON roles.uuid = users.role_uuid
1011 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
1012 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
1013 WHERE servers.node_uuid = $1 AND ($2 IS NULL OR servers.name ILIKE '%' || $2 || '%')
1014 ORDER BY servers.created
1015 LIMIT $3 OFFSET $4
1016 "#,
1017 Self::columns_sql(None)
1018 )))
1019 .bind(node_uuid)
1020 .bind(search)
1021 .bind(per_page)
1022 .bind(offset)
1023 .fetch_all(database.read())
1024 .await?;
1025
1026 Ok(super::Pagination {
1027 total: rows
1028 .first()
1029 .map_or(Ok(0), |row| row.try_get("total_count"))?,
1030 per_page,
1031 page,
1032 data: rows
1033 .into_iter()
1034 .map(|row| Self::map(None, &row))
1035 .try_collect_vec()?,
1036 })
1037 }
1038
1039 pub async fn by_node_uuid_transferring_with_pagination(
1040 database: &crate::database::Database,
1041 node_uuid: uuid::Uuid,
1042 page: i64,
1043 per_page: i64,
1044 search: Option<&str>,
1045 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
1046 let offset = (page - 1) * per_page;
1047
1048 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
1049 r#"
1050 SELECT {}, COUNT(*) OVER() AS total_count
1051 FROM servers
1052 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
1053 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1054 JOIN users ON users.uuid = servers.owner_uuid
1055 LEFT JOIN roles ON roles.uuid = users.role_uuid
1056 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
1057 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
1058 WHERE servers.node_uuid = $1 AND servers.destination_node_uuid IS NOT NULL
1059 AND ($2 IS NULL OR servers.name ILIKE '%' || $2 || '%')
1060 ORDER BY servers.created
1061 LIMIT $3 OFFSET $4
1062 "#,
1063 Self::columns_sql(None)
1064 )))
1065 .bind(node_uuid)
1066 .bind(search)
1067 .bind(per_page)
1068 .bind(offset)
1069 .fetch_all(database.read())
1070 .await?;
1071
1072 Ok(super::Pagination {
1073 total: rows
1074 .first()
1075 .map_or(Ok(0), |row| row.try_get("total_count"))?,
1076 per_page,
1077 page,
1078 data: rows
1079 .into_iter()
1080 .map(|row| Self::map(None, &row))
1081 .try_collect_vec()?,
1082 })
1083 }
1084
1085 pub async fn by_egg_uuid_with_pagination(
1086 database: &crate::database::Database,
1087 egg_uuid: uuid::Uuid,
1088 page: i64,
1089 per_page: i64,
1090 search: Option<&str>,
1091 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
1092 let offset = (page - 1) * per_page;
1093
1094 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
1095 r#"
1096 SELECT {}, COUNT(*) OVER() AS total_count
1097 FROM servers
1098 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
1099 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1100 JOIN users ON users.uuid = servers.owner_uuid
1101 LEFT JOIN roles ON roles.uuid = users.role_uuid
1102 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
1103 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
1104 WHERE servers.egg_uuid = $1 AND ($2 IS NULL OR servers.name ILIKE '%' || $2 || '%')
1105 ORDER BY servers.created
1106 LIMIT $3 OFFSET $4
1107 "#,
1108 Self::columns_sql(None)
1109 )))
1110 .bind(egg_uuid)
1111 .bind(search)
1112 .bind(per_page)
1113 .bind(offset)
1114 .fetch_all(database.read())
1115 .await?;
1116
1117 Ok(super::Pagination {
1118 total: rows
1119 .first()
1120 .map_or(Ok(0), |row| row.try_get("total_count"))?,
1121 per_page,
1122 page,
1123 data: rows
1124 .into_iter()
1125 .map(|row| Self::map(None, &row))
1126 .try_collect_vec()?,
1127 })
1128 }
1129
1130 pub async fn by_backup_configuration_uuid_with_pagination(
1131 database: &crate::database::Database,
1132 backup_configuration_uuid: uuid::Uuid,
1133 page: i64,
1134 per_page: i64,
1135 search: Option<&str>,
1136 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
1137 let offset = (page - 1) * per_page;
1138
1139 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
1140 r#"
1141 SELECT {}, COUNT(*) OVER() AS total_count
1142 FROM servers
1143 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
1144 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1145 JOIN users ON users.uuid = servers.owner_uuid
1146 LEFT JOIN roles ON roles.uuid = users.role_uuid
1147 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
1148 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
1149 WHERE servers.backup_configuration_uuid = $1 AND ($2 IS NULL OR servers.name ILIKE '%' || $2 || '%')
1150 ORDER BY servers.created
1151 LIMIT $3 OFFSET $4
1152 "#,
1153 Self::columns_sql(None)
1154 )))
1155 .bind(backup_configuration_uuid)
1156 .bind(search)
1157 .bind(per_page)
1158 .bind(offset)
1159 .fetch_all(database.read())
1160 .await?;
1161
1162 Ok(super::Pagination {
1163 total: rows
1164 .first()
1165 .map_or(Ok(0), |row| row.try_get("total_count"))?,
1166 per_page,
1167 page,
1168 data: rows
1169 .into_iter()
1170 .map(|row| Self::map(None, &row))
1171 .try_collect_vec()?,
1172 })
1173 }
1174
1175 pub async fn all_with_pagination(
1176 database: &crate::database::Database,
1177 page: i64,
1178 per_page: i64,
1179 search: Option<&str>,
1180 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
1181 let offset = (page - 1) * per_page;
1182
1183 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
1184 r#"
1185 SELECT {}, COUNT(*) OVER() AS total_count
1186 FROM servers
1187 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
1188 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1189 JOIN users ON users.uuid = servers.owner_uuid
1190 LEFT JOIN roles ON roles.uuid = users.role_uuid
1191 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
1192 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
1193 WHERE $1 IS NULL OR servers.name ILIKE '%' || $1 || '%'
1194 ORDER BY servers.created
1195 LIMIT $2 OFFSET $3
1196 "#,
1197 Self::columns_sql(None)
1198 )))
1199 .bind(search)
1200 .bind(per_page)
1201 .bind(offset)
1202 .fetch_all(database.read())
1203 .await?;
1204
1205 Ok(super::Pagination {
1206 total: rows
1207 .first()
1208 .map_or(Ok(0), |row| row.try_get("total_count"))?,
1209 per_page,
1210 page,
1211 data: rows
1212 .into_iter()
1213 .map(|row| Self::map(None, &row))
1214 .try_collect_vec()?,
1215 })
1216 }
1217
1218 pub async fn count_by_user_uuid(
1219 database: &crate::database::Database,
1220 user_uuid: uuid::Uuid,
1221 ) -> Result<i64, sqlx::Error> {
1222 sqlx::query_scalar(
1223 r#"
1224 SELECT COUNT(*)
1225 FROM servers
1226 WHERE servers.owner_uuid = $1
1227 "#,
1228 )
1229 .bind(user_uuid)
1230 .fetch_one(database.read())
1231 .await
1232 }
1233
1234 pub async fn count_by_node_uuid(
1235 database: &crate::database::Database,
1236 node_uuid: uuid::Uuid,
1237 ) -> Result<i64, sqlx::Error> {
1238 sqlx::query_scalar(
1239 r#"
1240 SELECT COUNT(*)
1241 FROM servers
1242 WHERE servers.node_uuid = $1
1243 "#,
1244 )
1245 .bind(node_uuid)
1246 .fetch_one(database.read())
1247 .await
1248 }
1249
1250 pub async fn count_by_egg_uuid(
1251 database: &crate::database::Database,
1252 egg_uuid: uuid::Uuid,
1253 ) -> Result<i64, sqlx::Error> {
1254 sqlx::query_scalar(
1255 r#"
1256 SELECT COUNT(*)
1257 FROM servers
1258 WHERE servers.egg_uuid = $1
1259 "#,
1260 )
1261 .bind(egg_uuid)
1262 .fetch_one(database.read())
1263 .await
1264 }
1265
1266 pub async fn fetch_status(
1268 &self,
1269 database: &crate::database::Database,
1270 ) -> Result<Option<ServerStatus>, crate::database::DatabaseError> {
1271 let status = sqlx::query_scalar(
1272 r#"
1273 SELECT status
1274 FROM servers
1275 WHERE servers.uuid = $1
1276 "#,
1277 )
1278 .bind(self.uuid)
1279 .fetch_one(database.read())
1280 .await?;
1281
1282 Ok(status)
1283 }
1284
1285 pub async fn try_set_status_by_uuid(
1288 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
1289 uuid: uuid::Uuid,
1290 from: Option<ServerStatus>,
1291 to: Option<ServerStatus>,
1292 ) -> Result<bool, sqlx::Error> {
1293 let rows_affected = sqlx::query!(
1294 "UPDATE servers
1295 SET status = $2
1296 WHERE servers.uuid = $1 AND servers.status IS NOT DISTINCT FROM $3",
1297 uuid,
1298 to as Option<ServerStatus>,
1299 from as Option<ServerStatus>
1300 )
1301 .execute(executor)
1302 .await?
1303 .rows_affected();
1304
1305 Ok(rows_affected > 0)
1306 }
1307
1308 pub async fn set_status(
1310 &mut self,
1311 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
1312 status: Option<ServerStatus>,
1313 ) -> Result<(), sqlx::Error> {
1314 sqlx::query!(
1315 "UPDATE servers
1316 SET status = $2
1317 WHERE servers.uuid = $1",
1318 self.uuid,
1319 status as Option<ServerStatus>
1320 )
1321 .execute(executor)
1322 .await?;
1323
1324 self.status = status;
1325
1326 Ok(())
1327 }
1328
1329 pub async fn try_set_status(
1332 &mut self,
1333 executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
1334 from: Option<ServerStatus>,
1335 to: Option<ServerStatus>,
1336 ) -> Result<bool, sqlx::Error> {
1337 if !Self::try_set_status_by_uuid(executor, self.uuid, from, to).await? {
1338 return Ok(false);
1339 }
1340
1341 self.status = to;
1342
1343 Ok(true)
1344 }
1345
1346 pub async fn sync(self, database: &crate::database::Database) -> Result<(), anyhow::Error> {
1348 self.node
1349 .fetch_cached(database)
1350 .await?
1351 .api_client(database)
1352 .await?
1353 .post_servers_server_sync(
1354 self.uuid,
1355 &wings_api::servers_server_sync::post::RequestBody {
1356 server: serde_json::to_value(self.into_remote_api_object(database).await?)?,
1357 },
1358 )
1359 .await?;
1360
1361 Ok(())
1362 }
1363
1364 pub async fn batch_sync(self, database: &Arc<crate::database::Database>) {
1368 database
1369 .batch_action("sync_server", self.uuid, {
1370 let database = database.clone();
1371
1372 async move { self.sync(&database).await }
1373 })
1374 .await;
1375 }
1376
1377 pub async fn install(
1381 &self,
1382 state: &crate::State,
1383 truncate_directory: bool,
1384 installation_script: Option<wings_api::InstallationScript>,
1385 ) -> Result<(), anyhow::Error> {
1386 let mut transaction = state.database.write().begin().await?;
1387
1388 if !Self::try_set_status_by_uuid(
1389 &mut *transaction,
1390 self.uuid,
1391 None,
1392 Some(ServerStatus::Installing),
1393 )
1394 .await?
1395 {
1396 transaction.rollback().await?;
1397
1398 return Err(DisplayError::new(
1399 "server is already installing or in an invalid state for reinstalling",
1400 )
1401 .into());
1402 }
1403
1404 match self
1405 .node
1406 .fetch_cached(&state.database)
1407 .await?
1408 .api_client(&state.database)
1409 .await?
1410 .post_servers_server_reinstall(
1411 self.uuid,
1412 &wings_api::servers_server_reinstall::post::RequestBody {
1413 truncate_directory,
1414 installation_script: Some(
1415 if let Some(installation_script) = &installation_script {
1416 installation_script.clone()
1417 } else {
1418 wings_api::InstallationScript {
1419 container_image: self.egg.config_script.container.clone(),
1420 entrypoint: self.egg.config_script.entrypoint.clone(),
1421 script: self.egg.config_script.content.to_compact_string(),
1422 environment: Default::default(),
1423 }
1424 },
1425 ),
1426 },
1427 )
1428 .await
1429 {
1430 Ok(_) => {}
1431 Err(err) => {
1432 transaction.rollback().await?;
1433
1434 return Err(err.into());
1435 }
1436 };
1437
1438 transaction.commit().await?;
1439
1440 Self::get_event_emitter().emit(
1441 state.clone(),
1442 events::ServerEvent::InstallStarted {
1443 server: Box::new(self.clone()),
1444 installation_script: Box::new(
1445 if let Some(installation_script) = installation_script {
1446 installation_script
1447 } else {
1448 wings_api::InstallationScript {
1449 container_image: self.egg.config_script.container.clone(),
1450 entrypoint: self.egg.config_script.entrypoint.clone(),
1451 script: self.egg.config_script.content.to_compact_string(),
1452 environment: Default::default(),
1453 }
1454 },
1455 ),
1456 },
1457 );
1458
1459 Ok(())
1460 }
1461
1462 pub async fn transfer(
1466 self,
1467 state: &crate::State,
1468 options: ServerTransferOptions,
1469 ) -> Result<(), anyhow::Error> {
1470 if self.destination_node.is_some() {
1471 return Err(DisplayError::new("server is already being transferred")
1472 .with_status(axum::http::StatusCode::CONFLICT)
1473 .into());
1474 }
1475
1476 if matches!(
1477 self.status,
1478 Some(ServerStatus::Installing) | Some(ServerStatus::RestoringBackup)
1479 ) {
1480 return Err(DisplayError::new(
1481 "server is installing or restoring a backup and cannot be transferred",
1482 )
1483 .with_status(axum::http::StatusCode::CONFLICT)
1484 .into());
1485 }
1486
1487 if self.node.uuid == options.destination_node.uuid {
1488 return Err(DisplayError::new(
1489 "destination node must be different from the current node",
1490 )
1491 .with_status(axum::http::StatusCode::CONFLICT)
1492 .into());
1493 }
1494
1495 if options.destination_node.is_all_in_one_node() {
1496 return Err(DisplayError::new("cannot transfer to an all-in-one node")
1497 .with_status(axum::http::StatusCode::CONFLICT)
1498 .into());
1499 }
1500
1501 if options.multiplex_channels > MAX_TRANSFER_MULTIPLEX_CHANNELS {
1502 return Err(DisplayError::new(format!(
1503 "multiplex channels cannot exceed {MAX_TRANSFER_MULTIPLEX_CHANNELS}"
1504 ))
1505 .with_status(axum::http::StatusCode::BAD_REQUEST)
1506 .into());
1507 }
1508
1509 let mut requested_allocations = options.allocation_uuids.clone();
1510 requested_allocations.extend(options.allocation_uuid);
1511 requested_allocations.sort_unstable();
1512 requested_allocations.dedup();
1513
1514 if !requested_allocations.is_empty() {
1515 let owned = sqlx::query!(
1516 "SELECT COUNT(*) AS count FROM node_allocations
1517 WHERE node_allocations.uuid = ANY($1) AND node_allocations.node_uuid = $2",
1518 &requested_allocations,
1519 options.destination_node.uuid
1520 )
1521 .fetch_one(state.database.read())
1522 .await?
1523 .count
1524 .unwrap_or(0);
1525
1526 if owned != requested_allocations.len() as i64 {
1527 return Err(DisplayError::new(
1528 "all allocations must belong to the destination node",
1529 )
1530 .with_status(axum::http::StatusCode::BAD_REQUEST)
1531 .into());
1532 }
1533 }
1534
1535 let mut transaction = state.database.write().begin().await?;
1536
1537 let destination_allocation_uuid = if let Some(allocation_uuid) = options.allocation_uuid {
1538 match sqlx::query!(
1539 "INSERT INTO server_allocations (server_uuid, allocation_uuid)
1540 VALUES ($1, $2)
1541 ON CONFLICT DO NOTHING
1542 RETURNING uuid",
1543 self.uuid,
1544 allocation_uuid
1545 )
1546 .fetch_optional(&mut *transaction)
1547 .await?
1548 {
1549 Some(row) => Some(row.uuid),
1550 None => {
1551 return Err(DisplayError::new(
1552 "the primary allocation is already assigned to a server",
1553 )
1554 .with_status(axum::http::StatusCode::CONFLICT)
1555 .into());
1556 }
1557 }
1558 } else {
1559 None
1560 };
1561
1562 sqlx::query!(
1563 "UPDATE servers
1564 SET destination_node_uuid = $2, destination_allocation_uuid = $3
1565 WHERE servers.uuid = $1",
1566 self.uuid,
1567 options.destination_node.uuid,
1568 destination_allocation_uuid
1569 )
1570 .execute(&mut *transaction)
1571 .await?;
1572
1573 if !options.allocation_uuids.is_empty() {
1574 sqlx::query!(
1575 "INSERT INTO server_allocations (server_uuid, allocation_uuid)
1576 SELECT $1, UNNEST($2::uuid[])
1577 ON CONFLICT DO NOTHING",
1578 self.uuid,
1579 &options.allocation_uuids
1580 )
1581 .execute(&mut *transaction)
1582 .await?;
1583 }
1584
1585 let token = options.destination_node.create_jwt(
1586 &state.database,
1587 &state.jwt,
1588 &crate::jwt::BasePayload {
1589 scope: "transfer".into(),
1590 issuer: "panel".into(),
1591 subject: Some(self.uuid.to_compact_string()),
1592 audience: Vec::new(),
1593 expiration_time: Some(chrono::Utc::now().timestamp() + 600),
1594 not_before: None,
1595 issued_at: Some(chrono::Utc::now().timestamp()),
1596 jwt_id: self.node.uuid.to_compact_string(),
1597 },
1598 )?;
1599
1600 let url = options.destination_node.url("/api/transfers");
1601
1602 match self
1605 .node
1606 .fetch_cached(&state.database)
1607 .await?
1608 .api_client(&state.database)
1609 .await?
1610 .post_servers_server_transfer(
1611 self.uuid,
1612 &wings_api::servers_server_transfer::post::RequestBody {
1613 url: url.to_compact_string(),
1614 token: format!("Bearer {token}").into(),
1615 backups: options.backups,
1616 delete_backups: options.delete_source_backups,
1617 archive_format: options.archive_format,
1618 compression_level: options.compression_level,
1619 multiplex_streams: options.multiplex_channels,
1620 },
1621 )
1622 .await
1623 {
1624 Ok(_) => {}
1625 Err(err) => {
1626 transaction.rollback().await?;
1627
1628 return Err(err.into());
1629 }
1630 }
1631
1632 transaction.commit().await?;
1633
1634 Self::invalidate_cached(&state.database, self.uuid).await;
1635
1636 Server::get_event_emitter().emit(
1637 state.clone(),
1638 ServerEvent::TransferStarted {
1639 server: Box::new(self),
1640 destination_node: Box::new(options.destination_node),
1641 destination_allocation: destination_allocation_uuid,
1642 destination_allocations: options.allocation_uuids,
1643 },
1644 );
1645
1646 Ok(())
1647 }
1648
1649 fn scope_allows_console_streams(scope: Option<&[compact_str::CompactString]>) -> bool {
1650 scope.is_none_or(|scope| scope.iter().any(|p| p == "control.read-console"))
1651 }
1652
1653 fn push_granted_permissions<'a>(
1654 permissions: &mut Vec<&'a str>,
1655 granted: impl Iterator<Item = &'a compact_str::CompactString>,
1656 scope: Option<&[compact_str::CompactString]>,
1657 settings: &crate::settings::AppSettings,
1658 ) {
1659 for permission in granted {
1660 if scope.is_some_and(|scope| !scope.contains(permission))
1661 || permissions.contains(&permission.as_str())
1662 {
1663 continue;
1664 }
1665
1666 if permission == "control.read-console" {
1667 if settings.server.allow_viewing_installation_logs {
1668 permissions.push("admin.websocket.install");
1669 }
1670 if settings.server.allow_viewing_transfer_progress {
1671 permissions.push("admin.websocket.transfer");
1672 }
1673 }
1674
1675 permissions.push(permission.as_str());
1676 }
1677 }
1678
1679 pub fn wings_permissions<'a>(
1680 &'a self,
1681 settings: &crate::settings::AppSettings,
1682 user: &'a super::user::User,
1683 scope: &'a crate::models::user::CredentialScope,
1684 ) -> Vec<&'a str> {
1685 let scope = scope.server_permissions();
1686 let mut permissions = vec!["websocket.connect", "meta.calagopus"];
1687
1688 if user.admin {
1689 permissions.reserve(scope.map_or(1, |s| s.len()) + 3);
1690
1691 crate::utils::push_scope_or_star(&mut permissions, scope);
1692
1693 if Self::scope_allows_console_streams(scope) {
1694 permissions.push("admin.websocket.errors");
1695 permissions.push("admin.websocket.install");
1696 permissions.push("admin.websocket.transfer");
1697 }
1698
1699 return permissions;
1700 }
1701
1702 if self.owner.uuid == user.uuid {
1703 permissions.reserve(scope.map_or(1, |s| s.len()) + 2);
1704
1705 if Self::scope_allows_console_streams(scope) {
1706 if settings.server.allow_viewing_installation_logs {
1707 permissions.push("admin.websocket.install");
1708 }
1709 if settings.server.allow_viewing_transfer_progress {
1710 permissions.push("admin.websocket.transfer");
1711 }
1712 }
1713
1714 crate::utils::push_scope_or_star(&mut permissions, scope);
1715
1716 return permissions;
1717 }
1718
1719 let role_permissions = user
1724 .role
1725 .as_ref()
1726 .map_or(&[][..], |role| role.server_permissions.as_slice());
1727 let subuser_permissions = self
1728 .subuser_permissions
1729 .iter()
1730 .flat_map(|permissions| permissions.iter());
1731
1732 permissions.reserve(
1733 role_permissions.len() + self.subuser_permissions.as_ref().map_or(0, |p| p.len()),
1734 );
1735 Self::push_granted_permissions(
1736 &mut permissions,
1737 role_permissions.iter().chain(subuser_permissions),
1738 scope,
1739 settings,
1740 );
1741
1742 permissions
1743 }
1744
1745 pub fn wings_subuser_permissions<'a>(
1746 &self,
1747 settings: &crate::settings::AppSettings,
1748 subuser: &'a super::server_subuser::ServerSubuser,
1749 scope: &'a crate::models::user::CredentialScope,
1750 ) -> Vec<&'a str> {
1751 let scope = scope.server_permissions();
1752 let mut permissions = vec!["websocket.connect", "meta.calagopus"];
1753
1754 if subuser.user.admin {
1755 permissions.reserve(scope.map_or(1, |s| s.len()) + 3);
1756
1757 crate::utils::push_scope_or_star(&mut permissions, scope);
1758
1759 if Self::scope_allows_console_streams(scope) {
1760 permissions.push("admin.websocket.errors");
1761 permissions.push("admin.websocket.install");
1762 permissions.push("admin.websocket.transfer");
1763 }
1764
1765 return permissions;
1766 }
1767
1768 let role_permissions = subuser
1769 .user
1770 .role
1771 .as_ref()
1772 .map_or(&[][..], |role| role.server_permissions.as_slice());
1773
1774 permissions.reserve(subuser.permissions.len() + role_permissions.len() + 1);
1775 Self::push_granted_permissions(
1776 &mut permissions,
1777 role_permissions.iter().chain(subuser.permissions.iter()),
1778 scope,
1779 settings,
1780 );
1781
1782 permissions
1783 }
1784
1785 pub async fn feature_limits(
1787 &self,
1788 state: &crate::State,
1789 ) -> Result<ApiServerFeatureLimits, anyhow::Error> {
1790 let feature_limits = ApiServerFeatureLimits::init_hooks(self, state).await?;
1791
1792 let feature_limits = finish_extendible!(
1793 ApiServerFeatureLimits {
1794 allocations: self.allocation_limit,
1795 databases: self.database_limit,
1796 backups: self.backup_limit,
1797 schedules: self.schedule_limit,
1798 },
1799 feature_limits,
1800 state
1801 )?;
1802
1803 Ok(feature_limits)
1804 }
1805
1806 pub async fn backup_configuration(
1807 &self,
1808 database: &crate::database::Database,
1809 ) -> Option<super::backup_configuration::BackupConfiguration> {
1810 if let Some(backup_configuration) = &self.backup_configuration
1811 && let Ok(backup_configuration) = backup_configuration.fetch_cached(database).await
1812 {
1813 return Some(backup_configuration);
1814 }
1815
1816 let node = self.node.fetch_cached(database).await.ok()?;
1817
1818 if let Some(backup_configuration) = node.backup_configuration
1819 && let Ok(backup_configuration) = backup_configuration.fetch_cached(database).await
1820 {
1821 return Some(backup_configuration);
1822 }
1823
1824 if let Some(backup_configuration) = node.location.backup_configuration
1825 && let Ok(backup_configuration) = backup_configuration.fetch_cached(database).await
1826 {
1827 return Some(backup_configuration);
1828 }
1829
1830 None
1831 }
1832
1833 pub fn is_ignored(&mut self, path: impl AsRef<std::path::Path>, is_dir: bool) -> bool {
1834 if let Some(ignored_files) = &self.subuser_ignored_files {
1835 if let Some(overrides) = &self.subuser_ignored_files_overrides {
1836 return is_path_ignored(overrides, path, is_dir);
1837 }
1838
1839 let mut override_builder = ignore::overrides::OverrideBuilder::new("/");
1840
1841 for file in ignored_files {
1842 override_builder.add(file).ok();
1843 }
1844
1845 match override_builder.build() {
1846 Ok(overrides) => {
1847 let ignored = is_path_ignored(&overrides, path, is_dir);
1848 self.subuser_ignored_files_overrides = Some(Box::new(overrides));
1849
1850 return ignored;
1851 }
1852 Err(err) => {
1853 tracing::error!(
1854 server = %self.uuid,
1855 "failed to compile subuser ignored files, denying access: {:#?}",
1856 err
1857 );
1858
1859 return true;
1860 }
1861 }
1862 }
1863
1864 false
1865 }
1866
1867 pub fn is_ignored_either(&mut self, path: impl AsRef<std::path::Path>) -> bool {
1868 let path = path.as_ref();
1869
1870 self.is_ignored(path, false) || self.is_ignored(path, true)
1871 }
1872
1873 #[inline]
1874 pub async fn into_remote_api_object(
1875 self,
1876 database: &crate::database::Database,
1877 ) -> Result<RemoteApiServer, anyhow::Error> {
1878 let (variables, backups, schedules, mounts, allocations, firewall_rules) = tokio::try_join!(
1879 sqlx::query!(
1880 "SELECT nest_egg_variables.env_variable, COALESCE(server_variables.value, nest_egg_variables.default_value) AS value
1881 FROM nest_egg_variables
1882 LEFT JOIN server_variables ON server_variables.variable_uuid = nest_egg_variables.uuid AND server_variables.server_uuid = $1
1883 WHERE nest_egg_variables.egg_uuid = $2",
1884 self.uuid,
1885 self.egg.uuid
1886 )
1887 .fetch_all(database.read()),
1888 sqlx::query!(
1889 "SELECT server_backups.uuid
1890 FROM server_backups
1891 WHERE server_backups.server_uuid = $1",
1892 self.uuid
1893 )
1894 .fetch_all(database.read()),
1895 sqlx::query!(
1896 "SELECT server_schedules.uuid, server_schedules.triggers, server_schedules.condition
1897 FROM server_schedules
1898 WHERE server_schedules.server_uuid = $1 AND server_schedules.enabled",
1899 self.uuid
1900 )
1901 .fetch_all(database.read()),
1902 sqlx::query!(
1903 "SELECT mounts.source, mounts.target, mounts.read_only
1904 FROM server_mounts
1905 JOIN mounts ON mounts.uuid = server_mounts.mount_uuid
1906 WHERE server_mounts.server_uuid = $1",
1907 self.uuid
1908 )
1909 .fetch_all(database.read()),
1910 sqlx::query!(
1911 "SELECT node_allocations.ip, node_allocations.port
1912 FROM server_allocations
1913 JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
1914 WHERE server_allocations.server_uuid = $1",
1915 self.uuid
1916 )
1917 .fetch_all(database.read()),
1918 firewall::fetch_raw_rules(database, self.uuid),
1919 )?;
1920
1921 let mut futures = Vec::new();
1922 futures.reserve_exact(schedules.len());
1923
1924 for schedule in &schedules {
1925 futures.push(
1926 sqlx::query!(
1927 "SELECT server_schedule_steps.uuid, server_schedule_steps.schedule_uuid, server_schedule_steps.action
1928 FROM server_schedule_steps
1929 WHERE server_schedule_steps.schedule_uuid = $1
1930 ORDER BY server_schedule_steps.order_, server_schedule_steps.created",
1931 schedule.uuid
1932 )
1933 .fetch_all(database.read()),
1934 );
1935 }
1936
1937 let results = futures_util::future::try_join_all(futures).await?;
1938 let mut schedule_steps = HashMap::new();
1939 schedule_steps.reserve(schedules.len());
1940
1941 for (i, steps) in results.into_iter().enumerate() {
1942 schedule_steps.insert(schedules[i].uuid, steps);
1943 }
1944
1945 Ok(RemoteApiServer {
1946 settings: wings_api::ServerConfiguration {
1947 uuid: self.uuid,
1948 start_on_completion: None,
1949 meta: wings_api::ServerConfigurationMeta {
1950 name: self.name,
1951 description: self.description.unwrap_or_default(),
1952 },
1953 suspended: self.suspended,
1954 invocation: self.startup,
1955 entrypoint: None,
1956 skip_egg_scripts: false,
1957 environment: variables
1958 .into_iter()
1959 .map(|v| {
1960 (
1961 v.env_variable.into(),
1962 serde_json::Value::String(v.value.unwrap_or_default()),
1963 )
1964 })
1965 .collect(),
1966 labels: IndexMap::new(),
1967 backups: backups.into_iter().map(|b| b.uuid).collect(),
1968 schedules: schedules
1969 .into_iter()
1970 .map(|s| {
1971 Ok::<_, serde_json::Error>(wings_api::Schedule {
1972 uuid: s.uuid,
1973 triggers: s.triggers,
1974 condition: s.condition,
1975 actions: schedule_steps
1976 .remove(&s.uuid)
1977 .unwrap_or_default()
1978 .into_iter()
1979 .map(|step| {
1980 serde_json::to_value(wings_api::ScheduleAction {
1981 uuid: step.uuid,
1982 inner: serde_json::from_value(step.action)?,
1983 })
1984 })
1985 .try_collect_vec()?,
1986 })
1987 })
1988 .try_collect_vec()?,
1989 allocations: wings_api::ServerConfigurationAllocations {
1990 force_outgoing_ip: self.egg.force_outgoing_ip,
1991 default: self.allocation.map(|a| {
1992 wings_api::ServerConfigurationAllocationsDefault {
1993 ip: compact_str::format_compact!("{}", a.allocation.ip.ip()),
1994 port: a.allocation.port as u32,
1995 }
1996 }),
1997 mappings: {
1998 let mut mappings = IndexMap::new();
1999 for allocation in allocations {
2000 mappings
2001 .entry(compact_str::format_compact!("{}", allocation.ip.ip()))
2002 .or_insert_with(Vec::new)
2003 .push(allocation.port as u32);
2004 }
2005
2006 mappings
2007 },
2008 },
2009 build: wings_api::ServerConfigurationBuild {
2010 memory_limit: self.memory,
2011 overhead_memory: self.memory_overhead,
2012 swap: self.swap,
2013 io_weight: self.io_weight.map(|w| w as u32),
2014 cpu_limit: self.cpu as i64,
2015 disk_space: self.disk as u64,
2016 threads: {
2017 let mut threads = compact_str::CompactString::default();
2018 for cpu in &self.pinned_cpus {
2019 if !threads.is_empty() {
2020 threads.push(',');
2021 }
2022 threads.push_str(&cpu.to_string());
2023 }
2024
2025 if threads.is_empty() {
2026 None
2027 } else {
2028 Some(threads)
2029 }
2030 },
2031 oom_disabled: false,
2032 },
2033 mounts: mounts
2034 .into_iter()
2035 .map(|m| wings_api::Mount {
2036 source: m.source.into(),
2037 target: m.target.into(),
2038 read_only: m.read_only,
2039 })
2040 .collect(),
2041 firewall: firewall::decode_rules(firewall_rules)?
2042 .into_iter()
2043 .map(Into::into)
2044 .collect(),
2045 egg: wings_api::ServerConfigurationEgg {
2046 id: self.egg.uuid,
2047 file_denylist: self.egg.file_denylist,
2048 },
2049 container: wings_api::ServerConfigurationContainer {
2050 image: self.image,
2051 timezone: self.timezone,
2052 hugepages_passthrough_enabled: self.hugepages_passthrough_enabled,
2053 kvm_passthrough_enabled: self.kvm_passthrough_enabled,
2054 seccomp: wings_api::ServerConfigurationContainerSeccomp {
2055 remove_allowed: vec![],
2056 },
2057 },
2058 auto_kill: self.auto_kill,
2059 auto_start_behavior: self.auto_start_behavior.into(),
2060 features: wings_api::ServerConfigurationFeatures {
2061 startup_cpu_boost: None,
2062 runtime_cpu_boost: None,
2063 },
2064 },
2065 process_configuration: super::nest_egg::ProcessConfiguration {
2066 startup: self.egg.config_startup,
2067 stop: self.egg.config_stop,
2068 configs: self.egg.config_files,
2069 },
2070 })
2071 }
2072}
2073
2074#[async_trait::async_trait]
2075impl super::IntoAdminApiObject for Server {
2076 type AdminApiObject = AdminApiServer;
2077 type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
2078
2079 async fn into_admin_api_object<'a>(
2080 self,
2081 state: &crate::State,
2082 storage_url_retriever: Self::ExtraArgs<'a>,
2083 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
2084 let feature_limits = ApiServerFeatureLimits::init_hooks(&self, state).await?;
2085 let api_object = AdminApiServer::init_hooks(&self, state).await?;
2086
2087 let allocation_uuid = self.allocation.as_ref().map(|a| a.uuid);
2088 let allocation = match self.allocation {
2089 Some(a) => Some(a.into_api_object(state, allocation_uuid).await?),
2090 None => None,
2091 };
2092
2093 let feature_limits = finish_extendible!(
2094 ApiServerFeatureLimits {
2095 allocations: self.allocation_limit,
2096 databases: self.database_limit,
2097 backups: self.backup_limit,
2098 schedules: self.schedule_limit,
2099 },
2100 feature_limits,
2101 state
2102 )?;
2103
2104 let (node, backup_configuration, egg) = tokio::join!(
2105 async {
2106 match self.node.fetch_cached(&state.database).await {
2107 Ok(node) => Ok(node.into_admin_api_object(state, ()).await?),
2108 Err(err) => Err(err),
2109 }
2110 },
2111 async {
2112 if let Some(backup_configuration) = self.backup_configuration {
2113 if let Ok(backup_configuration) =
2114 backup_configuration.fetch_cached(&state.database).await
2115 {
2116 backup_configuration
2117 .into_admin_api_object(state, ())
2118 .await
2119 .ok()
2120 } else {
2121 None
2122 }
2123 } else {
2124 None
2125 }
2126 },
2127 self.egg.into_admin_api_object(state, ())
2128 );
2129
2130 let api_object = finish_extendible!(
2131 AdminApiServer {
2132 uuid: self.uuid,
2133 uuid_short: format!("{:08x}", self.uuid_short).into(),
2134 external_id: self.external_id,
2135 allocation,
2136 node: node?,
2137 owner: self
2138 .owner
2139 .into_admin_api_object(state, storage_url_retriever)
2140 .await?,
2141 egg: egg?,
2142 nest: self.nest.into_admin_api_object(state, ()).await?,
2143 backup_configuration,
2144 status: self.status,
2145 is_suspended: self.suspended,
2146 is_transferring: self.destination_node.is_some(),
2147 name: self.name,
2148 description: self.description,
2149 limits: AdminApiServerLimits {
2150 cpu: self.cpu,
2151 memory: self.memory,
2152 memory_overhead: self.memory_overhead,
2153 swap: self.swap,
2154 disk: self.disk,
2155 io_weight: self.io_weight,
2156 },
2157 pinned_cpus: self.pinned_cpus,
2158 feature_limits,
2159 startup: self.startup,
2160 image: self.image,
2161 auto_kill: self.auto_kill,
2162 auto_start_behavior: self.auto_start_behavior,
2163 timezone: self.timezone,
2164 hugepages_passthrough_enabled: self.hugepages_passthrough_enabled,
2165 kvm_passthrough_enabled: self.kvm_passthrough_enabled,
2166 created: self.created.and_utc(),
2167 },
2168 api_object,
2169 state
2170 )?;
2171
2172 Ok(api_object)
2173 }
2174}
2175
2176#[async_trait::async_trait]
2177impl super::IntoApiObject for Server {
2178 type ApiObject = ApiServer;
2179 type ExtraArgs<'a> = &'a super::user::User;
2180
2181 async fn into_api_object<'a>(
2182 self,
2183 state: &crate::State,
2184 user: Self::ExtraArgs<'a>,
2185 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
2186 let feature_limits = ApiServerFeatureLimits::init_hooks(&self, state).await?;
2187 let api_object = ApiServer::init_hooks(&self, state).await?;
2188
2189 let allocation_uuid = self.allocation.as_ref().map(|a| a.uuid);
2190 let allocation = match self.allocation {
2191 Some(a) => Some(a.into_api_object(state, allocation_uuid).await?),
2192 None => None,
2193 };
2194
2195 let (node, egg_configuration) = tokio::try_join!(
2196 self.node.fetch_cached(&state.database),
2197 self.egg.configuration(&state.database)
2198 )?;
2199
2200 let feature_limits = finish_extendible!(
2201 ApiServerFeatureLimits {
2202 allocations: self.allocation_limit,
2203 databases: self.database_limit,
2204 backups: self.backup_limit,
2205 schedules: self.schedule_limit,
2206 },
2207 feature_limits,
2208 state
2209 )?;
2210
2211 let api_object = finish_extendible!(
2212 ApiServer {
2213 uuid: self.uuid,
2214 uuid_short: format!("{:08x}", self.uuid_short).into(),
2215 allocation,
2216 egg: self.egg.into_api_object(state, ()).await?,
2217 egg_configuration: egg_configuration.into_api_object(state, ()).await?,
2218 permissions: if user.admin {
2219 vec!["*".into()]
2220 } else {
2221 self.subuser_permissions
2222 .map_or_else(|| vec!["*".into()], |p| p.to_vec())
2223 },
2224 ignored_files: self.subuser_ignored_files.unwrap_or_default(),
2225 location_uuid: node.location.uuid,
2226 location_name: node.location.name,
2227 location_flag: node.location.flag,
2228 node_uuid: node.uuid,
2229 node_name: node.name,
2230 node_maintenance_enabled: node.maintenance_enabled,
2231 sftp_host: node.sftp_host.unwrap_or_else(|| {
2232 node.public_url
2233 .unwrap_or(node.url)
2234 .host_str()
2235 .unwrap_or("unknown.sftp.host")
2236 .into()
2237 }),
2238 sftp_port: node.sftp_port,
2239 status: self.status,
2240 is_suspended: self.suspended,
2241 is_owner: self.owner.uuid == user.uuid,
2242 is_transferring: self.destination_node.is_some(),
2243 name: self.name,
2244 description: self.description,
2245 limits: ApiServerLimits {
2246 cpu: self.cpu,
2247 memory: self.memory,
2248 swap: self.swap,
2249 disk: self.disk,
2250 },
2251 feature_limits,
2252 startup: self.startup,
2253 image: self.image,
2254 auto_kill: self.auto_kill,
2255 auto_start_behavior: self.auto_start_behavior,
2256 timezone: self.timezone,
2257 created: self.created.and_utc(),
2258 },
2259 api_object,
2260 state
2261 )?;
2262
2263 Ok(api_object)
2264 }
2265}
2266
2267#[async_trait::async_trait]
2268impl ByUuid for Server {
2269 async fn by_uuid(
2270 database: &crate::database::Database,
2271 uuid: uuid::Uuid,
2272 ) -> Result<Self, crate::database::DatabaseError> {
2273 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
2274 r#"
2275 SELECT {}
2276 FROM servers
2277 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
2278 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
2279 JOIN users ON users.uuid = servers.owner_uuid
2280 LEFT JOIN roles ON roles.uuid = users.role_uuid
2281 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
2282 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
2283 WHERE servers.uuid = $1
2284 "#,
2285 Self::columns_sql(None)
2286 )))
2287 .bind(uuid)
2288 .fetch_one(database.read())
2289 .await?;
2290
2291 Self::map(None, &row)
2292 }
2293
2294 async fn by_uuid_with_transaction(
2295 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
2296 uuid: uuid::Uuid,
2297 ) -> Result<Self, crate::database::DatabaseError> {
2298 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
2299 r#"
2300 SELECT {}
2301 FROM servers
2302 LEFT JOIN server_allocations ON server_allocations.uuid = servers.allocation_uuid
2303 LEFT JOIN node_allocations ON node_allocations.uuid = server_allocations.allocation_uuid
2304 JOIN users ON users.uuid = servers.owner_uuid
2305 LEFT JOIN roles ON roles.uuid = users.role_uuid
2306 JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
2307 JOIN nests ON nests.uuid = nest_eggs.nest_uuid
2308 WHERE servers.uuid = $1
2309 "#,
2310 Self::columns_sql(None)
2311 )))
2312 .bind(uuid)
2313 .fetch_one(&mut **transaction)
2314 .await?;
2315
2316 Self::map(None, &row)
2317 }
2318}
2319
2320#[derive(ToSchema, Validate, Deserialize)]
2321pub struct CreateServerOptions {
2322 #[garde(skip)]
2323 pub node_uuid: uuid::Uuid,
2324 #[garde(skip)]
2325 pub owner_uuid: uuid::Uuid,
2326 #[garde(skip)]
2327 pub egg_uuid: uuid::Uuid,
2328 #[garde(skip)]
2329 pub backup_configuration_uuid: Option<uuid::Uuid>,
2330
2331 #[garde(skip)]
2332 pub allocation_uuid: Option<uuid::Uuid>,
2333 #[garde(skip)]
2334 pub allocation_uuids: Vec<uuid::Uuid>,
2335
2336 #[garde(skip)]
2337 pub start_on_completion: bool,
2338 #[garde(skip)]
2339 pub skip_installer: bool,
2340
2341 #[garde(length(chars, min = 1, max = 255))]
2342 #[schema(min_length = 1, max_length = 255)]
2343 pub external_id: Option<compact_str::CompactString>,
2344 #[garde(length(chars, min = 1, max = 255))]
2345 #[schema(min_length = 1, max_length = 255)]
2346 pub name: compact_str::CompactString,
2347 #[garde(length(chars, min = 1, max = 1024))]
2348 #[schema(min_length = 1, max_length = 1024)]
2349 pub description: Option<compact_str::CompactString>,
2350
2351 #[garde(dive)]
2352 pub limits: AdminApiServerLimits,
2353 #[garde(inner(range(min = 0)))]
2354 pub pinned_cpus: Vec<i16>,
2355
2356 #[garde(length(chars, min = 1, max = 8192))]
2357 #[schema(min_length = 1, max_length = 8192)]
2358 pub startup: compact_str::CompactString,
2359 #[garde(length(chars, min = 2, max = 255))]
2360 #[schema(min_length = 2, max_length = 255)]
2361 pub image: compact_str::CompactString,
2362 #[garde(skip)]
2363 #[schema(value_type = Option<String>)]
2364 pub timezone: Option<chrono_tz::Tz>,
2365
2366 #[garde(skip)]
2367 pub hugepages_passthrough_enabled: bool,
2368 #[garde(skip)]
2369 pub kvm_passthrough_enabled: bool,
2370
2371 #[garde(dive)]
2372 pub feature_limits: ApiServerFeatureLimits,
2373 #[garde(skip)]
2374 pub variables: HashMap<uuid::Uuid, compact_str::CompactString>,
2375}
2376
2377#[async_trait::async_trait]
2378impl CreatableModel for Server {
2379 type CreateOptions<'a> = CreateServerOptions;
2380 type CreateResult = Self;
2381
2382 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
2383 static CREATE_LISTENERS: LazyLock<CreateListenerList<Server>> =
2384 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
2385
2386 &CREATE_LISTENERS
2387 }
2388
2389 async fn create_with_transaction(
2390 _state: &crate::State,
2391 _options: Self::CreateOptions<'_>,
2392 _transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
2393 ) -> Result<Self, crate::database::DatabaseError> {
2394 Err(anyhow::anyhow!("create_with_transaction is not supported for Server").into())
2395 }
2396
2397 async fn create(
2398 state: &crate::State,
2399 mut options: Self::CreateOptions<'_>,
2400 ) -> Result<Self, crate::database::DatabaseError> {
2401 options.validate()?;
2402
2403 let node = super::node::Node::by_uuid_optional(&state.database, options.node_uuid)
2404 .await?
2405 .ok_or(crate::database::InvalidRelationError("node"))?;
2406
2407 super::user::User::by_uuid_optional(&state.database, options.owner_uuid)
2408 .await?
2409 .ok_or(crate::database::InvalidRelationError("owner"))?;
2410
2411 super::nest_egg::NestEgg::by_uuid_optional(&state.database, options.egg_uuid)
2412 .await?
2413 .ok_or(crate::database::InvalidRelationError("egg"))?;
2414
2415 if let Some(backup_configuration_uuid) = options.backup_configuration_uuid {
2416 super::backup_configuration::BackupConfiguration::by_uuid_optional(
2417 &state.database,
2418 backup_configuration_uuid,
2419 )
2420 .await?
2421 .ok_or(crate::database::InvalidRelationError(
2422 "backup_configuration",
2423 ))?;
2424 }
2425
2426 let mut transaction = state.database.write().begin().await?;
2427 let mut attempts = 0;
2428
2429 loop {
2430 let server_uuid = uuid::Uuid::new_v4();
2431 let uuid_short = server_uuid.as_fields().0 as i32;
2432
2433 let mut query_builder = InsertQueryBuilder::new("servers");
2434
2435 Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
2436 .await?;
2437
2438 query_builder
2439 .set("uuid", server_uuid)
2440 .set("uuid_short", uuid_short)
2441 .set("external_id", &options.external_id)
2442 .set("node_uuid", options.node_uuid)
2443 .set("owner_uuid", options.owner_uuid)
2444 .set("egg_uuid", options.egg_uuid)
2445 .set(
2446 "backup_configuration_uuid",
2447 options.backup_configuration_uuid,
2448 )
2449 .set("name", &options.name)
2450 .set("description", &options.description)
2451 .set(
2452 "status",
2453 if options.skip_installer {
2454 None::<ServerStatus>
2455 } else {
2456 Some(ServerStatus::Installing)
2457 },
2458 )
2459 .set("memory", options.limits.memory)
2460 .set("memory_overhead", options.limits.memory_overhead)
2461 .set("swap", options.limits.swap)
2462 .set("disk", options.limits.disk)
2463 .set("io_weight", options.limits.io_weight)
2464 .set("cpu", options.limits.cpu)
2465 .set("pinned_cpus", &options.pinned_cpus)
2466 .set("startup", &options.startup)
2467 .set("image", &options.image)
2468 .set("timezone", options.timezone.as_ref().map(|t| t.name()))
2469 .set(
2470 "hugepages_passthrough_enabled",
2471 options.hugepages_passthrough_enabled,
2472 )
2473 .set("kvm_passthrough_enabled", options.kvm_passthrough_enabled)
2474 .set("allocation_limit", options.feature_limits.allocations)
2475 .set("database_limit", options.feature_limits.databases)
2476 .set("backup_limit", options.feature_limits.backups)
2477 .set("schedule_limit", options.feature_limits.schedules);
2478
2479 match query_builder
2480 .returning("uuid")
2481 .fetch_one(&mut *transaction)
2482 .await
2483 {
2484 Ok(_) => {
2485 let allocation_uuid: Option<uuid::Uuid> =
2486 if let Some(allocation_uuid) = options.allocation_uuid {
2487 let row = sqlx::query(
2488 r#"
2489 INSERT INTO server_allocations (server_uuid, allocation_uuid)
2490 VALUES ($1, $2)
2491 RETURNING uuid
2492 "#,
2493 )
2494 .bind(server_uuid)
2495 .bind(allocation_uuid)
2496 .fetch_one(&mut *transaction)
2497 .await?;
2498
2499 Some(row.get("uuid"))
2500 } else {
2501 None
2502 };
2503
2504 for allocation_uuid in &options.allocation_uuids {
2505 sqlx::query(
2506 r#"
2507 INSERT INTO server_allocations (server_uuid, allocation_uuid)
2508 VALUES ($1, $2)
2509 "#,
2510 )
2511 .bind(server_uuid)
2512 .bind(allocation_uuid)
2513 .execute(&mut *transaction)
2514 .await?;
2515 }
2516
2517 sqlx::query(
2518 r#"
2519 UPDATE servers
2520 SET allocation_uuid = $1
2521 WHERE servers.uuid = $2
2522 "#,
2523 )
2524 .bind(allocation_uuid)
2525 .bind(server_uuid)
2526 .execute(&mut *transaction)
2527 .await?;
2528
2529 for (variable_uuid, value) in &options.variables {
2530 sqlx::query(
2531 r#"
2532 INSERT INTO server_variables (server_uuid, variable_uuid, value)
2533 VALUES ($1, $2, $3)
2534 "#,
2535 )
2536 .bind(server_uuid)
2537 .bind(variable_uuid)
2538 .bind(value.as_str())
2539 .execute(&mut *transaction)
2540 .await?;
2541 }
2542
2543 let mut result =
2544 Self::by_uuid_with_transaction(&mut transaction, server_uuid).await?;
2545
2546 Self::run_after_create_handlers(&mut result, &options, state, &mut transaction)
2547 .await?;
2548
2549 transaction.commit().await?;
2550
2551 if let Err(err) = node
2552 .api_client(&state.database)
2553 .await?
2554 .post_servers(&wings_api::servers::post::RequestBody {
2555 uuid: server_uuid,
2556 start_on_completion: options.start_on_completion,
2557 skip_scripts: options.skip_installer,
2558 })
2559 .await
2560 {
2561 tracing::error!(server = %server_uuid, node = %node.uuid, "failed to create server: {:?}", err);
2562
2563 sqlx::query!("DELETE FROM servers WHERE servers.uuid = $1", server_uuid)
2564 .execute(state.database.write())
2565 .await?;
2566
2567 return Err(err.into());
2568 }
2569
2570 return Ok(result);
2571 }
2572 Err(_) if attempts < 3 => {
2573 attempts += 1;
2574 transaction.rollback().await?;
2575 transaction = state.database.write().begin().await?;
2576
2577 continue;
2578 }
2579 Err(err) => {
2580 transaction.rollback().await?;
2581 return Err(err.into());
2582 }
2583 }
2584 }
2585 }
2586}
2587
2588fn validate_auto_kill(
2589 value: &Option<wings_api::ServerConfigurationAutoKill>,
2590 _context: &(),
2591) -> garde::Result {
2592 match value {
2593 Some(auto_kill) if !(1..=3600).contains(&auto_kill.seconds) => Err(garde::Error::new(
2594 "auto kill seconds must be between 1 and 3600",
2595 )),
2596 _ => Ok(()),
2597 }
2598}
2599
2600#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
2601pub struct UpdateServerOptions {
2602 #[garde(skip)]
2603 pub owner_uuid: Option<uuid::Uuid>,
2604 #[garde(skip)]
2605 pub egg_uuid: Option<uuid::Uuid>,
2606 #[garde(skip)]
2607 #[serde(
2608 default,
2609 skip_serializing_if = "Option::is_none",
2610 with = "::serde_with::rust::double_option"
2611 )]
2612 pub backup_configuration_uuid: Option<Option<uuid::Uuid>>,
2613
2614 #[garde(skip)]
2615 pub suspended: Option<bool>,
2616
2617 #[garde(length(chars, min = 1, max = 255))]
2618 #[schema(min_length = 1, max_length = 255)]
2619 #[serde(
2620 default,
2621 skip_serializing_if = "Option::is_none",
2622 with = "::serde_with::rust::double_option"
2623 )]
2624 pub external_id: Option<Option<compact_str::CompactString>>,
2625 #[garde(length(chars, min = 1, max = 255))]
2626 #[schema(min_length = 1, max_length = 255)]
2627 pub name: Option<compact_str::CompactString>,
2628 #[garde(length(chars, min = 1, max = 1024))]
2629 #[schema(min_length = 1, max_length = 1024)]
2630 #[serde(
2631 default,
2632 skip_serializing_if = "Option::is_none",
2633 with = "::serde_with::rust::double_option"
2634 )]
2635 pub description: Option<Option<compact_str::CompactString>>,
2636
2637 #[garde(dive)]
2638 pub limits: Option<AdminApiServerLimits>,
2639 #[garde(inner(inner(range(min = 0))))]
2640 pub pinned_cpus: Option<Vec<i16>>,
2641
2642 #[garde(length(chars, min = 1, max = 8192))]
2643 #[schema(min_length = 1, max_length = 8192)]
2644 pub startup: Option<compact_str::CompactString>,
2645 #[garde(length(chars, min = 2, max = 255))]
2646 #[schema(min_length = 2, max_length = 255)]
2647 pub image: Option<compact_str::CompactString>,
2648 #[garde(custom(validate_auto_kill))]
2649 #[schema(inline)]
2650 pub auto_kill: Option<wings_api::ServerConfigurationAutoKill>,
2651 #[garde(skip)]
2652 pub auto_start_behavior: Option<ServerAutoStartBehavior>,
2653 #[garde(skip)]
2654 #[schema(value_type = Option<Option<String>>)]
2655 #[serde(
2656 default,
2657 skip_serializing_if = "Option::is_none",
2658 with = "::serde_with::rust::double_option"
2659 )]
2660 pub timezone: Option<Option<chrono_tz::Tz>>,
2661
2662 #[garde(skip)]
2663 pub hugepages_passthrough_enabled: Option<bool>,
2664 #[garde(skip)]
2665 pub kvm_passthrough_enabled: Option<bool>,
2666
2667 #[garde(dive)]
2668 pub feature_limits: Option<ApiServerFeatureLimits>,
2669}
2670
2671#[async_trait::async_trait]
2672impl UpdatableModel for Server {
2673 type UpdateOptions = UpdateServerOptions;
2674
2675 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
2676 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Server>> =
2677 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
2678
2679 &UPDATE_LISTENERS
2680 }
2681
2682 async fn update_with_transaction(
2683 &mut self,
2684 state: &crate::State,
2685 mut options: Self::UpdateOptions,
2686 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
2687 ) -> Result<(), crate::database::DatabaseError> {
2688 options.validate()?;
2689
2690 let owner = if let Some(owner_uuid) = options.owner_uuid {
2691 Some(
2692 super::user::User::by_uuid_optional(&state.database, owner_uuid)
2693 .await?
2694 .ok_or(crate::database::InvalidRelationError("owner"))?,
2695 )
2696 } else {
2697 None
2698 };
2699
2700 let egg = if let Some(egg_uuid) = options.egg_uuid {
2701 Some(
2702 super::nest_egg::NestEgg::by_uuid_optional(&state.database, egg_uuid)
2703 .await?
2704 .ok_or(crate::database::InvalidRelationError("egg"))?,
2705 )
2706 } else {
2707 None
2708 };
2709
2710 let backup_configuration =
2711 if let Some(backup_configuration_uuid) = &options.backup_configuration_uuid {
2712 match backup_configuration_uuid {
2713 Some(uuid) => {
2714 super::backup_configuration::BackupConfiguration::by_uuid_optional(
2715 &state.database,
2716 *uuid,
2717 )
2718 .await?
2719 .ok_or(crate::database::InvalidRelationError(
2720 "backup_configuration",
2721 ))?;
2722
2723 Some(Some(
2724 super::backup_configuration::BackupConfiguration::get_fetchable(*uuid),
2725 ))
2726 }
2727 None => Some(None),
2728 }
2729 } else {
2730 None
2731 };
2732
2733 let mut query_builder = UpdateQueryBuilder::new("servers");
2734
2735 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
2736 .await?;
2737
2738 query_builder
2739 .set("owner_uuid", options.owner_uuid.as_ref())
2740 .set("egg_uuid", options.egg_uuid.as_ref())
2741 .set(
2742 "backup_configuration_uuid",
2743 options
2744 .backup_configuration_uuid
2745 .as_ref()
2746 .map(|u| u.as_ref()),
2747 )
2748 .set("suspended", options.suspended)
2749 .set(
2750 "external_id",
2751 options.external_id.as_ref().map(|e| e.as_ref()),
2752 )
2753 .set("name", options.name.as_ref())
2754 .set(
2755 "description",
2756 options.description.as_ref().map(|d| d.as_ref()),
2757 )
2758 .set("pinned_cpus", options.pinned_cpus.as_ref())
2759 .set("startup", options.startup.as_ref())
2760 .set("image", options.image.as_ref())
2761 .set(
2762 "auto_kill",
2763 options
2764 .auto_kill
2765 .as_ref()
2766 .map(serde_json::to_value)
2767 .transpose()?,
2768 )
2769 .set("auto_start_behavior", options.auto_start_behavior)
2770 .set(
2771 "timezone",
2772 options
2773 .timezone
2774 .as_ref()
2775 .map(|t| t.as_ref().map(|t| t.name())),
2776 )
2777 .set(
2778 "hugepages_passthrough_enabled",
2779 options.hugepages_passthrough_enabled,
2780 )
2781 .set("kvm_passthrough_enabled", options.kvm_passthrough_enabled);
2782
2783 if let Some(limits) = &options.limits {
2784 query_builder
2785 .set("cpu", Some(limits.cpu))
2786 .set("memory", Some(limits.memory))
2787 .set("memory_overhead", Some(limits.memory_overhead))
2788 .set("swap", Some(limits.swap))
2789 .set("disk", Some(limits.disk))
2790 .set("io_weight", Some(limits.io_weight));
2791 }
2792
2793 if let Some(feature_limits) = &options.feature_limits {
2794 query_builder
2795 .set("allocation_limit", Some(feature_limits.allocations))
2796 .set("database_limit", Some(feature_limits.databases))
2797 .set("backup_limit", Some(feature_limits.backups))
2798 .set("schedule_limit", Some(feature_limits.schedules));
2799 }
2800
2801 query_builder.where_eq("uuid", self.uuid);
2802
2803 query_builder.execute(&mut **transaction).await?;
2804
2805 if let Some(owner) = owner {
2806 self.owner = owner;
2807 }
2808 if let Some(egg) = egg {
2809 *self.egg = egg;
2810 }
2811 if let Some(backup_configuration) = backup_configuration {
2812 self.backup_configuration = backup_configuration;
2813 }
2814 if let Some(suspended) = options.suspended {
2815 self.suspended = suspended;
2816 }
2817 if let Some(external_id) = options.external_id {
2818 self.external_id = external_id;
2819 }
2820 if let Some(name) = options.name {
2821 self.name = name;
2822 }
2823 if let Some(description) = options.description {
2824 self.description = description;
2825 }
2826 if let Some(limits) = options.limits {
2827 self.cpu = limits.cpu;
2828 self.memory = limits.memory;
2829 self.memory_overhead = limits.memory_overhead;
2830 self.swap = limits.swap;
2831 self.disk = limits.disk;
2832 self.io_weight = limits.io_weight;
2833 }
2834 if let Some(pinned_cpus) = options.pinned_cpus {
2835 self.pinned_cpus = pinned_cpus;
2836 }
2837 if let Some(startup) = options.startup {
2838 self.startup = startup;
2839 }
2840 if let Some(image) = options.image {
2841 self.image = image;
2842 }
2843 if let Some(auto_kill) = options.auto_kill {
2844 self.auto_kill = auto_kill;
2845 }
2846 if let Some(auto_start_behavior) = options.auto_start_behavior {
2847 self.auto_start_behavior = auto_start_behavior;
2848 }
2849 if let Some(timezone) = options.timezone {
2850 self.timezone = timezone.map(|t| t.name().into());
2851 }
2852 if let Some(hugepages_passthrough_enabled) = options.hugepages_passthrough_enabled {
2853 self.hugepages_passthrough_enabled = hugepages_passthrough_enabled;
2854 }
2855 if let Some(kvm_passthrough_enabled) = options.kvm_passthrough_enabled {
2856 self.kvm_passthrough_enabled = kvm_passthrough_enabled;
2857 }
2858 if let Some(feature_limits) = options.feature_limits {
2859 self.allocation_limit = feature_limits.allocations;
2860 self.database_limit = feature_limits.databases;
2861 self.backup_limit = feature_limits.backups;
2862 self.schedule_limit = feature_limits.schedules;
2863 }
2864
2865 self.run_after_update_handlers(state, transaction).await?;
2866
2867 Ok(())
2868 }
2869}
2870
2871#[derive(Clone, Default)]
2872pub struct DeleteServerOptions {
2873 pub force: bool,
2874}
2875
2876#[async_trait::async_trait]
2877impl DeletableModel for Server {
2878 type DeleteOptions = DeleteServerOptions;
2879
2880 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
2881 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Server>> =
2882 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
2883
2884 &DELETE_LISTENERS
2885 }
2886
2887 async fn delete_with_transaction(
2888 &self,
2889 _state: &crate::State,
2890 _options: Self::DeleteOptions,
2891 _transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
2892 ) -> Result<(), anyhow::Error> {
2893 Err(anyhow::anyhow!(
2894 "delete_with_transaction is not supported for Server"
2895 ))
2896 }
2897
2898 async fn delete(
2899 &self,
2900 state: &crate::State,
2901 options: Self::DeleteOptions,
2902 ) -> Result<(), anyhow::Error> {
2903 let node = self.node.fetch_cached(&state.database).await?;
2904 let databases =
2905 super::server_database::ServerDatabase::all_by_server_uuid(&state.database, self.uuid)
2906 .await?;
2907 let database_instances =
2908 super::server_database_instance::ServerDatabaseInstance::all_by_server_uuid(
2909 &state.database,
2910 self.uuid,
2911 )
2912 .await?;
2913
2914 let mut transaction = state.database.write().begin().await?;
2915 self.run_delete_handlers(&options, state, &mut transaction)
2916 .await?;
2917
2918 let state = state.clone();
2919 let server_uuid = self.uuid;
2920
2921 tokio::spawn(async move {
2922 for db in databases {
2923 match db.delete(&state, super::server_database::DeleteServerDatabaseOptions { force: options.force }).await {
2924 Ok(_) => {}
2925 Err(err) => {
2926 tracing::error!(server = %server_uuid, "failed to delete database: {:?}", err);
2927
2928 if !options.force {
2929 return Err(err);
2930 }
2931 }
2932 }
2933 }
2934
2935 for database_instance in database_instances {
2936 match database_instance.delete(&state, super::server_database_instance::DeleteServerDatabaseInstanceOptions { force: options.force }).await {
2937 Ok(_) => {}
2938 Err(err) => {
2939 tracing::error!(server = %server_uuid, "failed to delete database instance: {:?}", err);
2940
2941 if !options.force {
2942 return Err(err);
2943 }
2944 }
2945 }
2946 }
2947
2948 let on_mesh =
2949 crate::tunnel::bump_epoch_if_server_on_mesh(&mut transaction, server_uuid).await?;
2950
2951 sqlx::query!("DELETE FROM servers WHERE servers.uuid = $1", server_uuid)
2952 .execute(&mut *transaction)
2953 .await?;
2954
2955 match node
2956 .api_client(&state.database)
2957 .await?
2958 .delete_servers_server(server_uuid)
2959 .await
2960 {
2961 Ok(_) => {
2962 transaction.commit().await?;
2963
2964 Self::invalidate_cached(&state.database, server_uuid).await;
2965
2966 if on_mesh {
2967 crate::tunnel::poke_nodes(&state.database).await;
2968 }
2969
2970 Ok(())
2971 }
2972 Err(err) => {
2973 tracing::error!(server = %server_uuid, node = %node.uuid, "failed to delete server: {:?}", err);
2974
2975 if options.force {
2976 transaction.commit().await?;
2977
2978 Self::invalidate_cached(&state.database, server_uuid).await;
2979
2980 if on_mesh {
2981 crate::tunnel::poke_nodes(&state.database).await;
2982 }
2983
2984 Ok(())
2985 } else {
2986 transaction.rollback().await?;
2987 Err(err.into())
2988 }
2989 }
2990 }
2991 }).await?
2992 }
2993}
2994
2995#[derive(ToSchema, Serialize)]
2996#[schema(title = "RemoteServer")]
2997pub struct RemoteApiServer {
2998 settings: wings_api::ServerConfiguration,
2999 process_configuration: super::nest_egg::ProcessConfiguration,
3000}
3001
3002#[derive(ToSchema, Validate, Serialize, Deserialize, Clone, Copy)]
3003pub struct AdminApiServerLimits {
3004 #[garde(range(min = 0))]
3005 #[schema(minimum = 0)]
3006 pub cpu: i32,
3007 #[garde(range(min = 0))]
3008 #[schema(minimum = 0)]
3009 pub memory: i64,
3010 #[garde(range(min = 0))]
3011 #[schema(minimum = 0)]
3012 pub memory_overhead: i64,
3013 #[garde(range(min = -1))]
3014 #[schema(minimum = -1)]
3015 pub swap: i64,
3016 #[garde(range(min = 0))]
3017 #[schema(minimum = 0)]
3018 pub disk: i64,
3019 #[garde(range(min = 0, max = 1000))]
3020 #[schema(minimum = 0, maximum = 1000)]
3021 pub io_weight: Option<i16>,
3022}
3023
3024#[derive(ToSchema, Validate, Serialize, Deserialize, Clone, Copy)]
3025pub struct ApiServerLimits {
3026 #[garde(range(min = 0))]
3027 #[schema(minimum = 0)]
3028 pub cpu: i32,
3029 #[garde(range(min = 0))]
3030 #[schema(minimum = 0)]
3031 pub memory: i64,
3032 #[garde(range(min = -1))]
3033 #[schema(minimum = -1)]
3034 pub swap: i64,
3035 #[garde(range(min = 0))]
3036 #[schema(minimum = 0)]
3037 pub disk: i64,
3038}
3039
3040#[schema_extension_derive::extendible]
3041#[init_args(Server, crate::State)]
3042#[hook_args(crate::State)]
3043#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
3044pub struct ApiServerFeatureLimits {
3045 #[garde(range(min = 0))]
3046 #[schema(minimum = 0)]
3047 pub allocations: i32,
3048 #[garde(range(min = 0))]
3049 #[schema(minimum = 0)]
3050 pub databases: i32,
3051 #[garde(range(min = 0))]
3052 #[schema(minimum = 0)]
3053 pub backups: i32,
3054 #[garde(range(min = 0))]
3055 #[schema(minimum = 0)]
3056 pub schedules: i32,
3057}
3058
3059#[schema_extension_derive::extendible]
3060#[init_args(Server, crate::State)]
3061#[hook_args(crate::State)]
3062#[derive(ToSchema, Serialize)]
3063#[schema(title = "AdminServer")]
3064pub struct AdminApiServer {
3065 pub uuid: uuid::Uuid,
3066 pub uuid_short: compact_str::CompactString,
3067 pub external_id: Option<compact_str::CompactString>,
3068 pub allocation: Option<super::server_allocation::ApiServerAllocation>,
3069 pub node: super::node::AdminApiNode,
3070 pub owner: super::user::AdminApiUser,
3071 pub egg: super::nest_egg::AdminApiNestEgg,
3072 pub nest: super::nest::AdminApiNest,
3073 pub backup_configuration: Option<super::backup_configuration::AdminApiBackupConfiguration>,
3074
3075 pub status: Option<ServerStatus>,
3076
3077 pub is_suspended: bool,
3078 pub is_transferring: bool,
3079
3080 pub name: compact_str::CompactString,
3081 pub description: Option<compact_str::CompactString>,
3082
3083 #[schema(inline)]
3084 pub limits: AdminApiServerLimits,
3085 pub pinned_cpus: Vec<i16>,
3086 #[schema(inline)]
3087 pub feature_limits: ApiServerFeatureLimits,
3088
3089 pub startup: compact_str::CompactString,
3090 pub image: compact_str::CompactString,
3091 #[schema(inline)]
3092 pub auto_kill: wings_api::ServerConfigurationAutoKill,
3093 pub auto_start_behavior: ServerAutoStartBehavior,
3094 pub timezone: Option<compact_str::CompactString>,
3095
3096 pub hugepages_passthrough_enabled: bool,
3097 pub kvm_passthrough_enabled: bool,
3098
3099 pub created: chrono::DateTime<chrono::Utc>,
3100}
3101
3102#[schema_extension_derive::extendible]
3103#[init_args(Server, crate::State)]
3104#[hook_args(crate::State)]
3105#[derive(ToSchema, Serialize)]
3106#[schema(title = "Server")]
3107pub struct ApiServer {
3108 pub uuid: uuid::Uuid,
3109 pub uuid_short: compact_str::CompactString,
3110 pub allocation: Option<super::server_allocation::ApiServerAllocation>,
3111 pub egg: super::nest_egg::ApiNestEgg,
3112 pub egg_configuration: super::egg_configuration::ApiEggConfiguration,
3113
3114 pub status: Option<ServerStatus>,
3115
3116 pub is_owner: bool,
3117 pub is_suspended: bool,
3118 pub is_transferring: bool,
3119 pub permissions: Vec<compact_str::CompactString>,
3120 pub ignored_files: Vec<compact_str::CompactString>,
3121
3122 pub location_uuid: uuid::Uuid,
3123 pub location_name: compact_str::CompactString,
3124 pub location_flag: Option<compact_str::CompactString>,
3125 pub node_uuid: uuid::Uuid,
3126 pub node_name: compact_str::CompactString,
3127 pub node_maintenance_enabled: bool,
3128
3129 pub sftp_host: compact_str::CompactString,
3130 pub sftp_port: i32,
3131
3132 pub name: compact_str::CompactString,
3133 pub description: Option<compact_str::CompactString>,
3134
3135 #[schema(inline)]
3136 pub limits: ApiServerLimits,
3137 #[schema(inline)]
3138 pub feature_limits: ApiServerFeatureLimits,
3139
3140 pub startup: compact_str::CompactString,
3141 pub image: compact_str::CompactString,
3142 #[schema(inline)]
3143 pub auto_kill: wings_api::ServerConfigurationAutoKill,
3144 pub auto_start_behavior: ServerAutoStartBehavior,
3145 pub timezone: Option<compact_str::CompactString>,
3146
3147 pub created: chrono::DateTime<chrono::Utc>,
3148}
3149
3150#[cfg(test)]
3151mod tests {
3152 use super::is_path_ignored;
3153
3154 fn overrides(patterns: &[&str]) -> ignore::overrides::Override {
3155 let mut builder = ignore::overrides::OverrideBuilder::new("/");
3156
3157 for pattern in patterns {
3158 builder.add(pattern).unwrap();
3159 }
3160
3161 builder.build().unwrap()
3162 }
3163
3164 #[test]
3165 fn an_anchored_pattern_matches_every_spelling_of_the_path() {
3166 let overrides = overrides(&["/config/secrets.yml"]);
3167
3168 for path in [
3169 "/config/secrets.yml",
3170 "/./config/secrets.yml",
3171 "/config//secrets.yml",
3172 "/config/./secrets.yml",
3173 "/config/../config/secrets.yml",
3174 "/../config/secrets.yml",
3175 "config/secrets.yml",
3176 ] {
3177 assert!(is_path_ignored(&overrides, path, false), "{path}");
3178 }
3179
3180 assert!(!is_path_ignored(&overrides, "/config/public.yml", false));
3181 }
3182
3183 #[test]
3184 fn appending_a_list_keeps_everything_it_hid_hidden() {
3185 let caller = ["!/a/b", "/a/**"];
3188
3189 assert!(is_path_ignored(&overrides(&caller), "/a/b", false));
3190 assert!(!is_path_ignored(
3191 &overrides(&["/a/**", "!/a/b"]),
3192 "/a/b",
3193 false
3194 ));
3195
3196 for granted in [
3197 vec!["!/a/b", "/a/**"],
3198 vec!["/a/**", "!/a/b", "!/a/b", "/a/**"],
3199 vec!["!/a/**", "!/a/b", "/a/**"],
3200 ] {
3201 assert!(granted.ends_with(&caller));
3202 assert!(
3203 is_path_ignored(&overrides(&granted), "/a/b", false),
3204 "{granted:?}"
3205 );
3206 }
3207 }
3208
3209 #[test]
3210 fn the_server_root_is_never_ignored() {
3211 let overrides = overrides(&["*"]);
3212
3213 for path in ["/", "", ".", "/.", "/config/.."] {
3214 assert!(!is_path_ignored(&overrides, path, true), "{path}");
3215 }
3216 }
3217}