shared/models/server/
mod.rs

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