1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-01-08 21:18:00 +03:00

Adding Openstack Client tests and fixes to client and provider

This commit is contained in:
Adolfo Gómez García 2024-07-16 22:58:40 +02:00
parent 2a4ccac195
commit 286b7cb09f
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
5 changed files with 111 additions and 17 deletions

View File

@ -145,6 +145,9 @@ VOLUMES_LIST: list[openstack_types.VolumeInfo] = [
availability_zone=f'zone{n}',
bootable=n % 2 == 0,
encrypted=n % 3 == 0,
status=openstack_types.VolumeStatus.AVAILABLE,
created_at=datetime.datetime(2009, 12, 9, 0, 0, 0),
updated_at=datetime.datetime(2024, 1, 1, 0, 0, 0),
)
for n in range(1, 16)
]
@ -275,7 +278,7 @@ CLIENT_METHODS_INFO: typing.Final[list[AutoSpecMethodInfo]] = [
partial_args=(SERVERS_LIST,),
),
AutoSpecMethodInfo(
client.OpenStackClient.get_volume,
client.OpenStackClient.get_volume_info,
returns=search_id,
partial_args=(VOLUMES_LIST,),
), # pyright: ignore

View File

@ -40,8 +40,7 @@ from uds.services.OpenStack.openstack import (
client as openstack_client,
)
from tests.utils import vars
from tests.utils import helpers
from tests.utils import vars, helpers
from tests.utils.test import UDSTransactionTestCase
@ -56,6 +55,7 @@ class TestOpenStackClient(UDSTransactionTestCase):
_password: str
_auth_method: openstack_types.AuthMethod
_projectid: str
_regionid: str
oclient: openstack_client.OpenStackClient
@ -81,9 +81,17 @@ class TestOpenStackClient(UDSTransactionTestCase):
self._password = v['password']
self._auth_method = openstack_types.AuthMethod.from_str(v['auth_method'])
self._projectid = v['project_id']
self._regionid = v['region']
self.get_client()
def wait_for_volume(self, volume: openstack_types.VolumeInfo) -> None:
helpers.waiter(
lambda: self.oclient.get_volume_info(volume.id, force=True).status.is_available(),
timeout=30,
msg='Timeout waiting for volume to be available',
)
@contextlib.contextmanager
def create_test_volume(self) -> typing.Iterator[openstack_types.VolumeInfo]:
volume = self.oclient.t_create_volume(
@ -91,9 +99,21 @@ class TestOpenStackClient(UDSTransactionTestCase):
size=1,
)
try:
self.wait_for_volume(volume)
yield volume
finally:
self.wait_for_volume(volume)
self.oclient.t_delete_volume(volume.id)
def test_list_projects(self) -> None:
projects = self.oclient.list_projects()
self.assertGreaterEqual(len(projects), 1)
self.assertIn(self._projectid, [p.id for p in projects])
def test_list_regions(self) -> None:
regions = self.oclient.list_regions()
self.assertGreaterEqual(len(regions), 1)
self.assertIn(self._regionid, [r.id for r in regions])
def test_list_volumes(self) -> None:
with self.create_test_volume() as volume:
@ -101,9 +121,18 @@ class TestOpenStackClient(UDSTransactionTestCase):
with self.create_test_volume() as volume3:
volumes = self.oclient.list_volumes()
self.assertGreaterEqual(len(volumes), 3)
self.assertIn(volume, volumes)
self.assertIn(volume2, volumes)
self.assertIn(volume3, volumes)
self.assertIn(
(volume.id, volume.name, volume.description),
[(v.id, v.name, v.description) for v in volumes],
)
self.assertIn(
(volume2.id, volume2.name, volume2.description),
[(v.id, v.name, v.description) for v in volumes],
)
self.assertIn(
(volume3.id, volume3.name, volume3.description),
[(v.id, v.name, v.description) for v in volumes],
)
# if no project id, should fail
self.get_client(use_project_id=False)

View File

@ -598,7 +598,8 @@ class OpenStackClient: # pylint: disable=too-many-public-methods
)
return openstack_types.ServerInfo.from_dict(r.json()['server'])
def get_volume(self, volume_id: str) -> openstack_types.VolumeInfo:
@decorators.cached(prefix='vol', timeout=consts.cache.SHORTEST_CACHE_TIMEOUT, key_helper=cache_key_helper)
def get_volume_info(self, volume_id: str, **kwargs: typing.Any) -> openstack_types.VolumeInfo:
r = self._request_from_endpoint(
'get',
endpoints_types=VOLUMES_ENDPOINT_TYPES,

View File

@ -37,6 +37,7 @@ import enum
from uds.core.services.generics import exceptions
class AuthMethod(enum.StrEnum):
# Only theese two methods are supported by our OpenStack implementation
PASSWORD = 'password'
@ -47,7 +48,8 @@ class AuthMethod(enum.StrEnum):
try:
return AuthMethod(s.lower())
except ValueError:
return AuthMethod.PASSWORD
return AuthMethod.PASSWORD
class ServerStatus(enum.StrEnum):
ACTIVE = 'ACTIVE' # The server is active.
@ -84,16 +86,31 @@ class ServerStatus(enum.StrEnum):
# Helpers to check statuses
def is_lost(self) -> bool:
return self in [ServerStatus.DELETED, ServerStatus.ERROR, ServerStatus.UNKNOWN, ServerStatus.SOFT_DELETED]
return self in [
ServerStatus.DELETED,
ServerStatus.ERROR,
ServerStatus.UNKNOWN,
ServerStatus.SOFT_DELETED,
]
def is_paused(self) -> bool:
return self in [ServerStatus.PAUSED, ServerStatus.SUSPENDED]
def is_running(self) -> bool:
return self in [ServerStatus.ACTIVE, ServerStatus.RESCUE, ServerStatus.RESIZE, ServerStatus.VERIFY_RESIZE]
return self in [
ServerStatus.ACTIVE,
ServerStatus.RESCUE,
ServerStatus.RESIZE,
ServerStatus.VERIFY_RESIZE,
]
def is_stopped(self) -> bool:
return self in [ServerStatus.SHUTOFF, ServerStatus.SHELVED, ServerStatus.SHELVED_OFFLOADED, ServerStatus.SOFT_DELETED]
return self in [
ServerStatus.SHUTOFF,
ServerStatus.SHELVED,
ServerStatus.SHELVED_OFFLOADED,
ServerStatus.SOFT_DELETED,
]
class PowerState(enum.IntEnum):
@ -183,6 +200,39 @@ class PortStatus(enum.StrEnum):
return PortStatus.ERROR
class VolumeStatus(enum.StrEnum):
CREATING = 'creating' # The volume is being created.
AVAILABLE = 'available' # The volume is ready to attach to an instance.
RESERVED = 'reserved' # The volume is reserved for attaching or shelved.
ATTACHING = 'attaching' # The volume is attaching to an instance.
DETACHING = 'detaching' # The volume is detaching from an instance.
IN_USE = 'in-use' # The volume is attached to an instance.
MAINTENANCE = 'maintenance' # The volume is locked and being migrated.
DELETING = 'deleting' # The volume is being deleted.
AWAITING_TRANSFER = 'awaiting-transfer' # The volume is awaiting for transfer.
ERROR = 'error' # A volume creation error occurred.
ERROR_DELETING = 'error_deleting' # A volume deletion error occurred.
BACKING_UP = 'backing-up' # The volume is being backed up.
RESTORING_BACKUP = 'restoring-backup' # A backup is being restored to the volume.
ERROR_BACKING_UP = 'error_backing-up' # A backup error occurred.
ERROR_RESTORING = 'error_restoring' # A backup restoration error occurred.
ERROR_EXTENDING = 'error_extending' # An error occurred while attempting to extend a volume.
DOWNLOADING = 'downloading' # The volume is downloading an image.
UPLOADING = 'uploading' # The volume is being uploaded to an image.
RETYPING = 'retyping' # The volume is changing type to another volume type.
EXTENDING = 'extending' # The volume is being extended.
def is_available(self) -> bool:
return self in [VolumeStatus.AVAILABLE]
@staticmethod
def from_str(s: str) -> 'VolumeStatus':
try:
return VolumeStatus(s.lower())
except ValueError:
return VolumeStatus.ERROR
@dataclasses.dataclass
class ServerInfo:
@ -225,11 +275,11 @@ class ServerInfo:
access_addr_ipv6: str
fault: typing.Optional[str]
admin_pass: str
def validated(self) -> 'ServerInfo':
"""
Raises NotFoundError if server is lost
Returns:
self
"""
@ -325,6 +375,10 @@ class VolumeInfo:
availability_zone: str
bootable: bool
encrypted: bool
status: VolumeStatus
created_at: datetime.datetime # From ISO 8601 string
updated_at: datetime.datetime # From ISO 8601 string
@staticmethod
def from_dict(d: dict[str, typing.Any]) -> 'VolumeInfo':
@ -336,6 +390,9 @@ class VolumeInfo:
availability_zone=d.get('availability_zone', ''),
bootable=d.get('bootable', False),
encrypted=d.get('encrypted', False),
status=VolumeStatus.from_str(d.get('status', VolumeStatus.ERROR.value)),
created_at=datetime.datetime.fromisoformat(d.get('created_at') or '1970-01-01T00:00:00'),
updated_at=datetime.datetime.fromisoformat(d.get('updated_at') or '1970-01-01T00:00:00'),
)

View File

@ -35,7 +35,7 @@ import typing
from django.utils.translation import gettext_noop as _
from uds.core import types
from uds.core import exceptions, types
from uds.core.services import ServiceProvider
from uds.core.ui import gui
from uds.core.util import validators, fields
@ -127,8 +127,7 @@ class OpenStackProvider(ServiceProvider):
choices=INTERFACE_VALUES,
default='public',
)
domain = gui.TextField(
length=64,
label=_('Domain'),
@ -198,7 +197,6 @@ class OpenStackProvider(ServiceProvider):
tab=types.ui.Tab.ADVANCED,
old_field_name='httpsProxy',
)
legacy = False
@ -213,6 +211,12 @@ class OpenStackProvider(ServiceProvider):
if values is not None:
self.timeout.value = validators.validate_timeout(self.timeout.value)
if self.auth_method.value == openstack_types.AuthMethod.APPLICATION_CREDENTIAL:
# Ensure that the project_id is provided, so it's bound to the application credential
if not self.tenant.value:
raise exceptions.ui.ValidationError(
_('Project Id is required when using Application Credential')
)
def api(
self, projectid: typing.Optional[str] = None, region: typing.Optional[str] = None