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