inmanta-module-openstack 5.0.0__tar.gz → 5.0.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 (49) hide show
  1. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/PKG-INFO +1 -1
  2. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_module_openstack.egg-info/PKG-INFO +1 -1
  3. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/network/network.py +57 -2
  4. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/setup.cfg +1 -1
  5. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/setup.cfg +1 -1
  6. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/tests/test_neutron.py +133 -12
  7. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/MANIFEST.in +0 -0
  8. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/README.md +0 -0
  9. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_module_openstack.egg-info/SOURCES.txt +0 -0
  10. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_module_openstack.egg-info/dependency_links.txt +0 -0
  11. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_module_openstack.egg-info/not-zip-safe +0 -0
  12. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_module_openstack.egg-info/requires.txt +0 -0
  13. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_module_openstack.egg-info/top_level.txt +0 -0
  14. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/__init__.py +0 -0
  15. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/common/__init__.py +0 -0
  16. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/common/base.py +0 -0
  17. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/common/deps.py +0 -0
  18. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/compute/__init__.py +0 -0
  19. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/compute/flavor.py +0 -0
  20. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/compute/host_port.py +0 -0
  21. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/compute/virtual_machine.py +0 -0
  22. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/identity/__init__.py +0 -0
  23. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/identity/group.py +0 -0
  24. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/identity/group_role.py +0 -0
  25. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/identity/keystone_base.py +0 -0
  26. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/identity/project.py +0 -0
  27. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/identity/role.py +0 -0
  28. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/identity/user.py +0 -0
  29. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/image/__init__.py +0 -0
  30. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/image/image.py +0 -0
  31. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/model/_init.cf +0 -0
  32. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/network/__init__.py +0 -0
  33. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/network/floating_ip.py +0 -0
  34. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/network/qos_policy.py +0 -0
  35. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/network/router.py +0 -0
  36. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/network/router_port.py +0 -0
  37. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/network/security_group.py +0 -0
  38. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/network/subnet.py +0 -0
  39. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/network/subnet_v6.py +0 -0
  40. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/quota/__init__.py +0 -0
  41. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/inmanta_plugins/openstack/quota/quota.py +0 -0
  42. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/pyproject.toml +0 -0
  43. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/tests/test_dependency_handling.py +0 -0
  44. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/tests/test_examples.py +0 -0
  45. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/tests/test_flavor.py +0 -0
  46. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/tests/test_image.py +0 -0
  47. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/tests/test_keystone.py +0 -0
  48. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.1}/tests/test_nova.py +0 -0
  49. {inmanta_module_openstack-5.0.0 → inmanta_module_openstack-5.0.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: 5.0.0
3
+ Version: 5.0.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: 5.0.0
3
+ Version: 5.0.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 @@ limitations under the License.
16
16
  Contact: code@inmanta.com
17
17
  """
18
18
 
19
+ import time
19
20
  from typing import Any
20
21
 
21
22
  from inmanta.agent import handler
@@ -156,7 +157,7 @@ class NetworkHandler(OpenStackHandler):
156
157
  if "external" in changes:
157
158
  self.network.is_router_external = resource.external
158
159
  if "port_security_enabled" in changes:
159
- self.network.port_security_enabled = resource.port_security_enabled
160
+ self.network.is_port_security_enabled = resource.port_security_enabled
160
161
  if "mtu" in changes:
161
162
  self.network.mtu = resource.mtu
162
163
  if "segmentation_id" in changes:
@@ -172,10 +173,64 @@ class NetworkHandler(OpenStackHandler):
172
173
  else:
173
174
  self.network.qos_policy_id = None
174
175
 
175
- self.network = self._connection.network.update_network(self.network)
176
+ if "segmentation_id" in changes:
177
+ self._update_network_with_rebind(ctx)
178
+ else:
179
+ self.network = self._connection.network.update_network(self.network)
180
+
176
181
  ctx.fields_updated(tuple(changes.keys()))
177
182
  ctx.set_updated()
178
183
 
184
+ def _update_network_with_rebind(self, ctx: handler.HandlerContext) -> None:
185
+ """Update self.network after temporarily unbinding any bound ports.
186
+
187
+ Neutron ML2 rejects provider:segmentation_id updates when any port on
188
+ the network has a binding:vif_type other than 'unbound'/'binding_failed'.
189
+ Clearing binding:host_id forces ML2 to drop the binding; the agent
190
+ re-binds automatically once host_id is restored.
191
+
192
+ TODO: Persist original host_id in port metadata/tags before unbinding,
193
+ and recover possibly leaked bindings on next deploy (refer to #534).
194
+ """
195
+ saved_host_ids: dict[str, str] = {}
196
+ ports = list(self._connection.network.ports(network_id=self.network.id))
197
+ for port in ports:
198
+ if port.binding_vif_type not in ("unbound", "binding_failed"):
199
+ saved_host_ids[port.id] = port.binding_host_id or ""
200
+ self._connection.network.update_port(port.id, binding_host_id="")
201
+ ctx.debug(
202
+ "Cleared binding:host_id on port %(port_id)s "
203
+ "(was %(host)r) to allow segmentation_id update",
204
+ port_id=port.id,
205
+ host=saved_host_ids[port.id],
206
+ )
207
+
208
+ try:
209
+ if saved_host_ids:
210
+ deadline = time.monotonic() + 60
211
+ pending = set(saved_host_ids)
212
+ while pending and time.monotonic() < deadline:
213
+ time.sleep(1)
214
+ for port_id in list(pending):
215
+ port = self._connection.network.get_port(port_id)
216
+ if port.binding_vif_type in ("unbound", "binding_failed"):
217
+ pending.discard(port_id)
218
+ if pending:
219
+ raise TimeoutError(
220
+ f"Timed out waiting for ports to unbind before "
221
+ f"segmentation_id update. Ports still bound: {list(pending)}"
222
+ )
223
+
224
+ self.network = self._connection.network.update_network(self.network)
225
+ finally:
226
+ for port_id, host_id in saved_host_ids.items():
227
+ self._connection.network.update_port(port_id, binding_host_id=host_id)
228
+ ctx.debug(
229
+ "Restored binding:host_id on port %(port_id)s to %(host)r",
230
+ port_id=port_id,
231
+ host=host_id,
232
+ )
233
+
179
234
  def _set_facts(self, ctx):
180
235
  ctx.set_fact("network_id", self.network.id, expires=False)
181
236
  ctx.set_fact("status", self.network.status, expires=True)
@@ -22,7 +22,7 @@ target-version = 'py36', 'py37', 'py38'
22
22
  name = inmanta-module-openstack
23
23
  freeze_recursive = False
24
24
  freeze_operator = ~=
25
- version = 5.0.0
25
+ version = 5.0.1
26
26
  license = Apache 2.0
27
27
 
28
28
  [egg_info]
@@ -22,7 +22,7 @@ target-version = 'py36', 'py37', 'py38'
22
22
  name = inmanta-module-openstack
23
23
  freeze_recursive = False
24
24
  freeze_operator = ~=
25
- version = 5.0.0
25
+ version = 5.0.1
26
26
  license = Apache 2.0
27
27
 
28
28
  [egg_info]
@@ -25,6 +25,58 @@ import pytest
25
25
  from conftest import assert_dryrun_deploy_dryrun
26
26
 
27
27
 
28
+ def test_network_update(project, os_tester):
29
+
30
+ def get_model(port_security_enabled: bool):
31
+ return f"""
32
+ import unittest
33
+ project = openstack::Project(
34
+ provider=provider,
35
+ name=provider.project_name,
36
+ description="",
37
+ enabled=true,
38
+ managed=false
39
+ )
40
+ openstack::Network(
41
+ provider=provider,
42
+ name="test_net",
43
+ project=project,
44
+ external=true,
45
+ mtu=1000,
46
+ port_security_enabled={str(port_security_enabled).lower()},
47
+ )
48
+ """
49
+
50
+ network_name = "test_net"
51
+ try:
52
+ # Create network
53
+ os_tester.compile_with_provider(get_model(port_security_enabled=True))
54
+
55
+ assert project.dryrun_resource("openstack::Network", name=network_name)
56
+ project.deploy_resource("openstack::Network", name=network_name)
57
+ assert not project.dryrun_resource("openstack::Network", name=network_name)
58
+
59
+ networks = list(os_tester.connection.network.networks(name=network_name))
60
+ assert len(networks) == 1
61
+ assert networks[0].is_port_security_enabled
62
+
63
+ # Update network
64
+ os_tester.compile_with_provider(get_model(port_security_enabled=False))
65
+
66
+ assert project.dryrun_resource("openstack::Network", name=network_name)
67
+ project.deploy_resource("openstack::Network", name=network_name)
68
+ assert not project.dryrun_resource("openstack::Network", name=network_name)
69
+
70
+ networks = list(os_tester.connection.network.networks(name=network_name))
71
+ assert len(networks) == 1
72
+ assert not networks[0].is_port_security_enabled
73
+ finally:
74
+ # cleanup
75
+ networks = os_tester.connection.network.networks(name=network_name)
76
+ for network in networks:
77
+ os_tester.connection.network.delete_network(network)
78
+
79
+
28
80
  def test_net(project, os_tester):
29
81
  try:
30
82
  os_tester.compile_with_provider("""
@@ -1316,19 +1368,28 @@ def test_qos_policy(project, os_tester, require_network_extension):
1316
1368
  @pytest.fixture
1317
1369
  def segmentation_id_network(os_tester):
1318
1370
  net_name = "test_update_seg_id"
1319
- yield net_name
1320
- networks = os_tester.connection.network.networks(name=net_name)
1321
- for network in networks:
1322
- os_tester.connection.network.delete_network(network)
1371
+ subnet_name = "test_update_seg_id_subnet"
1372
+ yield net_name, subnet_name
1373
+ conn = os_tester.connection
1374
+ for subnet in conn.network.subnets(name=subnet_name):
1375
+ conn.network.delete_subnet(subnet, ignore_missing=True)
1376
+ for network in conn.network.networks(name=net_name):
1377
+ conn.network.delete_network(network, ignore_missing=True)
1323
1378
 
1324
1379
 
1325
1380
  def test_update_segmentation_id(project, os_tester, segmentation_id_network):
1326
1381
  """
1327
- Test that segmentation_id can be updated on an existing network.
1382
+ Test that segmentation_id can be updated on an existing network that has a
1383
+ DHCP-enabled subnet. When the DHCP agent has bound a port, the handler must
1384
+ temporarily clear binding:host_id on those ports so ML2 accepts the update,
1385
+ then restore it so the agent re-binds on the new VLAN.
1328
1386
  """
1329
- net_name = segmentation_id_network
1387
+ import time as _time
1388
+
1389
+ net_name, subnet_name = segmentation_id_network
1390
+ conn = os_tester.connection
1330
1391
 
1331
- # Create network with initial segmentation_id
1392
+ # Create network + subnet with initial segmentation_id.
1332
1393
  os_tester.compile_with_provider(f"""
1333
1394
  import unittest
1334
1395
  project = openstack::Project(provider=provider, name=provider.project_name, description="", enabled=true, managed=false)
@@ -1339,16 +1400,51 @@ def test_update_segmentation_id(project, os_tester, segmentation_id_network):
1339
1400
  network_type="vlan",
1340
1401
  physical_network="physnet1",
1341
1402
  segmentation_id=742,
1403
+ )
1404
+ subnet = openstack::Subnet(
1405
+ provider=provider,
1406
+ project=project,
1407
+ network=n,
1408
+ dhcp=true,
1409
+ name="{subnet_name}",
1410
+ network_address="192.168.242.0/24",
1342
1411
  )
1343
1412
  """)
1344
1413
 
1345
1414
  assert_dryrun_deploy_dryrun(project, "openstack::Network", name=net_name)
1415
+ assert_dryrun_deploy_dryrun(project, "openstack::Subnet", name=subnet_name)
1346
1416
 
1347
- networks = list(os_tester.connection.network.networks(name=net_name))
1417
+ networks = list(conn.network.networks(name=net_name))
1348
1418
  assert len(networks) == 1
1419
+ network_id = networks[0].id
1349
1420
  assert networks[0].provider_segmentation_id == 742
1350
1421
 
1351
- # Update segmentation_id to a new value
1422
+ # Wait up to 5 minutes for the DHCP agent to fully bind a port: vif_type must be
1423
+ # neither 'unbound' nor 'binding_failed', AND binding_host_id must be set.
1424
+ # Both conditions are required to exercise the handler's ML2 rebind path.
1425
+ # Agent binding can take on the order of minutes depending on the environment.
1426
+ bound_ports_before: dict[str, str] = {}
1427
+ deadline = _time.monotonic() + 300
1428
+ while _time.monotonic() < deadline:
1429
+ for port in conn.network.ports(
1430
+ network_id=network_id, device_owner="network:dhcp"
1431
+ ):
1432
+ if (
1433
+ port.binding_vif_type not in ("unbound", "binding_failed")
1434
+ and port.binding_host_id
1435
+ ):
1436
+ bound_ports_before[port.id] = port.binding_host_id
1437
+ if bound_ports_before:
1438
+ break
1439
+ _time.sleep(5)
1440
+
1441
+ assert bound_ports_before, (
1442
+ "No fully-bound DHCP port (vif_type != unbound/binding_failed AND "
1443
+ "binding_host_id set) appeared on the network within 5 minutes. "
1444
+ "Cannot verify the ML2 rebind path."
1445
+ )
1446
+
1447
+ # Update segmentation_id — handler must unbind, update, and rebind.
1352
1448
  os_tester.compile_with_provider(f"""
1353
1449
  import unittest
1354
1450
  project = openstack::Project(provider=provider, name=provider.project_name, description="", enabled=true, managed=false)
@@ -1359,15 +1455,31 @@ def test_update_segmentation_id(project, os_tester, segmentation_id_network):
1359
1455
  network_type="vlan",
1360
1456
  physical_network="physnet1",
1361
1457
  segmentation_id=3817,
1458
+ )
1459
+ subnet = openstack::Subnet(
1460
+ provider=provider,
1461
+ project=project,
1462
+ network=n,
1463
+ dhcp=true,
1464
+ name="{subnet_name}",
1465
+ network_address="192.168.242.0/24",
1362
1466
  )
1363
1467
  """)
1364
1468
 
1365
1469
  assert_dryrun_deploy_dryrun(project, "openstack::Network", name=net_name)
1366
1470
 
1367
- networks = list(os_tester.connection.network.networks(name=net_name))
1471
+ networks = list(conn.network.networks(name=net_name))
1368
1472
  assert len(networks) == 1
1369
1473
  assert networks[0].provider_segmentation_id == 3817
1370
1474
 
1475
+ # Verify binding:host_id was restored on every port that was bound before.
1476
+ for port_id, expected_host in bound_ports_before.items():
1477
+ port = conn.network.get_port(port_id)
1478
+ assert port.binding_host_id == expected_host, (
1479
+ f"Port {port_id} binding_host_id was not restored: "
1480
+ f"expected {expected_host!r}, got {port.binding_host_id!r}"
1481
+ )
1482
+
1371
1483
  # Cleanup
1372
1484
  os_tester.compile_with_provider(f"""
1373
1485
  import unittest
@@ -1377,10 +1489,19 @@ def test_update_segmentation_id(project, os_tester, segmentation_id_network):
1377
1489
  name="{net_name}",
1378
1490
  project=project,
1379
1491
  purged=true,
1492
+ )
1493
+ subnet = openstack::Subnet(
1494
+ provider=provider,
1495
+ project=project,
1496
+ network=n,
1497
+ dhcp=true,
1498
+ name="{subnet_name}",
1499
+ network_address="192.168.242.0/24",
1500
+ purged=true,
1380
1501
  )
1381
1502
  """)
1382
1503
 
1504
+ assert_dryrun_deploy_dryrun(project, "openstack::Subnet", name=subnet_name)
1383
1505
  assert_dryrun_deploy_dryrun(project, "openstack::Network", name=net_name)
1384
1506
 
1385
- networks = list(os_tester.connection.network.networks(name=net_name))
1386
- assert len(networks) == 0
1507
+ assert len(list(conn.network.networks(name=net_name))) == 0