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:
parent
773c3b5b50
commit
2a4ccac195
@ -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
|
||||
|
111
server/src/tests/services/openstack/test_client.py
Normal file
111
server/src/tests/services/openstack/test_client.py
Normal 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()
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
)
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user