inmanta-module-openstack 6.1.0__tar.gz → 6.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.
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/PKG-INFO +1 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/PKG-INFO +1 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/SOURCES.txt +7 -3
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/common/base.py +21 -6
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/compute/flavor.py +113 -3
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/compute/virtual_machine.py +120 -68
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/model/_init.cf +35 -2
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/network_port.py +51 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/router.py +15 -5
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/setup.cfg +1 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/setup.cfg +1 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_block_devices.py +1 -1
- inmanta_module_openstack-6.1.2/tests/test_common_base.py +66 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_flavor.py +329 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_host_aggregate.py +3 -3
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_image.py +5 -31
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_keystone.py +1 -1
- inmanta_module_openstack-6.1.2/tests/test_network.py +366 -0
- inmanta_module_openstack-6.1.2/tests/test_network_port.py +684 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_network_port_fixed_ips.py +1 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_network_port_security_groups.py +1 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_nova.py +37 -7
- inmanta_module_openstack-6.1.2/tests/test_qos_policy.py +156 -0
- inmanta_module_openstack-6.1.2/tests/test_router.py +263 -0
- inmanta_module_openstack-6.1.2/tests/test_security_group.py +406 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_security_group_ethertype.py +2 -2
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_server_group.py +114 -88
- inmanta_module_openstack-6.1.2/tests/test_subnet.py +378 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_trunk.py +3 -2
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_vm_boot_nic.py +127 -96
- inmanta_module_openstack-6.1.0/tests/test_network_port_missing_network.py +0 -176
- inmanta_module_openstack-6.1.0/tests/test_network_port_shared_network.py +0 -263
- inmanta_module_openstack-6.1.0/tests/test_neutron.py +0 -1633
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/MANIFEST.in +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/README.md +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/dependency_links.txt +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/not-zip-safe +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/requires.txt +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/top_level.txt +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/common/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/common/deps.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/compute/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/compute/host_aggregate.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/compute/server_group.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/group.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/group_role.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/keystone_base.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/project.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/role.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/user.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/image/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/image/image.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/floating_ip.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/network.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/qos_policy.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/router_port.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/security_group.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/subnet.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/subnet_v6.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/trunk_network.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/trunk_port.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/quota/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/quota/quota.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/pyproject.toml +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_dependency_handling.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_examples.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.2}/tests/test_quota.py +0 -0
|
@@ -43,20 +43,24 @@ inmanta_plugins/openstack/network/trunk_port.py
|
|
|
43
43
|
inmanta_plugins/openstack/quota/__init__.py
|
|
44
44
|
inmanta_plugins/openstack/quota/quota.py
|
|
45
45
|
tests/test_block_devices.py
|
|
46
|
+
tests/test_common_base.py
|
|
46
47
|
tests/test_dependency_handling.py
|
|
47
48
|
tests/test_examples.py
|
|
48
49
|
tests/test_flavor.py
|
|
49
50
|
tests/test_host_aggregate.py
|
|
50
51
|
tests/test_image.py
|
|
51
52
|
tests/test_keystone.py
|
|
53
|
+
tests/test_network.py
|
|
54
|
+
tests/test_network_port.py
|
|
52
55
|
tests/test_network_port_fixed_ips.py
|
|
53
|
-
tests/test_network_port_missing_network.py
|
|
54
56
|
tests/test_network_port_security_groups.py
|
|
55
|
-
tests/test_network_port_shared_network.py
|
|
56
|
-
tests/test_neutron.py
|
|
57
57
|
tests/test_nova.py
|
|
58
|
+
tests/test_qos_policy.py
|
|
58
59
|
tests/test_quota.py
|
|
60
|
+
tests/test_router.py
|
|
61
|
+
tests/test_security_group.py
|
|
59
62
|
tests/test_security_group_ethertype.py
|
|
60
63
|
tests/test_server_group.py
|
|
64
|
+
tests/test_subnet.py
|
|
61
65
|
tests/test_trunk.py
|
|
62
66
|
tests/test_vm_boot_nic.py
|
|
@@ -175,15 +175,19 @@ class OpenStackHandler(CRUDHandler):
|
|
|
175
175
|
"""
|
|
176
176
|
Retrieve the id of a project based on the given name
|
|
177
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
|
+
|
|
178
184
|
project = self._connection.identity.find_project(name)
|
|
179
185
|
if project:
|
|
180
186
|
return project.id
|
|
181
187
|
|
|
182
188
|
# fallback for non-admins: if we’re scoped to a project and its name matches
|
|
183
|
-
if
|
|
184
|
-
current = self._connection.identity.get_project(
|
|
185
|
-
self._connection.current_project_id
|
|
186
|
-
)
|
|
189
|
+
if current_project_id:
|
|
190
|
+
current = self._connection.identity.get_project(current_project_id)
|
|
187
191
|
if current and current.name == name:
|
|
188
192
|
return current.id
|
|
189
193
|
|
|
@@ -342,19 +346,30 @@ class OpenStackHandler(CRUDHandler):
|
|
|
342
346
|
return None
|
|
343
347
|
return self._connection.identity.find_domain(domain_name)
|
|
344
348
|
|
|
345
|
-
def get_server_by_name(self, name, project_id=None):
|
|
349
|
+
def get_server_by_name(self, name, project_id=None, all_projects=False):
|
|
346
350
|
"""
|
|
347
351
|
Retrieve a server by name
|
|
348
352
|
"""
|
|
349
353
|
additional_args = {}
|
|
350
354
|
if project_id is not None:
|
|
351
|
-
additional_args["all_projects"] = True
|
|
352
355
|
additional_args["project_id"] = project_id
|
|
356
|
+
if all_projects:
|
|
357
|
+
additional_args["all_projects"] = True
|
|
353
358
|
# regex for exact match
|
|
354
359
|
servers = self._connection.compute.servers(name=f"^{name}$", **additional_args)
|
|
355
360
|
|
|
356
361
|
return self.require_single_item(servers)
|
|
357
362
|
|
|
363
|
+
def get_project_server(self, resource, name, project_id):
|
|
364
|
+
"""
|
|
365
|
+
Retrieve a server by name in the project of the resource
|
|
366
|
+
"""
|
|
367
|
+
# Nova lists only the servers of the token's own project. Servers of another
|
|
368
|
+
# project need all_projects, the project_id filter is ignored without it.
|
|
369
|
+
if resource.project == resource.admin_project_name:
|
|
370
|
+
return self.get_server_by_name(name)
|
|
371
|
+
return self.get_server_by_name(name, project_id, all_projects=True)
|
|
372
|
+
|
|
358
373
|
def _get_qos_policy(self, **query_params):
|
|
359
374
|
try:
|
|
360
375
|
policies = self._connection.network.qos_policies(**query_params)
|
|
@@ -19,7 +19,7 @@ Contact: code@inmanta.com
|
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
21
|
from inmanta.agent import handler
|
|
22
|
-
from inmanta.agent.handler import ResourcePurged, cache, provider
|
|
22
|
+
from inmanta.agent.handler import ResourcePurged, SkipResource, cache, provider
|
|
23
23
|
from inmanta.resources import resource
|
|
24
24
|
from openstack.compute.v2.flavor import Flavor as SdkFlavor
|
|
25
25
|
|
|
@@ -47,6 +47,7 @@ class Flavor(OpenstackAdminResource):
|
|
|
47
47
|
"rxtx_factor",
|
|
48
48
|
"is_public",
|
|
49
49
|
"description",
|
|
50
|
+
"allowed_projects",
|
|
50
51
|
"extra_specs",
|
|
51
52
|
)
|
|
52
53
|
name: str
|
|
@@ -60,6 +61,17 @@ class Flavor(OpenstackAdminResource):
|
|
|
60
61
|
is_public: bool
|
|
61
62
|
description: str | None
|
|
62
63
|
extra_specs: dict[str, Any] | None
|
|
64
|
+
allowed_projects: list[dict[str, str]]
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def get_allowed_projects(_, obj):
|
|
68
|
+
return sorted(
|
|
69
|
+
[
|
|
70
|
+
{"name": project.name, "domain": project.domain}
|
|
71
|
+
for project in obj.allowed_projects
|
|
72
|
+
],
|
|
73
|
+
key=lambda project: (project["domain"], project["name"]),
|
|
74
|
+
)
|
|
63
75
|
|
|
64
76
|
@staticmethod
|
|
65
77
|
def get_extra_specs(_, obj):
|
|
@@ -72,7 +84,7 @@ class Flavor(OpenstackAdminResource):
|
|
|
72
84
|
class FlavorHandler(OpenStackHandler):
|
|
73
85
|
# Fields Nova can change on an existing flavor. Any other change requires
|
|
74
86
|
# deleting and recreating it.
|
|
75
|
-
IN_PLACE_FIELDS = {"description", "extra_specs"}
|
|
87
|
+
IN_PLACE_FIELDS = {"description", "extra_specs", "allowed_projects"}
|
|
76
88
|
|
|
77
89
|
# Set by read_resource, which raises ResourcePurged when the flavor is gone,
|
|
78
90
|
# so the other handler methods can rely on it being there.
|
|
@@ -163,14 +175,17 @@ class FlavorHandler(OpenStackHandler):
|
|
|
163
175
|
# value in place keeps whatever is on the flavor out of the diff.
|
|
164
176
|
if resource.description is not None:
|
|
165
177
|
resource.description = self.flavor.description or ""
|
|
178
|
+
resource.allowed_projects = self._current_allowed_projects(resource)
|
|
166
179
|
if resource.extra_specs is not None:
|
|
167
180
|
resource.extra_specs = self.flavor.extra_specs
|
|
168
181
|
self._set_facts(ctx)
|
|
169
182
|
|
|
170
183
|
def create_resource(self, ctx: handler.HandlerContext, resource: Flavor) -> None:
|
|
184
|
+
desired_project_ids = self._desired_project_ids(resource)
|
|
171
185
|
self.flavor = self._create_flavor(
|
|
172
186
|
resource, resource.description, resource.extra_specs
|
|
173
187
|
)
|
|
188
|
+
self._update_allowed_projects(desired_project_ids)
|
|
174
189
|
self._set_facts(ctx)
|
|
175
190
|
ctx.set_created()
|
|
176
191
|
|
|
@@ -187,6 +202,7 @@ class FlavorHandler(OpenStackHandler):
|
|
|
187
202
|
"""Update flavor parameters.
|
|
188
203
|
|
|
189
204
|
Nova lets us change the description and the extra specs on an existing
|
|
205
|
+
flavor. Project access can also be reconciled without recreating the
|
|
190
206
|
flavor. The sizing fields are immutable, so a change to any of those is
|
|
191
207
|
applied by deleting the existing flavor and recreating it. That is only
|
|
192
208
|
safe when OpenStack assigns the id: an explicit flavor_id would collide
|
|
@@ -196,7 +212,10 @@ class FlavorHandler(OpenStackHandler):
|
|
|
196
212
|
never updated, and a recreate carries the live values over rather than
|
|
197
213
|
dropping them.
|
|
198
214
|
"""
|
|
199
|
-
|
|
215
|
+
recreate_flavor = bool(set(changes) - self.IN_PLACE_FIELDS)
|
|
216
|
+
desired_project_ids = self._desired_project_ids(resource)
|
|
217
|
+
|
|
218
|
+
if not recreate_flavor:
|
|
200
219
|
if "description" in changes:
|
|
201
220
|
self._connection.compute.update_flavor(
|
|
202
221
|
self.flavor, description=resource.description
|
|
@@ -204,6 +223,8 @@ class FlavorHandler(OpenStackHandler):
|
|
|
204
223
|
# Unmanaged fields never end up in changes, so both are set here.
|
|
205
224
|
if "extra_specs" in changes and resource.extra_specs is not None:
|
|
206
225
|
self._update_extra_specs(resource.extra_specs)
|
|
226
|
+
if "allowed_projects" in changes:
|
|
227
|
+
self._update_allowed_projects(desired_project_ids)
|
|
207
228
|
ctx.set_updated()
|
|
208
229
|
return
|
|
209
230
|
|
|
@@ -232,9 +253,98 @@ class FlavorHandler(OpenStackHandler):
|
|
|
232
253
|
)
|
|
233
254
|
self._delete_flavor()
|
|
234
255
|
self.flavor = self._create_flavor(resource, description, extra_specs)
|
|
256
|
+
self._update_allowed_projects(desired_project_ids)
|
|
235
257
|
self._set_facts(ctx)
|
|
236
258
|
ctx.set_updated()
|
|
237
259
|
|
|
260
|
+
def _flavor_accesses(self) -> list[dict[str, str]]:
|
|
261
|
+
if self.flavor.is_public:
|
|
262
|
+
return []
|
|
263
|
+
return list(self._connection.compute.get_flavor_access(self.flavor))
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def _project_id_from_access(access: dict[str, str]) -> str | None:
|
|
267
|
+
return access.get("tenant_id") or access.get("project_id")
|
|
268
|
+
|
|
269
|
+
def _get_project(self, project: dict[str, str]):
|
|
270
|
+
domain_name = project["domain"]
|
|
271
|
+
domain = self._connection.identity.find_domain(domain_name)
|
|
272
|
+
if domain is None:
|
|
273
|
+
raise SkipResource(f"The domain {domain_name} does not exist.")
|
|
274
|
+
|
|
275
|
+
project_name = project["name"]
|
|
276
|
+
openstack_project = self._connection.identity.find_project(
|
|
277
|
+
project_name,
|
|
278
|
+
domain_id=domain.id,
|
|
279
|
+
)
|
|
280
|
+
if openstack_project is None:
|
|
281
|
+
raise SkipResource(
|
|
282
|
+
f"The project {project_name} does not exist in domain {domain_name}."
|
|
283
|
+
)
|
|
284
|
+
return openstack_project
|
|
285
|
+
|
|
286
|
+
def _current_allowed_projects(self, resource: Flavor) -> list[dict[str, str]]:
|
|
287
|
+
if resource.is_public:
|
|
288
|
+
return []
|
|
289
|
+
|
|
290
|
+
projects = []
|
|
291
|
+
for access in self._flavor_accesses():
|
|
292
|
+
project_id = self._project_id_from_access(access)
|
|
293
|
+
if project_id is None:
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
project = self._connection.identity.find_project(
|
|
297
|
+
project_id, ignore_missing=True
|
|
298
|
+
)
|
|
299
|
+
if project is None:
|
|
300
|
+
# Nova keeps the access row when the project is deleted in Keystone.
|
|
301
|
+
# Report it as an unknown grant so the update revokes it.
|
|
302
|
+
projects.append({"name": project_id, "domain": ""})
|
|
303
|
+
continue
|
|
304
|
+
domain = self._connection.identity.find_domain(
|
|
305
|
+
project.domain_id, ignore_missing=True
|
|
306
|
+
)
|
|
307
|
+
projects.append(
|
|
308
|
+
{"name": project.name, "domain": domain.name if domain else ""}
|
|
309
|
+
)
|
|
310
|
+
return sorted(
|
|
311
|
+
projects, key=lambda project: (project["domain"], project["name"])
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def _desired_project_ids(self, resource: Flavor) -> set[str]:
|
|
315
|
+
"""Resolve every desired project before touching the flavor, so an
|
|
316
|
+
unresolvable project skips the resource instead of leaving it half
|
|
317
|
+
applied (flavor created/recreated but with a partial access list).
|
|
318
|
+
"""
|
|
319
|
+
if resource.is_public and resource.allowed_projects:
|
|
320
|
+
raise ValueError(
|
|
321
|
+
"Project access can only be configured on a private flavor."
|
|
322
|
+
)
|
|
323
|
+
return {self._get_project(project).id for project in resource.allowed_projects}
|
|
324
|
+
|
|
325
|
+
def _update_allowed_projects(self, desired_project_ids: set[str]) -> None:
|
|
326
|
+
current_project_ids = {
|
|
327
|
+
project_id
|
|
328
|
+
for access in self._flavor_accesses()
|
|
329
|
+
if (project_id := self._project_id_from_access(access)) is not None
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for project_id in desired_project_ids - current_project_ids:
|
|
333
|
+
if hasattr(self._connection.compute, "flavor_add_tenant_access"):
|
|
334
|
+
self._connection.compute.flavor_add_tenant_access(
|
|
335
|
+
self.flavor, project_id
|
|
336
|
+
)
|
|
337
|
+
else:
|
|
338
|
+
self._connection.compute.add_flavor_access(self.flavor, project_id)
|
|
339
|
+
|
|
340
|
+
for project_id in current_project_ids - desired_project_ids:
|
|
341
|
+
if hasattr(self._connection.compute, "flavor_remove_tenant_access"):
|
|
342
|
+
self._connection.compute.flavor_remove_tenant_access(
|
|
343
|
+
self.flavor, project_id
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
self._connection.compute.remove_flavor_access(self.flavor, project_id)
|
|
347
|
+
|
|
238
348
|
def _set_facts(self, ctx):
|
|
239
349
|
ctx.set_fact("flavor_id", self.flavor.id)
|
|
240
350
|
ctx.set_fact("is_disabled", str(self.flavor.is_disabled))
|
|
@@ -17,7 +17,7 @@ Contact: code@inmanta.com
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import time
|
|
20
|
-
import
|
|
20
|
+
from collections.abc import Callable
|
|
21
21
|
from typing import Any
|
|
22
22
|
|
|
23
23
|
from inmanta import ast
|
|
@@ -30,7 +30,7 @@ from inmanta.agent.handler import (
|
|
|
30
30
|
)
|
|
31
31
|
from inmanta.execute import proxy
|
|
32
32
|
from inmanta.resources import resource
|
|
33
|
-
from openstack.exceptions import NotFoundException
|
|
33
|
+
from openstack.exceptions import NotFoundException
|
|
34
34
|
|
|
35
35
|
from inmanta_plugins.openstack.common.base import (
|
|
36
36
|
NotUniqueException,
|
|
@@ -191,17 +191,15 @@ class VirtualMachine(OpenstackResource):
|
|
|
191
191
|
|
|
192
192
|
@provider("openstack::VirtualMachine", name="openstack")
|
|
193
193
|
class VirtualMachineHandler(OpenStackHandler):
|
|
194
|
-
@cache(timeout=1, cache_none=False)
|
|
195
194
|
def _get_server(self, ctx, resource):
|
|
196
195
|
project_id = self.get_project_id(resource, resource.project)
|
|
197
|
-
server = self.
|
|
198
|
-
if
|
|
199
|
-
ctx.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
196
|
+
server = self.get_project_server(resource, resource.name, project_id)
|
|
197
|
+
if server is None:
|
|
198
|
+
ctx.debug(
|
|
199
|
+
"No server named %(name)s found in project %(project)s.",
|
|
200
|
+
name=resource.name,
|
|
201
|
+
project=resource.project,
|
|
203
202
|
)
|
|
204
|
-
return None
|
|
205
203
|
return server
|
|
206
204
|
|
|
207
205
|
@cache(timeout=10, cache_none=False)
|
|
@@ -298,21 +296,18 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
298
296
|
self._connection.compute.create_keypair(**keypair_args)
|
|
299
297
|
ctx.info(f"Created a new keypair with name {resource.key_name}")
|
|
300
298
|
|
|
301
|
-
def
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return server is not None
|
|
299
|
+
def _bound_ports(self) -> list:
|
|
300
|
+
"""Ports that are still bound to self.server."""
|
|
301
|
+
return list(self._connection.network.ports(device_id=self.server.id))
|
|
305
302
|
|
|
306
|
-
def _raise_server_error(
|
|
307
|
-
|
|
303
|
+
def _raise_server_error(
|
|
304
|
+
self, ctx: HandlerContext, server: Any, log_message: str
|
|
305
|
+
) -> None:
|
|
306
|
+
"""Log the Nova fault of `server` and raise.
|
|
308
307
|
|
|
309
|
-
Used both when a server is found in ERROR state by read_resource and
|
|
310
|
-
when it transitions to ERROR during a build. Re-fetching is necessary
|
|
311
|
-
because the list API used by get_server_by_name may omit the fault body.
|
|
312
308
|
The ERROR-state server must be deleted (manually or via purge) before
|
|
313
309
|
Inmanta will attempt to recreate it.
|
|
314
310
|
"""
|
|
315
|
-
server = self._connection.compute.get_server(self.server.id)
|
|
316
311
|
fault = getattr(server, "fault", None) or {}
|
|
317
312
|
msg = fault.get("message", "unknown error")
|
|
318
313
|
details = fault.get("details", "")
|
|
@@ -320,12 +315,42 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
320
315
|
log_message,
|
|
321
316
|
fault_message=msg,
|
|
322
317
|
fault_details=details,
|
|
323
|
-
server_id=
|
|
318
|
+
server_id=server.id,
|
|
324
319
|
)
|
|
325
320
|
raise Exception(
|
|
326
|
-
f"Server {
|
|
321
|
+
f"Server {server.id} ({server.name}) is in ERROR state: {msg}"
|
|
327
322
|
) from None
|
|
328
323
|
|
|
324
|
+
def _poll_server(self, ctx: HandlerContext, operation: str) -> Any:
|
|
325
|
+
"""Fetch self.server by id, None when it is not found.
|
|
326
|
+
|
|
327
|
+
Raises when Nova left it in ERROR state. An ERROR server that still has
|
|
328
|
+
a task state is left alone, Nova is still working on it.
|
|
329
|
+
"""
|
|
330
|
+
try:
|
|
331
|
+
server = self._connection.compute.get_server(self.server.id)
|
|
332
|
+
except NotFoundException:
|
|
333
|
+
return None
|
|
334
|
+
if server.status == "ERROR" and not server.task_state:
|
|
335
|
+
self._raise_server_error(
|
|
336
|
+
ctx, server, f"VM entered ERROR state during {operation}"
|
|
337
|
+
)
|
|
338
|
+
return server
|
|
339
|
+
|
|
340
|
+
def _wait_until(
|
|
341
|
+
self, condition: Callable[[], bool], timeout: int, interval: int = 2
|
|
342
|
+
) -> bool:
|
|
343
|
+
"""Poll `condition` every `interval` seconds until it returns True.
|
|
344
|
+
|
|
345
|
+
Returns False when `timeout` seconds pass before the condition is met.
|
|
346
|
+
"""
|
|
347
|
+
deadline = time.monotonic() + timeout
|
|
348
|
+
while not condition():
|
|
349
|
+
if time.monotonic() >= deadline:
|
|
350
|
+
return False
|
|
351
|
+
time.sleep(interval)
|
|
352
|
+
return True
|
|
353
|
+
|
|
329
354
|
def _wait_for_status(
|
|
330
355
|
self,
|
|
331
356
|
ctx: HandlerContext,
|
|
@@ -339,40 +364,31 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
339
364
|
and is only used to build the error messages.
|
|
340
365
|
|
|
341
366
|
Nova may briefly return a 404 for the instance while a resize is being
|
|
342
|
-
applied
|
|
343
|
-
resumes until the status is reached or the timeout elapses.
|
|
367
|
+
applied, this is treated as not ready yet.
|
|
344
368
|
"""
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
except ResourceFailure:
|
|
356
|
-
self._raise_server_error(
|
|
357
|
-
ctx, f"VM entered ERROR state during {operation}"
|
|
358
|
-
)
|
|
359
|
-
except ResourceTimeout:
|
|
360
|
-
self._raise_status_timeout(ctx, status, timeout)
|
|
361
|
-
except NotFoundException:
|
|
362
|
-
# The instance is temporarily not retrievable (e.g. mid-resize);
|
|
363
|
-
# give it a moment and keep polling until the deadline.
|
|
364
|
-
time.sleep(2)
|
|
369
|
+
|
|
370
|
+
def has_status() -> bool:
|
|
371
|
+
server = self._poll_server(ctx, operation)
|
|
372
|
+
if server is None:
|
|
373
|
+
return False
|
|
374
|
+
self.server = server
|
|
375
|
+
return server.status == status
|
|
376
|
+
|
|
377
|
+
if not self._wait_until(has_status, timeout):
|
|
378
|
+
self._raise_status_timeout(ctx, status, operation, timeout)
|
|
365
379
|
|
|
366
380
|
def _raise_status_timeout(
|
|
367
|
-
self, ctx: HandlerContext, status: str, timeout: int
|
|
381
|
+
self, ctx: HandlerContext, status: str, operation: str, timeout: int
|
|
368
382
|
) -> None:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
except NotFoundException:
|
|
383
|
+
# Fetch again so the error shows the state at timeout, not the last
|
|
384
|
+
# successful poll.
|
|
385
|
+
server = self._poll_server(ctx, operation)
|
|
386
|
+
if server is None:
|
|
374
387
|
current_status = "not_found"
|
|
375
388
|
task_state = "unknown"
|
|
389
|
+
else:
|
|
390
|
+
current_status = server.status
|
|
391
|
+
task_state = server.task_state
|
|
376
392
|
ctx.error(
|
|
377
393
|
f"Timed out waiting for VM to reach {status}",
|
|
378
394
|
timeout_seconds=timeout,
|
|
@@ -385,6 +401,27 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
385
401
|
f"(status={current_status}, task_state={task_state})"
|
|
386
402
|
) from None
|
|
387
403
|
|
|
404
|
+
def _raise_ports_timeout(self, ctx: HandlerContext, timeout: int) -> None:
|
|
405
|
+
ports = [
|
|
406
|
+
{
|
|
407
|
+
"id": port.id,
|
|
408
|
+
"name": port.name,
|
|
409
|
+
"status": port.status,
|
|
410
|
+
"device_owner": port.device_owner,
|
|
411
|
+
}
|
|
412
|
+
for port in self._bound_ports()
|
|
413
|
+
]
|
|
414
|
+
ctx.error(
|
|
415
|
+
"Timed out waiting for the ports of the deleted VM to be released",
|
|
416
|
+
timeout_seconds=timeout,
|
|
417
|
+
ports=ports,
|
|
418
|
+
server_id=self.server.id,
|
|
419
|
+
)
|
|
420
|
+
raise Exception(
|
|
421
|
+
f"Server {self.server.id} is deleted but its ports are still bound after "
|
|
422
|
+
f"{timeout}s: {', '.join(port['id'] for port in ports)}"
|
|
423
|
+
) from None
|
|
424
|
+
|
|
388
425
|
def _get_server_flavor_name(self, server) -> str:
|
|
389
426
|
"""Return the flavor name of `server`.
|
|
390
427
|
|
|
@@ -436,14 +473,24 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
436
473
|
"""
|
|
437
474
|
This method will check what the status of the give resource is on
|
|
438
475
|
openstack.
|
|
476
|
+
|
|
477
|
+
A server in ERROR state raises, unless it is being deleted or purged.
|
|
439
478
|
"""
|
|
440
479
|
self.server = self._get_server(ctx, resource)
|
|
441
480
|
if self.server is None:
|
|
442
481
|
raise ResourcePurged()
|
|
443
482
|
|
|
444
|
-
|
|
483
|
+
# A server that is being deleted, or that is to be purged, goes away anyway.
|
|
484
|
+
if (
|
|
485
|
+
self.server.status == "ERROR"
|
|
486
|
+
and self.server.task_state != "deleting"
|
|
487
|
+
and not resource.purged
|
|
488
|
+
):
|
|
445
489
|
self._raise_server_error(
|
|
446
|
-
ctx,
|
|
490
|
+
ctx,
|
|
491
|
+
self.server,
|
|
492
|
+
"Server is in ERROR state. Please check the OpenStack console for details. "
|
|
493
|
+
"Deployment cannot continue, please repair the server manually!",
|
|
447
494
|
)
|
|
448
495
|
|
|
449
496
|
resource.purged = False
|
|
@@ -497,6 +544,8 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
497
544
|
metadata=resource.metadata,
|
|
498
545
|
files=resource.personality,
|
|
499
546
|
)
|
|
547
|
+
# With block devices the boot disk, and its image, come from the mapping.
|
|
548
|
+
# image_id is only for booting from an image on local disk.
|
|
500
549
|
if resource.block_devices:
|
|
501
550
|
args["block_device_mapping_v2"] = list(resource.block_devices)
|
|
502
551
|
else:
|
|
@@ -527,32 +576,36 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
527
576
|
|
|
528
577
|
def delete_resource(self, ctx: HandlerContext, resource: VirtualMachine) -> None:
|
|
529
578
|
self._connection.compute.delete_server(self.server, ignore_missing=True)
|
|
579
|
+
timeout = 120
|
|
530
580
|
|
|
531
|
-
def
|
|
532
|
-
|
|
533
|
-
if ports:
|
|
534
|
-
return False
|
|
535
|
-
return self._get_server(ctx, resource) is None
|
|
581
|
+
def is_server_deleted() -> bool:
|
|
582
|
+
return self._poll_server(ctx, "delete") is None
|
|
536
583
|
|
|
537
|
-
#
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
time.sleep(1)
|
|
542
|
-
count += 1
|
|
584
|
+
# Nova releases the ports after the server is gone. A port that is still
|
|
585
|
+
# bound cannot be reused by a new server or by the NetworkPort handler.
|
|
586
|
+
def are_ports_released() -> bool:
|
|
587
|
+
return not self._bound_ports()
|
|
543
588
|
|
|
544
|
-
|
|
545
|
-
|
|
589
|
+
ctx.info("Waiting until server is deleted.")
|
|
590
|
+
if not self._wait_until(is_server_deleted, timeout):
|
|
591
|
+
self._raise_status_timeout(ctx, "DELETED", "delete", timeout)
|
|
592
|
+
if not self._wait_until(are_ports_released, timeout):
|
|
593
|
+
self._raise_ports_timeout(ctx, timeout)
|
|
546
594
|
|
|
595
|
+
ctx.info("Server was deleted.")
|
|
547
596
|
ctx.set_purged()
|
|
548
597
|
|
|
549
598
|
def update_resource(
|
|
550
599
|
self, ctx: HandlerContext, changes: dict, resource: VirtualMachine
|
|
551
600
|
) -> None:
|
|
552
|
-
|
|
601
|
+
# read_resource lets through an ERROR server that is being deleted.
|
|
553
602
|
if self.server.status == "ERROR":
|
|
554
603
|
self._raise_server_error(
|
|
555
|
-
ctx,
|
|
604
|
+
ctx,
|
|
605
|
+
self.server,
|
|
606
|
+
"Server is in ERROR state, thus cannot be updated. "
|
|
607
|
+
"Please check the OpenStack console for details. "
|
|
608
|
+
"Deployment cannot continue, please repair the server manually!",
|
|
556
609
|
)
|
|
557
610
|
|
|
558
611
|
self.validate_allowed_fields(
|
|
@@ -580,7 +633,6 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
580
633
|
self.server, remove_rule
|
|
581
634
|
)
|
|
582
635
|
|
|
583
|
-
self.server = self._get_server(ctx, resource)
|
|
584
636
|
ctx.set_updated()
|
|
585
637
|
|
|
586
638
|
def _set_facts(self, ctx):
|
|
@@ -173,8 +173,8 @@ entity GroupRole extends OpenStackResource:
|
|
|
173
173
|
"""
|
|
174
174
|
string role
|
|
175
175
|
string group_name
|
|
176
|
-
string group_domain="Default"
|
|
177
|
-
string domain=
|
|
176
|
+
string group_domain = "Default"
|
|
177
|
+
string? domain = null
|
|
178
178
|
end
|
|
179
179
|
index GroupRole(project, group_name, group_domain, role, domain)
|
|
180
180
|
|
|
@@ -865,6 +865,7 @@ entity Flavor extends OpenStackResource:
|
|
|
865
865
|
:attr description: A free-form description of the flavor. Unlike the other
|
|
866
866
|
attributes, this one can be updated in place, without recreating the flavor.
|
|
867
867
|
Leave it null to not manage the description at all.
|
|
868
|
+
:attr allowed_projects: Projects that can access this flavor when is_public is false
|
|
868
869
|
:attr extra_specs: Set extra specs on a flavor. See https://docs.openstack.org/nova/rocky/admin/flavors.html
|
|
869
870
|
This set is managed exhaustively: a spec present on the flavor but absent
|
|
870
871
|
here is removed, so the default of an empty dict clears every spec. Set it
|
|
@@ -887,6 +888,38 @@ end
|
|
|
887
888
|
|
|
888
889
|
implement Flavor using providerRequire
|
|
889
890
|
|
|
891
|
+
Flavor.allowed_projects [0:] -- Project.allowed_flavors [0:]
|
|
892
|
+
"""
|
|
893
|
+
:rel allowed_projects: The projects that may use this flavor. Only meaningful when
|
|
894
|
+
``is_public`` is false; setting it on a public flavor is an error, since Nova has
|
|
895
|
+
no access list for public flavors. The list is managed exhaustively: a grant on
|
|
896
|
+
the flavor but absent here is revoked, so the default of an empty relation clears
|
|
897
|
+
every grant.
|
|
898
|
+
"""
|
|
899
|
+
|
|
900
|
+
implementation flavorProjectAccessDependencies for Flavor:
|
|
901
|
+
for project in self.allowed_projects:
|
|
902
|
+
std::assert(
|
|
903
|
+
project.provider == self.provider,
|
|
904
|
+
"Flavor '{{name}}' is on provider '{{provider.name}}' but allowed project '{{project.name}}' is on another provider."
|
|
905
|
+
)
|
|
906
|
+
if not self.purged and not project.purged:
|
|
907
|
+
self.requires += project
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
implement Flavor using flavorProjectAccessDependencies
|
|
913
|
+
|
|
914
|
+
implementation flavorProjectAccessOnPrivateOnly for Flavor:
|
|
915
|
+
std::assert(
|
|
916
|
+
self.is_public == false or std::count(self.allowed_projects) == 0,
|
|
917
|
+
"Flavor '{{name}}' is public, so it cannot have allowed_projects: Nova has no access list for public flavors."
|
|
918
|
+
)
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
implement Flavor using flavorProjectAccessOnPrivateOnly
|
|
922
|
+
|
|
890
923
|
index HostAggregateMetadata(provider, aggregate, key)
|
|
891
924
|
HostAggregateMetadata.provider [1] -- Provider.host_aggregate_metadata [0:]
|
|
892
925
|
entity HostAggregateMetadata extends OpenStackResource:
|