inmanta-module-openstack 6.0.0__tar.gz → 6.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/PKG-INFO +1 -1
  2. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/PKG-INFO +1 -1
  3. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/SOURCES.txt +8 -1
  4. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/common/base.py +86 -15
  5. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/common/deps.py +60 -11
  6. inmanta_module_openstack-6.1.1/inmanta_plugins/openstack/compute/flavor.py +364 -0
  7. inmanta_module_openstack-6.1.1/inmanta_plugins/openstack/compute/server_group.py +87 -0
  8. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/compute/virtual_machine.py +97 -26
  9. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/model/_init.cf +121 -6
  10. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/network_port.py +138 -39
  11. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/security_group.py +38 -7
  12. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/setup.cfg +1 -1
  13. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/setup.cfg +1 -1
  14. inmanta_module_openstack-6.1.1/tests/test_common_base.py +60 -0
  15. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_dependency_handling.py +64 -0
  16. inmanta_module_openstack-6.1.1/tests/test_flavor.py +659 -0
  17. inmanta_module_openstack-6.1.1/tests/test_network_port_missing_network.py +177 -0
  18. inmanta_module_openstack-6.1.1/tests/test_network_port_shared_network.py +264 -0
  19. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_neutron.py +17 -7
  20. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_nova.py +25 -0
  21. inmanta_module_openstack-6.1.1/tests/test_security_group_ethertype.py +157 -0
  22. inmanta_module_openstack-6.1.1/tests/test_server_group.py +398 -0
  23. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_trunk.py +2 -1
  24. inmanta_module_openstack-6.1.1/tests/test_vm_boot_nic.py +322 -0
  25. inmanta_module_openstack-6.0.0/inmanta_plugins/openstack/compute/flavor.py +0 -181
  26. inmanta_module_openstack-6.0.0/tests/test_flavor.py +0 -227
  27. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/MANIFEST.in +0 -0
  28. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/README.md +0 -0
  29. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/dependency_links.txt +0 -0
  30. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/not-zip-safe +0 -0
  31. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/requires.txt +0 -0
  32. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/top_level.txt +0 -0
  33. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/__init__.py +0 -0
  34. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/common/__init__.py +0 -0
  35. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/compute/__init__.py +0 -0
  36. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/compute/host_aggregate.py +0 -0
  37. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/__init__.py +0 -0
  38. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/group.py +0 -0
  39. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/group_role.py +0 -0
  40. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/keystone_base.py +0 -0
  41. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/project.py +0 -0
  42. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/role.py +0 -0
  43. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/user.py +0 -0
  44. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/image/__init__.py +0 -0
  45. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/image/image.py +0 -0
  46. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/__init__.py +0 -0
  47. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/floating_ip.py +0 -0
  48. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/network.py +0 -0
  49. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/qos_policy.py +0 -0
  50. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/router.py +0 -0
  51. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/router_port.py +0 -0
  52. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/subnet.py +0 -0
  53. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/subnet_v6.py +0 -0
  54. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/trunk_network.py +0 -0
  55. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/trunk_port.py +0 -0
  56. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/quota/__init__.py +0 -0
  57. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/quota/quota.py +0 -0
  58. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/pyproject.toml +0 -0
  59. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_block_devices.py +0 -0
  60. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_examples.py +0 -0
  61. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_host_aggregate.py +0 -0
  62. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_image.py +0 -0
  63. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_keystone.py +0 -0
  64. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_network_port_fixed_ips.py +0 -0
  65. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_network_port_security_groups.py +0 -0
  66. {inmanta_module_openstack-6.0.0 → inmanta_module_openstack-6.1.1}/tests/test_quota.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inmanta-module-openstack
3
- Version: 6.0.0
3
+ Version: 6.1.1
4
4
  License: Apache 2.0
5
5
  Requires-Dist: inmanta-module-ssh
6
6
  Requires-Dist: inmanta-module-std
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inmanta-module-openstack
3
- Version: 6.0.0
3
+ Version: 6.1.1
4
4
  License: Apache 2.0
5
5
  Requires-Dist: inmanta-module-ssh
6
6
  Requires-Dist: inmanta-module-std
@@ -16,6 +16,7 @@ inmanta_plugins/openstack/common/deps.py
16
16
  inmanta_plugins/openstack/compute/__init__.py
17
17
  inmanta_plugins/openstack/compute/flavor.py
18
18
  inmanta_plugins/openstack/compute/host_aggregate.py
19
+ inmanta_plugins/openstack/compute/server_group.py
19
20
  inmanta_plugins/openstack/compute/virtual_machine.py
20
21
  inmanta_plugins/openstack/identity/__init__.py
21
22
  inmanta_plugins/openstack/identity/group.py
@@ -42,6 +43,7 @@ inmanta_plugins/openstack/network/trunk_port.py
42
43
  inmanta_plugins/openstack/quota/__init__.py
43
44
  inmanta_plugins/openstack/quota/quota.py
44
45
  tests/test_block_devices.py
46
+ tests/test_common_base.py
45
47
  tests/test_dependency_handling.py
46
48
  tests/test_examples.py
47
49
  tests/test_flavor.py
@@ -49,8 +51,13 @@ tests/test_host_aggregate.py
49
51
  tests/test_image.py
50
52
  tests/test_keystone.py
51
53
  tests/test_network_port_fixed_ips.py
54
+ tests/test_network_port_missing_network.py
52
55
  tests/test_network_port_security_groups.py
56
+ tests/test_network_port_shared_network.py
53
57
  tests/test_neutron.py
54
58
  tests/test_nova.py
55
59
  tests/test_quota.py
56
- tests/test_trunk.py
60
+ tests/test_security_group_ethertype.py
61
+ tests/test_server_group.py
62
+ tests/test_trunk.py
63
+ tests/test_vm_boot_nic.py
@@ -18,9 +18,11 @@ Contact: code@inmanta.com
18
18
 
19
19
  import openstack.connection
20
20
  import openstack.exceptions
21
- from inmanta.agent.handler import CRUDHandler, cache
21
+ from inmanta.agent.handler import CRUDHandler, SkipResource, cache
22
22
  from inmanta.execute import proxy
23
23
  from inmanta.resources import ManagedResource, PurgeableResource
24
+ from openstack.network.v2.network import Network as OSNetwork
25
+ from openstack.network.v2.subnet import Subnet as OSSubnet
24
26
 
25
27
  CRED_TIMEOUT = 600 # OpenStackHandler
26
28
 
@@ -94,8 +96,12 @@ class Port(OpenstackResource):
94
96
 
95
97
  fields = ("name", "address", "subnet", "network")
96
98
  name: str
97
- address: str
98
- subnet: str
99
+ # A NetworkPort does not need a fixed ip, nor to be pinned to a subnet: its getters
100
+ # report None for both when the relation is unset. RouterPort re-declares them as
101
+ # mandatory, which its model cardinality guarantees. The network is always there,
102
+ # taken from the relation itself or from the subnet the port is pinned to.
103
+ address: str | None
104
+ subnet: str | None
99
105
  network: str
100
106
 
101
107
  @staticmethod
@@ -169,15 +175,19 @@ class OpenStackHandler(CRUDHandler):
169
175
  """
170
176
  Retrieve the id of a project based on the given name
171
177
  """
178
+ # A project-scoped token already knows its own project id. Use it before
179
+ # querying Keystone: non-admin users may be forbidden to get/list projects.
180
+ current_project_id = self._connection.current_project_id
181
+ if name == resource.admin_project_name and current_project_id:
182
+ return current_project_id
183
+
172
184
  project = self._connection.identity.find_project(name)
173
185
  if project:
174
186
  return project.id
175
187
 
176
188
  # fallback for non-admins: if we’re scoped to a project and its name matches
177
- if self._connection.current_project_id:
178
- current = self._connection.identity.get_project(
179
- self._connection.current_project_id
180
- )
189
+ if current_project_id:
190
+ current = self._connection.identity.get_project(current_project_id)
181
191
  if current and current.name == name:
182
192
  return current.id
183
193
 
@@ -185,14 +195,30 @@ class OpenStackHandler(CRUDHandler):
185
195
  f"Project not found or not visible: {name}"
186
196
  )
187
197
 
188
- def get_network(self, project_id=None, name=None, network_id=None):
198
+ def get_network(
199
+ self,
200
+ project_id: str | None = None,
201
+ name: str | None = None,
202
+ network_id: str | None = None,
203
+ include_shared: bool = False,
204
+ include_external: bool = False,
205
+ ) -> OSNetwork | None:
189
206
  """
190
207
  Retrieve the network id based on the name of the network
208
+
209
+ :param include_shared: also consider networks owned by another project when the
210
+ project scoped lookup comes up empty, restricted to shared networks. A shared
211
+ network is owned by the project that created it, so a lookup scoped to a
212
+ project merely using it never finds it.
213
+ :param include_external: likewise for external networks, which another project
214
+ owns as well: the management network of an appliance typically is one. Note
215
+ that every project can read an external network, but only an admin can create
216
+ a port in its own project on one, unless it is shared as well.
191
217
  """
192
218
  if not name and not network_id:
193
219
  raise Exception("Either a name or an id needs to be provided.")
194
220
 
195
- query_params = {}
221
+ query_params: dict[str, object] = {}
196
222
  if project_id:
197
223
  query_params["project_id"] = project_id
198
224
  if name:
@@ -201,26 +227,70 @@ class OpenStackHandler(CRUDHandler):
201
227
  query_params["id"] = network_id
202
228
 
203
229
  networks = self._connection.network.networks(**query_params)
204
- return self.require_single_item(networks)
205
-
206
- def get_subnet(self, project_id=None, name=None, subnet_id=None):
230
+ network = self.require_single_item(networks)
231
+
232
+ if network is None and project_id and (include_shared or include_external):
233
+ del query_params["project_id"]
234
+ if include_shared:
235
+ networks = self._connection.network.networks(
236
+ **query_params, is_shared=True
237
+ )
238
+ network = self.require_single_item(networks)
239
+ if network is None and include_external:
240
+ networks = self._connection.network.networks(
241
+ **query_params, is_router_external=True
242
+ )
243
+ network = self.require_single_item(networks)
244
+
245
+ return network
246
+
247
+ def get_subnet(
248
+ self,
249
+ project_id: str | None = None,
250
+ name: str | None = None,
251
+ subnet_id: str | None = None,
252
+ network_id: str | None = None,
253
+ ) -> OSSubnet | None:
207
254
  """
208
255
  Retrieve the subnet based on name or id
209
256
  """
210
257
  if not name and not subnet_id:
211
258
  raise Exception("Either a name or an id needs to be provided.")
212
259
 
213
- query_params = {}
260
+ query_params: dict[str, object] = {}
214
261
  if project_id:
215
262
  query_params["project_id"] = project_id
216
263
  if name:
217
264
  query_params["name"] = name
218
265
  if subnet_id:
219
266
  query_params["id"] = subnet_id
267
+ if network_id:
268
+ query_params["network_id"] = network_id
220
269
 
221
270
  subnets = self._connection.network.subnets(**query_params)
222
271
  return self.require_single_item(subnets)
223
272
 
273
+ def get_server_group(self, name: str):
274
+ """
275
+ Retrieve a Nova server group by name.
276
+
277
+ Nova does not filter server groups by name server-side, so list the groups in the
278
+ connection's project scope and match on the name ourselves. Nova does allow
279
+ several groups to share a name, which cannot be managed unambiguously: skip
280
+ rather than pick one.
281
+ """
282
+ groups = [
283
+ group
284
+ for group in self._connection.compute.server_groups()
285
+ if group.name == name
286
+ ]
287
+ if len(groups) > 1:
288
+ raise SkipResource(
289
+ f"Found more than one server group named '{name}'; refusing "
290
+ "to manage a non-unique server group."
291
+ )
292
+ return groups[0] if groups else None
293
+
224
294
  def get_router(self, project_id=None, name=None, router_id=None):
225
295
  """
226
296
  Retrieve the router based on name or id
@@ -276,14 +346,15 @@ class OpenStackHandler(CRUDHandler):
276
346
  return None
277
347
  return self._connection.identity.find_domain(domain_name)
278
348
 
279
- def get_server_by_name(self, name, project_id=None):
349
+ def get_server_by_name(self, name, project_id=None, all_projects=False):
280
350
  """
281
351
  Retrieve a server by name
282
352
  """
283
353
  additional_args = {}
284
354
  if project_id is not None:
285
- additional_args["all_projects"] = True
286
355
  additional_args["project_id"] = project_id
356
+ if all_projects:
357
+ additional_args["all_projects"] = True
287
358
  # regex for exact match
288
359
  servers = self._connection.compute.servers(name=f"^{name}$", **additional_args)
289
360
 
@@ -18,11 +18,17 @@ Contact: code@inmanta.com
18
18
 
19
19
  from collections import defaultdict
20
20
  from collections.abc import Mapping
21
+ from typing import TYPE_CHECKING, cast
21
22
 
22
23
  from inmanta.export import dependency_manager
23
24
 
24
25
  from inmanta_plugins.openstack.common.base import OpenstackResource
25
26
 
27
+ if TYPE_CHECKING:
28
+ # Only a name for the cast below: this export-time module does not depend on a
29
+ # handler module at runtime.
30
+ from inmanta_plugins.openstack.network.network_port import NetworkPort
31
+
26
32
  # Per-provider view of the collected resources: resource type -> resource name -> resource.
27
33
  ResourceCollector = Mapping[str, Mapping[str, OpenstackResource]]
28
34
 
@@ -35,6 +41,7 @@ MANAGED_DEPENDENCIES = {
35
41
  "openstack::NetworkPort",
36
42
  "openstack::FloatingIP",
37
43
  "openstack::SecurityGroup",
44
+ "openstack::ServerGroup",
38
45
  }
39
46
 
40
47
 
@@ -68,9 +75,14 @@ def set_dependencies_on_purged_resources(collector: ResourceCollector) -> None:
68
75
  vms = collector.get("openstack::VirtualMachine", {})
69
76
  ports = collector.get("openstack::NetworkPort", {})
70
77
  sgs = collector.get("openstack::SecurityGroup", {})
78
+ server_groups = collector.get("openstack::ServerGroup", {})
71
79
 
72
80
  # When the VM is purged, all its purged ports should be deployed first
73
81
  for vm in vms.values():
82
+ # A purged server group is removed only after the VMs scheduled into it are gone.
83
+ if vm.server_group in server_groups:
84
+ server_groups[vm.server_group].requires.add(vm.id)
85
+
74
86
  for vm_port in vm.ports:
75
87
  if vm_port["name"] in ports:
76
88
  port = ports[vm_port["name"]]
@@ -99,6 +111,7 @@ def set_dependencies_on_non_purged_resources(collector: ResourceCollector) -> No
99
111
  ports = collector.get("openstack::NetworkPort", {})
100
112
  fips = collector.get("openstack::FloatingIP", {})
101
113
  sgs = collector.get("openstack::SecurityGroup", {})
114
+ server_groups = collector.get("openstack::ServerGroup", {})
102
115
  router_map = {}
103
116
 
104
117
  # they require the project to exist
@@ -133,22 +146,40 @@ def set_dependencies_on_non_purged_resources(collector: ResourceCollector) -> No
133
146
  if vm.model.project.name in projects:
134
147
  vm.requires.add(projects[vm.model.project.name].id)
135
148
 
149
+ # The vm resolves each boot NIC through the port's subnet, or through its network
150
+ # when the port is not pinned to a subnet. Either way that object has to exist
151
+ # before the vm boots.
136
152
  for port in vm.ports:
137
- if port["network"] in subnets:
138
- vm.requires.add(subnets[port["network"]].id)
153
+ if port["subnet"] in subnets:
154
+ vm.requires.add(subnets[port["subnet"]].id)
155
+ elif port["network"] in networks:
156
+ vm.requires.add(networks[port["network"]].id)
139
157
 
140
158
  for sg in vm.security_groups:
141
159
  if sg in sgs:
142
160
  vm.requires.add(sgs[sg].id)
143
161
 
144
- for port in ports.values():
162
+ # The scheduler hint is resolved at boot, so the group must exist by then.
163
+ if vm.server_group in server_groups:
164
+ vm.requires.add(server_groups[vm.server_group].id)
165
+
166
+ for collected_port in ports.values():
167
+ # The collector hands out the base type; every entry here is a NetworkPort.
168
+ port = cast("NetworkPort", collected_port)
169
+
145
170
  if port.model.project.name in projects:
146
171
  port.requires.add(projects[port.model.project.name].id)
147
172
 
148
- if port.network in projects:
149
- port.requires.add(subnets[port.network].id)
173
+ # The port lives on a network, and is pinned to one of its subnets when it
174
+ # requests a fixed ip. The network has to exist before the port is created at
175
+ # all; the subnet has to exist before the port can pin a fixed ip in it.
176
+ if port.network in networks:
177
+ port.requires.add(networks[port.network].id)
178
+
179
+ if port.subnet is not None and port.subnet in subnets:
180
+ port.requires.add(subnets[port.subnet].id)
150
181
 
151
- if port.host in vms:
182
+ if port.host is not None and port.host in vms:
152
183
  if port.is_trunk_parent:
153
184
  # A trunk parent must exist (unbound) before its vm: the trunk is built
154
185
  # on it and the vm then boots on the pre-created port. Ordering the port
@@ -167,13 +198,31 @@ def set_dependencies_on_non_purged_resources(collector: ResourceCollector) -> No
167
198
  fip.requires.add(networks[fip.external_network].id)
168
199
 
169
200
  if fip.port in ports:
170
- port = ports[fip.port]
201
+ port = cast("NetworkPort", ports[fip.port])
171
202
  fip.requires.add(port.id)
172
203
 
173
- # find router on which this floating ip is added
174
- key = (fip.external_network, port.subnet)
175
- if key in router_map:
176
- fip.requires.add(router_map[key].id)
204
+ # Find the router this floating ip is routed over: the one with a gateway on
205
+ # the external network and an interface on the port's network. A port pinned
206
+ # to a subnet names that subnet; a port that only knows its network is
207
+ # matched through every managed subnet of that network instead, as the
208
+ # router is attached to the network by one of them.
209
+ if port.subnet is not None:
210
+ port_subnets = [port.subnet]
211
+ else:
212
+ # Matching networks by name is enough even though the collector spans
213
+ # every project of the provider: a network resource is identified by
214
+ # (provider, name), so two projects of one provider cannot both hold a
215
+ # network under this name.
216
+ port_subnets = [
217
+ subnet_name
218
+ for subnet_name, subnet in subnets.items()
219
+ if subnet.model.network.name == port.network
220
+ ]
221
+
222
+ for subnet_name in port_subnets:
223
+ key = (fip.external_network, subnet_name)
224
+ if key in router_map:
225
+ fip.requires.add(router_map[key].id)
177
226
 
178
227
 
179
228
  @dependency_manager