1
0
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:
Adolfo Gómez García 2024-03-09 04:59:21 +01:00
parent 466bda440e
commit 62742e9438
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
10 changed files with 485 additions and 157 deletions

View File

@ -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

View File

@ -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',
)

View File

@ -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',
)

View File

@ -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

View File

@ -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},

View File

@ -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',

View 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'],
)

View File

@ -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:

View File

@ -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':

View 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_
)