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