From a505682d58c0b695f812e68992f89d0a0c1f2541 Mon Sep 17 00:00:00 2001 From: aschumann-virtualcable Date: Wed, 18 Mar 2026 18:11:31 +0100 Subject: [PATCH] Refactors Proxmox services to unify service type and storage logic Consolidates linked clone service implementation by removing redundant code and references, streamlining type hints and imports to use a single service class. Enhances storage selection logic to support snapshot capabilities based on Proxmox version, ensuring compatibility and correctness for linked clones. Improves timestamp handling with timezone awareness for better reliability. Addresses copyright formatting and minor code cleanups for clarity and maintainability. --- server/src/uds/services/Proxmox/__init__.py | 4 +- .../uds/services/Proxmox/deployment_fixed.py | 11 ++- .../uds/services/Proxmox/deployment_linked.py | 28 +----- server/src/uds/services/Proxmox/helpers.py | 8 +- server/src/uds/services/Proxmox/jobs.py | 17 ++-- server/src/uds/services/Proxmox/provider.py | 7 +- .../uds/services/Proxmox/proxmox/__init__.py | 5 +- .../uds/services/Proxmox/proxmox/client.py | 28 ++++-- .../uds/services/Proxmox/proxmox/consts.py | 6 +- .../services/Proxmox/proxmox/exceptions.py | 6 +- .../src/uds/services/Proxmox/proxmox/types.py | 30 ++++-- .../src/uds/services/Proxmox/publication.py | 10 +- .../Proxmox/{service_linked.py => service.py} | 92 ++++++++++--------- .../src/uds/services/Proxmox/service_fixed.py | 21 +++-- 14 files changed, 149 insertions(+), 124 deletions(-) rename server/src/uds/services/Proxmox/{service_linked.py => service.py} (81%) 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