-
Notifications
You must be signed in to change notification settings - Fork 63
Current IP pool utilization endpoint #5258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b865571
da38cd3
25d6e26
57dd18c
4629e40
e320f1a
03a6a58
7f4052f
759e3e6
9860fa8
39edb8f
9657e8d
56a4de3
24763ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,16 @@ use omicron_common::api::external::UpdateResult; | |
| use ref_cast::RefCast; | ||
| use uuid::Uuid; | ||
|
|
||
| pub struct IpsAllocated { | ||
| pub ipv4: i64, | ||
| pub ipv6: i64, | ||
| } | ||
|
|
||
| pub struct IpsCapacity { | ||
| pub ipv4: u32, | ||
| pub ipv6: u128, | ||
| } | ||
|
|
||
| impl DataStore { | ||
| /// List IP Pools | ||
| pub async fn ip_pools_list( | ||
|
|
@@ -324,6 +334,80 @@ impl DataStore { | |
| }) | ||
| } | ||
|
|
||
| pub async fn ip_pool_allocated_count( | ||
| &self, | ||
| opctx: &OpContext, | ||
| authz_pool: &authz::IpPool, | ||
| ) -> Result<IpsAllocated, Error> { | ||
| opctx.authorize(authz::Action::Read, authz_pool).await?; | ||
|
|
||
| use db::schema::external_ip; | ||
| use diesel::dsl::sql; | ||
| use diesel::sql_types::BigInt; | ||
|
|
||
| let (ipv4, ipv6) = external_ip::table | ||
| .filter(external_ip::ip_pool_id.eq(authz_pool.id())) | ||
| .filter(external_ip::time_deleted.is_null()) | ||
| .select(( | ||
| sql::<BigInt>("count(*) FILTER (WHERE family(ip) = 4)"), | ||
| sql::<BigInt>("count(*) FILTER (WHERE family(ip) = 6)"), | ||
| )) | ||
| .first_async::<(i64, i64)>( | ||
| &*self.pool_connection_authorized(opctx).await?, | ||
| ) | ||
| .await | ||
| .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; | ||
|
|
||
| Ok(IpsAllocated { ipv4, ipv6 }) | ||
| } | ||
|
|
||
| pub async fn ip_pool_total_capacity( | ||
| &self, | ||
| opctx: &OpContext, | ||
| authz_pool: &authz::IpPool, | ||
| ) -> Result<IpsCapacity, Error> { | ||
| opctx.authorize(authz::Action::Read, authz_pool).await?; | ||
| opctx.authorize(authz::Action::ListChildren, authz_pool).await?; | ||
|
|
||
| use db::schema::ip_pool_range; | ||
|
|
||
| let ranges = ip_pool_range::table | ||
| .filter(ip_pool_range::ip_pool_id.eq(authz_pool.id())) | ||
| .filter(ip_pool_range::time_deleted.is_null()) | ||
| .select(IpPoolRange::as_select()) | ||
| // This is a rare unpaginated DB query, which means we are | ||
| // vulnerable to a resource exhaustion attack in which someone | ||
| // creates a very large number of ranges in order to make this | ||
| // query slow. In order to mitigate that, we limit the query to the | ||
| // (current) max allowed page size, effectively making this query | ||
| // exactly as vulnerable as if it were paginated. If there are more | ||
| // than 10,000 ranges in a pool, we will undercount, but I have a | ||
| // hard time seeing that as a practical problem. | ||
| .limit(10000) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In private chat we discussed the alternative of doing this calculation in SQL so we don't have to limit it here. What we found is that neither CRDB nor Postgres support subtraction of IPv6 addresses when the result is greater than a 64 bit integer, and we couldn't figure out how to convert the addresses to 128 bit integers first and then subtract. So it seems we are forced to do this in Rust. |
||
| .get_results_async::<IpPoolRange>( | ||
| &*self.pool_connection_authorized(opctx).await?, | ||
| ) | ||
| .await | ||
| .map_err(|e| { | ||
| public_error_from_diesel( | ||
| e, | ||
| ErrorHandler::NotFoundByResource(authz_pool), | ||
| ) | ||
| })?; | ||
|
|
||
| let mut ipv4: u32 = 0; | ||
| let mut ipv6: u128 = 0; | ||
|
|
||
| for range in &ranges { | ||
| let r = IpRange::from(range); | ||
| match r { | ||
| IpRange::V4(r) => ipv4 += r.len(), | ||
| IpRange::V6(r) => ipv6 += r.len(), | ||
| } | ||
| } | ||
| Ok(IpsCapacity { ipv4, ipv6 }) | ||
| } | ||
|
|
||
| pub async fn ip_pool_silo_list( | ||
| &self, | ||
| opctx: &OpContext, | ||
|
|
@@ -846,10 +930,14 @@ mod test { | |
|
|
||
| use crate::authz; | ||
| use crate::db::datastore::test_utils::datastore_test; | ||
| use crate::db::model::{IpPool, IpPoolResource, IpPoolResourceType}; | ||
| use crate::db::model::{ | ||
| IpPool, IpPoolResource, IpPoolResourceType, Project, | ||
| }; | ||
| use assert_matches::assert_matches; | ||
| use nexus_test_utils::db::test_setup_database; | ||
| use nexus_types::external_api::params; | ||
| use nexus_types::identity::Resource; | ||
| use omicron_common::address::{IpRange, Ipv4Range, Ipv6Range}; | ||
| use omicron_common::api::external::http_pagination::PaginatedBy; | ||
| use omicron_common::api::external::{ | ||
| DataPageParams, Error, IdentityMetadataCreateParams, LookupType, | ||
|
|
@@ -1075,4 +1163,155 @@ mod test { | |
| db.cleanup().await.unwrap(); | ||
| logctx.cleanup_successful(); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_ip_pool_utilization() { | ||
| let logctx = dev::test_setup_log("test_ip_utilization"); | ||
| let mut db = test_setup_database(&logctx.log).await; | ||
| let (opctx, datastore) = datastore_test(&logctx, &db).await; | ||
|
|
||
| let authz_silo = opctx.authn.silo_required().unwrap(); | ||
| let project = Project::new( | ||
| authz_silo.id(), | ||
| params::ProjectCreate { | ||
| identity: IdentityMetadataCreateParams { | ||
| name: "my-project".parse().unwrap(), | ||
| description: "".to_string(), | ||
| }, | ||
| }, | ||
| ); | ||
| let (.., project) = | ||
| datastore.project_create(&opctx, project).await.unwrap(); | ||
|
|
||
| // create an IP pool for the silo, add a range to it, and link it to the silo | ||
| let identity = IdentityMetadataCreateParams { | ||
| name: "my-pool".parse().unwrap(), | ||
| description: "".to_string(), | ||
| }; | ||
| let pool = datastore | ||
| .ip_pool_create(&opctx, IpPool::new(&identity)) | ||
| .await | ||
| .expect("Failed to create IP pool"); | ||
| let authz_pool = authz::IpPool::new( | ||
| authz::FLEET, | ||
| pool.id(), | ||
| LookupType::ById(pool.id()), | ||
| ); | ||
|
|
||
| // capacity of zero because there are no ranges | ||
| let max_ips = datastore | ||
| .ip_pool_total_capacity(&opctx, &authz_pool) | ||
| .await | ||
| .unwrap(); | ||
| assert_eq!(max_ips.ipv4, 0); | ||
| assert_eq!(max_ips.ipv6, 0); | ||
|
|
||
| let range = IpRange::V4( | ||
| Ipv4Range::new( | ||
| std::net::Ipv4Addr::new(10, 0, 0, 1), | ||
| std::net::Ipv4Addr::new(10, 0, 0, 5), | ||
| ) | ||
| .unwrap(), | ||
| ); | ||
| datastore | ||
| .ip_pool_add_range(&opctx, &authz_pool, &range) | ||
| .await | ||
| .expect("Could not add range"); | ||
|
|
||
| // now it has a capacity of 5 because we added the range | ||
| let max_ips = datastore | ||
| .ip_pool_total_capacity(&opctx, &authz_pool) | ||
| .await | ||
| .unwrap(); | ||
| assert_eq!(max_ips.ipv4, 5); | ||
| assert_eq!(max_ips.ipv6, 0); | ||
|
|
||
| let link = IpPoolResource { | ||
| ip_pool_id: pool.id(), | ||
| resource_type: IpPoolResourceType::Silo, | ||
| resource_id: authz_silo.id(), | ||
| is_default: true, | ||
| }; | ||
| datastore | ||
| .ip_pool_link_silo(&opctx, link) | ||
| .await | ||
| .expect("Could not link pool to silo"); | ||
|
|
||
| let ip_count = datastore | ||
| .ip_pool_allocated_count(&opctx, &authz_pool) | ||
| .await | ||
| .unwrap(); | ||
| assert_eq!(ip_count.ipv4, 0); | ||
| assert_eq!(ip_count.ipv6, 0); | ||
|
|
||
| let identity = IdentityMetadataCreateParams { | ||
| name: "my-ip".parse().unwrap(), | ||
| description: "".to_string(), | ||
| }; | ||
| let ip = datastore | ||
| .allocate_floating_ip(&opctx, project.id(), identity, None, None) | ||
| .await | ||
| .expect("Could not allocate floating IP"); | ||
| assert_eq!(ip.ip.to_string(), "10.0.0.1/32"); | ||
|
|
||
| let ip_count = datastore | ||
| .ip_pool_allocated_count(&opctx, &authz_pool) | ||
| .await | ||
| .unwrap(); | ||
| assert_eq!(ip_count.ipv4, 1); | ||
| assert_eq!(ip_count.ipv6, 0); | ||
|
|
||
| // allocating one has nothing to do with total capacity | ||
| let max_ips = datastore | ||
| .ip_pool_total_capacity(&opctx, &authz_pool) | ||
| .await | ||
| .unwrap(); | ||
| assert_eq!(max_ips.ipv4, 5); | ||
| assert_eq!(max_ips.ipv6, 0); | ||
|
|
||
| let ipv6_range = IpRange::V6( | ||
| Ipv6Range::new( | ||
| std::net::Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 10), | ||
| std::net::Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 1, 20), | ||
| ) | ||
| .unwrap(), | ||
| ); | ||
| datastore | ||
| .ip_pool_add_range(&opctx, &authz_pool, &ipv6_range) | ||
| .await | ||
| .expect("Could not add range"); | ||
|
|
||
| // now test with additional v6 range | ||
| let max_ips = datastore | ||
| .ip_pool_total_capacity(&opctx, &authz_pool) | ||
| .await | ||
| .unwrap(); | ||
| assert_eq!(max_ips.ipv4, 5); | ||
| assert_eq!(max_ips.ipv6, 11 + 65536); | ||
|
|
||
| // add a giant range for fun | ||
| let ipv6_range = IpRange::V6( | ||
| Ipv6Range::new( | ||
| std::net::Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 1, 21), | ||
| std::net::Ipv6Addr::new( | ||
| 0xfd00, 0, 0, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, | ||
| ), | ||
| ) | ||
| .unwrap(), | ||
| ); | ||
| datastore | ||
| .ip_pool_add_range(&opctx, &authz_pool, &ipv6_range) | ||
| .await | ||
| .expect("Could not add range"); | ||
|
|
||
| let max_ips = datastore | ||
| .ip_pool_total_capacity(&opctx, &authz_pool) | ||
| .await | ||
| .unwrap(); | ||
| assert_eq!(max_ips.ipv4, 5); | ||
| assert_eq!(max_ips.ipv6, 1208925819614629174706166); | ||
|
|
||
| db.cleanup().await.unwrap(); | ||
| logctx.cleanup_successful(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.