|
7 | 7 | //! External subnets are similar to floating IPs but allocate entire subnets |
8 | 8 | //! rather than individual IP addresses. They can be attached to instances |
9 | 9 | //! to provide external connectivity. |
10 | | -//! |
11 | | -//! TODO(#9453): This module contains stub implementations that return |
12 | | -//! "not implemented" errors. Full implementation requires: |
13 | | -//! - Database schema and models (see nexus/db-model/) |
14 | | -//! - Datastore methods (see nexus/db-queries/src/db/datastore/) |
15 | | -//! - Authorization resources (see nexus/auth/src/authz/) |
16 | | -//! - Sagas for attach/detach operations |
17 | | -//! - Replacing these stubs with real implementations |
18 | 10 |
|
19 | | -use crate::app::Unimpl; |
| 11 | +use crate::app::sagas::subnet_attach; |
| 12 | +use crate::app::sagas::subnet_detach; |
| 13 | +use nexus_auth::authn; |
20 | 14 | use nexus_db_lookup::LookupPath; |
21 | 15 | use nexus_db_lookup::lookup; |
22 | 16 | use nexus_db_queries::authz; |
23 | 17 | use nexus_db_queries::context::OpContext; |
24 | 18 | use nexus_types::external_api::{params, views}; |
25 | 19 | use omicron_common::api::external::DeleteResult; |
26 | 20 | use omicron_common::api::external::Error; |
| 21 | +use omicron_common::api::external::InternalContext as _; |
27 | 22 | use omicron_common::api::external::ListResultVec; |
28 | 23 | use omicron_common::api::external::LookupResult; |
29 | | -use omicron_common::api::external::LookupType; |
30 | 24 | use omicron_common::api::external::NameOrId; |
31 | | -use omicron_common::api::external::ResourceType; |
32 | 25 | use omicron_common::api::external::UpdateResult; |
33 | 26 | use omicron_common::api::external::http_pagination::PaginatedBy; |
34 | 27 | use omicron_uuid_kinds::ExternalSubnetUuid; |
@@ -140,63 +133,86 @@ impl super::Nexus { |
140 | 133 | self.datastore().delete_external_subnet(opctx, &authz_subnet).await |
141 | 134 | } |
142 | 135 |
|
143 | | - // TODO-remove: This is a temporary method to ensure we continue to fail |
144 | | - // reliably for the methods below that remain unimplemented. |
145 | | - fn external_subnet_lookup_not_found( |
146 | | - &self, |
147 | | - selector: params::ExternalSubnetSelector, |
148 | | - ) -> LookupResult<()> { |
149 | | - let lookup_type = match selector { |
150 | | - params::ExternalSubnetSelector { |
151 | | - external_subnet: NameOrId::Id(id), |
152 | | - project: None, |
153 | | - } => LookupType::ById(id), |
154 | | - params::ExternalSubnetSelector { |
155 | | - external_subnet: NameOrId::Name(name), |
156 | | - project: Some(_), |
157 | | - } => LookupType::ByName(name.to_string()), |
158 | | - params::ExternalSubnetSelector { |
159 | | - external_subnet: NameOrId::Id(_), |
160 | | - .. |
161 | | - } => { |
162 | | - return Err(Error::invalid_request( |
163 | | - "when providing external subnet as an ID \ |
164 | | - project should not be specified", |
165 | | - )); |
166 | | - } |
167 | | - _ => { |
168 | | - return Err(Error::invalid_request( |
169 | | - "external subnet should either be a UUID or \ |
170 | | - project should be specified", |
171 | | - )); |
172 | | - } |
173 | | - }; |
174 | | - Err(lookup_type.into_not_found(ResourceType::ExternalSubnet)) |
175 | | - } |
176 | | - |
177 | 136 | pub(crate) async fn external_subnet_attach( |
178 | 137 | &self, |
179 | 138 | opctx: &OpContext, |
180 | 139 | selector: params::ExternalSubnetSelector, |
181 | | - _attach: params::ExternalSubnetAttach, |
| 140 | + attach: params::ExternalSubnetAttach, |
182 | 141 | ) -> UpdateResult<views::ExternalSubnet> { |
183 | | - let not_found = |
184 | | - self.external_subnet_lookup_not_found(selector).unwrap_err(); |
185 | | - Err(self |
186 | | - .unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found)) |
187 | | - .await) |
| 142 | + let (.., authz_project, authz_subnet, db_subnet) = self |
| 143 | + .external_subnet_lookup(opctx, selector)? |
| 144 | + .fetch_for(authz::Action::Modify) |
| 145 | + .await?; |
| 146 | + let instance_selector = match &attach.instance { |
| 147 | + NameOrId::Id(id) => params::InstanceSelector { |
| 148 | + project: None, |
| 149 | + instance: NameOrId::Id(*id), |
| 150 | + }, |
| 151 | + NameOrId::Name(name) => params::InstanceSelector { |
| 152 | + project: Some(NameOrId::Id(authz_project.id())), |
| 153 | + instance: NameOrId::Name(name.clone()), |
| 154 | + }, |
| 155 | + }; |
| 156 | + let (.., authz_instance, db_instance) = self |
| 157 | + .instance_lookup(opctx, instance_selector)? |
| 158 | + .fetch_for(authz::Action::Modify) |
| 159 | + .await?; |
| 160 | + if db_instance.project_id != db_subnet.project_id { |
| 161 | + return Err(Error::invalid_request( |
| 162 | + "External subnet and instance must be in the same project", |
| 163 | + )); |
| 164 | + } |
| 165 | + let params = subnet_attach::Params { |
| 166 | + authz_subnet, |
| 167 | + ip_version: db_subnet.subnet.ip_version(), |
| 168 | + authz_instance, |
| 169 | + serialized_authn: authn::saga::Serialized::for_opctx(opctx), |
| 170 | + }; |
| 171 | + let output = self |
| 172 | + .sagas |
| 173 | + .saga_execute::<subnet_attach::SagaSubnetAttach>(params) |
| 174 | + .await?; |
| 175 | + output |
| 176 | + .lookup_node_output::<views::ExternalSubnet>("output") |
| 177 | + .map_err(|e| Error::internal_error(&format!("{e:#}"))) |
| 178 | + .internal_context("looking up output from subnet attach saga") |
188 | 179 | } |
189 | 180 |
|
190 | 181 | pub(crate) async fn external_subnet_detach( |
191 | 182 | &self, |
192 | 183 | opctx: &OpContext, |
193 | 184 | selector: params::ExternalSubnetSelector, |
194 | 185 | ) -> UpdateResult<views::ExternalSubnet> { |
195 | | - let not_found = |
196 | | - self.external_subnet_lookup_not_found(selector).unwrap_err(); |
197 | | - Err(self |
198 | | - .unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found)) |
199 | | - .await) |
| 186 | + let (.., authz_subnet, db_subnet) = self |
| 187 | + .external_subnet_lookup(opctx, selector)? |
| 188 | + .fetch_for(authz::Action::Modify) |
| 189 | + .await?; |
| 190 | + let Some(instance_id) = &db_subnet.instance_id else { |
| 191 | + return Err(Error::invalid_request( |
| 192 | + "External subnet is not attached to an instance", |
| 193 | + )); |
| 194 | + }; |
| 195 | + let instance_selector = params::InstanceSelector { |
| 196 | + project: None, |
| 197 | + instance: NameOrId::Id(instance_id.into_untyped_uuid()), |
| 198 | + }; |
| 199 | + let (.., authz_instance, _db_instance) = self |
| 200 | + .instance_lookup(opctx, instance_selector)? |
| 201 | + .fetch_for(authz::Action::Modify) |
| 202 | + .await?; |
| 203 | + let params = subnet_detach::Params { |
| 204 | + authz_instance, |
| 205 | + authz_subnet, |
| 206 | + serialized_authn: authn::saga::Serialized::for_opctx(opctx), |
| 207 | + }; |
| 208 | + let output = self |
| 209 | + .sagas |
| 210 | + .saga_execute::<subnet_detach::SagaSubnetDetach>(params) |
| 211 | + .await?; |
| 212 | + output |
| 213 | + .lookup_node_output::<views::ExternalSubnet>("output") |
| 214 | + .map_err(|e| Error::internal_error(&format!("{e:#}"))) |
| 215 | + .internal_context("looking up output from subnet detach saga") |
200 | 216 | } |
201 | 217 |
|
202 | 218 | pub(crate) async fn instance_list_external_subnets( |
|
0 commit comments