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