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:
@@ -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')
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
34
server/src/uds/core/services/expecializations/__init__.py
Normal file
34
server/src/uds/core/services/expecializations/__init__.py
Normal 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
|
||||||
|
"""
|
||||||
@@ -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
|
||||||
|
"""
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user