Skip to main content

shared/models/
node_allocation.rs

1use crate::prelude::*;
2use compact_str::ToCompactString;
3use serde::{Deserialize, Serialize};
4use sqlx::{Row, postgres::PgRow};
5use std::{
6    collections::{BTreeMap, HashMap},
7    sync::LazyLock,
8};
9#[derive(Serialize, Deserialize, Clone)]
10pub struct NodeAllocation {
11    pub uuid: uuid::Uuid,
12    pub server: Option<Fetchable<super::server::Server>>,
13
14    pub ip: sqlx::types::ipnetwork::IpNetwork,
15    pub ip_alias: Option<compact_str::CompactString>,
16    pub port: i32,
17
18    pub created: chrono::NaiveDateTime,
19
20    extension_data: super::ModelExtensionData,
21}
22
23impl BaseModel for NodeAllocation {
24    const NAME: &'static str = "node_allocation";
25
26    fn get_extension_list() -> &'static super::ModelExtensionList {
27        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
28            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
29
30        &EXTENSIONS
31    }
32
33    fn get_extension_data(&self) -> &super::ModelExtensionData {
34        &self.extension_data
35    }
36
37    #[inline]
38    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
39        let prefix = prefix.unwrap_or_default();
40
41        BTreeMap::from([
42            (
43                "node_allocations.uuid",
44                compact_str::format_compact!("{prefix}uuid"),
45            ),
46            (
47                "node_allocations.ip",
48                compact_str::format_compact!("{prefix}ip"),
49            ),
50            (
51                "node_allocations.ip_alias",
52                compact_str::format_compact!("{prefix}ip_alias"),
53            ),
54            (
55                "node_allocations.port",
56                compact_str::format_compact!("{prefix}port"),
57            ),
58            (
59                "node_allocations.created",
60                compact_str::format_compact!("{prefix}created"),
61            ),
62        ])
63    }
64
65    #[inline]
66    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
67        let prefix = prefix.unwrap_or_default();
68
69        Ok(Self {
70            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
71            server: if let Ok(server_uuid) = row.try_get::<uuid::Uuid, _>("server_uuid") {
72                Some(super::server::Server::get_fetchable(server_uuid))
73            } else {
74                None
75            },
76            ip: row.try_get(compact_str::format_compact!("{prefix}ip").as_str())?,
77            ip_alias: row.try_get(compact_str::format_compact!("{prefix}ip_alias").as_str())?,
78            port: row.try_get(compact_str::format_compact!("{prefix}port").as_str())?,
79            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
80            extension_data: Self::map_extensions(prefix, row)?,
81        })
82    }
83}
84
85impl NodeAllocation {
86    pub async fn create(
87        database: &crate::database::Database,
88        node_uuid: uuid::Uuid,
89        ip: &sqlx::types::ipnetwork::IpNetwork,
90        ip_alias: Option<&str>,
91        port: i32,
92    ) -> Result<(), crate::database::DatabaseError> {
93        sqlx::query(
94            r#"
95            INSERT INTO node_allocations (node_uuid, ip, ip_alias, port)
96            VALUES ($1, $2, $3, $4)
97            "#,
98        )
99        .bind(node_uuid)
100        .bind(ip)
101        .bind(ip_alias)
102        .bind(port)
103        .execute(database.write())
104        .await?;
105
106        Ok(())
107    }
108
109    pub async fn get_random(
110        database: &crate::database::Database,
111        node_uuid: uuid::Uuid,
112        start_port: u16,
113        end_port: u16,
114        amount: i64,
115        exclude: &[uuid::Uuid],
116    ) -> Result<Vec<uuid::Uuid>, crate::database::DatabaseError> {
117        let rows = sqlx::query(
118            r#"
119            WITH eligible_ips AS (
120                SELECT node_allocations.ip
121                FROM node_allocations
122                LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
123                WHERE
124                    node_allocations.node_uuid = $1
125                    AND node_allocations.port BETWEEN $2 AND $3
126                    AND server_allocations.uuid IS NULL
127                    AND NOT (node_allocations.uuid = ANY($5))
128                GROUP BY node_allocations.ip
129                HAVING COUNT(*) >= $4
130            ),
131            random_ip AS (
132                SELECT ip FROM eligible_ips ORDER BY RANDOM() LIMIT 1
133            )
134            SELECT node_allocations.uuid
135            FROM node_allocations
136            LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
137            WHERE
138                node_allocations.node_uuid = $1
139                AND node_allocations.port BETWEEN $2 AND $3
140                AND server_allocations.uuid IS NULL
141                AND NOT (node_allocations.uuid = ANY($5))
142                AND node_allocations.ip = (SELECT ip FROM random_ip)
143            ORDER BY RANDOM()
144            LIMIT $4
145            "#,
146        )
147        .bind(node_uuid)
148        .bind(start_port as i32)
149        .bind(end_port as i32)
150        .bind(amount)
151        .bind(exclude)
152        .fetch_all(database.write())
153        .await?;
154
155        if rows.len() != amount as usize {
156            return Err(anyhow::anyhow!("only found {} available allocations", rows.len()).into());
157        }
158
159        Ok(rows
160            .into_iter()
161            .map(|row| row.get::<uuid::Uuid, _>("uuid"))
162            .collect())
163    }
164
165    pub async fn get_random_ip(
166        database: &crate::database::Database,
167        node_uuid: uuid::Uuid,
168        ip: &sqlx::types::ipnetwork::IpNetwork,
169        start_port: u16,
170        end_port: u16,
171        amount: i64,
172        exclude: &[uuid::Uuid],
173    ) -> Result<Vec<uuid::Uuid>, crate::database::DatabaseError> {
174        let rows = sqlx::query(
175            r#"
176            SELECT node_allocations.uuid
177            FROM node_allocations
178            LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
179            WHERE
180                node_allocations.node_uuid = $1
181                AND node_allocations.ip = $2
182                AND node_allocations.port BETWEEN $3 AND $4
183                AND server_allocations.uuid IS NULL
184                AND NOT (node_allocations.uuid = ANY($6))
185            ORDER BY RANDOM()
186            LIMIT $5
187            "#,
188        )
189        .bind(node_uuid)
190        .bind(ip)
191        .bind(start_port as i32)
192        .bind(end_port as i32)
193        .bind(amount)
194        .bind(exclude)
195        .fetch_all(database.write())
196        .await?;
197
198        if rows.len() != amount as usize {
199            return Err(anyhow::anyhow!(
200                "only found {} available allocations on this IP",
201                rows.len()
202            )
203            .into());
204        }
205
206        Ok(rows
207            .into_iter()
208            .map(|row| row.get::<uuid::Uuid, _>("uuid"))
209            .collect())
210    }
211
212    pub async fn get_random_dedicated(
213        database: &crate::database::Database,
214        node_uuid: uuid::Uuid,
215        start_port: u16,
216        end_port: u16,
217        amount: i64,
218    ) -> Result<Vec<uuid::Uuid>, crate::database::DatabaseError> {
219        let rows = sqlx::query(
220            r#"
221            WITH eligible_ips AS (
222                SELECT node_allocations.ip
223                FROM node_allocations
224                LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
225                WHERE node_allocations.node_uuid = $1
226                GROUP BY node_allocations.ip
227                HAVING 
228                    COUNT(server_allocations.uuid) = 0
229                    AND SUM(CASE WHEN node_allocations.port BETWEEN $2 AND $3 THEN 1 ELSE 0 END) >= $4
230            ),
231            random_ip AS (
232                SELECT ip FROM eligible_ips ORDER BY RANDOM() LIMIT 1
233            )
234            SELECT node_allocations.uuid
235            FROM node_allocations
236            WHERE
237                node_allocations.node_uuid = $1
238                AND node_allocations.port BETWEEN $2 AND $3
239                AND node_allocations.ip = (SELECT ip FROM random_ip)
240            ORDER BY RANDOM()
241            LIMIT $4
242            "#,
243        )
244        .bind(node_uuid)
245        .bind(start_port as i32)
246        .bind(end_port as i32)
247        .bind(amount)
248        .fetch_all(database.write())
249        .await?;
250
251        if rows.len() != amount as usize {
252            return Err(anyhow::anyhow!(
253                "only found {} available dedicated allocations",
254                rows.len()
255            )
256            .into());
257        }
258
259        Ok(rows
260            .into_iter()
261            .map(|row| row.get::<uuid::Uuid, _>("uuid"))
262            .collect())
263    }
264
265    pub async fn get_preserved(
266        database: &crate::database::Database,
267        node_uuid: uuid::Uuid,
268        ports: &[i32],
269    ) -> Result<
270        Option<(sqlx::types::ipnetwork::IpNetwork, Vec<(uuid::Uuid, i32)>)>,
271        crate::database::DatabaseError,
272    > {
273        let rows = sqlx::query(
274            r#"
275            WITH best_ip AS (
276                SELECT node_allocations.ip
277                FROM node_allocations
278                LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
279                WHERE
280                    node_allocations.node_uuid = $1
281                    AND node_allocations.port = ANY($2)
282                    AND server_allocations.uuid IS NULL
283                GROUP BY node_allocations.ip
284                ORDER BY COUNT(DISTINCT node_allocations.port) DESC, RANDOM()
285                LIMIT 1
286            )
287            SELECT node_allocations.uuid, node_allocations.ip, node_allocations.port
288            FROM node_allocations
289            LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
290            WHERE
291                node_allocations.node_uuid = $1
292                AND node_allocations.port = ANY($2)
293                AND server_allocations.uuid IS NULL
294                AND node_allocations.ip = (SELECT ip FROM best_ip)
295            "#,
296        )
297        .bind(node_uuid)
298        .bind(ports)
299        .fetch_all(database.write())
300        .await?;
301
302        let Some(first) = rows.first() else {
303            return Ok(None);
304        };
305
306        let ip = first.get::<sqlx::types::ipnetwork::IpNetwork, _>("ip");
307
308        Ok(Some((
309            ip,
310            rows.into_iter()
311                .map(|row| (row.get::<uuid::Uuid, _>("uuid"), row.get::<i32, _>("port")))
312                .collect(),
313        )))
314    }
315
316    pub async fn by_node_uuid_ip_port_unused(
317        database: &crate::database::Database,
318        node_uuid: uuid::Uuid,
319        ip: &sqlx::types::ipnetwork::IpNetwork,
320        port: i32,
321    ) -> Result<Option<Self>, crate::database::DatabaseError> {
322        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
323            r#"
324            SELECT {}
325            FROM node_allocations
326            LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
327            WHERE node_allocations.node_uuid = $1 AND node_allocations.ip = $2 AND node_allocations.port = $3 AND server_allocations.uuid IS NULL
328            "#,
329            Self::columns_sql(None)
330        )))
331        .bind(node_uuid)
332        .bind(ip)
333        .bind(port)
334        .fetch_optional(database.read())
335        .await?;
336
337        row.try_map(|row| Self::map(None, &row))
338    }
339
340    pub async fn get_from_deployment<'a>(
341        database: &crate::database::Database,
342        deployment: &'a super::egg_configuration::EggConfigAllocationsDeployment,
343        node_uuid: uuid::Uuid,
344        variables: &mut HashMap<&'a str, compact_str::CompactString>,
345    ) -> Result<(Option<uuid::Uuid>, Vec<uuid::Uuid>), crate::database::DatabaseError> {
346        let mut primary = None;
347        let mut additional = Vec::new();
348
349        const MAX_ITER: usize = 100;
350
351        macro_rules! is_unused {
352            ($uuid:expr) => {
353                primary.as_ref().map_or(true, |p| p.uuid != $uuid) && !additional.contains(&$uuid)
354            };
355        }
356
357        macro_rules! get_random {
358            ($start_port:expr, $end_port:expr) => {{
359                let mut exclude = additional.clone();
360                if let Some(primary) = &primary {
361                    exclude.push(primary.uuid);
362                    Self::get_random_ip(
363                        database,
364                        node_uuid,
365                        &primary.ip,
366                        $start_port,
367                        $end_port,
368                        1,
369                        &exclude,
370                    )
371                    .await?
372                } else {
373                    Self::get_random(database, node_uuid, $start_port, $end_port, 1, &exclude)
374                        .await?
375                }
376            }};
377        }
378
379        let mut success = false;
380
381        'primary: for i in 0..MAX_ITER {
382            if i != 0 && deployment.primary.is_none() {
383                break;
384            }
385
386            additional.clear();
387            variables.clear();
388
389            if let Some(primary_allocation) = &deployment.primary {
390                let random = if deployment.dedicated {
391                    Self::get_random_dedicated(
392                        database,
393                        node_uuid,
394                        primary_allocation.start_port,
395                        primary_allocation.end_port,
396                        1,
397                    )
398                    .await?
399                } else {
400                    Self::get_random(
401                        database,
402                        node_uuid,
403                        primary_allocation.start_port,
404                        primary_allocation.end_port,
405                        1,
406                        &[],
407                    )
408                    .await?
409                };
410
411                let Some(allocation) = random.into_iter().next() else {
412                    return Err(anyhow::anyhow!("no available primary allocation found").into());
413                };
414                let allocation =
415                    match Self::by_node_uuid_uuid(database, node_uuid, allocation).await? {
416                        Some(allocation) => allocation,
417                        None => {
418                            return Err(
419                                anyhow::anyhow!("allocated primary allocation not found").into()
420                            );
421                        }
422                    };
423
424                if let Some(variable_name) = &primary_allocation.assign_to_variable {
425                    variables.insert(variable_name, allocation.port.to_compact_string());
426                }
427
428                primary = Some(allocation);
429            }
430
431            for additional_allocation in &deployment.additional {
432                match additional_allocation.mode {
433                    super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::Random => {
434                        let random = get_random!(1, u16::MAX);
435
436                        let Some(allocation) = random.into_iter().next() else {
437                            return Err(anyhow::anyhow!("no available additional allocation found").into());
438                        };
439                        additional.push(allocation);
440
441                        if let Some(variable_name) = &additional_allocation.assign_to_variable {
442                            let allocation = match Self::by_node_uuid_uuid(database, node_uuid, allocation).await? {
443                                Some(allocation) => allocation,
444                                None => {
445                                    return Err(
446                                        anyhow::anyhow!("allocated additional allocation not found").into()
447                                    );
448                                }
449                            };
450
451                            variables.insert(variable_name, allocation.port.to_compact_string());
452                        }
453                    },
454                    super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::Range {
455                        start_port,
456                        end_port,
457                    } => {
458                        let random = get_random!(start_port, end_port);
459
460                        let Some(allocation) = random.into_iter().next() else {
461                            return Err(anyhow::anyhow!("no available additional allocation found").into());
462                        };
463                        additional.push(allocation);
464
465                        if let Some(variable_name) = &additional_allocation.assign_to_variable {
466                            let allocation = match Self::by_node_uuid_uuid(database, node_uuid, allocation).await? {
467                                Some(allocation) => allocation,
468                                None => {
469                                    return Err(
470                                        anyhow::anyhow!("allocated additional allocation not found").into()
471                                    );
472                                }
473                            };
474
475                            variables.insert(variable_name, allocation.port.to_compact_string());
476                        }
477                    }
478                    super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::AddPrimary { value } => {
479                        let primary = match &primary {
480                            Some(primary) => primary,
481                            None => {
482                                return Err(anyhow::anyhow!("primary allocation is required for `add_primary` mode").into());
483                            }
484                        };
485
486                        let allocation_port = primary.port + value as i32;
487
488                        let allocation = match Self::by_node_uuid_ip_port_unused(database, node_uuid, &primary.ip, allocation_port).await? {
489                            Some(allocation) => allocation,
490                            None => continue 'primary,
491                        };
492                        if !is_unused!(allocation.uuid) {
493                            return Err(anyhow::anyhow!("allocated additional allocation is already in use").into());
494                        }
495                        additional.push(allocation.uuid);
496
497                        if let Some(variable_name) = &additional_allocation.assign_to_variable {
498                            variables.insert(variable_name, allocation.port.to_compact_string());
499                        }
500                    }
501                    super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::SubtractPrimary { value } => {
502                        let primary = match &primary {
503                            Some(primary) => primary,
504                            None => {
505                                return Err(anyhow::anyhow!("primary allocation is required for `subtract_primary` mode").into());
506                            }
507                        };
508
509                        let allocation_port = primary.port - value as i32;
510
511                        let allocation = match Self::by_node_uuid_ip_port_unused(database, node_uuid, &primary.ip, allocation_port).await? {
512                            Some(allocation) => allocation,
513                            None => continue 'primary,
514                        };
515                        if !is_unused!(allocation.uuid) {
516                            return Err(anyhow::anyhow!("allocated additional allocation is already in use").into());
517                        }
518                        additional.push(allocation.uuid);
519
520                        if let Some(variable_name) = &additional_allocation.assign_to_variable {
521                            variables.insert(variable_name, allocation.port.to_compact_string());
522                        }
523                    }
524                    super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::MultiplyPrimary { value } => {
525                        let primary = match &primary {
526                            Some(primary) => primary,
527                            None => {
528                                return Err(anyhow::anyhow!("primary allocation is required for `multiply_primary` mode").into());
529                            }
530                        };
531
532                        let allocation_port = (primary.port as f64 * value) as i32;
533
534                        let allocation = match Self::by_node_uuid_ip_port_unused(database, node_uuid, &primary.ip, allocation_port).await? {
535                            Some(allocation) => allocation,
536                            None => continue 'primary,
537                        };
538                        if !is_unused!(allocation.uuid) {
539                            return Err(anyhow::anyhow!("allocated additional allocation is already in use").into());
540                        }
541                        additional.push(allocation.uuid);
542
543                        if let Some(variable_name) = &additional_allocation.assign_to_variable {
544                            variables.insert(variable_name, allocation.port.to_compact_string());
545                        }
546                    }
547                    super::egg_configuration::EggConfigAllocationDeploymentAdditionalAllocationMode::DividePrimary { value } => {
548                        let primary = match &primary {
549                            Some(primary) => primary,
550                            None => {
551                                return Err(anyhow::anyhow!("primary allocation is required for `divide_primary` mode").into());
552                            }
553                        };
554
555                        let allocation_port = (primary.port as f64 / value) as i32;
556
557                        let allocation = match Self::by_node_uuid_ip_port_unused(database, node_uuid, &primary.ip, allocation_port).await? {
558                            Some(allocation) => allocation,
559                            None => continue 'primary,
560                        };
561                        if !is_unused!(allocation.uuid) {
562                            return Err(anyhow::anyhow!("allocated additional allocation is already in use").into());
563                        }
564                        additional.push(allocation.uuid);
565
566                        if let Some(variable_name) = &additional_allocation.assign_to_variable {
567                            variables.insert(variable_name, allocation.port.to_compact_string());
568                        }
569                    }
570                }
571            }
572
573            success = true;
574            break;
575        }
576
577        if !success {
578            return Err(anyhow::anyhow!(
579                "could not satisfy all additional allocation rules after {MAX_ITER} attempts"
580            )
581            .into());
582        }
583
584        Ok((primary.map(|p| p.uuid), additional))
585    }
586
587    pub async fn by_node_uuid_uuid(
588        database: &crate::database::Database,
589        node_uuid: uuid::Uuid,
590        uuid: uuid::Uuid,
591    ) -> Result<Option<Self>, crate::database::DatabaseError> {
592        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
593            r#"
594            SELECT {}
595            FROM node_allocations
596            WHERE node_allocations.node_uuid = $1 AND node_allocations.uuid = $2
597            "#,
598            Self::columns_sql(None)
599        )))
600        .bind(node_uuid)
601        .bind(uuid)
602        .fetch_optional(database.read())
603        .await?;
604
605        row.try_map(|row| Self::map(None, &row))
606    }
607
608    pub async fn available_by_node_uuid_with_pagination(
609        database: &crate::database::Database,
610        node_uuid: uuid::Uuid,
611        page: i64,
612        per_page: i64,
613        search: Option<&str>,
614    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
615        let offset = (page - 1) * per_page;
616
617        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
618            r#"
619            SELECT {}, COUNT(*) OVER() AS total_count
620            FROM node_allocations
621            LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
622            WHERE node_allocations.node_uuid = $1 AND server_allocations.uuid IS NULL
623                AND (
624                    $2 IS NULL
625                    OR host(node_allocations.ip) || ':' || node_allocations.port ILIKE '%' || $2 || '%'
626                    OR (node_allocations.ip_alias IS NOT NULL AND node_allocations.ip_alias || ':' || node_allocations.port ILIKE '%' || $2 || '%')
627                )
628            ORDER BY node_allocations.ip, node_allocations.port
629            LIMIT $3 OFFSET $4
630            "#,
631            Self::columns_sql(None)
632        )))
633        .bind(node_uuid)
634        .bind(search)
635        .bind(per_page)
636        .bind(offset)
637        .fetch_all(database.read())
638        .await?;
639
640        Ok(super::Pagination {
641            total: rows
642                .first()
643                .map_or(Ok(0), |row| row.try_get("total_count"))?,
644            per_page,
645            page,
646            data: rows
647                .into_iter()
648                .map(|row| Self::map(None, &row))
649                .try_collect_vec()?,
650        })
651    }
652
653    pub async fn by_node_uuid_with_pagination(
654        database: &crate::database::Database,
655        node_uuid: uuid::Uuid,
656        page: i64,
657        per_page: i64,
658        search: Option<&str>,
659    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
660        let offset = (page - 1) * per_page;
661
662        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
663            r#"
664            SELECT {}, server_allocations.server_uuid, COUNT(*) OVER() AS total_count
665            FROM node_allocations
666            LEFT JOIN server_allocations ON server_allocations.allocation_uuid = node_allocations.uuid
667            WHERE node_allocations.node_uuid = $1
668                AND (
669                    $2 IS NULL OR host(node_allocations.ip) || ':' || node_allocations.port ILIKE '%' || $2 || '%'
670                    OR (node_allocations.ip_alias IS NOT NULL AND node_allocations.ip_alias || ':' || node_allocations.port ILIKE '%' || $2 || '%')
671                    OR server_allocations.notes ILIKE '%' || $2 || '%'
672                )
673            ORDER BY node_allocations.ip, node_allocations.port
674            LIMIT $3 OFFSET $4
675            "#,
676            Self::columns_sql(None)
677        )))
678        .bind(node_uuid)
679        .bind(search)
680        .bind(per_page)
681        .bind(offset)
682        .fetch_all(database.read())
683        .await?;
684
685        Ok(super::Pagination {
686            total: rows
687                .first()
688                .map_or(Ok(0), |row| row.try_get("total_count"))?,
689            per_page,
690            page,
691            data: rows
692                .into_iter()
693                .map(|row| Self::map(None, &row))
694                .try_collect_vec()?,
695        })
696    }
697
698    pub async fn delete_by_uuids(
699        database: &crate::database::Database,
700        node_uuid: uuid::Uuid,
701        uuids: &[uuid::Uuid],
702    ) -> Result<u64, crate::database::DatabaseError> {
703        let deleted = sqlx::query(
704            r#"
705            DELETE FROM node_allocations
706            WHERE node_allocations.node_uuid = $1 AND node_allocations.uuid = ANY($2)
707            "#,
708        )
709        .bind(node_uuid)
710        .bind(uuids)
711        .execute(database.write())
712        .await?
713        .rows_affected();
714
715        Ok(deleted)
716    }
717}
718
719#[async_trait::async_trait]
720impl IntoAdminApiObject for NodeAllocation {
721    type AdminApiObject = AdminApiNodeAllocation;
722    type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
723
724    async fn into_admin_api_object<'a>(
725        self,
726        state: &crate::State,
727        storage_url_retriever: Self::ExtraArgs<'a>,
728    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
729        let api_object = AdminApiNodeAllocation::init_hooks(&self, state).await?;
730
731        let server = match self.server {
732            Some(fetchable) => Some(
733                fetchable
734                    .fetch_cached(&state.database)
735                    .await?
736                    .into_admin_api_object(state, storage_url_retriever)
737                    .await?,
738            ),
739            None => None,
740        };
741
742        let api_object = finish_extendible!(
743            AdminApiNodeAllocation {
744                uuid: self.uuid,
745                server,
746                ip: self.ip.ip().to_compact_string(),
747                ip_alias: self.ip_alias,
748                port: self.port,
749                created: self.created.and_utc(),
750            },
751            api_object,
752            state
753        )?;
754
755        Ok(api_object)
756    }
757}
758
759#[schema_extension_derive::extendible]
760#[init_args(NodeAllocation, crate::State)]
761#[hook_args(crate::State)]
762#[derive(ToSchema, Serialize)]
763#[schema(title = "NodeAllocation")]
764pub struct AdminApiNodeAllocation {
765    pub uuid: uuid::Uuid,
766    pub server: Option<super::server::AdminApiServer>,
767
768    pub ip: compact_str::CompactString,
769    pub ip_alias: Option<compact_str::CompactString>,
770    pub port: i32,
771
772    pub created: chrono::DateTime<chrono::Utc>,
773}