Skip to content

Commit f76d1b4

Browse files
committed
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.
1 parent 8d13230 commit f76d1b4

File tree

15 files changed

+395
-122
lines changed

15 files changed

+395
-122
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/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.

nexus/src/db/datastore/vpc.rs

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::db;
1111
use crate::db::collection_insert::AsyncInsertError;
1212
use crate::db::collection_insert::DatastoreCollection;
1313
use crate::db::collection_insert::SyncInsertError;
14+
use crate::db::error::diesel_pool_result_optional;
1415
use crate::db::error::public_error_from_diesel_pool;
1516
use crate::db::error::ErrorHandler;
1617
use crate::db::error::TransactionError;
@@ -43,6 +44,7 @@ use omicron_common::api::external::ListResultVec;
4344
use omicron_common::api::external::LookupType;
4445
use omicron_common::api::external::ResourceType;
4546
use omicron_common::api::external::UpdateResult;
47+
use uuid::Uuid;
4648

4749
impl DataStore {
4850
pub async fn project_list_vpcs(
@@ -128,33 +130,80 @@ impl DataStore {
128130
pub async fn project_delete_vpc(
129131
&self,
130132
opctx: &OpContext,
133+
db_vpc: &Vpc,
131134
authz_vpc: &authz::Vpc,
132135
) -> DeleteResult {
133136
opctx.authorize(authz::Action::Delete, authz_vpc).await?;
134137

135138
use db::schema::vpc::dsl;
139+
use db::schema::vpc_subnet;
136140

137141
// Note that we don't ensure the firewall rules are empty here, because
138142
// we allow deleting VPCs with firewall rules present. Inserting new
139143
// rules is serialized with respect to the deletion by the row lock
140144
// associated with the VPC row, since we use the collection insert CTE
141145
// pattern to add firewall rules.
142146

147+
// We _do_ need to check for the existence of subnets. VPC Subnets
148+
// cannot be deleted while there are network interfaces in them
149+
// (associations between an instance and a VPC Subnet). Because VPC
150+
// Subnets are themselves containers for resources that we don't want to
151+
// auto-delete (now, anyway), we've got to check there aren't any. We
152+
// _might_ be able to make this a check for NICs, rather than subnets,
153+
// but we can't have NICs be a child of both tables at this point, and
154+
// we need to prevent VPC Subnets from being deleted while they have
155+
// NICs in them as well.
156+
println!("ABOUT TO SEARCH FOR SUBNETS");
157+
if diesel_pool_result_optional(
158+
vpc_subnet::dsl::vpc_subnet
159+
.filter(vpc_subnet::dsl::vpc_id.eq(authz_vpc.id()))
160+
.filter(vpc_subnet::dsl::time_deleted.is_null())
161+
.select(vpc_subnet::dsl::id)
162+
.limit(1)
163+
.first_async::<Uuid>(self.pool_authorized(opctx).await?)
164+
.await,
165+
)
166+
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))?
167+
.is_some()
168+
{
169+
println!("FOUND SOME SUBNETS");
170+
return Err(Error::InvalidRequest {
171+
message: String::from(
172+
"VPC cannot be deleted while VPC Subnets exist",
173+
),
174+
});
175+
}
176+
println!("FOUND NO SUBNETS");
177+
178+
// Delete the VPC, conditional on the subnet_gen not having changed.
143179
let now = Utc::now();
144-
diesel::update(dsl::vpc)
180+
println!("ABOUT TO UPDATE");
181+
let updated_rows = diesel::update(dsl::vpc)
145182
.filter(dsl::time_deleted.is_null())
146183
.filter(dsl::id.eq(authz_vpc.id()))
184+
.filter(dsl::subnet_gen.eq(db_vpc.subnet_gen))
147185
.set(dsl::time_deleted.eq(now))
148-
.returning(Vpc::as_returning())
149-
.get_result_async(self.pool_authorized(opctx).await?)
186+
.execute_async(self.pool_authorized(opctx).await?)
150187
.await
151188
.map_err(|e| {
189+
println!("FAILED TO UPDATE");
152190
public_error_from_diesel_pool(
153191
e,
154192
ErrorHandler::NotFoundByResource(authz_vpc),
155193
)
156194
})?;
157-
Ok(())
195+
println!("FINISHED UPDATE");
196+
if updated_rows == 0 {
197+
println!("NO ROWS UPDATED");
198+
Err(Error::InvalidRequest {
199+
message: String::from(
200+
"deletion failed to to concurrent modification",
201+
),
202+
})
203+
} else {
204+
println!("SOME ROWS UPDATED");
205+
Ok(())
206+
}
158207
}
159208

160209
pub async fn vpc_list_firewall_rules(
@@ -328,26 +377,59 @@ impl DataStore {
328377
pub async fn vpc_delete_subnet(
329378
&self,
330379
opctx: &OpContext,
380+
db_subnet: &VpcSubnet,
331381
authz_subnet: &authz::VpcSubnet,
332382
) -> DeleteResult {
333383
opctx.authorize(authz::Action::Delete, authz_subnet).await?;
334384

385+
use db::schema::network_interface;
335386
use db::schema::vpc_subnet::dsl;
387+
388+
// Verify there are no child network interfaces in this VPC Subnet
389+
if diesel_pool_result_optional(
390+
network_interface::dsl::network_interface
391+
.filter(network_interface::dsl::subnet_id.eq(authz_subnet.id()))
392+
.filter(network_interface::dsl::time_deleted.is_null())
393+
.select(network_interface::dsl::id)
394+
.limit(1)
395+
.first_async::<Uuid>(self.pool_authorized(opctx).await?)
396+
.await,
397+
)
398+
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))?
399+
.is_some()
400+
{
401+
return Err(Error::InvalidRequest {
402+
message: String::from(
403+
"VPC Subnet cannot be deleted while instances \
404+
with network interfaces in the subnet exist",
405+
),
406+
});
407+
}
408+
409+
// Delete the subnet, conditional on the rcgen not having changed.
336410
let now = Utc::now();
337-
diesel::update(dsl::vpc_subnet)
411+
let updated_rows = diesel::update(dsl::vpc_subnet)
338412
.filter(dsl::time_deleted.is_null())
339413
.filter(dsl::id.eq(authz_subnet.id()))
414+
.filter(dsl::rcgen.eq(db_subnet.rcgen))
340415
.set(dsl::time_deleted.eq(now))
341-
.returning(VpcSubnet::as_returning())
342-
.get_result_async(self.pool_authorized(opctx).await?)
416+
.execute_async(self.pool_authorized(opctx).await?)
343417
.await
344418
.map_err(|e| {
345419
public_error_from_diesel_pool(
346420
e,
347421
ErrorHandler::NotFoundByResource(authz_subnet),
348422
)
349423
})?;
350-
Ok(())
424+
if updated_rows == 0 {
425+
return Err(Error::InvalidRequest {
426+
message: String::from(
427+
"deletion failed to to concurrent modification",
428+
),
429+
});
430+
} else {
431+
Ok(())
432+
}
351433
}
352434

353435
pub async fn vpc_update_subnet(
@@ -463,8 +545,7 @@ impl DataStore {
463545
.filter(dsl::time_deleted.is_null())
464546
.filter(dsl::id.eq(authz_router.id()))
465547
.set(dsl::time_deleted.eq(now))
466-
.returning(VpcRouter::as_returning())
467-
.get_result_async(self.pool())
548+
.execute_async(self.pool())
468549
.await
469550
.map_err(|e| {
470551
public_error_from_diesel_pool(

nexus/src/db/model/vpc.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
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::db::collection_insert::DatastoreCollection;
77
use crate::db::model::Vni;
8-
use crate::db::schema::{vpc, vpc_firewall_rule};
8+
use crate::db::schema::{vpc, vpc_firewall_rule, vpc_subnet};
99
use crate::defaults;
1010
use crate::external_api::params;
1111
use chrono::{DateTime, Utc};
@@ -29,6 +29,9 @@ pub struct Vpc {
2929
/// firewall generation number, used as a child resource generation number
3030
/// per RFD 192
3131
pub firewall_gen: Generation,
32+
33+
/// VPC Subnet generation number
34+
pub subnet_gen: Generation,
3235
}
3336

3437
/// An `IncompleteVpc` is a candidate VPC, where some of the values may be
@@ -44,6 +47,7 @@ pub struct IncompleteVpc {
4447
pub ipv6_prefix: IpNetwork,
4548
pub dns_name: Name,
4649
pub firewall_gen: Generation,
50+
pub subnet_gen: Generation,
4751
}
4852

4953
impl IncompleteVpc {
@@ -78,6 +82,7 @@ impl IncompleteVpc {
7882
ipv6_prefix,
7983
dns_name: params.dns_name.into(),
8084
firewall_gen: Generation::new(),
85+
subnet_gen: Generation::new(),
8186
})
8287
}
8388
}
@@ -89,6 +94,13 @@ impl DatastoreCollection<VpcFirewallRule> for Vpc {
8994
type CollectionIdColumn = vpc_firewall_rule::dsl::vpc_id;
9095
}
9196

97+
impl DatastoreCollection<VpcSubnet> for Vpc {
98+
type CollectionId = Uuid;
99+
type GenerationNumberColumn = vpc::dsl::subnet_gen;
100+
type CollectionTimeDeletedColumn = vpc::dsl::time_deleted;
101+
type CollectionIdColumn = vpc_subnet::dsl::vpc_id;
102+
}
103+
92104
#[derive(AsChangeset)]
93105
#[diesel(table_name = vpc)]
94106
pub struct VpcUpdate {

0 commit comments

Comments
 (0)