inmanta-module-openstack 6.1.1__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.1 → inmanta_module_openstack-6.1.2}/PKG-INFO +1 -1
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/PKG-INFO +1 -1
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/SOURCES.txt +6 -3
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/common/base.py +10 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/compute/virtual_machine.py +115 -70
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/network_port.py +1 -6
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/router.py +15 -5
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/setup.cfg +1 -1
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/setup.cfg +1 -1
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_block_devices.py +1 -1
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_common_base.py +6 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_host_aggregate.py +3 -3
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_image.py +5 -31
- {inmanta_module_openstack-6.1.1 → 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.1 → inmanta_module_openstack-6.1.2}/tests/test_network_port_fixed_ips.py +1 -1
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_network_port_security_groups.py +1 -1
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_nova.py +12 -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.1 → inmanta_module_openstack-6.1.2}/tests/test_security_group_ethertype.py +2 -2
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_server_group.py +114 -89
- inmanta_module_openstack-6.1.2/tests/test_subnet.py +378 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_trunk.py +1 -1
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_vm_boot_nic.py +127 -97
- inmanta_module_openstack-6.1.1/tests/test_network_port_missing_network.py +0 -177
- inmanta_module_openstack-6.1.1/tests/test_network_port_shared_network.py +0 -264
- inmanta_module_openstack-6.1.1/tests/test_neutron.py +0 -1640
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/MANIFEST.in +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/README.md +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/dependency_links.txt +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/not-zip-safe +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/requires.txt +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_module_openstack.egg-info/top_level.txt +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/__init__.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/common/__init__.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/common/deps.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/compute/__init__.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/compute/flavor.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/compute/host_aggregate.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/compute/server_group.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/__init__.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/group.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/group_role.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/keystone_base.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/project.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/role.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/identity/user.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/image/__init__.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/image/image.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/model/_init.cf +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/__init__.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/floating_ip.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/network.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/qos_policy.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/router_port.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/security_group.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/subnet.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/subnet_v6.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/trunk_network.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/network/trunk_port.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/quota/__init__.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/inmanta_plugins/openstack/quota/quota.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/pyproject.toml +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_dependency_handling.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_examples.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_flavor.py +0 -0
- {inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_quota.py +0 -0
|
@@ -50,14 +50,17 @@ tests/test_flavor.py
|
|
|
50
50
|
tests/test_host_aggregate.py
|
|
51
51
|
tests/test_image.py
|
|
52
52
|
tests/test_keystone.py
|
|
53
|
+
tests/test_network.py
|
|
54
|
+
tests/test_network_port.py
|
|
53
55
|
tests/test_network_port_fixed_ips.py
|
|
54
|
-
tests/test_network_port_missing_network.py
|
|
55
56
|
tests/test_network_port_security_groups.py
|
|
56
|
-
tests/test_network_port_shared_network.py
|
|
57
|
-
tests/test_neutron.py
|
|
58
57
|
tests/test_nova.py
|
|
58
|
+
tests/test_qos_policy.py
|
|
59
59
|
tests/test_quota.py
|
|
60
|
+
tests/test_router.py
|
|
61
|
+
tests/test_security_group.py
|
|
60
62
|
tests/test_security_group_ethertype.py
|
|
61
63
|
tests/test_server_group.py
|
|
64
|
+
tests/test_subnet.py
|
|
62
65
|
tests/test_trunk.py
|
|
63
66
|
tests/test_vm_boot_nic.py
|
|
@@ -360,6 +360,16 @@ class OpenStackHandler(CRUDHandler):
|
|
|
360
360
|
|
|
361
361
|
return self.require_single_item(servers)
|
|
362
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
|
+
|
|
363
373
|
def _get_qos_policy(self, **query_params):
|
|
364
374
|
try:
|
|
365
375
|
policies = self._connection.network.qos_policies(**query_params)
|
|
@@ -17,6 +17,7 @@ Contact: code@inmanta.com
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import time
|
|
20
|
+
from collections.abc import Callable
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
from inmanta import ast
|
|
@@ -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 NotFoundException
|
|
33
|
+
from openstack.exceptions import NotFoundException
|
|
33
34
|
|
|
34
35
|
from inmanta_plugins.openstack.common.base import (
|
|
35
36
|
NotUniqueException,
|
|
@@ -190,13 +191,9 @@ class VirtualMachine(OpenstackResource):
|
|
|
190
191
|
|
|
191
192
|
@provider("openstack::VirtualMachine", name="openstack")
|
|
192
193
|
class VirtualMachineHandler(OpenStackHandler):
|
|
193
|
-
@cache(timeout=1, cache_none=False)
|
|
194
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
|
-
|
|
198
195
|
project_id = self.get_project_id(resource, resource.project)
|
|
199
|
-
server = self.
|
|
196
|
+
server = self.get_project_server(resource, resource.name, project_id)
|
|
200
197
|
if server is None:
|
|
201
198
|
ctx.debug(
|
|
202
199
|
"No server named %(name)s found in project %(project)s.",
|
|
@@ -299,26 +296,18 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
299
296
|
self._connection.compute.create_keypair(**keypair_args)
|
|
300
297
|
ctx.info(f"Created a new keypair with name {resource.key_name}")
|
|
301
298
|
|
|
302
|
-
def
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
)
|
|
310
|
-
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))
|
|
311
302
|
|
|
312
|
-
def _raise_server_error(
|
|
313
|
-
|
|
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.
|
|
314
307
|
|
|
315
|
-
Used both when a server is found in ERROR state by read_resource and
|
|
316
|
-
when it transitions to ERROR during a build. Re-fetching is necessary
|
|
317
|
-
because the list API used by get_server_by_name may omit the fault body.
|
|
318
308
|
The ERROR-state server must be deleted (manually or via purge) before
|
|
319
309
|
Inmanta will attempt to recreate it.
|
|
320
310
|
"""
|
|
321
|
-
server = self._connection.compute.get_server(self.server.id)
|
|
322
311
|
fault = getattr(server, "fault", None) or {}
|
|
323
312
|
msg = fault.get("message", "unknown error")
|
|
324
313
|
details = fault.get("details", "")
|
|
@@ -326,12 +315,42 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
326
315
|
log_message,
|
|
327
316
|
fault_message=msg,
|
|
328
317
|
fault_details=details,
|
|
329
|
-
server_id=
|
|
318
|
+
server_id=server.id,
|
|
330
319
|
)
|
|
331
320
|
raise Exception(
|
|
332
|
-
f"Server {
|
|
321
|
+
f"Server {server.id} ({server.name}) is in ERROR state: {msg}"
|
|
333
322
|
) from None
|
|
334
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
|
+
|
|
335
354
|
def _wait_for_status(
|
|
336
355
|
self,
|
|
337
356
|
ctx: HandlerContext,
|
|
@@ -345,40 +364,31 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
345
364
|
and is only used to build the error messages.
|
|
346
365
|
|
|
347
366
|
Nova may briefly return a 404 for the instance while a resize is being
|
|
348
|
-
applied
|
|
349
|
-
resumes until the status is reached or the timeout elapses.
|
|
367
|
+
applied, this is treated as not ready yet.
|
|
350
368
|
"""
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
except ResourceFailure:
|
|
362
|
-
self._raise_server_error(
|
|
363
|
-
ctx, f"VM entered ERROR state during {operation}"
|
|
364
|
-
)
|
|
365
|
-
except ResourceTimeout:
|
|
366
|
-
self._raise_status_timeout(ctx, status, timeout)
|
|
367
|
-
except NotFoundException:
|
|
368
|
-
# The instance is temporarily not retrievable (e.g. mid-resize);
|
|
369
|
-
# give it a moment and keep polling until the deadline.
|
|
370
|
-
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)
|
|
371
379
|
|
|
372
380
|
def _raise_status_timeout(
|
|
373
|
-
self, ctx: HandlerContext, status: str, timeout: int
|
|
381
|
+
self, ctx: HandlerContext, status: str, operation: str, timeout: int
|
|
374
382
|
) -> None:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
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:
|
|
380
387
|
current_status = "not_found"
|
|
381
388
|
task_state = "unknown"
|
|
389
|
+
else:
|
|
390
|
+
current_status = server.status
|
|
391
|
+
task_state = server.task_state
|
|
382
392
|
ctx.error(
|
|
383
393
|
f"Timed out waiting for VM to reach {status}",
|
|
384
394
|
timeout_seconds=timeout,
|
|
@@ -391,6 +401,27 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
391
401
|
f"(status={current_status}, task_state={task_state})"
|
|
392
402
|
) from None
|
|
393
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
|
+
|
|
394
425
|
def _get_server_flavor_name(self, server) -> str:
|
|
395
426
|
"""Return the flavor name of `server`.
|
|
396
427
|
|
|
@@ -442,15 +473,24 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
442
473
|
"""
|
|
443
474
|
This method will check what the status of the give resource is on
|
|
444
475
|
openstack.
|
|
476
|
+
|
|
477
|
+
A server in ERROR state raises, unless it is being deleted or purged.
|
|
445
478
|
"""
|
|
446
479
|
self.server = self._get_server(ctx, resource)
|
|
447
480
|
if self.server is None:
|
|
448
481
|
raise ResourcePurged()
|
|
449
482
|
|
|
450
|
-
#
|
|
451
|
-
if
|
|
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
|
+
):
|
|
452
489
|
self._raise_server_error(
|
|
453
|
-
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!",
|
|
454
494
|
)
|
|
455
495
|
|
|
456
496
|
resource.purged = False
|
|
@@ -504,6 +544,8 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
504
544
|
metadata=resource.metadata,
|
|
505
545
|
files=resource.personality,
|
|
506
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.
|
|
507
549
|
if resource.block_devices:
|
|
508
550
|
args["block_device_mapping_v2"] = list(resource.block_devices)
|
|
509
551
|
else:
|
|
@@ -534,32 +576,36 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
534
576
|
|
|
535
577
|
def delete_resource(self, ctx: HandlerContext, resource: VirtualMachine) -> None:
|
|
536
578
|
self._connection.compute.delete_server(self.server, ignore_missing=True)
|
|
579
|
+
timeout = 120
|
|
537
580
|
|
|
538
|
-
def
|
|
539
|
-
|
|
540
|
-
if ports:
|
|
541
|
-
return False
|
|
542
|
-
return self._get_server(ctx, resource) is None
|
|
581
|
+
def is_server_deleted() -> bool:
|
|
582
|
+
return self._poll_server(ctx, "delete") is None
|
|
543
583
|
|
|
544
|
-
#
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
time.sleep(1)
|
|
549
|
-
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()
|
|
550
588
|
|
|
551
|
-
|
|
552
|
-
|
|
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)
|
|
553
594
|
|
|
595
|
+
ctx.info("Server was deleted.")
|
|
554
596
|
ctx.set_purged()
|
|
555
597
|
|
|
556
598
|
def update_resource(
|
|
557
599
|
self, ctx: HandlerContext, changes: dict, resource: VirtualMachine
|
|
558
600
|
) -> None:
|
|
559
|
-
|
|
601
|
+
# read_resource lets through an ERROR server that is being deleted.
|
|
560
602
|
if self.server.status == "ERROR":
|
|
561
603
|
self._raise_server_error(
|
|
562
|
-
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!",
|
|
563
609
|
)
|
|
564
610
|
|
|
565
611
|
self.validate_allowed_fields(
|
|
@@ -587,7 +633,6 @@ class VirtualMachineHandler(OpenStackHandler):
|
|
|
587
633
|
self.server, remove_rule
|
|
588
634
|
)
|
|
589
635
|
|
|
590
|
-
self.server = self._get_server(ctx, resource)
|
|
591
636
|
ctx.set_updated()
|
|
592
637
|
|
|
593
638
|
def _set_facts(self, ctx):
|
|
@@ -224,12 +224,7 @@ 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
|
-
|
|
228
|
-
vm = self.get_server_by_name(
|
|
229
|
-
resource.host,
|
|
230
|
-
project_id if cross_project else None,
|
|
231
|
-
all_projects=cross_project,
|
|
232
|
-
)
|
|
227
|
+
vm = self.get_project_server(resource, resource.host, project_id)
|
|
233
228
|
if vm is None:
|
|
234
229
|
return None
|
|
235
230
|
|
|
@@ -20,6 +20,7 @@ from inmanta.agent import handler
|
|
|
20
20
|
from inmanta.agent.handler import ResourcePurged, SkipResource, provider
|
|
21
21
|
from inmanta.execute import proxy
|
|
22
22
|
from inmanta.resources import resource
|
|
23
|
+
from openstack.network.v2.subnet import Subnet as OSSubnet
|
|
23
24
|
|
|
24
25
|
from inmanta_plugins.openstack.common.base import (
|
|
25
26
|
OpenStackHandler,
|
|
@@ -163,14 +164,24 @@ class RouterHandler(OpenStackHandler):
|
|
|
163
164
|
self._connection.network.delete_router(self.router.id, ignore_missing=True)
|
|
164
165
|
ctx.set_purged()
|
|
165
166
|
|
|
167
|
+
def _get_router_subnet(self, name: str) -> OSSubnet | None:
|
|
168
|
+
"""
|
|
169
|
+
Retrieve a subnet of the router by name. A subnet name is only unique within a
|
|
170
|
+
project, so the router's own project is searched first. Only when it has no such
|
|
171
|
+
subnet, the subnet of another project is looked up, e.g. one of a shared network.
|
|
172
|
+
"""
|
|
173
|
+
subnet = self.get_subnet(self.project_id, name=name)
|
|
174
|
+
if subnet is None:
|
|
175
|
+
subnet = self.get_subnet(name=name)
|
|
176
|
+
return subnet
|
|
177
|
+
|
|
166
178
|
def _update_subnets(self, current, desired):
|
|
167
179
|
current = set(current)
|
|
168
180
|
to = set(desired)
|
|
169
181
|
|
|
170
182
|
# subnets to add to the router
|
|
171
183
|
for subnet_name in to - current:
|
|
172
|
-
|
|
173
|
-
subnet = self.get_subnet(name=subnet_name)
|
|
184
|
+
subnet = self._get_router_subnet(subnet_name)
|
|
174
185
|
if subnet is None:
|
|
175
186
|
raise Exception(f"Unable to find id of subnet {subnet_name}")
|
|
176
187
|
|
|
@@ -180,10 +191,9 @@ class RouterHandler(OpenStackHandler):
|
|
|
180
191
|
|
|
181
192
|
# subnets to delete
|
|
182
193
|
for subnet_name in current - to:
|
|
183
|
-
|
|
184
|
-
subnet = self.get_subnet(name=subnet_name)
|
|
194
|
+
subnet = self._get_router_subnet(subnet_name)
|
|
185
195
|
if subnet is None:
|
|
186
|
-
raise Exception("Unable to find id of subnet
|
|
196
|
+
raise Exception(f"Unable to find id of subnet {subnet_name}")
|
|
187
197
|
|
|
188
198
|
self._connection.network.remove_interface_from_router(
|
|
189
199
|
self.router, subnet_id=subnet.id
|
|
@@ -29,6 +29,11 @@ def _handler(connection) -> OpenStackHandler:
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def test_get_project_id_uses_scoped_project_without_identity_lookup() -> None:
|
|
32
|
+
"""
|
|
33
|
+
A project-scoped token already knows its own project id, so it is used without a
|
|
34
|
+
Keystone lookup: a non-admin user may be forbidden to get or list projects. The
|
|
35
|
+
deployment tests run with admin credentials, which are never refused that lookup.
|
|
36
|
+
"""
|
|
32
37
|
connection = SimpleNamespace(
|
|
33
38
|
current_project_id="project-id",
|
|
34
39
|
identity=MagicMock(),
|
|
@@ -43,6 +48,7 @@ def test_get_project_id_uses_scoped_project_without_identity_lookup() -> None:
|
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
def test_get_project_id_can_still_resolve_another_visible_project() -> None:
|
|
51
|
+
"""Another project than the one the token is scoped to is still looked up."""
|
|
46
52
|
connection = SimpleNamespace(
|
|
47
53
|
current_project_id="current-project-id",
|
|
48
54
|
identity=MagicMock(),
|
{inmanta_module_openstack-6.1.1 → inmanta_module_openstack-6.1.2}/tests/test_host_aggregate.py
RENAMED
|
@@ -19,12 +19,12 @@ Contact: code@inmanta.com
|
|
|
19
19
|
import inmanta
|
|
20
20
|
import pytest
|
|
21
21
|
|
|
22
|
-
AGGREGATE_NAME = "
|
|
22
|
+
AGGREGATE_NAME = "os_unit_test_aggregate"
|
|
23
23
|
# An unrelated key seeded on the aggregate that must survive every operation, proving
|
|
24
24
|
# the resource only manages its own key and never clobbers others.
|
|
25
25
|
OTHER_KEY = "filter_tenant_id_other"
|
|
26
26
|
OTHER_VALUE = "other-project-uuid"
|
|
27
|
-
MANAGED_KEY = "
|
|
27
|
+
MANAGED_KEY = "filter_tenant_id_os_unit_test"
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _find_aggregate(connection):
|
|
@@ -128,7 +128,7 @@ def test_host_aggregate_metadata_missing_aggregate_is_skipped(project, os_tester
|
|
|
128
128
|
os_tester.compile_with_provider("""
|
|
129
129
|
openstack::HostAggregateMetadata(
|
|
130
130
|
provider=provider,
|
|
131
|
-
aggregate="
|
|
131
|
+
aggregate="os_unit_test_missing_aggregate",
|
|
132
132
|
key="filter_tenant_id_x",
|
|
133
133
|
value="uuid-x",
|
|
134
134
|
)
|
|
@@ -24,7 +24,7 @@ import inmanta
|
|
|
24
24
|
import inmanta.agent.handler
|
|
25
25
|
import pytest
|
|
26
26
|
|
|
27
|
-
TEST_IMAGE_NAME = "
|
|
27
|
+
TEST_IMAGE_NAME = "os_unit_test"
|
|
28
28
|
CIRROS_URI = "http://download.cirros-cloud.net/0.6.2/cirros-0.6.2-x86_64-disk.img"
|
|
29
29
|
|
|
30
30
|
|
|
@@ -424,6 +424,7 @@ image=openstack::Image(
|
|
|
424
424
|
|
|
425
425
|
ctx_deploy = project.deploy(created_image)
|
|
426
426
|
assert ctx_deploy.status == inmanta.const.ResourceState.deployed
|
|
427
|
+
assert not project.dryrun(created_image).changes
|
|
427
428
|
|
|
428
429
|
matching_images = list(os_tester.connection.image.images(name=TEST_IMAGE_NAME))
|
|
429
430
|
assert len(matching_images) == 1
|
|
@@ -436,7 +437,9 @@ def test_file_upload_without_glance_direct(tmp_path):
|
|
|
436
437
|
Uploading a local file must not require the 'glance-direct' import method.
|
|
437
438
|
When the cloud only enables 'web-download', the handler falls back to the
|
|
438
439
|
classic direct data upload (PUT /v2/images/{id}/file) instead of raising.
|
|
439
|
-
Regression test for clouds that do not enable glance-direct.
|
|
440
|
+
Regression test for clouds that do not enable glance-direct. The lab cloud does
|
|
441
|
+
enable it, so this cannot be a deployment test: test_create_image_from_file covers
|
|
442
|
+
the glance-direct upload.
|
|
440
443
|
"""
|
|
441
444
|
from unittest.mock import MagicMock
|
|
442
445
|
|
|
@@ -459,32 +462,3 @@ def test_file_upload_without_glance_direct(tmp_path):
|
|
|
459
462
|
handler.image.upload.assert_called_once()
|
|
460
463
|
handler._connection.image.stage_image.assert_not_called()
|
|
461
464
|
handler._connection.image.import_image.assert_not_called()
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
def test_file_upload_with_glance_direct(tmp_path):
|
|
465
|
-
"""
|
|
466
|
-
When the cloud enables 'glance-direct', a local file is staged and imported
|
|
467
|
-
through the interoperable image import API rather than uploaded directly.
|
|
468
|
-
"""
|
|
469
|
-
from unittest.mock import MagicMock
|
|
470
|
-
|
|
471
|
-
from inmanta_plugins.openstack.image.image import ImageHandler
|
|
472
|
-
|
|
473
|
-
image_file = tmp_path / "image.img"
|
|
474
|
-
image_file.write_bytes(b"fake-image-data")
|
|
475
|
-
|
|
476
|
-
handler = ImageHandler.__new__(ImageHandler)
|
|
477
|
-
handler._connection = MagicMock()
|
|
478
|
-
handler.image = MagicMock()
|
|
479
|
-
|
|
480
|
-
resource = MagicMock()
|
|
481
|
-
resource.file = str(image_file)
|
|
482
|
-
resource.uri = None
|
|
483
|
-
|
|
484
|
-
handler._import_image_via_methods(resource, {"web-download", "glance-direct"})
|
|
485
|
-
|
|
486
|
-
handler._connection.image.stage_image.assert_called_once()
|
|
487
|
-
handler._connection.image.import_image.assert_called_once_with(
|
|
488
|
-
handler.image, method="glance-direct"
|
|
489
|
-
)
|
|
490
|
-
handler.image.upload.assert_not_called()
|
|
@@ -27,7 +27,7 @@ import pytest
|
|
|
27
27
|
USERNAME = "test_user123"
|
|
28
28
|
EMAIL = "test@example.com"
|
|
29
29
|
PASSWORD = "password123"
|
|
30
|
-
PROJECT_NAME = "
|
|
30
|
+
PROJECT_NAME = "os_unit_test"
|
|
31
31
|
ROLE_MEMBER = "member"
|
|
32
32
|
GROUP_NAME = "inmanta_test_group"
|
|
33
33
|
GROUP_USER_NAME = "inmanta_group_test_user"
|