Skip to main content

shared/models/server/
mod.rs

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