mirror of
https://github.com/dkmstr/openuds.git
synced 2025-01-08 21:18:00 +03:00
Cleaning and fixing up Openstack Client
This commit is contained in:
parent
b303ffc858
commit
9d51963903
@ -31,6 +31,7 @@
|
||||
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import contextlib
|
||||
import copy
|
||||
import datetime
|
||||
import typing
|
||||
import uuid
|
||||
@ -38,12 +39,11 @@ import random
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from tests.services.ovirt.fixtures import SNAPSHOTS_INFO
|
||||
from uds.core import environment, types
|
||||
from uds.core.ui.user_interface import gui
|
||||
|
||||
from tests.utils.autospec import autospec, AutoSpecMethodInfo
|
||||
from tests.utils import search_item_by_attr
|
||||
from tests.utils import helpers, search_item_by_attr
|
||||
|
||||
from uds.services.OpenStack import (
|
||||
provider,
|
||||
@ -61,9 +61,9 @@ AnyOpenStackProvider: typing.TypeAlias = typing.Union[
|
||||
]
|
||||
|
||||
|
||||
GUEST_IP_ADDRESS: str = '1.0.0.1'
|
||||
DEF_GUEST_IP_ADDRESS: typing.Final[str] = '1.0.0.1'
|
||||
|
||||
FLAVORS_LIST: list[openstack_types.FlavorInfo] = [
|
||||
DEF_FLAVORS_LIST: typing.Final[list[openstack_types.FlavorInfo]] = [
|
||||
openstack_types.FlavorInfo(
|
||||
id=f'fid{n}',
|
||||
name=f'Flavor name{n}',
|
||||
@ -77,7 +77,7 @@ FLAVORS_LIST: list[openstack_types.FlavorInfo] = [
|
||||
for n in range(1, 16)
|
||||
]
|
||||
|
||||
AVAILABILITY_ZONES_LIST: list[openstack_types.AvailabilityZoneInfo] = [
|
||||
DEF_AVAILABILITY_ZONES_LIST: typing.Final[list[openstack_types.AvailabilityZoneInfo]] = [
|
||||
openstack_types.AvailabilityZoneInfo(
|
||||
id=f'az{n}',
|
||||
name=f'az name{n}',
|
||||
@ -87,20 +87,20 @@ AVAILABILITY_ZONES_LIST: list[openstack_types.AvailabilityZoneInfo] = [
|
||||
]
|
||||
|
||||
|
||||
PROJECTS_LIST: list[openstack_types.ProjectInfo] = [
|
||||
DEF_PROJECTS_LIST: typing.Final[list[openstack_types.ProjectInfo]] = [
|
||||
openstack_types.ProjectInfo(id=f'pid{n}', name=f'project name{n}') for n in range(1, 16)
|
||||
]
|
||||
|
||||
REGIONS_LIST: list[openstack_types.RegionInfo] = [
|
||||
DEF_REGIONS_LIST: typing.Final[list[openstack_types.RegionInfo]] = [
|
||||
openstack_types.RegionInfo(id=f'rid{n}', name=f'region name{n}') for n in range(1, 16)
|
||||
]
|
||||
|
||||
SERVERS_LIST: list[openstack_types.ServerInfo] = [
|
||||
DEF_SERVERS_LIST: typing.Final[list[openstack_types.ServerInfo]] = [
|
||||
openstack_types.ServerInfo(
|
||||
id=f'sid{n}',
|
||||
name=f'server name{n}',
|
||||
href=f'https://xxxx/v2/yyyy/servers/zzzzz{n}',
|
||||
flavor=FLAVORS_LIST[(n - 1) % len(FLAVORS_LIST)].id,
|
||||
flavor=DEF_FLAVORS_LIST[(n - 1) % len(DEF_FLAVORS_LIST)].id,
|
||||
status=openstack_types.ServerStatus.ACTIVE,
|
||||
power_state=openstack_types.PowerState.SHUTDOWN,
|
||||
addresses=[
|
||||
@ -120,15 +120,7 @@ SERVERS_LIST: list[openstack_types.ServerInfo] = [
|
||||
for n in range(1, 32)
|
||||
]
|
||||
|
||||
IMAGES_LIST: list[openstack_types.ImageInfo] = [
|
||||
openstack_types.ImageInfo(
|
||||
id=f'iid{n}',
|
||||
name=f'image name{n}',
|
||||
)
|
||||
for n in range(1, 16)
|
||||
]
|
||||
|
||||
VOLUMES_TYPE_LIST: list[openstack_types.VolumeTypeInfo] = [
|
||||
DEF_VOLUMES_TYPE_LIST: typing.Final[list[openstack_types.VolumeTypeInfo]] = [
|
||||
openstack_types.VolumeTypeInfo(
|
||||
id=f'vid{n}',
|
||||
name=f'volume type name{n}',
|
||||
@ -136,7 +128,7 @@ VOLUMES_TYPE_LIST: list[openstack_types.VolumeTypeInfo] = [
|
||||
for n in range(1, 16)
|
||||
]
|
||||
|
||||
VOLUMES_LIST: list[openstack_types.VolumeInfo] = [
|
||||
DEF_VOLUMES_LIST: typing.Final[list[openstack_types.VolumeInfo]] = [
|
||||
openstack_types.VolumeInfo(
|
||||
id=f'vid{n}',
|
||||
name=f'volume name{n}',
|
||||
@ -152,10 +144,10 @@ VOLUMES_LIST: list[openstack_types.VolumeInfo] = [
|
||||
for n in range(1, 16)
|
||||
]
|
||||
|
||||
VOLUME_SNAPSHOTS_LIST: list[openstack_types.SnapshotInfo] = [
|
||||
DEF_VOLUME_SNAPSHOTS_LIST: typing.Final[list[openstack_types.SnapshotInfo]] = [
|
||||
openstack_types.SnapshotInfo(
|
||||
id=f'vsid{n}',
|
||||
volume_id=VOLUMES_LIST[(n - 1) % len(VOLUMES_LIST)].id,
|
||||
volume_id=DEF_VOLUMES_LIST[(n - 1) % len(DEF_VOLUMES_LIST)].id,
|
||||
name=f'volume snapshot name{n}',
|
||||
description=f'volume snapshot description{n}',
|
||||
status=openstack_types.SnapshotStatus.AVAILABLE,
|
||||
@ -166,7 +158,7 @@ VOLUME_SNAPSHOTS_LIST: list[openstack_types.SnapshotInfo] = [
|
||||
for n in range(1, 16)
|
||||
]
|
||||
|
||||
SUBNETS_LIST: list[openstack_types.SubnetInfo] = [
|
||||
DEF_SUBNETS_LIST: typing.Final[list[openstack_types.SubnetInfo]] = [
|
||||
openstack_types.SubnetInfo(
|
||||
id=f'subnetid{n}',
|
||||
name=f'subnet name{n}',
|
||||
@ -179,21 +171,21 @@ SUBNETS_LIST: list[openstack_types.SubnetInfo] = [
|
||||
for n in range(1, 16)
|
||||
]
|
||||
|
||||
NETWORKS_LIST: list[openstack_types.NetworkInfo] = [
|
||||
DEF_NETWORKS_LIST: typing.Final[list[openstack_types.NetworkInfo]] = [
|
||||
openstack_types.NetworkInfo(
|
||||
id=f'netid{n}',
|
||||
name=f'network name{n}',
|
||||
status=openstack_types.NetworkStatus.ACTIVE,
|
||||
shared=n % 2 == 0,
|
||||
subnets=random.sample([s.id for s in SUBNETS_LIST], 2),
|
||||
subnets=random.sample([s.id for s in DEF_SUBNETS_LIST], 2),
|
||||
availability_zones=[
|
||||
AVAILABILITY_ZONES_LIST[(j - 1) % len(AVAILABILITY_ZONES_LIST)].id for j in range(1, 4)
|
||||
DEF_AVAILABILITY_ZONES_LIST[(j - 1) % len(DEF_AVAILABILITY_ZONES_LIST)].id for j in range(1, 4)
|
||||
],
|
||||
)
|
||||
for n in range(1, 16)
|
||||
]
|
||||
|
||||
PORTS_LIST: list[openstack_types.PortInfo] = [
|
||||
DEF_PORTS_LIST: typing.Final[list[openstack_types.PortInfo]] = [
|
||||
openstack_types.PortInfo(
|
||||
id=f'portid{n}',
|
||||
name=f'port name{n}',
|
||||
@ -204,7 +196,7 @@ PORTS_LIST: list[openstack_types.PortInfo] = [
|
||||
fixed_ips=[
|
||||
openstack_types.PortInfo.FixedIpInfo(
|
||||
ip_address=f'192.168.{j}.1',
|
||||
subnet_id=random.choice([s.id for s in SUBNETS_LIST]),
|
||||
subnet_id=random.choice([s.id for s in DEF_SUBNETS_LIST]),
|
||||
)
|
||||
for j in range(1, 4)
|
||||
],
|
||||
@ -212,7 +204,7 @@ PORTS_LIST: list[openstack_types.PortInfo] = [
|
||||
for n in range(1, 16)
|
||||
]
|
||||
|
||||
SECURITY_GROUPS_LIST: list[openstack_types.SecurityGroupInfo] = [
|
||||
DEF_SECURITY_GROUPS_LIST: typing.Final[list[openstack_types.SecurityGroupInfo]] = [
|
||||
openstack_types.SecurityGroupInfo(
|
||||
id=f'sgid{n}',
|
||||
name=f'security group name{n}',
|
||||
@ -221,57 +213,93 @@ SECURITY_GROUPS_LIST: list[openstack_types.SecurityGroupInfo] = [
|
||||
for n in range(1, 16)
|
||||
]
|
||||
|
||||
CONSOLE_CONNECTION_INFO: 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,
|
||||
DEF_CONSOLE_CONNECTION_INFO: typing.Final[types.services.ConsoleConnectionInfo] = (
|
||||
types.services.ConsoleConnectionInfo(
|
||||
type='spice',
|
||||
address=DEF_GUEST_IP_ADDRESS,
|
||||
port=5900,
|
||||
secure_port=5901,
|
||||
cert_subject='',
|
||||
ticket=types.services.ConsoleConnectionTicket(value='ticket'),
|
||||
ca='',
|
||||
proxy='',
|
||||
monitors=1,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
GUEST_IP_ADDRESS = DEF_GUEST_IP_ADDRESS
|
||||
FLAVORS_LIST = copy.deepcopy(DEF_FLAVORS_LIST)
|
||||
AVAILABILITY_ZONES_LIST = copy.deepcopy(DEF_AVAILABILITY_ZONES_LIST)
|
||||
PROJECTS_LIST = copy.deepcopy(DEF_PROJECTS_LIST)
|
||||
REGIONS_LIST = copy.deepcopy(DEF_REGIONS_LIST)
|
||||
SERVERS_LIST = copy.deepcopy(DEF_SERVERS_LIST)
|
||||
VOLUMES_TYPE_LIST = copy.deepcopy(DEF_VOLUMES_TYPE_LIST)
|
||||
VOLUMES_LIST = copy.deepcopy(DEF_VOLUMES_LIST)
|
||||
VOLUME_SNAPSHOTS_LIST = copy.deepcopy(DEF_VOLUME_SNAPSHOTS_LIST)
|
||||
SUBNETS_LIST = copy.deepcopy(DEF_SUBNETS_LIST)
|
||||
NETWORKS_LIST = copy.deepcopy(DEF_NETWORKS_LIST)
|
||||
PORTS_LIST = copy.deepcopy(DEF_PORTS_LIST)
|
||||
SECURITY_GROUPS_LIST = copy.deepcopy(DEF_SECURITY_GROUPS_LIST)
|
||||
CONSOLE_CONNECTION_INFO = copy.deepcopy(DEF_CONSOLE_CONNECTION_INFO)
|
||||
|
||||
|
||||
def clear() -> None:
|
||||
global GUEST_IP_ADDRESS, CONSOLE_CONNECTION_INFO
|
||||
GUEST_IP_ADDRESS = DEF_GUEST_IP_ADDRESS # pyright: ignore[reportConstantRedefinition]
|
||||
|
||||
FLAVORS_LIST[:] = copy.deepcopy(DEF_FLAVORS_LIST)
|
||||
AVAILABILITY_ZONES_LIST[:] = copy.deepcopy(DEF_AVAILABILITY_ZONES_LIST)
|
||||
PROJECTS_LIST[:] = copy.deepcopy(DEF_PROJECTS_LIST)
|
||||
REGIONS_LIST[:] = copy.deepcopy(DEF_REGIONS_LIST)
|
||||
SERVERS_LIST[:] = copy.deepcopy(DEF_SERVERS_LIST)
|
||||
VOLUMES_TYPE_LIST[:] = copy.deepcopy(DEF_VOLUMES_TYPE_LIST)
|
||||
VOLUMES_LIST[:] = copy.deepcopy(DEF_VOLUMES_LIST)
|
||||
VOLUME_SNAPSHOTS_LIST[:] = copy.deepcopy(DEF_VOLUME_SNAPSHOTS_LIST)
|
||||
SUBNETS_LIST[:] = copy.deepcopy(DEF_SUBNETS_LIST)
|
||||
NETWORKS_LIST[:] = copy.deepcopy(DEF_NETWORKS_LIST)
|
||||
PORTS_LIST[:] = copy.deepcopy(DEF_PORTS_LIST)
|
||||
SECURITY_GROUPS_LIST[:] = copy.deepcopy(DEF_SECURITY_GROUPS_LIST)
|
||||
CONSOLE_CONNECTION_INFO = copy.deepcopy( # pyright: ignore[reportConstantRedefinition]
|
||||
DEF_CONSOLE_CONNECTION_INFO
|
||||
)
|
||||
|
||||
|
||||
T = typing.TypeVar('T')
|
||||
|
||||
|
||||
def set_all_vms_status(status: openstack_types.ServerStatus) -> None:
|
||||
for vm in SERVERS_LIST:
|
||||
vm.status = status
|
||||
|
||||
|
||||
|
||||
def search_id(lst: list[T], id: str, *args: typing.Any, **kwargs: typing.Any) -> T:
|
||||
return search_item_by_attr(lst, 'id', id)
|
||||
|
||||
|
||||
def set_vm_state(id: str, state: openstack_types.PowerState, **kwargs: typing.Any) -> str:
|
||||
vm = search_id(SERVERS_LIST, id)
|
||||
vm.power_state = state
|
||||
return str(state) + '_task_uuid'
|
||||
|
||||
|
||||
def random_element(lst: list[T], *args: typing.Any, **kwargs: typing.Any) -> T:
|
||||
return random.choice(lst)
|
||||
|
||||
|
||||
# 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]] = [
|
||||
AutoSpecMethodInfo(client.OpenStackClient.list_flavors, returns=FLAVORS_LIST),
|
||||
AutoSpecMethodInfo(
|
||||
client.OpenStackClient.list_availability_zones, returns=AVAILABILITY_ZONES_LIST
|
||||
),
|
||||
AutoSpecMethodInfo(client.OpenStackClient.list_availability_zones, returns=AVAILABILITY_ZONES_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(
|
||||
client.OpenStackClient.list_volume_snapshots, returns=VOLUME_SNAPSHOTS_LIST
|
||||
),
|
||||
AutoSpecMethodInfo(client.OpenStackClient.list_networks, returns=NETWORKS_LIST),
|
||||
AutoSpecMethodInfo(client.OpenStackClient.list_ports, returns=PORTS_LIST),
|
||||
AutoSpecMethodInfo(
|
||||
client.OpenStackClient.list_security_groups, returns=SECURITY_GROUPS_LIST
|
||||
),
|
||||
AutoSpecMethodInfo(client.OpenStackClient.list_security_groups, returns=SECURITY_GROUPS_LIST),
|
||||
AutoSpecMethodInfo(
|
||||
client.OpenStackClient.get_server_info,
|
||||
returns=search_id,
|
||||
@ -340,7 +368,6 @@ CLIENT_METHODS_INFO: typing.Final[list[AutoSpecMethodInfo]] = [
|
||||
returns=set_vm_state,
|
||||
partial_kwargs={'state': openstack_types.PowerState.RUNNING},
|
||||
),
|
||||
|
||||
# connect returns None
|
||||
# Test method
|
||||
# AutoSpecMethodInfo(client.Client.list_projects, returns=True),
|
||||
@ -351,7 +378,8 @@ CLIENT_METHODS_INFO: typing.Final[list[AutoSpecMethodInfo]] = [
|
||||
]
|
||||
|
||||
PROVIDER_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
|
||||
'endpoint': 'host',
|
||||
'endpoint': 'https://host',
|
||||
'auth_method': 'application_credential',
|
||||
'access': 'public',
|
||||
'domain': 'domain',
|
||||
'username': 'username',
|
||||
@ -359,7 +387,7 @@ PROVIDER_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
|
||||
'concurrent_creation_limit': 1,
|
||||
'concurrent_removal_limit': 1,
|
||||
'timeout': 10,
|
||||
'tenant': 'tenant',
|
||||
'project_id': 'tenant', # Old name, new is project_id
|
||||
'region': 'region',
|
||||
'use_subnets_name': False,
|
||||
'https_proxy': 'https_proxy',
|
||||
@ -382,24 +410,25 @@ PROVIDER_LEGACY_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
|
||||
|
||||
|
||||
SERVICE_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
|
||||
'region': random.choice(REGIONS_LIST).id,
|
||||
'project': random.choice(PROJECTS_LIST).id,
|
||||
'availability_zone': random.choice(AVAILABILITY_ZONES_LIST).id,
|
||||
'volume': random.choice(VOLUMES_LIST).id,
|
||||
'network': random.choice(NETWORKS_LIST).id,
|
||||
'flavor': random.choice(FLAVORS_LIST).id,
|
||||
'security_groups': [random.choice(SECURITY_GROUPS_LIST).id],
|
||||
'region': random.choice(DEF_REGIONS_LIST).id,
|
||||
'project': random.choice(DEF_PROJECTS_LIST).id,
|
||||
'availability_zone': random.choice(DEF_AVAILABILITY_ZONES_LIST).id,
|
||||
'volume': random.choice(DEF_VOLUMES_LIST).id,
|
||||
'network': random.choice(DEF_NETWORKS_LIST).id,
|
||||
'flavor': random.choice(DEF_FLAVORS_LIST).id,
|
||||
'security_groups': [random.choice(DEF_SECURITY_GROUPS_LIST).id],
|
||||
'basename': 'bname',
|
||||
'lenname': 5,
|
||||
'maintain_on_error': False,
|
||||
# 'prov_uuid': str(uuid.uuid4()), # Not stored on db, so not needed
|
||||
'try_soft_shutdown': False,
|
||||
'prov_uuid': 'prov_uuid',
|
||||
}
|
||||
|
||||
SERVICES_FIXED_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
|
||||
'token': 'token',
|
||||
'region': random.choice(REGIONS_LIST).id,
|
||||
'project': random.choice(PROJECTS_LIST).id,
|
||||
'machines': [i.id for i in random.sample(SERVERS_LIST, 10)],
|
||||
'region': random.choice(DEF_REGIONS_LIST).id,
|
||||
'project': random.choice(DEF_PROJECTS_LIST).id,
|
||||
'machines': [i.id for i in random.sample(DEF_SERVERS_LIST, 10)],
|
||||
# 'prov_uuid': str(uuid.uuid4()), # Not stored on db, so not needed
|
||||
}
|
||||
|
||||
@ -422,6 +451,7 @@ def patched_provider(
|
||||
api.return_value = client
|
||||
yield provider
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def patched_provider_legacy(
|
||||
**kwargs: typing.Any,
|
||||
@ -432,6 +462,7 @@ def patched_provider_legacy(
|
||||
api.return_value = client
|
||||
yield provider
|
||||
|
||||
|
||||
def create_provider(**kwargs: typing.Any) -> provider.OpenStackProvider:
|
||||
"""
|
||||
Create a provider
|
||||
@ -492,7 +523,7 @@ def create_publication(service: service.OpenStackLiveService) -> publication.Ope
|
||||
servicepool_name='servicepool_name',
|
||||
uuid=uuid_,
|
||||
)
|
||||
pub._vmid = random_element(SNAPSHOTS_INFO).id
|
||||
pub._vmid = helpers.random_string(8)
|
||||
return pub
|
||||
|
||||
|
||||
|
@ -117,9 +117,12 @@ class TestOpenStackClient(UDSTransactionTestCase):
|
||||
)
|
||||
try:
|
||||
self.wait_for_volume(volume)
|
||||
# Set volume bootable
|
||||
self.oclient.t_set_volume_bootable(volume.id, bootable=True)
|
||||
yield volume
|
||||
finally:
|
||||
self.wait_for_volume(volume)
|
||||
logger.info('Volume; %s', self.oclient.get_volume_info(volume.id, force=True))
|
||||
self.oclient.t_delete_volume(volume.id)
|
||||
|
||||
@contextlib.contextmanager
|
||||
@ -137,6 +140,18 @@ class TestOpenStackClient(UDSTransactionTestCase):
|
||||
self.wait_for_snapshot(snapshot)
|
||||
self.oclient.delete_snapshot(snapshot.id)
|
||||
|
||||
# Ensure that the snapshot is deleted
|
||||
def snapshot_removal_checker():
|
||||
try:
|
||||
self.oclient.get_snapshot_info(snapshot.id)
|
||||
return False
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
helpers.waiter(
|
||||
snapshot_removal_checker, timeout=30, msg='Timeout waiting for snapshot to be deleted'
|
||||
)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def create_test_server(self) -> typing.Iterator[openstack_types.ServerInfo]:
|
||||
with self.create_test_volume() as volume:
|
||||
@ -149,10 +164,10 @@ class TestOpenStackClient(UDSTransactionTestCase):
|
||||
security_groups_names=[],
|
||||
availability_zone=self._availability_zone_id,
|
||||
)
|
||||
try:
|
||||
yield server
|
||||
finally:
|
||||
self.oclient.delete_server(server.id)
|
||||
try:
|
||||
yield server
|
||||
finally:
|
||||
self.oclient.delete_server(server.id)
|
||||
|
||||
def test_list_projects(self) -> None:
|
||||
projects = self.oclient.list_projects()
|
||||
@ -164,6 +179,21 @@ class TestOpenStackClient(UDSTransactionTestCase):
|
||||
self.assertGreaterEqual(len(regions), 1)
|
||||
self.assertIn(self._regionid, [r.id for r in regions])
|
||||
|
||||
def test_list_servers(self) -> None:
|
||||
with self.create_test_server() as server1:
|
||||
with self.create_test_server() as server2:
|
||||
servers = self.oclient.list_servers(force=True)
|
||||
self.assertGreaterEqual(len(servers), 2)
|
||||
self.assertIn(
|
||||
(server1.id, server1.flavor),
|
||||
[(s.id, s.flavor) for s in servers],
|
||||
)
|
||||
self.assertIn(
|
||||
(server2.id, server2.flavor),
|
||||
[(s.id, s.flavor) for s in servers],
|
||||
)
|
||||
|
||||
|
||||
def test_list_volumes(self) -> None:
|
||||
with self.create_test_volume() as volume:
|
||||
with self.create_test_volume() as volume2:
|
||||
|
119
server/src/tests/services/openstack/test_helpers.py
Normal file
119
server/src/tests/services/openstack/test_helpers.py
Normal file
@ -0,0 +1,119 @@
|
||||
# -*- 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 typing
|
||||
from unittest import mock
|
||||
|
||||
from uds.services.OpenStack import helpers
|
||||
|
||||
from . import fixtures
|
||||
|
||||
from tests.utils.test import UDSTransactionTestCase
|
||||
from tests.utils import search_dict_by_attr
|
||||
|
||||
# models.Provider.objects.get(uuid=parameters['prov_uuid']).get_instance(),
|
||||
# )
|
||||
|
||||
# if isinstance(provider, OpenStackProvider):
|
||||
# use_subnets_names = provider.use_subnets_name.as_bool()
|
||||
# else:
|
||||
# use_subnets_names = False
|
||||
|
||||
# return (provider.api(parameters['project'], parameters['region']), use_subnets_names)
|
||||
|
||||
|
||||
class TestOpenStackHelpers(UDSTransactionTestCase):
|
||||
_parameters: dict[str, typing.Any] = {
|
||||
'prov_uuid': 'test',
|
||||
'project': fixtures.PROJECTS_LIST[0].id,
|
||||
'region': fixtures.REGIONS_LIST[0].id,
|
||||
}
|
||||
|
||||
def test_get_api(self) -> None:
|
||||
# with fixtures.patched_provider() as provider:
|
||||
# pass
|
||||
with mock.patch('uds.models.Provider.objects.get') as get_provider:
|
||||
helpers.get_api(self._parameters)
|
||||
get_provider.assert_called_once_with(uuid=self._parameters['prov_uuid'])
|
||||
|
||||
def test_get_resources(self) -> None:
|
||||
with fixtures.patched_provider() as provider:
|
||||
with mock.patch('uds.models.Provider.objects.get') as get_provider:
|
||||
get_provider.return_value.get_instance.return_value = provider
|
||||
result = helpers.list_resources(self._parameters)
|
||||
self.assertEqual(len(result), 4)
|
||||
# These are all lists
|
||||
availability_zone_choices = search_dict_by_attr(result, 'name', 'availability_zone')['choices']
|
||||
network_choices = search_dict_by_attr(result, 'name', 'network')['choices']
|
||||
flavor_choices = search_dict_by_attr(result, 'name', 'flavor')['choices']
|
||||
security_groups_choices = search_dict_by_attr(result, 'name', 'security_groups')['choices']
|
||||
|
||||
self.assertEqual(
|
||||
{(i.id, i.name) for i in fixtures.AVAILABILITY_ZONES_LIST},
|
||||
{(i['id'], i['text']) for i in availability_zone_choices},
|
||||
)
|
||||
self.assertEqual(
|
||||
{(i.id, i.name) for i in fixtures.NETWORKS_LIST},
|
||||
{(i['id'], i['text']) for i in network_choices},
|
||||
)
|
||||
self.assertEqual(
|
||||
{i.id for i in fixtures.FLAVORS_LIST if not i.disabled}, {i['id'] for i in flavor_choices}
|
||||
)
|
||||
self.assertEqual(
|
||||
{(i.name, i.name) for i in fixtures.SECURITY_GROUPS_LIST},
|
||||
{(i['id'], i['text']) for i in security_groups_choices},
|
||||
)
|
||||
|
||||
def test_get_volumes(self) -> None:
|
||||
with fixtures.patched_provider() as provider:
|
||||
with mock.patch('uds.models.Provider.objects.get') as get_provider:
|
||||
get_provider.return_value.get_instance.return_value = provider
|
||||
result = helpers.list_volumes(self._parameters)
|
||||
self.assertEqual(len(result), 1)
|
||||
volume_choices = search_dict_by_attr(result, 'name', 'volume')['choices']
|
||||
self.assertEqual(
|
||||
{(i.id, i.name) for i in fixtures.VOLUMES_LIST},
|
||||
{(i['id'], i['text']) for i in volume_choices},
|
||||
)
|
||||
|
||||
def test_list_servers(self) -> None:
|
||||
with fixtures.patched_provider() as provider:
|
||||
with mock.patch('uds.models.Provider.objects.get') as get_provider:
|
||||
# api = typing.cast(mock.Mock, provider.api)
|
||||
get_provider.return_value.get_instance.return_value = provider
|
||||
result = helpers.list_servers(self._parameters)
|
||||
self.assertEqual(len(result), 1)
|
||||
server_choices = search_dict_by_attr(result, 'name', 'machines')['choices']
|
||||
self.assertEqual(
|
||||
{(i.id, i.name) for i in fixtures.SERVERS_LIST},
|
||||
{(i['id'], i['text']) for i in server_choices},
|
||||
)
|
@ -90,7 +90,7 @@ class TestOpenstackProvider(UDSTransactionTestCase):
|
||||
"""
|
||||
Test the Helpers. In fact, not used on provider, but on services (fixed, live, ...)
|
||||
"""
|
||||
from uds.services.OpenStack.helpers import get_machines, get_resources, get_volumes
|
||||
from uds.services.OpenStack.helpers import list_servers, list_resources, list_volumes
|
||||
|
||||
for patcher in (fixtures.patched_provider, fixtures.patched_provider_legacy):
|
||||
with patcher() as prov:
|
||||
@ -113,12 +113,12 @@ class TestOpenstackProvider(UDSTransactionTestCase):
|
||||
with mock.patch('uds.services.OpenStack.helpers.get_api') as get_api:
|
||||
get_api.return_value = (prov.api(), False)
|
||||
|
||||
h_machines = get_machines(parameters)
|
||||
h_machines = list_servers(parameters)
|
||||
self.assertEqual(len(h_machines), 1)
|
||||
self.assertEqual(h_machines[0]['name'], 'machines')
|
||||
self.assertEqual(sorted(i['id'] for i in h_machines[0]['choices']), sorted(i.id for i in fixtures.SERVERS_LIST))
|
||||
|
||||
h_resources = get_resources(parameters)
|
||||
h_resources = list_resources(parameters)
|
||||
# [{'name': 'availability_zone', 'choices': [...]}, {'name': 'network', 'choices': [...]}, {'name': 'flavor', 'choices': [...]}, {'name': 'security_groups', 'choices': [...]}]
|
||||
self.assertEqual(len(h_resources), 4)
|
||||
self.assertEqual(sorted(i['name'] for i in h_resources), ['availability_zone', 'flavor', 'network', 'security_groups'])
|
||||
@ -128,10 +128,10 @@ class TestOpenstackProvider(UDSTransactionTestCase):
|
||||
self.assertEqual(sorted(_get_choices_for('availability_zone')), sorted(i.id for i in fixtures.AVAILABILITY_ZONES_LIST))
|
||||
self.assertEqual(sorted(_get_choices_for('network')), sorted(i.id for i in fixtures.NETWORKS_LIST))
|
||||
self.assertEqual(sorted(_get_choices_for('flavor')), sorted(i.id for i in fixtures.FLAVORS_LIST if not i.disabled))
|
||||
self.assertEqual(sorted(_get_choices_for('security_groups')), sorted(i.id for i in fixtures.SECURITY_GROUPS_LIST))
|
||||
self.assertEqual(sorted(_get_choices_for('security_groups')), sorted(i.name for i in fixtures.SECURITY_GROUPS_LIST))
|
||||
|
||||
# [{'name': 'volume', 'choices': [...]}]
|
||||
h_volumes = get_volumes(parameters)
|
||||
h_volumes = list_volumes(parameters)
|
||||
self.assertEqual(len(h_volumes), 1)
|
||||
self.assertEqual(h_volumes[0]['name'], 'volume')
|
||||
self.assertEqual(sorted(i['id'] for i in h_volumes[0]['choices']), sorted(i.id for i in fixtures.VOLUMES_LIST))
|
||||
|
@ -41,8 +41,6 @@ from ...utils.test import UDSTransactionTestCase
|
||||
# from uds.services.OpenStack.service import OpenStackLiveService
|
||||
from uds.services.OpenStack.openstack import types as openstack_types
|
||||
|
||||
# We have only one service type for both providers
|
||||
|
||||
|
||||
class TestOpenstackService(UDSTransactionTestCase):
|
||||
def test_service(self) -> None:
|
||||
@ -58,11 +56,11 @@ class TestOpenstackService(UDSTransactionTestCase):
|
||||
|
||||
template = service.make_template('template', 'desc')
|
||||
self.assertIsInstance(template, openstack_types.SnapshotInfo)
|
||||
api.create_volume_snapshot.assert_called_once_with(service.volume.value, 'template', 'desc')
|
||||
api.create_snapshot.assert_called_once_with(service.volume.value, 'template', 'desc')
|
||||
|
||||
template = service.get_template(fixtures.VOLUME_SNAPSHOTS_LIST[0].id)
|
||||
self.assertIsInstance(template, openstack_types.SnapshotInfo)
|
||||
api.get_volume_snapshot.assert_called_once_with(fixtures.VOLUME_SNAPSHOTS_LIST[0].id)
|
||||
api.get_snapshot_info.assert_called_once_with(fixtures.VOLUME_SNAPSHOTS_LIST[0].id)
|
||||
|
||||
data: typing.Any = service.deploy_from_template('name', fixtures.VOLUME_SNAPSHOTS_LIST[0].id)
|
||||
self.assertIsInstance(data, openstack_types.ServerInfo)
|
||||
@ -72,21 +70,17 @@ class TestOpenstackService(UDSTransactionTestCase):
|
||||
availability_zone=service.availability_zone.value,
|
||||
flavor_id=service.flavor.value,
|
||||
network_id=service.network.value,
|
||||
security_groups_ids=service.security_groups.value,
|
||||
security_groups_names=service.security_groups.value,
|
||||
)
|
||||
data = service.api.get_server_info(fixtures.SERVERS_LIST[0].id).status
|
||||
self.assertIsInstance(data, openstack_types.ServerStatus)
|
||||
api.get_server.assert_called_once_with(fixtures.SERVERS_LIST[0].id)
|
||||
# Reset mocks, get server should be called again
|
||||
api.reset_mock()
|
||||
|
||||
data = service.api.get_server_info(fixtures.SERVERS_LIST[0].id).power_state
|
||||
self.assertIsInstance(data, openstack_types.PowerState)
|
||||
api.get_server.assert_called_once_with(fixtures.SERVERS_LIST[0].id)
|
||||
|
||||
server = fixtures.SERVERS_LIST[0]
|
||||
service.api.start_server(server.id)
|
||||
|
||||
|
||||
server.power_state = openstack_types.PowerState.SHUTDOWN
|
||||
api.start_server.assert_called_once_with(server.id)
|
||||
|
||||
@ -113,5 +107,7 @@ class TestOpenstackService(UDSTransactionTestCase):
|
||||
|
||||
self.assertEqual(service.get_basename(), service.basename.value)
|
||||
self.assertEqual(service.get_lenname(), service.lenname.value)
|
||||
self.assertEqual(service.allows_errored_userservice_cleanup(), not service.maintain_on_error.value)
|
||||
self.assertEqual(
|
||||
service.allows_errored_userservice_cleanup(), not service.maintain_on_error.value
|
||||
)
|
||||
self.assertEqual(service.should_maintain_on_error(), service.maintain_on_error.value)
|
||||
|
@ -98,7 +98,7 @@ class TestOpenstackFixedService(UDSTransactionTestCase):
|
||||
# How many assignables machines are available?
|
||||
remaining = len(list(service.enumerate_assignables()))
|
||||
|
||||
api.get_server.reset_mock()
|
||||
api.get_server_info.reset_mock()
|
||||
# Now get_and_assign_machine as much as remaining machines
|
||||
for _ in range(remaining):
|
||||
vm = service.get_and_assign()
|
||||
@ -108,7 +108,7 @@ class TestOpenstackFixedService(UDSTransactionTestCase):
|
||||
self.assertEqual(list(service.enumerate_assignables()), [])
|
||||
|
||||
# And get_server should have been called remaining times
|
||||
self.assertEqual(api.get_server.call_count, remaining)
|
||||
self.assertEqual(api.get_server_info.call_count, remaining)
|
||||
|
||||
# And a new try, should raise an exception
|
||||
self.assertRaises(Exception, service.get_and_assign)
|
||||
|
@ -86,7 +86,7 @@ class TestOpenstackLiveDeployment(UDSTransactionTestCase):
|
||||
availability_zone=service.availability_zone.value,
|
||||
flavor_id=service.flavor.value,
|
||||
network_id=service.network.value,
|
||||
security_groups_ids=service.security_groups.value,
|
||||
security_groups_names=service.security_groups.value,
|
||||
)
|
||||
|
||||
def test_userservice_linked_cache_l1(self) -> None:
|
||||
@ -194,7 +194,7 @@ class TestOpenstackLiveDeployment(UDSTransactionTestCase):
|
||||
|
||||
# Now, should be finished without any problem, no call to api should have been done
|
||||
self.assertEqual(state, types.states.TaskState.FINISHED, f'State: {state} {userservice._error_debug_info}')
|
||||
api().get_server.assert_called()
|
||||
api().get_server_info.assert_called()
|
||||
api().stop_server.assert_called()
|
||||
api().delete_server.assert_called()
|
||||
|
||||
|
@ -40,6 +40,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
T = typing.TypeVar('T')
|
||||
V = typing.TypeVar('V', bound=collections.abc.Mapping[str, typing.Any])
|
||||
|
||||
|
||||
def compare_dicts(
|
||||
@ -58,7 +59,7 @@ def compare_dicts(
|
||||
ignore_keys_startswith = ignore_keys_startswith or []
|
||||
ignore_values_startswith = ignore_values_startswith or []
|
||||
|
||||
errors:list[tuple[str, str]] = []
|
||||
errors: list[tuple[str, str]] = []
|
||||
|
||||
for k, v in expected.items():
|
||||
if k in ignore_keys:
|
||||
@ -147,10 +148,10 @@ def random_hostname() -> str:
|
||||
# This is a simple class that returns true if the types of the two objects are the same
|
||||
class MustBeOfType:
|
||||
_kind: type[typing.Any]
|
||||
|
||||
|
||||
def __init__(self, kind: type) -> None:
|
||||
self._kind = kind
|
||||
|
||||
|
||||
def __eq__(self, other: typing.Any) -> bool:
|
||||
return isinstance(other, self._kind)
|
||||
|
||||
@ -163,6 +164,7 @@ class MustBeOfType:
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
|
||||
def search_item_by_attr(lst: list[T], attribute: str, value: typing.Any, **kwargs: typing.Any) -> T:
|
||||
"""
|
||||
Returns an item from a list of items
|
||||
@ -173,7 +175,21 @@ def search_item_by_attr(lst: list[T], attribute: str, value: typing.Any, **kwarg
|
||||
return item
|
||||
raise ValueError(f'Item with {attribute}=="{value}" not found in list {str(lst)[:100]}')
|
||||
|
||||
def filter_list_by_attr(lst: list[T], attribute: str, value: typing.Any,*, sorted_by: str = '', **kwargs: typing.Any) -> list[T]:
|
||||
|
||||
def search_dict_by_attr(lst: list[V], attribute: str, value: typing.Any, **kwargs: typing.Any) -> V:
|
||||
"""
|
||||
Returns an item from a list of items
|
||||
kwargs are not used, just to let use it as partial on fixtures
|
||||
"""
|
||||
for item in lst:
|
||||
if item.get(attribute) == value:
|
||||
return item
|
||||
raise ValueError(f'Item with {attribute}=="{value}" not found in list {str(lst)[:100]}')
|
||||
|
||||
|
||||
def filter_list_by_attr(
|
||||
lst: list[T], attribute: str, value: typing.Any, *, sorted_by: str = '', **kwargs: typing.Any
|
||||
) -> list[T]:
|
||||
"""
|
||||
Returns a list of items from a list of items
|
||||
kwargs are not used, just to let use it as partial on fixtures
|
||||
@ -183,7 +199,10 @@ def filter_list_by_attr(lst: list[T], attribute: str, value: typing.Any,*, sorte
|
||||
values.sort(key=lambda x: getattr(x, sorted_by))
|
||||
return values
|
||||
|
||||
def filter_list_by_attr_list(lst: list[T], attribute: str, values: list[typing.Any], *, sorted_by: str = '', **kwargs: typing.Any) -> list[T]:
|
||||
|
||||
def filter_list_by_attr_list(
|
||||
lst: list[T], attribute: str, values: list[typing.Any], *, sorted_by: str = '', **kwargs: typing.Any
|
||||
) -> list[T]:
|
||||
"""
|
||||
Returns a list of items from a list of items
|
||||
kwargs are not used, just to let use it as partial on fixtures
|
||||
@ -193,6 +212,7 @@ def filter_list_by_attr_list(lst: list[T], attribute: str, values: list[typing.A
|
||||
values.sort(key=lambda x: getattr(x, sorted_by))
|
||||
return values
|
||||
|
||||
|
||||
def check_userinterface_values(obj: ui.UserInterface, values: ui.gui.ValuesDictType) -> None:
|
||||
"""
|
||||
Checks that a user interface object has the values specified
|
||||
|
@ -629,9 +629,7 @@ class gui:
|
||||
old_field_name=old_field_name,
|
||||
)
|
||||
# Update parent type
|
||||
self.field_type = (
|
||||
types.ui.FieldType.TEXT_AUTOCOMPLETE
|
||||
) # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
self.field_type = types.ui.FieldType.TEXT_AUTOCOMPLETE
|
||||
self._fields_info.choices = gui.as_choices(choices or [])
|
||||
|
||||
def set_choices(self, values: collections.abc.Iterable[typing.Union[str, types.ui.ChoiceItem]]) -> None:
|
||||
|
@ -60,7 +60,7 @@ def get_api(parameters: dict[str, str]) -> tuple[client.OpenStackClient, bool]:
|
||||
return (provider.api(parameters['project'], parameters['region']), use_subnets_names)
|
||||
|
||||
|
||||
def get_resources(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
def list_resources(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
'''
|
||||
This helper is designed as a callback for Project Selector
|
||||
'''
|
||||
@ -71,7 +71,8 @@ def get_resources(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
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, f'{z.name} ({z.vcpus} vCPUs, {z.ram} MiB)') for z in api.list_flavors() if not z.disabled]
|
||||
security_groups = [gui.choice_item(z.id, z.name) for z in api.list_security_groups()]
|
||||
# Security groups are used on creation, and used by name...
|
||||
security_groups = [gui.choice_item(z.name, z.name) for z in api.list_security_groups()]
|
||||
|
||||
data: types.ui.CallbackResultType = [
|
||||
{'name': 'availability_zone', 'choices': zones},
|
||||
@ -83,7 +84,7 @@ def get_resources(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
return data
|
||||
|
||||
|
||||
def get_volumes(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
def list_volumes(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
'''
|
||||
This helper is designed as a callback for Zone Selector
|
||||
'''
|
||||
@ -98,7 +99,7 @@ def get_volumes(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
logger.debug('Return data: %s', data)
|
||||
return data
|
||||
|
||||
def get_machines(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
def list_servers(parameters: dict[str, str]) -> types.ui.CallbackResultType:
|
||||
# Needs prov_uuid, project and region in order to work
|
||||
api = get_api(parameters)[0]
|
||||
|
||||
|
@ -36,7 +36,9 @@ import json
|
||||
import typing
|
||||
import collections.abc
|
||||
|
||||
import requests
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from uds.core import consts
|
||||
|
||||
from uds.core.services.generics import exceptions
|
||||
@ -44,9 +46,6 @@ 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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -165,7 +164,9 @@ class OpenStackClient: # pylint: disable=too-many-public-methods
|
||||
if self._identity_endpoint[-1] != '/':
|
||||
self._identity_endpoint += '/'
|
||||
|
||||
self.cache = cache.Cache(f'openstack_{identity_endpoint}_{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]:
|
||||
@ -255,6 +256,8 @@ class OpenStackClient: # pylint: disable=too-many-public-methods
|
||||
OpenStackClient._ensure_valid_response(r, error_message, expects_json=expects_json)
|
||||
logger.debug('Result: %s', r.content)
|
||||
return r
|
||||
except exceptions.NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
if i == len(found_endpoints) - 1:
|
||||
# Endpoint is down, can retry if none is working
|
||||
@ -305,7 +308,7 @@ 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
|
||||
|
||||
@ -427,6 +430,7 @@ class OpenStackClient: # pylint: disable=too-many-public-methods
|
||||
self,
|
||||
detail: bool = False,
|
||||
params: typing.Optional[dict[str, str]] = None,
|
||||
**kwargs: typing.Any,
|
||||
) -> list[openstack_types.ServerInfo]:
|
||||
return [
|
||||
openstack_types.ServerInfo.from_dict(s)
|
||||
@ -439,30 +443,6 @@ class OpenStackClient: # pylint: disable=too-many-public-methods
|
||||
)
|
||||
]
|
||||
|
||||
@decorators.cached(prefix='imgs', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
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.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',
|
||||
)
|
||||
]
|
||||
|
||||
@decorators.cached(prefix='vols', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_volumes(self) -> list[openstack_types.VolumeInfo]:
|
||||
return [
|
||||
@ -477,17 +457,19 @@ class OpenStackClient: # pylint: disable=too-many-public-methods
|
||||
|
||||
@decorators.cached(prefix='snps', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
def list_volume_snapshots(
|
||||
self, volume_id: typing.Optional[dict[str, typing.Any]] = None
|
||||
self, volume_id: typing.Optional[str] = None
|
||||
) -> list[openstack_types.SnapshotInfo]:
|
||||
path = '/snapshots'
|
||||
if volume_id is not None:
|
||||
path += f'?volume_id={volume_id}'
|
||||
return [
|
||||
openstack_types.SnapshotInfo.from_dict(snapshot)
|
||||
for snapshot in self._get_recurring_from_endpoint(
|
||||
endpoint_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path='/snapshots',
|
||||
path=path,
|
||||
error_message='List snapshots',
|
||||
key='snapshots',
|
||||
)
|
||||
if volume_id is None or snapshot['volume_id'] == volume_id
|
||||
]
|
||||
|
||||
@decorators.cached(prefix='azs', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
|
||||
@ -727,9 +709,10 @@ class OpenStackClient: # pylint: disable=too-many-public-methods
|
||||
'max_count': count,
|
||||
'min_count': count,
|
||||
'networks': [{'uuid': network_id}],
|
||||
'security_groups': [{'name': sg} for sg in security_groups_names],
|
||||
}
|
||||
}
|
||||
if security_groups_names:
|
||||
data['server']['security_groups'] = [{'name': sg} for sg in security_groups_names]
|
||||
|
||||
r = self._request_from_endpoint(
|
||||
'post',
|
||||
@ -739,7 +722,10 @@ class OpenStackClient: # pylint: disable=too-many-public-methods
|
||||
error_message='Create instance from snapshot',
|
||||
)
|
||||
|
||||
return openstack_types.ServerInfo.from_dict(r.json()['server'])
|
||||
server = openstack_types.ServerInfo.from_dict(r.json()['server'])
|
||||
# Update name, not returned by openstack
|
||||
server.name = name
|
||||
return server
|
||||
|
||||
def delete_server(self, server_id: str) -> None:
|
||||
# This does not returns anything
|
||||
@ -915,24 +901,24 @@ class OpenStackClient: # pylint: disable=too-many-public-methods
|
||||
|
||||
@staticmethod
|
||||
def _ensure_valid_response(
|
||||
response: 'requests.Response', errMsg: typing.Optional[str] = None, expects_json: bool = True
|
||||
response: 'requests.Response', error_message: typing.Optional[str] = None, expects_json: bool = True
|
||||
) -> None:
|
||||
if response.ok is False:
|
||||
if not expects_json:
|
||||
return # If not expecting json, simply return
|
||||
if response.status_code == 404:
|
||||
raise exceptions.NotFoundError('Not found')
|
||||
try:
|
||||
# Extract any key, in case of error is expected to have only one top key so this will work
|
||||
_, err = response.json().popitem()
|
||||
msg = ': {message}'.format(**err)
|
||||
errMsg = errMsg + msg if errMsg else msg
|
||||
error_message = error_message + msg if error_message else msg
|
||||
except (
|
||||
Exception
|
||||
): # nosec: If error geting error message, simply ignore it (will be loged on service log anyway)
|
||||
pass
|
||||
if errMsg is None:
|
||||
errMsg = 'Error checking response'
|
||||
logger.error('%s: %s', errMsg, response.content)
|
||||
raise Exception(errMsg)
|
||||
if error_message is None and expects_json:
|
||||
error_message = 'Error checking response'
|
||||
logger.error('%s: %s', error_message, response.content)
|
||||
raise Exception(error_message)
|
||||
|
||||
# Only for testing purposes, not used at runtime
|
||||
def t_create_volume(self, name: str, size: int) -> openstack_types.VolumeInfo:
|
||||
@ -954,6 +940,17 @@ class OpenStackClient: # pylint: disable=too-many-public-methods
|
||||
|
||||
return openstack_types.VolumeInfo.from_dict(r.json()['volume'])
|
||||
|
||||
def t_set_volume_bootable(self, volume_id: str, bootable: bool) -> None:
|
||||
# Set boot info if needed
|
||||
self._request_from_endpoint(
|
||||
'post',
|
||||
endpoints_types=VOLUMES_ENDPOINT_TYPES,
|
||||
path=f'/volumes/{volume_id}/action',
|
||||
data=json.dumps({'os-set_bootable': {'bootable': bootable}}),
|
||||
error_message='Set Volume bootable',
|
||||
expects_json=False,
|
||||
)
|
||||
|
||||
def t_delete_volume(self, volume_id: str) -> None:
|
||||
# This does not returns anything
|
||||
self._request_from_endpoint(
|
||||
|
@ -174,7 +174,7 @@ class VolumeStatus(enum.StrEnum):
|
||||
EXTENDING = 'extending' # The volume is being extended.
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self in [VolumeStatus.AVAILABLE, VolumeStatus.IN_USE]
|
||||
return self == VolumeStatus.AVAILABLE
|
||||
|
||||
@staticmethod
|
||||
def from_str(s: str) -> 'VolumeStatus':
|
||||
@ -308,7 +308,7 @@ class ServerInfo:
|
||||
flavor = ''
|
||||
return ServerInfo(
|
||||
id=d['id'],
|
||||
name=d.get('name', d['id']),
|
||||
name=d.get('name', d['id']), # On create server, name is not returned, so use id
|
||||
href=href,
|
||||
flavor=flavor,
|
||||
status=ServerStatus.from_str(d.get('status', ServerStatus.UNKNOWN.value)),
|
||||
@ -356,19 +356,6 @@ class RegionInfo:
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
|
@ -156,7 +156,7 @@ class OpenStackProvider(ServiceProvider):
|
||||
concurrent_removal_limit = fields.concurrent_removal_limit_field()
|
||||
timeout = fields.timeout_field(default=10)
|
||||
|
||||
tenant = gui.TextField(
|
||||
project_id = gui.TextField(
|
||||
length=64,
|
||||
label=_('Project Id'),
|
||||
order=40,
|
||||
@ -164,6 +164,7 @@ class OpenStackProvider(ServiceProvider):
|
||||
required=False,
|
||||
default='',
|
||||
tab=types.ui.Tab.ADVANCED,
|
||||
old_field_name='tenant',
|
||||
)
|
||||
region = gui.TextField(
|
||||
length=64,
|
||||
@ -213,7 +214,7 @@ class OpenStackProvider(ServiceProvider):
|
||||
self.timeout.value = validators.validate_timeout(self.timeout.value)
|
||||
if self.auth_method.value == openstack_types.AuthMethod.APPLICATION_CREDENTIAL:
|
||||
# Ensure that the project_id is provided, so it's bound to the application credential
|
||||
if not self.tenant.value:
|
||||
if not self.project_id.value:
|
||||
raise exceptions.ui.ValidationError(
|
||||
_('Project Id is required when using Application Credential')
|
||||
)
|
||||
@ -221,7 +222,7 @@ class OpenStackProvider(ServiceProvider):
|
||||
def api(
|
||||
self, projectid: typing.Optional[str] = None, region: typing.Optional[str] = None
|
||||
) -> client.OpenStackClient:
|
||||
projectid = projectid or self.tenant.value or None
|
||||
projectid = projectid or self.project_id.value or None
|
||||
region = region or self.region.value or None
|
||||
if self._api is None:
|
||||
proxies: 'dict[str, str]|None' = None
|
||||
|
@ -50,26 +50,6 @@ class OpenStackLivePublication(DynamicPublication, autoserializable.AutoSerializ
|
||||
"""
|
||||
This class provides the publication of a oVirtLinkedService
|
||||
"""
|
||||
|
||||
# _name = autoserializable.StringField(default='')
|
||||
# _vmid = autoserializable.StringField(default='')
|
||||
# _reason = autoserializable.StringField(default='')
|
||||
# _status = autoserializable.StringField(default='r')
|
||||
# _destroy_after = autoserializable.BoolField(default=False)
|
||||
|
||||
|
||||
# _name = autoserializable.StringField(default='')
|
||||
# _vmid = autoserializable.StringField(default='')
|
||||
# _queue = autoserializable.ListField[Operation]()
|
||||
# _reason = autoserializable.StringField(default='')
|
||||
# _is_flagged_for_destroy = autoserializable.BoolField(default=False)
|
||||
|
||||
# _name: str = ''
|
||||
# _reason: str = ''
|
||||
# _template_id: str = ''
|
||||
# _state: str = 'r'
|
||||
# _destroyAfter: str = 'n'
|
||||
|
||||
suggested_delay = 20 # : Suggested recheck time if publication is unfinished in seconds
|
||||
|
||||
def service(self) -> 'OpenStackLiveService':
|
||||
|
@ -116,7 +116,7 @@ class OpenStackLiveService(DynamicService):
|
||||
order=2,
|
||||
fills={
|
||||
'callback_name': 'osFillResources',
|
||||
'function': helpers.get_resources,
|
||||
'function': helpers.list_resources,
|
||||
'parameters': ['prov_uuid', 'project', 'region'],
|
||||
},
|
||||
tooltip=_('Project for this service'),
|
||||
@ -128,7 +128,7 @@ class OpenStackLiveService(DynamicService):
|
||||
order=3,
|
||||
fills={
|
||||
'callback_name': 'osFillVolumees',
|
||||
'function': helpers.get_volumes,
|
||||
'function': helpers.list_volumes,
|
||||
'parameters': [
|
||||
'prov_uuid',
|
||||
'project',
|
||||
@ -177,6 +177,7 @@ class OpenStackLiveService(DynamicService):
|
||||
lenname = DynamicService.lenname
|
||||
|
||||
maintain_on_error = DynamicService.maintain_on_error
|
||||
try_soft_shutdown = DynamicService.try_soft_shutdown
|
||||
|
||||
prov_uuid = gui.HiddenField()
|
||||
|
||||
@ -214,11 +215,11 @@ class OpenStackLiveService(DynamicService):
|
||||
|
||||
self.region.set_choices(regions)
|
||||
|
||||
if parent and parent.tenant.value:
|
||||
tenants = [gui.choice_item(parent.tenant.value, parent.tenant.value)]
|
||||
if parent and parent.project_id.value:
|
||||
projects = [gui.choice_item(parent.project_id.value, parent.project_id.value)]
|
||||
else:
|
||||
tenants = [gui.choice_item(t.id, t.name) for t in api.list_projects()]
|
||||
self.project.set_choices(tenants)
|
||||
projects = [gui.choice_item(t.id, t.name) for t in api.list_projects()]
|
||||
self.project.set_choices(projects)
|
||||
|
||||
self.prov_uuid.value = self.provider().get_uuid()
|
||||
|
||||
|
@ -90,7 +90,7 @@ class OpenStackServiceFixed(FixedService): # pylint: disable=too-many-public-me
|
||||
order=2,
|
||||
fills={
|
||||
'callback_name': 'osGetMachines',
|
||||
'function': helpers.get_machines,
|
||||
'function': helpers.list_servers,
|
||||
'parameters': ['prov_uuid', 'project', 'region'],
|
||||
},
|
||||
tooltip=_('Project for this service'),
|
||||
@ -127,8 +127,8 @@ class OpenStackServiceFixed(FixedService): # pylint: disable=too-many-public-me
|
||||
|
||||
self.region.set_choices(regions)
|
||||
|
||||
if parent and parent.tenant.value:
|
||||
tenants = [gui.choice_item(parent.tenant.value, parent.tenant.value)]
|
||||
if parent and parent.project_id.value:
|
||||
tenants = [gui.choice_item(parent.project_id.value, parent.project_id.value)]
|
||||
else:
|
||||
tenants = [gui.choice_item(t.id, t.name) for t in api.list_projects()]
|
||||
self.project.set_choices(tenants)
|
||||
|
Loading…
Reference in New Issue
Block a user