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