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