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:
parent
2a4ccac195
commit
286b7cb09f
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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'),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user