1
0
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:
Adolfo Gómez García 2024-07-17 18:27:09 +02:00
parent b303ffc858
commit 9d51963903
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
16 changed files with 351 additions and 190 deletions

View File

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

View File

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

View 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},
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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