mirror of
https://github.com/dkmstr/openuds.git
synced 2025-01-21 18:03:54 +03:00
* updated admin
* Added Xen Fixed Services * Fixed specializations folders name
This commit is contained in:
parent
e921c76530
commit
34005c2af3
@ -76,6 +76,7 @@ class FixedUserService(services.UserService, autoserializable.AutoSerializable,
|
||||
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='')
|
||||
@ -84,8 +85,12 @@ class FixedUserService(services.UserService, autoserializable.AutoSerializable,
|
||||
_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]
|
||||
|
||||
_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
|
||||
@ -126,6 +131,14 @@ class FixedUserService(services.UserService, autoserializable.AutoSerializable,
|
||||
logger.debug('Setting error state, reason: %s', reason)
|
||||
self.do_log(log.LogLevel.ERROR, reason)
|
||||
|
||||
if self._vmid:
|
||||
try:
|
||||
self.service().remove_and_free_machine(self._vmid)
|
||||
self.service().process_snapshot(remove=True, userservice_instace=self)
|
||||
self._vmid = ''
|
||||
except Exception as e:
|
||||
logger.exception('Exception removing machine: %s', e)
|
||||
|
||||
self._queue = [Operation.ERROR]
|
||||
self._reason = reason
|
||||
return State.ERROR
|
||||
@ -236,7 +249,7 @@ class FixedUserService(services.UserService, autoserializable.AutoSerializable,
|
||||
|
||||
# Try to process snaptshots if needed
|
||||
state = self.service().process_snapshot(remove=False, userservice_instace=self)
|
||||
|
||||
|
||||
if state == State.ERROR:
|
||||
return state
|
||||
|
||||
@ -247,7 +260,7 @@ class FixedUserService(services.UserService, autoserializable.AutoSerializable,
|
||||
userService.set_in_use(True)
|
||||
|
||||
return state
|
||||
|
||||
|
||||
@typing.final
|
||||
def _remove(self) -> str:
|
||||
"""
|
@ -37,7 +37,7 @@ import typing
|
||||
import collections.abc
|
||||
|
||||
from uds.core import services
|
||||
from uds.core.services.expecializations.fixed_machine.fixed_userservice import FixedUserService, Operation
|
||||
from uds.core.services.specializations.fixed_machine.fixed_userservice import FixedUserService, Operation
|
||||
from uds.core.types.states import State
|
||||
from uds.core.util import log, autoserializable
|
||||
|
||||
@ -65,7 +65,7 @@ class ProxmoxFixedUserService(FixedUserService, autoserializable.AutoSerializabl
|
||||
|
||||
# : Recheck every ten seconds by default (for task methods)
|
||||
suggested_delay = 4
|
||||
def _store_task(self, upid: 'client.types.UPID'):
|
||||
def _store_task(self, upid: 'client.types.UPID') -> None:
|
||||
self._task = '\t'.join([upid.node, upid.upid])
|
||||
|
||||
def _retrieve_task(self) -> tuple[str, str]:
|
||||
|
@ -33,8 +33,8 @@ import typing
|
||||
|
||||
from django.utils.translation import gettext_noop as _, gettext
|
||||
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.services.specializations.fixed_machine.fixed_service import FixedService
|
||||
from uds.core.services.specializations.fixed_machine.fixed_userservice import FixedUserService
|
||||
from uds.core.ui import gui
|
||||
from uds.core.util import validators, log
|
||||
from uds.core.util.decorators import cached
|
||||
@ -157,12 +157,13 @@ class ProxmoxFixedService(FixedService): # pylint: disable=too-many-public-meth
|
||||
def reset_machine(self, vmId: int) -> 'client.types.UPID':
|
||||
return self.parent().reset_machine(vmId)
|
||||
|
||||
def suspend_machine(self, vmId: int) -> 'client.types.UPID':
|
||||
return self.parent().suspend_machine(vmId)
|
||||
|
||||
def shutdown_machine(self, vmId: int) -> 'client.types.UPID':
|
||||
return self.parent().shutdown_machine(vmId)
|
||||
|
||||
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT)
|
||||
def is_avaliable(self) -> bool:
|
||||
return self.parent().is_available()
|
||||
|
||||
def enumerate_assignables(self) -> list[tuple[str, str]]:
|
||||
# Obtain machines names and ids for asignables
|
||||
vms: dict[int, str] = {}
|
||||
@ -185,10 +186,6 @@ class ProxmoxFixedService(FixedService): # pylint: disable=too-many-public-meth
|
||||
|
||||
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():
|
||||
|
@ -147,11 +147,11 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
return State.FINISHED
|
||||
|
||||
try:
|
||||
state = self.service().getVMPowerState(self._vmid)
|
||||
state = self.service().get_machine_power_state(self._vmid)
|
||||
|
||||
if state != XenPowerState.running:
|
||||
self._queue = [Operation.START, Operation.FINISH]
|
||||
return self._execute_from_queue()
|
||||
return self._execute_queue()
|
||||
|
||||
self.cache.put('ready', '1', 30)
|
||||
except Exception as e:
|
||||
@ -163,7 +163,7 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
|
||||
def reset(self) -> None:
|
||||
if self._vmid:
|
||||
self.service().resetVM(self._vmid) # Reset in sync
|
||||
self.service().reset_machine(self._vmid) # Reset in sync
|
||||
|
||||
def process_ready_from_os_manager(self, data: typing.Any) -> str:
|
||||
# Here we will check for suspending the VM (when full ready)
|
||||
@ -171,7 +171,7 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
if self._get_current_op() == Operation.WAIT:
|
||||
logger.debug('Machine is ready. Moving to level 2')
|
||||
self._pop_current_op() # Remove current state
|
||||
return self._execute_from_queue()
|
||||
return self._execute_queue()
|
||||
# Do not need to go to level 2 (opWait is in fact "waiting for moving machine to cache level 2)
|
||||
return State.FINISHED
|
||||
|
||||
@ -181,14 +181,14 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
"""
|
||||
logger.debug('Deploying for user')
|
||||
self._init_queue_for_deployment(False)
|
||||
return self._execute_from_queue()
|
||||
return self._execute_queue()
|
||||
|
||||
def deploy_for_cache(self, cacheLevel: int) -> str:
|
||||
"""
|
||||
Deploys an service instance for cache
|
||||
"""
|
||||
self._init_queue_for_deployment(cacheLevel == self.L2_CACHE)
|
||||
return self._execute_from_queue()
|
||||
return self._execute_queue()
|
||||
|
||||
def _init_queue_for_deployment(self, forLevel2: bool = False) -> None:
|
||||
if forLevel2 is False:
|
||||
@ -235,14 +235,14 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
|
||||
if self._vmid != '': # Powers off and delete VM
|
||||
try:
|
||||
state = self.service().getVMPowerState(self._vmid)
|
||||
state = self.service().get_machine_power_state(self._vmid)
|
||||
if state in (
|
||||
XenPowerState.running,
|
||||
XenPowerState.paused,
|
||||
XenPowerState.suspended,
|
||||
):
|
||||
self.service().stopVM(self._vmid, False) # In sync mode
|
||||
self.service().removeVM(self._vmid)
|
||||
self.service().stop_machine(self._vmid, False) # In sync mode
|
||||
self.service().remove_machine(self._vmid)
|
||||
except Exception:
|
||||
logger.debug('Can\'t set machine %s state to stopped', self._vmid)
|
||||
|
||||
@ -250,7 +250,7 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
self._reason = str(reason)
|
||||
return State.ERROR
|
||||
|
||||
def _execute_from_queue(self) -> str:
|
||||
def _execute_queue(self) -> str:
|
||||
self.__debug('executeQueue')
|
||||
op = self._get_current_op()
|
||||
|
||||
@ -313,12 +313,12 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
'No more names available for this service. (Increase digits for this service to fix)'
|
||||
)
|
||||
|
||||
name = 'UDS service ' + self.service().sanitizeVmName(
|
||||
name = 'UDS service ' + self.service().sanitized_name(
|
||||
name
|
||||
) # oVirt don't let us to create machines with more than 15 chars!!!
|
||||
comments = 'UDS Linked clone'
|
||||
|
||||
self._task = self.service().startDeployFromTemplate(name, comments, templateId)
|
||||
self._task = self.service().start_deploy_from_template(name, comments, templateId)
|
||||
if self._task is None:
|
||||
raise Exception('Can\'t create machine')
|
||||
|
||||
@ -328,13 +328,13 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
"""
|
||||
Removes a machine from system
|
||||
"""
|
||||
state = self.service().getVMPowerState(self._vmid)
|
||||
state = self.service().get_machine_power_state(self._vmid)
|
||||
|
||||
if state not in (XenPowerState.halted, XenPowerState.suspended):
|
||||
self._push_front_op(Operation.STOP)
|
||||
self._execute_from_queue()
|
||||
self._execute_queue()
|
||||
else:
|
||||
self.service().removeVM(self._vmid)
|
||||
self.service().remove_machine(self._vmid)
|
||||
|
||||
return State.RUNNING
|
||||
|
||||
@ -342,7 +342,7 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
"""
|
||||
Powers on the machine
|
||||
"""
|
||||
task = self.service().startVM(self._vmid)
|
||||
task = self.service().start_machine(self._vmid)
|
||||
|
||||
if task is not None:
|
||||
self._task = task
|
||||
@ -355,7 +355,7 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
"""
|
||||
Powers off the machine
|
||||
"""
|
||||
task = self.service().stopVM(self._vmid)
|
||||
task = self.service().stop_machine(self._vmid)
|
||||
|
||||
if task is not None:
|
||||
self._task = task
|
||||
@ -388,7 +388,7 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
"""
|
||||
Provisions machine & changes the mac of the indicated nic
|
||||
"""
|
||||
self.service().configureVM(self._vmid, self.get_unique_id())
|
||||
self.service().configure_machine(self._vmid, self.get_unique_id())
|
||||
|
||||
return State.RUNNING
|
||||
|
||||
@ -396,7 +396,7 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
"""
|
||||
Makes machine usable on Xen
|
||||
"""
|
||||
self.service().provisionVM(self._vmid, False) # Let's try this in "sync" mode, this must be fast enough
|
||||
self.service().provision_machine(self._vmid, False) # Let's try this in "sync" mode, this must be fast enough
|
||||
|
||||
return State.RUNNING
|
||||
|
||||
@ -494,7 +494,7 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
state = chkFnc()
|
||||
if state == State.FINISHED:
|
||||
self._pop_current_op() # Remove runing op
|
||||
return self._execute_from_queue()
|
||||
return self._execute_queue()
|
||||
|
||||
return state
|
||||
except Exception as e:
|
||||
@ -512,7 +512,7 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
else:
|
||||
self._queue = [Operation.START, Operation.SUSPEND, Operation.FINISH]
|
||||
|
||||
return self._execute_from_queue()
|
||||
return self._execute_queue()
|
||||
|
||||
def error_reason(self) -> str:
|
||||
return self._reason
|
||||
@ -528,7 +528,7 @@ class XenLinkedDeployment(services.UserService, autoserializable.AutoSerializabl
|
||||
|
||||
if op == Operation.FINISH or op == Operation.WAIT:
|
||||
self._queue = [Operation.STOP, Operation.REMOVE, Operation.FINISH]
|
||||
return self._execute_from_queue()
|
||||
return self._execute_queue()
|
||||
|
||||
self._queue = [op, Operation.STOP, Operation.REMOVE, Operation.FINISH]
|
||||
# Do not execute anything.here, just continue normally
|
||||
|
165
server/src/uds/services/Xen/deployment_fixed.py
Normal file
165
server/src/uds/services/Xen/deployment_fixed.py
Normal file
@ -0,0 +1,165 @@
|
||||
# -*- 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 pickle # nosec: controled data
|
||||
import enum
|
||||
import logging
|
||||
import typing
|
||||
import collections.abc
|
||||
|
||||
from uds.core import services
|
||||
from uds.core.services.specializations.fixed_machine.fixed_userservice import FixedUserService, Operation
|
||||
from uds.core.types.states import State
|
||||
from uds.core.util import log, autoserializable
|
||||
|
||||
from . import xen_client
|
||||
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from . import service_fixed
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XenFixedUserService(FixedUserService, autoserializable.AutoSerializable):
|
||||
"""
|
||||
This class generates the user consumable elements of the service tree.
|
||||
|
||||
After creating at administration interface an Deployed Service, UDS will
|
||||
create consumable services for users using UserDeployment class as
|
||||
provider of this elements.
|
||||
|
||||
The logic for managing vmware deployments (user machines in this case) is here.
|
||||
|
||||
"""
|
||||
|
||||
# : Recheck every ten seconds by default (for task methods)
|
||||
suggested_delay = 4
|
||||
# Utility overrides for type checking...
|
||||
def service(self) -> 'service_fixed.XenFixedService':
|
||||
return typing.cast('service_fixed.XenFixedService', super().service())
|
||||
|
||||
def set_ready(self) -> str:
|
||||
if self.cache.get('ready') == '1':
|
||||
return State.FINISHED
|
||||
|
||||
try:
|
||||
state = self.service().get_machine_power_state(self._vmid)
|
||||
|
||||
if state != xen_client.XenPowerState.running:
|
||||
self._queue = [Operation.START, Operation.FINISH]
|
||||
return self._execute_queue()
|
||||
|
||||
self.cache.put('ready', '1', 30)
|
||||
except Exception as e:
|
||||
# On case of exception, log an an error and return as if the operation was executed
|
||||
self.do_log(log.LogLevel.ERROR, 'Error setting machine state: {}'.format(e))
|
||||
# return self.__error('Machine is not available anymore')
|
||||
|
||||
return State.FINISHED
|
||||
|
||||
def reset(self) -> None:
|
||||
if self._vmid:
|
||||
self.service().reset_machine(self._vmid) # Reset in sync
|
||||
|
||||
def process_ready_from_os_manager(self, data: typing.Any) -> str:
|
||||
return State.FINISHED
|
||||
|
||||
def error(self, reason: str) -> str:
|
||||
return self._error(reason)
|
||||
|
||||
def _start_machine(self) -> str:
|
||||
try:
|
||||
state = self.service().get_machine_power_state(self._vmid)
|
||||
except Exception as e:
|
||||
raise Exception('Machine not found on start machine') from e
|
||||
|
||||
if state != xen_client.XenPowerState.running:
|
||||
self._task = self.service().start_machine(self._vmid) or ''
|
||||
|
||||
return State.RUNNING
|
||||
|
||||
def _stop_machine(self) -> str:
|
||||
try:
|
||||
state = self.service().get_machine_power_state(self._vmid)
|
||||
except Exception as e:
|
||||
raise Exception('Machine not found on stop machine') from e
|
||||
|
||||
if state == xen_client.XenPowerState.running:
|
||||
logger.debug('Stopping machine %s', self._vmid)
|
||||
self._task = self.service().stop_machine(self._vmid) or ''
|
||||
|
||||
return State.RUNNING
|
||||
|
||||
# Check methods
|
||||
def _check_task_finished(self) -> str:
|
||||
if self._task == '':
|
||||
return State.FINISHED
|
||||
|
||||
try:
|
||||
finished, per = self.service().check_task_finished(self._task)
|
||||
except xen_client.XenFailure:
|
||||
return State.RUNNING # Try again later
|
||||
except Exception as e: # Failed for some other reason
|
||||
if isinstance(e.args[0], dict) and 'error_connection' in e.args[0]:
|
||||
return State.RUNNING # Try again later
|
||||
raise e
|
||||
|
||||
if finished:
|
||||
return State.FINISHED
|
||||
|
||||
return State.RUNNING
|
||||
|
||||
# Check methods
|
||||
def _create_checker(self) -> str:
|
||||
"""
|
||||
Checks the state of a deploy for an user or cache
|
||||
"""
|
||||
return State.FINISHED
|
||||
|
||||
def _start_checker(self) -> str:
|
||||
"""
|
||||
Checks if machine has started
|
||||
"""
|
||||
return self._check_task_finished()
|
||||
|
||||
def _stop_checker(self) -> str:
|
||||
"""
|
||||
Checks if machine has stoped
|
||||
"""
|
||||
return self._check_task_finished()
|
||||
|
||||
def _removed_checker(self) -> str:
|
||||
"""
|
||||
Checks if a machine has been removed
|
||||
"""
|
||||
return self._check_task_finished()
|
64
server/src/uds/services/Xen/helpers.py
Normal file
64
server/src/uds/services/Xen/helpers.py
Normal file
@ -0,0 +1,64 @@
|
||||
#
|
||||
# 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 collections.abc
|
||||
import logging
|
||||
from multiprocessing import pool
|
||||
import typing
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from uds.core import types
|
||||
from uds.core.environment import Environment
|
||||
from uds.core.ui.user_interface import gui
|
||||
from uds import models
|
||||
from uds.services.OpenNebula.on import vm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_machines(parameters: typing.Any) -> types.ui.CallbackResultType:
|
||||
from .provider import XenProvider # pylint: disable=import-outside-toplevel
|
||||
|
||||
logger.debug('Parameters received by getResources Helper: %s', parameters)
|
||||
provider = typing.cast(
|
||||
XenProvider, models.Provider.objects.get(uuid=parameters['prov_uuid']).get_instance()
|
||||
)
|
||||
|
||||
try:
|
||||
machines = [m for m in provider.get_machines_from_folder(parameters['folder'], retrieve_names=True) if not m.get('name', '').startswith('UDS')]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
return [
|
||||
{
|
||||
'name': 'machines',
|
||||
'choices': [gui.choice_item(machine['id'], machine['name']) for machine in machines],
|
||||
}
|
||||
]
|
@ -39,8 +39,10 @@ from uds.core.services import ServiceProvider
|
||||
from uds.core.ui import gui
|
||||
from uds.core.util.cache import Cache
|
||||
from uds.core.util.decorators import cached
|
||||
from uds.core.util import fields
|
||||
|
||||
from .service import XenLinkedService
|
||||
from .service_fixed import XenFixedService
|
||||
from .xen_client import XenServer
|
||||
|
||||
# from uds.core.util import validators
|
||||
@ -76,7 +78,7 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
|
||||
# : What kind of services we offer, this are classes inherited from Service
|
||||
offers = [XenLinkedService]
|
||||
offers = [XenLinkedService, XenFixedService]
|
||||
# : Name to show the administrator. This string will be translated BEFORE
|
||||
# : sending it to administration interface, so don't forget to
|
||||
# : mark it as _ (using gettext_noop)
|
||||
@ -121,61 +123,20 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
tooltip=_('Password of the user of XenServer'),
|
||||
required=True,
|
||||
)
|
||||
concurrent_creation_limit = fields.concurrent_creation_limit_field()
|
||||
concurrent_removal_limit = fields.concurrent_removal_limit_field()
|
||||
|
||||
concurrent_creation_limit = gui.NumericField(
|
||||
length=3,
|
||||
label=_('Creation concurrency'),
|
||||
default=10,
|
||||
min_value=1,
|
||||
max_value=65536,
|
||||
order=50,
|
||||
tooltip=_('Maximum number of concurrently creating VMs'),
|
||||
required=True,
|
||||
tab=types.ui.Tab.ADVANCED,
|
||||
old_field_name='maxPreparingServices',
|
||||
)
|
||||
concurrent_removal_limit = gui.NumericField(
|
||||
length=3,
|
||||
label=_('Removal concurrency'),
|
||||
default=5,
|
||||
min_value=1,
|
||||
max_value=65536,
|
||||
order=51,
|
||||
tooltip=_('Maximum number of concurrently removing VMs'),
|
||||
required=True,
|
||||
tab=types.ui.Tab.ADVANCED,
|
||||
old_field_name='maxRemovingServices',
|
||||
)
|
||||
|
||||
macsRange = gui.TextField(
|
||||
length=36,
|
||||
label=_('Macs range'),
|
||||
default='02:46:00:00:00:00-02:46:00:FF:FF:FF',
|
||||
order=90,
|
||||
readonly=True,
|
||||
tooltip=_('Range of valid macs for created machines'),
|
||||
required=True,
|
||||
tab=types.ui.Tab.ADVANCED,
|
||||
)
|
||||
verifySSL = gui.CheckBoxField(
|
||||
label=_('Verify Certificate'),
|
||||
order=91,
|
||||
tooltip=_(
|
||||
'If selected, certificate will be checked against system valid certificate providers'
|
||||
),
|
||||
tab=types.ui.Tab.ADVANCED,
|
||||
default=False,
|
||||
)
|
||||
|
||||
hostBackup = gui.TextField(
|
||||
macs_range = fields.macs_range_field(default='02:46:00:00:00:00-02:46:00:FF:FF:FF')
|
||||
verify_ssl = fields.verify_ssl_field(old_field_name='verifySSL')
|
||||
|
||||
host_backup = gui.TextField(
|
||||
length=64,
|
||||
label=_('Backup Host'),
|
||||
order=92,
|
||||
tooltip=_(
|
||||
'XenServer BACKUP IP or Hostname (used on connection failure to main server)'
|
||||
),
|
||||
tooltip=_('XenServer BACKUP IP or Hostname (used on connection failure to main server)'),
|
||||
tab=types.ui.Tab.ADVANCED,
|
||||
required=False,
|
||||
old_field_name='hostBackup',
|
||||
)
|
||||
|
||||
_api: typing.Optional[XenServer]
|
||||
@ -184,19 +145,19 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
# If we want to connect to more than one server, we need keep locked access to api, change api server, etc..
|
||||
# We have implemented an "exclusive access" client that will only connect to one server at a time (using locks)
|
||||
# and this way all will be fine
|
||||
def __getApi(self, force: bool = False) -> XenServer:
|
||||
def _get_api(self, force: bool = False) -> XenServer:
|
||||
"""
|
||||
Returns the connection API object for XenServer (using XenServersdk)
|
||||
"""
|
||||
if not self._api or force:
|
||||
self._api = XenServer(
|
||||
self.host.value,
|
||||
self.hostBackup.value,
|
||||
self.host_backup.value,
|
||||
443,
|
||||
self.username.value,
|
||||
self.password.value,
|
||||
True,
|
||||
self.verifySSL.as_bool(),
|
||||
self.verify_ssl.as_bool(),
|
||||
)
|
||||
|
||||
return self._api
|
||||
@ -210,7 +171,7 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
# Just reset _api connection variable
|
||||
self._api = None
|
||||
|
||||
def testConnection(self):
|
||||
def test_connection(self):
|
||||
"""
|
||||
Test that conection to XenServer server is fine
|
||||
|
||||
@ -218,9 +179,9 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
|
||||
True if all went fine, false if id didn't
|
||||
"""
|
||||
self.__getApi().test()
|
||||
self._get_api().test()
|
||||
|
||||
def checkTaskFinished(self, task: typing.Optional[str]) -> tuple[bool, str]:
|
||||
def check_task_finished(self, task: typing.Optional[str]) -> tuple[bool, str]:
|
||||
"""
|
||||
Checks a task state.
|
||||
Returns None if task is Finished
|
||||
@ -230,7 +191,7 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
if not task:
|
||||
return True, ''
|
||||
|
||||
ts = self.__getApi().getTaskInfo(task)
|
||||
ts = self._get_api().get_task_info(task)
|
||||
logger.debug('Task status: %s', ts)
|
||||
if ts['status'] == 'running':
|
||||
return False, ts['progress']
|
||||
@ -238,11 +199,9 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
return True, ts['result']
|
||||
|
||||
# Any other state, raises an exception
|
||||
raise Exception(str(ts['result'])) # Should be error message
|
||||
raise Exception(ts) # Should be error message
|
||||
|
||||
def getMachines(
|
||||
self, force: bool = False
|
||||
) -> collections.abc.Iterable[collections.abc.MutableMapping[str, typing.Any]]:
|
||||
def list_machines(self, force: bool = False) -> list[collections.abc.MutableMapping[str, typing.Any]]:
|
||||
"""
|
||||
Obtains the list of machines inside XenServer.
|
||||
Machines starting with UDS are filtered out
|
||||
@ -258,14 +217,9 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
'cluster_id'
|
||||
"""
|
||||
|
||||
for m in self.__getApi().getVMs():
|
||||
if m['name'][:3] == 'UDS':
|
||||
continue
|
||||
yield m
|
||||
return [m for m in self._get_api().list_machines() if m['name'][:3] != 'UDS']
|
||||
|
||||
def getStorages(
|
||||
self, force: bool = False
|
||||
) -> collections.abc.Iterable[collections.abc.MutableMapping[str, typing.Any]]:
|
||||
def list_storages(self, force: bool = False) -> list[dict[str, typing.Any]]:
|
||||
"""
|
||||
Obtains the list of storages inside XenServer.
|
||||
|
||||
@ -280,11 +234,9 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
'size'
|
||||
'used'
|
||||
"""
|
||||
return self.__getApi().getSRs()
|
||||
return self._get_api().list_srs()
|
||||
|
||||
def getStorageInfo(
|
||||
self, storageId: str, force=False
|
||||
) -> collections.abc.MutableMapping[str, typing.Any]:
|
||||
def get_storage_info(self, storageId: str, force=False) -> collections.abc.MutableMapping[str, typing.Any]:
|
||||
"""
|
||||
Obtains the storage info
|
||||
|
||||
@ -304,19 +256,19 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
# 'active' -> True or False --> This is not provided by api?? (api.storagedomains.get)
|
||||
|
||||
"""
|
||||
return self.__getApi().getSRInfo(storageId)
|
||||
return self._get_api().get_sr_info(storageId)
|
||||
|
||||
def getNetworks(
|
||||
def get_networks(
|
||||
self, force: bool = False
|
||||
) -> collections.abc.Iterable[collections.abc.MutableMapping[str, typing.Any]]:
|
||||
return self.__getApi().getNetworks()
|
||||
return self._get_api().list_networks()
|
||||
|
||||
def cloneForTemplate(self, name: str, comments: str, machineId: str, sr: str):
|
||||
task = self.__getApi().clone_vm(machineId, name, sr)
|
||||
def clone_for_template(self, name: str, comments: str, machineId: str, sr: str):
|
||||
task = self._get_api().clone_machine(machineId, name, sr)
|
||||
logger.debug('Task for cloneForTemplate: %s', task)
|
||||
return task
|
||||
|
||||
def convertToTemplate(self, machineId: str, shadowMultiplier: int = 4) -> None:
|
||||
def convert_to_template(self, machine_id: str, shadow_multiplier: int = 4) -> None:
|
||||
"""
|
||||
Publish the machine (makes a template from it so we can create COWs) and returns the template id of
|
||||
the creating machine
|
||||
@ -331,17 +283,17 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
Returns
|
||||
Raises an exception if operation could not be acomplished, or returns the id of the template being created.
|
||||
"""
|
||||
self.__getApi().convertToTemplate(machineId, shadowMultiplier)
|
||||
self._get_api().convert_to_template(machine_id, shadow_multiplier)
|
||||
|
||||
def removeTemplate(self, templateId: str) -> None:
|
||||
def remove_template(self, templateId: str) -> None:
|
||||
"""
|
||||
Removes a template from XenServer server
|
||||
|
||||
Returns nothing, and raises an Exception if it fails
|
||||
"""
|
||||
self.__getApi().removeTemplate(templateId)
|
||||
self._get_api().remove_template(templateId)
|
||||
|
||||
def startDeployFromTemplate(self, name: str, comments: str, templateId: str) -> str:
|
||||
def start_deploy_from_template(self, name: str, comments: str, template_id: str) -> str:
|
||||
"""
|
||||
Deploys a virtual machine on selected cluster from selected template
|
||||
|
||||
@ -357,15 +309,28 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
Returns:
|
||||
Id of the machine being created form template
|
||||
"""
|
||||
return self.__getApi().cloneTemplate(templateId, name)
|
||||
return self._get_api().start_deploy_from_template(template_id, name)
|
||||
|
||||
def getVMPowerState(self, machineId: str) -> str:
|
||||
def get_machine_power_state(self, machine_id: str) -> str:
|
||||
"""
|
||||
Returns current machine power state
|
||||
"""
|
||||
return self.__getApi().getVMPowerState(machineId)
|
||||
return self._get_api().get_machine_power_state(machine_id)
|
||||
|
||||
def startVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
def get_machine_name(self, machine_id: str) -> str:
|
||||
return self._get_api().get_machine_info(machine_id).get('name_label', '')
|
||||
|
||||
def list_folders(self) -> list[str]:
|
||||
return self._get_api().list_folders()
|
||||
|
||||
def get_machine_folder(self, machine_id: str) -> str:
|
||||
return self._get_api().get_machine_folder(machine_id)
|
||||
|
||||
def get_machines_from_folder(self, folder: str, retrieve_names: bool = False) -> list[dict[str, typing.Any]]:
|
||||
return self._get_api().get_machines_from_folder(folder, retrieve_names)
|
||||
|
||||
|
||||
def start_machine(self, machine_id: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to start a machine. No check is done, it is simply requested to XenServer.
|
||||
|
||||
@ -376,9 +341,9 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.__getApi().startVM(machineId, asnc)
|
||||
return self._get_api().start_machine(machine_id, as_async)
|
||||
|
||||
def stopVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
def stop_machine(self, machine_id: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to start a machine. No check is done, it is simply requested to XenServer
|
||||
|
||||
@ -387,9 +352,9 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.__getApi().stopVM(machineId, asnc)
|
||||
return self._get_api().stop_machine(machine_id, as_async)
|
||||
|
||||
def resetVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
def reset_machine(self, machine_id: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to start a machine. No check is done, it is simply requested to XenServer
|
||||
|
||||
@ -398,9 +363,9 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.__getApi().resetVM(machineId, asnc)
|
||||
return self._get_api().reset_machine(machine_id, as_async)
|
||||
|
||||
def canSuspendVM(self, machineId: str) -> bool:
|
||||
def can_suspend_machine(self, machine_id: str) -> bool:
|
||||
"""
|
||||
The machine can be suspended only when "suspend" is in their operations list (mush have xentools installed)
|
||||
|
||||
@ -410,9 +375,9 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
Returns:
|
||||
True if the machien can be suspended
|
||||
"""
|
||||
return self.__getApi().canSuspendVM(machineId)
|
||||
return self._get_api().can_suspend_machine(machine_id)
|
||||
|
||||
def suspendVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
def suspend_machine(self, machine_id: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to start a machine. No check is done, it is simply requested to XenServer
|
||||
|
||||
@ -421,9 +386,9 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.__getApi().suspendVM(machineId, asnc)
|
||||
return self._get_api().suspend_machine(machine_id, as_async)
|
||||
|
||||
def resumeVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
def resume_machine(self, machine_id: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to start a machine. No check is done, it is simply requested to XenServer
|
||||
|
||||
@ -432,9 +397,20 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.__getApi().resumeVM(machineId, asnc)
|
||||
return self._get_api().resume_machine(machine_id, as_async)
|
||||
|
||||
def removeVM(self, machineId: str) -> None:
|
||||
def shutdown_machine(self, machine_id: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to start a machine. No check is done, it is simply requested to XenServer
|
||||
|
||||
Args:
|
||||
machineId: Id of the machine
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self._get_api().shutdown_machine(machine_id, as_async)
|
||||
|
||||
def remove_machine(self, machine_id: str) -> None:
|
||||
"""
|
||||
Tries to delete a machine. No check is done, it is simply requested to XenServer
|
||||
|
||||
@ -443,23 +419,39 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
|
||||
Returns:
|
||||
"""
|
||||
self.__getApi().removeVM(machineId)
|
||||
self._get_api().remove_machine(machine_id)
|
||||
|
||||
def configureVM(self, machineId: str, netId: str, mac: str, memory: int) -> None:
|
||||
self.__getApi().configureVM(
|
||||
machineId, mac={'network': netId, 'mac': mac}, memory=memory
|
||||
)
|
||||
def configure_machine(self, machine_id: str, netId: str, mac: str, memory: int) -> None:
|
||||
self._get_api().configure_machine(machine_id, mac={'network': netId, 'mac': mac}, memory=memory)
|
||||
|
||||
def provisionVM(self, machineId: str, asnc: bool = True) -> str:
|
||||
return self.__getApi().provisionVM(machineId, asnc=asnc)
|
||||
def provision_machine(self, machine_id: str, as_async: bool = True) -> str:
|
||||
return self._get_api().provision_machine(machine_id, as_async=as_async)
|
||||
|
||||
def getMacRange(self) -> str:
|
||||
return self.macsRange.value
|
||||
def get_first_ip(self, machine_id: str) -> str:
|
||||
return self._get_api().get_first_ip(machine_id)
|
||||
|
||||
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT)
|
||||
def get_first_mac(self, machine_id: str) -> str:
|
||||
return self._get_api().get_first_mac(machine_id)
|
||||
|
||||
def create_snapshot(self, machine_id: str, name: str) -> str:
|
||||
return self._get_api().create_snapshot(machine_id, name)
|
||||
|
||||
def restore_snapshot(self, snapshot_id: str) -> str:
|
||||
return self._get_api().restore_snapshot(snapshot_id)
|
||||
|
||||
def remove_snapshot(self, snapshot_id: str) -> str:
|
||||
return self._get_api().remove_snapshot(snapshot_id)
|
||||
|
||||
def list_snapshots(self, machine_id: str, full_info: bool = False) -> list[dict[str, typing.Any]]:
|
||||
return self._get_api().list_snapshots(machine_id)
|
||||
|
||||
def get_macs_range(self) -> str:
|
||||
return self.macs_range.value
|
||||
|
||||
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT, key_fnc=lambda x: x.host.as_str())
|
||||
def is_available(self) -> bool:
|
||||
try:
|
||||
self.testConnection()
|
||||
self.test_connection()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@ -495,7 +487,7 @@ class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods
|
||||
# return [True, _('Nothing tested, but all went fine..')]
|
||||
xe = XenProvider(env, data)
|
||||
try:
|
||||
xe.testConnection()
|
||||
xe.test_connection()
|
||||
return [True, _('Connection test successful')]
|
||||
except Exception as e:
|
||||
return [False, _("Connection failed: {}").format(str(e))]
|
||||
|
@ -92,7 +92,7 @@ class XenPublication(Publication, autoserializable.AutoSerializable):
|
||||
"""
|
||||
Realizes the publication of the service
|
||||
"""
|
||||
self._name = self.service().sanitizeVmName(
|
||||
self._name = self.service().sanitized_name(
|
||||
'UDS Pub ' + self.servicepool_name() + "-" + str(self.revision())
|
||||
)
|
||||
comments = _('UDS pub for {0} at {1}').format(
|
||||
@ -103,7 +103,7 @@ class XenPublication(Publication, autoserializable.AutoSerializable):
|
||||
self._state = 'ok'
|
||||
|
||||
try:
|
||||
self._task = self.service().startDeployTemplate(self._name, comments)
|
||||
self._task = self.service().start_deploy_of_template(self._name, comments)
|
||||
except Exception as e:
|
||||
self._state = 'error'
|
||||
self._reason = str(e)
|
||||
@ -130,7 +130,7 @@ class XenPublication(Publication, autoserializable.AutoSerializable):
|
||||
self._destroy_after = False
|
||||
return self.destroy()
|
||||
|
||||
self.service().convertToTemplate(self._template_id)
|
||||
self.service().convert_to_template(self._template_id)
|
||||
return State.FINISHED
|
||||
except Exception as e:
|
||||
self._state = 'error'
|
||||
@ -149,7 +149,7 @@ class XenPublication(Publication, autoserializable.AutoSerializable):
|
||||
return State.RUNNING
|
||||
|
||||
try:
|
||||
self.service().removeTemplate(self._template_id)
|
||||
self.service().remove_template(self._template_id)
|
||||
except Exception as e:
|
||||
self._state = 'error'
|
||||
self._reason = str(e)
|
||||
|
@ -35,7 +35,7 @@ import collections.abc
|
||||
|
||||
from django.utils.translation import gettext_noop as _
|
||||
from uds.core import services, exceptions, types
|
||||
from uds.core.util import validators
|
||||
from uds.core.util import fields, validators
|
||||
from uds.core.ui import gui
|
||||
|
||||
from .publication import XenPublication
|
||||
@ -101,25 +101,23 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
|
||||
services_type_provided = types.services.ServiceType.VDI
|
||||
|
||||
|
||||
# Now the form part
|
||||
datastore = gui.ChoiceField(
|
||||
label=_("Storage SR"),
|
||||
readonly=False,
|
||||
order=100,
|
||||
tooltip=_(
|
||||
'Storage where to publish and put incrementals (only shared storages are supported)'
|
||||
),
|
||||
tooltip=_('Storage where to publish and put incrementals (only shared storages are supported)'),
|
||||
required=True,
|
||||
)
|
||||
|
||||
minSpaceGB = gui.NumericField(
|
||||
min_space_gb = gui.NumericField(
|
||||
length=3,
|
||||
label=_('Reserved Space'),
|
||||
default=32,
|
||||
order=101,
|
||||
tooltip=_('Minimal free space in GB'),
|
||||
required=True,
|
||||
old_field_name='minSpaceGB',
|
||||
)
|
||||
|
||||
machine = gui.ChoiceField(
|
||||
@ -161,24 +159,8 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
required=True,
|
||||
)
|
||||
|
||||
baseName = gui.TextField(
|
||||
label=_('Machine Names'),
|
||||
readonly=False,
|
||||
order=114,
|
||||
tooltip=_('Base name for clones from this machine'),
|
||||
tab=_('Machine'),
|
||||
required=True,
|
||||
)
|
||||
|
||||
lenName = gui.NumericField(
|
||||
length=1,
|
||||
label=_('Name Length'),
|
||||
default=5,
|
||||
order=115,
|
||||
tooltip=_('Size of numeric part for the names of these machines'),
|
||||
tab=_('Machine'),
|
||||
required=True,
|
||||
)
|
||||
basename = fields.basename_field(order=114)
|
||||
lenname = fields.lenname_field(order=115)
|
||||
|
||||
def initialize(self, values):
|
||||
"""
|
||||
@ -188,12 +170,10 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
initialized by __init__ method of base class, before invoking this.
|
||||
"""
|
||||
if values:
|
||||
validators.validate_basename(self.baseName.value, self.lenName.as_int())
|
||||
validators.validate_basename(self.basename.value, self.lenname.as_int())
|
||||
|
||||
if int(self.memory.value) < 256:
|
||||
raise exceptions.ui.ValidationError(
|
||||
_('The minimum allowed memory is 256 Mb')
|
||||
)
|
||||
raise exceptions.ui.ValidationError(_('The minimum allowed memory is 256 Mb'))
|
||||
|
||||
def parent(self) -> 'XenProvider':
|
||||
return typing.cast('XenProvider', super().parent())
|
||||
@ -203,12 +183,10 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
# This is that value is always '', so if we want to change something, we have to do it
|
||||
# at defValue
|
||||
|
||||
machines_list = [
|
||||
gui.choice_item(m['id'], m['name']) for m in self.parent().getMachines()
|
||||
]
|
||||
machines_list = [gui.choice_item(m['id'], m['name']) for m in self.parent().list_machines()]
|
||||
|
||||
storages_list = []
|
||||
for storage in self.parent().getStorages():
|
||||
for storage in self.parent().list_storages():
|
||||
space, free = (
|
||||
storage['size'] / 1024,
|
||||
(storage['size'] - storage['used']) / 1024,
|
||||
@ -220,37 +198,34 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
)
|
||||
)
|
||||
|
||||
network_list = [
|
||||
gui.choice_item(net['id'], net['name'])
|
||||
for net in self.parent().getNetworks()
|
||||
]
|
||||
network_list = [gui.choice_item(net['id'], net['name']) for net in self.parent().get_networks()]
|
||||
|
||||
self.machine.set_choices(machines_list)
|
||||
self.datastore.set_choices(storages_list)
|
||||
self.network.set_choices(network_list)
|
||||
|
||||
def check_task_finished(self, task: str) -> tuple[bool, str]:
|
||||
return self.parent().checkTaskFinished(task)
|
||||
return self.parent().check_task_finished(task)
|
||||
|
||||
def datastoreHasSpace(self) -> None:
|
||||
def has_datastore_space(self) -> None:
|
||||
# Get storages for that datacenter
|
||||
info = self.parent().getStorageInfo(self.datastore.value)
|
||||
info = self.parent().get_storage_info(self.datastore.value)
|
||||
logger.debug('Checking datastore space for %s: %s', self.datastore.value, info)
|
||||
availableGB = (info['size'] - info['used']) / 1024
|
||||
if availableGB < self.minSpaceGB.as_int():
|
||||
if availableGB < self.min_space_gb.as_int():
|
||||
raise Exception(
|
||||
'Not enough free space available: (Needs at least {} GB and there is only {} GB '.format(
|
||||
self.minSpaceGB.as_int(), availableGB
|
||||
self.min_space_gb.as_int(), availableGB
|
||||
)
|
||||
)
|
||||
|
||||
def sanitizeVmName(self, name: str) -> str:
|
||||
def sanitized_name(self, name: str) -> str:
|
||||
"""
|
||||
Xen Seems to allow all kind of names
|
||||
"""
|
||||
return name
|
||||
|
||||
def startDeployTemplate(self, name: str, comments: str) -> str:
|
||||
def start_deploy_of_template(self, name: str, comments: str) -> str:
|
||||
"""
|
||||
Invokes makeTemplate from parent provider, completing params
|
||||
|
||||
@ -271,19 +246,17 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
)
|
||||
|
||||
# Checks datastore available space, raises exeception in no min available
|
||||
self.datastoreHasSpace()
|
||||
self.has_datastore_space()
|
||||
|
||||
return self.parent().cloneForTemplate(
|
||||
name, comments, self.machine.value, self.datastore.value
|
||||
)
|
||||
return self.parent().clone_for_template(name, comments, self.machine.value, self.datastore.value)
|
||||
|
||||
def convertToTemplate(self, machineId: str) -> None:
|
||||
def convert_to_template(self, machineId: str) -> None:
|
||||
"""
|
||||
converts machine to template
|
||||
"""
|
||||
self.parent().convertToTemplate(machineId, self.shadow.value)
|
||||
self.parent().convert_to_template(machineId, self.shadow.value)
|
||||
|
||||
def startDeployFromTemplate(self, name: str, comments: str, templateId: str) -> str:
|
||||
def start_deploy_from_template(self, name: str, comments: str, templateId: str) -> str:
|
||||
"""
|
||||
Deploys a virtual machine on selected cluster from selected template
|
||||
|
||||
@ -299,17 +272,17 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
Id of the machine being created form template
|
||||
"""
|
||||
logger.debug('Deploying from template %s machine %s', templateId, name)
|
||||
self.datastoreHasSpace()
|
||||
self.has_datastore_space()
|
||||
|
||||
return self.parent().startDeployFromTemplate(name, comments, templateId)
|
||||
return self.parent().start_deploy_from_template(name, comments, templateId)
|
||||
|
||||
def removeTemplate(self, templateId: str) -> None:
|
||||
def remove_template(self, templateId: str) -> None:
|
||||
"""
|
||||
invokes removeTemplate from parent provider
|
||||
"""
|
||||
self.parent().removeTemplate(templateId)
|
||||
self.parent().remove_template(templateId)
|
||||
|
||||
def getVMPowerState(self, machineId: str) -> str:
|
||||
def get_machine_power_state(self, machineId: str) -> str:
|
||||
"""
|
||||
Invokes getMachineState from parent provider
|
||||
|
||||
@ -319,9 +292,9 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
Returns:
|
||||
one of this values:
|
||||
"""
|
||||
return self.parent().getVMPowerState(machineId)
|
||||
return self.parent().get_machine_power_state(machineId)
|
||||
|
||||
def startVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
def start_machine(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to start a machine. No check is done, it is simply requested to Xen.
|
||||
|
||||
@ -332,9 +305,9 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.parent().startVM(machineId, asnc)
|
||||
return self.parent().start_machine(machineId, asnc)
|
||||
|
||||
def stopVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
def stop_machine(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to stop a machine. No check is done, it is simply requested to Xen
|
||||
|
||||
@ -343,9 +316,9 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.parent().stopVM(machineId, asnc)
|
||||
return self.parent().stop_machine(machineId, asnc)
|
||||
|
||||
def resetVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
def reset_machine(self, machine_id: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to stop a machine. No check is done, it is simply requested to Xen
|
||||
|
||||
@ -354,7 +327,7 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.parent().resetVM(machineId, asnc)
|
||||
return self.parent().reset_machine(machine_id, asnc)
|
||||
|
||||
def can_suspend_machine(self, machineId: str) -> bool:
|
||||
"""
|
||||
@ -366,7 +339,7 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
Returns:
|
||||
True if the machien can be suspended
|
||||
"""
|
||||
return self.parent().canSuspendVM(machineId)
|
||||
return self.parent().can_suspend_machine(machineId)
|
||||
|
||||
def suspend_machine(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
@ -377,9 +350,9 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.parent().suspendVM(machineId, asnc)
|
||||
return self.parent().suspend_machine(machineId, asnc)
|
||||
|
||||
def resumeVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
def resume_machine(self, machineId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to resume a machine. No check is done, it is simply requested to Xen
|
||||
|
||||
@ -388,9 +361,9 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.parent().suspendVM(machineId, asnc)
|
||||
return self.parent().suspend_machine(machineId, asnc)
|
||||
|
||||
def removeVM(self, machineId: str) -> None:
|
||||
def remove_machine(self, machineId: str) -> None:
|
||||
"""
|
||||
Tries to delete a machine. No check is done, it is simply requested to Xen
|
||||
|
||||
@ -399,28 +372,28 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met
|
||||
|
||||
Returns:
|
||||
"""
|
||||
self.parent().removeVM(machineId)
|
||||
self.parent().remove_machine(machineId)
|
||||
|
||||
def configureVM(self, machineId: str, mac: str) -> None:
|
||||
self.parent().configureVM(machineId, self.network.value, mac, self.memory.value)
|
||||
def configure_machine(self, machine_id: str, mac: str) -> None:
|
||||
self.parent().configure_machine(machine_id, self.network.value, mac, self.memory.value)
|
||||
|
||||
def provisionVM(self, machineId: str, asnc: bool = True) -> str:
|
||||
return self.parent().provisionVM(machineId, asnc)
|
||||
def provision_machine(self, machine_id: str, as_async: bool = True) -> str:
|
||||
return self.parent().provision_machine(machine_id, as_async)
|
||||
|
||||
def get_macs_range(self) -> str:
|
||||
"""
|
||||
Returns de selected mac range
|
||||
"""
|
||||
return self.parent().getMacRange()
|
||||
return self.parent().get_macs_range()
|
||||
|
||||
def get_basename(self) -> str:
|
||||
"""
|
||||
Returns the base name
|
||||
"""
|
||||
return self.baseName.value
|
||||
return self.basename.value
|
||||
|
||||
def get_lenname(self) -> int:
|
||||
"""
|
||||
Returns the length of numbers part
|
||||
"""
|
||||
return int(self.lenName.value)
|
||||
return int(self.lenname.value)
|
||||
|
293
server/src/uds/services/Xen/service_fixed.py
Normal file
293
server/src/uds/services/Xen/service_fixed.py
Normal file
@ -0,0 +1,293 @@
|
||||
#
|
||||
# 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 logging
|
||||
import typing
|
||||
|
||||
from django.utils.translation import gettext_noop as _, gettext
|
||||
from uds.core import services, types, consts, exceptions
|
||||
from uds.core.services.specializations.fixed_machine.fixed_service import FixedService
|
||||
from uds.core.services.specializations.fixed_machine.fixed_userservice import FixedUserService
|
||||
from uds.core.ui import gui
|
||||
from uds.core.util import validators, log
|
||||
from uds.core.util.decorators import cached
|
||||
from uds.core.workers import initialize
|
||||
|
||||
from . import helpers
|
||||
from .deployment_fixed import XenFixedUserService
|
||||
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds.core.module import Module
|
||||
from uds import models
|
||||
|
||||
from .provider import XenProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XenFixedService(FixedService): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Proxmox fixed machines service.
|
||||
"""
|
||||
|
||||
type_name = _('Proxmox Fixed Machines')
|
||||
type_type = 'ProxmoxFixedService'
|
||||
type_description = _('Proxmox Services based on fixed machines. Needs qemu agent installed on machines.')
|
||||
icon_file = 'service.png'
|
||||
|
||||
can_reset = True
|
||||
|
||||
# : Types of publications (preparated data for deploys)
|
||||
# : In our case, we do no need a publication, so this is None
|
||||
publication_type = None
|
||||
# : Types of deploys (services in cache and/or assigned to users)
|
||||
user_service_type = XenFixedUserService
|
||||
|
||||
allowed_protocols = types.transports.Protocol.generic_vdi(types.transports.Protocol.SPICE)
|
||||
services_type_provided = types.services.ServiceType.VDI
|
||||
|
||||
# Gui
|
||||
token = FixedService.token
|
||||
|
||||
folder = gui.ChoiceField(
|
||||
label=_("Folder"),
|
||||
readonly=False,
|
||||
order=20,
|
||||
fills={
|
||||
'callback_name': 'xmFillMachinesFromFolder',
|
||||
'function': helpers.get_machines,
|
||||
'parameters': ['prov_uuid', 'folder'],
|
||||
},
|
||||
tooltip=_('Resource Pool containing base machines'),
|
||||
required=True,
|
||||
tab=_('Machines'),
|
||||
old_field_name='resourcePool',
|
||||
)
|
||||
# Keep name as "machine" so we can use VCHelpers.getMachines
|
||||
machines = gui.MultiChoiceField(
|
||||
label=_("Machines"),
|
||||
order=21,
|
||||
tooltip=_('Machines for this service'),
|
||||
required=True,
|
||||
tab=_('Machines'),
|
||||
rows=10,
|
||||
)
|
||||
|
||||
use_snapshots = gui.CheckBoxField(
|
||||
label=_('Use snapshots'),
|
||||
default=False,
|
||||
order=22,
|
||||
tooltip=_('If active, UDS will try to create an snapshot on VM use and recover if on exit.'),
|
||||
tab=_('Machines'),
|
||||
old_field_name='useSnapshots',
|
||||
)
|
||||
|
||||
prov_uuid = gui.HiddenField(value=None)
|
||||
|
||||
def initialize(self, values: 'Module.ValuesType') -> None:
|
||||
"""
|
||||
Loads the assigned machines from storage
|
||||
"""
|
||||
if values:
|
||||
if not self.machines.value:
|
||||
raise exceptions.ui.ValidationError(gettext('We need at least a machine'))
|
||||
|
||||
self.storage.put_pickle('userservices_limit', len(self.machines.as_list()))
|
||||
|
||||
# Remove machines not in values from "assigned" set
|
||||
self._save_assigned_machines(self._get_assigned_machines() & set(self.machines.as_list()))
|
||||
self.token.value = self.token.value.strip()
|
||||
self.userservices_limit = self.storage.get_unpickle('userservices_limit')
|
||||
|
||||
def init_gui(self) -> None:
|
||||
# Here we have to use "default values", cause values aren't used at form initialization
|
||||
# This is that value is always '', so if we want to change something, we have to do it
|
||||
# at defValue
|
||||
self.prov_uuid.value = self.parent().get_uuid()
|
||||
|
||||
self.folder.set_choices([gui.choice_item(folder, folder) for folder in self.parent().list_folders()])
|
||||
|
||||
def parent(self) -> 'XenProvider':
|
||||
return typing.cast('XenProvider', super().parent())
|
||||
|
||||
def get_machine_power_state(self, machine_id: str) -> str:
|
||||
"""
|
||||
Invokes getMachineState from parent provider
|
||||
|
||||
Args:
|
||||
machineId: If of the machine to get state
|
||||
|
||||
Returns:
|
||||
one of this values:
|
||||
"""
|
||||
return self.parent().get_machine_power_state(machine_id)
|
||||
|
||||
def start_machine(self, machine_id: str) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to start a machine. No check is done, it is simply requested to Xen.
|
||||
|
||||
This start also "resume" suspended/paused machines
|
||||
|
||||
Args:
|
||||
machineId: Id of the machine
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.parent().start_machine(machine_id)
|
||||
|
||||
def stop_machine(self, machine_id: str) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to stop a machine. No check is done, it is simply requested to Xen
|
||||
|
||||
Args:
|
||||
machineId: Id of the machine
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.parent().stop_machine(machine_id)
|
||||
|
||||
def reset_machine(self, machine_id: str) -> typing.Optional[str]:
|
||||
"""
|
||||
Tries to stop a machine. No check is done, it is simply requested to Xen
|
||||
|
||||
Args:
|
||||
machineId: Id of the machine
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.parent().reset_machine(machine_id)
|
||||
|
||||
def shutdown_machine(self, vm_id: str) -> typing.Optional[str]:
|
||||
return self.parent().shutdown_machine(vm_id)
|
||||
|
||||
def check_task_finished(self, task: str) -> tuple[bool, str]:
|
||||
return self.parent().check_task_finished(task)
|
||||
|
||||
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT)
|
||||
def is_avaliable(self) -> bool:
|
||||
return self.parent().is_available()
|
||||
|
||||
def enumerate_assignables(self) -> list[tuple[str, str]]:
|
||||
# Obtain machines names and ids for asignables
|
||||
vms: dict[int, str] = {}
|
||||
|
||||
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(XenFixedUserService, 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!')
|
||||
|
||||
def process_snapshot(self, remove: bool, userservice_instace: FixedUserService) -> str:
|
||||
userservice_instace = typing.cast(XenFixedUserService, userservice_instace)
|
||||
if self.use_snapshots.as_bool():
|
||||
vmid = userservice_instace._vmid
|
||||
|
||||
snapshots = [i['id'] for i in self.parent().list_snapshots(vmid)]
|
||||
snapshot = snapshots[0] if snapshots else None
|
||||
|
||||
if remove and snapshot:
|
||||
try:
|
||||
userservice_instace._task = self.parent().restore_snapshot(snapshot['id'])
|
||||
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 snapshot: # No snapshot, try to create one
|
||||
logger.debug('Not current snapshot')
|
||||
# We don't need the snapshot nor the task, will simply restore to newer snapshot on remove
|
||||
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[str] = None
|
||||
try:
|
||||
assigned_vms = self._get_assigned_machines()
|
||||
for k in self.machines.as_list():
|
||||
checking_vmid = k
|
||||
if found_vmid not in assigned_vms: # Not assigned
|
||||
# Check that the machine exists...
|
||||
try:
|
||||
vm_name = self.parent().get_machine_name(checking_vmid)
|
||||
found_vmid = checking_vmid
|
||||
break
|
||||
except Exception: # Notifies on log, but skipt it
|
||||
self.parent().do_log(
|
||||
log.LogLevel.WARNING, 'Machine {} not accesible'.format(found_vmid)
|
||||
)
|
||||
logger.warning(
|
||||
'The service has machines that cannot be checked on proxmox (connection error or machine has been deleted): %s',
|
||||
found_vmid,
|
||||
)
|
||||
|
||||
if found_vmid:
|
||||
assigned_vms.add(str(found_vmid))
|
||||
self._save_assigned_machines(assigned_vms)
|
||||
except Exception: #
|
||||
raise Exception('No machine available')
|
||||
|
||||
if not found_vmid:
|
||||
raise Exception('All machines from list already assigned.')
|
||||
|
||||
return str(found_vmid)
|
||||
|
||||
def get_first_network_mac(self, vmid: str) -> str:
|
||||
return self.parent().get_first_mac(vmid)
|
||||
|
||||
def get_guest_ip_address(self, vmid: str) -> str:
|
||||
return self.parent().get_first_ip(vmid)
|
||||
|
||||
def get_machine_name(self, vmid: str) -> str:
|
||||
return self.parent().get_machine_name(vmid)
|
||||
|
||||
def remove_and_free_machine(self, vmid: str) -> None:
|
||||
try:
|
||||
self._save_assigned_machines(self._get_assigned_machines() - {str(vmid)}) # Remove from assigned
|
||||
except Exception as e:
|
||||
logger.warn('Cound not save assigned machines on fixed pool: %s', e)
|
@ -31,6 +31,12 @@ import logging
|
||||
import typing
|
||||
import collections.abc
|
||||
|
||||
from MySQLdb import connect
|
||||
import server
|
||||
from uds.core import consts
|
||||
|
||||
from uds.core.util.decorators import cached
|
||||
|
||||
import XenAPI
|
||||
|
||||
|
||||
@ -44,6 +50,10 @@ class XenFault(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def cache_key_helper(server_api: 'XenServer') -> str:
|
||||
return server_api._url
|
||||
|
||||
|
||||
class XenFailure(XenAPI.Failure, XenFault):
|
||||
exBadVmPowerState = 'VM_BAD_POWER_STATE'
|
||||
exVmMissingPVDrivers = 'VM_MISSING_PV_DRIVERS'
|
||||
@ -58,16 +68,16 @@ class XenFailure(XenAPI.Failure, XenFault):
|
||||
def isHandleInvalid(self) -> bool:
|
||||
return self.details[0] == XenFailure.exHandleInvalid
|
||||
|
||||
def needsXenTools(self) -> bool:
|
||||
def needs_xen_tools(self) -> bool:
|
||||
return self.details[0] == XenFailure.exVmMissingPVDrivers
|
||||
|
||||
def badPowerState(self) -> bool:
|
||||
def bad_power_state(self) -> bool:
|
||||
return self.details[0] == XenFailure.exBadVmPowerState
|
||||
|
||||
def isSlave(self) -> bool:
|
||||
def is_slave(self) -> bool:
|
||||
return self.details[0] == XenFailure.exHostIsSlave
|
||||
|
||||
def asHumanReadable(self) -> str:
|
||||
def as_human_readable(self) -> str:
|
||||
try:
|
||||
errList = {
|
||||
XenFailure.exBadVmPowerState: 'Machine state is invalid for requested operation (needs {2} and state is {3})',
|
||||
@ -83,7 +93,7 @@ class XenFailure(XenAPI.Failure, XenFault):
|
||||
return 'Unknown exception: {0}'.format(self.details)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.asHumanReadable()
|
||||
return self.as_human_readable()
|
||||
|
||||
|
||||
class XenException(XenFault):
|
||||
@ -104,16 +114,16 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
_host: str
|
||||
_host_backup: str
|
||||
_port: str
|
||||
_useSSL: bool
|
||||
_verifySSL: bool
|
||||
_use_ssl: bool
|
||||
_verify_ssl: bool
|
||||
_protocol: str
|
||||
_url: str
|
||||
_loggedIn: bool
|
||||
_logged_in: bool
|
||||
_username: str
|
||||
_password: str
|
||||
_session: typing.Any
|
||||
_poolName: str
|
||||
_apiVersion: str
|
||||
_pool_name: str
|
||||
_api_version: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -128,61 +138,96 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
self._originalHost = self._host = host
|
||||
self._host_backup = host_backup or ''
|
||||
self._port = str(port)
|
||||
self._useSSL = bool(useSSL)
|
||||
self._verifySSL = bool(verifySSL)
|
||||
self._protocol = 'http' + ('s' if self._useSSL else '') + '://'
|
||||
self._use_ssl = bool(useSSL)
|
||||
self._verify_ssl = bool(verifySSL)
|
||||
self._protocol = 'http' + ('s' if self._use_ssl else '') + '://'
|
||||
self._url = ''
|
||||
self._loggedIn = False
|
||||
self._logged_in = False
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._session = None
|
||||
self._poolName = self._apiVersion = ''
|
||||
self._pool_name = self._api_version = ''
|
||||
|
||||
@staticmethod
|
||||
def toMb(number: typing.Union[str, int]) -> int:
|
||||
def to_mb(number: typing.Union[str, int]) -> int:
|
||||
return int(number) // (1024 * 1024)
|
||||
|
||||
def check_login(self) -> bool:
|
||||
if not self._loggedIn:
|
||||
self.login(switchToMaster=True)
|
||||
return self._loggedIn
|
||||
if not self._logged_in:
|
||||
self.login(swithc_to_master=True)
|
||||
return self._logged_in
|
||||
|
||||
def getXenapiProperty(self, prop: str) -> typing.Any:
|
||||
def get_xenapi_property(self, prop: str) -> typing.Any:
|
||||
if not self.check_login():
|
||||
raise Exception("Can't log in")
|
||||
return getattr(self._session.xenapi, prop)
|
||||
|
||||
# Properties to fast access XenApi classes
|
||||
Async = property(lambda self: self.getXenapiProperty('Async'))
|
||||
task = property(lambda self: self.getXenapiProperty('task'))
|
||||
VM = property(lambda self: self.getXenapiProperty('VM'))
|
||||
SR = property(lambda self: self.getXenapiProperty('SR'))
|
||||
pool = property(lambda self: self.getXenapiProperty('pool'))
|
||||
host = property(lambda self: self.getXenapiProperty('host'))
|
||||
network = property(lambda self: self.getXenapiProperty('network'))
|
||||
VIF = property(lambda self: self.getXenapiProperty('VIF')) # Virtual Interface
|
||||
VDI = property(lambda self: self.getXenapiProperty('VDI')) # Virtual Disk Image
|
||||
VBD = property(lambda self: self.getXenapiProperty('VBD')) # Virtual Block Device
|
||||
@property
|
||||
def Async(self) -> typing.Any:
|
||||
return self.get_xenapi_property('Async')
|
||||
|
||||
@property
|
||||
def task(self) -> typing.Any:
|
||||
return self.get_xenapi_property('task')
|
||||
|
||||
@property
|
||||
def VM(self) -> typing.Any:
|
||||
return self.get_xenapi_property('VM')
|
||||
|
||||
@property
|
||||
def SR(self) -> typing.Any:
|
||||
return self.get_xenapi_property('SR')
|
||||
|
||||
@property
|
||||
def pool(self) -> typing.Any:
|
||||
return self.get_xenapi_property('pool')
|
||||
|
||||
@property
|
||||
def host(self) -> typing.Any: # Host
|
||||
return self.get_xenapi_property('host')
|
||||
|
||||
@property
|
||||
def network(self) -> typing.Any: # Networks
|
||||
return self.get_xenapi_property('network')
|
||||
|
||||
@property
|
||||
def VIF(self) -> typing.Any: # Virtual Interface
|
||||
return self.get_xenapi_property('VIF')
|
||||
|
||||
@property
|
||||
def VDI(self) -> typing.Any: # Virtual Disk Image
|
||||
return self.get_xenapi_property('VDI')
|
||||
|
||||
@property
|
||||
def VBD(self) -> typing.Any: # Virtual Block Device
|
||||
return self.get_xenapi_property('VBD')
|
||||
|
||||
@property
|
||||
def VM_guest_metrics(self) -> typing.Any:
|
||||
return self.get_xenapi_property('VM_guest_metrics')
|
||||
|
||||
# Properties to access private vars
|
||||
poolName = property(lambda self: self.check_login() and self._poolName)
|
||||
hasPool = property(lambda self: self.check_login() and self._poolName)
|
||||
# p
|
||||
def has_pool(self) -> bool:
|
||||
return self.check_login() and bool(self._pool_name)
|
||||
|
||||
def getPoolName(self) -> str:
|
||||
@cached(prefix='xen_pool', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_fnc=cache_key_helper)
|
||||
def get_pool_name(self) -> str:
|
||||
pool = self.pool.get_all()[0]
|
||||
return self.pool.get_name_label(pool)
|
||||
|
||||
# Login/Logout
|
||||
def login(self, switchToMaster: bool = False, backupChecked: bool = False) -> None:
|
||||
def login(self, swithc_to_master: bool = False, backup_checked: bool = False) -> None:
|
||||
try:
|
||||
# We recalculate here url, because we can "switch host" on any moment
|
||||
self._url = self._protocol + self._host + ':' + self._port
|
||||
|
||||
transport = None
|
||||
|
||||
if self._useSSL:
|
||||
if self._use_ssl:
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
||||
if self._verifySSL is False:
|
||||
if self._verify_ssl is False:
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
else:
|
||||
context.verify_mode = ssl.CERT_REQUIRED
|
||||
@ -192,49 +237,50 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
|
||||
self._session = XenAPI.Session(self._url, transport=transport)
|
||||
self._session.xenapi.login_with_password(self._username, self._password)
|
||||
self._loggedIn = True
|
||||
self._apiVersion = self._session.API_version
|
||||
self._poolName = str(self.getPoolName())
|
||||
self._logged_in = True
|
||||
self._api_version = self._session.API_version
|
||||
self._pool_name = str(self.get_pool_name())
|
||||
except (
|
||||
XenAPI.Failure
|
||||
) as e: # XenAPI.Failure: ['HOST_IS_SLAVE', '172.27.0.29'] indicates that this host is an slave of 172.27.0.29, connect to it...
|
||||
if switchToMaster and e.details[0] == 'HOST_IS_SLAVE':
|
||||
if swithc_to_master and e.details[0] == 'HOST_IS_SLAVE':
|
||||
logger.info(
|
||||
'%s is an Slave, connecting to master at %s',
|
||||
self._host,
|
||||
e.details[1],
|
||||
)
|
||||
self._host = e.details[1]
|
||||
self.login(backupChecked=backupChecked)
|
||||
self.login(backup_checked=backup_checked)
|
||||
else:
|
||||
raise XenFailure(e.details)
|
||||
except Exception:
|
||||
if self._host == self._host_backup or not self._host_backup or backupChecked:
|
||||
if self._host == self._host_backup or not self._host_backup or backup_checked:
|
||||
logger.exception('Connection to master server is broken and backup connection unavailable.')
|
||||
raise
|
||||
# Retry connection to backup host
|
||||
self._host = self._host_backup
|
||||
self.login(backupChecked=True)
|
||||
self.login(backup_checked=True)
|
||||
|
||||
def test(self) -> None:
|
||||
self.login(False)
|
||||
|
||||
def logout(self) -> None:
|
||||
self._session.logout()
|
||||
self._loggedIn = False
|
||||
self._logged_in = False
|
||||
self._session = None
|
||||
self._poolName = self._apiVersion = ''
|
||||
self._pool_name = self._api_version = ''
|
||||
|
||||
def getHost(self) -> str:
|
||||
def get_host(self) -> str:
|
||||
return self._host
|
||||
|
||||
def setHost(self, host: str) -> None:
|
||||
def set_host(self, host: str) -> None:
|
||||
self._host = host
|
||||
|
||||
def getTaskInfo(self, task: str) -> collections.abc.MutableMapping[str, typing.Any]:
|
||||
def get_task_info(self, task: str) -> dict[str, typing.Any]:
|
||||
progress = 0
|
||||
result = None
|
||||
destroyTask = False
|
||||
destroy_task = False
|
||||
connection_error = False
|
||||
try:
|
||||
status = self.task.get_status(task)
|
||||
logger.debug('Task %s in state %s', task, status)
|
||||
@ -243,10 +289,10 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
progress = int(self.task.get_progress(task) * 100)
|
||||
elif status == 'success':
|
||||
result = self.task.get_result(task)
|
||||
destroyTask = True
|
||||
destroy_task = True
|
||||
elif status == 'failure':
|
||||
result = XenFailure(self.task.get_error_info(task))
|
||||
destroyTask = True
|
||||
destroy_task = True
|
||||
except XenAPI.Failure as e:
|
||||
logger.debug('XenServer Failure: %s', e.details[0])
|
||||
if e.details[0] == 'HANDLE_INVALID':
|
||||
@ -254,9 +300,14 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
status = 'unknown'
|
||||
progress = 0
|
||||
else:
|
||||
destroyTask = True
|
||||
destroy_task = True
|
||||
result = e.details[0]
|
||||
status = 'failure'
|
||||
except ConnectionError as e:
|
||||
logger.debug('Connection error: %s', e)
|
||||
result = 'Connection error'
|
||||
status = 'failure'
|
||||
connection_error = True
|
||||
except Exception as e:
|
||||
logger.exception('Unexpected exception!')
|
||||
result = str(e)
|
||||
@ -266,15 +317,17 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
if result and not isinstance(result, XenFailure) and result.startswith('<value>'):
|
||||
result = result[7:-8]
|
||||
|
||||
if destroyTask:
|
||||
if destroy_task:
|
||||
try:
|
||||
self.task.destroy(task)
|
||||
except Exception as e:
|
||||
logger.warning('Destroy task %s returned error %s', task, str(e))
|
||||
|
||||
return {'result': result, 'progress': progress, 'status': str(status)}
|
||||
return {'result': result, 'progress': progress, 'status': str(status), 'connection_error': True}
|
||||
|
||||
def getSRs(self) -> collections.abc.Iterable[collections.abc.MutableMapping[str, typing.Any]]:
|
||||
@cached(prefix='xen_srs', timeout=consts.cache.DEFAULT_CACHE_TIMEOUT, key_fnc=cache_key_helper)
|
||||
def list_srs(self) -> list[dict[str, typing.Any]]:
|
||||
return_list: list[dict[str, typing.Any]] = []
|
||||
for srId in self.SR.get_all():
|
||||
# Only valid SR shared, non iso
|
||||
name_label = self.SR.get_name_label(srId)
|
||||
@ -289,33 +342,46 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
valid = False
|
||||
|
||||
if valid:
|
||||
yield {
|
||||
'id': srId,
|
||||
'name': name_label,
|
||||
'size': XenServer.toMb(self.SR.get_physical_size(srId)),
|
||||
'used': XenServer.toMb(self.SR.get_physical_utilisation(srId)),
|
||||
}
|
||||
return_list.append(
|
||||
{
|
||||
'id': srId,
|
||||
'name': name_label,
|
||||
'size': XenServer.to_mb(self.SR.get_physical_size(srId)),
|
||||
'used': XenServer.to_mb(self.SR.get_physical_utilisation(srId)),
|
||||
}
|
||||
)
|
||||
return return_list
|
||||
|
||||
def getSRInfo(self, srId: str) -> collections.abc.MutableMapping[str, typing.Any]:
|
||||
@cached(prefix='xen_sr', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_fnc=cache_key_helper)
|
||||
def get_sr_info(self, srid: str) -> dict[str, typing.Any]:
|
||||
return {
|
||||
'id': srId,
|
||||
'name': self.SR.get_name_label(srId),
|
||||
'size': XenServer.toMb(self.SR.get_physical_size(srId)),
|
||||
'used': XenServer.toMb(self.SR.get_physical_utilisation(srId)),
|
||||
'id': srid,
|
||||
'name': self.SR.get_name_label(srid),
|
||||
'size': XenServer.to_mb(self.SR.get_physical_size(srid)),
|
||||
'used': XenServer.to_mb(self.SR.get_physical_utilisation(srid)),
|
||||
}
|
||||
|
||||
def getNetworks(self) -> collections.abc.Iterable[collections.abc.MutableMapping[str, typing.Any]]:
|
||||
@cached(prefix='xen_nets', timeout=consts.cache.DEFAULT_CACHE_TIMEOUT, key_fnc=cache_key_helper)
|
||||
def list_networks(self, **kwargs) -> list[dict[str, typing.Any]]:
|
||||
return_list: list[dict[str, typing.Any]] = []
|
||||
for netId in self.network.get_all():
|
||||
if self.network.get_other_config(netId).get('is_host_internal_management_network', False) is False:
|
||||
yield {
|
||||
'id': netId,
|
||||
'name': self.network.get_name_label(netId),
|
||||
}
|
||||
return_list.append(
|
||||
{
|
||||
'id': netId,
|
||||
'name': self.network.get_name_label(netId),
|
||||
}
|
||||
)
|
||||
|
||||
def getNetworkInfo(self, netId: str) -> collections.abc.MutableMapping[str, typing.Any]:
|
||||
return {'id': netId, 'name': self.network.get_name_label(netId)}
|
||||
return return_list
|
||||
|
||||
def getVMs(self) -> collections.abc.Iterable[collections.abc.MutableMapping[str, typing.Any]]:
|
||||
@cached(prefix='xen_net', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_fnc=cache_key_helper)
|
||||
def get_network_info(self, net_id: str) -> dict[str, typing.Any]:
|
||||
return {'id': net_id, 'name': self.network.get_name_label(net_id)}
|
||||
|
||||
@cached(prefix='xen_vms', timeout=consts.cache.DEFAULT_CACHE_TIMEOUT, key_fnc=cache_key_helper)
|
||||
def list_machines(self) -> list[dict[str, typing.Any]]:
|
||||
return_list: list[dict[str, typing.Any]] = []
|
||||
try:
|
||||
vms = self.VM.get_all()
|
||||
for vm in vms:
|
||||
@ -326,16 +392,17 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
if self.VM.get_is_control_domain(vm) or self.VM.get_is_a_template(vm):
|
||||
continue
|
||||
|
||||
yield {'id': vm, 'name': self.VM.get_name_label(vm)}
|
||||
return_list.append({'id': vm, 'name': self.VM.get_name_label(vm)})
|
||||
except Exception as e:
|
||||
logger.warning('VM %s returned error %s', vm, str(e))
|
||||
continue
|
||||
return return_list
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
except Exception as e:
|
||||
raise XenException(str(e))
|
||||
|
||||
def getVMPowerState(self, vmId: str) -> str:
|
||||
def get_machine_power_state(self, vmId: str) -> str:
|
||||
try:
|
||||
power_state = self.VM.get_power_state(vmId)
|
||||
logger.debug('Power state of %s: %s', vmId, power_state)
|
||||
@ -343,63 +410,66 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
def getVMInfo(self, vmId: str) -> typing.Any:
|
||||
@cached(prefix='xen_vm', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_fnc=cache_key_helper)
|
||||
def get_machine_info(self, vmid: str, **kwargs) -> dict[str, typing.Any]:
|
||||
try:
|
||||
return self.VM.get_record(vmId)
|
||||
return self.VM.get_record(vmid)
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
def startVM(self, vmId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
vmState = self.getVMPowerState(vmId)
|
||||
@cached(prefix='xen_vm_f', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_fnc=cache_key_helper)
|
||||
def get_machine_folder(self, vmid: str, **kwargs) -> str:
|
||||
try:
|
||||
other_config = self.VM.get_other_config(vmid)
|
||||
return other_config.get('folder', '')
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
def start_machine(self, vmId: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
vmState = self.get_machine_power_state(vmId)
|
||||
if vmState == XenPowerState.running:
|
||||
return None # Already powered on
|
||||
|
||||
if vmState == XenPowerState.suspended:
|
||||
return self.resumeVM(vmId, asnc)
|
||||
return self.resume_machine(vmId, as_async)
|
||||
return (self.Async if as_async else self).VM.start(vmId, False, False)
|
||||
|
||||
if asnc:
|
||||
return self.Async.VM.start(vmId, False, False)
|
||||
return self.VM.start(vmId, False, False)
|
||||
|
||||
def stopVM(self, vmId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
vmState = self.getVMPowerState(vmId)
|
||||
def stop_machine(self, vmid: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
vmState = self.get_machine_power_state(vmid)
|
||||
if vmState in (XenPowerState.suspended, XenPowerState.halted):
|
||||
return None # Already powered off
|
||||
if asnc:
|
||||
return self.Async.VM.hard_shutdown(vmId)
|
||||
return self.VM.hard_shutdown(vmId)
|
||||
return (self.Async if as_async else self).VM.hard_shutdown(vmid)
|
||||
|
||||
def resetVM(self, vmId, asnc=True) -> typing.Optional[str]:
|
||||
vmState = self.getVMPowerState(vmId)
|
||||
def reset_machine(self, vmid, as_async=True) -> typing.Optional[str]:
|
||||
vmState = self.get_machine_power_state(vmid)
|
||||
if vmState in (XenPowerState.suspended, XenPowerState.halted):
|
||||
return None # Already powered off, cannot reboot
|
||||
return (self.Async if as_async else self).VM.hard_reboot(vmid)
|
||||
|
||||
if asnc:
|
||||
return self.Async.VM.hard_reboot(vmId)
|
||||
return self.VM.hard_reboot(vmId)
|
||||
|
||||
def canSuspendVM(self, vmId: str) -> bool:
|
||||
operations = self.VM.get_allowed_operations(vmId)
|
||||
def can_suspend_machine(self, vmid: str) -> bool:
|
||||
operations = self.VM.get_allowed_operations(vmid)
|
||||
logger.debug('Operations: %s', operations)
|
||||
return 'suspend' in operations
|
||||
|
||||
def suspendVM(self, vmId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
vmState = self.getVMPowerState(vmId)
|
||||
if vmState == XenPowerState.suspended:
|
||||
def suspend_machine(self, vmid: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
vm_state = self.get_machine_power_state(vmid)
|
||||
if vm_state == XenPowerState.suspended:
|
||||
return None
|
||||
if asnc:
|
||||
return self.Async.VM.suspend(vmId)
|
||||
return self.VM.suspend(vmId)
|
||||
return (self.Async if as_async else self).VM.suspend(vmid)
|
||||
|
||||
def resumeVM(self, vmId: str, asnc: bool = True) -> typing.Optional[str]:
|
||||
vmState = self.getVMPowerState(vmId)
|
||||
if vmState != XenPowerState.suspended:
|
||||
def resume_machine(self, vmid: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
vm_state = self.get_machine_power_state(vmid)
|
||||
if vm_state != XenPowerState.suspended:
|
||||
return None
|
||||
if asnc:
|
||||
return self.Async.VM.resume(vmId, False, False)
|
||||
return self.VM.resume(vmId, False, False)
|
||||
return (self.Async if as_async else self).VM.resume(vmid, False, False)
|
||||
|
||||
def clone_vm(self, vmId: str, target_name: str, target_sr: typing.Optional[str] = None) -> str:
|
||||
def shutdown_machine(self, vmid: str, as_async: bool = True) -> typing.Optional[str]:
|
||||
vm_state = self.get_machine_power_state(vmid)
|
||||
if vm_state in (XenPowerState.suspended, XenPowerState.halted):
|
||||
return None
|
||||
return (self.Async if as_async else self).VM.clean_shutdown(vmid)
|
||||
|
||||
def clone_machine(self, vmId: str, target_name: str, target_sr: typing.Optional[str] = None) -> str:
|
||||
"""
|
||||
If target_sr is NONE:
|
||||
Clones the specified VM, making a new VM.
|
||||
@ -429,10 +499,10 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
def removeVM(self, vmId: str) -> None:
|
||||
def remove_machine(self, vmid: str) -> None:
|
||||
logger.debug('Removing machine')
|
||||
vdisToDelete = []
|
||||
for vdb in self.VM.get_VBDs(vmId):
|
||||
for vdb in self.VM.get_VBDs(vmid):
|
||||
vdi = None
|
||||
try:
|
||||
vdi = self.VBD.get_VDI(vdb)
|
||||
@ -447,11 +517,16 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
continue
|
||||
logger.debug('VDI to delete: %s', vdi)
|
||||
vdisToDelete.append(vdi)
|
||||
self.VM.destroy(vmId)
|
||||
self.VM.destroy(vmid)
|
||||
for vdi in vdisToDelete:
|
||||
self.VDI.destroy(vdi)
|
||||
|
||||
def configureVM(self, vmId: str, **kwargs):
|
||||
def configure_machine(
|
||||
self,
|
||||
vmid: str,
|
||||
mac: typing.Optional[dict[str, str]] = None,
|
||||
memory: typing.Optional[typing.Union[str, int]] = None,
|
||||
):
|
||||
"""
|
||||
Optional args:
|
||||
mac = { 'network': netId, 'mac': mac }
|
||||
@ -459,13 +534,11 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
|
||||
Mac address should be in the range 02:xx:xx:xx:xx (recommended, but not a "have to")
|
||||
"""
|
||||
mac: typing.Optional[dict[str, str]] = kwargs.get('mac', None)
|
||||
memory: typing.Optional[typing.Union[str, int]] = kwargs.get('memory', None)
|
||||
|
||||
# If requested mac address change
|
||||
try:
|
||||
if mac is not None:
|
||||
all_VIFs = self.VM.get_VIFs(vmId)
|
||||
all_VIFs: list[str] = self.VM.get_VIFs(vmid)
|
||||
if not all_VIFs:
|
||||
raise XenException('No Network interfaces found!')
|
||||
found = (all_VIFs[0], self.VIF.get_record(all_VIFs[0]))
|
||||
@ -491,24 +564,135 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
logger.debug('Setting up memory to %s MB', memory)
|
||||
# Convert memory to MB
|
||||
memory = str(int(memory) * 1024 * 1024)
|
||||
self.VM.set_memory_limits(vmId, memory, memory, memory, memory)
|
||||
self.VM.set_memory_limits(vmid, memory, memory, memory, memory)
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
def provisionVM(self, vmId: str, **kwargs):
|
||||
tags = self.VM.get_tags(vmId)
|
||||
def get_first_ip(
|
||||
self, vmid: str, ip_type: typing.Optional[typing.Union[typing.Literal['4'], typing.Literal['6']]] = None
|
||||
) -> str:
|
||||
"""Returns the first IP of the machine, or '' if not found"""
|
||||
try:
|
||||
guest_metrics = self.VM_guest_metrics.get_record(self.VM.get_guest_metrics(vmid))
|
||||
networks = guest_metrics.get('networks', {})
|
||||
# Networks has this format:
|
||||
# {'0/ip': '172.27.242.218',
|
||||
# '0/ipv4/0': '172.27.242.218',
|
||||
# '0/ipv6/1': 'fe80::a496:4ff:feca:404d',
|
||||
# '0/ipv6/0': '2a0c:5a81:2304:8100:a496:4ff:feca:404d'}
|
||||
if ip_type != '6':
|
||||
if '0/ip' in networks:
|
||||
return networks['0/ip']
|
||||
|
||||
for net_name in sorted(networks.keys()):
|
||||
if ip_type is None or f'/ipv{ip_type}/' in net_name:
|
||||
return networks[net_name]
|
||||
return ''
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
def get_first_mac(self, vmid: str) -> str:
|
||||
"""Returns the first MAC of the machine, or '' if not found"""
|
||||
try:
|
||||
vifs = self.VM.get_VIFs(vmid)
|
||||
if not vifs:
|
||||
return ''
|
||||
vif = self.VIF.get_record(vifs[0])
|
||||
return vif['MAC']
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
def provision_machine(self, vmid: str, as_async: bool = True) -> str:
|
||||
tags = self.VM.get_tags(vmid)
|
||||
try:
|
||||
del tags[tags.index(TAG_TEMPLATE)]
|
||||
except Exception: # nosec: ignored, maybe tag is not pressent
|
||||
pass
|
||||
tags.append(TAG_MACHINE)
|
||||
self.VM.set_tags(vmId, tags)
|
||||
self.VM.set_tags(vmid, tags)
|
||||
|
||||
if kwargs.get('asnc', True) is True:
|
||||
return self.Async.VM.provision(vmId)
|
||||
return self.VM.provision(vmId)
|
||||
if as_async:
|
||||
return self.Async.VM.provision(vmid)
|
||||
return self.VM.provision(vmid)
|
||||
|
||||
def convertToTemplate(self, vmId: str, shadowMultiplier: int = 4) -> None:
|
||||
def create_snapshot(self, vmid: str, name: str) -> str:
|
||||
try:
|
||||
return self.Async.VM.snapshot(vmid, name)
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
def restore_snapshot(self, snapshot_id: str) -> str:
|
||||
try:
|
||||
return self.Async.VM.snapshot_revert(snapshot_id)
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
def remove_snapshot(self, snapshot_id: str) -> str:
|
||||
try:
|
||||
return self.Async.VM.destroy(snapshot_id)
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
@cached(prefix='xen_snapshots', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_fnc=cache_key_helper)
|
||||
def list_snapshots(self, vmid: str, full_info: bool = False, **kwargs) -> list[dict[str, typing.Any]]:
|
||||
"""Returns a list of snapshots for the specified VM, sorted by snapshot_time in descending order.
|
||||
(That is, the most recent snapshot is first in the list.)
|
||||
|
||||
Args:
|
||||
vmid: The VM for which to list snapshots.
|
||||
full_info: If True, return full information about each snapshot. If False, return only the snapshot ID
|
||||
|
||||
Returns:
|
||||
A list of dictionaries, each containing the following keys:
|
||||
id: The snapshot ID.
|
||||
name: The snapshot name.
|
||||
"""
|
||||
try:
|
||||
snapshots = self.VM.get_snapshots(vmid)
|
||||
if not full_info:
|
||||
return [{'id': snapshot for snapshot in snapshots}]
|
||||
# Return full info, thatis, name, id and snapshot_time
|
||||
return_list: list[dict[str, typing.Any]] = []
|
||||
for snapshot in snapshots:
|
||||
return_list.append(
|
||||
{
|
||||
'id': snapshot,
|
||||
'name': self.VM.get_name_label(snapshot),
|
||||
'snapshot_time': self.VM.get_snapshot_time(snapshot),
|
||||
}
|
||||
)
|
||||
return sorted(return_list, key=lambda x: x['snapshot_time'], reverse=True)
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
@cached(prefix='xen_folders', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_fnc=cache_key_helper)
|
||||
def list_folders(self, **kwargs) -> list[str]:
|
||||
"""list "Folders" from the "Organizations View" of the XenServer
|
||||
|
||||
Returns:
|
||||
A list of 'folders' (organizations, str) in the XenServer
|
||||
"""
|
||||
folders: set[str] = set('/') # Add root folder for machines without folder
|
||||
for vm in self.list_machines():
|
||||
other_config = self.VM.get_other_config(vm['id'])
|
||||
folder: typing.Optional[str] = other_config.get('folder')
|
||||
if folder:
|
||||
folders.add(folder)
|
||||
return sorted(folders)
|
||||
|
||||
def get_machines_from_folder(
|
||||
self, folder: str, retrieve_names: bool = False
|
||||
) -> list[dict[str, typing.Any]]:
|
||||
result_list: list[dict[str, typing.Any]] = []
|
||||
for vm in self.list_machines():
|
||||
other_config = self.VM.get_other_config(vm['id'])
|
||||
if other_config.get('folder', '/') == folder:
|
||||
if retrieve_names:
|
||||
vm['name'] = self.VM.get_name_label(vm['id'])
|
||||
result_list.append(vm)
|
||||
return result_list
|
||||
|
||||
def convert_to_template(self, vmId: str, shadowMultiplier: int = 4) -> None:
|
||||
try:
|
||||
operations = self.VM.get_allowed_operations(vmId)
|
||||
logger.debug('Allowed operations: %s', operations)
|
||||
@ -533,11 +717,11 @@ class XenServer: # pylint: disable=too-many-public-methods
|
||||
except XenAPI.Failure as e:
|
||||
raise XenFailure(e.details)
|
||||
|
||||
def removeTemplate(self, templateId: str) -> None:
|
||||
self.removeVM(templateId)
|
||||
def remove_template(self, templateId: str) -> None:
|
||||
self.remove_machine(templateId)
|
||||
|
||||
def cloneTemplate(self, templateId: str, target_name: str) -> str:
|
||||
def start_deploy_from_template(self, templateId: str, target_name: str) -> str:
|
||||
"""
|
||||
After cloning template, we must deploy the VM so it's a full usable VM
|
||||
"""
|
||||
return self.clone_vm(templateId, target_name)
|
||||
return self.clone_machine(templateId, target_name)
|
||||
|
File diff suppressed because one or more lines are too long
@ -102,6 +102,6 @@
|
||||
</svg>
|
||||
</div>
|
||||
</uds-root>
|
||||
<script src="/uds/res/admin/runtime.js?stamp=1706650006" type="module"></script><script src="/uds/res/admin/polyfills.js?stamp=1706650006" type="module"></script><script src="/uds/res/admin/main.js?stamp=1706650006" type="module"></script></body>
|
||||
<script src="/uds/res/admin/runtime.js?stamp=1707019600" type="module"></script><script src="/uds/res/admin/polyfills.js?stamp=1707019600" type="module"></script><script src="/uds/res/admin/main.js?stamp=1707019600" type="module"></script></body>
|
||||
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user