1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-02-02 09:47:13 +03:00

Many more improvements to openstack and advanced on tests. Hope to finish them by tomorrow

This commit is contained in:
Adolfo Gómez García 2024-03-12 03:17:24 +01:00
parent d542e65a9c
commit 12548b44cc
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
10 changed files with 397 additions and 84 deletions

View File

@ -98,21 +98,29 @@ class Publication(Environmentable, Serializable):
service: 'services.Service',
osmanager: typing.Optional['osmanagers.OSManager'] = None,
revision: int = -1,
servicepool_name: str = 'Unknown',
servicepool_name: typing.Optional[str] = None,
uuid: str = '',
) -> None:
"""
Do not forget to invoke this in your derived class using "super(self.__class__, self).__init__(environment, values)"
Instead of overriding __init__, override initialize method, that will be called
just after all internal initialization is completed.
We want to use the env, cache and storage methods outside class. If not called, you must implement your own methods
cache and storage are "convenient" methods to access _env.cache and _env.storage
@param environment: Environment assigned to this publication
Args:
environment (Environment): Environment of the service
service (services.Service): Service that owns this publication
osmanager (osmanagers.OSManager, optional): OsManager associated with this publication. Defaults to None.
revision (int, optional): Revision of the publication. Defaults to -1.
servicepool_name (str, optional): Name of the service pool. Defaults to None. (Unknown)
uuid (str, optional): UUID of the publication. Defaults to ''.
"""
Environmentable.__init__(self, environment)
Serializable.__init__(self)
self._service = service
self._osmanager = osmanager
self._revision = revision
self._servicepool_name = servicepool_name
self._servicepool_name = servicepool_name or 'Unknown'
self._uuid = uuid
self.initialize()

View File

@ -109,7 +109,7 @@ class UserService(Environmentable, Serializable):
method error_reason can be called multiple times, including
serializations in middle, so remember to include reason of error in serializations
"""
USER: int = 0 # : Constant for User cache level
L1_CACHE = 1 # : Constant for Cache of level 1
L2_CACHE = 2 # : Constant for Cache of level 2

View File

@ -51,6 +51,10 @@ if typing.TYPE_CHECKING:
logger = logging.getLogger(__name__)
# How many times we will check for a machine to be ready/stopped/whatever
# 25 = 25 * 5 = 125 seconds (5 is suggested_delay)
CHECK_COUNT_BEFORE_FAILURE: typing.Final[int] = 25
class Operation(enum.IntEnum):
CREATE = 0
@ -61,6 +65,7 @@ class Operation(enum.IntEnum):
ERROR = 5
FINISH = 6
RETRY = 7
STOP = 8
UNKNOWN = 99
@ -72,7 +77,7 @@ class Operation(enum.IntEnum):
return Operation.UNKNOWN
class OpenStackLiveDeployment(
class OpenStackLiveUserService(
services.UserService, autoserializable.AutoSerializable
): # pylint: disable=too-many-public-methods
"""
@ -90,6 +95,7 @@ class OpenStackLiveDeployment(
_mac = autoserializable.StringField(default='')
_vmid = autoserializable.StringField(default='')
_reason = autoserializable.StringField(default='')
_check_count = autoserializable.IntegerField(default=CHECK_COUNT_BEFORE_FAILURE)
_queue = autoserializable.ListField[Operation]()
# _name: str = ''
@ -200,26 +206,32 @@ class OpenStackLiveDeployment(
"""
Deploys an service instance for cache
"""
self._init_queue_for_deploy(level == self.L2_CACHE)
self._init_queue_for_deploy(level)
return self._execute_queue()
def _init_queue_for_deploy(self, forLevel2: bool = False) -> None:
if forLevel2 is False:
def _init_queue_for_deploy(self, level: int) -> None:
if level in (self.USER, self.L1_CACHE):
self._queue = [Operation.CREATE, Operation.FINISH]
else:
self._queue = [Operation.CREATE, Operation.WAIT, Operation.SUSPEND, Operation.FINISH]
def _check_machine_power_state(self, check_state: openstack_types.PowerState) -> types.states.TaskState:
def _check_machine_power_state(self, *check_state: 'openstack_types.PowerState') -> types.states.TaskState:
self._check_count -= 1
if self._check_count < 0:
return self._error('Machine is not {str(check_state)} after {CHECK_COUNT_BEFORE_FAILURE} checks')
logger.debug(
'Checking that state of machine %s (%s) is %s',
'Checking that state of machine %s (%s) is %s (remaining checks: %s)',
self._vmid,
self._name,
check_state,
self._check_count,
)
power_state = self.service().get_machine_power_state(self._vmid)
ret = types.states.TaskState.RUNNING
if power_state == check_state:
if power_state in check_state:
ret = types.states.TaskState.FINISHED
return ret
@ -236,6 +248,11 @@ class OpenStackLiveDeployment(
return self._queue.pop(0)
def _reset_check_count(self) -> None:
# Check the maximum number of checks before failure
# So we dont stuck forever on check state to CHECK_COUNT_BEFORE_FAILURE
self._check_count = CHECK_COUNT_BEFORE_FAILURE
def _error(self, reason: typing.Any) -> types.states.TaskState:
"""
Internal method to set object as error state
@ -244,16 +261,26 @@ class OpenStackLiveDeployment(
types.states.DeployState.ERROR, so we can do "return self.__error(reason)"
"""
logger.debug('Setting error state, reason: %s', reason)
is_creation = self._get_current_op() == Operation.CREATE
self._queue = [Operation.ERROR]
self._reason = str(reason)
self.do_log(log.LogLevel.ERROR, self._reason)
if self._vmid and self.service().keep_on_error() is False: # Powers off & delete it
try:
self.service().delete_machine(self._vmid)
except Exception:
logger.warning('Can\t set machine %s state to stopped', self._vmid)
if self._vmid:
# Creating machines should be deleted on error
if is_creation or self.service().keep_on_error() is False: # Powers off & delete it
try:
self.service().delete_machine(self._vmid)
except Exception:
logger.warning('Can\t set machine %s state to stopped', self._vmid)
else:
self.do_log(
log.LogLevel.INFO, 'Keep on error is enabled, machine will not be marked for deletion'
)
# Simple fix queue to FINISH and return it
self._queue = [Operation.FINISH]
return types.states.TaskState.FINISHED
return types.states.TaskState.ERROR
@ -267,27 +294,18 @@ class OpenStackLiveDeployment(
if op == Operation.FINISH:
return types.states.TaskState.FINISHED
fncs: dict[int, collections.abc.Callable[[], str]] = {
Operation.CREATE: self._create,
Operation.RETRY: self._retry,
Operation.START: self._start_machine,
Operation.SUSPEND: self._suspend_machine,
Operation.WAIT: self._wait,
Operation.REMOVE: self._remove,
}
try:
if op not in fncs:
if op not in _EXECUTE_FNCS:
return self._error('Unknown operation found at execution queue ({0})'.format(op))
fncs[op]()
_EXECUTE_FNCS[op](self)
return types.states.TaskState.RUNNING
except Exception as e:
return self._error(e)
# Queue execution methods
def _retry(self) -> types.states.TaskState:
def _retry(self) -> None:
"""
Used to retry an operation
In fact, this will not be never invoked, unless we push it twice, because
@ -295,15 +313,15 @@ class OpenStackLiveDeployment(
At executeQueue this return value will be ignored, and it will only be used at check_state
"""
return types.states.TaskState.FINISHED
pass
def _wait(self) -> types.states.TaskState:
def _wait(self) -> None:
"""
Executes opWait, it simply waits something "external" to end
"""
return types.states.TaskState.RUNNING
pass
def _create(self) -> str:
def _create(self) -> None:
"""
Deploys a machine from template for user/cache
"""
@ -320,9 +338,11 @@ class OpenStackLiveDeployment(
if not self._vmid:
raise Exception('Can\'t create machine')
return types.states.TaskState.RUNNING
self._reset_check_count()
def _remove(self) -> str:
return None
def _remove(self) -> None:
"""
Removes a machine from system
"""
@ -333,22 +353,40 @@ class OpenStackLiveDeployment(
self.service().delete_machine(self._vmid)
return types.states.TaskState.RUNNING
def _start_machine(self) -> str:
def _start_machine(self) -> None:
"""
Powers on the machine
"""
self.service().start_machine(self._vmid)
return types.states.TaskState.RUNNING
self._reset_check_count()
def _suspend_machine(self) -> str:
def _stop_machine(self) -> None:
"""
Powers off the machine
"""
self.service().stop_machine(self._vmid)
self._reset_check_count()
def _suspend_machine(self) -> None:
"""
Suspends the machine
"""
self.service().suspend_machine(self._vmid)
self._reset_check_count()
def _check_retry(self) -> types.states.TaskState:
"""
This method is invoked when a task has been retried.
"""
return types.states.TaskState.FINISHED
def _check_wait(self) -> types.states.TaskState:
"""
This method is invoked when a task is waiting for something.
"""
return types.states.TaskState.RUNNING
# Check methods
@ -356,8 +394,11 @@ class OpenStackLiveDeployment(
"""
Checks the state of a deploy for an user or cache
"""
# Checks if machine has been created
ret = self._check_machine_power_state(openstack_types.PowerState.RUNNING)
if ret == types.states.TaskState.FINISHED:
# If machine is requested to not be removed never, we may end with
# an empty mac and ip, but no problem. Next time we will get it
# Get IP & MAC (early stage)
addr = self.service().get_server_address(self._vmid)
self._mac, self._ip = addr.mac, addr.ip
@ -370,6 +411,16 @@ class OpenStackLiveDeployment(
"""
return self._check_machine_power_state(openstack_types.PowerState.RUNNING)
def _check_stop(self) -> types.states.TaskState:
"""
Checks if machine has stopped
"""
return self._check_machine_power_state(
openstack_types.PowerState.SHUTDOWN,
openstack_types.PowerState.CRASHED,
openstack_types.PowerState.SUSPENDED,
)
def _check_suspend(self) -> types.states.TaskState:
"""
Check if the machine has suspended
@ -395,20 +446,11 @@ class OpenStackLiveDeployment(
if op == Operation.FINISH:
return types.states.TaskState.FINISHED
fncs: dict[int, collections.abc.Callable[[], types.states.TaskState]] = {
Operation.CREATE: self._check_create,
Operation.RETRY: self._retry,
Operation.WAIT: self._wait,
Operation.START: self._check_start,
Operation.SUSPEND: self._check_suspend,
Operation.REMOVE: self._check_removed,
}
try:
if op not in fncs:
if op not in _CHECK_FNCS:
return self._error('Unknown operation found at execution queue ({0})'.format(op))
state = fncs[op]()
state = _CHECK_FNCS[op](self)
if state == types.states.TaskState.FINISHED:
self._pop_current_op() # Remove runing op
return self._execute_queue()
@ -426,7 +468,7 @@ class OpenStackLiveDeployment(
if level == self.L1_CACHE:
self._queue = [Operation.START, Operation.FINISH]
else:
else: # Currently L2 is not supported
self._queue = [Operation.START, Operation.SUSPEND, Operation.FINISH]
return self._execute_queue()
@ -446,11 +488,15 @@ class OpenStackLiveDeployment(
if op == Operation.ERROR:
return self._error('Machine is already in error state!')
if op == Operation.FINISH or op == Operation.WAIT:
self._queue = [Operation.REMOVE, Operation.FINISH]
return self._execute_queue()
ops = [Operation.STOP, Operation.REMOVE, Operation.FINISH]
if op == Operation.FINISH or op == Operation.WAIT:
self._queue = ops
return self._execute_queue() # Run it right now
# If an operation is pending, maybe checking, so we will wait until it finishes
self._queue = [op] + ops
self._queue = [op, Operation.REMOVE, Operation.FINISH]
# Do not execute anything.here, just continue normally
return types.states.TaskState.RUNNING
@ -487,5 +533,28 @@ class OpenStackLiveDeployment(
self._ip,
self._mac,
self._vmid,
[OpenStackLiveDeployment._op2str(op) for op in self._queue],
[OpenStackLiveUserService._op2str(op) for op in self._queue],
)
# Execution methods
_EXECUTE_FNCS: dict[int, collections.abc.Callable[[OpenStackLiveUserService], None]] = {
Operation.CREATE: OpenStackLiveUserService._create,
Operation.RETRY: OpenStackLiveUserService._retry,
Operation.START: OpenStackLiveUserService._start_machine,
Operation.STOP: OpenStackLiveUserService._stop_machine,
Operation.SUSPEND: OpenStackLiveUserService._suspend_machine,
Operation.WAIT: OpenStackLiveUserService._wait,
Operation.REMOVE: OpenStackLiveUserService._remove,
}
# Check methods
_CHECK_FNCS: dict[int, collections.abc.Callable[[OpenStackLiveUserService], types.states.TaskState]] = {
Operation.CREATE: OpenStackLiveUserService._check_create,
Operation.RETRY: OpenStackLiveUserService._check_retry,
Operation.WAIT: OpenStackLiveUserService._check_wait,
Operation.START: OpenStackLiveUserService._check_start,
Operation.STOP: OpenStackLiveUserService._check_stop,
Operation.SUSPEND: OpenStackLiveUserService._check_suspend,
Operation.REMOVE: OpenStackLiveUserService._check_removed,
}

View File

@ -558,6 +558,10 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
)
]
# Very small timeout, so repeated operations will use same data
# Any cache time less than 5 seconds will be fine, beceuse checks on
# openstack are done every 5 seconds
@decorators.cached(prefix='svr', timeout=3, key_helper=cache_key_helper)
def get_server(self, server_id: str) -> openstack_types.ServerInfo:
r = self._request_from_endpoint(
'get',
@ -771,14 +775,15 @@ class OpenstackClient: # pylint: disable=too-many-public-methods
expects_json=False,
)
def reset_server(self, server_id: str) -> None:
def reset_server(self, server_id: str, hard: bool = True) -> None:
# Does not need return value
try:
type_reboot = 'HARD' if hard else 'SOFT'
self._request_from_endpoint(
'post',
endpoints_types=COMPUTE_ENDPOINT_TYPES,
path=f'/servers/{server_id}/action',
data='{"reboot":{"type":"HARD"}}',
data='{"reboot":{"type":"' + type_reboot + '"}}',
error_message='Resetting server',
expects_json=False,
)

View File

@ -40,7 +40,7 @@ from uds.core.util import fields, validators
from uds.core.ui import gui
from .publication import OpenStackLivePublication
from .deployment import OpenStackLiveDeployment
from .deployment import OpenStackLiveUserService
from .openstack import types as openstack_types, openstack_client
from . import helpers
@ -97,7 +97,7 @@ class OpenStackLiveService(services.Service):
# : In our case, we do no need a publication, so this is None
publication_type = OpenStackLivePublication
# : Types of deploys (services in cache and/or assigned to users)
user_service_type = OpenStackLiveDeployment
user_service_type = OpenStackLiveUserService
allowed_protocols = types.transports.Protocol.generic_vdi(types.transports.Protocol.SPICE)
services_type_provided = types.services.ServiceType.VDI
@ -175,11 +175,14 @@ class OpenStackLiveService(services.Service):
basename = fields.basename_field(order=9, tab=_('Machine'))
lenname = fields.lenname_field(order=10, tab=_('Machine'))
maintain_on_error = fields.maintain_on_error_field(order=104)
maintain_on_error = fields.maintain_on_error_field(order=11, tab=_('Machine'))
prov_uuid = gui.HiddenField()
_api: typing.Optional['openstack_client.OpenstackClient'] = None
# Note: currently, Openstack does not provides a way of specifying how to stop the server
# At least, i have not found it on the documentation
def initialize(self, values: types.core.ValuesType) -> None:
"""
@ -305,6 +308,10 @@ class OpenStackLiveService(services.Service):
Returns:
"""
# if already running, do nothing
if self.get_machine_power_state(machineId) == openstack_types.PowerState.RUNNING:
return
self.api.start_server(machineId)
def stop_machine(self, machineId: str) -> None:
@ -316,6 +323,10 @@ class OpenStackLiveService(services.Service):
Returns:
"""
# If already stopped, do nothing
if self.get_machine_power_state(machineId) == openstack_types.PowerState.SHUTDOWN:
return
self.api.stop_server(machineId)
def reset_machine(self, machineId: str) -> None:
@ -338,6 +349,9 @@ class OpenStackLiveService(services.Service):
Returns:
"""
# If not running, do nothing
if self.get_machine_power_state(machineId) != openstack_types.PowerState.RUNNING:
return
self.api.suspend_server(machineId)
def resume_machine(self, machineid: str) -> None:
@ -349,6 +363,9 @@ class OpenStackLiveService(services.Service):
Returns:
"""
# If not suspended, do nothing
if self.get_machine_power_state(machineid) != openstack_types.PowerState.SUSPENDED:
return
self.api.resume_server(machineid)
def delete_machine(self, machine_id: str) -> None:

View File

@ -84,6 +84,12 @@ class StorageTest(UDSTestCase):
self.assertEqual(d[UNICODE_CHARS], 'chars')
self.assertEqual(d['test_key'], UNICODE_CHARS_2)
# Assert that UNICODE_CHARS is in the dict
d['test_key2'] = 0
d['test_key2'] += 1
self.assertEqual(d['test_key2'], 1)
# The values set inside the "with" are not available "outside"
# because the format is not compatible (with the dict, the values are stored as a tuple, with the original key stored

View File

@ -43,7 +43,7 @@ from uds.core.ui.user_interface import gui
from ...utils.autospec import autospec, AutoSpecMethodInfo
from uds.services.OpenStack import provider, provider_legacy, service
from uds.services.OpenStack import provider, provider_legacy, service, publication, deployment
from uds.services.OpenStack.openstack import openstack_client, types as openstack_types
AnyOpenStackProvider: typing.TypeAlias = typing.Union[provider.OpenStackProvider, provider_legacy.OpenStackProviderLegacy]
@ -395,7 +395,7 @@ def create_provider_legacy(**kwargs: typing.Any) -> provider_legacy.OpenStackPro
)
def create_service(
def create_live_service(
provider: AnyOpenStackProvider, **kwargs: typing.Any
) -> service.OpenStackLiveService:
"""
@ -411,3 +411,31 @@ def create_service(
values=values,
uuid=uuid_,
)
def create_publication(service: service.OpenStackLiveService) -> publication.OpenStackLivePublication:
"""
Create a publication
"""
uuid_ = str(uuid.uuid4())
return publication.OpenStackLivePublication(
environment=environment.Environment.private_environment(uuid_),
service=service,
revision=1,
servicepool_name='servicepool_name',
uuid=uuid_,
)
def create_live_userservice(
service: service.OpenStackLiveService,
publication: typing.Optional[publication.OpenStackLivePublication] = None,
) -> deployment.OpenStackLiveUserService:
"""
Create a linked user service
"""
uuid_ = str(uuid.uuid4())
return deployment.OpenStackLiveUserService(
environment=environment.Environment.private_environment(uuid_),
service=service,
publication=publication or create_publication(service),
uuid=uuid_,
)

View File

@ -58,6 +58,7 @@ EXPECTED_FIELDS: typing.Final[set[str]] = {
'_vmid',
'_reason',
'_queue',
'_check_count'
}
TEST_QUEUE: typing.Final[list[deployment.Operation]] = [
@ -74,7 +75,7 @@ LAST_VERSION: typing.Final[str] = sorted(SERIALIZED_DEPLOYMENT_DATA.keys(), reve
class OpenStackDeploymentSerializationTest(UDSTransactionTestCase):
def check(self, version: str, instance: deployment.OpenStackLiveDeployment) -> None:
def check(self, version: str, instance: deployment.OpenStackLiveUserService) -> None:
self.assertEqual(instance._name, 'name')
self.assertEqual(instance._ip, 'ip')
self.assertEqual(instance._mac, 'mac')
@ -86,8 +87,8 @@ class OpenStackDeploymentSerializationTest(UDSTransactionTestCase):
# queue is kept on "storage", so we need always same environment
environment = Environment.testing_environment()
def _create_instance(unmarshal_data: 'bytes|None' = None) -> deployment.OpenStackLiveDeployment:
instance = deployment.OpenStackLiveDeployment(environment=environment, service=None) # type: ignore
def _create_instance(unmarshal_data: 'bytes|None' = None) -> deployment.OpenStackLiveUserService:
instance = deployment.OpenStackLiveUserService(environment=environment, service=None) # type: ignore
if unmarshal_data:
instance.unmarshal(unmarshal_data)
return instance
@ -117,8 +118,8 @@ class OpenStackDeploymentSerializationTest(UDSTransactionTestCase):
# Store queue
environment.storage.put_pickle('queue', TEST_QUEUE)
def _create_instance(unmarshal_data: 'bytes|None' = None) -> deployment.OpenStackLiveDeployment:
instance = deployment.OpenStackLiveDeployment(environment=environment, service=None) # type: ignore
def _create_instance(unmarshal_data: 'bytes|None' = None) -> deployment.OpenStackLiveUserService:
instance = deployment.OpenStackLiveUserService(environment=environment, service=None) # type: ignore
if unmarshal_data:
instance.unmarshal(unmarshal_data)
return instance
@ -162,6 +163,6 @@ class OpenStackDeploymentSerializationTest(UDSTransactionTestCase):
# This test is designed to ensure that all fields are autoserializable
# If some field is added or removed, this tests will warn us about it to fix the rest of the related tests
with Environment.temporary_environment() as env:
instance = deployment.OpenStackLiveDeployment(environment=env, service=None) # type: ignore
instance = deployment.OpenStackLiveUserService(environment=env, service=None) # type: ignore
self.assertSetEqual(set(f[0] for f in instance._autoserializable_fields()), EXPECTED_FIELDS)

View File

@ -51,7 +51,7 @@ class TestOpenstackService(UDSTransactionTestCase):
"""
for prov in (fixtures.create_provider_legacy(), fixtures.create_provider()):
with fixtures.patch_provider_api(legacy=prov.legacy) as client:
service = fixtures.create_service(prov) # Will use provider patched api
service = fixtures.create_live_service(prov) # Will use provider patched api
self.assertEqual(service.api, client)
self.assertEqual(service.sanitized_name('a b c'), 'a_b_c')
@ -83,24 +83,29 @@ class TestOpenstackService(UDSTransactionTestCase):
self.assertIsInstance(data, openstack_types.PowerState)
client.get_server.assert_called_once_with(fixtures.SERVERS_LIST[0].id)
server_id = fixtures.SERVERS_LIST[0].id
service.start_machine(server_id)
client.start_server.assert_called_once_with(server_id)
server = fixtures.SERVERS_LIST[0]
service.start_machine(server.id)
server.power_state = openstack_types.PowerState.SHUTDOWN
client.start_server.assert_called_once_with(server.id)
service.stop_machine(server_id)
client.stop_server.assert_called_once_with(server_id)
server.power_state = openstack_types.PowerState.RUNNING
service.stop_machine(server.id)
client.stop_server.assert_called_once_with(server.id)
service.suspend_machine(server_id)
client.suspend_server.assert_called_once_with(server_id)
server.power_state = openstack_types.PowerState.RUNNING
service.suspend_machine(server.id)
client.suspend_server.assert_called_once_with(server.id)
service.resume_machine(server_id)
client.resume_server.assert_called_once_with(server_id)
server.power_state = openstack_types.PowerState.SUSPENDED
service.resume_machine(server.id)
client.resume_server.assert_called_once_with(server.id)
service.reset_machine(server_id)
client.reset_server.assert_called_once_with(server_id)
service.reset_machine(server.id)
client.reset_server.assert_called_once_with(server.id)
service.delete_machine(server_id)
client.delete_server.assert_called_once_with(server_id)
service.delete_machine(server.id)
client.delete_server.assert_called_once_with(server.id)
self.assertTrue(service.is_avaliable())
client.is_available.assert_called_once_with()

View File

@ -0,0 +1,174 @@
# -*- 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
"""
from uds import models
from uds.core import types
from uds.services.OpenStack.openstack import types as openstack_types
from uds.services.OpenStack.deployment import Operation
from . import fixtures
from ...utils.test import UDSTransactionTestCase
from ...utils.generators import limited_iterator
# We use transactions on some related methods (storage access, etc...)
class TestOpenstackLiveDeployment(UDSTransactionTestCase):
def setUp(self) -> None:
pass
# Openstack only have l1 cache. L2 is not considered useful right now
def test_userservice_cachel1_and_user(self) -> None:
"""
Test the user service
"""
# Deploy for cache and deploy for user are the same, so we will test both at the same time
for to_test in ['cache', 'user']:
for prov in (fixtures.create_provider_legacy(), fixtures.create_provider()):
with fixtures.patch_provider_api(legacy=prov.legacy) as api:
service = fixtures.create_live_service(prov)
userservice = fixtures.create_live_userservice(service=service)
publication = userservice.publication()
publication._template_id = 'snap1'
if to_test == 'cache':
state = userservice.deploy_for_cache(level=1)
else:
state = userservice.deploy_for_user(models.User())
self.assertEqual(state, types.states.TaskState.RUNNING, f'Error on {to_test} deployment')
# Create server should have been called
api.create_server_from_snapshot.assert_called_with(
snapshot_id='snap1',
name=userservice._name,
availability_zone=service.availability_zone.value,
flavor_id=service.flavor.value,
network_id=service.network.value,
security_groups_ids=service.security_groups.value,
)
vmid = userservice._vmid
# Set power state of machine to running (userservice._vmid)
fixtures.get_id(fixtures.SERVERS_LIST, vmid).power_state = (
openstack_types.PowerState.RUNNING
)
# Ensure that in the event of failure, we don't loop forever
for _ in limited_iterator(lambda: state == types.states.TaskState.RUNNING, limit=128):
state = userservice.check_state()
self.assertEqual(state, types.states.TaskState.FINISHED, f'Error on {to_test} deployment')
self.assertEqual(
userservice._name[: len(service.get_basename())],
service.get_basename(),
f'Error on {to_test} deployment',
)
self.assertEqual(
len(userservice._name),
len(service.get_basename()) + service.get_lenname(),
f'Error on {to_test} deployment',
)
# Get server should have been called at least once
api.get_server.assert_called_with(vmid)
# Mac an ip should have been set
self.assertNotEqual(userservice._mac, '', f'Error on {to_test} deployment')
self.assertNotEqual(userservice._ip, '', f'Error on {to_test} deployment')
# And queue must be finished
self.assertEqual(userservice._queue, [Operation.FINISH], f'Error on {to_test} deployment')
def test_userservice_cancel(self) -> None:
"""
Test the user service
"""
for prov in (fixtures.create_provider_legacy(), fixtures.create_provider()):
with fixtures.patch_provider_api(legacy=prov.legacy) as _api:
service = fixtures.create_live_service(prov)
userservice = fixtures.create_live_userservice(service=service)
publication = userservice.publication()
publication._template_id = 'snap1'
state = userservice.deploy_for_user(models.User())
self.assertEqual(state, types.states.TaskState.RUNNING)
server = fixtures.get_id(fixtures.SERVERS_LIST, userservice._vmid)
server.power_state = openstack_types.PowerState.RUNNING
current_op = userservice._get_current_op()
# Invoke cancel
state = userservice.cancel()
self.assertEqual(state, types.states.TaskState.RUNNING)
self.assertEqual(
userservice._queue,
[current_op] + [Operation.STOP, Operation.REMOVE, Operation.FINISH],
)
counter = 0
for counter in limited_iterator(lambda: state == types.states.TaskState.RUNNING, limit=128):
state = userservice.check_state()
if counter > 5:
server.power_state = openstack_types.PowerState.SHUTDOWN
self.assertGreater(counter, 5)
self.assertEqual(state, types.states.TaskState.FINISHED)
def test_userservice_error(self) -> None:
"""
This test will not have keep on error active, and will create correctly
but will error on set_ready, so it will be put on error state
"""
pass
def test_userservice_error_keep_create(self) -> None:
"""
This test will have keep on error active, and will create incorrectly
so vm will be deleted and put on error state
"""
pass
def test_userservice_error_keep(self) -> None:
"""
This test will have keep on error active, and will create correctly
but error will came later (on set_ready) and will not be put on error state
nor deleted
"""
pass