Skip to main content

shared/models/server/
mod.rs

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