Skip to main content

shared/models/server/
mod.rs

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