1
0
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:
Adolfo Gómez García 2024-02-04 05:12:44 +01:00
parent e921c76530
commit 34005c2af3
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
16 changed files with 1039 additions and 358 deletions

View File

@ -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:
"""

View File

@ -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]:

View File

@ -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():

View File

@ -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

View 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()

View 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],
}
]

View File

@ -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))]

View File

@ -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)

View File

@ -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)

View 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)

View File

@ -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

View File

@ -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>