mirror of
https://github.com/dkmstr/openuds.git
synced 2025-02-02 09:47:13 +03:00
Adding type info to OpenStack, to enhance resilence and code sanity
This commit is contained in:
parent
466bda440e
commit
62742e9438
@ -47,6 +47,8 @@ from ldap import (
|
||||
# SCOPE_SUBORDINATE, # pyright: ignore
|
||||
)
|
||||
|
||||
# Reexporting, so we can use them as ldaputil.SCOPE_BASE, etc...
|
||||
# This allows us to replace this in a future with another ldap library if needed
|
||||
SCOPE_BASE: int = S_BASE # pyright: ignore
|
||||
SCOPE_SUBTREE: int = S_SUBTREE # pyright: ignore
|
||||
SCOPE_ONELEVEL: int = S_ONELEVEL # pyright: ignore
|
||||
@ -56,7 +58,11 @@ from django.utils.translation import gettext as _
|
||||
from django.conf import settings
|
||||
|
||||
# So it is avaliable for importers
|
||||
from ldap.ldapobject import LDAPObject
|
||||
from ldap.ldapobject import LDAPObject as S_LDAPObject # pyright: ignore
|
||||
|
||||
# Reexporting, so we can use them as ldaputil.LDAPObject, etc...
|
||||
# This allows us to replace this in a future with another ldap library if needed
|
||||
LDAPObject: typing.TypeAlias = S_LDAPObject
|
||||
|
||||
from uds.core.util import utils
|
||||
|
||||
|
@ -44,18 +44,20 @@ from .linux_osmanager import LinuxOsManager
|
||||
from .linux_randompass_osmanager import LinuxRandomPassManager
|
||||
from .linux_ad_osmanager import LinuxOsADManager
|
||||
|
||||
# We know for sure __package__ is not None, because we are a submodules of a package
|
||||
_mypath = os.path.dirname(typing.cast(str, sys.modules[__package__].__file__)) # pyright: ignore
|
||||
|
||||
downloads_manager().register(
|
||||
f'udsactor_{VERSION}_all.deb',
|
||||
_('UDS Actor for Debian, Ubuntu, ... Linux machines <b>(Requires python >= 3.6)</b>'),
|
||||
os.path.dirname(typing.cast(str, sys.modules[__package__].__file__)) + f'/files/udsactor_{VERSION}_all.deb',
|
||||
_mypath + f'/files/udsactor_{VERSION}_all.deb',
|
||||
'application/x-debian-package',
|
||||
)
|
||||
|
||||
downloads_manager().register(
|
||||
f'udsactor-{VERSION}-1.noarch.rpm',
|
||||
_('UDS Actor for Centos, Fedora, RH, Suse, ... Linux machines <b>(Requires python >= 3.6)</b>'),
|
||||
os.path.dirname(typing.cast(str, sys.modules[__package__].__file__))
|
||||
+ f'/files/udsactor-{VERSION}-1.noarch.rpm',
|
||||
_mypath + f'/files/udsactor-{VERSION}-1.noarch.rpm',
|
||||
'application/x-redhat-package-manager',
|
||||
)
|
||||
|
||||
@ -64,8 +66,7 @@ downloads_manager().register(
|
||||
_(
|
||||
'UDS Actor for Debian based Linux machines. Used ONLY for static machines. <b>(Requires python >= 3.6)</b>'
|
||||
),
|
||||
os.path.dirname(typing.cast(str, sys.modules[__package__].__file__))
|
||||
+ f'/files/udsactor-unmanaged_{VERSION}_all.deb',
|
||||
_mypath + f'/files/udsactor-unmanaged_{VERSION}_all.deb',
|
||||
'application/x-debian-package',
|
||||
)
|
||||
|
||||
@ -74,30 +75,27 @@ downloads_manager().register(
|
||||
_(
|
||||
'UDS Actor for Centos, Fedora, RH, Suse, ... Linux machines. Used ONLY for static machines. <b>(Requires python >= 3.6)</b>'
|
||||
),
|
||||
os.path.dirname(typing.cast(str, sys.modules[__package__].__file__))
|
||||
+ f'/files/udsactor-unmanaged-{VERSION}-1.noarch.rpm',
|
||||
_mypath + f'/files/udsactor-unmanaged-{VERSION}-1.noarch.rpm',
|
||||
'application/x-redhat-package-manager',
|
||||
)
|
||||
|
||||
downloads_manager().register(
|
||||
'udsactor_2.2.0_legacy.deb',
|
||||
_('<b>Legacy</b> UDS Actor for Debian, Ubuntu, ... Linux machines <b>(Requires python 2.7)</b>'),
|
||||
os.path.dirname(typing.cast(str, sys.modules[__package__].__file__)) + '/files/udsactor_2.2.0_legacy.deb',
|
||||
_mypath + '/files/udsactor_2.2.0_legacy.deb',
|
||||
'application/x-debian-package',
|
||||
)
|
||||
|
||||
downloads_manager().register(
|
||||
'udsactor-legacy-2.2.1-1.noarch.rpm',
|
||||
_('<b>Legacy</b> UDS Actor for Centos, Fedora, RH, ... Linux machines <b>(Requires python 2.7)</b>'),
|
||||
os.path.dirname(typing.cast(str, sys.modules[__package__].__file__))
|
||||
+ '/files/udsactor-legacy-2.2.1-1.noarch.rpm',
|
||||
_mypath + '/files/udsactor-legacy-2.2.1-1.noarch.rpm',
|
||||
'application/x-redhat-package-manager',
|
||||
)
|
||||
|
||||
downloads_manager().register(
|
||||
'udsactor-opensuse-legacy-2.2.1-1.noarch.rpm',
|
||||
_('<b>Legacy</b> UDS Actor for OpenSUSE, ... Linux machines <b>(Requires python 2.7)</b>'),
|
||||
os.path.dirname(typing.cast(str, sys.modules[__package__].__file__))
|
||||
+ '/files/udsactor-opensuse-legacy-2.2.1-1.noarch.rpm',
|
||||
_mypath + '/files/udsactor-opensuse-legacy-2.2.1-1.noarch.rpm',
|
||||
'application/x-redhat-package-manager',
|
||||
)
|
||||
|
@ -44,18 +44,20 @@ from .windows import WindowsOsManager
|
||||
from .windows_domain import WinDomainOsManager
|
||||
from .windows_random import WinRandomPassManager
|
||||
|
||||
# We know for sure __package__ is not None, because we are a submodules of a package
|
||||
_mypath = os.path.dirname(typing.cast(str, sys.modules[__package__].__file__)) # pyright: ignore
|
||||
|
||||
|
||||
managers.downloads_manager().register(
|
||||
f'UDSActorSetup-{VERSION}.exe',
|
||||
_('UDS Actor for windows machines'),
|
||||
os.path.dirname(typing.cast(str, sys.modules[__package__].__file__))
|
||||
+ f'/files/UDSActorSetup-{VERSION}.exe',
|
||||
_mypath + f'/files/UDSActorSetup-{VERSION}.exe',
|
||||
'application/vnd.microsoft.portable-executable',
|
||||
)
|
||||
|
||||
managers.downloads_manager().register(
|
||||
f'UDSActorUnmanagedSetup-{VERSION}.exe',
|
||||
_('UDS Actor for Unmanaged windows machines. Used ONLY for static machines.'),
|
||||
os.path.dirname(typing.cast(str, sys.modules[__package__].__file__))
|
||||
+ f'/files/UDSActorUnmanagedSetup-{VERSION}.exe',
|
||||
_mypath + f'/files/UDSActorUnmanagedSetup-{VERSION}.exe',
|
||||
'application/vnd.microsoft.portable-executable',
|
||||
)
|
||||
|
@ -31,5 +31,5 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
# pyright: reportUnusedImport=false
|
||||
|
||||
from .provider_legacy import ProviderLegacy
|
||||
from .provider_legacy import OpenStackProviderLegacy
|
||||
from .provider import OpenStackProvider
|
||||
|
@ -44,11 +44,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def getApi(parameters: dict[str, str]) -> tuple[openstack.Client, bool]:
|
||||
from .provider_legacy import ProviderLegacy
|
||||
from .provider_legacy import OpenStackProviderLegacy
|
||||
from .provider import OpenStackProvider
|
||||
|
||||
provider = typing.cast(
|
||||
typing.Union[ProviderLegacy, OpenStackProvider],
|
||||
typing.Union[OpenStackProviderLegacy, OpenStackProvider],
|
||||
models.Provider.objects.get(uuid=parameters['prov_uuid']).get_instance(),
|
||||
)
|
||||
|
||||
@ -60,25 +60,20 @@ def getApi(parameters: dict[str, str]) -> tuple[openstack.Client, bool]:
|
||||
return (provider.api(parameters['project'], parameters['region']), use_subnets_names)
|
||||
|
||||
|
||||
def get_resources(
|
||||
parameters: dict[str, str]
|
||||
) -> types.ui.CallbackResultType:
|
||||
def get_resources(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
'''
|
||||
This helper is designed as a callback for Project Selector
|
||||
'''
|
||||
api, name_from_subnets = getApi(parameters)
|
||||
|
||||
zones = [gui.choice_item(z, z) for z in api.list_availability_zones()]
|
||||
zones = [gui.choice_item(z.id, z.name) for z in api.list_availability_zones()]
|
||||
networks = [
|
||||
gui.choice_item(z['id'], z['name'])
|
||||
for z in api.list_networks(name_from_subnets=name_from_subnets)
|
||||
gui.choice_item(z['id'], z['name']) for z in api.list_networks(name_from_subnets=name_from_subnets)
|
||||
]
|
||||
flavors = [gui.choice_item(z['id'], z['name']) for z in api.list_flavors()]
|
||||
securityGroups = [
|
||||
gui.choice_item(z['id'], z['name']) for z in api.list_security_groups()
|
||||
]
|
||||
securityGroups = [gui.choice_item(z['id'], z['name']) for z in api.list_security_groups()]
|
||||
volumeTypes = [gui.choice_item('-', _('None'))] + [
|
||||
gui.choice_item(t['id'], t['name']) for t in api.list_volume_types()
|
||||
gui.choice_item(t.id, t.name) for t in api.list_volume_types()
|
||||
]
|
||||
|
||||
data: types.ui.CallbackResultType = [
|
||||
@ -92,18 +87,14 @@ def get_resources(
|
||||
return data
|
||||
|
||||
|
||||
def get_volumes(
|
||||
parameters: dict[str, str]
|
||||
) -> types.ui.CallbackResultType:
|
||||
def get_volumes(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
'''
|
||||
This helper is designed as a callback for Zone Selector
|
||||
'''
|
||||
api, _ = getApi(parameters)
|
||||
# Source volumes are all available for us
|
||||
# volumes = [gui.choice_item(v['id'], v['name']) for v in api.listVolumes() if v['name'] != '' and v['availability_zone'] == parameters['availabilityZone']]
|
||||
volumes = [
|
||||
gui.choice_item(v['id'], v['name']) for v in api.list_volumes() if v['name'] != ''
|
||||
]
|
||||
volumes = [gui.choice_item(v.id, v.name) for v in api.list_volumes() if v.name]
|
||||
|
||||
data: types.ui.CallbackResultType = [
|
||||
{'name': 'volume', 'choices': volumes},
|
||||
|
@ -41,6 +41,8 @@ from uds.core import consts
|
||||
|
||||
from uds.core.util import security, cache, decorators
|
||||
|
||||
from . import types as openstack_types
|
||||
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
import requests
|
||||
@ -54,7 +56,12 @@ logger = logging.getLogger(__name__)
|
||||
# These are related to auth, compute & network basically
|
||||
|
||||
# Do not verify SSL conections right now
|
||||
VERIFY_SSL = False
|
||||
VERIFY_SSL: typing.Final[bool] = False
|
||||
VOLUMES_ENDPOINT_TYPES = [
|
||||
'volumev3',
|
||||
'volumev2',
|
||||
] # 'volume' is also valid, but it is deprecated A LONG TYPE AGO
|
||||
COMPUTE_ENDPOINT_TYPES = ['compute', 'compute_legacy']
|
||||
|
||||
|
||||
# Helpers
|
||||
@ -154,7 +161,6 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
_tokenid: typing.Optional[str]
|
||||
_catalog: typing.Optional[list[dict[str, typing.Any]]]
|
||||
_is_legacy: bool
|
||||
_volume: str
|
||||
_access: typing.Optional[str]
|
||||
_domain: str
|
||||
_username: str
|
||||
@ -178,7 +184,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
username: str,
|
||||
password: str,
|
||||
is_legacy: bool = True,
|
||||
use_ssl: bool = False,
|
||||
use_ssl: bool = False, # Only used for legacy
|
||||
projectid: typing.Optional[str] = None,
|
||||
region: typing.Optional[str] = None,
|
||||
access: typing.Optional[str] = None,
|
||||
@ -201,7 +207,6 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
self._project = None
|
||||
self._region = region
|
||||
self._timeout = 10
|
||||
self._volume = ''
|
||||
|
||||
if is_legacy:
|
||||
self._authurl = 'http{}://{}:{}/'.format('s' if use_ssl else '', host, port)
|
||||
@ -388,23 +393,26 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
# Now, if endpoints are present (only if tenant was specified), store them
|
||||
if self._projectid is not None:
|
||||
self._catalog = token['catalog']
|
||||
logger.debug('Catalog found: %s', self._catalog)
|
||||
# Check for the presence of the endpoint for volumes
|
||||
# Volume v2 api was deprecated in Pike release, and removed on Xena release
|
||||
# Volume v3 api is available since Mitaka. Both are API compatible
|
||||
if self._catalog:
|
||||
if any(v['type'] == 'volumev3' for v in self._catalog):
|
||||
self._volume = 'volumev3'
|
||||
else:
|
||||
self._volume = 'volumev2'
|
||||
# if self._catalog:
|
||||
# if any(v['type'] == 'volumev3' for v in self._catalog):
|
||||
# 'volumev3', 'volumev2' = 'volumev3'
|
||||
# else:
|
||||
# 'volumev3', 'volumev2' = 'volumev2'
|
||||
|
||||
def ensure_authenticated(self) -> None:
|
||||
if self._authenticated is False or self._projectid != self._authenticatedProjectId:
|
||||
self.authenticate_with_password()
|
||||
|
||||
@auth_required()
|
||||
def list_projects(self) -> list[typing.Any]:
|
||||
return list(
|
||||
get_recurring_url_json(
|
||||
@decorators.cached(prefix='prjs', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_projects(self) -> list[openstack_types.ProjectInfo]:
|
||||
return [
|
||||
openstack_types.ProjectInfo.from_dict(p)
|
||||
for p in get_recurring_url_json(
|
||||
self._authurl,
|
||||
'v3/users/{user_id}/projects'.format(user_id=self._userid),
|
||||
self._session,
|
||||
@ -413,63 +421,68 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
error_message='List Projects',
|
||||
timeout=self._timeout,
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
@auth_required()
|
||||
def list_regions(self) -> list[typing.Any]:
|
||||
return list(
|
||||
get_recurring_url_json(
|
||||
@decorators.cached(prefix='rgns', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_regions(self) -> list[openstack_types.RegionInfo]:
|
||||
return [
|
||||
openstack_types.RegionInfo.from_dict(r)
|
||||
for r in get_recurring_url_json(
|
||||
self._authurl,
|
||||
'v3/regions/',
|
||||
'v3/regions',
|
||||
self._session,
|
||||
headers=self._get_request_headers(),
|
||||
key='regions',
|
||||
error_message='List Regions',
|
||||
timeout=self._timeout,
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
@decorators.cached(prefix='svrs', timeout=consts.cache.DEFAULT_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_servers(
|
||||
self,
|
||||
detail: bool = False,
|
||||
params: typing.Optional[dict[str, str]] = None,
|
||||
) -> list[typing.Any]:
|
||||
return list(
|
||||
self._get_recurring_from_endpoint(
|
||||
endpoint_types=['compute', 'compute_legacy'],
|
||||
) -> list[openstack_types.VMInfo]:
|
||||
return [
|
||||
openstack_types.VMInfo.from_dict(s)
|
||||
for s in self._get_recurring_from_endpoint(
|
||||
endpoint_types=COMPUTE_ENDPOINT_TYPES,
|
||||
path='/servers' + ('/detail' if detail is True else ''),
|
||||
error_message='List Vms',
|
||||
key='servers',
|
||||
params=params,
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
@decorators.cached(prefix='imgs', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_images(self) -> list[typing.Any]:
|
||||
return list(
|
||||
self._get_recurring_from_endpoint(
|
||||
def list_images(self) -> list[openstack_types.ImageInfo]:
|
||||
return [
|
||||
openstack_types.ImageInfo.from_dict(i)
|
||||
for i in self._get_recurring_from_endpoint(
|
||||
endpoint_types=['image'],
|
||||
path='/v2/images?status=active',
|
||||
error_message='List Images',
|
||||
key='images',
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
@decorators.cached(prefix='volts', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_volume_types(self) -> list[typing.Any]:
|
||||
return list(
|
||||
self._get_recurring_from_endpoint(
|
||||
endpoint_types=[self._volume],
|
||||
@decorators.cached(prefix='volts', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_volume_types(self) -> list[openstack_types.VolumeTypeInfo]:
|
||||
return [
|
||||
openstack_types.VolumeTypeInfo.from_dict(t)
|
||||
for t in self._get_recurring_from_endpoint(
|
||||
endpoint_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path='/types',
|
||||
error_message='List Volume Types',
|
||||
key='volume_types',
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
# TODO: Remove this
|
||||
# return get_recurring_url_json(
|
||||
# self._get_endpoint_for(self._volume) + '/types',
|
||||
# self._get_endpoint_for('volumev3', 'volumev2') + '/types',
|
||||
# self._session,
|
||||
# headers=self._get_request_headers(),
|
||||
# key='volume_types',
|
||||
@ -478,19 +491,20 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
# )
|
||||
|
||||
@decorators.cached(prefix='vols', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_volumes(self) -> list[typing.Any]:
|
||||
return list(
|
||||
self._get_recurring_from_endpoint(
|
||||
endpoint_types=[self._volume],
|
||||
def list_volumes(self) -> list[openstack_types.VolumeInfo]:
|
||||
return [
|
||||
openstack_types.VolumeInfo.from_dict(v)
|
||||
for v in self._get_recurring_from_endpoint(
|
||||
endpoint_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path='/volumes/detail',
|
||||
error_message='List Volumes',
|
||||
key='volumes',
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
# TODO: Remove this
|
||||
# return get_recurring_url_json(
|
||||
# self._get_endpoint_for(self._volume) + '/volumes/detail',
|
||||
# self._get_endpoint_for('volumev3', 'volumev2') + '/volumes/detail',
|
||||
# self._session,
|
||||
# headers=self._get_request_headers(),
|
||||
# key='volumes',
|
||||
@ -500,11 +514,11 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
|
||||
def list_volume_snapshots(
|
||||
self, volume_id: typing.Optional[dict[str, typing.Any]] = None
|
||||
) -> list[typing.Any]:
|
||||
) -> list[openstack_types.VolumeSnapshotInfo]:
|
||||
return [
|
||||
snapshot
|
||||
openstack_types.VolumeSnapshotInfo.from_dict(snapshot)
|
||||
for snapshot in self._get_recurring_from_endpoint(
|
||||
endpoint_types=[self._volume],
|
||||
endpoint_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path='/snapshots',
|
||||
error_message='List snapshots',
|
||||
key='snapshots',
|
||||
@ -514,7 +528,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
|
||||
# TODO: Remove this
|
||||
# for s in get_recurring_url_json(
|
||||
# self._get_endpoint_for(self._volume) + '/snapshots',
|
||||
# self._get_endpoint_for('volumev3', 'volumev2') + '/snapshots',
|
||||
# self._session,
|
||||
# headers=self._get_request_headers(),
|
||||
# key='snapshots',
|
||||
@ -524,12 +538,12 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
# if volume_id is None or s['volume_id'] == volume_id:
|
||||
# yield s
|
||||
|
||||
@decorators.cached(prefix='azs', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_availability_zones(self) -> list[typing.Any]:
|
||||
@decorators.cached(prefix='azs', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_availability_zones(self) -> list[openstack_types.AvailabilityZoneInfo]:
|
||||
return [
|
||||
availability_zone['zoneName']
|
||||
openstack_types.AvailabilityZoneInfo.from_dict(availability_zone)
|
||||
for availability_zone in self._get_recurring_from_endpoint(
|
||||
endpoint_types=['compute', 'compute_legacy'],
|
||||
endpoint_types=COMPUTE_ENDPOINT_TYPES,
|
||||
path='/os-availability-zone',
|
||||
error_message='List Availability Zones',
|
||||
key='availabilityZoneInfo',
|
||||
@ -549,11 +563,11 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
# if az['zoneState']['available'] is True:
|
||||
# yield az['zoneName']
|
||||
|
||||
@decorators.cached(prefix='flvs', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
@decorators.cached(prefix='flvs', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_flavors(self) -> list[typing.Any]:
|
||||
return list(
|
||||
self._get_recurring_from_endpoint(
|
||||
endpoint_types=['compute', 'compute_legacy'],
|
||||
endpoint_types=COMPUTE_ENDPOINT_TYPES,
|
||||
path='/flavors',
|
||||
error_message='List Flavors',
|
||||
key='flavors',
|
||||
@ -681,7 +695,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
def get_server(self, server_id: str) -> dict[str, typing.Any]:
|
||||
r = self._request_from_endpoint(
|
||||
'get',
|
||||
endpoints_types=['compute', 'compute_legacy'],
|
||||
endpoints_types=COMPUTE_ENDPOINT_TYPES,
|
||||
path=f'/servers/{server_id}',
|
||||
error_message='Get Server information',
|
||||
)
|
||||
@ -690,14 +704,14 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
def get_volume(self, volumeId: str) -> dict[str, typing.Any]:
|
||||
r = self._request_from_endpoint(
|
||||
'get',
|
||||
endpoints_types=[self._volume],
|
||||
endpoints_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path=f'/volumes/{volumeId}',
|
||||
error_message='Get Volume information',
|
||||
)
|
||||
|
||||
# TODO: Remove this
|
||||
# r = self._session.get(
|
||||
# self._get_endpoint_for(self._volume) + '/volumes/{volume_id}'.format(volume_id=volumeId),
|
||||
# self._get_endpoint_for('volumev3', 'volumev2') + '/volumes/{volume_id}'.format(volume_id=volumeId),
|
||||
# headers=self._get_request_headers(),
|
||||
# timeout=self._timeout,
|
||||
# )
|
||||
@ -713,14 +727,14 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
r = self._request_from_endpoint(
|
||||
'get',
|
||||
endpoints_types=[self._volume],
|
||||
endpoints_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path=f'/snapshots/{snapshot_id}',
|
||||
error_message='Get Snaphost information',
|
||||
)
|
||||
|
||||
# TODO: Remove this
|
||||
# r = self._session.get(
|
||||
# self._get_endpoint_for(self._volume) + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshotId),
|
||||
# self._get_endpoint_for('volumev3', 'volumev2') + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshotId),
|
||||
# headers=self._get_request_headers(),
|
||||
# timeout=self._timeout,
|
||||
# )
|
||||
@ -744,7 +758,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
|
||||
r = self._request_from_endpoint(
|
||||
'put',
|
||||
endpoints_types=[self._volume],
|
||||
endpoints_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path=f'/snapshots/{snapshot_id}',
|
||||
data=json.dumps(data),
|
||||
error_message='Update Snaphost information',
|
||||
@ -752,7 +766,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
|
||||
# TODO: Remove this
|
||||
# r = self._session.put(
|
||||
# self._get_endpoint_for(self._volume) + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id),
|
||||
# self._get_endpoint_for('volumev3', 'volumev2') + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id),
|
||||
# data=json.dumps(data),
|
||||
# headers=self._get_request_headers(),
|
||||
# timeout=self._timeout,
|
||||
@ -779,7 +793,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
|
||||
r = self._request_from_endpoint(
|
||||
'post',
|
||||
endpoints_types=[self._volume],
|
||||
endpoints_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path=f'/volumes/{volume_id}',
|
||||
data=json.dumps(data),
|
||||
error_message='Get Volume information',
|
||||
@ -787,7 +801,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
|
||||
# TODO: Remove this
|
||||
# r = self._session.post(
|
||||
# self._get_endpoint_for(self._volume) + '/snapshots',
|
||||
# self._get_endpoint_for('volumev3', 'volumev2') + '/snapshots',
|
||||
# data=json.dumps(data),
|
||||
# headers=self._get_request_headers(),
|
||||
# timeout=self._timeout,
|
||||
@ -812,7 +826,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
|
||||
r = self._request_from_endpoint(
|
||||
'post',
|
||||
endpoints_types=[self._volume],
|
||||
endpoints_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path='/volumes',
|
||||
data=json.dumps(data),
|
||||
error_message='Create Volume from Snapshot',
|
||||
@ -820,7 +834,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
|
||||
# TODO: Remove this
|
||||
# r = self._session.post(
|
||||
# self._get_endpoint_for(self._volume) + '/volumes',
|
||||
# self._get_endpoint_for('volumev3', 'volumev2') + '/volumes',
|
||||
# data=json.dumps(data),
|
||||
# headers=self._get_request_headers(),
|
||||
# timeout=self._timeout,
|
||||
@ -869,7 +883,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
|
||||
r = self._request_from_endpoint(
|
||||
'post',
|
||||
endpoints_types=['compute', 'compute_legacy'],
|
||||
endpoints_types=COMPUTE_ENDPOINT_TYPES,
|
||||
path='/servers',
|
||||
data=json.dumps(data),
|
||||
error_message='Create instance from snapshot',
|
||||
@ -892,7 +906,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
# This does not returns anything
|
||||
self._request_from_endpoint(
|
||||
'delete',
|
||||
endpoints_types=['compute', 'compute_legacy'],
|
||||
endpoints_types=COMPUTE_ENDPOINT_TYPES,
|
||||
path=f'/servers/{server_id}',
|
||||
error_message='Cannot delete server (probably server does not exists).',
|
||||
)
|
||||
@ -917,14 +931,14 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
# This does not returns anything
|
||||
self._request_from_endpoint(
|
||||
'delete',
|
||||
endpoints_types=[self._volume],
|
||||
endpoints_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path=f'/snapshots/{snapshot_id}',
|
||||
error_message='Cannot remove snapshot.',
|
||||
)
|
||||
|
||||
# TODO: Remove this
|
||||
# r = self._session.delete(
|
||||
# self._get_endpoint_for(self._volume) + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshotId),
|
||||
# self._get_endpoint_for('volumev3', 'volumev2') + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshotId),
|
||||
# headers=self._get_request_headers(),
|
||||
# timeout=self._timeout,
|
||||
# )
|
||||
@ -937,7 +951,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
# this does not returns anything
|
||||
self._request_from_endpoint(
|
||||
'post',
|
||||
endpoints_types=['compute', 'compute_legacy'],
|
||||
endpoints_types=COMPUTE_ENDPOINT_TYPES,
|
||||
path=f'/servers/{server_id}/action',
|
||||
data='{"os-start": null}',
|
||||
error_message='Starting server',
|
||||
@ -957,7 +971,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
# this does not returns anything
|
||||
self._request_from_endpoint(
|
||||
'post',
|
||||
endpoints_types=['compute', 'compute_legacy'],
|
||||
endpoints_types=COMPUTE_ENDPOINT_TYPES,
|
||||
path=f'/servers/{server_id}/action',
|
||||
data='{"os-stop": null}',
|
||||
error_message='Stoping server',
|
||||
@ -978,7 +992,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
# this does not returns anything
|
||||
self._request_from_endpoint(
|
||||
'post',
|
||||
endpoints_types=['compute', 'compute_legacy'],
|
||||
endpoints_types=COMPUTE_ENDPOINT_TYPES,
|
||||
path=f'/servers/{server_id}/action',
|
||||
data='{"suspend": null}',
|
||||
error_message='Suspending server',
|
||||
@ -998,7 +1012,7 @@ class Client: # pylint: disable=too-many-public-methods
|
||||
# This does not returns anything
|
||||
self._request_from_endpoint(
|
||||
'post',
|
||||
endpoints_types=['compute', 'compute_legacy'],
|
||||
endpoints_types=COMPUTE_ENDPOINT_TYPES,
|
||||
path=f'/servers/{server_id}/action',
|
||||
data='{"resume": null}',
|
||||
error_message='Resuming server',
|
||||
|
192
server/src/uds/services/OpenStack/openstack/types.py
Normal file
192
server/src/uds/services/OpenStack/openstack/types.py
Normal file
@ -0,0 +1,192 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2012-2019 Virtual Cable S.L.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import datetime
|
||||
import typing
|
||||
import dataclasses
|
||||
import enum
|
||||
|
||||
|
||||
class State(enum.StrEnum):
|
||||
ACTIVE = 'ACTIVE'
|
||||
BUILDING = 'BUILDING'
|
||||
DELETED = 'DELETED'
|
||||
ERROR = 'ERROR'
|
||||
HARD_REBOOT = 'HARD_REBOOT'
|
||||
MIGRATING = 'MIGRATING'
|
||||
PASSWORD = 'PASSWORD'
|
||||
PAUSED = 'PAUSED'
|
||||
REBOOT = 'REBOOT'
|
||||
REBUILD = 'REBUILD'
|
||||
RESCUED = 'RESCUED'
|
||||
RESIZED = 'RESIZED'
|
||||
REVERT_RESIZE = 'REVERT_RESIZE'
|
||||
SOFT_DELETED = 'SOFT_DELETED'
|
||||
STOPPED = 'STOPPED'
|
||||
SUSPENDED = 'SUSPENDED'
|
||||
UNKNOWN = 'UNKNOWN'
|
||||
VERIFY_RESIZE = 'VERIFY_RESIZE'
|
||||
SHUTOFF = 'SHUTOFF'
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class VMInfo:
|
||||
id: str
|
||||
name: str
|
||||
href: str = ''
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, typing.Any]) -> 'VMInfo':
|
||||
# Look for self link
|
||||
href: str = ''
|
||||
for link in d.get('links', []):
|
||||
try:
|
||||
if link.get('rel', '') == 'self':
|
||||
href = typing.cast(str, link['href'])
|
||||
break
|
||||
except Exception:
|
||||
pass # Just ignore any error here
|
||||
return VMInfo(
|
||||
id=d['id'],
|
||||
name=d['name'],
|
||||
href=href,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ProjectInfo:
|
||||
id: str
|
||||
name: str
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, typing.Any]) -> 'ProjectInfo':
|
||||
return ProjectInfo(
|
||||
id=d['id'],
|
||||
name=d['name'],
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RegionInfo:
|
||||
id: str
|
||||
name: str
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, typing.Any]) -> 'RegionInfo':
|
||||
# Try to guess name
|
||||
# Api definition does not includes name, nor locale, but some implementations includes it
|
||||
name: str = d['id']
|
||||
if 'name' in d:
|
||||
name = d['name']
|
||||
# Mayby it has a locales dict, if this is the case and it contains en-us (case insensitive), we will use it
|
||||
if 'locales' in d and isinstance(d['locales'], dict):
|
||||
if 'en-us' in d['locales'] and isinstance(d['locales']['en-us'], str):
|
||||
name = d['locales']['en-us']
|
||||
return RegionInfo(
|
||||
id=d['id'],
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ImageInfo:
|
||||
id: str
|
||||
name: str
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, typing.Any]) -> 'ImageInfo':
|
||||
return ImageInfo(
|
||||
id=d['id'],
|
||||
name=d.get('name', d['id']),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class VolumeInfo:
|
||||
id: str
|
||||
name: str
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, typing.Any]) -> 'VolumeInfo':
|
||||
return VolumeInfo(
|
||||
id=d['id'],
|
||||
name=d['name'] or '',
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class VolumeSnapshotInfo:
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
status: str
|
||||
size: int # in gibibytes (GiB)
|
||||
created_at: datetime.datetime
|
||||
updated_at: datetime.datetime
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, typing.Any]) -> 'VolumeSnapshotInfo':
|
||||
# Try to get created_at and updated_at, if not possible, just ignore it
|
||||
return VolumeSnapshotInfo(
|
||||
id=d['id'],
|
||||
name=d['name'],
|
||||
description=d['description'] or '',
|
||||
status=d['status'],
|
||||
size=d['size'],
|
||||
created_at=datetime.datetime.fromisoformat(d['created_at']),
|
||||
updated_at=datetime.datetime.fromisoformat(d['updated_at']),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class VolumeTypeInfo:
|
||||
id: str
|
||||
name: str
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, typing.Any]) -> 'VolumeTypeInfo':
|
||||
return VolumeTypeInfo(
|
||||
id=d['id'],
|
||||
name=d['name'],
|
||||
)
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AvailabilityZoneInfo:
|
||||
id: str
|
||||
name: str
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, typing.Any]) -> 'AvailabilityZoneInfo':
|
||||
return AvailabilityZoneInfo(
|
||||
id=d['zoneName'],
|
||||
name=d['zoneName'],
|
||||
)
|
@ -40,7 +40,7 @@ from django.utils.translation import gettext_noop as _
|
||||
from uds.core import environment, types, consts
|
||||
from uds.core.services import ServiceProvider
|
||||
from uds.core.ui import gui
|
||||
from uds.core.util import validators
|
||||
from uds.core.util import validators, fields
|
||||
from uds.core.util.decorators import cached
|
||||
|
||||
from . import openstack
|
||||
@ -60,7 +60,7 @@ INTERFACE_VALUES = [
|
||||
]
|
||||
|
||||
|
||||
class ProviderLegacy(ServiceProvider):
|
||||
class OpenStackProviderLegacy(ServiceProvider):
|
||||
"""
|
||||
This class represents the sample services provider
|
||||
|
||||
@ -157,44 +157,11 @@ class ProviderLegacy(ServiceProvider):
|
||||
required=True,
|
||||
)
|
||||
|
||||
concurrent_creation_limit = gui.NumericField(
|
||||
length=3,
|
||||
label=_('Creation concurrency'),
|
||||
default=10,
|
||||
min_value=1,
|
||||
max_value=65536,
|
||||
order=50,
|
||||
tooltip=_('Maximum number of concurrently creating VMs'),
|
||||
required=True,
|
||||
tab=types.ui.Tab.ADVANCED,
|
||||
old_field_name='maxPreparingServices',
|
||||
)
|
||||
concurrent_removal_limit = gui.NumericField(
|
||||
length=3,
|
||||
label=_('Removal concurrency'),
|
||||
default=5,
|
||||
min_value=1,
|
||||
max_value=65536,
|
||||
order=51,
|
||||
tooltip=_('Maximum number of concurrently removing VMs'),
|
||||
required=True,
|
||||
tab=types.ui.Tab.ADVANCED,
|
||||
old_field_name='maxRemovingServices',
|
||||
)
|
||||
concurrent_creation_limit = fields.concurrent_creation_limit_field()
|
||||
concurrent_removal_limit = fields.concurrent_removal_limit_field()
|
||||
timeout = fields.timeout_field(default=10)
|
||||
|
||||
timeout = gui.NumericField(
|
||||
length=3,
|
||||
label=_('Timeout'),
|
||||
default=10,
|
||||
min_value=1,
|
||||
max_value=128,
|
||||
order=99,
|
||||
tooltip=_('Timeout in seconds of connection to OpenStack'),
|
||||
required=True,
|
||||
tab=types.ui.Tab.ADVANCED,
|
||||
)
|
||||
|
||||
httpsProxy = gui.TextField(
|
||||
https_proxy = gui.TextField(
|
||||
length=96,
|
||||
label=_('Proxy'),
|
||||
order=91,
|
||||
@ -203,6 +170,7 @@ class ProviderLegacy(ServiceProvider):
|
||||
),
|
||||
required=False,
|
||||
tab=types.ui.Tab.ADVANCED,
|
||||
old_field_name='httpsProxy',
|
||||
)
|
||||
|
||||
# tenant = gui.TextField(length=64, label=_('Project'), order=6, tooltip=_('Project (tenant) for this provider'), required=True, default='')
|
||||
@ -224,8 +192,8 @@ class ProviderLegacy(ServiceProvider):
|
||||
|
||||
def api(self, projectid: typing.Optional[str]=None, region: typing.Optional[str]=None) -> openstack.Client:
|
||||
proxies: typing.Optional[dict[str, str]] = None
|
||||
if self.httpsProxy.value.strip():
|
||||
proxies = {'https': self.httpsProxy.value}
|
||||
if self.https_proxy.value.strip():
|
||||
proxies = {'https': self.https_proxy.value}
|
||||
return openstack.Client(
|
||||
self.host.value,
|
||||
self.port.value,
|
||||
@ -277,7 +245,7 @@ class ProviderLegacy(ServiceProvider):
|
||||
second is an String with error, preferably internacionalizated..
|
||||
|
||||
"""
|
||||
return ProviderLegacy(env, data).test_connection()
|
||||
return OpenStackProviderLegacy(env, data).test_connection()
|
||||
|
||||
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT)
|
||||
def is_available(self) -> bool:
|
||||
|
@ -50,9 +50,9 @@ logger = logging.getLogger(__name__)
|
||||
if typing.TYPE_CHECKING:
|
||||
from . import openstack
|
||||
from .provider import OpenStackProvider
|
||||
from .provider_legacy import ProviderLegacy
|
||||
from .provider_legacy import OpenStackProviderLegacy
|
||||
|
||||
AnyOpenStackProvider = typing.Union[OpenStackProvider, ProviderLegacy]
|
||||
AnyOpenStackProvider = typing.Union[OpenStackProvider, OpenStackProviderLegacy]
|
||||
|
||||
|
||||
class OpenStackLiveService(services.Service):
|
||||
@ -221,33 +221,33 @@ class OpenStackLiveService(services.Service):
|
||||
api = self.provider().api()
|
||||
|
||||
# Checks if legacy or current openstack provider
|
||||
parentCurrent = (
|
||||
parent = (
|
||||
typing.cast('OpenStackProvider', self.provider())
|
||||
if not self.provider().legacy
|
||||
else None
|
||||
)
|
||||
|
||||
if parentCurrent and parentCurrent.region.value:
|
||||
if parent and parent.region.value:
|
||||
regions = [
|
||||
gui.choice_item(parentCurrent.region.value, parentCurrent.region.value)
|
||||
gui.choice_item(parent.region.value, parent.region.value)
|
||||
]
|
||||
else:
|
||||
regions = [gui.choice_item(r['id'], r['id']) for r in api.list_regions()]
|
||||
regions = [gui.choice_item(r.id, r.name) for r in api.list_regions()]
|
||||
|
||||
self.region.set_choices(regions)
|
||||
|
||||
if parentCurrent and parentCurrent.tenant.value:
|
||||
if parent and parent.tenant.value:
|
||||
tenants = [
|
||||
gui.choice_item(parentCurrent.tenant.value, parentCurrent.tenant.value)
|
||||
gui.choice_item(parent.tenant.value, parent.tenant.value)
|
||||
]
|
||||
else:
|
||||
tenants = [gui.choice_item(t['id'], t['name']) for t in api.list_projects()]
|
||||
tenants = [gui.choice_item(t.id, t.name) for t in api.list_projects()]
|
||||
self.project.set_choices(tenants)
|
||||
|
||||
# So we can instantiate parent to get API
|
||||
logger.debug(self.provider().serialize())
|
||||
|
||||
self.parent_uuid = self.provider().get_uuid()
|
||||
self.parent_uuid.value = self.provider().get_uuid()
|
||||
|
||||
@property
|
||||
def api(self) -> 'openstack.Client':
|
||||
|
157
server/tests/services/openstack/fixtures.py
Normal file
157
server/tests/services/openstack/fixtures.py
Normal file
@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2024 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import contextlib
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from uds.core import environment, types
|
||||
from uds.core.ui.user_interface import gui
|
||||
|
||||
from ...utils.autospec import autospec, AutoSpecMethodInfo
|
||||
|
||||
from uds.services.OpenStack import provider, provider_legacy, openstack as client
|
||||
|
||||
|
||||
GUEST_IP_ADDRESS: typing.Final[str] = '1.0.0.1'
|
||||
|
||||
CONSOLE_CONNECTION_INFO: typing.Final[types.services.ConsoleConnectionInfo] = (
|
||||
types.services.ConsoleConnectionInfo(
|
||||
type='spice',
|
||||
address=GUEST_IP_ADDRESS,
|
||||
port=5900,
|
||||
secure_port=5901,
|
||||
cert_subject='',
|
||||
ticket=types.services.ConsoleConnectionTicket(value='ticket'),
|
||||
ca='',
|
||||
proxy='',
|
||||
monitors=1,
|
||||
)
|
||||
)
|
||||
|
||||
# Methods that returns None or "internal" methods are not tested
|
||||
# The idea behind this is to allow testing the provider, service and deployment classes
|
||||
# without the need of a real OpenStack environment
|
||||
CLIENT_METHODS_INFO: typing.Final[list[AutoSpecMethodInfo]] = [
|
||||
# connect returns None
|
||||
# Test method
|
||||
# AutoSpecMethodInfo(client.Client.list_projects, return_value=True),
|
||||
# AutoSpecMethodInfo(
|
||||
# client.ProxmoxClient.get_node_stats,
|
||||
# method=lambda node, **kwargs: next(filter(lambda n: n.name == node, NODE_STATS)), # pyright: ignore
|
||||
# ),
|
||||
]
|
||||
|
||||
PROVIDER_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
|
||||
'endpoint': 'host',
|
||||
'access': 'public',
|
||||
'domain': 'domain',
|
||||
'username': 'username',
|
||||
'password': 'password',
|
||||
'concurrent_creation_limit': 1,
|
||||
'concurrent_removal_limit': 1,
|
||||
'timeout': 10,
|
||||
'tenant': 'tenant',
|
||||
'region': 'region',
|
||||
'use_subnets_name': False,
|
||||
'https_proxy': 'https_proxy',
|
||||
}
|
||||
|
||||
PROVIDER_LEGACY_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
|
||||
'host': 'host',
|
||||
'port': 5000,
|
||||
'ssl': False,
|
||||
'access': 'public',
|
||||
'domain': 'domain',
|
||||
'username': 'username',
|
||||
'password': 'password',
|
||||
'concurrent_creation_limit': 1,
|
||||
'concurrent_removal_limit': 1,
|
||||
'timeout': 10,
|
||||
'https_proxy': 'https_proxy',
|
||||
}
|
||||
|
||||
|
||||
SERVICE_VALUES_DICT: typing.Final[gui.ValuesDictType] = {}
|
||||
|
||||
|
||||
def create_client_mock() -> mock.Mock:
|
||||
"""
|
||||
Create a mock of ProxmoxClient
|
||||
"""
|
||||
return autospec(client.Client, CLIENT_METHODS_INFO)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def patch_provider_api(
|
||||
legacy: bool = False,
|
||||
**kwargs: typing.Any,
|
||||
) -> typing.Generator[mock.Mock, None, None]:
|
||||
client = create_client_mock()
|
||||
path = (
|
||||
'uds.services.OpenStack.provider_legacy.OpenStackProviderLegacy'
|
||||
if legacy
|
||||
else 'uds.services.OpenStack.provider.OpenStackProvider'
|
||||
)
|
||||
try:
|
||||
mock.patch(path + 'api', return_value=client).start()
|
||||
yield client
|
||||
finally:
|
||||
mock.patch.stopall()
|
||||
|
||||
|
||||
def create_provider(**kwargs: typing.Any) -> provider.OpenStackProvider:
|
||||
"""
|
||||
Create a provider
|
||||
"""
|
||||
values = PROVIDER_VALUES_DICT.copy()
|
||||
values.update(kwargs)
|
||||
|
||||
uuid_ = str(uuid.uuid4())
|
||||
return provider.OpenStackProvider(
|
||||
environment=environment.Environment.private_environment(uuid), values=values, uuid=uuid_
|
||||
)
|
||||
|
||||
|
||||
def create_provider_legacy(**kwargs: typing.Any) -> provider_legacy.OpenStackProviderLegacy:
|
||||
"""
|
||||
Create a provider legacy
|
||||
"""
|
||||
values = PROVIDER_LEGACY_VALUES_DICT.copy()
|
||||
values.update(kwargs)
|
||||
|
||||
uuid_ = str(uuid.uuid4())
|
||||
return provider_legacy.OpenStackProviderLegacy(
|
||||
environment=environment.Environment.private_environment(uuid), values=values, uuid=uuid_
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user