From c875f44b141888ffa842a8a51c8baf571cc7dcb4 Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Mon, 2 Sep 2024 11:12:12 -0700 Subject: [PATCH] create default internet gateway on vpc create --- nexus/src/app/sagas/vpc_create.rs | 143 +++++++++++++++++- .../integration_tests/internet_gateway.rs | 10 +- 2 files changed, 142 insertions(+), 11 deletions(-) diff --git a/nexus/src/app/sagas/vpc_create.rs b/nexus/src/app/sagas/vpc_create.rs index 832ca64acea..9bc85abc2f0 100644 --- a/nexus/src/app/sagas/vpc_create.rs +++ b/nexus/src/app/sagas/vpc_create.rs @@ -8,15 +8,13 @@ use super::NexusSaga; use super::ACTION_GENERATE_ID; use crate::app::sagas::declare_saga_actions; use crate::external_api::params; +use nexus_db_model::InternetGatewayIpPool; use nexus_db_queries::db::queries::vpc_subnet::InsertVpcSubnetError; use nexus_db_queries::{authn, authz, db}; use nexus_defaults as defaults; use omicron_common::api::external; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::LookupType; -use omicron_common::api::external::RouteDestination; -use omicron_common::api::external::RouteTarget; -use omicron_common::api::external::RouterRouteKind; use oxnet::IpNet; use serde::Deserialize; use serde::Serialize; @@ -45,6 +43,10 @@ declare_saga_actions! { + svc_create_router - svc_create_router_undo } + VPC_CREATE_GATEWAY -> "gateway" { + + svc_create_gateway + - svc_create_gateway_undo + } VPC_CREATE_V4_ROUTE -> "route4" { + svc_create_v4_route - svc_create_v4_route_undo @@ -98,12 +100,18 @@ pub fn create_dag( "GenerateDefaultSubnetId", ACTION_GENERATE_ID.as_ref(), )); + builder.append(Node::action( + "default_internet_gateway_id", + "GenerateDefaultInternetGatewayId", + ACTION_GENERATE_ID.as_ref(), + )); builder.append(vpc_create_vpc_action()); builder.append(vpc_create_router_action()); builder.append(vpc_create_v4_route_action()); builder.append(vpc_create_v6_route_action()); builder.append(vpc_create_subnet_action()); builder.append(vpc_update_firewall_action()); + builder.append(vpc_create_gateway_action()); builder.append(vpc_notify_sleds_action()); Ok(builder.build()?) @@ -280,14 +288,16 @@ async fn svc_create_route( let route = db::model::RouterRoute::new( route_id, system_router_id, - RouterRouteKind::Default, + external::RouterRouteKind::Default, params::RouterRouteCreate { identity: IdentityMetadataCreateParams { name: name.parse().unwrap(), description: "The default route of a vpc".to_string(), }, - target: RouteTarget::InternetGateway("outbound".parse().unwrap()), - destination: RouteDestination::IpNet(default_net), + target: external::RouteTarget::InternetGateway( + "default".parse().unwrap(), + ), + destination: external::RouteDestination::IpNet(default_net), }, ); @@ -460,6 +470,92 @@ async fn svc_update_firewall_undo( Ok(()) } +async fn svc_create_gateway( + sagactx: NexusActionContext, +) -> Result { + let osagactx = sagactx.user_data(); + let params = sagactx.saga_params::()?; + let opctx = crate::context::op_context_for_saga_action( + &sagactx, + ¶ms.serialized_authn, + ); + let vpc_id = sagactx.lookup::("vpc_id")?; + let default_igw_id = + sagactx.lookup::("default_internet_gateway_id")?; + let (authz_vpc, _) = + sagactx.lookup::<(authz::Vpc, db::model::Vpc)>("vpc")?; + + let igw = db::model::InternetGateway::new( + default_igw_id, + vpc_id, + params::InternetGatewayCreate { + identity: IdentityMetadataCreateParams { + name: "default".parse().unwrap(), + description: "Automatically created default VPC gateway".into(), + }, + }, + ); + + let (authz_igw, _) = osagactx + .datastore() + .vpc_create_internet_gateway(&opctx, &authz_vpc, igw) + .await + .map_err(ActionError::action_failed)?; + + match osagactx.datastore().ip_pools_fetch_default(&opctx).await { + Ok((authz_ip_pool, _db_ip_pool)) => { + // Attach the default IP pool to the default gateway. + // Failure of this saga takes out the gateway with a cascading delete and + // thus this ip pool. + osagactx + .datastore() + .internet_gateway_attach_ip_pool( + &opctx, + &authz_igw, + InternetGatewayIpPool::new( + Uuid::new_v4(), + authz_ip_pool.id(), + authz_igw.id(), + IdentityMetadataCreateParams { + name: "default".parse().unwrap(), + description: + "Automatically attached default IP pool".into(), + }, + ), + ) + .await + .map_err(ActionError::action_failed)?; + } + Err(e) => { + warn!( + opctx.log, + "Default ip pool lookup failed: {e}. \ + Default gateway has no ip pool association", + ); + } + }; + + Ok(authz_igw) +} + +async fn svc_create_gateway_undo( + sagactx: NexusActionContext, +) -> Result<(), anyhow::Error> { + let osagactx = sagactx.user_data(); + let params = sagactx.saga_params::()?; + let opctx = crate::context::op_context_for_saga_action( + &sagactx, + ¶ms.serialized_authn, + ); + let authz_igw = sagactx.lookup::("gateway")?; + + osagactx + .datastore() + .vpc_delete_internet_gateway(&opctx, &authz_igw, true) + .await?; + Ok(()) +} + async fn svc_notify_sleds( sagactx: NexusActionContext, ) -> Result<(), ActionError> { @@ -623,6 +719,19 @@ pub(crate) mod test { .await .expect("Failed to delete system router"); + // Default gateway + let (.., authz_igw, _igw) = LookupPath::new(&opctx, &datastore) + .project_id(project_id) + .vpc_name(&default_name.clone().into()) + .internet_gateway_name(&default_name.clone().into()) + .fetch() + .await + .expect("Failed to fetch default gateway"); + datastore + .vpc_delete_internet_gateway(&opctx, &authz_igw, true) + .await + .expect("Failed to delete default gateway"); + // Default VPC & Firewall Rules let (.., authz_vpc, vpc) = LookupPath::new(&opctx, &datastore) .project_id(project_id) @@ -645,6 +754,7 @@ pub(crate) mod test { assert!(no_routers_exist(datastore).await); assert!(no_routes_exist(datastore).await); assert!(no_subnets_exist(datastore).await); + assert!(no_gateways_exist(datastore).await); assert!(no_firewall_rules_exist(datastore).await); } @@ -690,6 +800,27 @@ pub(crate) mod test { .is_none() } + async fn no_gateways_exist(datastore: &DataStore) -> bool { + use nexus_db_queries::db::model::InternetGateway; + use nexus_db_queries::db::schema::internet_gateway::dsl; + + dsl::internet_gateway + .filter(dsl::time_deleted.is_null()) + // ignore built-in services VPC + .filter(dsl::vpc_id.ne(*SERVICES_VPC_ID)) + .select(InternetGateway::as_select()) + .first_async::( + &*datastore.pool_connection_for_tests().await.unwrap(), + ) + .await + .optional() + .unwrap() + .map(|router| { + eprintln!("Internet gateway exists: {router:?}"); + }) + .is_none() + } + async fn no_routes_exist(datastore: &DataStore) -> bool { use nexus_db_queries::db::model::RouterRoute; use nexus_db_queries::db::schema::router_route::dsl; diff --git a/nexus/tests/integration_tests/internet_gateway.rs b/nexus/tests/integration_tests/internet_gateway.rs index 86777b9d787..620e3c36596 100644 --- a/nexus/tests/integration_tests/internet_gateway.rs +++ b/nexus/tests/integration_tests/internet_gateway.rs @@ -52,15 +52,15 @@ async fn test_internet_gateway_basic_crud(ctx: &ControlPlaneTestContext) { let c = &ctx.external_client; test_setup(c).await; - // should start with zero gateways + // should start with just default gateway let igws = list_internet_gateways(c, PROJECT_NAME, VPC_NAME).await; - assert_eq!(igws.len(), 0, "should start with zero internet gateways"); + assert_eq!(igws.len(), 1, "should start with zero internet gateways"); expect_igw_not_found(c, PROJECT_NAME, VPC_NAME, IGW_NAME).await; // create an internet gateway let gw = create_internet_gateway(c, PROJECT_NAME, VPC_NAME, IGW_NAME).await; let igws = list_internet_gateways(c, PROJECT_NAME, VPC_NAME).await; - assert_eq!(igws.len(), 1, "should now have one internet gateway"); + assert_eq!(igws.len(), 2, "should now have two internet gateways"); // should be able to get the gateway just created let same_igw = get_igw(c, PROJECT_NAME, VPC_NAME, IGW_NAME).await; @@ -136,7 +136,7 @@ async fn test_internet_gateway_basic_crud(ctx: &ControlPlaneTestContext) { let igw_addrs = list_internet_gateway_ip_pools(c, PROJECT_NAME, VPC_NAME, IGW_NAME) .await; - assert_eq!(igw_addrs.len(), 0, "should now have zero attached ip pool"); + assert_eq!(igw_addrs.len(), 0, "should now have zero attached ip pools"); // detach an ip address detach_ip_address_from_igw( @@ -160,7 +160,7 @@ async fn test_internet_gateway_basic_crud(ctx: &ControlPlaneTestContext) { // delete internet gateay delete_internet_gateway(c, PROJECT_NAME, VPC_NAME, IGW_NAME, false).await; let igws = list_internet_gateways(c, PROJECT_NAME, VPC_NAME).await; - assert_eq!(igws.len(), 0, "should now have zero internet gateways"); + assert_eq!(igws.len(), 1, "should now just have default gateway"); // looking for gateway should return 404 expect_igw_not_found(c, PROJECT_NAME, VPC_NAME, IGW_NAME).await;