inmanta-module-openstack 5.1.0__tar.gz → 5.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.
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/PKG-INFO +1 -1
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_module_openstack.egg-info/PKG-INFO +1 -1
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_module_openstack.egg-info/SOURCES.txt +1 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/compute/host_port.py +88 -30
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/compute/virtual_machine.py +161 -29
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/model/_init.cf +27 -4
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/network_port.py +15 -1
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/setup.cfg +1 -1
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/setup.cfg +1 -1
- inmanta_module_openstack-5.1.1/tests/test_host_port_fixed_ips.py +347 -0
- inmanta_module_openstack-5.1.1/tests/test_nova.py +570 -0
- inmanta_module_openstack-5.1.0/tests/test_nova.py +0 -287
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/MANIFEST.in +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/README.md +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_module_openstack.egg-info/dependency_links.txt +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_module_openstack.egg-info/not-zip-safe +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_module_openstack.egg-info/requires.txt +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_module_openstack.egg-info/top_level.txt +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/__init__.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/common/__init__.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/common/base.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/common/deps.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/compute/__init__.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/compute/flavor.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/compute/host_aggregate.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/identity/__init__.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/identity/group.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/identity/group_role.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/identity/keystone_base.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/identity/project.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/identity/role.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/identity/user.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/image/__init__.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/image/image.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/__init__.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/floating_ip.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/network.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/qos_policy.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/router.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/router_port.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/security_group.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/subnet.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/subnet_v6.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/trunk_network.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/network/trunk_port.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/quota/__init__.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/inmanta_plugins/openstack/quota/quota.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/pyproject.toml +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_block_devices.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_dependency_handling.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_examples.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_flavor.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_host_aggregate.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_host_port_security_groups.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_image.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_keystone.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_neutron.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_quota.py +0 -0
- {inmanta_module_openstack-5.1.0 → inmanta_module_openstack-5.1.1}/tests/test_trunk.py +0 -0
|
@@ -16,6 +16,8 @@ limitations under the License.
|
|
|
16
16
|
Contact: code@inmanta.com
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
import ipaddress
|
|
20
|
+
import json
|
|
19
21
|
import time
|
|
20
22
|
from typing import Optional
|
|
21
23
|
|
|
@@ -35,7 +37,7 @@ from inmanta_plugins.openstack.common.base import (
|
|
|
35
37
|
@resource("openstack::HostPort", agent="provider.name", id_attribute="name")
|
|
36
38
|
class HostPort(Port):
|
|
37
39
|
"""
|
|
38
|
-
A port
|
|
40
|
+
A port attached to a VM.
|
|
39
41
|
"""
|
|
40
42
|
|
|
41
43
|
fields = (
|
|
@@ -48,6 +50,7 @@ class HostPort(Port):
|
|
|
48
50
|
"allowed_address_pairs",
|
|
49
51
|
"wait_for_vm",
|
|
50
52
|
"security_groups",
|
|
53
|
+
"address_v6",
|
|
51
54
|
)
|
|
52
55
|
host: str
|
|
53
56
|
portsecurity: bool
|
|
@@ -58,6 +61,21 @@ class HostPort(Port):
|
|
|
58
61
|
allowed_address_pairs: dict[str, Optional[str]]
|
|
59
62
|
wait_for_vm: bool
|
|
60
63
|
security_groups: list[str]
|
|
64
|
+
address_v6: Optional[str]
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def get_address(_, port):
|
|
68
|
+
try:
|
|
69
|
+
return port.address
|
|
70
|
+
except ast.OptionalValueException:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def get_address_v6(_, port):
|
|
75
|
+
try:
|
|
76
|
+
return port.address_v6
|
|
77
|
+
except ast.OptionalValueException:
|
|
78
|
+
return None
|
|
61
79
|
|
|
62
80
|
@staticmethod
|
|
63
81
|
def get_host(_, port):
|
|
@@ -86,6 +104,11 @@ class HostPort(Port):
|
|
|
86
104
|
|
|
87
105
|
@provider("openstack::HostPort", name="openstack")
|
|
88
106
|
class HostPortHandler(OpenStackHandler):
|
|
107
|
+
"""
|
|
108
|
+
Manage a neutron port on a VM. Its ``subnet`` relation may be an IPv4 or an
|
|
109
|
+
IPv6 (``SubnetBase``) subnet, so a port can live on an IPv6-only network.
|
|
110
|
+
"""
|
|
111
|
+
|
|
89
112
|
def _get_port(self, ctx, network_id, device_id):
|
|
90
113
|
"""
|
|
91
114
|
Retrieve a port attached to a given device (VM) on a specific network.
|
|
@@ -119,10 +142,13 @@ class HostPortHandler(OpenStackHandler):
|
|
|
119
142
|
if vm_state == "active":
|
|
120
143
|
return vm
|
|
121
144
|
else:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
if resource.wait_for_vm:
|
|
146
|
+
ctx.info(
|
|
147
|
+
"VM for port is not in active state, but %(state)s. Waiting and retrying in 5 seconds.",
|
|
148
|
+
state=vm_state,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
raise ResourcePurged()
|
|
126
152
|
else:
|
|
127
153
|
if not resource.wait_for_vm:
|
|
128
154
|
# Vm doesn't exist, so we can assume this port doesn't exist either
|
|
@@ -165,21 +191,17 @@ class HostPortHandler(OpenStackHandler):
|
|
|
165
191
|
raise ResourcePurged()
|
|
166
192
|
|
|
167
193
|
resource.purged = False
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
else:
|
|
180
|
-
resource.subnet = subnet.name
|
|
181
|
-
else:
|
|
182
|
-
resource.subnet = ""
|
|
194
|
+
|
|
195
|
+
# A fixed ip is pinned at creation and exposed through the fixed_ips fact; it is
|
|
196
|
+
# not reflected back into the resource, as reflecting it would surface a spurious
|
|
197
|
+
# diff on an address the handler does not reconcile. The one exception is a
|
|
198
|
+
# statically requested IPv6: a dual-stack port boots with only its IPv4 (a boot
|
|
199
|
+
# NIC holds a single fixed_ip), so drive an update when the requested address is
|
|
200
|
+
# not already assigned. Test for that exact address (a port may carry several
|
|
201
|
+
# IPv6, e.g. an extra subnet or a SLAAC address) and leave the attribute alone
|
|
202
|
+
# when no IPv6 was requested, so an auto-assigned address is not seen as a change.
|
|
203
|
+
if resource.address_v6 and not self._has_fixed_ip(resource.address_v6):
|
|
204
|
+
resource.address_v6 = None
|
|
183
205
|
|
|
184
206
|
resource.portsecurity = self.port.is_port_security_enabled
|
|
185
207
|
|
|
@@ -216,10 +238,17 @@ class HostPortHandler(OpenStackHandler):
|
|
|
216
238
|
"project_id": self.project_id,
|
|
217
239
|
}
|
|
218
240
|
|
|
219
|
-
if
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
241
|
+
if not resource.dhcp:
|
|
242
|
+
fixed_ips = []
|
|
243
|
+
if resource.address:
|
|
244
|
+
fixed_ips.append(
|
|
245
|
+
{"subnet_id": subnet.id, "ip_address": resource.address}
|
|
246
|
+
)
|
|
247
|
+
if resource.address_v6:
|
|
248
|
+
# No subnet_id: neutron allocates from the matching IPv6 subnet
|
|
249
|
+
fixed_ips.append({"ip_address": resource.address_v6})
|
|
250
|
+
if fixed_ips:
|
|
251
|
+
port_attrs["fixed_ips"] = fixed_ips
|
|
223
252
|
|
|
224
253
|
port_attrs["port_security_enabled"] = resource.portsecurity
|
|
225
254
|
if resource.security_groups:
|
|
@@ -268,7 +297,13 @@ class HostPortHandler(OpenStackHandler):
|
|
|
268
297
|
) -> None:
|
|
269
298
|
self.validate_allowed_fields(
|
|
270
299
|
changes,
|
|
271
|
-
{
|
|
300
|
+
{
|
|
301
|
+
"portsecurity",
|
|
302
|
+
"name",
|
|
303
|
+
"allowed_address_pairs",
|
|
304
|
+
"security_groups",
|
|
305
|
+
"address_v6",
|
|
306
|
+
},
|
|
272
307
|
)
|
|
273
308
|
|
|
274
309
|
disabling_portsecurity = (
|
|
@@ -299,18 +334,41 @@ class HostPortHandler(OpenStackHandler):
|
|
|
299
334
|
]
|
|
300
335
|
self.port.allowed_address_pairs = allowed_address_pairs
|
|
301
336
|
|
|
337
|
+
if "address_v6" in changes:
|
|
338
|
+
# The requested IPv6 is not on the port (a dual-stack port boots with only
|
|
339
|
+
# its IPv4). Add it, keeping the existing v4 and any other assigned ips;
|
|
340
|
+
# neutron picks the matching subnet. Updating fixed_ips does not move the
|
|
341
|
+
# port's interface, so its boot-time ordering is preserved.
|
|
342
|
+
self.port.fixed_ips = list(self.port.fixed_ips) + [
|
|
343
|
+
{"ip_address": resource.address_v6}
|
|
344
|
+
]
|
|
345
|
+
|
|
302
346
|
try:
|
|
303
347
|
self.port = self._connection.network.update_port(self.port)
|
|
304
348
|
except openstack.exceptions.ConflictException as e:
|
|
305
349
|
raise SkipResource(f"Port is not ready: {e}")
|
|
350
|
+
|
|
351
|
+
# The fixed ips may have changed; re-report the fixed_ips fact for this deploy.
|
|
352
|
+
self._set_facts(ctx)
|
|
306
353
|
ctx.set_updated()
|
|
307
354
|
|
|
308
|
-
def
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
355
|
+
def _has_fixed_ip(self, address: str) -> bool:
|
|
356
|
+
"""Whether the neutron port already has ``address`` as a fixed ip.
|
|
357
|
+
|
|
358
|
+
Compares normalised addresses so equivalent textual forms of the same IPv6
|
|
359
|
+
(e.g. ``2001:db8::1`` and ``2001:0db8::0001``) are treated as equal.
|
|
360
|
+
"""
|
|
361
|
+
target = ipaddress.ip_address(address)
|
|
362
|
+
return any(
|
|
363
|
+
ipaddress.ip_address(ip["ip_address"]) == target
|
|
364
|
+
for ip in self.port.fixed_ips
|
|
365
|
+
)
|
|
313
366
|
|
|
367
|
+
def _set_facts(self, ctx):
|
|
368
|
+
# Report every assigned ip as a plain list of addresses. This is the only channel
|
|
369
|
+
# exposing IPv6 and pool-assigned addresses, which the model cannot hold.
|
|
370
|
+
fixed_ips = [ip["ip_address"] for ip in self.port.fixed_ips]
|
|
371
|
+
ctx.set_fact("fixed_ips", json.dumps(fixed_ips), expires=True)
|
|
314
372
|
ctx.set_fact("mac_address", self.port.mac_address, expires=True)
|
|
315
373
|
ctx.set_fact("port_id", self.port.id, expires=False)
|
|
316
374
|
|
|
@@ -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
|
|
|
@@ -176,8 +203,13 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
176
203
|
if network is None:
|
|
177
204
|
raise SkipResource(f"Network {port['network']} not found")
|
|
178
205
|
nic = {"uuid": network}
|
|
206
|
+
# A boot NIC carries a single fixed_ip. A dual-stack port boots with its
|
|
207
|
+
# IPv4 here (preserving its port_index slot); the HostPort handler adds the
|
|
208
|
+
# IPv6 to the same port afterwards via update_port, which keeps the slot.
|
|
179
209
|
if port["address"] is not None:
|
|
180
210
|
nic["fixed_ip"] = port["address"]
|
|
211
|
+
elif port["address_v6"] is not None:
|
|
212
|
+
nic["fixed_ip"] = port["address_v6"]
|
|
181
213
|
else:
|
|
182
214
|
nic["port"] = port_id
|
|
183
215
|
|
|
@@ -245,29 +277,111 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
245
277
|
f"Server {self.server.id} ({self.server.name}) is in ERROR state: {msg}"
|
|
246
278
|
) from None
|
|
247
279
|
|
|
248
|
-
def
|
|
249
|
-
|
|
280
|
+
def _wait_for_status(
|
|
281
|
+
self,
|
|
282
|
+
ctx: HandlerContext,
|
|
283
|
+
status: str,
|
|
284
|
+
operation: str,
|
|
285
|
+
timeout: int = 600,
|
|
286
|
+
) -> None:
|
|
287
|
+
"""Wait for self.server to reach `status`, raise on ERROR or timeout.
|
|
288
|
+
|
|
289
|
+
`operation` describes the action in progress (e.g. "build" or "resize")
|
|
290
|
+
and is only used to build the error messages.
|
|
291
|
+
|
|
292
|
+
Nova may briefly return a 404 for the instance while a resize is being
|
|
293
|
+
applied; such transient NotFoundException errors are tolerated and polling
|
|
294
|
+
resumes until the status is reached or the timeout elapses.
|
|
295
|
+
"""
|
|
296
|
+
deadline = time.monotonic() + timeout
|
|
297
|
+
while True:
|
|
298
|
+
remaining = deadline - time.monotonic()
|
|
299
|
+
if remaining <= 0:
|
|
300
|
+
self._raise_status_timeout(ctx, status, timeout)
|
|
301
|
+
try:
|
|
302
|
+
self.server = self._connection.compute.wait_for_server(
|
|
303
|
+
self.server, status=status, wait=max(1, int(remaining))
|
|
304
|
+
)
|
|
305
|
+
return
|
|
306
|
+
except ResourceFailure:
|
|
307
|
+
self._raise_server_error(
|
|
308
|
+
ctx, f"VM entered ERROR state during {operation}"
|
|
309
|
+
)
|
|
310
|
+
except ResourceTimeout:
|
|
311
|
+
self._raise_status_timeout(ctx, status, timeout)
|
|
312
|
+
except NotFoundException:
|
|
313
|
+
# The instance is temporarily not retrievable (e.g. mid-resize);
|
|
314
|
+
# give it a moment and keep polling until the deadline.
|
|
315
|
+
time.sleep(2)
|
|
316
|
+
|
|
317
|
+
def _raise_status_timeout(
|
|
318
|
+
self, ctx: HandlerContext, status: str, timeout: int
|
|
319
|
+
) -> None:
|
|
250
320
|
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
321
|
server = self._connection.compute.get_server(self.server.id)
|
|
258
|
-
|
|
322
|
+
current_status = getattr(server, "status", "unknown")
|
|
259
323
|
task_state = getattr(server, "task_state", "unknown")
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
324
|
+
except NotFoundException:
|
|
325
|
+
current_status = "not_found"
|
|
326
|
+
task_state = "unknown"
|
|
327
|
+
ctx.error(
|
|
328
|
+
f"Timed out waiting for VM to reach {status}",
|
|
329
|
+
timeout_seconds=timeout,
|
|
330
|
+
current_status=current_status,
|
|
331
|
+
current_task_state=task_state,
|
|
332
|
+
server_id=self.server.id,
|
|
333
|
+
)
|
|
334
|
+
raise Exception(
|
|
335
|
+
f"Server {self.server.id} did not reach {status} within {timeout}s "
|
|
336
|
+
f"(status={current_status}, task_state={task_state})"
|
|
337
|
+
) from None
|
|
338
|
+
|
|
339
|
+
def _get_server_flavor_name(self, server) -> str:
|
|
340
|
+
"""Return the flavor name of `server`.
|
|
341
|
+
|
|
342
|
+
Depending on the negotiated Nova microversion the flavor embedded in a
|
|
343
|
+
server may only carry its id, so fall back to resolving the flavor by id.
|
|
344
|
+
"""
|
|
345
|
+
flavor = server.flavor
|
|
346
|
+
if flavor.original_name:
|
|
347
|
+
return flavor.original_name
|
|
348
|
+
return self._connection.compute.get_flavor(flavor.id).name
|
|
349
|
+
|
|
350
|
+
def _get_server_flavor_spec(self, server) -> dict[str, int]:
|
|
351
|
+
"""Return the resources `server` is actually running with.
|
|
352
|
+
|
|
353
|
+
Mirrors _get_server_flavor_name: recent Nova microversions embed the full
|
|
354
|
+
flavor spec in the server, older ones only its id, so resolve by id then.
|
|
355
|
+
The keys match those exported by VirtualMachine.get_flavor_spec.
|
|
356
|
+
"""
|
|
357
|
+
flavor = server.flavor
|
|
358
|
+
if not flavor.original_name:
|
|
359
|
+
flavor = self._connection.compute.get_flavor(flavor.id)
|
|
360
|
+
return {
|
|
361
|
+
"ram": flavor.ram,
|
|
362
|
+
"vcpus": flavor.vcpus,
|
|
363
|
+
"disk": flavor.disk,
|
|
364
|
+
"ephemeral": getattr(flavor, "ephemeral", 0) or 0,
|
|
365
|
+
"swap": getattr(flavor, "swap", 0) or 0,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
def _resize_server(
|
|
369
|
+
self, ctx: HandlerContext, resource: VirtualMachine, flavor_name: str
|
|
370
|
+
) -> None:
|
|
371
|
+
"""Resize self.server to the flavor named `flavor_name` and confirm it."""
|
|
372
|
+
flavor = self._connection.compute.find_flavor(flavor_name, ignore_missing=False)
|
|
373
|
+
# Confirming a resize returns the server to the status it had before,
|
|
374
|
+
# so remember it to know what to wait for afterwards.
|
|
375
|
+
original_status = self.server.status
|
|
376
|
+
ctx.info(
|
|
377
|
+
f"Resizing server {resource.name} to flavor {flavor_name}",
|
|
378
|
+
flavor_id=flavor.id,
|
|
379
|
+
)
|
|
380
|
+
self._connection.compute.resize_server(self.server, flavor.id)
|
|
381
|
+
# A resize must be explicitly confirmed once the server reports VERIFY_RESIZE.
|
|
382
|
+
self._wait_for_status(ctx, "VERIFY_RESIZE", "resize")
|
|
383
|
+
self._connection.compute.confirm_server_resize(self.server)
|
|
384
|
+
self._wait_for_status(ctx, original_status, "resize")
|
|
271
385
|
|
|
272
386
|
def read_resource(self, ctx: HandlerContext, resource: VirtualMachine) -> None:
|
|
273
387
|
"""
|
|
@@ -278,12 +392,16 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
278
392
|
if self.server is None:
|
|
279
393
|
raise ResourcePurged()
|
|
280
394
|
|
|
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
395
|
resource.purged = False
|
|
396
|
+
# Reflect the live flavor so a flavor change in the model is detected as
|
|
397
|
+
# drift and handled by update_resource through a resize.
|
|
398
|
+
resource.flavor = self._get_server_flavor_name(self.server)
|
|
399
|
+
# When the VM is linked to a managed flavor, also reflect the resources it
|
|
400
|
+
# is currently running with. This makes a flavor that was redefined under
|
|
401
|
+
# the same name (e.g. deleted and recreated with more RAM) show up as drift
|
|
402
|
+
# even though its name is unchanged.
|
|
403
|
+
if resource.flavor_spec is not None:
|
|
404
|
+
resource.flavor_spec = self._get_server_flavor_spec(self.server)
|
|
287
405
|
if resource.security_groups:
|
|
288
406
|
# An empty relation leaves the server-level security groups unmanaged, so port
|
|
289
407
|
# security groups (managed through neutron) can be used for fine-grained control
|
|
@@ -335,7 +453,7 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
335
453
|
)
|
|
336
454
|
|
|
337
455
|
self.server = self._connection.compute.create_server(**args)
|
|
338
|
-
self.
|
|
456
|
+
self._wait_for_status(ctx, "ACTIVE", "build")
|
|
339
457
|
|
|
340
458
|
self._set_facts(ctx)
|
|
341
459
|
# Note: attach/detach of interfaces handled by Neutron or by passing ports at creation time
|
|
@@ -365,9 +483,23 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
365
483
|
def update_resource(
|
|
366
484
|
self, ctx: HandlerContext, changes: dict, resource: VirtualMachine
|
|
367
485
|
) -> None:
|
|
368
|
-
|
|
486
|
+
|
|
487
|
+
if self.server.status == "ERROR":
|
|
488
|
+
self._raise_server_error(
|
|
489
|
+
ctx, "Server is in ERROR state; delete it to allow redeployment"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
self.validate_allowed_fields(
|
|
493
|
+
changes, {"security_groups", "key_name", "flavor", "flavor_spec"}
|
|
494
|
+
)
|
|
369
495
|
|
|
370
496
|
self._ensure_key(ctx, resource)
|
|
497
|
+
|
|
498
|
+
# A change to either the flavor name or, for a flavor-linked VM, the flavor
|
|
499
|
+
# resources both translate to an in-place resize to the desired flavor.
|
|
500
|
+
if "flavor" in changes or "flavor_spec" in changes:
|
|
501
|
+
self._resize_server(ctx, resource, resource.flavor)
|
|
502
|
+
|
|
371
503
|
if "security_groups" in changes:
|
|
372
504
|
current = set(changes["security_groups"]["current"])
|
|
373
505
|
desired = set(changes["security_groups"]["desired"])
|
|
@@ -350,6 +350,10 @@ entity HostPort extends Port:
|
|
|
350
350
|
A port attached to a VM
|
|
351
351
|
|
|
352
352
|
:param name: The name of the host port.
|
|
353
|
+
:param address: An optional fixed IPv4 to request. When omitted neutron assigns one; leave unset
|
|
354
|
+
for an IPv6-only port. Ignored when dhcp is true.
|
|
355
|
+
:param address_v6: An optional fixed IPv6 to request from an IPv6 subnet on the port's network.
|
|
356
|
+
When omitted no IPv6 is requested. Ignored when dhcp is true.
|
|
353
357
|
:param portsecurity: Enable or disable port security (security groups and spoofing filters)
|
|
354
358
|
:param dhcp: Enable dhcp for this port or not for this port
|
|
355
359
|
:param port_index: The index of the port. This determines the order of the interfaces on the virtual machine. 0 means no specific order.
|
|
@@ -357,7 +361,8 @@ entity HostPort extends Port:
|
|
|
357
361
|
speed up deployments, the handler can retry this number of times before skipping the resource.
|
|
358
362
|
:param wait: The number of seconds to wait between retries.
|
|
359
363
|
"""
|
|
360
|
-
std::ipv4_address address
|
|
364
|
+
std::ipv4_address? address = null
|
|
365
|
+
std::ipv6_address? address_v6 = null
|
|
361
366
|
bool portsecurity=true
|
|
362
367
|
bool dhcp=true
|
|
363
368
|
int port_index=0
|
|
@@ -376,7 +381,7 @@ end
|
|
|
376
381
|
implement HostPort using providerRequire
|
|
377
382
|
implement HostPort using noSecurityGroupsWhenPortSecurityDisabled when not self.portsecurity
|
|
378
383
|
|
|
379
|
-
HostPort.subnet [1] --
|
|
384
|
+
HostPort.subnet [1] -- SubnetBase.host_ports [0:]
|
|
380
385
|
HostPort.vm [1] -- VirtualMachine.ports [0:]
|
|
381
386
|
|
|
382
387
|
index HostPort(provider, vm, name)
|
|
@@ -510,9 +515,16 @@ entity NetworkPort extends Port:
|
|
|
510
515
|
:attr portsecurity: Enable or disable port security (security groups and spoofing filters).
|
|
511
516
|
:attr address: An optional fixed ip address for the port. When omitted, neutron
|
|
512
517
|
assigns one from the network. When set, a subnet must be provided.
|
|
518
|
+
:attr mac_address: An optional MAC address for the port. When omitted, neutron
|
|
519
|
+
assigns one. It is typed as a plain string (not the ``mac_addr``
|
|
520
|
+
typedef) so it can hold a fact reference resolved at deploy time,
|
|
521
|
+
e.g. ``future::std::create_fact_reference(parent_port, "mac_address")``
|
|
522
|
+
to make a trunk sub-port share its parent port's MAC. Set only at
|
|
523
|
+
creation; neutron does not allow changing it afterwards.
|
|
513
524
|
"""
|
|
514
525
|
bool portsecurity=true
|
|
515
526
|
std::ipv4_address? address=null
|
|
527
|
+
string? mac_address=null
|
|
516
528
|
end
|
|
517
529
|
|
|
518
530
|
index NetworkPort(project, name)
|
|
@@ -713,14 +725,25 @@ entity VirtualMachine extends OpenStackResource, VMAttributes:
|
|
|
713
725
|
string name
|
|
714
726
|
end
|
|
715
727
|
|
|
716
|
-
implement VirtualMachine using providerRequire
|
|
717
|
-
|
|
718
728
|
index VirtualMachine(provider, name)
|
|
719
729
|
|
|
720
730
|
VirtualMachine.eth0_port [1] -- HostPort
|
|
721
731
|
VirtualMachine.key_pair [1] -- ssh::Key
|
|
722
732
|
VirtualMachine.project [1] -- Project
|
|
723
733
|
VirtualMachine.provider [1] -- Provider.virtual_machines [0:]
|
|
734
|
+
# Optional link to the managed Flavor backing this VM. When set, the flavor name
|
|
735
|
+
# is derived from it and the handler compares the flavor's resources against what
|
|
736
|
+
# the VM is actually running, so a flavor recreated with different resources under
|
|
737
|
+
# the same name is detected as drift and applied as an in-place resize.
|
|
738
|
+
VirtualMachine.flavor_rel [0:1] -- Flavor
|
|
739
|
+
|
|
740
|
+
implementation flavorFromRelation for VirtualMachine:
|
|
741
|
+
self.flavor = self.flavor_rel.name
|
|
742
|
+
self.requires += self.flavor_rel
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
implement VirtualMachine using providerRequire
|
|
746
|
+
implement VirtualMachine using flavorFromRelation when flavor_rel is defined
|
|
724
747
|
|
|
725
748
|
entity Host extends std::Host, VMAttributes:
|
|
726
749
|
"""
|
|
@@ -35,12 +35,13 @@ class NetworkPort(OpenstackResource):
|
|
|
35
35
|
A standalone neutron port, not bound to a router or a virtual machine
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
fields = ("name", "portsecurity", "address", "network", "subnet")
|
|
38
|
+
fields = ("name", "portsecurity", "address", "network", "subnet", "mac_address")
|
|
39
39
|
name: str
|
|
40
40
|
portsecurity: bool
|
|
41
41
|
address: Optional[str]
|
|
42
42
|
network: str
|
|
43
43
|
subnet: Optional[str]
|
|
44
|
+
mac_address: Optional[str]
|
|
44
45
|
|
|
45
46
|
@staticmethod
|
|
46
47
|
def get_network(_, port):
|
|
@@ -96,6 +97,14 @@ class NetworkPortHandler(OpenStackHandler):
|
|
|
96
97
|
resource.address = None
|
|
97
98
|
resource.subnet = None
|
|
98
99
|
|
|
100
|
+
if resource.mac_address:
|
|
101
|
+
# A MAC was requested (set only at creation, see create_resource);
|
|
102
|
+
# reflect what neutron has so an already-correct port shows no diff.
|
|
103
|
+
resource.mac_address = self.port.mac_address
|
|
104
|
+
else:
|
|
105
|
+
# No MAC is managed; leave neutron's auto-assigned MAC untouched.
|
|
106
|
+
resource.mac_address = None
|
|
107
|
+
|
|
99
108
|
self._set_facts(ctx)
|
|
100
109
|
|
|
101
110
|
def create_resource(
|
|
@@ -123,6 +132,11 @@ class NetworkPortHandler(OpenStackHandler):
|
|
|
123
132
|
{"subnet_id": subnet.id, "ip_address": resource.address}
|
|
124
133
|
]
|
|
125
134
|
|
|
135
|
+
if resource.mac_address:
|
|
136
|
+
# Pin the port MAC (e.g. to a trunk parent port's MAC) so a sub-port
|
|
137
|
+
# sharing the parent MAC is not dropped by port security as spoofed.
|
|
138
|
+
port_attrs["mac_address"] = resource.mac_address
|
|
139
|
+
|
|
126
140
|
self.port = self._connection.network.create_port(**port_attrs)
|
|
127
141
|
if self.port is None:
|
|
128
142
|
raise Exception("Unable to create port.")
|