1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-01-22 22:03:54 +03:00

Added initial fixed service to OpenStack. Need to complete related tests

This commit is contained in:
Adolfo Gómez García 2024-03-12 18:52:09 +01:00
parent c64a788523
commit 7be200f173
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
9 changed files with 419 additions and 24 deletions

View File

@ -134,7 +134,7 @@ class OpenStackLiveUserService(
def get_name(self) -> str:
if self._name == '':
try:
self._name = self.name_generator().get(
self._name = 'UDS-U-' + self.name_generator().get(
self.service().get_basename(), self.service().get_lenname()
)
except KeyError:
@ -278,7 +278,7 @@ class OpenStackLiveUserService(
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
# Fix queue to FINISH and return it
self._queue = [Operation.FINISH]
return types.states.TaskState.FINISHED

View File

@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# 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 logging
import typing
from uds.core import types
from uds.core.services.specializations.fixed_machine.fixed_userservice import FixedUserService, Operation
from uds.core.util import autoserializable
from .openstack import types as openstack_types
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from . import service_fixed
logger = logging.getLogger(__name__)
class OpenStackUserServiceFixed(FixedUserService, autoserializable.AutoSerializable):
"""
This class generates the user consumable elements of the service tree.
After creating at administration interface an Deployed Service, UDS will
create consumable services for users using UserDeployment class as
provider of this elements.
The logic for managing vmware deployments (user machines in this case) is here.
"""
# : Recheck every ten seconds by default (for task methods)
suggested_delay = 4
# Utility overrides for type checking...
def service(self) -> 'service_fixed.OpenStackServiceFixed':
return typing.cast('service_fixed.OpenStackServiceFixed', super().service())
def set_ready(self) -> types.states.TaskState:
if self.cache.get('ready') == '1':
return types.states.TaskState.FINISHED
try:
vminfo = self.service().get_machine_info(self._vmid)
except Exception as e:
return self._error(f'Machine not found: {e}')
if vminfo.status == 'stopped':
self._queue = [Operation.START, Operation.FINISH]
return self._execute_queue()
self.cache.put('ready', '1')
return types.states.TaskState.FINISHED
def reset(self) -> None:
"""
OpenStack, reset operation
"""
if self._vmid != '':
try:
self.service().api.reset_server(self._vmid)
except Exception: # nosec: if cannot reset, ignore it
pass # If could not reset, ignore it...
def process_ready_from_os_manager(self, data: typing.Any) -> types.states.TaskState:
return types.states.TaskState.FINISHED
def error(self, reason: str) -> types.states.TaskState:
return self._error(reason)
def _start_machine(self) -> None:
try:
vminfo = self.service().get_machine_info(self._vmid)
except Exception as e:
raise Exception('Machine not found on start machine') from e
if vminfo.power_state != openstack_types.PowerState.RUNNING:
self.service().api.start_server(self._vmid) # Start the server

View File

@ -43,7 +43,7 @@ from .openstack import openstack_client
logger = logging.getLogger(__name__)
def getApi(parameters: dict[str, str]) -> tuple[openstack_client.OpenstackClient, bool]:
def get_api(parameters: dict[str, str]) -> tuple[openstack_client.OpenstackClient, bool]:
from .provider_legacy import OpenStackProviderLegacy
from .provider import OpenStackProvider
@ -64,7 +64,7 @@ def get_resources(parameters: dict[str, str]) -> types.ui.CallbackResultType:
'''
This helper is designed as a callback for Project Selector
'''
api, name_from_subnets = getApi(parameters)
api, name_from_subnets = get_api(parameters)
zones = [gui.choice_item(z.id, z.name) for z in api.list_availability_zones()]
networks = [
@ -87,7 +87,7 @@ def get_volumes(parameters: dict[str, str]) -> types.ui.CallbackResultType:
'''
This helper is designed as a callback for Zone Selector
'''
api, _ = getApi(parameters)
api, _ = get_api(parameters)
# Source volumes are all available for us
# volumes = [gui.choice_item(v['id'], v['name']) for v in api.listVolumes() if v['name'] != '' and v['availability_zone'] == parameters['availabilityZone']]
volumes = [gui.choice_item(v.id, v.name) for v in api.list_volumes() if v.name]
@ -97,3 +97,19 @@ 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:
# Needs prov_uuid, project and region in order to work
api = get_api(parameters)[0]
try:
servers = [gui.choice_item(s.id, s.name) for s in api.list_servers() if not s.name.lower().startswith('uds')]
except Exception:
return []
return [
{
'name': 'machines',
'choices': servers,
}
]

View File

@ -35,14 +35,14 @@ import typing
from django.utils.translation import gettext_noop as _
from uds.core import types, consts
from uds.core import types
from uds.core.services import ServiceProvider
from uds.core.ui import gui
from uds.core.util import validators, fields
from uds.core.util.decorators import cached
from .openstack import openstack_client, sanitized_name, types as openstack_types
from .service import OpenStackLiveService
from .service_fixed import OpenStackServiceFixed
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
@ -76,7 +76,7 @@ class OpenStackProvider(ServiceProvider):
"""
# : What kind of services we offer, this are classes inherited from Service
offers = [OpenStackLiveService]
offers = [OpenStackLiveService, OpenStackServiceFixed]
# : Name to show the administrator. This string will be translated BEFORE
# : sending it to administration interface, so don't forget to
# : mark it as _ (using gettext_noop)

View File

@ -45,6 +45,7 @@ from uds.core.util.decorators import cached
from .openstack import openstack_client, sanitized_name, types as openstack_types
from .service import OpenStackLiveService
from .service_fixed import OpenStackServiceFixed
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
@ -79,7 +80,7 @@ class OpenStackProviderLegacy(ServiceProvider):
"""
# : What kind of services we offer, this are classes inherited from Service
offers = [OpenStackLiveService]
offers = [OpenStackLiveService, OpenStackServiceFixed]
# : Name to show the administrator. This string will be translated BEFORE
# : sending it to administration interface, so don't forget to
# : mark it as _ (using gettext_noop)

View File

@ -90,7 +90,7 @@ class OpenStackLivePublication(Publication, autoserializable.AutoSerializable):
Realizes the publication of the service
"""
self._name = self.service().sanitized_name(
'UDSP ' + self.servicepool_name() + "-" + str(self.revision())
'UDS-P-' + self.servicepool_name() + "-" + str(self.revision())
)
self._reason = '' # No error, no reason for it
self._destroy_after = False

View File

@ -222,9 +222,6 @@ class OpenStackLiveService(services.Service):
tenants = [gui.choice_item(t.id, t.name) for t in api.list_projects()]
self.project.set_choices(tenants)
# So we can instantiate parent to get API
logger.debug(self.provider().serialize())
self.prov_uuid.value = self.provider().get_uuid()
@property

View File

@ -0,0 +1,240 @@
#
# Copyright (c) 2012-2022 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 collections.abc
import logging
import typing
from django.utils.translation import gettext
from django.utils.translation import gettext_noop as _
from uds.core import exceptions, services, types
from uds.core.services.specializations.fixed_machine.fixed_service import FixedService
from uds.core.services.specializations.fixed_machine.fixed_userservice import FixedUserService
from uds.core.ui import gui
from uds.core.util import log
from . import helpers
from .deployment_fixed import OpenStackUserServiceFixed
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds import models
from .openstack import openstack_client, types as openstack_types
from .provider import OpenStackProvider
from .provider_legacy import OpenStackProviderLegacy
AnyOpenStackProvider: typing.TypeAlias = typing.Union[OpenStackProvider, OpenStackProviderLegacy]
logger = logging.getLogger(__name__)
class OpenStackServiceFixed(FixedService): # pylint: disable=too-many-public-methods
"""
OpenStack fixed machines service.
"""
type_name = _('OpenStack Fixed Machines')
type_type = 'OpenStackFixedService'
type_description = _('OpenStack Services based on fixed machines.')
icon_file = 'service.png'
can_reset = True
# : Types of publications (preparated data for deploys)
# : In our case, we do no need a publication, so this is None
publication_type = None
# : Types of deploys (services in cache and/or assigned to users)
user_service_type = OpenStackUserServiceFixed
allowed_protocols = types.transports.Protocol.generic_vdi()
services_type_provided = types.services.ServiceType.VDI
# Gui
token = FixedService.token
# Now the form part
region = gui.ChoiceField(
label=_('Region'),
order=1,
tooltip=_('Service region'),
required=True,
readonly=True,
)
project = gui.ChoiceField(
label=_('Project'),
order=2,
fills={
'callback_name': 'osGetMachines',
'function': helpers.get_machines,
'parameters': ['prov_uuid', 'project', 'region'],
},
tooltip=_('Project for this service'),
required=True,
readonly=True,
)
machines = FixedService.machines
prov_uuid = gui.HiddenField()
_api: typing.Optional['openstack_client.OpenstackClient'] = None
@property
def api(self) -> 'openstack_client.OpenstackClient':
if not self._api:
self._api = self.provider().api(projectid=self.project.value, region=self.region.value)
return self._api
def initialize(self, values: 'types.core.ValuesType') -> None:
"""
Loads the assigned machines from storage
"""
if values:
if not self.machines.value:
raise exceptions.ui.ValidationError(gettext('We need at least a machine'))
with self.storage.as_dict() as d:
d['userservices_limit'] = len(self.machines.as_list())
# Remove machines not in values from "assigned" set
self._save_assigned_machines(self._get_assigned_machines() & set(self.machines.as_list()))
self.token.value = self.token.value.strip()
with self.storage.as_dict() as d:
self.userservices_limit = d.get('userservices_limit', 0)
def init_gui(self) -> None:
api = self.provider().api()
# Checks if legacy or current openstack provider
parent = typing.cast('OpenStackProvider', self.provider()) if not self.provider().legacy else None
if parent and parent.region.value:
regions = [gui.choice_item(parent.region.value, parent.region.value)]
else:
regions = [gui.choice_item(r.id, r.name) for r in api.list_regions()]
self.region.set_choices(regions)
if parent and parent.tenant.value:
tenants = [gui.choice_item(parent.tenant.value, parent.tenant.value)]
else:
tenants = [gui.choice_item(t.id, t.name) for t in api.list_projects()]
self.project.set_choices(tenants)
self.prov_uuid.value = self.provider().get_uuid()
def provider(self) -> 'AnyOpenStackProvider':
return typing.cast('AnyOpenStackProvider', super().provider())
def get_machine_info(self, vmid: str) -> 'openstack_types.ServerInfo':
return self.api.get_server(vmid)
def is_avaliable(self) -> bool:
return self.provider().is_available()
def enumerate_assignables(self) -> collections.abc.Iterable[types.ui.ChoiceItem]:
# Obtain machines names and ids for asignables
servers = {server.id:server.name for server in self.api.list_servers() if not server.name.startswith('UDS-')}
assigned_servers = self._get_assigned_machines()
return [
gui.choice_item(k, servers.get(k, 'Unknown!'))
for k in self.machines.as_list()
if k not in assigned_servers
]
def assign_from_assignables(
self, assignable_id: str, user: 'models.User', userservice_instance: 'services.UserService'
) -> types.states.TaskState:
proxmox_service_instance = typing.cast(OpenStackUserServiceFixed, userservice_instance)
assigned_vms = self._get_assigned_machines()
if assignable_id not in assigned_vms:
assigned_vms.add(assignable_id)
self._save_assigned_machines(assigned_vms)
return proxmox_service_instance.assign(assignable_id)
return proxmox_service_instance.error('VM not available!')
def process_snapshot(self, remove: bool, userservice_instance: FixedUserService) -> None:
return # No snapshots support
def get_and_assign_machine(self) -> str:
found_vmid: typing.Optional[str] = None
try:
assigned_vms = self._get_assigned_machines()
for checking_vmid in self.machines.as_list():
if checking_vmid not in assigned_vms: # Not already assigned
try:
# Invoke to check it exists, do not need to store the result
if self.api.get_server(checking_vmid).status.is_lost():
raise Exception('Machine not found') # Process on except
found_vmid = checking_vmid
break
except Exception: # Notifies on log, but skipt it
self.provider().do_log(
log.LogLevel.WARNING, 'Machine {} not accesible'.format(found_vmid)
)
logger.warning(
'The service has machines that cannot be checked on proxmox (connection error or machine has been deleted): %s',
found_vmid,
)
if found_vmid:
assigned_vms.add(found_vmid)
self._save_assigned_machines(assigned_vms)
except Exception as e: #
logger.debug('Error getting machine: %s', e)
raise Exception('No machine available')
if not found_vmid:
raise Exception('All machines from list already assigned.')
return str(found_vmid)
def get_first_network_mac(self, vmid: str) -> str:
return self.api.get_server(vmid).addresses[0].mac
def get_guest_ip_address(self, vmid: str) -> str:
return self.api.get_server(vmid).addresses[0].ip
def get_machine_name(self, vmid: str) -> str:
return self.api.get_server(vmid).name
def remove_and_free_machine(self, vmid: str) -> str:
try:
self._save_assigned_machines(self._get_assigned_machines() - {str(vmid)}) # Remove from assigned
return types.states.State.FINISHED
except Exception as e:
logger.warning('Cound not save assigned machines on fixed pool: %s', e)
raise

View File

@ -156,19 +156,52 @@ class TestOpenstackLiveDeployment(UDSTransactionTestCase):
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
"""
Test the user service
"""
for keep_on_error in (True, False):
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, maintain_on_error=keep_on_error)
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
for _counter in limited_iterator(lambda: state == types.states.TaskState.RUNNING, limit=128):
state = userservice.check_state()
# Correctly created
self.assertEqual(state, types.states.TaskState.FINISHED)
# We are going to force an error on set_ready
server.status = openstack_types.ServerStatus.ERROR
state = userservice.set_ready()
if keep_on_error:
self.assertEqual(state, types.states.TaskState.FINISHED)
else:
self.assertEqual(state, types.states.TaskState.ERROR)
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
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, maintain_on_eror=True)
userservice = fixtures.create_live_userservice(service=service)
publication = userservice.publication()
publication._template_id = 'snap1'
api.create_server_from_snapshot.side_effect = Exception('Error')
state = userservice.deploy_for_user(models.User())
self.assertEqual(state, types.states.TaskState.ERROR)