inmanta-module-openstack 6.1.0__tar.gz → 6.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/PKG-INFO +1 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/PKG-INFO +1 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/SOURCES.txt +1 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/common/base.py +11 -6
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/compute/flavor.py +113 -3
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/compute/virtual_machine.py +18 -11
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/model/_init.cf +35 -2
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/network_port.py +56 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/setup.cfg +1 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/setup.cfg +1 -1
- inmanta_module_openstack-6.1.1/tests/test_common_base.py +60 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_flavor.py +329 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_network_port_missing_network.py +1 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_network_port_shared_network.py +1 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_neutron.py +12 -5
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_nova.py +25 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_server_group.py +1 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_trunk.py +2 -1
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_vm_boot_nic.py +1 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/MANIFEST.in +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/README.md +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/dependency_links.txt +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/not-zip-safe +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/requires.txt +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_module_openstack.egg-info/top_level.txt +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/common/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/common/deps.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/compute/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/compute/host_aggregate.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/compute/server_group.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/group.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/group_role.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/keystone_base.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/project.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/role.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/identity/user.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/image/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/image/image.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/floating_ip.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/network.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/qos_policy.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/router.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/router_port.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/security_group.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/subnet.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/subnet_v6.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/trunk_network.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/network/trunk_port.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/quota/__init__.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/inmanta_plugins/openstack/quota/quota.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/pyproject.toml +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_block_devices.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_dependency_handling.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_examples.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_host_aggregate.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_image.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_keystone.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_network_port_fixed_ips.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_network_port_security_groups.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_quota.py +0 -0
- {inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_security_group_ethertype.py +0 -0
|
@@ -43,6 +43,7 @@ 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
|
|
@@ -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,14 +346,15 @@ 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
|
|
|
@@ -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,6 @@ Contact: code@inmanta.com
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import time
|
|
20
|
-
import traceback
|
|
21
20
|
from typing import Any
|
|
22
21
|
|
|
23
22
|
from inmanta import ast
|
|
@@ -193,15 +192,17 @@ class VirtualMachine(OpenstackResource):
|
|
|
193
192
|
class VirtualMachineHandler(OpenStackHandler):
|
|
194
193
|
@cache(timeout=1, cache_none=False)
|
|
195
194
|
def _get_server(self, ctx, resource):
|
|
195
|
+
if resource.project == resource.admin_project_name:
|
|
196
|
+
return self.get_server_by_name(resource.name)
|
|
197
|
+
|
|
196
198
|
project_id = self.get_project_id(resource, resource.project)
|
|
197
|
-
server = self.get_server_by_name(resource.name, project_id)
|
|
198
|
-
if
|
|
199
|
-
ctx.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
server = self.get_server_by_name(resource.name, project_id, all_projects=True)
|
|
200
|
+
if server is None:
|
|
201
|
+
ctx.debug(
|
|
202
|
+
"No server named %(name)s found in project %(project)s.",
|
|
203
|
+
name=resource.name,
|
|
204
|
+
project=resource.project,
|
|
203
205
|
)
|
|
204
|
-
return None
|
|
205
206
|
return server
|
|
206
207
|
|
|
207
208
|
@cache(timeout=10, cache_none=False)
|
|
@@ -299,8 +300,13 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
299
300
|
ctx.info(f"Created a new keypair with name {resource.key_name}")
|
|
300
301
|
|
|
301
302
|
def _server_with_name_exists(self, resource):
|
|
302
|
-
|
|
303
|
-
|
|
303
|
+
if resource.project == resource.admin_project_name:
|
|
304
|
+
server = self.get_server_by_name(resource.name)
|
|
305
|
+
else:
|
|
306
|
+
project_id = self.get_project_id(resource, resource.project)
|
|
307
|
+
server = self.get_server_by_name(
|
|
308
|
+
resource.name, project_id, all_projects=True
|
|
309
|
+
)
|
|
304
310
|
return server is not None
|
|
305
311
|
|
|
306
312
|
def _raise_server_error(self, ctx: HandlerContext, log_message: str) -> None:
|
|
@@ -441,7 +447,8 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
441
447
|
if self.server is None:
|
|
442
448
|
raise ResourcePurged()
|
|
443
449
|
|
|
444
|
-
|
|
450
|
+
# Raise error always except when the resource is being purged.
|
|
451
|
+
if self.server.status == "ERROR" and not resource.purged:
|
|
445
452
|
self._raise_server_error(
|
|
446
453
|
ctx, "Server is in ERROR state; delete it to allow redeployment"
|
|
447
454
|
)
|
|
@@ -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:
|
|
@@ -224,7 +224,12 @@ class NetworkPortHandler(OpenStackHandler):
|
|
|
224
224
|
tries = 0
|
|
225
225
|
max_attempts = resource.retries if resource.retries > 0 else 1
|
|
226
226
|
while tries < max_attempts:
|
|
227
|
-
|
|
227
|
+
cross_project = resource.project != resource.admin_project_name
|
|
228
|
+
vm = self.get_server_by_name(
|
|
229
|
+
resource.host,
|
|
230
|
+
project_id if cross_project else None,
|
|
231
|
+
all_projects=cross_project,
|
|
232
|
+
)
|
|
228
233
|
if vm is None:
|
|
229
234
|
return None
|
|
230
235
|
|
|
@@ -322,6 +327,39 @@ class NetworkPortHandler(OpenStackHandler):
|
|
|
322
327
|
resource.name = self.port.name
|
|
323
328
|
self._set_facts(ctx)
|
|
324
329
|
|
|
330
|
+
def calculate_diff(
|
|
331
|
+
self, ctx: handler.HandlerContext, current: NetworkPort, desired: NetworkPort
|
|
332
|
+
) -> dict[str, dict[str, object]]:
|
|
333
|
+
changes = super().calculate_diff(ctx, current, desired)
|
|
334
|
+
|
|
335
|
+
pairs = changes.get("allowed_address_pairs")
|
|
336
|
+
if pairs is not None and self._equivalent_address_pairs(
|
|
337
|
+
current=pairs["current"], desired=pairs["desired"]
|
|
338
|
+
):
|
|
339
|
+
del changes["allowed_address_pairs"]
|
|
340
|
+
|
|
341
|
+
return changes
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def _equivalent_address_pairs(
|
|
345
|
+
*, current: dict[str, Optional[str]], desired: dict[str, Optional[str]]
|
|
346
|
+
) -> bool:
|
|
347
|
+
"""
|
|
348
|
+
Whether two sets of allowed address pairs describe the same desired state.
|
|
349
|
+
|
|
350
|
+
Neutron pins the port's own MAC on a pair that does not carry one, so a pair
|
|
351
|
+
left without a MAC always reads back with one: the MAC of such a pair is not
|
|
352
|
+
managed and any value it holds is equivalent. A pair that does request a MAC
|
|
353
|
+
is compared as is.
|
|
354
|
+
"""
|
|
355
|
+
if current.keys() != desired.keys():
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
return all(
|
|
359
|
+
desired_mac is None or desired_mac == current[address]
|
|
360
|
+
for address, desired_mac in desired.items()
|
|
361
|
+
)
|
|
362
|
+
|
|
325
363
|
def create_resource(
|
|
326
364
|
self, ctx: handler.HandlerContext, resource: NetworkPort
|
|
327
365
|
) -> None:
|
|
@@ -455,6 +493,23 @@ class NetworkPortHandler(OpenStackHandler):
|
|
|
455
493
|
changes: dict,
|
|
456
494
|
resource: NetworkPort,
|
|
457
495
|
) -> None:
|
|
496
|
+
if "mac_address" in changes:
|
|
497
|
+
# Neutron only accepts a MAC address when it creates the port, so a port
|
|
498
|
+
# that is already there with another one cannot be reconciled - it has to be
|
|
499
|
+
# recreated. A port created before its MAC was pinned in the model looks
|
|
500
|
+
# exactly like this. Fail with the reason rather than through the generic
|
|
501
|
+
# unexpected-change check below, and never let the change through: nothing
|
|
502
|
+
# here can apply it, so the port would report an update on every deploy
|
|
503
|
+
# while staying on its original MAC.
|
|
504
|
+
raise ValueError(
|
|
505
|
+
f"Port {resource.name} has MAC address "
|
|
506
|
+
f"{changes['mac_address']['current']} but the model requests "
|
|
507
|
+
f"{changes['mac_address']['desired']}. Neutron assigns a MAC address "
|
|
508
|
+
"when the port is created and does not allow changing it afterwards, "
|
|
509
|
+
"so this port has to be deleted and recreated to take the requested "
|
|
510
|
+
"one."
|
|
511
|
+
)
|
|
512
|
+
|
|
458
513
|
self.validate_allowed_fields(
|
|
459
514
|
changes,
|
|
460
515
|
{
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2026 Inmanta
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: code@inmanta.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from types import SimpleNamespace
|
|
20
|
+
from unittest.mock import MagicMock
|
|
21
|
+
|
|
22
|
+
from inmanta_plugins.openstack.common.base import OpenStackHandler
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _handler(connection) -> OpenStackHandler:
|
|
26
|
+
handler = object.__new__(OpenStackHandler)
|
|
27
|
+
handler._connection = connection
|
|
28
|
+
return handler
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_get_project_id_uses_scoped_project_without_identity_lookup() -> None:
|
|
32
|
+
connection = SimpleNamespace(
|
|
33
|
+
current_project_id="project-id",
|
|
34
|
+
identity=MagicMock(),
|
|
35
|
+
)
|
|
36
|
+
resource = SimpleNamespace(admin_project_name="tenant-project")
|
|
37
|
+
|
|
38
|
+
assert (
|
|
39
|
+
_handler(connection).get_project_id(resource, "tenant-project") == "project-id"
|
|
40
|
+
)
|
|
41
|
+
connection.identity.find_project.assert_not_called()
|
|
42
|
+
connection.identity.get_project.assert_not_called()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_get_project_id_can_still_resolve_another_visible_project() -> None:
|
|
46
|
+
connection = SimpleNamespace(
|
|
47
|
+
current_project_id="current-project-id",
|
|
48
|
+
identity=MagicMock(),
|
|
49
|
+
)
|
|
50
|
+
connection.identity.find_project.return_value = SimpleNamespace(
|
|
51
|
+
id="other-project-id"
|
|
52
|
+
)
|
|
53
|
+
resource = SimpleNamespace(admin_project_name="current-project")
|
|
54
|
+
|
|
55
|
+
assert (
|
|
56
|
+
_handler(connection).get_project_id(resource, "other-project")
|
|
57
|
+
== "other-project-id"
|
|
58
|
+
)
|
|
59
|
+
connection.identity.find_project.assert_called_once_with("other-project")
|
|
60
|
+
connection.identity.get_project.assert_not_called()
|
|
@@ -18,6 +18,7 @@ Contact: code@inmanta.com
|
|
|
18
18
|
|
|
19
19
|
import json
|
|
20
20
|
import os
|
|
21
|
+
import uuid
|
|
21
22
|
|
|
22
23
|
import inmanta
|
|
23
24
|
import pytest
|
|
@@ -56,6 +57,59 @@ def cleanup(os_tester):
|
|
|
56
57
|
cleanup_flavor(os_tester.connection)
|
|
57
58
|
|
|
58
59
|
|
|
60
|
+
@pytest.fixture()
|
|
61
|
+
def flavor_access_projects(os_tester):
|
|
62
|
+
project_domain_name = os.environ["OS_PROJECT_DOMAIN_NAME"]
|
|
63
|
+
projects = (
|
|
64
|
+
os_tester.get_project(f"flavor-access-1-{uuid.uuid4().hex[:8]}"),
|
|
65
|
+
os_tester.get_project(f"flavor-access-2-{uuid.uuid4().hex[:8]}"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
cleanup_flavor(os_tester.connection)
|
|
69
|
+
|
|
70
|
+
yield project_domain_name, projects
|
|
71
|
+
|
|
72
|
+
cleanup_flavor(os_tester.connection)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture()
|
|
76
|
+
def flavor_access_member_user(os_tester, flavor_access_projects):
|
|
77
|
+
_, os_projects = flavor_access_projects
|
|
78
|
+
identity = os_tester.connection.identity
|
|
79
|
+
user_domain_name = os.environ["OS_USER_DOMAIN_NAME"]
|
|
80
|
+
user_domain = identity.find_domain(user_domain_name, ignore_missing=True)
|
|
81
|
+
if user_domain is None:
|
|
82
|
+
pytest.skip(f"The user domain {user_domain_name} does not exist.")
|
|
83
|
+
|
|
84
|
+
role = identity.find_role("member", ignore_missing=True)
|
|
85
|
+
if role is None:
|
|
86
|
+
pytest.skip("The member role does not exist.")
|
|
87
|
+
|
|
88
|
+
user_name = f"test-flavor-access-{uuid.uuid4().hex[:8]}"
|
|
89
|
+
password = f"test-flavor-access-{uuid.uuid4().hex}"
|
|
90
|
+
user = identity.create_user(
|
|
91
|
+
name=user_name,
|
|
92
|
+
password=password,
|
|
93
|
+
is_enabled=True,
|
|
94
|
+
domain_id=user_domain.id,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Use a non-admin tenant user so admin policy does not bypass flavor access.
|
|
98
|
+
for os_project in os_projects:
|
|
99
|
+
identity.assign_project_role_to_user(os_project.project_object, user, role)
|
|
100
|
+
|
|
101
|
+
yield user_name, password
|
|
102
|
+
|
|
103
|
+
for os_project in os_projects:
|
|
104
|
+
try:
|
|
105
|
+
identity.unassign_project_role_from_user(
|
|
106
|
+
os_project.project_object, user, role
|
|
107
|
+
)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
identity.delete_user(user, ignore_missing=True)
|
|
111
|
+
|
|
112
|
+
|
|
59
113
|
def compile_flavor(
|
|
60
114
|
os_tester,
|
|
61
115
|
project,
|
|
@@ -328,3 +382,278 @@ flavor=openstack::Flavor(
|
|
|
328
382
|
assert os_flavor.id == flavor_id
|
|
329
383
|
assert os_flavor.description == "Set on an explicit-id flavor"
|
|
330
384
|
assert os_flavor.extra_specs == {"hw:watchdog_action": "reset"}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def test_flavor_project_access(project, os_tester, flavor_access_projects):
|
|
388
|
+
project_domain_name, os_projects = flavor_access_projects
|
|
389
|
+
project_name_1, project_name_2 = (
|
|
390
|
+
os_project.project_object.name for os_project in os_projects
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def compile_model(allowed_projects):
|
|
394
|
+
os_tester.compile_with_provider(f"""
|
|
395
|
+
project_1 = openstack::Project(
|
|
396
|
+
provider=provider,
|
|
397
|
+
name="{project_name_1}",
|
|
398
|
+
domain="{project_domain_name}",
|
|
399
|
+
description="",
|
|
400
|
+
enabled=true,
|
|
401
|
+
purged=false,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
project_2 = openstack::Project(
|
|
405
|
+
provider=provider,
|
|
406
|
+
name="{project_name_2}",
|
|
407
|
+
domain="{project_domain_name}",
|
|
408
|
+
description="",
|
|
409
|
+
enabled=true,
|
|
410
|
+
purged=false,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
flavor = openstack::Flavor(
|
|
414
|
+
provider=provider,
|
|
415
|
+
name="test-flavor",
|
|
416
|
+
ram=1024,
|
|
417
|
+
vcpus=4,
|
|
418
|
+
disk=10,
|
|
419
|
+
is_public=false,
|
|
420
|
+
allowed_projects={allowed_projects},
|
|
421
|
+
)
|
|
422
|
+
""")
|
|
423
|
+
|
|
424
|
+
def project_ids_for_flavor_access():
|
|
425
|
+
flavor = get_test_flavor(os_tester.connection)
|
|
426
|
+
compute = os_tester.connection.compute
|
|
427
|
+
if hasattr(compute, "get_flavor_access"):
|
|
428
|
+
accesses = compute.get_flavor_access(flavor)
|
|
429
|
+
else:
|
|
430
|
+
accesses = compute.flavor_accesses(flavor)
|
|
431
|
+
|
|
432
|
+
project_ids = set()
|
|
433
|
+
for access in accesses:
|
|
434
|
+
project_id = getattr(access, "tenant_id", None) or getattr(
|
|
435
|
+
access, "project_id", None
|
|
436
|
+
)
|
|
437
|
+
if project_id is None and hasattr(access, "get"):
|
|
438
|
+
project_id = access.get("tenant_id") or access.get("project_id")
|
|
439
|
+
project_ids.add(project_id)
|
|
440
|
+
return project_ids
|
|
441
|
+
|
|
442
|
+
def project_id(name):
|
|
443
|
+
return os_tester.connection.identity.find_project(name).id
|
|
444
|
+
|
|
445
|
+
compile_model("[project_1, project_2]")
|
|
446
|
+
flavor = project.get_resource("openstack::Flavor", name="test-flavor")
|
|
447
|
+
assert flavor.allowed_projects == [
|
|
448
|
+
{"name": project_name_1, "domain": project_domain_name},
|
|
449
|
+
{"name": project_name_2, "domain": project_domain_name},
|
|
450
|
+
]
|
|
451
|
+
|
|
452
|
+
for project_name in (project_name_1, project_name_2):
|
|
453
|
+
project_resource = project.get_resource("openstack::Project", name=project_name)
|
|
454
|
+
assert (
|
|
455
|
+
project.deploy(project_resource).status
|
|
456
|
+
== inmanta.const.ResourceState.deployed
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
assert project.deploy(flavor).status == inmanta.const.ResourceState.deployed
|
|
460
|
+
assert project_ids_for_flavor_access() == {
|
|
461
|
+
project_id(project_name_1),
|
|
462
|
+
project_id(project_name_2),
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
flavor_id = get_test_flavor(os_tester.connection).id
|
|
466
|
+
compile_model("[project_1]")
|
|
467
|
+
flavor = project.get_resource("openstack::Flavor", name="test-flavor")
|
|
468
|
+
assert project.deploy(flavor).status == inmanta.const.ResourceState.deployed
|
|
469
|
+
assert get_test_flavor(os_tester.connection).id == flavor_id
|
|
470
|
+
assert project_ids_for_flavor_access() == {project_id(project_name_1)}
|
|
471
|
+
assert not project.dryrun(flavor).changes
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def test_private_flavor_controls_vm_creation(
|
|
475
|
+
project,
|
|
476
|
+
os_tester,
|
|
477
|
+
flavor_access_projects,
|
|
478
|
+
flavor_access_member_user,
|
|
479
|
+
ssh_public_key,
|
|
480
|
+
):
|
|
481
|
+
project_domain_name, os_projects = flavor_access_projects
|
|
482
|
+
allowed_project, denied_project = os_projects
|
|
483
|
+
allowed_project_name = allowed_project.project_object.name
|
|
484
|
+
denied_project_name = denied_project.project_object.name
|
|
485
|
+
flavor_name = "test-flavor"
|
|
486
|
+
tenant_username, tenant_password = flavor_access_member_user
|
|
487
|
+
|
|
488
|
+
def compile_flavor_model():
|
|
489
|
+
os_tester.compile_with_provider(
|
|
490
|
+
f"""
|
|
491
|
+
project_1 = openstack::Project(
|
|
492
|
+
provider=provider,
|
|
493
|
+
name="{allowed_project_name}",
|
|
494
|
+
domain="{project_domain_name}",
|
|
495
|
+
description="",
|
|
496
|
+
enabled=true,
|
|
497
|
+
purged=false,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
project_2 = openstack::Project(
|
|
501
|
+
provider=provider,
|
|
502
|
+
name="{denied_project_name}",
|
|
503
|
+
domain="{project_domain_name}",
|
|
504
|
+
description="",
|
|
505
|
+
enabled=true,
|
|
506
|
+
purged=false,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
flavor = openstack::Flavor(
|
|
510
|
+
provider=provider,
|
|
511
|
+
name="{flavor_name}",
|
|
512
|
+
ram=1024,
|
|
513
|
+
vcpus=1,
|
|
514
|
+
disk=1,
|
|
515
|
+
is_public=false,
|
|
516
|
+
allowed_projects=[project_1],
|
|
517
|
+
)
|
|
518
|
+
""",
|
|
519
|
+
name="flavor_access_admin",
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def vm_names(os_project):
|
|
523
|
+
return {
|
|
524
|
+
"key": os_project.get_resource_name("flavor-access-key"),
|
|
525
|
+
"network": os_project.get_resource_name("flavor-access-net"),
|
|
526
|
+
"port": os_project.get_resource_name("flavor-access-port"),
|
|
527
|
+
"server": (
|
|
528
|
+
os_project.get_resource_name("flavor-access-server").replace("_", "-")
|
|
529
|
+
),
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
def compile_vm_model(os_project, names, provider_name: str, purged: bool = False):
|
|
533
|
+
purged_value = str(purged).lower()
|
|
534
|
+
os_tester.compile_with_provider(
|
|
535
|
+
f"""
|
|
536
|
+
import unittest
|
|
537
|
+
import ssh
|
|
538
|
+
|
|
539
|
+
tenant_project = openstack::Project(
|
|
540
|
+
provider=provider,
|
|
541
|
+
name=provider.project_name,
|
|
542
|
+
domain="{project_domain_name}",
|
|
543
|
+
description="",
|
|
544
|
+
enabled=true,
|
|
545
|
+
managed=false,
|
|
546
|
+
)
|
|
547
|
+
n = openstack::Network(
|
|
548
|
+
provider=provider,
|
|
549
|
+
name="{names["network"]}",
|
|
550
|
+
project=tenant_project,
|
|
551
|
+
purged={purged_value},
|
|
552
|
+
)
|
|
553
|
+
subnet = openstack::Subnet(
|
|
554
|
+
provider=provider,
|
|
555
|
+
project=tenant_project,
|
|
556
|
+
network=n,
|
|
557
|
+
dhcp=true,
|
|
558
|
+
name="{names["network"]}",
|
|
559
|
+
network_address="10.255.255.0/24",
|
|
560
|
+
purged={purged_value},
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
os = std::OS(name="cirros", version=0.4, family=std::linux)
|
|
564
|
+
key = ssh::Key(name="{names["key"]}", public_key="{ssh_public_key}")
|
|
565
|
+
vm = openstack::VirtualMachine(
|
|
566
|
+
provider=provider,
|
|
567
|
+
project=tenant_project,
|
|
568
|
+
key_pair=key,
|
|
569
|
+
name="{names["server"]}",
|
|
570
|
+
image=openstack::find_image(provider, os),
|
|
571
|
+
flavor="{flavor_name}",
|
|
572
|
+
user_data="",
|
|
573
|
+
purged={purged_value},
|
|
574
|
+
)
|
|
575
|
+
port = openstack::NetworkPort(
|
|
576
|
+
provider=provider,
|
|
577
|
+
project=tenant_project,
|
|
578
|
+
name="{names["port"]}",
|
|
579
|
+
subnet=subnet,
|
|
580
|
+
address="10.255.255.10",
|
|
581
|
+
dhcp=false,
|
|
582
|
+
vm=vm,
|
|
583
|
+
retries=1,
|
|
584
|
+
wait=0,
|
|
585
|
+
purged={purged_value},
|
|
586
|
+
)
|
|
587
|
+
vm.eth0_port = port
|
|
588
|
+
""",
|
|
589
|
+
name=provider_name,
|
|
590
|
+
username=tenant_username,
|
|
591
|
+
password=tenant_password,
|
|
592
|
+
project_name=os_project.project_object.name,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
def deploy(resource_type: str, resource_name: str) -> None:
|
|
596
|
+
project.deploy_resource(resource_type, name=resource_name)
|
|
597
|
+
|
|
598
|
+
allowed_names = vm_names(allowed_project)
|
|
599
|
+
denied_names = vm_names(denied_project)
|
|
600
|
+
|
|
601
|
+
compile_flavor_model()
|
|
602
|
+
for project_name in (allowed_project_name, denied_project_name):
|
|
603
|
+
deploy("openstack::Project", project_name)
|
|
604
|
+
deploy("openstack::Flavor", flavor_name)
|
|
605
|
+
|
|
606
|
+
compile_vm_model(
|
|
607
|
+
denied_project,
|
|
608
|
+
denied_names,
|
|
609
|
+
# The find_image plugin caches by provider name, so each scope gets its own.
|
|
610
|
+
provider_name="flavor_access_denied",
|
|
611
|
+
)
|
|
612
|
+
deploy("openstack::Network", denied_names["network"])
|
|
613
|
+
deploy("openstack::Subnet", denied_names["network"])
|
|
614
|
+
project.deploy_resource(
|
|
615
|
+
"openstack::VirtualMachine",
|
|
616
|
+
name=denied_names["server"],
|
|
617
|
+
status=inmanta.const.ResourceState.failed,
|
|
618
|
+
)
|
|
619
|
+
assert (
|
|
620
|
+
denied_project.connection.compute.find_server(
|
|
621
|
+
denied_names["server"], ignore_missing=True
|
|
622
|
+
)
|
|
623
|
+
is None
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
compile_vm_model(
|
|
627
|
+
allowed_project,
|
|
628
|
+
allowed_names,
|
|
629
|
+
provider_name="flavor_access_allowed",
|
|
630
|
+
)
|
|
631
|
+
deploy("openstack::Network", allowed_names["network"])
|
|
632
|
+
deploy("openstack::Subnet", allowed_names["network"])
|
|
633
|
+
deploy("openstack::VirtualMachine", allowed_names["server"])
|
|
634
|
+
assert (
|
|
635
|
+
allowed_project.connection.compute.find_server(
|
|
636
|
+
allowed_names["server"], ignore_missing=True
|
|
637
|
+
)
|
|
638
|
+
is not None
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
compile_vm_model(
|
|
642
|
+
allowed_project,
|
|
643
|
+
allowed_names,
|
|
644
|
+
provider_name="flavor_access_allowed",
|
|
645
|
+
purged=True,
|
|
646
|
+
)
|
|
647
|
+
deploy("openstack::VirtualMachine", allowed_names["server"])
|
|
648
|
+
deploy("openstack::Subnet", allowed_names["network"])
|
|
649
|
+
deploy("openstack::Network", allowed_names["network"])
|
|
650
|
+
|
|
651
|
+
compile_vm_model(
|
|
652
|
+
denied_project,
|
|
653
|
+
denied_names,
|
|
654
|
+
provider_name="flavor_access_denied",
|
|
655
|
+
purged=True,
|
|
656
|
+
)
|
|
657
|
+
deploy("openstack::VirtualMachine", denied_names["server"])
|
|
658
|
+
deploy("openstack::Subnet", denied_names["network"])
|
|
659
|
+
deploy("openstack::Network", denied_names["network"])
|
|
@@ -73,6 +73,7 @@ def _port_handler(
|
|
|
73
73
|
|
|
74
74
|
port_handler = typing.cast(NetworkPortHandler, project.get_handler(resource, False))
|
|
75
75
|
port_handler._connection = MagicMock()
|
|
76
|
+
port_handler._connection.current_project_id = "project-id"
|
|
76
77
|
port_handler._connection.identity.find_project.return_value = SimpleNamespace(
|
|
77
78
|
id="project-id"
|
|
78
79
|
)
|
|
@@ -88,6 +88,7 @@ def _port_handler(
|
|
|
88
88
|
|
|
89
89
|
port_handler = typing.cast(NetworkPortHandler, project.get_handler(resource, False))
|
|
90
90
|
port_handler._connection = MagicMock()
|
|
91
|
+
port_handler._connection.current_project_id = PROJECT_ID
|
|
91
92
|
port_handler._connection.identity.find_project.return_value = SimpleNamespace(
|
|
92
93
|
id=PROJECT_ID
|
|
93
94
|
)
|
|
@@ -825,7 +825,9 @@ port = openstack::NetworkPort(
|
|
|
825
825
|
assert ports[0].network_id == networks[0].id
|
|
826
826
|
|
|
827
827
|
|
|
828
|
-
def test_shared_network(
|
|
828
|
+
def test_shared_network(
|
|
829
|
+
project: Project, os_tester: OpenstackTester, ssh_public_key: str
|
|
830
|
+
) -> None:
|
|
829
831
|
"""
|
|
830
832
|
Create a shared network as one project and add ports to it as another project
|
|
831
833
|
"""
|
|
@@ -866,7 +868,7 @@ def test_shared_network(project: Project, os_tester: OpenstackTester) -> None:
|
|
|
866
868
|
network_address="10.255.254.0/24", dns_servers=["8.8.8.8", "8.8.4.4"], managed=true)
|
|
867
869
|
|
|
868
870
|
os = std::OS(name="cirros", version=0.4, family=std::linux)
|
|
869
|
-
key = ssh::Key(name="%(key_name)s", public_key="")
|
|
871
|
+
key = ssh::Key(name="%(key_name)s", public_key="%(ssh_public_key)s")
|
|
870
872
|
vm = openstack::Host(provider=provider, project=project, key_pair=key, name="%(server_name)s", os=os,
|
|
871
873
|
ip=std::getfact(vm.vm, "ip_address"),
|
|
872
874
|
image=openstack::find_image(provider, os), flavor=openstack::find_flavor(provider, 1, 0.5),
|
|
@@ -877,6 +879,7 @@ def test_shared_network(project: Project, os_tester: OpenstackTester) -> None:
|
|
|
877
879
|
% {
|
|
878
880
|
"net_name": net_name,
|
|
879
881
|
"key_name": key_name,
|
|
882
|
+
"ssh_public_key": ssh_public_key,
|
|
880
883
|
"server_name": server_name,
|
|
881
884
|
},
|
|
882
885
|
project_name=project2._project_name,
|
|
@@ -908,7 +911,7 @@ def test_shared_network(project: Project, os_tester: OpenstackTester) -> None:
|
|
|
908
911
|
network_address="10.255.253.0/24", dns_servers=["8.8.8.8", "8.8.4.4"], managed=true)
|
|
909
912
|
|
|
910
913
|
os = std::OS(name="cirros", version=0.4, family=std::linux)
|
|
911
|
-
key = ssh::Key(name="%(key_name)s", public_key="")
|
|
914
|
+
key = ssh::Key(name="%(key_name)s", public_key="%(ssh_public_key)s")
|
|
912
915
|
vm = openstack::Host(provider=provider, project=project, key_pair=key, name="%(server_name)s", os=os,
|
|
913
916
|
ip=std::getfact(vm.vm, "ip_address"),
|
|
914
917
|
image=openstack::find_image(provider, os), flavor=openstack::find_flavor(provider, 1, 0.5),
|
|
@@ -923,6 +926,7 @@ def test_shared_network(project: Project, os_tester: OpenstackTester) -> None:
|
|
|
923
926
|
% {
|
|
924
927
|
"net_name": net_name,
|
|
925
928
|
"key_name": key_name,
|
|
929
|
+
"ssh_public_key": ssh_public_key,
|
|
926
930
|
"server_name": server_name,
|
|
927
931
|
},
|
|
928
932
|
project_name=project2._project_name,
|
|
@@ -933,7 +937,9 @@ def test_shared_network(project: Project, os_tester: OpenstackTester) -> None:
|
|
|
933
937
|
project.deploy_resource("openstack::NetworkPort", name=server_name + "_eth2")
|
|
934
938
|
|
|
935
939
|
|
|
936
|
-
def test_allowed_addr_port_with_trunk(
|
|
940
|
+
def test_allowed_addr_port_with_trunk(
|
|
941
|
+
project, os_tester, require_network_extension, ssh_public_key
|
|
942
|
+
):
|
|
937
943
|
"""
|
|
938
944
|
Test creating a port with allowed address pairs and trunk network/subports
|
|
939
945
|
"""
|
|
@@ -957,7 +963,7 @@ def test_allowed_addr_port_with_trunk(project, os_tester, require_network_extens
|
|
|
957
963
|
network_address="10.255.255.0/24", dns_servers=["8.8.8.8", "8.8.4.4"])
|
|
958
964
|
|
|
959
965
|
os = std::OS(name="cirros", version=0.4, family=std::linux)
|
|
960
|
-
key = ssh::Key(name="%(key_name)s", public_key="")
|
|
966
|
+
key = ssh::Key(name="%(key_name)s", public_key="%(ssh_public_key)s")
|
|
961
967
|
|
|
962
968
|
vm = openstack::VirtualMachine(provider=provider, project=project, key_pair=key, name="%(server_name)s",
|
|
963
969
|
image=openstack::find_image(provider, os), flavor=openstack::find_flavor(provider, 1, 0.5),
|
|
@@ -993,6 +999,7 @@ def test_allowed_addr_port_with_trunk(project, os_tester, require_network_extens
|
|
|
993
999
|
"port_name_2": port_name_2,
|
|
994
1000
|
"server_name": server_name,
|
|
995
1001
|
"key_name": key_name,
|
|
1002
|
+
"ssh_public_key": ssh_public_key,
|
|
996
1003
|
"trunk_name": trunk_name,
|
|
997
1004
|
"subport_name": subport_name,
|
|
998
1005
|
},
|
|
@@ -17,7 +17,9 @@ Contact: code@inmanta.com
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import time
|
|
20
|
+
from types import SimpleNamespace
|
|
20
21
|
from typing import Optional
|
|
22
|
+
from unittest.mock import Mock
|
|
21
23
|
|
|
22
24
|
import inmanta
|
|
23
25
|
import inmanta.const
|
|
@@ -26,6 +28,29 @@ from conftest import OpenstackTester
|
|
|
26
28
|
from inmanta.ast import ExplicitPluginException
|
|
27
29
|
from pytest_inmanta.plugin import Project
|
|
28
30
|
|
|
31
|
+
from inmanta_plugins.openstack.common.base import OpenStackHandler
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_get_server_by_name_all_projects_is_opt_in() -> None:
|
|
35
|
+
handler = OpenStackHandler.__new__(OpenStackHandler)
|
|
36
|
+
server = SimpleNamespace(name="server")
|
|
37
|
+
servers = Mock(return_value=iter([server]))
|
|
38
|
+
handler._connection = SimpleNamespace(compute=SimpleNamespace(servers=servers))
|
|
39
|
+
|
|
40
|
+
assert handler.get_server_by_name("server", project_id="project-id") is server
|
|
41
|
+
servers.assert_called_once_with(name="^server$", project_id="project-id")
|
|
42
|
+
|
|
43
|
+
servers.reset_mock()
|
|
44
|
+
servers.return_value = iter([server])
|
|
45
|
+
|
|
46
|
+
assert (
|
|
47
|
+
handler.get_server_by_name("server", project_id="project-id", all_projects=True)
|
|
48
|
+
is server
|
|
49
|
+
)
|
|
50
|
+
servers.assert_called_once_with(
|
|
51
|
+
name="^server$", project_id="project-id", all_projects=True
|
|
52
|
+
)
|
|
53
|
+
|
|
29
54
|
|
|
30
55
|
def get_model(
|
|
31
56
|
name: str,
|
{inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_server_group.py
RENAMED
|
@@ -203,6 +203,7 @@ def _vm_handler(project):
|
|
|
203
203
|
VirtualMachineHandler, project.get_handler(resource, False)
|
|
204
204
|
)
|
|
205
205
|
vm_handler._connection = MagicMock()
|
|
206
|
+
vm_handler._connection.current_project_id = "project-id"
|
|
206
207
|
vm_handler._connection.identity.find_project.return_value = SimpleNamespace(
|
|
207
208
|
id="project-id"
|
|
208
209
|
)
|
|
@@ -296,6 +296,7 @@ def test_trunk_deploy_on_vm_parent_port(
|
|
|
296
296
|
project: pytest_inmanta.plugin.Project,
|
|
297
297
|
os_tester,
|
|
298
298
|
require_network_extension,
|
|
299
|
+
ssh_public_key,
|
|
299
300
|
):
|
|
300
301
|
"""
|
|
301
302
|
Deploy a trunk whose parent port is attached to a VM.
|
|
@@ -327,7 +328,7 @@ def test_trunk_deploy_on_vm_parent_port(
|
|
|
327
328
|
network_address="10.234.0.0/24", dns_servers=["8.8.8.8", "8.8.4.4"])
|
|
328
329
|
|
|
329
330
|
os = std::OS(name="cirros", version=0.4, family=std::linux)
|
|
330
|
-
key = ssh::Key(name="{key_name}", public_key="")
|
|
331
|
+
key = ssh::Key(name="{key_name}", public_key="{ssh_public_key}")
|
|
331
332
|
|
|
332
333
|
vm = openstack::VirtualMachine(provider=provider, project=project, key_pair=key, name="{server_name}",
|
|
333
334
|
image=openstack::find_image(provider, os),
|
|
@@ -157,6 +157,7 @@ def _vm_handler(
|
|
|
157
157
|
VirtualMachineHandler, project.get_handler(resource, False)
|
|
158
158
|
)
|
|
159
159
|
vm_handler._connection = MagicMock()
|
|
160
|
+
vm_handler._connection.current_project_id = PROJECT_ID
|
|
160
161
|
vm_handler._connection.identity.find_project.return_value = SimpleNamespace(
|
|
161
162
|
id=PROJECT_ID
|
|
162
163
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_block_devices.py
RENAMED
|
File without changes
|
{inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_dependency_handling.py
RENAMED
|
File without changes
|
|
File without changes
|
{inmanta_module_openstack-6.1.0 → inmanta_module_openstack-6.1.1}/tests/test_host_aggregate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|