1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-10-23 23:34:07 +03:00

Moving common "specializations" algos for services to a common base

This commit is contained in:
Adolfo Gómez García
2024-02-02 00:22:17 +01:00
parent c4a5beafd4
commit 1d569d009a
16 changed files with 732 additions and 513 deletions

View File

@@ -129,7 +129,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
return self.fill_instance_fields(k, val) return self.fill_instance_fields(k, val)
except Exception as e: except Exception as e:
logger.error('Error getting services for %s: %s', parent, e) logger.error('Error getting services for %s: %s', parent, e)
raise self.invalid_item_response() from e raise self.invalid_item_response(repr(e)) from e
def get_row_style(self, parent: 'Model') -> types.ui.RowStyleInfo: def get_row_style(self, parent: 'Model') -> types.ui.RowStyleInfo:
return types.ui.RowStyleInfo(prefix='row-maintenance-', field='maintenance_mode') return types.ui.RowStyleInfo(prefix='row-maintenance-', field='maintenance_mode')

View File

@@ -99,7 +99,7 @@ class RegexLdap(auths.Authenticator):
tab=types.ui.Tab.CREDENTIALS, tab=types.ui.Tab.CREDENTIALS,
) )
timeout = fields.timeout_field(tab=False, default=10) # Use "main tab" timeout = fields.timeout_field(tab=None, default=10) # Use "main tab"
verify_ssl = fields.verify_ssl_field(order=11) verify_ssl = fields.verify_ssl_field(order=11)
certificate = gui.TextField( certificate = gui.TextField(

View File

@@ -90,7 +90,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
tab=types.ui.Tab.CREDENTIALS, tab=types.ui.Tab.CREDENTIALS,
) )
timeout = fields.timeout_field(tab=False, default=10) # Use "main tab" timeout = fields.timeout_field(tab=None, default=10) # Use "main tab"
verify_ssl = fields.verify_ssl_field(order=11) verify_ssl = fields.verify_ssl_field(order=11)
certificate = gui.TextField( certificate = gui.TextField(

View File

@@ -48,7 +48,7 @@ from .serializable import Serializable
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Module(abc.ABC, UserInterface, Environmentable, Serializable): class Module(UserInterface, Environmentable, Serializable, abc.ABC):
""" """
Base class for all modules used by UDS. Base class for all modules used by UDS.
This base module provides all the needed methods that modules must implement This base module provides all the needed methods that modules must implement

View File

@@ -0,0 +1,34 @@
# -*- 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.
"""
UDS Service modules interfaces and classes.
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""

View File

@@ -0,0 +1,34 @@
# -*- 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.
"""
UDS Service modules interfaces and classes.
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""

View File

@@ -0,0 +1,155 @@
#
# 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 abc
import logging
import re
import typing
import collections.abc
from django.utils.translation import gettext_noop as _, gettext
from uds.core import services, types, consts, exceptions
from uds.core.ui import gui
from uds.core.util import validators, log
from uds.core.util.cache import Cache
from uds.core.util.decorators import cached
from uds.core.workers import initialize
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core.module import Module
from uds import models
from .fixed_userservice import FixedUserService
logger = logging.getLogger(__name__)
class FixedService(services.Service, abc.ABC): # pylint: disable=too-many-public-methods
"""
Proxmox fixed machines service.
"""
is_base: typing.ClassVar[bool] = True # This is a base service, not a final one
uses_cache = False # Cache are running machine awaiting to be assigned
uses_cache_l2 = False # L2 Cache are running machines in suspended state
needs_osmanager = False # If the service needs a s.o. manager (managers are related to agents provided by services, i.e. virtual machines with agent)
must_assign_manually = False # If true, the system can't do an automatic assignation of a deployed user service from this service
# 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)
# Ensure to provide this on inherited classes
# user_service_type = FixedUserService
# allowed_protocols = types.transports.Protocol.generic_vdi(types.transports.Protocol.SPICE)
# services_type_provided = types.services.ServiceType.VDI
# Gui
token = gui.TextField(
order=1,
label=_('Service Token'),
length=16,
tooltip=_(
'Service token that will be used by actors to communicate with service. Leave empty for persistent assignation.'
),
default='',
required=False,
readonly=False,
)
def _get_assigned_machines(self) -> typing.Set[str]:
vals = self.storage.get_unpickle('vms')
logger.debug('Got storage VMS: %s', vals)
return vals or set()
def _save_assigned_machines(self, vals: typing.Set[str]) -> None:
logger.debug('Saving storage VMS: %s', vals)
self.storage.put_pickle('vms', vals)
def sanitized_name(self, name: str) -> str:
"""
Proxmox only allows machine names with [a-zA-Z0-9_-]
"""
return re.sub("[^a-zA-Z0-9_-]", "-", name)
def process_snapshot(self, remove: bool, userservice_instace: 'FixedUserService') -> str:
"""
Processes snapshot creation if needed for this service
Defaults to do nothing
Args:
remove (bool): If True, called from "remove" action, else called from "create" action
returns:
str: the state to be processes by user service
"""
return types.states.State.RUNNING
@abc.abstractmethod
def get_machine_name(self, vmid: str) -> str:
"""
Returns the machine name for the given vmid
"""
pass
@abc.abstractmethod
def get_and_assign_machine(self) -> str:
"""
Gets automatically an assigns a machine
"""
pass
@abc.abstractmethod
def remove_and_free_machine(self, vmid: str) -> None:
"""
Removes and frees a machine
"""
pass
@abc.abstractmethod
def get_first_network_mac(self, vmid: str) -> str:
"""If no mac, return empty string"""
pass
@abc.abstractmethod
def get_guest_ip_address(self, vmid: str) -> str:
pass
@abc.abstractmethod
def enumerate_assignables(self) -> list[tuple[str, str]]:
pass
@abc.abstractmethod
def assign_from_assignables(
self, assignable_id: str, user: 'models.User', user_deployment: 'services.UserService'
) -> str:
pass

View File

@@ -0,0 +1,404 @@
# -*- 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 abc
import pickle # nosec: controled data
import enum
import logging
import typing
import collections.abc
from uds.core import services, consts
from uds.core.managers.user_service import UserServiceManager
from uds.core.types.states import State
from uds.core.util import log, autoserializable
from uds.core.util.model import sql_stamp_seconds
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds import models
from . import fixed_service
logger = logging.getLogger(__name__)
class Operation(enum.IntEnum):
CREATE = 0
START = 1
STOP = 2
REMOVE = 3
WAIT = 4
ERROR = 5
FINISH = 6
RETRY = 7
UNKNOWN = 99
@staticmethod
def from_int(value: int) -> 'Operation':
try:
return Operation(value)
except ValueError:
return Operation.UNKNOWN
class FixedUserService(services.UserService, autoserializable.AutoSerializable, abc.ABC):
"""
This class represents a fixed user service, that is, a service that is assigned to an user
and that will be always the from a "fixed" machine, that is, a machine that is not created.
"""
suggested_delay = 4
_name = autoserializable.StringField(default='')
_mac = autoserializable.StringField(default='')
_vmid = autoserializable.StringField(default='')
_reason = autoserializable.StringField(default='')
_task = autoserializable.StringField(default='')
_queue = autoserializable.ListField[Operation]() # Default is empty list
_create_queue: typing.ClassVar[typing.List[Operation]] = [Operation.CREATE, Operation.START, Operation.FINISH]
_destrpy_queue: typing.ClassVar[typing.List[Operation]] = [Operation.REMOVE, Operation.FINISH]
@typing.final
def _get_current_op(self) -> Operation:
if not self._queue:
return Operation.FINISH
return self._queue[0]
@typing.final
def _pop_current_op(self) -> Operation:
if not self._queue:
return Operation.FINISH
return self._queue.pop(0)
@typing.final
def _push_front_op(self, op: Operation) -> None:
self._queue.insert(0, op)
@typing.final
def _push_back_op(self, op) -> None:
self._queue.append(op)
@typing.final
def _retry_later(self) -> str:
self._push_front_op(Operation.RETRY)
return State.RUNNING
def _error(self, reason: typing.Union[str, Exception]) -> str:
"""
Internal method to set object as error state
Returns:
State.ERROR, so we can do "return self._error(reason)"
"""
reason = str(reason)
logger.debug('Setting error state, reason: %s', reason)
self.do_log(log.LogLevel.ERROR, reason)
self._queue = [Operation.ERROR]
self._reason = reason
return State.ERROR
# Utility overrides for type checking...
# Probably, overriden again on child classes
def service(self) -> 'fixed_service.FixedService':
return typing.cast('fixed_service.FixedService', super().service())
@typing.final
def get_name(self) -> str:
return self._name
@typing.final
def set_ip(self, ip: str) -> None:
logger.debug('Setting IP to %s (ignored!!)', ip)
@typing.final
def get_unique_id(self) -> str:
return self._mac or self._name
@typing.final
def get_ip(self) -> str:
try:
if self._vmid:
return self.service().get_guest_ip_address(self._vmid)
except Exception:
pass
return ''
@typing.final
def deploy_for_user(self, user: 'models.User') -> str:
"""
Deploys an service instance for an user.
"""
logger.debug('Deploying for user')
self._init_queue_for_deploy(False)
return self._execute_queue()
@typing.final
def assign(self, vmid: str) -> str:
logger.debug('Assigning from VM {}'.format(vmid))
return self._create(vmid)
@typing.final
def _init_queue_for_deploy(self, for_level2: bool = False) -> None:
self._queue = FixedUserService._create_queue.copy() # copy is needed to avoid modifying class var
@typing.final
def _execute_queue(self) -> str:
self._debug('executeQueue')
op = self._get_current_op()
if op == Operation.ERROR:
return State.ERROR
if op == Operation.FINISH:
return State.FINISHED
fncs: collections.abc.Mapping[Operation, typing.Optional[collections.abc.Callable[[], str]]] = {
Operation.CREATE: self._create,
Operation.RETRY: self._retry,
Operation.START: self._start_machine,
Operation.STOP: self._stop_machine,
Operation.WAIT: self._wait,
Operation.REMOVE: self._remove,
}
try:
operation_runner: typing.Optional[collections.abc.Callable[[], str]] = fncs.get(op, None)
if not operation_runner:
return self._error(f'Unknown operation found at execution queue ({op})')
operation_runner()
return State.RUNNING
except Exception as e:
logger.exception('Unexpected VMware exception: %s', e)
return self._error(str(e))
@typing.final
def _retry(self) -> str:
"""
Used to retry an operation
In fact, this will not be never invoked, unless we push it twice, because
check_state method will "pop" first item when a check operation returns State.FINISHED
At executeQueue this return value will be ignored, and it will only be used at check_state
"""
return State.FINISHED
@typing.final
def _wait(self) -> str:
"""
Executes opWait, it simply waits something "external" to end
"""
return State.RUNNING
@typing.final
def _create(self, vmid: str = '') -> str:
"""
Deploys a machine from template for user/cache
"""
self._vmid = vmid or self.service().get_and_assign_machine()
self._mac = self.service().get_first_network_mac(self._vmid) or ''
self._name = self.service().get_machine_name(self._vmid) or f'VM-{self._vmid}'
# Try to process snaptshots if needed
state = self.service().process_snapshot(remove=False, userservice_instace=self)
if state == State.ERROR:
return state
# If not to be managed by a token, "autologin" user
if not self.service().get_token():
userService = self.db_obj()
if userService:
userService.set_in_use(True)
return state
@typing.final
def _remove(self) -> str:
"""
Removes the snapshot if needed and releases the machine again
"""
self.service().remove_and_free_machine(self._vmid)
state = self.service().process_snapshot(remove=True, userservice_instace=self)
return state
@abc.abstractmethod
def _start_machine(self) -> str:
pass
@abc.abstractmethod
def _stop_machine(self) -> str:
pass
# Check methods
def _create_checker(self) -> str:
"""
Checks the state of a deploy for an user or cache
"""
return State.FINISHED
@abc.abstractmethod
def _start_checker(self) -> str:
"""
Checks if machine has started
"""
pass
@abc.abstractmethod
def _stop_checker(self) -> str:
"""
Checks if machine has stoped
"""
pass
@abc.abstractmethod
def _removed_checker(self) -> str:
"""
Checks if a machine has been removed
"""
pass
@typing.final
def check_state(self) -> str:
"""
Check what operation is going on, and acts acordly to it
"""
self._debug('check_state')
op = self._get_current_op()
if op == Operation.ERROR:
return State.ERROR
if op == Operation.FINISH:
return State.FINISHED
fncs: collections.abc.Mapping[Operation, typing.Optional[collections.abc.Callable[[], str]]] = {
Operation.CREATE: self._create_checker,
Operation.RETRY: self._retry,
Operation.WAIT: self._wait,
Operation.START: self._start_checker,
Operation.STOP: self._stop_checker,
Operation.REMOVE: self._removed_checker,
}
try:
check_function: typing.Optional[collections.abc.Callable[[], str]] = fncs.get(op, None)
if check_function is None:
return self._error('Unknown operation found at check queue ({0})'.format(op))
state = check_function()
if state == State.FINISHED:
self._pop_current_op() # Remove runing op, till now only was "peek"
return self._execute_queue()
return state
except Exception as e:
logger.exception('Unexpected VMware exception: %s', e)
return self._error(str(e))
@typing.final
def finish(self) -> None:
"""
Invoked when the core notices that the deployment of a service has finished.
(No matter wether it is for cache or for an user)
"""
logger.debug('Finished machine %s', self._name)
@typing.final
def error_reason(self) -> str:
"""
Returns the reason of the error.
Remember that the class is responsible of returning this whenever asked
for it, and it will be asked everytime it's needed to be shown to the
user (when the administation asks for it).
"""
return self._reason
@typing.final
def destroy(self) -> str:
"""
Invoked for destroying a deployed service
"""
self._queue = FixedUserService._destrpy_queue.copy() # copy is needed to avoid modifying class var
return self._execute_queue()
@typing.final
def cancel(self) -> str:
"""
This is a task method. As that, the excepted return values are
State values RUNNING, FINISHED or ERROR.
This can be invoked directly by an administration or by the clean up
of the deployed service (indirectly).
When administrator requests it, the cancel is "delayed" and not
invoked directly.
"""
logger.debug('Canceling %s with taskId=%s, vmId=%s', self._name, self._task, self._vmid)
return self.destroy()
@staticmethod
def _op2str(op: Operation) -> str:
return {
Operation.CREATE: 'create',
Operation.START: 'start',
Operation.STOP: 'stop',
Operation.REMOVE: 'remove',
Operation.WAIT: 'wait',
Operation.ERROR: 'error',
Operation.FINISH: 'finish',
Operation.RETRY: 'retry',
}.get(op, '????')
def _debug(self, txt: str) -> None:
# logger.debug('_name {0}: {1}'.format(txt, self._name))
# logger.debug('_ip {0}: {1}'.format(txt, self._ip))
# logger.debug('_mac {0}: {1}'.format(txt, self._mac))
# logger.debug('_vmId {0}: {1}'.format(txt, self._vmId))
logger.debug(
'Queue at %s for %s: %s, mac:%s, vmId:%s, task:%s',
txt,
self._name,
[FixedUserService._op2str(op) for op in self._queue],
self._mac,
self._vmid,
self._task,
)

View File

@@ -5,4 +5,4 @@ This module contains the definition of UserInterface, needed to describe the int
between an UDS module and the administration interface between an UDS module and the administration interface
""" """
from .user_interface import gui, UserInterface, UserInterfaceType, UserInterfaceAbstract from .user_interface import gui, UserInterface

View File

@@ -1342,7 +1342,7 @@ class gui:
) )
class UserInterfaceType(type): class UserInterfaceType(abc.ABCMeta, type):
""" """
Metaclass definition for moving the user interface descriptions to a usable Metaclass definition for moving the user interface descriptions to a usable
better place. This is done this way because we will "deepcopy" these fields better place. This is done this way because we will "deepcopy" these fields
@@ -1368,15 +1368,10 @@ class UserInterfaceType(type):
new_class_dict[attrName] = attr new_class_dict[attrName] = attr
new_class_dict['_gui_fields_template'] = _gui new_class_dict['_gui_fields_template'] = _gui
return typing.cast('UserInterfaceType', type.__new__(mcs, classname, bases, new_class_dict)) return typing.cast('UserInterfaceType', super().__new__(mcs, classname, bases, new_class_dict))
# Intermediate class to allow to use abc.ABC and UserInterface class UserInterface(metaclass=UserInterfaceType):
class UserInterfaceAbstract(abc.ABCMeta, UserInterfaceType):
pass
class UserInterface(metaclass=UserInterfaceAbstract):
""" """
This class provides the management for gui descriptions (user forms) This class provides the management for gui descriptions (user forms)

View File

@@ -54,6 +54,7 @@ import zlib
import base64 import base64
import hashlib import hashlib
import struct import struct
import abc
# Import the cryptography library # Import the cryptography library
from cryptography import fernet from cryptography import fernet
@@ -120,6 +121,7 @@ def is_autoserializable_data(data: bytes) -> bool:
""" """
return data[: len(HEADER_BASE)] == HEADER_BASE return data[: len(HEADER_BASE)] == HEADER_BASE
@dataclasses.dataclass(slots=True) @dataclasses.dataclass(slots=True)
class _MarshalInfo: class _MarshalInfo:
name: str name: str
@@ -205,7 +207,7 @@ class _SerializableField(typing.Generic[T]):
# Try casting to load values (maybe a namedtuple, i.e.) # Try casting to load values (maybe a namedtuple, i.e.)
try: try:
if isinstance(value, collections.abc.Mapping): if isinstance(value, collections.abc.Mapping):
value = self.obj_type(**value) # If a dict, try to cast it to the object value = self.obj_type(**value) # If a dict, try to cast it to the object
elif isinstance(value, collections.abc.Iterable): # IF a list, tuple, etc... try to cast it elif isinstance(value, collections.abc.Iterable): # IF a list, tuple, etc... try to cast it
value = self.obj_type(*value) value = self.obj_type(*value)
else: # Maybe it has a constructor that accepts a single value or is a callable... else: # Maybe it has a constructor that accepts a single value or is a callable...
@@ -424,7 +426,7 @@ class PasswordField(StringField):
# ************************ # ************************
class _FieldNameSetter(type): class _FieldNameSetter(abc.ABCMeta, type):
"""Simply adds the name of the field in the class to the field object""" """Simply adds the name of the field in the class to the field object"""
def __new__(mcs, name, bases, attrs): def __new__(mcs, name, bases, attrs):
@@ -450,7 +452,7 @@ class AutoSerializable(Serializable, metaclass=_FieldNameSetter):
def _autoserializable_fields(self) -> collections.abc.Iterator[tuple[str, _SerializableField]]: def _autoserializable_fields(self) -> collections.abc.Iterator[tuple[str, _SerializableField]]:
"""Returns an iterator over all fields in the class, including inherited ones """Returns an iterator over all fields in the class, including inherited ones
Returns: Returns:
Tuple(name, field) for each field in the class and its bases Tuple(name, field) for each field in the class and its bases
""" """
@@ -566,9 +568,9 @@ class AutoSerializable(Serializable, metaclass=_FieldNameSetter):
""" """
if not isinstance(other, AutoSerializable): if not isinstance(other, AutoSerializable):
return False return False
all_fields_attrs = list(self._autoserializable_fields()) all_fields_attrs = list(self._autoserializable_fields())
if {k for k, _ in all_fields_attrs} != {k for k, _ in other._autoserializable_fields()}: if {k for k, _ in all_fields_attrs} != {k for k, _ in other._autoserializable_fields()}:
return False return False

View File

@@ -38,9 +38,10 @@ import collections.abc
from django.utils.translation import gettext, gettext_noop as _ from django.utils.translation import gettext, gettext_noop as _
from uds.core.ui import UserInterface, UserInterfaceAbstract, gui from uds.core.ui import gui
from uds.core.reports import Report from uds.core.reports import Report
from uds import models from uds import models
from uds.core.ui.user_interface import UserInterfaceType
from . import fields from . import fields
@@ -61,7 +62,7 @@ reportAutoModelDct: collections.abc.Mapping[str, type[ReportAutoModel]] = { # t
} }
class ReportAutoType(UserInterfaceAbstract): class ReportAutoType(UserInterfaceType):
def __new__(mcs, name, bases, attrs) -> 'ReportAutoType': def __new__(mcs, name, bases, attrs) -> 'ReportAutoType':
# Add gui for elements... # Add gui for elements...
order = 1 order = 1
@@ -86,7 +87,7 @@ class ReportAutoType(UserInterfaceAbstract):
attrs['interval'] = fields.intervals_field(order) attrs['interval'] = fields.intervals_field(order)
order += 1 order += 1
return typing.cast('ReportAutoType', UserInterfaceAbstract.__new__(mcs, name, bases, attrs)) return typing.cast('ReportAutoType', super().__new__(mcs, name, bases, attrs))
# pylint: disable=abstract-method # pylint: disable=abstract-method
class ReportAuto(Report, metaclass=ReportAutoType): class ReportAuto(Report, metaclass=ReportAutoType):

View File

@@ -113,7 +113,7 @@ class OpenGnsysUserService(services.UserService, autoserializable.AutoSerializab
pub = super().publication() pub = super().publication()
if pub is None: if pub is None:
raise Exception('No publication for this element!') raise Exception('No publication for this element!')
return typing.cast('OGPublication', pub) return typing.cast('OpenGnsysPublication', pub)
def unmarshal(self, data: bytes) -> None: def unmarshal(self, data: bytes) -> None:
""" """

View File

@@ -36,44 +36,22 @@ import logging
import typing import typing
import collections.abc import collections.abc
from uds.core import services, consts from uds.core import services
from uds.core.managers.user_service import UserServiceManager from uds.core.services.expecializations.fixed_machine.fixed_userservice import FixedUserService, Operation
from uds.core.types.states import State from uds.core.types.states import State
from uds.core.util import log, autoserializable from uds.core.util import log, autoserializable
from uds.core.util.model import sql_stamp_seconds
from .jobs import ProxmoxDeferredRemoval
from . import client from . import client
# Not imported at runtime, just for type checking # Not imported at runtime, just for type checking
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from uds import models from uds import models
from . import service_fixed, publication from . import service_fixed
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Operation(enum.IntEnum): class ProxmoxFixedUserService(FixedUserService, autoserializable.AutoSerializable):
CREATE = 0
START = 1
STOP = 2
REMOVE = 3
WAIT = 4
ERROR = 5
FINISH = 6
RETRY = 7
UNKNOWN = 99
@staticmethod
def from_int(value: int) -> 'Operation':
try:
return Operation(value)
except ValueError:
return Operation.UNKNOWN
class ProxmoxFixedUserService(services.UserService, autoserializable.AutoSerializable):
""" """
This class generates the user consumable elements of the service tree. This class generates the user consumable elements of the service tree.
@@ -87,36 +65,6 @@ class ProxmoxFixedUserService(services.UserService, autoserializable.AutoSeriali
# : Recheck every ten seconds by default (for task methods) # : Recheck every ten seconds by default (for task methods)
suggested_delay = 4 suggested_delay = 4
_name = autoserializable.StringField(default='')
_mac = autoserializable.StringField(default='')
_vmid = autoserializable.IntegerField(default=0)
_reason = autoserializable.StringField(default='')
_task = autoserializable.StringField(default='')
_queue = autoserializable.ListField[Operation]() # Default is empty list
def _get_current_op(self) -> Operation:
if not self._queue:
return Operation.FINISH
return self._queue[0]
def _pop_current_op(self) -> Operation:
if not self._queue:
return Operation.FINISH
return self._queue.pop(0)
def _push_front_op(self, op: Operation) -> None:
self._queue.insert(0, op)
def _push_back_op(self, op) -> None:
self._queue.append(op)
def _retry_later(self) -> str:
self._push_front_op(Operation.RETRY)
return State.RUNNING
def _store_task(self, upid: 'client.types.UPID'): def _store_task(self, upid: 'client.types.UPID'):
self._task = '\t'.join([upid.node, upid.upid]) self._task = '\t'.join([upid.node, upid.upid])
@@ -124,48 +72,16 @@ class ProxmoxFixedUserService(services.UserService, autoserializable.AutoSeriali
vals = self._task.split('\t') vals = self._task.split('\t')
return (vals[0], vals[1]) return (vals[0], vals[1])
def _error(self, reason: typing.Union[str, Exception]) -> str:
"""
Internal method to set object as error state
Returns:
State.ERROR, so we can do "return self._error(reason)"
"""
reason = str(reason)
logger.debug('Setting error state, reason: %s', reason)
self.do_log(log.LogLevel.ERROR, reason)
self._queue = [Operation.ERROR]
self._reason = reason
return State.ERROR
# Utility overrides for type checking... # Utility overrides for type checking...
def service(self) -> 'service_fixed.ProxmoxFixedService': def service(self) -> 'service_fixed.ProxmoxFixedService':
return typing.cast('service_fixed.ProxmoxFixedService', super().service()) return typing.cast('service_fixed.ProxmoxFixedService', super().service())
def get_name(self) -> str:
return self._name
def set_ip(self, ip: str) -> None:
logger.debug('Setting IP to %s (ignored!!)', ip)
def get_unique_id(self) -> str:
return self._mac or self._name
def get_ip(self) -> str:
try:
if self._vmid:
return self.service().get_guest_ip_address(self._vmid)
except Exception:
pass
return ''
def set_ready(self) -> str: def set_ready(self) -> str:
if self.cache.get('ready') == '1': if self.cache.get('ready') == '1':
return State.FINISHED return State.FINISHED
try: try:
vminfo = self.service().get_machine_info(self._vmid) vminfo = self.service().get_machine_info(int(self._vmid))
except client.ProxmoxConnectionError: except client.ProxmoxConnectionError:
raise # If connection fails, let it fail on parent raise # If connection fails, let it fail on parent
except Exception as e: except Exception as e:
@@ -191,119 +107,9 @@ class ProxmoxFixedUserService(services.UserService, autoserializable.AutoSeriali
def process_ready_from_os_manager(self, data: typing.Any) -> str: def process_ready_from_os_manager(self, data: typing.Any) -> str:
return State.FINISHED return State.FINISHED
def deploy_for_user(self, user: 'models.User') -> str:
"""
Deploys an service instance for an user.
"""
logger.debug('Deploying for user')
self._init_queue_for_deploy(False)
return self._execute_queue()
def assign(self, vmid: int) -> str:
logger.debug('Assigning from VM {}'.format(vmid))
return self._create(vmid)
def error(self, reason: str) -> str: def error(self, reason: str) -> str:
return self._error(reason) return self._error(reason)
def _init_queue_for_deploy(self, forLevel2: bool = False) -> None:
self._queue = [Operation.CREATE, Operation.START, Operation.FINISH]
def _execute_queue(self) -> str:
self._debug('executeQueue')
op = self._get_current_op()
if op == Operation.ERROR:
return State.ERROR
if op == Operation.FINISH:
return State.FINISHED
fncs: collections.abc.Mapping[Operation, typing.Optional[collections.abc.Callable[[], str]]] = {
Operation.CREATE: self._create,
Operation.RETRY: self._retry,
Operation.START: self._start_machine,
Operation.STOP: self._stop_machine,
Operation.WAIT: self._wait,
Operation.REMOVE: self._remove,
}
try:
operation_runner: typing.Optional[collections.abc.Callable[[], str]] = fncs.get(op, None)
if not operation_runner:
return self._error(f'Unknown operation found at execution queue ({op})')
operation_runner()
return State.RUNNING
except Exception as e:
logger.exception('Unexpected VMware exception: %s', e)
return self._error(str(e))
# Queue execution methods
def _retry(self) -> str:
"""
Used to retry an operation
In fact, this will not be never invoked, unless we push it twice, because
check_state method will "pop" first item when a check operation returns State.FINISHED
At executeQueue this return value will be ignored, and it will only be used at check_state
"""
return State.FINISHED
def _wait(self) -> str:
"""
Executes opWait, it simply waits something "external" to end
"""
return State.RUNNING
def _create(self, vmid: int = 0) -> str:
"""
Deploys a machine from template for user/cache
"""
self._vmid = vmid or self.service().get_machine_from_pool()
self._mac = self.service().get_nic_mac(self._vmid)
self._name = self.service().get_machine_info(self._vmid).name or f'VM-{self._vmid}'
if self.service().use_snapshots.as_bool():
logger.debug('Using snapshots')
# If no snapshot exists for this vm, try to create one for it on background
# Lauch an snapshot. We will not wait for it to finish, but instead let it run "as is"
try:
if not self.service().parent().get_current_snapshot(self._vmid):
logger.debug('Not current snapshot')
self.service().parent().create_snapshot(
self._vmid,
name='UDS Snapshot',
)
except Exception as e:
self.do_log(log.LogLevel.WARNING, 'Could not create SNAPSHOT for this VM. ({})'.format(e))
# If not to be managed by a token, "autologin" user
if not self.service().get_token():
userService = self.db_obj()
if userService:
userService.set_in_use(True)
return State.RUNNING
def _remove(self) -> str:
"""
Removes the snapshot if needed and releases the machine again
"""
try:
if self.service().use_snapshots.as_bool():
# try to revert to snapshot
snapshot = self.service().parent().get_current_snapshot(self._vmid)
if snapshot:
self._store_task(self.service().parent().restore_snapshot(self._vmid, name=snapshot.name))
except Exception as e:
self.do_log(log.LogLevel.WARNING, 'Could not restore SNAPSHOT for this VM. ({})'.format(e))
self.service().release_machine_from_pool(self._vmid)
return State.RUNNING
def _start_machine(self) -> str: def _start_machine(self) -> str:
try: try:
vminfo = self.service().get_machine_info(int(self._vmid)) vminfo = self.service().get_machine_info(int(self._vmid))
@@ -373,106 +179,3 @@ class ProxmoxFixedUserService(services.UserService, autoserializable.AutoSeriali
Checks if a machine has been removed Checks if a machine has been removed
""" """
return self._check_task_finished() return self._check_task_finished()
def check_state(self) -> str:
"""
Check what operation is going on, and acts acordly to it
"""
self._debug('check_state')
op = self._get_current_op()
if op == Operation.ERROR:
return State.ERROR
if op == Operation.FINISH:
return State.FINISHED
fncs: collections.abc.Mapping[Operation, typing.Optional[collections.abc.Callable[[], str]]] = {
Operation.CREATE: self._create_checker,
Operation.RETRY: self._retry,
Operation.WAIT: self._wait,
Operation.START: self._start_checker,
Operation.STOP: self._stop_checker,
Operation.REMOVE: self._removed_checker,
}
try:
check_function: typing.Optional[collections.abc.Callable[[], str]] = fncs.get(op, None)
if check_function is None:
return self._error('Unknown operation found at check queue ({0})'.format(op))
state = check_function()
if state == State.FINISHED:
self._pop_current_op() # Remove runing op, till now only was "peek"
return self._execute_queue()
return state
except Exception as e:
logger.exception('Unexpected VMware exception: %s', e)
return self._error(str(e))
def finish(self) -> None:
"""
Invoked when the core notices that the deployment of a service has finished.
(No matter wether it is for cache or for an user)
"""
logger.debug('Finished machine %s', self._name)
def error_reason(self) -> str:
"""
Returns the reason of the error.
Remember that the class is responsible of returning this whenever asked
for it, and it will be asked everytime it's needed to be shown to the
user (when the administation asks for it).
"""
return self._reason
def destroy(self) -> str:
"""
Invoked for destroying a deployed service
"""
self._queue = [Operation.REMOVE, Operation.FINISH]
return self._execute_queue()
def cancel(self) -> str:
"""
This is a task method. As that, the excepted return values are
State values RUNNING, FINISHED or ERROR.
This can be invoked directly by an administration or by the clean up
of the deployed service (indirectly).
When administrator requests it, the cancel is "delayed" and not
invoked directly.
"""
logger.debug('Canceling %s with taskId=%s, vmId=%s', self._name, self._task, self._vmid)
return self.destroy()
@staticmethod
def _op2str(op: Operation) -> str:
return {
Operation.CREATE: 'create',
Operation.START: 'start',
Operation.STOP: 'stop',
Operation.REMOVE: 'remove',
Operation.WAIT: 'wait',
Operation.ERROR: 'error',
Operation.FINISH: 'finish',
Operation.RETRY: 'retry',
}.get(op, '????')
def _debug(self, txt: str) -> None:
# logger.debug('_name {0}: {1}'.format(txt, self._name))
# logger.debug('_ip {0}: {1}'.format(txt, self._ip))
# logger.debug('_mac {0}: {1}'.format(txt, self._mac))
# logger.debug('_vmId {0}: {1}'.format(txt, self._vmId))
logger.debug(
'Queue at %s for %s: %s, mac:%s, vmId:%s, task:%s',
txt,
self._name,
[ProxmoxFixedUserService._op2str(op) for op in self._queue],
self._mac,
self._vmid,
self._task,
)

View File

@@ -29,15 +29,14 @@
Author: Adolfo Gómez, dkmaster at dkmon dot com Author: Adolfo Gómez, dkmaster at dkmon dot com
""" """
import logging import logging
import re
import typing import typing
import collections.abc
from django.utils.translation import gettext_noop as _, gettext from django.utils.translation import gettext_noop as _, gettext
from uds.core import services, types, consts, exceptions from uds.core import services, types, consts, exceptions
from uds.core.services.expecializations.fixed_machine.fixed_service import FixedService
from uds.core.services.expecializations.fixed_machine.fixed_userservice import FixedUserService
from uds.core.ui import gui from uds.core.ui import gui
from uds.core.util import validators, log from uds.core.util import validators, log
from uds.core.util.cache import Cache
from uds.core.util.decorators import cached from uds.core.util.decorators import cached
from uds.core.workers import initialize from uds.core.workers import initialize
@@ -55,7 +54,7 @@ if typing.TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ProxmoxFixedService(services.Service): # pylint: disable=too-many-public-methods class ProxmoxFixedService(FixedService): # pylint: disable=too-many-public-methods
""" """
Proxmox fixed machines service. Proxmox fixed machines service.
""" """
@@ -65,10 +64,6 @@ class ProxmoxFixedService(services.Service): # pylint: disable=too-many-public-
type_description = _('Proxmox Services based on fixed machines. Needs qemu agent installed on machines.') type_description = _('Proxmox Services based on fixed machines. Needs qemu agent installed on machines.')
icon_file = 'service.png' icon_file = 'service.png'
uses_cache = False # Cache are running machine awaiting to be assigned
uses_cache_l2 = False # L2 Cache are running machines in suspended state
needs_osmanager = False # If the service needs a s.o. manager (managers are related to agents provided by services, i.e. virtual machines with agent)
must_assign_manually = False # If true, the system can't do an automatic assignation of a deployed user service from this service
can_reset = True can_reset = True
# : Types of publications (preparated data for deploys) # : Types of publications (preparated data for deploys)
@@ -81,17 +76,7 @@ class ProxmoxFixedService(services.Service): # pylint: disable=too-many-public-
services_type_provided = types.services.ServiceType.VDI services_type_provided = types.services.ServiceType.VDI
# Gui # Gui
token = gui.TextField( token = FixedService.token
order=1,
label=_('Service Token'),
length=16,
tooltip=_(
'Service token that will be used by actors to communicate with service. Leave empty for persistent assignation.'
),
default='',
required=False,
readonly=False,
)
pool = gui.ChoiceField( pool = gui.ChoiceField(
label=_("Resource Pool"), label=_("Resource Pool"),
@@ -128,15 +113,6 @@ class ProxmoxFixedService(services.Service): # pylint: disable=too-many-public-
prov_uuid = gui.HiddenField(value=None) prov_uuid = gui.HiddenField(value=None)
def _get_assigned_machines(self) -> typing.Set[int]:
vals = self.storage.get_unpickle('vms')
logger.debug('Got storage VMS: %s', vals)
return vals or set()
def _save_assigned_machines(self, vals: typing.Set[int]) -> None:
logger.debug('Saving storage VMS: %s', vals)
self.storage.put_pickle('vms', vals)
def initialize(self, values: 'Module.ValuesType') -> None: def initialize(self, values: 'Module.ValuesType') -> None:
""" """
Loads the assigned machines from storage Loads the assigned machines from storage
@@ -145,12 +121,12 @@ class ProxmoxFixedService(services.Service): # pylint: disable=too-many-public-
if not self.machines.value: if not self.machines.value:
raise exceptions.ui.ValidationError(gettext('We need at least a machine')) raise exceptions.ui.ValidationError(gettext('We need at least a machine'))
self.storage.put_pickle('maxDeployed', len(self.machines.as_list())) self.storage.put_pickle('userservices_limit', len(self.machines.as_list()))
# Remove machines not in values from "assigned" set # Remove machines not in values from "assigned" set
self._save_assigned_machines(self._get_assigned_machines() & set(self.machines.as_list())) self._save_assigned_machines(self._get_assigned_machines() & set(self.machines.as_list()))
self.token.value = self.token.value.strip() self.token.value = self.token.value.strip()
self.userservices_limit = self.storage.get_unpickle('maxDeployed') self.userservices_limit = self.storage.get_unpickle('userservices_limit')
def init_gui(self) -> None: def init_gui(self) -> None:
# Here we have to use "default values", cause values aren't used at form initialization # Here we have to use "default values", cause values aren't used at form initialization
@@ -166,22 +142,9 @@ class ProxmoxFixedService(services.Service): # pylint: disable=too-many-public-
def parent(self) -> 'ProxmoxProvider': def parent(self) -> 'ProxmoxProvider':
return typing.cast('ProxmoxProvider', super().parent()) return typing.cast('ProxmoxProvider', super().parent())
def sanitized_name(self, name: str) -> str:
"""
Proxmox only allows machine names with [a-zA-Z0-9_-]
"""
return re.sub("[^a-zA-Z0-9_-]", "-", name)
def get_machine_info(self, vmId: int) -> 'client.types.VMInfo': def get_machine_info(self, vmId: int) -> 'client.types.VMInfo':
return self.parent().get_machine_info(vmId, self.pool.value.strip()) return self.parent().get_machine_info(vmId, self.pool.value.strip())
def get_nic_mac(self, vmid: int) -> str:
config = self.parent().get_machine_configuration(vmid)
return config.networks[0].mac.lower()
def get_guest_ip_address(self, vmid: int) -> str:
return self.parent().get_guest_ip_address(vmid)
def get_task_info(self, node: str, upid: str) -> 'client.types.TaskStatus': def get_task_info(self, node: str, upid: str) -> 'client.types.TaskStatus':
return self.parent().get_task_info(node, upid) return self.parent().get_task_info(node, upid)
@@ -200,7 +163,64 @@ class ProxmoxFixedService(services.Service): # pylint: disable=too-many-public-
def shutdown_machine(self, vmId: int) -> 'client.types.UPID': def shutdown_machine(self, vmId: int) -> 'client.types.UPID':
return self.parent().shutdown_machine(vmId) return self.parent().shutdown_machine(vmId)
def get_machine_from_pool(self) -> int: def enumerate_assignables(self) -> list[tuple[str, str]]:
# Obtain machines names and ids for asignables
vms: dict[int, str] = {}
for member in self.parent().get_pool_info(self.pool.value.strip(), retrieve_vm_names=True).members:
vms[member.vmid] = member.vmname
assigned_vms = self._get_assigned_machines()
return [(k, vms.get(int(k), 'Unknown!')) for k in self.machines.as_list() if int(k) not in assigned_vms]
def assign_from_assignables(
self, assignable_id: str, user: 'models.User', user_deployment: 'services.UserService'
) -> str:
userservice_instance = typing.cast(ProxmoxFixedUserService, user_deployment)
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 userservice_instance.assign(assignable_id)
return userservice_instance.error('VM not available!')
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT)
def is_avaliable(self) -> bool:
return self.parent().is_available()
def process_snapshot(self, remove: bool, userservice_instace: FixedUserService) -> str:
userservice_instace = typing.cast(ProxmoxFixedUserService, userservice_instace)
if self.use_snapshots.as_bool():
vmid = int(userservice_instace._vmid)
if remove:
try:
# try to revert to snapshot
snapshot = self.parent().get_current_snapshot(vmid)
if snapshot:
userservice_instace._store_task(
self.parent().restore_snapshot(vmid, name=snapshot.name)
)
except Exception as e:
self.do_log(log.LogLevel.WARNING, 'Could not restore SNAPSHOT for this VM. ({})'.format(e))
else:
logger.debug('Using snapshots')
# If no snapshot exists for this vm, try to create one for it on background
# Lauch an snapshot. We will not wait for it to finish, but instead let it run "as is"
try:
if not self.parent().get_current_snapshot(vmid):
logger.debug('Not current snapshot')
self.parent().create_snapshot(
vmid,
name='UDS Snapshot',
)
except Exception as e:
self.do_log(log.LogLevel.WARNING, 'Could not create SNAPSHOT for this VM. ({})'.format(e))
return types.states.State.RUNNING
def get_and_assign_machine(self) -> str:
found_vmid: typing.Optional[int] = None found_vmid: typing.Optional[int] = None
try: try:
assigned_vms = self._get_assigned_machines() assigned_vms = self._get_assigned_machines()
@@ -222,7 +242,7 @@ class ProxmoxFixedService(services.Service): # pylint: disable=too-many-public-
) )
if found_vmid: if found_vmid:
assigned_vms.add(found_vmid) assigned_vms.add(str(found_vmid))
self._save_assigned_machines(assigned_vms) self._save_assigned_machines(assigned_vms)
except Exception: # except Exception: #
raise Exception('No machine available') raise Exception('No machine available')
@@ -230,38 +250,22 @@ class ProxmoxFixedService(services.Service): # pylint: disable=too-many-public-
if not found_vmid: if not found_vmid:
raise Exception('All machines from list already assigned.') raise Exception('All machines from list already assigned.')
return found_vmid return str(found_vmid)
def get_first_network_mac(self, vmid: str) -> str:
config = self.parent().get_machine_configuration(int(vmid))
return config.networks[0].mac.lower()
def release_machine_from_pool(self, vmid: int) -> None:
def get_guest_ip_address(self, vmid: str) -> str:
return self.parent().get_guest_ip_address(int(vmid))
def get_machine_name(self, vmid: str) -> str:
return self.parent().get_machine_info(int(vmid)).name or ''
def remove_and_free_machine(self, vmid: str) -> None:
try: try:
self._save_assigned_machines(self._get_assigned_machines() - {vmid}) # Remove from assigned self._save_assigned_machines(self._get_assigned_machines() - {str(vmid)}) # Remove from assigned
except Exception as e: except Exception as e:
logger.warn('Cound not save assigned machines on vmware fixed pool: %s', e) logger.warn('Cound not save assigned machines on fixed pool: %s', e)
def enumerate_assignables(self) -> list[tuple[str, str]]:
# Obtain machines names and ids for asignables
vms: dict[int, str] = {}
for member in self.parent().get_pool_info(self.pool.value.strip(), retrieve_vm_names=True).members:
vms[member.vmid] = member.vmname
assigned_vms = self._get_assigned_machines()
return [
(k, vms.get(int(k), 'Unknown!')) for k in self.machines.as_list() if int(k) not in assigned_vms
]
def assign_from_assignables(
self, assignable_id: str, user: 'models.User', user_deployment: 'services.UserService'
) -> str:
userservice_instance = typing.cast(ProxmoxFixedUserService, user_deployment)
assignedVmsSet = self._get_assigned_machines()
if assignable_id not in assignedVmsSet:
assignedVmsSet.add(int(assignable_id))
self._save_assigned_machines(assignedVmsSet)
return userservice_instance.assign(int(assignable_id))
return userservice_instance.error('VM not available!')
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT)
def is_avaliable(self) -> bool:
return self.parent().is_available()

View File

@@ -1,113 +0,0 @@
# -*- 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 pickle
import typing
from tests.utils.test import UDSTestCase
from uds.core.environment import Environment
from uds.core.util import autoserializable
from uds.services.Proxmox.publication import ProxmoxPublication as Publication
# if not data.startswith(b'v'):
# return super().unmarshal(data)
# logger.debug('Data: %s', data)
# vals = data.decode('utf8').split('\t')
# if vals[0] == 'v1':
# (
# self._name,
# self._vm,
# self._task,
# self._state,
# self._operation,
# destroy_after,
# self._reason,
# ) = vals[1:]
# else:
# raise ValueError('Invalid data format')
# self._destroy_after = destroy_after != ''
EXPECTED_FIELDS: typing.Final[set[str]] = {
'_name',
'_vm',
'_task',
'_state',
'_operation',
'_destroy_after',
'_reason',
}
SERIALIZED_PUBLICATION_DATA: typing.Final[bytes] = b'v1\tname\tvm\ttask\tstate\toperation\ty\treason'
class ProxmoxPublicationSerializationTest(UDSTestCase):
def check(self, instance: Publication) -> None:
self.assertEqual(instance._name, 'name')
self.assertEqual(instance._vm, 'vm')
self.assertEqual(instance._task, 'task')
self.assertEqual(instance._state, 'state')
self.assertEqual(instance._operation, 'operation')
self.assertTrue(instance._destroy_after)
self.assertEqual(instance._reason, 'reason')
def test_marshaling(self) -> None:
environment = Environment.testing_environment()
instance = Publication(environment=environment, service=None)
instance.unmarshal(SERIALIZED_PUBLICATION_DATA)
self.check(instance)
# Ensure remarshalled flag is set
self.assertTrue(instance.needs_upgrade())
instance.mark_for_upgrade(False) # reset flag
marshaled_data = instance.marshal()
# Ensure fields has been marshalled using new format
self.assertFalse(marshaled_data.startswith(b'\1'))
# Reunmarshall again and check that remarshalled flag is not set
instance = Publication(environment=environment, service=None)
instance.unmarshal(marshaled_data)
self.assertFalse(instance.needs_upgrade())
# Check that data is correct
self.check(instance)
def test_autoserialization_fields(self) -> None:
# 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 = Publication(environment=env, service=None)
self.assertSetEqual(set(f[0] for f in instance._autoserializable_fields()), EXPECTED_FIELDS)