1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-22 13:34:04 +03:00

Fixing up openstack client, adding support for Application Credentials

This commit is contained in:
Adolfo Gómez García 2024-07-16 21:21:58 +02:00
parent 773c3b5b50
commit 2a4ccac195
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
9 changed files with 300 additions and 114 deletions

View File

@ -54,7 +54,7 @@ from uds.services.OpenStack import (
service_fixed,
deployment_fixed,
)
from uds.services.OpenStack.openstack import openstack_client, types as openstack_types
from uds.services.OpenStack.openstack import client, types as openstack_types
AnyOpenStackProvider: typing.TypeAlias = typing.Union[
provider.OpenStackProvider, provider_legacy.OpenStackProviderLegacy
@ -251,89 +251,89 @@ def random_element(lst: list[T], *args: typing.Any, **kwargs: typing.Any) -> T:
# 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]] = [
AutoSpecMethodInfo(openstack_client.OpenstackClient.list_flavors, returns=FLAVORS_LIST),
AutoSpecMethodInfo(client.OpenStackClient.list_flavors, returns=FLAVORS_LIST),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.list_availability_zones, returns=AVAILABILITY_ZONES_LIST
client.OpenStackClient.list_availability_zones, returns=AVAILABILITY_ZONES_LIST
),
AutoSpecMethodInfo(openstack_client.OpenstackClient.list_projects, returns=PROJECTS_LIST),
AutoSpecMethodInfo(openstack_client.OpenstackClient.list_regions, returns=REGIONS_LIST),
AutoSpecMethodInfo(openstack_client.OpenstackClient.list_servers, returns=SERVERS_LIST),
AutoSpecMethodInfo(openstack_client.OpenstackClient.list_images, returns=IMAGES_LIST),
AutoSpecMethodInfo(openstack_client.OpenstackClient.list_volume_types, returns=VOLUMES_TYPE_LIST),
AutoSpecMethodInfo(openstack_client.OpenstackClient.list_volumes, returns=VOLUMES_LIST),
AutoSpecMethodInfo(client.OpenStackClient.list_projects, returns=PROJECTS_LIST),
AutoSpecMethodInfo(client.OpenStackClient.list_regions, returns=REGIONS_LIST),
AutoSpecMethodInfo(client.OpenStackClient.list_servers, returns=SERVERS_LIST),
AutoSpecMethodInfo(client.OpenStackClient.list_images, returns=IMAGES_LIST),
AutoSpecMethodInfo(client.OpenStackClient.list_volume_types, returns=VOLUMES_TYPE_LIST),
AutoSpecMethodInfo(client.OpenStackClient.list_volumes, returns=VOLUMES_LIST),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.list_volume_snapshots, returns=VOLUME_SNAPSHOTS_LIST
client.OpenStackClient.list_volume_snapshots, returns=VOLUME_SNAPSHOTS_LIST
),
AutoSpecMethodInfo(openstack_client.OpenstackClient.list_networks, returns=NETWORKS_LIST),
AutoSpecMethodInfo(openstack_client.OpenstackClient.list_ports, returns=PORTS_LIST),
AutoSpecMethodInfo(client.OpenStackClient.list_networks, returns=NETWORKS_LIST),
AutoSpecMethodInfo(client.OpenStackClient.list_ports, returns=PORTS_LIST),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.list_security_groups, returns=SECURITY_GROUPS_LIST
client.OpenStackClient.list_security_groups, returns=SECURITY_GROUPS_LIST
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.get_server,
client.OpenStackClient.get_server,
returns=search_id,
partial_args=(SERVERS_LIST,),
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.get_volume,
client.OpenStackClient.get_volume,
returns=search_id,
partial_args=(VOLUMES_LIST,),
), # pyright: ignore
AutoSpecMethodInfo(
openstack_client.OpenstackClient.get_volume_snapshot,
client.OpenStackClient.get_volume_snapshot,
returns=search_id,
partial_args=(VOLUME_SNAPSHOTS_LIST,),
), # pyright: ignore
AutoSpecMethodInfo(
openstack_client.OpenstackClient.update_snapshot,
client.OpenStackClient.update_snapshot,
returns=search_id,
partial_args=(VOLUME_SNAPSHOTS_LIST,),
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.create_volume_snapshot,
client.OpenStackClient.create_volume_snapshot,
returns=random_element,
partial_args=(VOLUME_SNAPSHOTS_LIST,),
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.create_volume_from_snapshot,
client.OpenStackClient.create_volume_from_snapshot,
returns=random_element,
partial_args=(VOLUMES_LIST,),
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.create_server_from_snapshot,
client.OpenStackClient.create_server_from_snapshot,
returns=random_element,
partial_args=(SERVERS_LIST,),
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.test_connection,
client.OpenStackClient.test_connection,
returns=True,
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.is_available,
client.OpenStackClient.is_available,
returns=True,
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.start_server,
client.OpenStackClient.start_server,
returns=set_vm_state,
partial_kwargs={'state': openstack_types.PowerState.RUNNING},
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.stop_server,
client.OpenStackClient.stop_server,
returns=set_vm_state,
partial_kwargs={'state': openstack_types.PowerState.SHUTDOWN},
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.reboot_server,
client.OpenStackClient.reboot_server,
returns=set_vm_state,
partial_kwargs={'state': openstack_types.PowerState.RUNNING},
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.suspend_server,
client.OpenStackClient.suspend_server,
returns=set_vm_state,
partial_kwargs={'state': openstack_types.PowerState.SUSPENDED},
),
AutoSpecMethodInfo(
openstack_client.OpenstackClient.resume_server,
client.OpenStackClient.resume_server,
returns=set_vm_state,
partial_kwargs={'state': openstack_types.PowerState.RUNNING},
),
@ -360,6 +360,7 @@ PROVIDER_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
'region': 'region',
'use_subnets_name': False,
'https_proxy': 'https_proxy',
'verify_ssl': False,
}
PROVIDER_LEGACY_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
@ -404,7 +405,7 @@ def create_client_mock() -> mock.Mock:
"""
Create a mock of ProxmoxClient
"""
return autospec(openstack_client.OpenstackClient, CLIENT_METHODS_INFO)
return autospec(client.OpenStackClient, CLIENT_METHODS_INFO)
@contextlib.contextmanager

View File

@ -0,0 +1,111 @@
# -*- 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 logging
import typing
from uds.services.OpenStack.openstack import (
types as openstack_types,
client as openstack_client,
)
from tests.utils import vars
from tests.utils import helpers
from tests.utils.test import UDSTransactionTestCase
logger = logging.getLogger(__name__)
class TestOpenStackClient(UDSTransactionTestCase):
_identity_endpoint: str
_domain: str
_username: str
_password: str
_auth_method: openstack_types.AuthMethod
_projectid: str
oclient: openstack_client.OpenStackClient
def get_client(self, use_project_id: bool = True) -> None:
self.oclient = openstack_client.OpenStackClient(
identity_endpoint=self._identity_endpoint,
domain=self._domain,
username=self._username,
password=self._password,
auth_method=self._auth_method,
projectid=self._projectid if use_project_id else None,
verify_ssl=False,
)
def setUp(self) -> None:
v = vars.get_vars('openstack')
if not v:
self.skipTest('No openstack vars')
self._identity_endpoint = v['identity_endpoint']
self._domain = v['domain']
self._username = v['username']
self._password = v['password']
self._auth_method = openstack_types.AuthMethod.from_str(v['auth_method'])
self._projectid = v['project_id']
self.get_client()
@contextlib.contextmanager
def create_test_volume(self) -> typing.Iterator[openstack_types.VolumeInfo]:
volume = self.oclient.t_create_volume(
name='uds-test-volume' + helpers.random_string(5),
size=1,
)
try:
yield volume
finally:
self.oclient.t_delete_volume(volume.id)
def test_list_volumes(self) -> None:
with self.create_test_volume() as volume:
with self.create_test_volume() as volume2:
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)
# if no project id, should fail
self.get_client(use_project_id=False)
with self.assertRaises(Exception):
self.oclient.list_volumes()

View File

@ -38,12 +38,12 @@ from uds import models
from uds.core import types
from uds.core.ui import gui
from .openstack import openstack_client
from .openstack import client
logger = logging.getLogger(__name__)
def get_api(parameters: dict[str, str]) -> tuple[openstack_client.OpenstackClient, bool]:
def get_api(parameters: dict[str, str]) -> tuple[client.OpenStackClient, bool]:
from .provider_legacy import OpenStackProviderLegacy
from .provider import OpenStackProvider

View File

@ -57,7 +57,6 @@ logger = logging.getLogger(__name__)
# These are related to auth, compute & network basically
# Do not verify SSL conections right now
VERIFY_SSL: typing.Final[bool] = False
VOLUMES_ENDPOINT_TYPES = [
'volumev3',
'volumev2',
@ -77,7 +76,7 @@ def auth_required(
def decorator(func: collections.abc.Callable[P, T]) -> collections.abc.Callable[P, T]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> typing.Any:
obj = typing.cast('OpenstackClient', args[0])
obj = typing.cast('OpenStackClient', args[0])
if for_project is True:
if obj._projectid is None:
raise Exception('Need a project for method {}'.format(func))
@ -89,10 +88,10 @@ def auth_required(
return decorator
def cache_key_helper(obj: 'OpenstackClient') -> str:
def cache_key_helper(obj: 'OpenStackClient') -> str:
return '_'.join(
[
obj._authurl,
obj._identity_endpoint,
obj._domain,
obj._username,
obj._password,
@ -103,10 +102,10 @@ def cache_key_helper(obj: 'OpenstackClient') -> str:
)
class OpenstackClient: # pylint: disable=too-many-public-methods
class OpenStackClient: # pylint: disable=too-many-public-methods
_authenticated: bool
_authenticatedProjectId: typing.Optional[str]
_authurl: str
_authenticated_projectid: typing.Optional[str]
_identity_endpoint: str
_tokenid: typing.Optional[str]
_catalog: typing.Optional[list[dict[str, typing.Any]]]
_is_legacy: bool
@ -114,9 +113,9 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
_domain: str
_username: str
_password: str
_auth_method: openstack_types.AuthMethod
_userid: typing.Optional[str]
_projectid: typing.Optional[str]
_project: typing.Optional[str]
_region: typing.Optional[str]
_timeout: int
_session: 'requests.Session'
@ -127,45 +126,46 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
# Legacyversion is True for versions <= Ocata
def __init__(
self,
host: str,
port: typing.Union[str, int],
identity_endpoint: str,
domain: str,
username: str,
password: str,
is_legacy: bool = True,
port: int = -1, # Only used for legacy
use_ssl: bool = False, # Only used for legacy
projectid: typing.Optional[str] = None,
region: typing.Optional[str] = None,
access: typing.Optional[openstack_types.AccessType] = None,
proxies: typing.Optional[dict[str, str]] = None,
timeout: int = 10,
verify_ssl: bool = True,
auth_method: openstack_types.AuthMethod = openstack_types.AuthMethod.PASSWORD,
):
self._session = security.secure_requests_session(verify=VERIFY_SSL)
self._session = security.secure_requests_session(verify=verify_ssl)
if proxies:
self._session.proxies = proxies
self._authenticated = False
self._authenticatedProjectId = None
self._authenticated_projectid = None
self._tokenid = None
self._catalog = None
self._is_legacy = is_legacy
self._is_legacy = port != -1 # If port is present, we are using legacy
self._access = openstack_types.AccessType.PUBLIC if access is None else access
self._domain, self._username, self._password = domain, username, password
self._domain, self._username, self._password = domain or 'Default', username, password
self._userid = None
self._projectid = projectid
self._project = None
self._region = region
self._timeout = timeout
self._auth_method = auth_method
if is_legacy:
self._authurl = 'http{}://{}:{}/'.format('s' if use_ssl else '', host, port)
if self._is_legacy:
self._identity_endpoint = 'http{}://{}:{}/'.format('s' if use_ssl else '', identity_endpoint, port)
else:
self._authurl = host # Host contains auth URL
if self._authurl[-1] != '/':
self._authurl += '/'
self._identity_endpoint = identity_endpoint # Host contains auth URL
if self._identity_endpoint[-1] != '/':
self._identity_endpoint += '/'
self.cache = cache.Cache(f'openstack_{host}_{port}_{domain}_{username}_{projectid}_{region}')
self.cache = cache.Cache(f'openstack_{identity_endpoint}_{port}_{domain}_{username}_{projectid}_{region}')
def _get_endpoints_for(self, *endpoint_types: str) -> collections.abc.Generator[str, None, None]:
def inner_get(for_type: str) -> collections.abc.Generator[str, None, None]:
@ -238,7 +238,7 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
) -> typing.Any:
cache_key = ''.join(endpoints_types)
found_endpoints = self._get_endpoints_iterable(cache_key, *endpoints_types)
for i, endpoint in enumerate(found_endpoints):
try:
logger.debug(
@ -252,7 +252,7 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
timeout=self._timeout,
)
OpenstackClient._ensure_valid_response(r, error_message, expects_json=expects_json)
OpenStackClient._ensure_valid_response(r, error_message, expects_json=expects_json)
logger.debug('Result: %s', r.content)
return r
except Exception as e:
@ -285,7 +285,7 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
self.cache.put(
cache_key, endpoint, consts.cache.EXTREME_CACHE_TIMEOUT
) # Cache endpoint for a very long time
yield from OpenstackClient._get_recurring_url_json(
yield from OpenStackClient._get_recurring_url_json(
endpoint=endpoint,
path=path,
session=self._session,
@ -305,42 +305,61 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
raise e
logger.warning('Error requesting %s: %s (%s)', endpoint + path, e, error_message)
self.cache.remove(cache_key)
def set_projectid(self, projectid: str) -> None:
self._projectid = projectid
def authenticate_with_password(self) -> None:
def authenticate(self) -> None:
# logger.debug('Authenticating...')
data: dict[str, typing.Any] = {
'auth': {
'identity': {
'methods': ['password'],
'password': {
'user': {
'name': self._username,
'domain': {'name': 'Default' if not self._domain else self._domain},
'password': self._password,
}
},
data: dict[str, typing.Any]
if self._auth_method == openstack_types.AuthMethod.APPLICATION_CREDENTIAL:
data = {
'auth': {
'identity': {
'methods': ['application_credential'],
'application_credential': {
'id': self._username,
'secret': self._password,
},
}
}
}
else:
data = {
'auth': {
'identity': {
'methods': ['password'],
'password': {
'user': {
'name': self._username,
'domain': {'name': 'Default' if not self._domain else self._domain},
'password': self._password,
}
},
}
}
}
}
if self._projectid is None:
self._authenticatedProjectId = None
self._authenticated_projectid = None
if self._is_legacy:
data['auth']['scope'] = 'unscoped'
else:
self._authenticatedProjectId = self._projectid
data['auth']['scope'] = {'project': {'id': self._projectid, 'domain': {'name': self._domain}}}
self._authenticated_projectid = self._projectid
# Scope only if project is present and auth method is password, app credentials is implicit...
if self._auth_method == openstack_types.AuthMethod.PASSWORD:
data['auth']['scope'] = {'project': {'id': self._projectid, 'domain': {'name': self._domain}}}
# logger.debug('Request data: {}'.format(data))
r = self._session.post(
self._authurl + 'v3/auth/tokens',
self._identity_endpoint + 'v3/auth/tokens',
data=json.dumps(data),
headers={'content-type': 'application/json'},
timeout=self._timeout,
)
OpenstackClient._ensure_valid_response(r, 'Invalid Credentials')
OpenStackClient._ensure_valid_response(r, 'Invalid Credentials')
self._authenticated = True
self._tokenid = r.headers['X-Subject-Token']
@ -366,16 +385,16 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
# 'volumev3', 'volumev2' = 'volumev2'
def ensure_authenticated(self) -> None:
if self._authenticated is False or self._projectid != self._authenticatedProjectId:
self.authenticate_with_password()
if self._authenticated is False or self._projectid != self._authenticated_projectid:
self.authenticate()
@auth_required()
@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 OpenstackClient._get_recurring_url_json(
self._authurl,
for p in OpenStackClient._get_recurring_url_json(
self._identity_endpoint,
'v3/users/{user_id}/projects'.format(user_id=self._userid),
self._session,
headers=self._get_request_headers(),
@ -390,8 +409,8 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
def list_regions(self) -> list[openstack_types.RegionInfo]:
return [
openstack_types.RegionInfo.from_dict(r)
for r in OpenstackClient._get_recurring_url_json(
self._authurl,
for r in OpenStackClient._get_recurring_url_json(
self._identity_endpoint,
'v3/regions',
self._session,
headers=self._get_request_headers(),
@ -560,14 +579,14 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
openstack_types.SecurityGroupInfo.from_dict(sg)
for sg in self._get_recurring_from_endpoint(
endpoint_types=NETWORKS_ENDPOINT_TYPES,
path='/v2.0/security-groups',
path=f'/v2.0/security-groups?project_id={self._projectid}',
error_message='List security groups',
key='security_groups',
)
]
# Very small timeout, so repeated operations will use same data
# Any cache time less than 5 seconds will be fine, beceuse checks on
# Any cache time less than 5 seconds will be fine, beceuse checks on
# openstack are done every 5 seconds
@decorators.cached(prefix='svr', timeout=consts.cache.SHORTEST_CACHE_TIMEOUT, key_helper=cache_key_helper)
def get_server(self, server_id: str) -> openstack_types.ServerInfo:
@ -761,7 +780,7 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
error_message='Stoping server',
expects_json=False,
)
def reboot_server(self, server_id: str, hard: bool = True) -> None:
# Does not need return value
try:
@ -818,7 +837,7 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
# First, ensure requested api is supported
# We need api version 3.2 or greater
try:
r = self._session.get(self._authurl, headers=self._get_request_headers())
r = self._session.get(self._identity_endpoint, headers=self._get_request_headers())
except Exception:
logger.exception('Testing')
raise Exception('Connection error')
@ -828,7 +847,7 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
if v['id'] >= 'v3.1':
# Tries to authenticate
try:
self.authenticate_with_password()
self.authenticate()
# Log some useful information
logger.info('Openstack version: %s', v['id'])
logger.info('Endpoints: %s', json.dumps(self._catalog, indent=4))
@ -851,7 +870,7 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
def is_available(self) -> bool:
try:
# If we can connect, it is available
self._session.get(self._authurl, headers=self._get_request_headers())
self._session.get(self._identity_endpoint, headers=self._get_request_headers())
return True
except Exception:
return False
@ -875,7 +894,7 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
logger.debug('Requesting url #%s: %s / %s', counter, path, params)
r = session.get(path, params=params, headers=headers, timeout=timeout)
OpenstackClient._ensure_valid_response(r, error_message)
OpenStackClient._ensure_valid_response(r, error_message)
j = r.json()
@ -911,3 +930,33 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
errMsg = 'Error checking response'
logger.error('%s: %s', errMsg, response.content)
raise Exception(errMsg)
# Only for testing purposes, not used at runtime
def t_create_volume(self, name: str, size: int) -> openstack_types.VolumeInfo:
data = {
'volume': {
'size': size,
'name': name,
# 'volume_type': volume_type,
}
}
r = self._request_from_endpoint(
'post',
endpoints_types=VOLUMES_ENDPOINT_TYPES,
path='/volumes',
data=json.dumps(data),
error_message='Create Volume',
)
return openstack_types.VolumeInfo.from_dict(r.json()['volume'])
def t_delete_volume(self, volume_id: str) -> None:
# This does not returns anything
self._request_from_endpoint(
'delete',
endpoints_types=VOLUMES_ENDPOINT_TYPES,
path=f'/volumes/{volume_id}',
error_message='Cannot delete volume (probably volume does not exists).',
expects_json=False,
)

View File

@ -37,6 +37,17 @@ 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'
APPLICATION_CREDENTIAL = 'application_credential'
@staticmethod
def from_str(s: str) -> 'AuthMethod':
try:
return AuthMethod(s.lower())
except ValueError:
return AuthMethod.PASSWORD
class ServerStatus(enum.StrEnum):
ACTIVE = 'ACTIVE' # The server is active.

View File

@ -40,7 +40,7 @@ from uds.core.services import ServiceProvider
from uds.core.ui import gui
from uds.core.util import validators, fields
from .openstack import openstack_client, sanitized_name, types as openstack_types
from .openstack import client, sanitized_name, types as openstack_types
from .service import OpenStackLiveService
from .service_fixed import OpenStackServiceFixed
@ -109,6 +109,17 @@ class OpenStackProvider(ServiceProvider):
required=True,
)
auth_method = gui.ChoiceField(
label=_('Authentication method'),
order=2,
tooltip=_('Authentication method to be used'),
choices=[
gui.choice_item(str(openstack_types.AuthMethod.PASSWORD), 'Password'),
gui.choice_item(str(openstack_types.AuthMethod.APPLICATION_CREDENTIAL), 'Application Credential'),
],
default='password',
)
access = gui.ChoiceField(
label=_('Access interface'),
order=5,
@ -116,7 +127,8 @@ class OpenStackProvider(ServiceProvider):
choices=INTERFACE_VALUES,
default='public',
)
domain = gui.TextField(
length=64,
label=_('Domain'),
@ -127,17 +139,17 @@ class OpenStackProvider(ServiceProvider):
)
username = gui.TextField(
length=64,
label=_('Username'),
label=_('Username/Application Credential ID'),
order=9,
tooltip=_('User with valid privileges on OpenStack'),
tooltip=_('User with valid privileges on OpenStack/Application Credential ID with valid privileges'),
required=True,
default='admin',
)
password = gui.PasswordField(
length=32,
label=_('Password'),
label=_('Password/Application Credential Secret'),
order=10,
tooltip=_('Password of the user of OpenStack'),
tooltip=_('Password of the user of OpenStack/Application Credential Secret'),
required=True,
)
@ -148,7 +160,7 @@ class OpenStackProvider(ServiceProvider):
tenant = gui.TextField(
length=64,
label=_('Project Id'),
order=6,
order=40,
tooltip=_('Project (tenant) for this provider. Set only if required by server.'),
required=False,
default='',
@ -157,7 +169,7 @@ class OpenStackProvider(ServiceProvider):
region = gui.TextField(
length=64,
label=_('Region'),
order=7,
order=41,
tooltip=_('Region for this provider. Set only if required by server.'),
required=False,
default='',
@ -166,17 +178,19 @@ class OpenStackProvider(ServiceProvider):
use_subnets_name = gui.CheckBoxField(
label=_('Subnets names'),
order=8,
order=42,
tooltip=_('If checked, the name of the subnets will be used instead of the names of networks'),
default=False,
tab=types.ui.Tab.ADVANCED,
old_field_name='useSubnetsName',
)
verify_ssl = fields.verify_ssl_field(order=91)
https_proxy = gui.TextField(
length=96,
label=_('Proxy'),
order=91,
order=92,
tooltip=_(
'Proxy used on server connections for HTTPS connections (use PROTOCOL://host:port, i.e. http://10.10.0.1:8080)'
),
@ -184,11 +198,12 @@ class OpenStackProvider(ServiceProvider):
tab=types.ui.Tab.ADVANCED,
old_field_name='httpsProxy',
)
legacy = False
# Own variables
_api: typing.Optional[openstack_client.OpenstackClient] = None
_api: typing.Optional[client.OpenStackClient] = None
def initialize(self, values: 'types.core.ValuesType' = None) -> None:
"""
@ -201,26 +216,25 @@ class OpenStackProvider(ServiceProvider):
def api(
self, projectid: typing.Optional[str] = None, region: typing.Optional[str] = None
) -> openstack_client.OpenstackClient:
) -> client.OpenStackClient:
projectid = projectid or self.tenant.value or None
region = region or self.region.value or None
if self._api is None:
proxies: 'dict[str, str]|None' = None
if self.https_proxy.value.strip():
proxies = {'https': self.https_proxy.value}
self._api = openstack_client.OpenstackClient(
self._api = client.OpenStackClient(
self.endpoint.value,
-1,
self.domain.value,
self.username.value,
self.password.value,
is_legacy=False,
use_ssl=False,
projectid=projectid,
region=region,
access=openstack_types.AccessType.from_str(self.access.value),
proxies=proxies,
timeout=self.timeout.value,
auth_method=openstack_types.AuthMethod.from_str(self.auth_method.value),
verify_ssl=self.verify_ssl.value,
)
return self._api

View File

@ -42,7 +42,7 @@ from uds.core.services import ServiceProvider
from uds.core.ui import gui
from uds.core.util import validators, fields
from .openstack import openstack_client, sanitized_name, types as openstack_types
from .openstack import client, sanitized_name, types as openstack_types
from .service import OpenStackLiveService
from .service_fixed import OpenStackServiceFixed
@ -175,7 +175,7 @@ class OpenStackProviderLegacy(ServiceProvider):
legacy = True
# Own variables
_api: typing.Optional[openstack_client.OpenstackClient] = None
_api: typing.Optional[client.OpenStackClient] = None
def initialize(self, values: 'types.core.ValuesType') -> None:
"""
@ -188,23 +188,23 @@ class OpenStackProviderLegacy(ServiceProvider):
def api(
self, projectid: typing.Optional[str] = None, region: typing.Optional[str] = None
) -> openstack_client.OpenstackClient:
) -> client.OpenStackClient:
proxies: typing.Optional[dict[str, str]] = None
if self.https_proxy.value.strip():
proxies = {'https': self.https_proxy.value}
return openstack_client.OpenstackClient(
return client.OpenStackClient(
self.host.value,
self.port.value,
self.domain.value,
self.username.value,
self.password.value,
is_legacy=True,
port=self.port.value,
use_ssl=self.ssl.as_bool(),
projectid=projectid,
region=region,
access=openstack_types.AccessType.from_str(self.access.value),
proxies=proxies,
timeout=self.timeout.value,
verify_ssl=False,
)
def sanitized_name(self, name: str) -> str:

View File

@ -42,7 +42,7 @@ from uds.core.ui import gui
from .publication import OpenStackLivePublication
from .deployment import OpenStackLiveUserService
from .openstack import types as openstack_types, openstack_client
from .openstack import client, types as openstack_types
from . import helpers
@ -180,7 +180,7 @@ class OpenStackLiveService(DynamicService):
prov_uuid = gui.HiddenField()
cached_api: typing.Optional['openstack_client.OpenstackClient'] = None
cached_api: typing.Optional['client.OpenStackClient'] = None
# Note: currently, Openstack does not provides a way of specifying how to stop the server
# At least, i have not found it on the documentation
@ -223,7 +223,7 @@ class OpenStackLiveService(DynamicService):
self.prov_uuid.value = self.provider().get_uuid()
@property
def api(self) -> 'openstack_client.OpenstackClient':
def api(self) -> 'client.OpenStackClient':
if not self.cached_api:
self.cached_api = self.provider().api(projectid=self.project.value, region=self.region.value)

View File

@ -43,7 +43,7 @@ from .deployment_fixed import OpenStackUserServiceFixed
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from .openstack import openstack_client
from .openstack import client
from .provider import OpenStackProvider
from .provider_legacy import OpenStackProviderLegacy
@ -103,10 +103,10 @@ class OpenStackServiceFixed(FixedService): # pylint: disable=too-many-public-me
prov_uuid = gui.HiddenField()
_api: typing.Optional['openstack_client.OpenstackClient'] = None
_api: typing.Optional['client.OpenStackClient'] = None
@property
def api(self) -> 'openstack_client.OpenstackClient':
def api(self) -> 'client.OpenStackClient':
if not self._api:
self._api = self.provider().api(projectid=self.project.value, region=self.region.value)