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