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