@@ -57,16 +57,6 @@ use omicron_common::api::external::http_pagination::PaginatedBy;
57
57
use ref_cast:: RefCast ;
58
58
use uuid:: Uuid ;
59
59
60
- pub struct IpsAllocated {
61
- pub ipv4 : i64 ,
62
- pub ipv6 : i64 ,
63
- }
64
-
65
- pub struct IpsCapacity {
66
- pub ipv4 : u32 ,
67
- pub ipv6 : u128 ,
68
- }
69
-
70
60
/// Helper type with both an authz IP Pool and the actual DB record.
71
61
#[ derive( Debug , Clone ) ]
72
62
pub struct ServiceIpPool {
@@ -401,53 +391,100 @@ impl DataStore {
401
391
} )
402
392
}
403
393
404
- pub async fn ip_pool_allocated_count (
394
+ /// Return the number of IPs allocated from and the capacity of the provided
395
+ /// IP Pool.
396
+ pub async fn ip_pool_utilization (
405
397
& self ,
406
398
opctx : & OpContext ,
407
399
authz_pool : & authz:: IpPool ,
408
- ) -> Result < IpsAllocated , Error > {
400
+ ) -> Result < ( i64 , u128 ) , Error > {
409
401
opctx. authorize ( authz:: Action :: Read , authz_pool) . await ?;
402
+ opctx. authorize ( authz:: Action :: ListChildren , authz_pool) . await ?;
403
+ let conn = self . pool_connection_authorized ( opctx) . await ?;
404
+ let ( allocated, ranges) = self
405
+ . transaction_retry_wrapper ( "ip_pool_utilization" )
406
+ . transaction ( & conn, |conn| async move {
407
+ let allocated = self
408
+ . ip_pool_allocated_count_on_connection ( & conn, authz_pool)
409
+ . await ?;
410
+ let ranges = self
411
+ . ip_pool_list_ranges_batched_on_connection (
412
+ & conn, authz_pool,
413
+ )
414
+ . await ?;
415
+ Ok ( ( allocated, ranges) )
416
+ } )
417
+ . await
418
+ . map_err ( |e| match & e {
419
+ DieselError :: NotFound => public_error_from_diesel (
420
+ e,
421
+ ErrorHandler :: NotFoundByResource ( authz_pool) ,
422
+ ) ,
423
+ _ => public_error_from_diesel ( e, ErrorHandler :: Server ) ,
424
+ } ) ?;
425
+ let capacity = Self :: accumulate_ip_range_sizes ( ranges) ?;
426
+ Ok ( ( allocated, capacity) )
427
+ }
410
428
411
- use diesel:: dsl:: sql;
412
- use diesel:: sql_types:: BigInt ;
413
- use nexus_db_schema:: schema:: external_ip;
429
+ /// Return the total number of IPs allocated from the provided pool.
430
+ #[ cfg( test) ]
431
+ async fn ip_pool_allocated_count (
432
+ & self ,
433
+ opctx : & OpContext ,
434
+ authz_pool : & authz:: IpPool ,
435
+ ) -> Result < i64 , Error > {
436
+ opctx. authorize ( authz:: Action :: Read , authz_pool) . await ?;
437
+ let conn = self . pool_connection_authorized ( opctx) . await ?;
438
+ self . ip_pool_allocated_count_on_connection ( & conn, authz_pool)
439
+ . await
440
+ . map_err ( |e| public_error_from_diesel ( e, ErrorHandler :: Server ) )
441
+ }
414
442
415
- let ( ipv4, ipv6) = external_ip:: table
443
+ async fn ip_pool_allocated_count_on_connection (
444
+ & self ,
445
+ conn : & async_bb8_diesel:: Connection < DbConnection > ,
446
+ authz_pool : & authz:: IpPool ,
447
+ ) -> Result < i64 , DieselError > {
448
+ use nexus_db_schema:: schema:: external_ip;
449
+ external_ip:: table
416
450
. filter ( external_ip:: ip_pool_id. eq ( authz_pool. id ( ) ) )
417
451
. filter ( external_ip:: time_deleted. is_null ( ) )
418
- // need to do distinct IP because SNAT IPs are shared between
419
- // multiple instances, and each gets its own row in the table
420
- . select ( (
421
- sql :: < BigInt > (
422
- "count(distinct ip) FILTER (WHERE family(ip) = 4)" ,
423
- ) ,
424
- sql :: < BigInt > (
425
- "count(distinct ip) FILTER (WHERE family(ip) = 6)" ,
426
- ) ,
427
- ) )
428
- . first_async :: < ( i64 , i64 ) > (
429
- & * self . pool_connection_authorized ( opctx) . await ?,
430
- )
452
+ . select ( diesel:: dsl:: count_distinct ( external_ip:: ip) )
453
+ . first_async :: < i64 > ( conn)
431
454
. await
432
- . map_err ( |e| public_error_from_diesel ( e, ErrorHandler :: Server ) ) ?;
433
-
434
- Ok ( IpsAllocated { ipv4, ipv6 } )
435
455
}
436
456
437
- pub async fn ip_pool_total_capacity (
457
+ /// Return the total capacity of the provided pool.
458
+ #[ cfg( test) ]
459
+ async fn ip_pool_total_capacity (
438
460
& self ,
439
461
opctx : & OpContext ,
440
462
authz_pool : & authz:: IpPool ,
441
- ) -> Result < IpsCapacity , Error > {
463
+ ) -> Result < u128 , Error > {
442
464
opctx. authorize ( authz:: Action :: Read , authz_pool) . await ?;
443
465
opctx. authorize ( authz:: Action :: ListChildren , authz_pool) . await ?;
466
+ let conn = self . pool_connection_authorized ( opctx) . await ?;
467
+ self . ip_pool_list_ranges_batched_on_connection ( & conn, authz_pool)
468
+ . await
469
+ . map_err ( |e| {
470
+ public_error_from_diesel (
471
+ e,
472
+ ErrorHandler :: NotFoundByResource ( authz_pool) ,
473
+ )
474
+ } )
475
+ . and_then ( Self :: accumulate_ip_range_sizes)
476
+ }
444
477
478
+ async fn ip_pool_list_ranges_batched_on_connection (
479
+ & self ,
480
+ conn : & async_bb8_diesel:: Connection < DbConnection > ,
481
+ authz_pool : & authz:: IpPool ,
482
+ ) -> Result < Vec < ( IpNetwork , IpNetwork ) > , DieselError > {
445
483
use nexus_db_schema:: schema:: ip_pool_range;
446
-
447
- let ranges = ip_pool_range:: table
484
+ ip_pool_range:: table
448
485
. filter ( ip_pool_range:: ip_pool_id. eq ( authz_pool. id ( ) ) )
449
486
. filter ( ip_pool_range:: time_deleted. is_null ( ) )
450
- . select ( IpPoolRange :: as_select ( ) )
487
+ . select ( ( ip_pool_range :: first_address , ip_pool_range :: last_address ) )
451
488
// This is a rare unpaginated DB query, which means we are
452
489
// vulnerable to a resource exhaustion attack in which someone
453
490
// creates a very large number of ranges in order to make this
@@ -457,28 +494,25 @@ impl DataStore {
457
494
// than 10,000 ranges in a pool, we will undercount, but I have a
458
495
// hard time seeing that as a practical problem.
459
496
. limit ( 10000 )
460
- . get_results_async :: < IpPoolRange > (
461
- & * self . pool_connection_authorized ( opctx) . await ?,
462
- )
497
+ . get_results_async :: < ( IpNetwork , IpNetwork ) > ( conn)
463
498
. await
464
- . map_err ( |e| {
465
- public_error_from_diesel (
466
- e,
467
- ErrorHandler :: NotFoundByResource ( authz_pool) ,
468
- )
469
- } ) ?;
470
-
471
- let mut ipv4: u32 = 0 ;
472
- let mut ipv6: u128 = 0 ;
499
+ }
473
500
474
- for range in & ranges {
475
- let r = IpRange :: from ( range) ;
501
+ fn accumulate_ip_range_sizes (
502
+ ranges : Vec < ( IpNetwork , IpNetwork ) > ,
503
+ ) -> Result < u128 , Error > {
504
+ let mut count: u128 = 0 ;
505
+ for range in ranges. into_iter ( ) {
506
+ let first = range. 0 . ip ( ) ;
507
+ let last = range. 1 . ip ( ) ;
508
+ let r = IpRange :: try_from ( ( first, last) )
509
+ . map_err ( |e| Error :: internal_error ( e. as_str ( ) ) ) ?;
476
510
match r {
477
- IpRange :: V4 ( r) => ipv4 += r. len ( ) ,
478
- IpRange :: V6 ( r) => ipv6 += r. len ( ) ,
511
+ IpRange :: V4 ( r) => count += u128 :: from ( r. len ( ) ) ,
512
+ IpRange :: V6 ( r) => count += r. len ( ) ,
479
513
}
480
514
}
481
- Ok ( IpsCapacity { ipv4 , ipv6 } )
515
+ Ok ( count )
482
516
}
483
517
484
518
pub async fn ip_pool_silo_list (
@@ -1523,8 +1557,7 @@ mod test {
1523
1557
. ip_pool_total_capacity ( & opctx, & authz_pool)
1524
1558
. await
1525
1559
. unwrap ( ) ;
1526
- assert_eq ! ( max_ips. ipv4, 0 ) ;
1527
- assert_eq ! ( max_ips. ipv6, 0 ) ;
1560
+ assert_eq ! ( max_ips, 0 ) ;
1528
1561
1529
1562
let range = IpRange :: V4 (
1530
1563
Ipv4Range :: new (
@@ -1543,8 +1576,7 @@ mod test {
1543
1576
. ip_pool_total_capacity ( & opctx, & authz_pool)
1544
1577
. await
1545
1578
. unwrap ( ) ;
1546
- assert_eq ! ( max_ips. ipv4, 5 ) ;
1547
- assert_eq ! ( max_ips. ipv6, 0 ) ;
1579
+ assert_eq ! ( max_ips, 5 ) ;
1548
1580
1549
1581
let link = IpPoolResource {
1550
1582
ip_pool_id : pool. id ( ) ,
@@ -1561,8 +1593,7 @@ mod test {
1561
1593
. ip_pool_allocated_count ( & opctx, & authz_pool)
1562
1594
. await
1563
1595
. unwrap ( ) ;
1564
- assert_eq ! ( ip_count. ipv4, 0 ) ;
1565
- assert_eq ! ( ip_count. ipv6, 0 ) ;
1596
+ assert_eq ! ( ip_count, 0 ) ;
1566
1597
1567
1598
let identity = IdentityMetadataCreateParams {
1568
1599
name : "my-ip" . parse ( ) . unwrap ( ) ,
@@ -1578,16 +1609,14 @@ mod test {
1578
1609
. ip_pool_allocated_count ( & opctx, & authz_pool)
1579
1610
. await
1580
1611
. unwrap ( ) ;
1581
- assert_eq ! ( ip_count. ipv4, 1 ) ;
1582
- assert_eq ! ( ip_count. ipv6, 0 ) ;
1612
+ assert_eq ! ( ip_count, 1 ) ;
1583
1613
1584
1614
// allocating one has nothing to do with total capacity
1585
1615
let max_ips = datastore
1586
1616
. ip_pool_total_capacity ( & opctx, & authz_pool)
1587
1617
. await
1588
1618
. unwrap ( ) ;
1589
- assert_eq ! ( max_ips. ipv4, 5 ) ;
1590
- assert_eq ! ( max_ips. ipv6, 0 ) ;
1619
+ assert_eq ! ( max_ips, 5 ) ;
1591
1620
1592
1621
db. terminate ( ) . await ;
1593
1622
logctx. cleanup_successful ( ) ;
@@ -1642,8 +1671,7 @@ mod test {
1642
1671
. ip_pool_total_capacity ( & opctx, & authz_pool)
1643
1672
. await
1644
1673
. unwrap ( ) ;
1645
- assert_eq ! ( max_ips. ipv4, 0 ) ;
1646
- assert_eq ! ( max_ips. ipv6, 0 ) ;
1674
+ assert_eq ! ( max_ips, 0 ) ;
1647
1675
1648
1676
// Add an IPv6 range
1649
1677
let ipv6_range = IpRange :: V6 (
@@ -1661,15 +1689,13 @@ mod test {
1661
1689
. ip_pool_total_capacity ( & opctx, & authz_pool)
1662
1690
. await
1663
1691
. unwrap ( ) ;
1664
- assert_eq ! ( max_ips. ipv4, 0 ) ;
1665
- assert_eq ! ( max_ips. ipv6, 11 + 65536 ) ;
1692
+ assert_eq ! ( max_ips, 11 + 65536 ) ;
1666
1693
1667
1694
let ip_count = datastore
1668
1695
. ip_pool_allocated_count ( & opctx, & authz_pool)
1669
1696
. await
1670
1697
. unwrap ( ) ;
1671
- assert_eq ! ( ip_count. ipv4, 0 ) ;
1672
- assert_eq ! ( ip_count. ipv6, 0 ) ;
1698
+ assert_eq ! ( ip_count, 0 ) ;
1673
1699
1674
1700
let identity = IdentityMetadataCreateParams {
1675
1701
name : "my-ip" . parse ( ) . unwrap ( ) ,
@@ -1685,16 +1711,14 @@ mod test {
1685
1711
. ip_pool_allocated_count ( & opctx, & authz_pool)
1686
1712
. await
1687
1713
. unwrap ( ) ;
1688
- assert_eq ! ( ip_count. ipv4, 0 ) ;
1689
- assert_eq ! ( ip_count. ipv6, 1 ) ;
1714
+ assert_eq ! ( ip_count, 1 ) ;
1690
1715
1691
1716
// allocating one has nothing to do with total capacity
1692
1717
let max_ips = datastore
1693
1718
. ip_pool_total_capacity ( & opctx, & authz_pool)
1694
1719
. await
1695
1720
. unwrap ( ) ;
1696
- assert_eq ! ( max_ips. ipv4, 0 ) ;
1697
- assert_eq ! ( max_ips. ipv6, 11 + 65536 ) ;
1721
+ assert_eq ! ( max_ips, 11 + 65536 ) ;
1698
1722
1699
1723
// add a giant range for fun
1700
1724
let ipv6_range = IpRange :: V6 (
@@ -1715,8 +1739,7 @@ mod test {
1715
1739
. ip_pool_total_capacity ( & opctx, & authz_pool)
1716
1740
. await
1717
1741
. unwrap ( ) ;
1718
- assert_eq ! ( max_ips. ipv4, 0 ) ;
1719
- assert_eq ! ( max_ips. ipv6, 1208925819614629174706166 ) ;
1742
+ assert_eq ! ( max_ips, 1208925819614629174706166 ) ;
1720
1743
1721
1744
db. terminate ( ) . await ;
1722
1745
logctx. cleanup_successful ( ) ;
0 commit comments