diff --git a/server/src/uds/services/Proxmox/__init__.py b/server/src/uds/services/Proxmox/__init__.py index a4da375cf..02caaf0bc 100644 --- a/server/src/uds/services/Proxmox/__init__.py +++ b/server/src/uds/services/Proxmox/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012-2023 Virtual Cable S.L.U. +# Copyright (c) 2012-2023 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # diff --git a/server/src/uds/services/Proxmox/deployment_fixed.py b/server/src/uds/services/Proxmox/deployment_fixed.py index 0445053b4..3b346a818 100644 --- a/server/src/uds/services/Proxmox/deployment_fixed.py +++ b/server/src/uds/services/Proxmox/deployment_fixed.py @@ -12,7 +12,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -78,13 +78,13 @@ def reset(self) -> types.states.TaskState: self.service().provider().api.reset_vm(int(self._vmid)) except Exception: # nosec: if cannot reset, ignore it pass # If could not reset, ignore it... - + return types.states.TaskState.FINISHED def op_start(self) -> None: vminfo = self.service().get_vm_info(int(self._vmid)).validate() - if not vminfo.status.is_running(): + if not vminfo.status.is_running(): self._store_task(self.service().provider().api.start_vm(int(self._vmid))) # Check methods @@ -113,3 +113,8 @@ def op_start_checker(self) -> types.states.TaskState: Checks if machine has started """ return self._check_task_finished() + + def get_console_connection( + self, + ) -> types.services.ConsoleConnectionInfo | None: + return self.service().get_console_connection(self._vmid) diff --git a/server/src/uds/services/Proxmox/deployment_linked.py b/server/src/uds/services/Proxmox/deployment_linked.py index 83f21fb78..18af5b06c 100644 --- a/server/src/uds/services/Proxmox/deployment_linked.py +++ b/server/src/uds/services/Proxmox/deployment_linked.py @@ -12,7 +12,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -37,7 +37,6 @@ from uds.core import types from uds.core.services.generics.dynamic.userservice import DynamicUserService -from uds.core.managers.userservice import UserServiceManager from uds.core.util import autoserializable import uds.services.Proxmox.proxmox.exceptions @@ -46,7 +45,7 @@ # Not imported at runtime, just for type checking if typing.TYPE_CHECKING: - from .service_linked import ProxmoxServiceLinked + from .service import ProxmoxService from .publication import ProxmoxPublication logger = logging.getLogger(__name__) @@ -144,8 +143,8 @@ def _check_task_finished(self) -> types.states.TaskState: return types.states.TaskState.RUNNING - def service(self) -> 'ProxmoxServiceLinked': - return typing.cast('ProxmoxServiceLinked', super().service()) + def service(self) -> 'ProxmoxService': + return typing.cast('ProxmoxService', super().service()) def publication(self) -> 'ProxmoxPublication': pub = super().publication() @@ -204,22 +203,3 @@ def get_console_connection( self, ) -> typing.Optional[types.services.ConsoleConnectionInfo]: return self.service().get_console_connection(self._vmid) - - def desktop_login( - self, - username: str, - password: str, - domain: str = '', - ) -> None: - script = ( - 'import sys\n' - 'if sys.platform == "win32":\n' - 'from uds import operations\n' - f'''operations.writeToPipe("\\\\.\\pipe\\VDSMDPipe", struct.pack('!IsIs', 1, '{username}'.encode('utf8'), 2, '{password}'.encode('utf8')), True)''' - ) - # Post script to service - # operations.writeToPipe("\\\\.\\pipe\\VDSMDPipe", packet, True) - try: - UserServiceManager.manager().send_script(self.db_obj(), script) - except Exception as e: - logger.info('Exception sending loggin to %s: %s', self.db_obj(), e) diff --git a/server/src/uds/services/Proxmox/helpers.py b/server/src/uds/services/Proxmox/helpers.py index 39e3bae7f..83f06b6be 100644 --- a/server/src/uds/services/Proxmox/helpers.py +++ b/server/src/uds/services/Proxmox/helpers.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012-2023 Virtual Cable S.L.U. +# Copyright (c) 2012-2023 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -41,11 +41,13 @@ logger = logging.getLogger(__name__) + def get_provider(parameters: typing.Any) -> 'ProxmoxProvider': return typing.cast( 'ProxmoxProvider', models.Provider.objects.get(uuid=parameters['prov_uuid']).get_instance() ) + def get_storage(parameters: typing.Any) -> types.ui.CallbackResultType: logger.debug('Parameters received by getResources Helper: %s', parameters) provider = get_provider(parameters) @@ -59,8 +61,6 @@ def get_storage(parameters: typing.Any) -> types.ui.CallbackResultType: res: list[types.ui.ChoiceItem] = [] # Get storages for that datacenter for storage in sorted(provider.api.list_storages(node=vm_info.node), key=lambda x: int(not x.shared)): - if storage.type in ('lvm', 'iscsi', 'iscsidirect'): # does not allow differential storage (snapshots, etc.) - continue space, free = ( storage.avail / 1024 / 1024 / 1024, (storage.avail - storage.used) / 1024 / 1024 / 1024, diff --git a/server/src/uds/services/Proxmox/jobs.py b/server/src/uds/services/Proxmox/jobs.py index 19cc9c3fa..0c81f2bf1 100644 --- a/server/src/uds/services/Proxmox/jobs.py +++ b/server/src/uds/services/Proxmox/jobs.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012-2023 Virtual Cable S.L.U. +# Copyright (c) 2012-2023 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -56,7 +56,7 @@ class ProxmoxDeferredRemoval(jobs.Job): frecuency = 60 * 3 # Once every NN minutes friendly_name = 'Proxmox removal' counter = 0 - + def get_vmid_stored_data_from(self, data: bytes) -> typing.Tuple[int, bool]: vmdata = data.decode() if ':' in vmdata: @@ -66,7 +66,6 @@ def get_vmid_stored_data_from(self, data: bytes) -> typing.Tuple[int, bool]: vmid = vmdata try_graceful_shutdown = False return int(vmid), try_graceful_shutdown - # @staticmethod # def remove(provider_instance: 'provider.ProxmoxProvider', vmid: int, try_graceful_shutdown: bool) -> None: @@ -79,19 +78,19 @@ def get_vmid_stored_data_from(self, data: bytes) -> typing.Tuple[int, bool]: # vminfo = provider_instance.get_machine_info(vmid) # if vminfo.status == 'running': # if try_graceful_shutdown: - # # If running vm, simply try to shutdown + # # If running vm, simply try to shutdown # provider_instance.shutdown_machine(vmid) # # Store for later removal - # else: + # else: # # If running vm, simply stops it and wait for next # provider_instance.stop_machine(vmid) - + # store_for_deferred_removal() # return # provider_instance.remove_machine(vmid) # Try to remove, launch removal, but check later # store_for_deferred_removal() - + # except client.ProxmoxNotFound: # return # Machine does not exists # except Exception as e: @@ -132,7 +131,7 @@ def run(self) -> None: vmid, _try_graceful_shutdown = self.get_vmid_stored_data_from(data[1]) # In fact, here, _try_graceful_shutdown is not used, but we keep it for mayby future use # The soft shutdown has already being initiated by the remove method - + try: vm_info = instance.api.get_vm_info(vmid) logger.debug('Found %s for removal %s', vmid, data) diff --git a/server/src/uds/services/Proxmox/provider.py b/server/src/uds/services/Proxmox/provider.py index 79cde5f29..b01497250 100644 --- a/server/src/uds/services/Proxmox/provider.py +++ b/server/src/uds/services/Proxmox/provider.py @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -39,7 +39,7 @@ from uds.core.util.unique_id_generator import UniqueIDGenerator from .proxmox import client, types as prox_types, exceptions as prox_exceptions -from .service_linked import ProxmoxServiceLinked +from .service import ProxmoxService from .service_fixed import ProxmoxServiceFixed # Not imported at runtime, just for type checking @@ -64,7 +64,7 @@ class ProxmoxProvider(services.ServiceProvider): type_description = _('Proxmox platform service provider') icon_file = 'provider.png' - offers = [ProxmoxServiceLinked, ProxmoxServiceFixed] + offers = [ProxmoxService, ProxmoxServiceFixed] host = gui.TextField( length=64, @@ -161,6 +161,7 @@ def initialize(self, values: 'types.core.ValuesType') -> None: if values is not None: self.timeout.value = validators.validate_timeout(self.timeout.value) + self.macs_range.value = validators.validate_mac_range(self.macs_range.value) logger.debug(self.host.value) # All proxmox use same UniqueId generator, even if they are different servers diff --git a/server/src/uds/services/Proxmox/proxmox/__init__.py b/server/src/uds/services/Proxmox/proxmox/__init__.py index 84c48adf9..b00adb760 100644 --- a/server/src/uds/services/Proxmox/proxmox/__init__.py +++ b/server/src/uds/services/Proxmox/proxmox/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2019-2021 Virtual Cable S.L.U. +# Copyright (c) 2019-2021 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -27,4 +27,3 @@ """ Author: Adolfo Gómez, dkmaster at dkmon dot com """ - diff --git a/server/src/uds/services/Proxmox/proxmox/client.py b/server/src/uds/services/Proxmox/proxmox/client.py index ca15c8d2f..3373a26ec 100644 --- a/server/src/uds/services/Proxmox/proxmox/client.py +++ b/server/src/uds/services/Proxmox/proxmox/client.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2019-2021 Virtual Cable S.L.U. +# Copyright (c) 2019-2021 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -383,9 +383,18 @@ def clone_vm( # Ensure exists target pool, (id is in fact the name of the pool) if target_pool and not any(p.id == target_pool for p in self.list_pools()): raise exceptions.ProxmoxDoesNotExists(f'Pool "{target_pool}" does not exist') - + logger.debug('Cloning VM %s to %s', vmid, new_vmid) - logger.debug('Parameters: %s %s %s %s %s %s %s', name, description, as_linked_clone, target_node, target_storage, target_pool, must_have_vgpus) + logger.debug( + 'Parameters: %s %s %s %s %s %s %s', + name, + description, + as_linked_clone, + target_node, + target_storage, + target_pool, + must_have_vgpus, + ) src_node = vminfo.node @@ -403,13 +412,13 @@ def clone_vm( target_node = node.name else: target_node = src_node - - logger.debug('Target node: %s', target_node) + + logger.debug('Target node: %s', target_node) # Ensure exists target node if not any(n.name == target_node for n in self.get_cluster_info().nodes): raise exceptions.ProxmoxDoesNotExists(f'Node "{target_node}" does not exist') - + logger.debug('VM info: %s', vminfo) logger.debug('Type of vminfo.vgpu_type: %s', type(vminfo.vgpu_type)) logger.debug('Value of vminfo.vgpu_type: %s', vminfo.vgpu_type) @@ -537,7 +546,7 @@ def get_guest_ip_address( if found_ips: sorted_ips = sorted(found_ips, key=lambda x: ':' in x) return sorted_ips[0] - + except Exception as e: logger.warning('Error getting guest ip address for machine %s: %s', vmid, e) raise exceptions.ProxmoxError(f'No ip address for vm {vmid}: {e}') @@ -822,9 +831,10 @@ def list_storages( case collections.abc.Iterable(): node_list = set(node) + version = self.get_version() return sorted( [ - types.StorageInfo.from_dict(st_info) + types.StorageInfo.from_dict(st_info, version=version) for st_info in self.get_cluster_resources('storage') if st_info['node'] in node_list and (content is None or content in st_info['content']) ], diff --git a/server/src/uds/services/Proxmox/proxmox/consts.py b/server/src/uds/services/Proxmox/proxmox/consts.py index 76537a1c5..620c578b6 100644 --- a/server/src/uds/services/Proxmox/proxmox/consts.py +++ b/server/src/uds/services/Proxmox/proxmox/consts.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012-2023 Virtual Cable S.L.U. +# Copyright (c) 2012-2023 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -40,4 +40,4 @@ CACHE_INFO_DURATION: typing.Final[int] = consts.cache.SHORT_CACHE_TIMEOUT CACHE_VM_INFO_DURATION: typing.Final[int] = consts.cache.SHORTEST_CACHE_TIMEOUT # Cache duration is 3 minutes, so this is 60 mins * 24 = 1 day (default) -CACHE_DURATION_LONG: typing.Final[int] = consts.cache.EXTREME_CACHE_TIMEOUT \ No newline at end of file +CACHE_DURATION_LONG: typing.Final[int] = consts.cache.EXTREME_CACHE_TIMEOUT diff --git a/server/src/uds/services/Proxmox/proxmox/exceptions.py b/server/src/uds/services/Proxmox/proxmox/exceptions.py index ce34aecd5..922ac5389 100644 --- a/server/src/uds/services/Proxmox/proxmox/exceptions.py +++ b/server/src/uds/services/Proxmox/proxmox/exceptions.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012-2023 Virtual Cable S.L.U. +# Copyright (c) 2012-2023 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -43,9 +43,11 @@ class ProxmoxConnectionError(ProxmoxError, exceptions.services.generics.Retryabl class ProxmoxAuthError(ProxmoxError, exceptions.services.generics.FatalError): pass + class ProxmoxDoesNotExists(ProxmoxError): pass + class ProxmoxNotFound(ProxmoxDoesNotExists, exceptions.services.generics.NotFoundError): pass diff --git a/server/src/uds/services/Proxmox/proxmox/types.py b/server/src/uds/services/Proxmox/proxmox/types.py index ec5814903..67ce9f1a2 100644 --- a/server/src/uds/services/Proxmox/proxmox/types.py +++ b/server/src/uds/services/Proxmox/proxmox/types.py @@ -5,6 +5,8 @@ import re import typing +from django.utils import timezone + from . import exceptions as prox_exceptions NETWORK_RE: typing.Final[typing.Pattern[str]] = re.compile(r'([a-zA-Z0-9]+)=([^,]+)') # May have vla id at end @@ -169,7 +171,7 @@ def from_dict(data: collections.abc.MutableMapping[str, typing.Any]) -> 'ExecRes node=d[1], pid=int(d[2], 16), pstart=int(d[3], 16), - starttime=datetime.datetime.fromtimestamp(int(d[4], 16)), + starttime=timezone.make_aware(datetime.datetime.fromtimestamp(int(d[4], 16))), type=d[5], vmid=int(d[6]), user=d[7], @@ -182,7 +184,7 @@ def null() -> 'ExecResult': node='', pid=0, pstart=0, - starttime=datetime.datetime.now(), + starttime=timezone.localtime(), type='', vmid=0, user='', @@ -210,7 +212,7 @@ def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'T node=data['node'], pid=data['pid'], pstart=data['pstart'], - starttime=datetime.datetime.fromtimestamp(data['starttime']), + starttime=timezone.make_aware(datetime.datetime.fromtimestamp(data['starttime'])), type=data['type'], status=data['status'], exitstatus=data.get('exitstatus', ''), @@ -225,7 +227,7 @@ def done_task() -> 'TaskStatus': node='', pid=0, pstart=0, - starttime=datetime.datetime.now(), + starttime=timezone.localtime(), type='', status='stopped', exitstatus='OK', @@ -454,12 +456,15 @@ class StorageInfo: used: int avail: int total: int + _version: str = dataclasses.field(repr=False, compare=False, default='') def is_null(self) -> bool: return self.node == '' and self.storage == '' @staticmethod - def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'StorageInfo': + def from_dict( + dictionary: collections.abc.MutableMapping[str, typing.Any], version: str = '' + ) -> 'StorageInfo': if 'maxdisk' in dictionary: # From cluster/resources total = int(dictionary['maxdisk']) used = int(dictionary['disk']) @@ -482,6 +487,7 @@ def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'S used=used, avail=avail, total=total, + _version=version, ) @staticmethod @@ -498,6 +504,16 @@ def null() -> 'StorageInfo': total=0, ) + def supports_snapshots(self) -> bool: + # Only storage types that support snapshots are those that allow differential storage + # Note: On Promxmox 9, lvm allows snapshots + return self.type not in ('iscsi', 'iscsidirect', 'lvm') or (self._version >= '9' and self.type == 'lvm') + + def supports_linked_clone(self) -> bool: + # Only storage types that support snapshots are those that allow differential storage + # Note: On Promxmox 9, lvm allows snapshots + return self.type not in ('iscsi', 'iscsidirect', 'lvm') + @dataclasses.dataclass class PoolMemberInfo: @@ -527,7 +543,9 @@ class PoolInfo: members: list[PoolMemberInfo] @staticmethod - def from_dict(data: collections.abc.MutableMapping[str, typing.Any], *, poolid: typing.Optional[str] = None) -> 'PoolInfo': + def from_dict( + data: collections.abc.MutableMapping[str, typing.Any], *, poolid: typing.Optional[str] = None + ) -> 'PoolInfo': if 'members' in data: members: list[PoolMemberInfo] = [PoolMemberInfo.from_dict(i) for i in data['members']] else: diff --git a/server/src/uds/services/Proxmox/publication.py b/server/src/uds/services/Proxmox/publication.py index 4c1f3569a..df43e4c34 100644 --- a/server/src/uds/services/Proxmox/publication.py +++ b/server/src/uds/services/Proxmox/publication.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012-2024 Virtual Cable S.L.U. +# Copyright (c) 2012-2024 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -39,7 +39,7 @@ # Not imported at runtime, just for type checking if typing.TYPE_CHECKING: - from .service_linked import ProxmoxServiceLinked + from .service import ProxmoxService logger = logging.getLogger(__name__) @@ -55,8 +55,8 @@ class ProxmoxPublication(DynamicPublication, autoserializable.AutoSerializable): _task = autoserializable.StringField(default='') # Utility overrides for type checking... - def service(self) -> 'ProxmoxServiceLinked': - return typing.cast('ProxmoxServiceLinked', super().service()) + def service(self) -> 'ProxmoxService': + return typing.cast('ProxmoxService', super().service()) def unmarshal(self, data: bytes) -> None: """ diff --git a/server/src/uds/services/Proxmox/service_linked.py b/server/src/uds/services/Proxmox/service.py similarity index 81% rename from server/src/uds/services/Proxmox/service_linked.py rename to server/src/uds/services/Proxmox/service.py index 13b5d3aa5..08d10f0c4 100644 --- a/server/src/uds/services/Proxmox/service_linked.py +++ b/server/src/uds/services/Proxmox/service.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012-2022 Virtual Cable S.L.U. +# Copyright (c) 2012-2022 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -35,7 +35,7 @@ from django.utils.translation import gettext_noop as _ -from uds.core import types +from uds.core import types, exceptions from uds.core.services.generics.dynamic.publication import DynamicPublication from uds.core.services.generics.dynamic.service import DynamicService from uds.core.services.generics.dynamic.userservice import DynamicUserService @@ -57,9 +57,9 @@ logger = logging.getLogger(__name__) -class ProxmoxServiceLinked(DynamicService): +class ProxmoxService(DynamicService): """ - Proxmox Linked clones service. This is based on creating a template from selected vm, and then use it to + Proxmox clones service. This is based on creating a template from selected vm, and then use it to Notes: * We do not suspend machines, we try to shutdown them gracefully @@ -68,11 +68,11 @@ class ProxmoxServiceLinked(DynamicService): # : 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) - type_name = _('Proxmox Linked Clone') + type_name = _('Proxmox Clone') # : Type used internally to identify this provider, must not be modified once created type_type = 'ProxmoxLinkedService' # : Description shown at administration interface for this provider - type_description = _('Proxmox Services based on templates and COW') + type_description = _('Proxmox Services based on templates') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using gettext_noop) @@ -142,10 +142,18 @@ class ProxmoxServiceLinked(DynamicService): required=True, ) + use_full_clone = gui.CheckBoxField( + label=_('Use full clone'), + default=False, + order=111, + tab=types.ui.Tab.MACHINE, + required=True, + ) + datastore = gui.ChoiceField( label=_("Storage"), readonly=False, - order=111, + order=112, tooltip=_('Storage for publications & machines.'), tab=types.ui.Tab.MACHINE, required=True, @@ -154,12 +162,12 @@ class ProxmoxServiceLinked(DynamicService): gpu = gui.ChoiceField( label=_("GPU Availability"), readonly=False, - order=112, - choices={ - '0': _('Do not check'), - '1': _('Only if available'), - '2': _('Only if NOT available'), - }, + order=113, + choices=[ + gui.choice_item('0', _('Do not check')), + gui.choice_item('1', _('Only if available')), + gui.choice_item('2', _('Only if NOT available')), + ], tooltip=_('Checking method for GPU availability'), tab=types.ui.Tab.MACHINE, required=True, @@ -175,6 +183,17 @@ def initialize(self, values: 'types.core.ValuesType') -> None: self.basename.value = validators.validate_basename( self.basename.value, length=self.lenname.as_int() ) + # Do not allow linked clones on lvm-thin + try: + storage = next( + filter(lambda x: x.storage == self.datastore.value, self.provider().api.list_storages()) + ) + except StopIteration: + raise exceptions.ui.ValidationError(_('Selected storage not found on Proxmox')) + if not storage.supports_linked_clone() and not self.use_full_clone.value: + # if storage does not support linked clones and user wants to use linked clones, raise an error + raise exceptions.ui.ValidationError(_('Linked clones are not allowed on the storage')) + # if int(self.memory.value) < 128: # raise exceptions.ValidationException(_('The minimum allowed memory is 128 Mb')) @@ -219,24 +238,19 @@ def find_duplicates(self, name: str, mac: str) -> collections.abc.Iterable[str]: def clone_vm(self, name: str, description: str, vmid: int = -1) -> 'prox_types.VmCreationResult': name = self.sanitized_name(name) pool = self.pool.value or None - if vmid == -1: # vmId == -1 if cloning for template - return self.provider().clone_vm( - self.machine.as_int(), - name, - description, - as_linked_clone=False, - target_storage=self.datastore.value, - target_pool=pool, - ) + clone_vm_args: dict[str, typing.Any] = { + 'must_have_vgpus': {'1': True, '2': False}.get(self.gpu.value, None) + } + use_linked_clones = not self.use_full_clone.value and vmid > 0 return self.provider().clone_vm( - vmid, + self.machine.as_int() if vmid < 0 else vmid, name, description, - as_linked_clone=True, + as_linked_clone=use_linked_clones, target_storage=self.datastore.value, target_pool=pool, - must_have_vgpus={'1': True, '2': False}.get(self.gpu.value, None), + **clone_vm_args if use_linked_clones else {}, ) def get_vm_info(self, vmid: int) -> 'prox_types.VMInfo': @@ -261,20 +275,18 @@ def get_macs_range(self) -> str: def is_ha_enabled(self) -> bool: return self.ha.value != '__' - def get_console_connection(self, vmid: str) -> typing.Optional[types.services.ConsoleConnectionInfo]: + def get_console_connection(self, vmid: str) -> types.services.ConsoleConnectionInfo | None: return self.provider().api.get_console_connection(int(vmid)) - def is_avaliable(self) -> bool: + def is_available(self) -> bool: return self.provider().is_available() - def get_ip( - self, caller_instance: typing.Optional['DynamicUserService | DynamicPublication'], vmid: str - ) -> str: + def get_ip(self, caller_instance: 'DynamicUserService | DynamicPublication | None', vmid: str) -> str: return self.provider().api.get_guest_ip_address(int(vmid)) def get_mac( self, - caller_instance: typing.Optional['DynamicUserService | DynamicPublication'], + caller_instance: 'DynamicUserService | DynamicPublication | None', vmid: str, *, for_unique_id: bool = False, @@ -286,9 +298,7 @@ def get_mac( return self.mac_generator().get(self.get_macs_range()) return self.provider().api.get_vm_config(int(vmid)).networks[0].macaddr.lower() - def start( - self, caller_instance: typing.Optional['DynamicUserService | DynamicPublication'], vmid: str - ) -> None: + def start(self, caller_instance: 'DynamicUserService | DynamicPublication | None', vmid: str) -> None: if isinstance(caller_instance, ProxmoxUserserviceLinked): if self.is_running(caller_instance, vmid): # If running, skip caller_instance._task = '' @@ -297,9 +307,7 @@ def start( else: self.provider().api.start_vm(int(vmid)) - def stop( - self, caller_instance: typing.Optional['DynamicUserService | DynamicPublication'], vmid: str - ) -> None: + def stop(self, caller_instance: 'DynamicUserService | DynamicPublication | None', vmid: str) -> None: if isinstance(caller_instance, ProxmoxUserserviceLinked): if self.is_running(caller_instance, vmid): caller_instance._store_task(self.provider().api.stop_vm(int(vmid))) @@ -308,9 +316,7 @@ def stop( else: self.provider().api.stop_vm(int(vmid)) - def shutdown( - self, caller_instance: typing.Optional['DynamicUserService | DynamicPublication'], vmid: str - ) -> None: + def shutdown(self, caller_instance: 'DynamicUserService | DynamicPublication | None', vmid: str) -> None: if isinstance(caller_instance, ProxmoxUserserviceLinked): if self.is_running(caller_instance, vmid): caller_instance._store_task(self.provider().api.shutdown_vm(int(vmid))) @@ -319,9 +325,7 @@ def shutdown( else: self.provider().api.shutdown_vm(int(vmid)) # Just shutdown it, do not stores anything - def is_running( - self, caller_instance: typing.Optional['DynamicUserService | DynamicPublication'], vmid: str - ) -> bool: + def is_running(self, caller_instance: 'DynamicUserService | DynamicPublication | None', vmid: str) -> bool: # Raise an exception if fails to get machine info return self.get_vm_info(int(vmid)).validate().status.is_running() diff --git a/server/src/uds/services/Proxmox/service_fixed.py b/server/src/uds/services/Proxmox/service_fixed.py index 9c4eefad4..65c87fc9b 100644 --- a/server/src/uds/services/Proxmox/service_fixed.py +++ b/server/src/uds/services/Proxmox/service_fixed.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012-2022 Virtual Cable S.L.U. +# Copyright (c) 2012-2022 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -10,7 +10,7 @@ # * 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 +# * Neither the name of Virtual Cable S.L. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -93,7 +93,7 @@ class ProxmoxServiceFixed(FixedService): # pylint: disable=too-many-public-meth machines = FixedService.machines use_snapshots = FixedService.use_snapshots - + maintain_on_error = FixedService.maintain_on_error prov_uuid = gui.HiddenField(value=None) @@ -115,12 +115,15 @@ def init_gui(self) -> None: def provider(self) -> 'ProxmoxProvider': return typing.cast('ProxmoxProvider', super().provider()) - def is_avaliable(self) -> bool: + def get_console_connection(self, vmid: str) -> types.services.ConsoleConnectionInfo | None: + return self.provider().api.get_console_connection(int(vmid)) + + def is_available(self) -> bool: return self.provider().is_available() def get_vm_info(self, vmid: int) -> 'prox_types.VMInfo': return self.provider().api.get_vm_info(vmid).validate() - + def is_ready(self, vmid: str) -> bool: return self.provider().api.get_vm_info(int(vmid)).validate().status.is_running() @@ -129,7 +132,9 @@ def enumerate_assignables(self) -> collections.abc.Iterable[types.ui.ChoiceItem] # Only machines that already exists on proxmox and are not already assigned vms: dict[int, str] = {} - for member in self.provider().api.get_pool_info(self.pool.value.strip(), retrieve_vm_names=True).members: + for member in ( + self.provider().api.get_pool_info(self.pool.value.strip(), retrieve_vm_names=True).members + ): vms[member.vmid] = member.vmname with self._assigned_access() as assigned_vms: @@ -180,7 +185,9 @@ def snapshot_recovery(self, userservice_instance: FixedUserService) -> None: self.provider().api.restore_snapshot(vmid, name=snapshot.name) ) except Exception as e: - self.do_log(types.log.LogLevel.WARNING, 'Could not restore SNAPSHOT for this VM. ({})'.format(e)) + self.do_log( + types.log.LogLevel.WARNING, 'Could not restore SNAPSHOT for this VM. ({})'.format(e) + ) def get_and_assign(self) -> str: found_vmid: typing.Optional[str] = None