Skip to content

Commit fb0b6cf

Browse files
authored
Check VPC and VPC Subnet for children before deleting (#1483)
* Check VPC and VPC Subnet for children before deleting - Add rcgen to vpc_subnet table and model - Add an implementation of `DatastoreCollection` for VPC Subnets, where the children are network interfaces. This bumps the `rcgen` whenever a new NIC is inserted. - Check for child NICs before deleting a VPC Subnet, and make the deletion conditional on the `rcgen` being the same while doing so. - Add integration test verifying that VPC Subnets can't be deleted while they contain an instance with a NIC in that subnet. - Ditto for VPCs, checking for VPC Subnets before deleting. * Remove debugging printlns
1 parent 6d8d6a4 commit fb0b6cf

File tree

15 files changed

+389
-123
lines changed

15 files changed

+389
-123
lines changed

common/src/sql/dbinit.sql

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,10 @@ CREATE TABLE omicron.public.vpc (
721721

722722
/* Used to ensure that two requests do not concurrently modify the
723723
VPC's firewall */
724-
firewall_gen INT NOT NULL
724+
firewall_gen INT NOT NULL,
725+
726+
/* Child-resource generation number for VPC Subnets. */
727+
subnet_gen INT8 NOT NULL
725728
);
726729

727730
CREATE UNIQUE INDEX ON omicron.public.vpc (
@@ -745,6 +748,8 @@ CREATE TABLE omicron.public.vpc_subnet (
745748
/* Indicates that the object has been deleted */
746749
time_deleted TIMESTAMPTZ,
747750
vpc_id UUID NOT NULL,
751+
/* Child resource creation generation number */
752+
rcgen INT8 NOT NULL,
748753
ipv4_block INET NOT NULL,
749754
ipv6_block INET NOT NULL
750755
);

nexus/db-model/src/schema.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ table! {
439439
ipv6_prefix -> Inet,
440440
dns_name -> Text,
441441
firewall_gen -> Int8,
442+
subnet_gen -> Int8,
442443
}
443444
}
444445

@@ -451,6 +452,7 @@ table! {
451452
time_modified -> Timestamptz,
452453
time_deleted -> Nullable<Timestamptz>,
453454
vpc_id -> Uuid,
455+
rcgen -> Int8,
454456
ipv4_block -> Inet,
455457
ipv6_block -> Inet,
456458
}

nexus/db-model/src/vpc.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
use super::{Generation, Ipv6Net, Name, VpcFirewallRule};
5+
use super::{Generation, Ipv6Net, Name, VpcFirewallRule, VpcSubnet};
66
use crate::collection::DatastoreCollectionConfig;
7-
use crate::schema::{vpc, vpc_firewall_rule};
7+
use crate::schema::{vpc, vpc_firewall_rule, vpc_subnet};
88
use crate::Vni;
99
use chrono::{DateTime, Utc};
1010
use db_macros::Resource;
@@ -31,6 +31,9 @@ pub struct Vpc {
3131
/// firewall generation number, used as a child resource generation number
3232
/// per RFD 192
3333
pub firewall_gen: Generation,
34+
35+
/// VPC Subnet generation number
36+
pub subnet_gen: Generation,
3437
}
3538

3639
impl From<Vpc> for views::Vpc {
@@ -58,6 +61,7 @@ pub struct IncompleteVpc {
5861
pub ipv6_prefix: IpNetwork,
5962
pub dns_name: Name,
6063
pub firewall_gen: Generation,
64+
pub subnet_gen: Generation,
6165
}
6266

6367
impl IncompleteVpc {
@@ -92,6 +96,7 @@ impl IncompleteVpc {
9296
ipv6_prefix,
9397
dns_name: params.dns_name.into(),
9498
firewall_gen: Generation::new(),
99+
subnet_gen: Generation::new(),
95100
})
96101
}
97102
}
@@ -103,6 +108,13 @@ impl DatastoreCollectionConfig<VpcFirewallRule> for Vpc {
103108
type CollectionIdColumn = vpc_firewall_rule::dsl::vpc_id;
104109
}
105110

111+
impl DatastoreCollectionConfig<VpcSubnet> for Vpc {
112+
type CollectionId = Uuid;
113+
type GenerationNumberColumn = vpc::dsl::subnet_gen;
114+
type CollectionTimeDeletedColumn = vpc::dsl::time_deleted;
115+
type CollectionIdColumn = vpc_subnet::dsl::vpc_id;
116+
}
117+
106118
#[derive(AsChangeset)]
107119
#[diesel(table_name = vpc)]
108120
pub struct VpcUpdate {

nexus/db-model/src/vpc_subnet.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5+
use super::Generation;
56
use super::{Ipv4Net, Ipv6Net, Name};
7+
use crate::collection::DatastoreCollectionConfig;
8+
use crate::schema::network_interface;
69
use crate::schema::vpc_subnet;
10+
use crate::NetworkInterface;
711
use chrono::{DateTime, Utc};
812
use db_macros::Resource;
913
use nexus_types::external_api::params;
@@ -20,6 +24,7 @@ pub struct VpcSubnet {
2024
pub identity: VpcSubnetIdentity,
2125

2226
pub vpc_id: Uuid,
27+
pub rcgen: Generation,
2328
pub ipv4_block: Ipv4Net,
2429
pub ipv6_block: Ipv6Net,
2530
}
@@ -40,6 +45,7 @@ impl VpcSubnet {
4045
Self {
4146
identity,
4247
vpc_id,
48+
rcgen: Generation::new(),
4349
ipv4_block: Ipv4Net(ipv4_block),
4450
ipv6_block: Ipv6Net(ipv6_block),
4551
}
@@ -105,3 +111,10 @@ impl From<params::VpcSubnetUpdate> for VpcSubnetUpdate {
105111
}
106112
}
107113
}
114+
115+
impl DatastoreCollectionConfig<NetworkInterface> for VpcSubnet {
116+
type CollectionId = Uuid;
117+
type GenerationNumberColumn = vpc_subnet::dsl::rcgen;
118+
type CollectionTimeDeletedColumn = vpc_subnet::dsl::time_deleted;
119+
type CollectionIdColumn = network_interface::dsl::subnet_id;
120+
}

nexus/src/app/vpc.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,22 @@ impl super::Nexus {
261261
LookupType::ById(db_vpc.system_router_id),
262262
);
263263

264+
// Possibly delete the VPC, then the router and firewall.
265+
//
266+
// We must delete the VPC first. This will fail if the VPC still
267+
// contains at least one subnet, since those are independent containers
268+
// that track network interfaces as child resources. If we delete the
269+
// router first, it'll succeed even if the VPC contains Subnets, which
270+
// means the router is now gone from an otherwise-live subnet.
271+
//
272+
// This is a good example of need for the original comment:
273+
//
264274
// TODO: This should eventually use a saga to call the
265275
// networking subsystem to have it clean up the networking resources
276+
self.db_datastore
277+
.project_delete_vpc(opctx, &db_vpc, &authz_vpc)
278+
.await?;
266279
self.db_datastore.vpc_delete_router(&opctx, &authz_vpc_router).await?;
267-
self.db_datastore.project_delete_vpc(opctx, &authz_vpc).await?;
268280

269281
// Delete all firewall rules after deleting the VPC, to ensure no
270282
// firewall rules get added between rules deletion and VPC deletion.

nexus/src/app/vpc_subnet.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -261,14 +261,17 @@ impl super::Nexus {
261261
vpc_name: &Name,
262262
subnet_name: &Name,
263263
) -> DeleteResult {
264-
let (.., authz_subnet) = LookupPath::new(opctx, &self.db_datastore)
265-
.organization_name(organization_name)
266-
.project_name(project_name)
267-
.vpc_name(vpc_name)
268-
.vpc_subnet_name(subnet_name)
269-
.lookup_for(authz::Action::Delete)
270-
.await?;
271-
self.db_datastore.vpc_delete_subnet(opctx, &authz_subnet).await
264+
let (.., authz_subnet, db_subnet) =
265+
LookupPath::new(opctx, &self.db_datastore)
266+
.organization_name(organization_name)
267+
.project_name(project_name)
268+
.vpc_name(vpc_name)
269+
.vpc_subnet_name(subnet_name)
270+
.fetch_for(authz::Action::Delete)
271+
.await?;
272+
self.db_datastore
273+
.vpc_delete_subnet(opctx, &db_subnet, &authz_subnet)
274+
.await
272275
}
273276

274277
pub async fn subnet_list_network_interfaces(

nexus/src/db/datastore/network_interface.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use super::DataStore;
88
use crate::authz;
99
use crate::context::OpContext;
1010
use crate::db;
11+
use crate::db::collection_insert::AsyncInsertError;
12+
use crate::db::collection_insert::DatastoreCollection;
1113
use crate::db::error::public_error_from_diesel_pool;
1214
use crate::db::error::ErrorHandler;
1315
use crate::db::error::TransactionError;
@@ -16,6 +18,7 @@ use crate::db::model::Instance;
1618
use crate::db::model::Name;
1719
use crate::db::model::NetworkInterface;
1820
use crate::db::model::NetworkInterfaceUpdate;
21+
use crate::db::model::VpcSubnet;
1922
use crate::db::pagination::paginated;
2023
use crate::db::queries::network_interface;
2124
use async_bb8_diesel::AsyncConnection;
@@ -27,6 +30,8 @@ use omicron_common::api::external::DataPageParams;
2730
use omicron_common::api::external::DeleteResult;
2831
use omicron_common::api::external::Error;
2932
use omicron_common::api::external::ListResultVec;
33+
use omicron_common::api::external::LookupType;
34+
use omicron_common::api::external::ResourceType;
3035
use omicron_common::api::external::UpdateResult;
3136
use sled_agent_client::types as sled_client_types;
3237

@@ -56,19 +61,31 @@ impl DataStore {
5661
interface: IncompleteNetworkInterface,
5762
) -> Result<NetworkInterface, network_interface::InsertError> {
5863
use db::schema::network_interface::dsl;
64+
let subnet_id = interface.subnet.identity.id;
5965
let query = network_interface::InsertQuery::new(interface.clone());
60-
diesel::insert_into(dsl::network_interface)
61-
.values(query)
62-
.returning(NetworkInterface::as_returning())
63-
.get_result_async(
64-
self.pool_authorized(opctx)
65-
.await
66-
.map_err(network_interface::InsertError::External)?,
67-
)
68-
.await
69-
.map_err(|e| {
66+
VpcSubnet::insert_resource(
67+
subnet_id,
68+
diesel::insert_into(dsl::network_interface).values(query),
69+
)
70+
.insert_and_get_result_async(
71+
self.pool_authorized(opctx)
72+
.await
73+
.map_err(network_interface::InsertError::External)?,
74+
)
75+
.await
76+
.map_err(|e| match e {
77+
AsyncInsertError::CollectionNotFound => {
78+
network_interface::InsertError::External(
79+
Error::ObjectNotFound {
80+
type_name: ResourceType::VpcSubnet,
81+
lookup_type: LookupType::ById(subnet_id),
82+
},
83+
)
84+
}
85+
AsyncInsertError::DatabaseError(e) => {
7086
network_interface::InsertError::from_pool(e, &interface)
71-
})
87+
}
88+
})
7289
}
7390

7491
/// Delete all network interfaces attached to the given instance.

0 commit comments

Comments
 (0)