inmanta-module-openstack 5.1.0__tar.gz → 5.1.2__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 (60) hide show
  1. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/PKG-INFO +1 -1
  2. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_module_openstack.egg-info/PKG-INFO +1 -1
  3. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_module_openstack.egg-info/SOURCES.txt +2 -2
  4. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/common/deps.py +19 -6
  5. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/compute/virtual_machine.py +164 -30
  6. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/model/_init.cf +78 -60
  7. inmanta_module_openstack-5.1.2/inmanta_plugins/openstack/network/network_port.py +504 -0
  8. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/trunk_network.py +2 -2
  9. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/setup.cfg +1 -1
  10. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/setup.cfg +1 -1
  11. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/tests/test_dependency_handling.py +6 -6
  12. inmanta_module_openstack-5.1.2/tests/test_network_port_fixed_ips.py +347 -0
  13. inmanta_module_openstack-5.1.0/tests/test_host_port_security_groups.py → inmanta_module_openstack-5.1.2/tests/test_network_port_security_groups.py +8 -8
  14. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/tests/test_neutron.py +28 -26
  15. inmanta_module_openstack-5.1.2/tests/test_nova.py +570 -0
  16. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/tests/test_trunk.py +72 -0
  17. inmanta_module_openstack-5.1.0/inmanta_plugins/openstack/compute/host_port.py +0 -330
  18. inmanta_module_openstack-5.1.0/inmanta_plugins/openstack/network/network_port.py +0 -163
  19. inmanta_module_openstack-5.1.0/tests/test_nova.py +0 -287
  20. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/MANIFEST.in +0 -0
  21. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/README.md +0 -0
  22. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_module_openstack.egg-info/dependency_links.txt +0 -0
  23. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_module_openstack.egg-info/not-zip-safe +0 -0
  24. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_module_openstack.egg-info/requires.txt +0 -0
  25. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_module_openstack.egg-info/top_level.txt +0 -0
  26. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/__init__.py +0 -0
  27. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/common/__init__.py +0 -0
  28. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/common/base.py +0 -0
  29. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/compute/__init__.py +0 -0
  30. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/compute/flavor.py +0 -0
  31. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/compute/host_aggregate.py +0 -0
  32. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/identity/__init__.py +0 -0
  33. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/identity/group.py +0 -0
  34. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/identity/group_role.py +0 -0
  35. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/identity/keystone_base.py +0 -0
  36. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/identity/project.py +0 -0
  37. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/identity/role.py +0 -0
  38. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/identity/user.py +0 -0
  39. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/image/__init__.py +0 -0
  40. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/image/image.py +0 -0
  41. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/__init__.py +0 -0
  42. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/floating_ip.py +0 -0
  43. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/network.py +0 -0
  44. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/qos_policy.py +0 -0
  45. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/router.py +0 -0
  46. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/router_port.py +0 -0
  47. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/security_group.py +0 -0
  48. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/subnet.py +0 -0
  49. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/subnet_v6.py +0 -0
  50. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/network/trunk_port.py +0 -0
  51. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/quota/__init__.py +0 -0
  52. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/inmanta_plugins/openstack/quota/quota.py +0 -0
  53. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/pyproject.toml +0 -0
  54. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/tests/test_block_devices.py +0 -0
  55. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/tests/test_examples.py +0 -0
  56. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/tests/test_flavor.py +0 -0
  57. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/tests/test_host_aggregate.py +0 -0
  58. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/tests/test_image.py +0 -0
  59. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/tests/test_keystone.py +0 -0
  60. {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.2}/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.1.0
3
+ Version: 5.1.2
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.1.0
3
+ Version: 5.1.2
4
4
  License: Apache 2.0
5
5
  Requires-Dist: inmanta-module-ssh
6
6
  Requires-Dist: inmanta-module-std
@@ -16,7 +16,6 @@ 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/host_port.py
20
19
  inmanta_plugins/openstack/compute/virtual_machine.py
21
20
  inmanta_plugins/openstack/identity/__init__.py
22
21
  inmanta_plugins/openstack/identity/group.py
@@ -47,9 +46,10 @@ tests/test_dependency_handling.py
47
46
  tests/test_examples.py
48
47
  tests/test_flavor.py
49
48
  tests/test_host_aggregate.py
50
- tests/test_host_port_security_groups.py
51
49
  tests/test_image.py
52
50
  tests/test_keystone.py
51
+ tests/test_network_port_fixed_ips.py
52
+ tests/test_network_port_security_groups.py
53
53
  tests/test_neutron.py
54
54
  tests/test_nova.py
55
55
  tests/test_quota.py
@@ -32,7 +32,7 @@ MANAGED_DEPENDENCIES = {
32
32
  "openstack::Router",
33
33
  "openstack::Subnet",
34
34
  "openstack::VirtualMachine",
35
- "openstack::HostPort",
35
+ "openstack::NetworkPort",
36
36
  "openstack::FloatingIP",
37
37
  "openstack::SecurityGroup",
38
38
  }
@@ -66,15 +66,21 @@ def set_dependencies_on_purged_resources(collector: ResourceCollector) -> None:
66
66
  Set the dependencies between purged VMs/ports and the security groups they reference.
67
67
  """
68
68
  vms = collector.get("openstack::VirtualMachine", {})
69
- ports = collector.get("openstack::HostPort", {})
69
+ ports = collector.get("openstack::NetworkPort", {})
70
70
  sgs = collector.get("openstack::SecurityGroup", {})
71
71
 
72
72
  # When the VM is purged, all its purged ports should be deployed first
73
73
  for vm in vms.values():
74
74
  for vm_port in vm.ports:
75
75
  if vm_port["name"] in ports:
76
- port_id = ports[vm_port["name"]].id
77
- vm.requires.add(port_id)
76
+ port = ports[vm_port["name"]]
77
+ if port.is_trunk_parent:
78
+ # The vm boots on (and binds) its trunk parent port, so the vm must be
79
+ # torn down before the port. This mirrors the create-time inversion and
80
+ # avoids a cycle with the trunk (which is deleted before its parent).
81
+ port.requires.add(vm.id)
82
+ else:
83
+ vm.requires.add(port.id)
78
84
 
79
85
  # When a security group is purged, every port referencing it must be removed (or have the
80
86
  # reference dropped) before the group itself can be deleted.
@@ -90,7 +96,7 @@ def set_dependencies_on_non_purged_resources(collector: ResourceCollector) -> No
90
96
  routers = collector.get("openstack::Router", {})
91
97
  subnets = collector.get("openstack::Subnet", {})
92
98
  vms = collector.get("openstack::VirtualMachine", {})
93
- ports = collector.get("openstack::HostPort", {})
99
+ ports = collector.get("openstack::NetworkPort", {})
94
100
  fips = collector.get("openstack::FloatingIP", {})
95
101
  sgs = collector.get("openstack::SecurityGroup", {})
96
102
  router_map = {}
@@ -143,7 +149,14 @@ def set_dependencies_on_non_purged_resources(collector: ResourceCollector) -> No
143
149
  port.requires.add(subnets[port.network].id)
144
150
 
145
151
  if port.host in vms:
146
- port.requires.add(vms[port.host].id)
152
+ if port.is_trunk_parent:
153
+ # A trunk parent must exist (unbound) before its vm: the trunk is built
154
+ # on it and the vm then boots on the pre-created port. Ordering the port
155
+ # after the vm would both bind it too early (neutron rejects a trunk on a
156
+ # bound port) and cycle with the trunk (vm -> trunk -> parent port).
157
+ vms[port.host].requires.add(port.id)
158
+ else:
159
+ port.requires.add(vms[port.host].id)
147
160
 
148
161
  for sg_name in port.security_groups:
149
162
  if sg_name in sgs:
@@ -20,6 +20,7 @@ import time
20
20
  import traceback
21
21
  from typing import Any
22
22
 
23
+ from inmanta import ast
23
24
  from inmanta.agent.handler import (
24
25
  HandlerContext,
25
26
  ResourcePurged,
@@ -29,7 +30,7 @@ from inmanta.agent.handler import (
29
30
  )
30
31
  from inmanta.execute import proxy
31
32
  from inmanta.resources import resource
32
- from openstack.exceptions import ResourceFailure, ResourceTimeout
33
+ from openstack.exceptions import NotFoundException, ResourceFailure, ResourceTimeout
33
34
 
34
35
  from inmanta_plugins.openstack.common.base import (
35
36
  NotUniqueException,
@@ -47,6 +48,7 @@ class VirtualMachine(OpenstackResource):
47
48
  fields = (
48
49
  "name",
49
50
  "flavor",
51
+ "flavor_spec",
50
52
  "image",
51
53
  "key_name",
52
54
  "user_data",
@@ -61,6 +63,7 @@ class VirtualMachine(OpenstackResource):
61
63
  )
62
64
  name: str
63
65
  flavor: str
66
+ flavor_spec: dict[str, int] | None
64
67
  image: str
65
68
  key_name: str
66
69
  user_data: str
@@ -73,6 +76,25 @@ class VirtualMachine(OpenstackResource):
73
76
  availability_zone: str | None
74
77
  block_devices: list[dict]
75
78
 
79
+ @staticmethod
80
+ def get_flavor_spec(_, vm):
81
+ """Resources of the linked flavor, used to detect a redefined flavor.
82
+
83
+ Returns None when no flavor relation is set; in that case the handler
84
+ only compares flavor names and does not resize on a content change.
85
+ """
86
+ try:
87
+ flavor = vm.flavor_rel
88
+ except ast.OptionalValueException:
89
+ return None
90
+ return {
91
+ "ram": flavor.ram,
92
+ "vcpus": flavor.vcpus,
93
+ "disk": flavor.disk,
94
+ "ephemeral": flavor.ephemeral,
95
+ "swap": flavor.swap,
96
+ }
97
+
76
98
  @staticmethod
77
99
  def get_key_name(exporter, vm):
78
100
  return vm.key_pair.name
@@ -100,13 +122,18 @@ class VirtualMachine(OpenstackResource):
100
122
  port = {
101
123
  "name": p.name,
102
124
  "address": None,
125
+ "address_v6": None,
103
126
  "network": p.subnet.name,
104
127
  "dhcp": p.dhcp,
105
128
  "index": p.port_index,
106
129
  }
107
130
  try:
108
131
  port["address"] = p.address
109
- except proxy.UnknownException:
132
+ except (proxy.UnknownException, ast.OptionalValueException):
133
+ pass
134
+ try:
135
+ port["address_v6"] = p.address_v6
136
+ except (proxy.UnknownException, ast.OptionalValueException):
110
137
  pass
111
138
  ports.append(port)
112
139
 
@@ -156,7 +183,9 @@ class VirtualMachineHandler(OpenStackHandler):
156
183
  @cache(timeout=10, cache_none=False)
157
184
  def _port_id(self, port_name):
158
185
  ports = self._connection.network.ports(name=port_name)
159
- return self.require_single_item(ports)
186
+ port = self.require_single_item(ports)
187
+ # Nova expects the port id (a string) in the boot NIC, not the port object.
188
+ return port.id if port is not None else None
160
189
 
161
190
  @cache(timeout=10, cache_none=False)
162
191
  def _get_subnet_id(self, subnet_name):
@@ -176,8 +205,13 @@ class VirtualMachineHandler(OpenStackHandler):
176
205
  if network is None:
177
206
  raise SkipResource(f"Network {port['network']} not found")
178
207
  nic = {"uuid": network}
208
+ # A boot NIC carries a single fixed_ip. A dual-stack port boots with its
209
+ # IPv4 here (preserving its port_index slot); the NetworkPort handler adds the
210
+ # IPv6 to the same port afterwards via update_port, which keeps the slot.
179
211
  if port["address"] is not None:
180
212
  nic["fixed_ip"] = port["address"]
213
+ elif port["address_v6"] is not None:
214
+ nic["fixed_ip"] = port["address_v6"]
181
215
  else:
182
216
  nic["port"] = port_id
183
217
 
@@ -245,29 +279,111 @@ class VirtualMachineHandler(OpenStackHandler):
245
279
  f"Server {self.server.id} ({self.server.name}) is in ERROR state: {msg}"
246
280
  ) from None
247
281
 
248
- def _wait_for_active(self, ctx: HandlerContext, timeout: int = 600) -> None:
249
- """Wait for self.server to reach ACTIVE, raise on ERROR or timeout."""
282
+ def _wait_for_status(
283
+ self,
284
+ ctx: HandlerContext,
285
+ status: str,
286
+ operation: str,
287
+ timeout: int = 600,
288
+ ) -> None:
289
+ """Wait for self.server to reach `status`, raise on ERROR or timeout.
290
+
291
+ `operation` describes the action in progress (e.g. "build" or "resize")
292
+ and is only used to build the error messages.
293
+
294
+ Nova may briefly return a 404 for the instance while a resize is being
295
+ applied; such transient NotFoundException errors are tolerated and polling
296
+ resumes until the status is reached or the timeout elapses.
297
+ """
298
+ deadline = time.monotonic() + timeout
299
+ while True:
300
+ remaining = deadline - time.monotonic()
301
+ if remaining <= 0:
302
+ self._raise_status_timeout(ctx, status, timeout)
303
+ try:
304
+ self.server = self._connection.compute.wait_for_server(
305
+ self.server, status=status, wait=max(1, int(remaining))
306
+ )
307
+ return
308
+ except ResourceFailure:
309
+ self._raise_server_error(
310
+ ctx, f"VM entered ERROR state during {operation}"
311
+ )
312
+ except ResourceTimeout:
313
+ self._raise_status_timeout(ctx, status, timeout)
314
+ except NotFoundException:
315
+ # The instance is temporarily not retrievable (e.g. mid-resize);
316
+ # give it a moment and keep polling until the deadline.
317
+ time.sleep(2)
318
+
319
+ def _raise_status_timeout(
320
+ self, ctx: HandlerContext, status: str, timeout: int
321
+ ) -> None:
250
322
  try:
251
- self.server = self._connection.compute.wait_for_server(
252
- self.server, wait=timeout
253
- )
254
- except ResourceFailure:
255
- self._raise_server_error(ctx, "VM entered ERROR state during build")
256
- except ResourceTimeout:
257
323
  server = self._connection.compute.get_server(self.server.id)
258
- status = getattr(server, "status", "unknown")
324
+ current_status = getattr(server, "status", "unknown")
259
325
  task_state = getattr(server, "task_state", "unknown")
260
- ctx.error(
261
- "Timed out waiting for VM to become ACTIVE",
262
- timeout_seconds=timeout,
263
- current_status=status,
264
- current_task_state=task_state,
265
- server_id=self.server.id,
266
- )
267
- raise Exception(
268
- f"Server {self.server.id} did not reach ACTIVE within {timeout}s "
269
- f"(status={status}, task_state={task_state})"
270
- ) from None
326
+ except NotFoundException:
327
+ current_status = "not_found"
328
+ task_state = "unknown"
329
+ ctx.error(
330
+ f"Timed out waiting for VM to reach {status}",
331
+ timeout_seconds=timeout,
332
+ current_status=current_status,
333
+ current_task_state=task_state,
334
+ server_id=self.server.id,
335
+ )
336
+ raise Exception(
337
+ f"Server {self.server.id} did not reach {status} within {timeout}s "
338
+ f"(status={current_status}, task_state={task_state})"
339
+ ) from None
340
+
341
+ def _get_server_flavor_name(self, server) -> str:
342
+ """Return the flavor name of `server`.
343
+
344
+ Depending on the negotiated Nova microversion the flavor embedded in a
345
+ server may only carry its id, so fall back to resolving the flavor by id.
346
+ """
347
+ flavor = server.flavor
348
+ if flavor.original_name:
349
+ return flavor.original_name
350
+ return self._connection.compute.get_flavor(flavor.id).name
351
+
352
+ def _get_server_flavor_spec(self, server) -> dict[str, int]:
353
+ """Return the resources `server` is actually running with.
354
+
355
+ Mirrors _get_server_flavor_name: recent Nova microversions embed the full
356
+ flavor spec in the server, older ones only its id, so resolve by id then.
357
+ The keys match those exported by VirtualMachine.get_flavor_spec.
358
+ """
359
+ flavor = server.flavor
360
+ if not flavor.original_name:
361
+ flavor = self._connection.compute.get_flavor(flavor.id)
362
+ return {
363
+ "ram": flavor.ram,
364
+ "vcpus": flavor.vcpus,
365
+ "disk": flavor.disk,
366
+ "ephemeral": getattr(flavor, "ephemeral", 0) or 0,
367
+ "swap": getattr(flavor, "swap", 0) or 0,
368
+ }
369
+
370
+ def _resize_server(
371
+ self, ctx: HandlerContext, resource: VirtualMachine, flavor_name: str
372
+ ) -> None:
373
+ """Resize self.server to the flavor named `flavor_name` and confirm it."""
374
+ flavor = self._connection.compute.find_flavor(flavor_name, ignore_missing=False)
375
+ # Confirming a resize returns the server to the status it had before,
376
+ # so remember it to know what to wait for afterwards.
377
+ original_status = self.server.status
378
+ ctx.info(
379
+ f"Resizing server {resource.name} to flavor {flavor_name}",
380
+ flavor_id=flavor.id,
381
+ )
382
+ self._connection.compute.resize_server(self.server, flavor.id)
383
+ # A resize must be explicitly confirmed once the server reports VERIFY_RESIZE.
384
+ self._wait_for_status(ctx, "VERIFY_RESIZE", "resize")
385
+ self._connection.compute.confirm_server_resize(self.server)
386
+ self._wait_for_status(ctx, original_status, "resize")
271
387
 
272
388
  def read_resource(self, ctx: HandlerContext, resource: VirtualMachine) -> None:
273
389
  """
@@ -278,12 +394,16 @@ class VirtualMachineHandler(OpenStackHandler):
278
394
  if self.server is None:
279
395
  raise ResourcePurged()
280
396
 
281
- if self.server.status == "ERROR":
282
- self._raise_server_error(
283
- ctx, "Server is in ERROR state; delete it to allow redeployment"
284
- )
285
-
286
397
  resource.purged = False
398
+ # Reflect the live flavor so a flavor change in the model is detected as
399
+ # drift and handled by update_resource through a resize.
400
+ resource.flavor = self._get_server_flavor_name(self.server)
401
+ # When the VM is linked to a managed flavor, also reflect the resources it
402
+ # is currently running with. This makes a flavor that was redefined under
403
+ # the same name (e.g. deleted and recreated with more RAM) show up as drift
404
+ # even though its name is unchanged.
405
+ if resource.flavor_spec is not None:
406
+ resource.flavor_spec = self._get_server_flavor_spec(self.server)
287
407
  if resource.security_groups:
288
408
  # An empty relation leaves the server-level security groups unmanaged, so port
289
409
  # security groups (managed through neutron) can be used for fine-grained control
@@ -335,7 +455,7 @@ class VirtualMachineHandler(OpenStackHandler):
335
455
  )
336
456
 
337
457
  self.server = self._connection.compute.create_server(**args)
338
- self._wait_for_active(ctx)
458
+ self._wait_for_status(ctx, "ACTIVE", "build")
339
459
 
340
460
  self._set_facts(ctx)
341
461
  # Note: attach/detach of interfaces handled by Neutron or by passing ports at creation time
@@ -365,9 +485,23 @@ class VirtualMachineHandler(OpenStackHandler):
365
485
  def update_resource(
366
486
  self, ctx: HandlerContext, changes: dict, resource: VirtualMachine
367
487
  ) -> None:
368
- self.validate_allowed_fields(changes, {"security_groups", "key_name"})
488
+
489
+ if self.server.status == "ERROR":
490
+ self._raise_server_error(
491
+ ctx, "Server is in ERROR state; delete it to allow redeployment"
492
+ )
493
+
494
+ self.validate_allowed_fields(
495
+ changes, {"security_groups", "key_name", "flavor", "flavor_spec"}
496
+ )
369
497
 
370
498
  self._ensure_key(ctx, resource)
499
+
500
+ # A change to either the flavor name or, for a flavor-linked VM, the flavor
501
+ # resources both translate to an in-place resize to the desired flavor.
502
+ if "flavor" in changes or "flavor_spec" in changes:
503
+ self._resize_server(ctx, resource, resource.flavor)
504
+
371
505
  if "security_groups" in changes:
372
506
  current = set(changes["security_groups"]["current"])
373
507
  desired = set(changes["security_groups"]["desired"])
@@ -345,42 +345,6 @@ implement RouterPort using providerRequire
345
345
 
346
346
  RouterPort.subnet [1] -- Subnet.routers [0:]
347
347
 
348
- entity HostPort extends Port:
349
- """
350
- A port attached to a VM
351
-
352
- :param name: The name of the host port.
353
- :param portsecurity: Enable or disable port security (security groups and spoofing filters)
354
- :param dhcp: Enable dhcp for this port or not for this port
355
- :param port_index: The index of the port. This determines the order of the interfaces on the virtual machine. 0 means no specific order.
356
- :param retries: A hostport can only be attached to a VM when it is in an active state. The handler will skip this port when the VM is not ready. To
357
- speed up deployments, the handler can retry this number of times before skipping the resource.
358
- :param wait: The number of seconds to wait between retries.
359
- """
360
- std::ipv4_address address
361
- bool portsecurity=true
362
- bool dhcp=true
363
- int port_index=0
364
- int retries=20
365
- int wait=5
366
- end
367
-
368
- implementation noSecurityGroupsWhenPortSecurityDisabled for HostPort:
369
- # Security groups can only be enforced on a port when port security is enabled.
370
- std::assert(
371
- std::count(self.security_groups) == 0,
372
- "HostPort '{{name}}' cannot have security_groups assigned while portsecurity is disabled."
373
- )
374
- end
375
-
376
- implement HostPort using providerRequire
377
- implement HostPort using noSecurityGroupsWhenPortSecurityDisabled when not self.portsecurity
378
-
379
- HostPort.subnet [1] -- Subnet.host_ports [0:]
380
- HostPort.vm [1] -- VirtualMachine.ports [0:]
381
-
382
- index HostPort(provider, vm, name)
383
-
384
348
  entity SubnetBase extends OpenStackResource:
385
349
  """
386
350
  A neutron subnet
@@ -502,25 +466,66 @@ implement QoSPolicy using providerRequire
502
466
 
503
467
  entity NetworkPort extends Port:
504
468
  """
505
- A standalone neutron port, managed directly on a network and not bound
506
- to a router or a virtual machine. This mirrors the
507
- ``openstack port create --network <network> <name>`` command and is
508
- suitable as the parent port of a :py:class:`TrunkNetwork`.
509
-
469
+ A neutron port. By default it is a standalone port, managed directly on a
470
+ network and not bound to a virtual machine -- this mirrors the
471
+ ``openstack port create --network <network> <name>`` command and is suitable
472
+ as the parent port of a :py:class:`TrunkNetwork`. Setting the ``vm`` relation
473
+ instead attaches the port to that virtual machine.
474
+
475
+ The port lives on an explicit ``network`` or, when only a ``subnet`` is given,
476
+ on that subnet's network.
477
+
478
+ :attr address: An optional fixed IPv4 to request. When omitted neutron assigns one; leave
479
+ unset for an IPv6-only port. Ignored on a vm-attached port when dhcp is true
480
+ (a standalone port always takes the address it is given).
481
+ :attr address_v6: An optional fixed IPv6 to request from an IPv6 subnet on the port's
482
+ network. When omitted no IPv6 is requested. Ignored when dhcp is true.
483
+ :attr mac_address: An optional MAC address for the port. When omitted, neutron assigns one.
484
+ It is typed as a plain string (not the ``mac_addr`` typedef) so it can hold a
485
+ fact reference resolved at deploy time, e.g.
486
+ ``future::std::create_fact_reference(parent_port, "mac_address")`` to make a
487
+ trunk sub-port share its parent port's MAC. Set only at creation; neutron does
488
+ not allow changing it afterwards.
510
489
  :attr portsecurity: Enable or disable port security (security groups and spoofing filters).
511
- :attr address: An optional fixed ip address for the port. When omitted, neutron
512
- assigns one from the network. When set, a subnet must be provided.
513
- """
490
+ :attr dhcp: Enable dhcp for this port or not. Only relevant when the port is attached to a vm.
491
+ :attr port_index: The index of the port. This determines the order of the interfaces on the
492
+ virtual machine. 0 means no specific order. Only relevant when the port is
493
+ attached to a vm.
494
+ :attr retries: A port attached to a vm can only be attached when the vm is in an active state.
495
+ The handler will skip this port when the vm is not ready. To speed up
496
+ deployments, the handler can retry this number of times before skipping.
497
+ :attr wait: The number of seconds to wait between retries.
498
+ """
499
+ std::ipv4_address? address = null
500
+ std::ipv6_address? address_v6 = null
501
+ string? mac_address = null
514
502
  bool portsecurity=true
515
- std::ipv4_address? address=null
503
+ bool dhcp=true
504
+ int port_index=0
505
+ int retries=20
506
+ int wait=5
516
507
  end
517
508
 
518
- index NetworkPort(project, name)
519
-
520
- NetworkPort.network [1] -- Network.network_ports [0:]
521
- NetworkPort.subnet [0:1] -- Subnet.network_ports [0:]
509
+ implementation noSecurityGroupsWhenPortSecurityDisabled for NetworkPort:
510
+ # Security groups can only be enforced on a port when port security is enabled.
511
+ std::assert(
512
+ std::count(self.security_groups) == 0,
513
+ "NetworkPort '{{name}}' cannot have security_groups assigned while portsecurity is disabled."
514
+ )
515
+ end
522
516
 
523
517
  implement NetworkPort using providerRequire
518
+ implement NetworkPort using noSecurityGroupsWhenPortSecurityDisabled when not self.portsecurity
519
+
520
+ index NetworkPort(project, name)
521
+
522
+ # The port lives on an explicit network, or derives it from its subnet when only the
523
+ # subnet is given.
524
+ NetworkPort.network [0:1] -- Network.network_ports [0:]
525
+ NetworkPort.subnet [0:1] -- SubnetBase.network_ports [0:]
526
+ # When set, the port is attached to this virtual machine. Left unset, the port is a
527
+ # standalone port (e.g. the parent port of a trunk).
528
+ NetworkPort.vm [0:1] -- VirtualMachine.ports [0:]
524
529
 
525
530
 
526
531
  entity TrunkNetwork extends OpenStackResource:
@@ -529,9 +534,11 @@ entity TrunkNetwork extends OpenStackResource:
529
534
  subports (see :py:class:`TrunkPort`) so that a single VM interface can
530
535
  carry traffic for multiple networks using VLAN tagging.
531
536
 
532
- The trunk is built on top of an existing parent port. That port can be a
533
- standalone :py:class:`NetworkPort` or a :py:class:`HostPort` attached to
534
- a virtual machine. The parent port is managed separately.
537
+ The trunk is built on top of an existing parent port (a
538
+ :py:class:`NetworkPort`). Neutron only accepts a parent port that is not yet
539
+ bound to a virtual machine, so when the trunk parent is attached to a vm the
540
+ port must be created (and the trunk built on it) before the vm boots on it.
541
+ The parent port is managed separately.
535
542
 
536
543
  :attr name: The name of the trunk. Must be unique per project.
537
544
  :attr description: An optional human-readable description.
@@ -553,8 +560,8 @@ implement TrunkNetwork using providerRequire
553
560
 
554
561
  entity TrunkPort extends OpenStackResource:
555
562
  """
556
- A subport of a :py:class:`TrunkNetwork`. It attaches an existing port
557
- (a :py:class:`NetworkPort` or :py:class:`HostPort`) to the trunk with the
563
+ A subport of a :py:class:`TrunkNetwork`. It attaches an existing
564
+ :py:class:`NetworkPort` to the trunk with the
558
565
  configured segmentation type and id (typically a VLAN). The referenced
559
566
  port is managed separately; this resource only manages the trunk
560
567
  subport attachment, not the port itself.
@@ -627,7 +634,7 @@ end
627
634
  SecurityGroup.provider [1] -- Provider.security_groups [0:]
628
635
  SecurityGroup.project [1] -- Project.security_groups [0:]
629
636
  SecurityGroup.virtual_machines [0:] -- VirtualMachine.security_groups [0:]
630
- HostPort.security_groups [0:] -- SecurityGroup
637
+ NetworkPort.security_groups [0:] -- SecurityGroup
631
638
  """
632
639
  :rel security_groups: The security groups to enforce on this port. When left empty (the default), the
633
640
  port's security groups are left unmanaged for backward compatibility: existing groups (including the
@@ -686,7 +693,7 @@ implement FloatingIP using fipAddr when not force_ip
686
693
  FloatingIP.project [1] -- Project.floating_ips [0:]
687
694
  FloatingIP.provider [1] -- Provider.floating_ips [0:]
688
695
  FloatingIP.external_network [1] -- Network.floating_ips [0:]
689
- FloatingIP.port [1] -- HostPort.floating_ips [0:]
696
+ FloatingIP.port [1] -- NetworkPort.floating_ips [0:]
690
697
 
691
698
  entity VMAttributes:
692
699
  """
@@ -713,14 +720,25 @@ entity VirtualMachine extends OpenStackResource, VMAttributes:
713
720
  string name
714
721
  end
715
722
 
716
- implement VirtualMachine using providerRequire
717
-
718
723
  index VirtualMachine(provider, name)
719
724
 
720
- VirtualMachine.eth0_port [1] -- HostPort
725
+ VirtualMachine.eth0_port [1] -- NetworkPort
721
726
  VirtualMachine.key_pair [1] -- ssh::Key
722
727
  VirtualMachine.project [1] -- Project
723
728
  VirtualMachine.provider [1] -- Provider.virtual_machines [0:]
729
+ # Optional link to the managed Flavor backing this VM. When set, the flavor name
730
+ # is derived from it and the handler compares the flavor's resources against what
731
+ # the VM is actually running, so a flavor recreated with different resources under
732
+ # the same name is detected as drift and applied as an in-place resize.
733
+ VirtualMachine.flavor_rel [0:1] -- Flavor
734
+
735
+ implementation flavorFromRelation for VirtualMachine:
736
+ self.flavor = self.flavor_rel.name
737
+ self.requires += self.flavor_rel
738
+ end
739
+
740
+ implement VirtualMachine using providerRequire
741
+ implement VirtualMachine using flavorFromRelation when flavor_rel is defined
724
742
 
725
743
  entity Host extends std::Host, VMAttributes:
726
744
  """
@@ -739,7 +757,7 @@ Host.provider [1] -- Provider
739
757
  Host.security_groups [0:] -- SecurityGroup
740
758
 
741
759
  implementation eth0Port for Host:
742
- port = HostPort(provider=provider, vm=self.vm, subnet=subnet, name="{{name}}_eth0", address=std::getfact(self.vm, "ip_address"),
760
+ port = NetworkPort(provider=provider, vm=self.vm, subnet=subnet, name="{{name}}_eth0", address=std::getfact(self.vm, "ip_address"),
743
761
  project=project, port_index=1, purged=purged, purge_on_delete=self.purge_on_delete)
744
762
  self.vm.eth0_port = port
745
763
  end