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