1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-03-12 04:58:34 +03:00

Adding new specialization and remaking Proxmox conector based on this.

Also have left old proxmox while testing aroung (As ProxmoxOrig, but with typenames changed to avoid collisions). Will be removed as soon as all is working fine
This commit is contained in:
Adolfo Gómez García 2024-03-19 04:42:33 +01:00
parent 4fb1da1554
commit 5c9bf779e3
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
27 changed files with 4605 additions and 984 deletions

View File

@ -465,6 +465,11 @@ LOGGING = {
'level': LOGLEVEL,
'propagate': False,
},
'uds.core.services': {
'handlers': ['servicesFile'],
'level': LOGLEVEL,
'propagate': False,
},
# Custom Auth log
'authLog': {
'handlers': ['authFile'],

View File

@ -179,7 +179,9 @@ class UserServiceManager(metaclass=singleton.Singleton):
in_use=False,
)
def create_cache_for(self, publication: ServicePoolPublication, cache_level: types.services.CacheLevel) -> UserService:
def create_cache_for(
self, publication: ServicePoolPublication, cache_level: types.services.CacheLevel
) -> UserService:
"""
Creates a new cache for the deployed service publication at level indicated
"""
@ -314,20 +316,18 @@ class UserServiceManager(metaclass=singleton.Singleton):
operations_logger.info('Canceling userService %s', user_service.name)
user_service_instance = user_service.get_instance()
if (
not user_service_instance.supports_cancel()
): # Does not supports cancel, but destroy, so mark it for "later" destroy
# State is kept, just mark it for destroy after finished preparing
user_service.destroy_after = True
else:
user_service.set_state(State.CANCELING)
# We simply notify service that it should cancel operation
state = user_service_instance.cancel()
# We have fixed cancelling
# previuously, we only allows cancelling if cancel method
# was overrided, but now, we invoke cancel in any case
# And will let the service to decide if it can cancel, delay it or whatever
user_service.set_state(State.CANCELING)
# We simply notify service that it should cancel operation
state = user_service_instance.cancel()
# Data will be serialized on makeUnique process
# If cancel is not supported, base cancel always returns "FINISHED", and
# opchecker will set state to "removable"
UserServiceOpChecker.make_unique(user_service, user_service_instance, state)
# Data will be serialized on makeUnique process
# If cancel is not supported, base cancel always returns "FINISHED", and
# opchecker will set state to "removable"
UserServiceOpChecker.make_unique(user_service, user_service_instance, state)
def remove(self, userservice: UserService) -> None:
"""
@ -606,13 +606,13 @@ class UserServiceManager(metaclass=singleton.Singleton):
except Exception:
logger.exception('Reseting service')
return
logger.debug('State: %s', state)
if state == types.states.TaskState.FINISHED:
user_service.update_data(userservice_instance)
return
UserServiceOpChecker.make_unique(user_service, userservice_instance, state)
def notify_preconnect(self, user_service: UserService, info: types.connections.ConnectionData) -> None:
@ -698,7 +698,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
if kind in 'A': # This is an assigned service
logger.debug('Getting A service %s', uuid_user_service)
userservice = UserService.objects.get(uuid=uuid_user_service, user=user)
typing.cast(UserService, userservice).deployed_service.validate_user(user) # pyright: ignore reportGeneralTypeIssues # Mypy thinks that userservice is None
userservice.deployed_service.validate_user(user)
else:
try:
service_pool: ServicePool = ServicePool.objects.get(uuid=uuid_user_service)
@ -752,7 +752,9 @@ class UserServiceManager(metaclass=singleton.Singleton):
)
# Early log of "access try" so we can imagine what is going on
user_service.set_connection_source(types.connections.ConnectionSource(src_ip, client_hostname or src_ip))
user_service.set_connection_source(
types.connections.ConnectionSource(src_ip, client_hostname or src_ip)
)
if user_service.is_in_maintenance():
raise ServiceInMaintenanceMode()
@ -793,7 +795,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
if not validate_with_test:
# traceLogger.info('GOT service "{}" for user "{}" with transport "{}" (NOT TESTED)'.format(userService.name, userName, trans.name))
return None, user_service, None, transport, None
service_status: types.services.ReadyStatus = types.services.ReadyStatus.USERSERVICE_NOT_READY
ip = 'unknown'
# Test if the service is ready

View File

@ -295,7 +295,6 @@ class Publication(Environmentable, Serializable):
"""
raise NotImplementedError(f'destroy method for class {self.__class__.__name__} not provided!')
@abc.abstractmethod
def cancel(self) -> types.states.TaskState:
"""
This is a task method. As that, the expected return values are
@ -313,8 +312,9 @@ class Publication(Environmentable, Serializable):
all exceptions, and never raise an exception from these methods
to the core. Take that into account and handle exceptions inside
this method.
Defaults to calling destroy method (previously was fully abstract, but this is a good default)
"""
raise NotImplementedError(f'cancel method for class {self.__class__.__name__} not provided!')
return self.destroy()
def __str__(self) -> str:
"""

View File

@ -1,218 +1,497 @@
# # -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# #
# # Copyright (c) 2012-2019 Virtual Cable S.L.
# # All rights reserved.
# #
# """
# Author: Adolfo Gómez, dkmaster at dkmon dot com
# """
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
# import abc
# from datetime import datetime
# import logging
# import time
# import typing
import abc
import collections.abc
import logging
import time
import typing
# from django.utils.translation import gettext as _
# from uds.core import services, types
# from uds.core.types.services import Operation
# from uds.core.util import autoserializable
from django.utils.translation import gettext as _
from uds.core import services, types
from uds.core.types.services import Operation
from uds.core.util import autoserializable
# if typing.TYPE_CHECKING:
# from .dynamic_service import DynamicService
if typing.TYPE_CHECKING:
from .dynamic_service import DynamicService
# class DynamicPublication(services.Publication, autoserializable.AutoSerializable, abc.ABC):
# suggested_delay = 20 # For publications, we can check every 20 seconds
logger = logging.getLogger(__name__)
# _name = autoserializable.StringField(default='')
# _vm = autoserializable.StringField(default='')
# _queue = autoserializable.ListField[Operation]()
# # Utility overrides for type checking...
# def _current_op(self) -> Operation:
# if not self._queue:
# return Operation.FINISH
class DynamicPublication(services.Publication, autoserializable.AutoSerializable, abc.ABC):
# Very simmilar to DynamicUserService, but with some differences
suggested_delay = 20 # For publications, we can check every 20 seconds
# return self._queue[0]
# Some customization fields
# How many times we will check for a state before giving up
max_state_checks: typing.ClassVar[int] = 20
# If must wait untill finish queue for destroying the machine
wait_until_finish_to_destroy: typing.ClassVar[bool] = False
_name = autoserializable.StringField(default='')
_vmid = autoserializable.StringField(default='')
_queue = autoserializable.ListField[Operation]()
_reason = autoserializable.StringField(default='')
_is_flagged_for_destroy = autoserializable.BoolField(default=False)
_publish_queue: typing.ClassVar[list[Operation]] = [
Operation.INITIALIZE,
Operation.CREATE,
Operation.CREATE_COMPLETED,
Operation.FINISH,
]
_destroy_queue: typing.ClassVar[list[Operation]] = [
Operation.REMOVE,
Operation.REMOVE_COMPLETED,
Operation.FINISH,
]
# Utility overrides for type checking...
def _reset_checks_counter(self) -> None:
with self.storage.as_dict() as data:
data['exec_count'] = 0
def _inc_checks_counter(self, info: typing.Optional[str] = None) -> typing.Optional[types.states.TaskState]:
with self.storage.as_dict() as data:
count = data.get('exec_count', 0) + 1
data['exec_count'] = count
if count > self.max_state_checks:
return self._error(f'Max checks reached on {info or "unknown"}')
return None
def _current_op(self) -> Operation:
if not self._queue:
return Operation.FINISH
return self._queue[0]
def _error(self, reason: typing.Union[str, Exception]) -> types.states.TaskState:
"""
Internal method to set object as error state
Returns:
State.ERROR, so we can do "return self._error(reason)"
"""
reason = str(reason)
logger.error(reason)
if self._vmid:
try:
# TODO: Remove VM using service or put it on a "to be removed" queue for a parallel job
self._vmid = ''
except Exception as e:
logger.exception('Exception removing machine: %s', e)
self._queue = [Operation.ERROR]
self._reason = reason
return types.states.TaskState.ERROR
def service(self) -> 'DynamicService':
return typing.cast('DynamicService', super().service())
def check_space(self) -> bool:
"""
If the service needs to check space before publication, it should override this method
"""
return True
@abc.abstractmethod
def publish(self) -> types.states.TaskState:
""" """
self._queue = self._publish_queue
return self._execute_queue()
def _execute_queue(self) -> types.states.TaskState:
op = self._current_op()
if op == Operation.ERROR:
return types.states.TaskState.ERROR
if op == Operation.FINISH:
return types.states.TaskState.FINISHED
try:
self._reset_checks_counter() # Reset checks counter
# For custom operations, we will call the only one method
if op.is_custom():
self.op_custom(op)
else:
operation_runner = _EXECUTORS[op]
# Invoke using instance, we have overrided methods
# and we want to use the overrided ones
getattr(self, operation_runner.__name__)()
return types.states.TaskState.RUNNING
except Exception as e:
logger.exception('Unexpected FixedUserService exception: %s', e)
return self._error(str(e))
def check_state(self) -> types.states.TaskState:
"""
Check what operation is going on, and acts acordly to it
"""
self._debug('check_state')
op = self._current_op()
if op == Operation.ERROR:
return types.states.TaskState.ERROR
if op == Operation.FINISH:
if self.wait_until_finish_to_destroy and self._is_flagged_for_destroy:
self._is_flagged_for_destroy = False
self._queue = [Operation.FINISH] # For destroy to start "clean"
return self.destroy()
return types.states.TaskState.FINISHED
if op != Operation.WAIT:
# All operations except WAIT will check against checks counter
state = self._inc_checks_counter(self._op2str(op))
if state is not None:
return state # Error, Finished or None
try:
if op.is_custom():
state = self.op_custom_checker(op)
else:
state = _CHECKERS[op](self)
if state == types.states.TaskState.FINISHED:
# Remove runing op
self._queue.pop(0)
return self._execute_queue()
return state
except Exception as e:
return self._error(e)
@typing.final
def destroy(self) -> types.states.TaskState:
"""
Destroys the publication (or cancels it if it's in the middle of a creation process)
"""
self._is_flagged_for_destroy = False # Reset flag
op = self._current_op()
if op == Operation.ERROR:
return self._error('Machine is already in error state!')
# If a "paused" state, reset queue to destroy
if op == Operation.FINISH:
self._queue = self._destroy_queue
return self._execute_queue()
# If must wait until finish, flag for destroy and wait
if self.wait_until_finish_to_destroy:
self._is_flagged_for_destroy = True
else:
# If other operation, wait for finish before destroying
self._queue = [op] + self._destroy_queue
# Do not execute anything.here, just continue normally
return types.states.TaskState.RUNNING
# def service(self) -> 'DynamicService':
# return typing.cast('DynamicService', super().service())
# def check_space(self) -> bool:
# """
# If the service needs to check space before publication, it should override this method
# """
# return True
def error_reason(self) -> str:
return self._reason
# def publish(self) -> types.states.TaskState:
# """
# """
# try:
# # First we should create a full clone, so base machine do not get fullfilled with "garbage" delta disks...
# self._name = self.service().sanitize_machine_name(
# 'UDS Pub'
# + ' '
# + self.servicepool_name()
# + "-"
# + str(self.revision()) # plus current time, to avoid name collisions
# + "-"
# + f'{int(time.time())%256:2X}'
# )
# comments = _('UDS Publication for {0} created at {1}').format(
# self.servicepool_name(), str(datetime.now()).split('.')[0]
# )
# self._state = types.states.State.RUNNING
# self._operation = 'p' # Publishing
# self._destroy_after = False
# return types.states.TaskState.RUNNING
# except Exception as e:
# logger.exception('Caught exception %s', e)
# self._reason = str(e)
# return types.states.TaskState.ERROR
# def _execute_queue(self) -> types.states.TaskState:
# op = self._current_op()
# if op == Operation.ERROR:
# return types.states.TaskState.ERROR
# Execution methods
# Every Operation has an execution method and a check method
def op_initialize(self) -> None:
"""
This method is called when the service is initialized
"""
if self.check_space() is False:
raise Exception('Not enough space to publish')
# if op == Operation.FINISH:
# return types.states.TaskState.FINISHED
# First we should create a full clone, so base machine do not get fullfilled with "garbage" delta disks...
# Add a number based on current time to avoid collisions
self._name = self.service().sanitize_machine_name(
f'UDS-Pub {self.servicepool_name()}-{int(time.time())%256:2X} {self.revision()}'
)
self._is_flagged_for_destroy = False
# def check_state(
# self,
# ) -> types.states.State: # pylint: disable = too-many-branches,too-many-return-statements
# if self._state != types.states.State.RUNNING:
# return types.states.State.from_str(self._state)
@abc.abstractmethod
def op_create(self) -> None:
"""
This method is called when the service is created
At least, we must provide this method
"""
...
# task = self.service().get_task_info(self._task)
# trans: typing.Dict[str, str] = {
# VMWTask.ERROR: types.states.State.ERROR,
# VMWTask.RUNNING: types.states.State.RUNNING,
# VMWTask.FINISHED: types.states.State.FINISHED,
# VMWTask.UNKNOWN_TASK: types.states.State.ERROR,
# }
# reasons: typing.Dict[str, str] = {
# VMWTask.ERROR: 'Error',
# VMWTask.RUNNING: 'Already running',
# VMWTask.FINISHED: 'Finished',
# VMWTask.UNKNOWN_TASK: 'Task not known by VC',
# }
def op_create_completed(self) -> None:
"""
This method is called when the service creation is completed
"""
pass
# try:
# st = task.state() or VMWTask.UNKNOWN_TASK
# except TypeError as e:
# logger.exception(
# 'Catch exception invoking vmware, delaying request: %s %s',
# e.__class__,
# e,
# )
# return types.states.State.from_str(self._state)
# except Exception as e:
# logger.exception('Catch exception invoking vmware: %s %s', e.__class__, e)
# self._state = types.states.State.ERROR
# self._reason = str(e)
# return self._state
# self._reason = reasons[st]
# self._state = trans[st]
# if self._state == types.states.State.ERROR:
# self._reason = task.result() or 'Publication not found!'
# elif self._state == types.states.State.FINISHED:
# if self._operation == 'x': # Destroying snapshot
# return self._remove_machine()
# if self._operation == 'p':
# self._vm = str(task.result() or '')
# # Now make snapshot
# if self._destroy_after:
# return self.destroy()
def op_start(self) -> None:
"""
This method is called when the service is started
"""
self.service().start_machine(self, self._vmid)
# if self.isFullCloner() is True: # If full cloner is our father
# self._snapshot = ''
# self._task = ''
# self._state = types.states.State.FINISHED
# return self._state
def op_start_completed(self) -> None:
"""
This method is called when the service start is completed
"""
pass
# try:
# comments = 'UDS Snapshot created at ' + str(datetime.now())
# self._task = self.service().create_snapshot(self._vm, SNAPNAME, comments)
# self._state = types.states.State.RUNNING
# self._operation = 's' # Snapshoting
# except Exception as e:
# self._state = types.states.State.ERROR
# self._reason = str(e)
# logger.exception('Exception caught creating snapshot')
# elif self._operation == 's':
# self._snapshot = task.result() or ''
# if (
# self._destroy_after
# ): # If publishing and was canceled or destroyed before finishing, do it now
# return self.destroy()
# else:
# self._snapshot = ''
# return types.states.State.from_str(self._state)
def op_stop(self) -> None:
"""
This method is called for stopping the service
"""
self.service().stop_machine(self, self._vmid)
# def finish(self) -> None:
# self._task = ''
# self._destroy_after = False
def op_stop_completed(self) -> None:
"""
This method is called when the service stop is completed
"""
pass
# def destroy(self) -> types.states.State:
# if (
# self._state == types.states.State.RUNNING and self._destroy_after is False
# ): # If called destroy twice, will BREAK STOP publication
# self._destroy_after = True
# return types.states.State.RUNNING
# self._destroy_after = False
# # if self.snapshot != '':
# # return self.__removeSnapshot()
# return self._remove_machine()
def op_shutdown(self) -> None:
"""
This method is called for shutdown the service
"""
self.service().shutdown_machine(self, self._vmid)
# def cancel(self) -> types.states.State:
# return self.destroy()
def op_shutdown_completed(self) -> None:
"""
This method is called when the service shutdown is completed
"""
pass
# def error_reason(self) -> str:
# return self._reason
def op_suspend(self) -> None:
"""
This method is called for suspend the service
"""
# Note that by default suspend is "shutdown" and not "stop" because we
self.service().suspend_machine(self, self._vmid)
# def snapshot_reference(self) -> str:
# return self.service().provider().get_current_snapshot(self._vm) or 'invalid-snapshot'
# # return self.snapshot
def op_suspend_completed(self) -> None:
"""
This method is called when the service suspension is completed
"""
pass
# def machine_reference(self) -> str:
# return self._vm
def op_reset(self) -> None:
"""
This method is called when the service is reset
"""
pass
# def _remove_machine(self) -> types.states.State:
# if not self._vm:
# logger.error("Machine reference not found!!")
# return types.states.State.ERROR
# try:
# self._task = self.service().remove_machine(self._vm)
# self._state = types.states.State.RUNNING
# self._operation = 'd'
# self._destroy_after = False
# return types.states.State.RUNNING
# except Exception as e:
# # logger.exception("Caught exception at __removeMachine %s:%s", e.__class__, e)
# logger.error('Error removing machine: %s', e)
# self._reason = str(e)
# return types.states.State.ERROR
def op_reset_completed(self) -> None:
"""
This method is called when the service reset is completed
"""
self.service().reset_machine(self, self._vmid)
# def unmarshal(self, data: bytes) -> None:
# if autoserializable.is_autoserializable_data(data):
# return super().unmarshal(data)
def op_remove(self) -> None:
"""
This method is called when the service is removed
By default, we need a remove machine on the service, use it
"""
self.service().remove_machine(self, self._vmid)
# _auto_data = OldSerialData()
# _auto_data.unmarshal(data)
def op_remove_completed(self) -> None:
"""
This method is called when the service removal is completed
"""
pass
# # Fill own data from restored data
# self._name = _auto_data._name
# self._vm = _auto_data._vm
# self._snapshot = _auto_data._snapshot
# self._task = _auto_data._task
# self._state = _auto_data._state
# self._operation = _auto_data._operation
# self._destroy_after = _auto_data._destroyAfter
# self._reason = _auto_data._reason
def op_wait(self) -> None:
"""
This method is called when the service is waiting
Basically, will stop the execution of the queue until something external changes it (i.e. poping from the queue)
Executor does nothing
"""
pass
# # Flag for upgrade
# self.mark_for_upgrade(True)
def op_nop(self) -> None:
"""
This method is called when the service is doing nothing
This does nothing, as it's a NOP operation
"""
pass
def op_custom(self, operation: Operation) -> None:
"""
This method is called when the service is doing a custom operation
"""
pass
# ERROR, FINISH and UNKNOWN are not here, as they are final states not needing to be executed
def op_initialize_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service is initialized
"""
return types.states.TaskState.FINISHED
def op_create_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service is created
"""
return types.states.TaskState.FINISHED
def op_create_completed_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service creation is completed
"""
return types.states.TaskState.FINISHED
def op_start_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service is started
"""
return types.states.TaskState.FINISHED
def op_start_completed_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service start is completed
"""
return types.states.TaskState.FINISHED
def op_stop_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service is stopped
"""
return types.states.TaskState.FINISHED
def op_stop_completed_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service stop is completed
"""
return types.states.TaskState.FINISHED
def op_shutdown_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service is shutdown
"""
return types.states.TaskState.FINISHED
def op_shutdown_completed_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service shutdown is completed
"""
return types.states.TaskState.FINISHED
def op_suspend_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service is suspended
"""
return types.states.TaskState.FINISHED
def op_suspend_completed_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service suspension is completed
"""
return types.states.TaskState.FINISHED
def op_reset_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service is reset
"""
return types.states.TaskState.FINISHED
def op_remove_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service is removed
"""
return types.states.TaskState.FINISHED
def op_remove_completed_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service removal is completed
"""
return types.states.TaskState.FINISHED
def op_wait_checker(self) -> types.states.TaskState:
"""
Wait will remain in the same state until something external changes it (i.e. poping from the queue)
"""
return types.states.TaskState.RUNNING
def op_nop_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service is doing nothing
"""
return types.states.TaskState.FINISHED
def op_custom_checker(self, operation: Operation) -> types.states.TaskState:
"""
This method is called to check if the service is doing a custom operation
"""
return types.states.TaskState.FINISHED
# ERROR, FINISH and UNKNOWN are not here, as they are final states not needing to be checked
@staticmethod
def _op2str(op: Operation) -> str:
return op.name
def _debug(self, txt: str) -> None:
logger.debug(
'Queue at %s for %s: %s, mac:%s, vmId:%s',
txt,
self._name,
[DynamicPublication._op2str(op) for op in self._queue],
self._vmid,
)
def get_template_id(self) -> str:
return self._vmid
# This is a map of operations to methods
# Operations, duwe to the fact that can be overrided some of them, must be invoked via instance
# Basically, all methods starting with _ are final, and all other are overridable
# We use __name__ later to use them, so we can use type checking and invoke them via instance
# Note that ERROR and FINISH are not here, as they final states not needing to be executed
_EXECUTORS: typing.Final[
collections.abc.Mapping[Operation, collections.abc.Callable[[DynamicPublication], None]]
] = {
Operation.INITIALIZE: DynamicPublication.op_initialize,
Operation.CREATE: DynamicPublication.op_create,
Operation.CREATE_COMPLETED: DynamicPublication.op_create_completed,
Operation.START: DynamicPublication.op_start,
Operation.START_COMPLETED: DynamicPublication.op_start_completed,
Operation.STOP: DynamicPublication.op_stop,
Operation.STOP_COMPLETED: DynamicPublication.op_stop_completed,
Operation.SHUTDOWN: DynamicPublication.op_shutdown,
Operation.SHUTDOWN_COMPLETED: DynamicPublication.op_shutdown_completed,
Operation.SUSPEND: DynamicPublication.op_suspend,
Operation.SUSPEND_COMPLETED: DynamicPublication.op_suspend_completed,
Operation.REMOVE: DynamicPublication.op_remove,
Operation.REMOVE_COMPLETED: DynamicPublication.op_remove_completed,
Operation.WAIT: DynamicPublication.op_wait,
Operation.NOP: DynamicPublication.op_nop,
}
# Same af before, but for check methods
_CHECKERS: typing.Final[
collections.abc.Mapping[Operation, collections.abc.Callable[[DynamicPublication], types.states.TaskState]]
] = {
Operation.INITIALIZE: DynamicPublication.op_initialize_checker,
Operation.CREATE: DynamicPublication.op_create_checker,
Operation.CREATE_COMPLETED: DynamicPublication.op_create_completed_checker,
Operation.START: DynamicPublication.op_start_checker,
Operation.START_COMPLETED: DynamicPublication.op_start_completed_checker,
Operation.STOP: DynamicPublication.op_stop_checker,
Operation.STOP_COMPLETED: DynamicPublication.op_stop_completed_checker,
Operation.SHUTDOWN: DynamicPublication.op_shutdown_checker,
Operation.SHUTDOWN_COMPLETED: DynamicPublication.op_shutdown_completed_checker,
Operation.SUSPEND: DynamicPublication.op_suspend_checker,
Operation.SUSPEND_COMPLETED: DynamicPublication.op_suspend_completed_checker,
Operation.REMOVE: DynamicPublication.op_remove_checker,
Operation.REMOVE_COMPLETED: DynamicPublication.op_remove_completed_checker,
Operation.WAIT: DynamicPublication.op_wait_checker,
Operation.NOP: DynamicPublication.op_nop_checker,
}

View File

@ -40,8 +40,7 @@ from uds.core.util import fields, validators
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from .dynamic_userservice import DynamicUserService
pass
from .dynamic_publication import DynamicPublication
logger = logging.getLogger(__name__)
@ -110,7 +109,7 @@ class DynamicService(services.Service, abc.ABC): # pylint: disable=too-many-pub
def get_lenname(self) -> int:
return self.lenname.value
def sanitize_machine_name(self, name: str) -> str:
"""
Sanitize machine name
@ -119,7 +118,9 @@ class DynamicService(services.Service, abc.ABC): # pylint: disable=too-many-pub
return name
@abc.abstractmethod
def get_machine_ip(self, userservice_instance: 'DynamicUserService', machine_id: str) -> str:
def get_machine_ip(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> str:
"""
Returns the ip of the machine
If cannot be obtained, MUST raise an exception
@ -127,7 +128,9 @@ class DynamicService(services.Service, abc.ABC): # pylint: disable=too-many-pub
...
@abc.abstractmethod
def get_machine_mac(self, userservice_instance: 'DynamicUserService', machine_id: str) -> str:
def get_machine_mac(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> str:
"""
Returns the mac of the machine
If cannot be obtained, MUST raise an exception
@ -135,75 +138,93 @@ class DynamicService(services.Service, abc.ABC): # pylint: disable=too-many-pub
...
@abc.abstractmethod
def is_machine_running(self, userservice_instance: 'DynamicUserService', machine_id: str) -> bool:
def is_machine_running(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> bool:
"""
Returns if the machine is running
Returns if the machine is ready and running
"""
...
def is_machine_stopped(self, userservice_instance: 'DynamicUserService', machine_id: str) -> bool:
def is_machine_stopped(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> bool:
"""
Returns if the machine is stopped
"""
return not self.is_machine_running(userservice_instance, machine_id)
return not self.is_machine_running(caller_instance, machine_id)
def is_machine_suspended(self, userservice_instance: 'DynamicUserService', machine_id: str) -> bool:
def is_machine_suspended(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> bool:
"""
Returns if the machine is suspended
"""
return self.is_machine_stopped(userservice_instance, machine_id)
@abc.abstractmethod
def create_machine(self, userservice_instance: 'DynamicUserService') -> str:
"""
Creates a new machine
Note that this must, in instance, or invoke somthing of the userservice
or operate by itself on userservice_instance
"""
...
return self.is_machine_stopped(caller_instance, machine_id)
@abc.abstractmethod
def start_machine(self, userservice_instance: 'DynamicUserService', machine_id: str) -> None:
def start_machine(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> typing.Any:
"""
Starts the machine
Can return a task, or None if no task is returned
"""
...
@abc.abstractmethod
def stop_machine(self, userservice_instance: 'DynamicUserService', machine_id: str) -> None:
def stop_machine(self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str) -> typing.Any:
"""
Stops the machine
Can return a task, or None if no task is returned
"""
...
def shutdown_machine(self, userservice_instance: 'DynamicUserService', machine_id: str) -> None:
def shutdown_machine(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> typing.Any:
"""
Shutdowns the machine
Defaults to stop_machine
Can return a task, or None if no task is returned
"""
self.stop_machine(userservice_instance, machine_id)
return self.stop_machine(caller_instance, machine_id)
@abc.abstractmethod
def reset_machine(self, userservice_instance: 'DynamicUserService', machine_id: str) -> None:
def reset_machine(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> typing.Any:
"""
Resets the machine
Can return a task, or None if no task is returned
"""
...
# Default is to stop "hard"
return self.stop_machine(caller_instance, machine_id)
def suspend_machine(self, userservice_instance: 'DynamicUserService', machine_id: str) -> None:
def suspend_machine(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> typing.Any:
"""
Suspends the machine
Defaults to shutdown_machine.
Can be overriden if the service supports suspending.
"""
self.shutdown_machine(userservice_instance, machine_id)
return self.shutdown_machine(caller_instance, machine_id)
@abc.abstractmethod
def remove_machine(self, muserservice_instance: 'DynamicUserService', achine_id: str) -> None:
def remove_machine(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> typing.Any:
"""
Removes the machine
Removes the machine, or queues it for removal, or whatever :)
"""
...
def keep_on_error(self) -> bool:
return self.maintain_on_error.value
if self.has_field('maintain_on_error'): # If has been defined on own class...
return self.maintain_on_error.value
return False
def try_graceful_shutdown(self) -> bool:
if self.has_field('try_soft_shutdown'):
return self.try_soft_shutdown.value
return False

View File

@ -38,6 +38,7 @@ import collections.abc
from uds.core import services, types, consts
from uds.core.types.services import Operation
from uds.core.util import log, autoserializable
from uds.core.util.model import sql_stamp_seconds
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
@ -62,12 +63,17 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
max_state_checks: typing.ClassVar[int] = 20
# If keep_state_sets_error is true, and an error occurs, the machine is set to FINISHED instead of ERROR
keep_state_sets_error: typing.ClassVar[bool] = False
# If must wait untill finish queue for destroying the machine
wait_until_finish_to_destroy: typing.ClassVar[bool] = False
_name = autoserializable.StringField(default='')
_mac = autoserializable.StringField(default='')
_vmid = autoserializable.StringField(default='')
_reason = autoserializable.StringField(default='')
_queue = autoserializable.ListField[Operation]() # Default is empty list
_is_flagged_for_destroy = autoserializable.BoolField(default=False)
# In order to allow migrating from old data, we will mark if the _queue has our format or the old one
_queue_has_new_format = autoserializable.BoolField(default=False)
# Note that even if SNAPHSHOT operations are in middel
# implementations may opt to no have snapshots at all
@ -96,6 +102,8 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
Operation.START,
Operation.START_COMPLETED,
Operation.WAIT,
Operation.SUSPEND,
Operation.SUSPEND_COMPLETED,
Operation.FINISH,
]
# If gracefull_stop, will prepend a soft_shutdown
@ -107,11 +115,6 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
Operation.FINISH,
]
# helpers
def _get_checks_counter(self) -> int:
with self.storage.as_dict() as data:
return data.get('exec_count', 0)
def _reset_checks_counter(self) -> None:
with self.storage.as_dict() as data:
data['exec_count'] = 0
@ -125,10 +128,41 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
return None
def _current_op(self) -> Operation:
"""
Get the current operation from the queue
Checks that the queue is upgraded, and if not, migrates it
Note:
This method will be here for a while, to ensure future compat with old data.
Eventually, this mechanincs will be removed, but no date is set for that.
There is almos not penalty on keeping this here, as it's only an small check
We also could have used marshal/unmarshal, but this is more clear and easy to maintain
"""
if self._queue_has_new_format is False:
self.migrate_old_queue()
self._queue_has_new_format = True
if not self._queue:
return Operation.FINISH
return self._queue[0]
def _set_queue(self, queue: list[Operation]) -> None:
"""
Sets the queue of tasks to be executed
Ensures that we mark it as new format
"""
self._queue = queue
self._queue_has_new_format = True
def _retry_again(self) -> types.states.TaskState:
"""
Retries the current operation
For this, we insert a NOP that will be consumed instead of the current operationç
by the queue runner
"""
self._queue.insert(0, Operation.NOP)
return types.states.TaskState.RUNNING
def _generate_name(self) -> str:
"""
@ -154,17 +188,17 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
if self._vmid:
if self.service().keep_on_error() is False:
try:
# TODO: Remove VM using service or put it on a "to be removed" queue for a parallel job
self.service().remove_machine(self, self._vmid)
self._vmid = ''
except Exception as e:
logger.exception('Exception removing machine: %s', e)
else:
logger.debug('Keep on error is enabled, not removing machine')
if self.keep_state_sets_error is False:
self._queue = [Operation.FINISH]
self._set_queue([Operation.FINISH])
return types.states.TaskState.FINISHED
self._queue = [Operation.ERROR]
self._set_queue([Operation.ERROR])
self._reason = reason
return types.states.TaskState.ERROR
@ -215,36 +249,32 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
Deploys an service instance for an user.
"""
logger.debug('Deploying for user')
self._queue = self._create_queue.copy() # copy is needed to avoid modifying class var
self._set_queue(self._create_queue.copy()) # copy is needed to avoid modifying class var
return self._execute_queue()
@typing.final
def deploy_for_cache(self, level: types.services.CacheLevel) -> types.states.TaskState:
if level == types.services.CacheLevel.L1:
self._queue = self._create_queue_l1_cache.copy()
self._set_queue(self._create_queue_l1_cache.copy())
else:
self._queue = self._create_queue_l2_cache.copy()
self._set_queue(self._create_queue_l2_cache.copy())
return self._execute_queue()
@typing.final
def set_ready(self) -> types.states.TaskState:
# If already ready, return finished
if self.cache.get('ready') == '1':
return types.states.TaskState.FINISHED
try:
if self.service().is_machine_running(self, self._vmid):
self.cache.put('ready', '1')
return types.states.TaskState.FINISHED
self._queue = [Operation.START, Operation.START_COMPLETED, Operation.FINISH]
return self._execute_queue()
if self.cache.get('ready') == '1' or self.service().is_machine_running(self, self._vmid):
self._set_queue([Operation.START_COMPLETED, Operation.FINISH])
else:
self._set_queue([Operation.START, Operation.START_COMPLETED, Operation.FINISH])
except Exception as e:
return self._error(f'Error on setReady: {e}')
return self._execute_queue()
def reset(self) -> types.states.TaskState:
if self._vmid != '':
self._queue = [Operation.RESET, Operation.RESET_COMPLETED, Operation.FINISH]
self._set_queue([Operation.RESET, Operation.RESET_COMPLETED, Operation.FINISH])
return types.states.TaskState.FINISHED
@ -288,6 +318,12 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
return types.states.TaskState.ERROR
if op == Operation.FINISH:
# If has a deferred destroy, do it now
if self.wait_until_finish_to_destroy and self._is_flagged_for_destroy:
self._is_flagged_for_destroy = False
# Simply ensures nothing is left on queue and returns FINISHED
self._set_queue([Operation.FINISH])
return self.destroy()
return types.states.TaskState.FINISHED
if op != Operation.WAIT:
@ -303,13 +339,42 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
state = _CHECKERS[op](self)
if state == types.states.TaskState.FINISHED:
# Remove runing op
# Remove finished operation from queue
self._queue.pop(0)
return self._execute_queue()
return state
except Exception as e:
return self._error(e)
@typing.final
def destroy(self) -> types.states.TaskState:
"""
Destroys the service
"""
self._is_flagged_for_destroy = False # Reset
op = self._current_op()
if op == Operation.ERROR:
return self._error('Machine is already in error state!')
shutdown_operations: list[Operation] = [] if not self.service().try_graceful_shutdown() else [Operation.SHUTDOWN, Operation.SHUTDOWN_COMPLETED]
destroy_operations = shutdown_operations + self._destroy_queue
# If a "paused" state, reset queue to destroy
if op in (Operation.FINISH, Operation.WAIT):
self._queue[:] = destroy_operations
return self._execute_queue()
# If must wait until finish, flag for destroy and wait
if self.wait_until_finish_to_destroy:
self._is_flagged_for_destroy = True
else:
# If other operation, wait for finish before destroying
self._queue = [op] + destroy_operations
# Do not execute anything.here, just continue normally
return types.states.TaskState.RUNNING
# Execution methods
# Every Operation has an execution method and a check method
@ -359,7 +424,17 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
"""
This method is called for shutdown the service
"""
shutdown_stamp = -1
is_running = self.service().is_machine_running(self, self._vmid)
if not is_running:
# Already stopped, just finish
return
self.service().shutdown_machine(self, self._vmid)
shutdown_stamp = sql_stamp_seconds()
with self.storage.as_dict() as data:
data['shutdown'] = shutdown_stamp
def op_shutdown_completed(self) -> None:
"""
@ -471,9 +546,41 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
def op_shutdown_checker(self) -> types.states.TaskState:
"""
This method is called to check if the service is shutdown
This method is called to check if the service is shutdown in time
Else, will fall back to stop
"""
return types.states.TaskState.FINISHED
with self.storage.as_dict() as data:
shutdown_start = data.get('shutdown', -1)
logger.debug('Shutdown start: %s', shutdown_start)
if shutdown_start < 0: # Was already stopped
# Machine is already stop
logger.debug('Machine WAS stopped')
return types.states.TaskState.FINISHED
logger.debug('Checking State')
# Check if machine is already stopped (As soon as it is not running, we will consider it stopped)
if self.service().is_machine_running(self, self._vmid) is False:
return types.states.TaskState.FINISHED
logger.debug('State is running')
if sql_stamp_seconds() - shutdown_start > consts.os.MAX_GUEST_SHUTDOWN_WAIT:
logger.debug('Time is consumed, falling back to stop on vmid %s', self._vmid)
self.do_log(
log.LogLevel.ERROR,
f'Could not shutdown machine using soft power off in time ({consts.os.MAX_GUEST_SHUTDOWN_WAIT} seconds). Powering off.',
)
# Not stopped by guest in time, but must be stopped normally
with self.storage.as_dict() as data:
data['shutdown'] = -1
# If stop is in queue, mark this as finished, else, add it to queue just after first (our) operation
if Operation.STOP not in self._queue:
# After current operation, add stop
self._queue.insert(1, Operation.STOP)
return types.states.TaskState.FINISHED
# Not finished yet
return types.states.TaskState.RUNNING
def op_shutdown_completed_checker(self) -> types.states.TaskState:
"""
@ -530,6 +637,13 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
return types.states.TaskState.FINISHED
# ERROR, FINISH and UNKNOWN are not here, as they are final states not needing to be checked
def migrate_old_queue(self) -> None:
"""
If format has to be converted, override this method and do the conversion here
Remember to replace self_queue with the new one
"""
pass
@staticmethod
def _op2str(op: Operation) -> str:

View File

@ -30,6 +30,7 @@
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import abc
import typing
from uds.core import types
@ -46,7 +47,7 @@ if typing.TYPE_CHECKING:
from uds.core.util.unique_name_generator import UniqueNameGenerator
class UserService(Environmentable, Serializable):
class UserService(Environmentable, Serializable, abc.ABC):
"""
Interface for user services.
@ -257,6 +258,7 @@ class UserService(Environmentable, Serializable):
"""
return typing.cast('UniqueGIDGenerator', self.id_generator('id'))
@abc.abstractmethod
def get_unique_id(self) -> str:
"""
Obtains an unique id for this deployed service, you MUST override this
@ -266,7 +268,7 @@ class UserService(Environmentable, Serializable):
An unique identifier for this object, that is an string and must be
unique.
"""
raise NotImplementedError('Base getUniqueId for User Deployment called!!!')
raise NotImplementedError(f'get_unique_id method for class {self.__class__.__name__} not provided!')
def process_ready_from_os_manager(self, data: typing.Any) -> types.states.TaskState:
"""
@ -296,6 +298,7 @@ class UserService(Environmentable, Serializable):
"""
return types.states.TaskState.FINISHED
@abc.abstractmethod
def get_ip(self) -> str:
"""
All services are "IP" services, so this method is a MUST
@ -305,13 +308,14 @@ class UserService(Environmentable, Serializable):
The needed ip to let the user connect to the his deployed service.
This ip will be managed by transports, without IP there is no connection
"""
raise Exception('Base getIp for User Deployment got called!!!')
raise NotImplementedError(f'get_ip method for class {self.__class__.__name__} not provided!')
def set_ip(self, ip: str) -> None:
"""
This is an utility method, invoked by some os manager to notify what they thinks is the ip for this service.
If you assign the service IP by your own methods, do not override this
"""
pass
def set_ready(self) -> types.states.TaskState:
"""
@ -347,6 +351,7 @@ class UserService(Environmentable, Serializable):
"""
return types.states.TaskState.FINISHED
@abc.abstractmethod
def deploy_for_cache(self, level: types.services.CacheLevel) -> types.states.TaskState:
"""
Deploys a user deployment as cache.
@ -386,6 +391,7 @@ class UserService(Environmentable, Serializable):
"""
raise Exception(f'Base deploy for cache invoked! for class {self.__class__.__name__}')
@abc.abstractmethod
def deploy_for_user(self, user: 'models.User') -> types.states.TaskState:
"""
Deploys an service instance for an user.
@ -421,6 +427,7 @@ class UserService(Environmentable, Serializable):
"""
raise NotImplementedError(f'Base deploy for user invoked! for class {self.__class__.__name__}')
@abc.abstractmethod
def check_state(self) -> types.states.TaskState:
"""
This is a task method. As that, the expected return values are
@ -537,6 +544,7 @@ class UserService(Environmentable, Serializable):
"""
return 'unknown'
@abc.abstractmethod
def destroy(self) -> types.states.TaskState:
"""
This is a task method. As that, the excepted return values are
@ -571,15 +579,11 @@ class UserService(Environmentable, Serializable):
all exceptions, and never raise an exception from these methods
to the core. Take that into account and handle exceptions inside
this method.
Defaults to calling destroy, but can be overriden to provide a more
controlled way of cancelling the operation.
"""
return types.states.TaskState.RUNNING
@classmethod
def supports_cancel(cls: type['UserService']) -> bool:
"""
Helper to query if a class is custom (implements getJavascript method)
"""
return cls.cancel != UserService.cancel
return self.destroy()
def reset(self) -> types.states.TaskState:
"""

View File

@ -1757,6 +1757,13 @@ class UserInterface(metaclass=UserInterfaceType):
field_names_translations[fld_old_field_name] = fld_name
return field_names_translations
def has_field(self, field_name: str) -> bool:
"""
So we can check against field existence on "own" instance
If not redeclared in derived class, it will return False
"""
return field_name in self._gui
# Dictionaries used to encode/decode fields to be stored on database

View File

@ -562,8 +562,8 @@ class ProxmoxClient:
self._get(f'nodes/{node}/tasks/{urllib.parse.quote(upid)}/status', node=node)
)
@ensure_connected
@cached('vms', CACHE_DURATION, key_helper=caching_key_helper)
@ensure_connected
def list_machines(
self, node: typing.Union[None, str, collections.abc.Iterable[str]] = None, **kwargs: typing.Any
) -> list[types.VMInfo]:
@ -583,8 +583,8 @@ class ProxmoxClient:
return sorted(result, key=lambda x: '{}{}'.format(x.node, x.name))
@ensure_connected
@cached('vmip', CACHE_INFO_DURATION, key_helper=caching_key_helper)
@ensure_connected
def get_machine_pool_info(self, vmid: int, poolid: typing.Optional[str], **kwargs: typing.Any) -> types.VMInfo:
# try to locate machine in pool
node = None

File diff suppressed because it is too large Load Diff

View File

@ -40,41 +40,60 @@ from uds.core.util.unique_id_generator import UniqueIDGenerator
from . import provider
from . import client
MAX_VMID_LIFE_SECS = 365 * 24 * 60 * 60 * 3 # 3 years for "reseting"
# Note that even reseting, UDS will get always a FREE vmid, so even if the machine is already in use
# (and removed from used db), it will not be reused until it has dissapeared from the proxmox server
MAX_VMID_LIFE_SECS: typing.Final[int] = 365 * 24 * 60 * 60 * 3 # 3 years for "reseting"
logger = logging.getLogger(__name__)
class ProxmoxDeferredRemoval(jobs.Job):
frecuency = 60 * 5 # Once every NN minutes
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:
vmid, try_graceful_shutdown_s = vmdata.split(':')
try_graceful_shutdown = try_graceful_shutdown_s == 'y'
else:
vmid = vmdata
try_graceful_shutdown = False
return int(vmid), try_graceful_shutdown
@staticmethod
def remove(providerInstance: 'provider.ProxmoxProvider', vmId: int) -> None:
logger.debug(
'Adding %s from %s to defeffed removal process', vmId, providerInstance
)
def remove(provider_instance: 'provider.ProxmoxProvider', vmid: int, try_graceful_shutdown: bool) -> None:
def storeDeferredRemoval() -> None:
provider_instance.storage.save_to_db('tr' + str(vmid), f'{vmid}:{"y" if try_graceful_shutdown else "n"}', attr1='tRm')
ProxmoxDeferredRemoval.counter += 1
logger.debug('Adding %s from %s to defeffed removal process', vmid, provider_instance)
try:
# First check state & stop machine if needed
vmInfo = providerInstance.get_machine_info(vmId)
if vmInfo.status == 'running':
# If running vm, simply stops it and wait for next
ProxmoxDeferredRemoval.waitForTaskFinish(
providerInstance, providerInstance.stop_machine(vmId)
)
vminfo = provider_instance.get_machine_info(vmid)
if vminfo.status == 'running':
if try_graceful_shutdown:
# If running vm, simply try to shutdown
provider_instance.shutdown_machine(vmid)
# Store for later removal
else:
# If running vm, simply stops it and wait for next
provider_instance.stop_machine(vmid)
storeDeferredRemoval()
return
ProxmoxDeferredRemoval.waitForTaskFinish(
providerInstance, providerInstance.remove_machine(vmId)
)
provider_instance.remove_machine(vmid) # Try to remove, launch removal, but check later
storeDeferredRemoval()
except client.ProxmoxNotFound:
return # Machine does not exists
except Exception as e:
providerInstance.storage.save_to_db('tr' + str(vmId), str(vmId), attr1='tRm')
storeDeferredRemoval()
logger.info(
'Machine %s could not be removed right now, queued for later: %s',
vmId,
vmid,
e,
)
@ -82,13 +101,10 @@ class ProxmoxDeferredRemoval(jobs.Job):
def waitForTaskFinish(
providerInstance: 'provider.ProxmoxProvider',
upid: 'client.types.UPID',
maxWait: int = 30,
maxWait: int = 30, # 30 * 0.3 = 9 seconds
) -> bool:
counter = 0
while (
providerInstance.get_task_info(upid.node, upid.upid).is_running()
and counter < maxWait
):
while providerInstance.get_task_info(upid.node, upid.upid).is_running() and counter < maxWait:
time.sleep(0.3)
counter += 1
@ -107,34 +123,30 @@ class ProxmoxDeferredRemoval(jobs.Job):
provider.ProxmoxProvider, dbProvider.get_instance()
)
for i in storage.filter('tRm'):
vmId = int(i[1].decode())
for data in storage.filter('tRm'):
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:
vmInfo = instance.get_machine_info(vmId)
logger.debug('Found %s for removal %s', vmId, i)
vmInfo = instance.get_machine_info(vmid)
logger.debug('Found %s for removal %s', vmid, data)
# If machine is powered on, tries to stop it
# tries to remove in sync mode
if vmInfo.status == 'running':
ProxmoxDeferredRemoval.waitForTaskFinish(
instance, instance.stop_machine(vmId)
)
ProxmoxDeferredRemoval.waitForTaskFinish(instance, instance.stop_machine(vmid))
return
if (
vmInfo.status == 'stopped'
): # Machine exists, try to remove it now
ProxmoxDeferredRemoval.waitForTaskFinish(
instance, instance.remove_machine(vmId)
)
if vmInfo.status == 'stopped': # Machine exists, try to remove it now
ProxmoxDeferredRemoval.waitForTaskFinish(instance, instance.remove_machine(vmid))
# It this is reached, remove check
storage.remove('tr' + str(vmId))
storage.remove('tr' + str(vmid))
except client.ProxmoxNotFound:
storage.remove('tr' + str(vmId)) # VM does not exists anymore
storage.remove('tr' + str(vmid)) # VM does not exists anymore
except Exception as e: # Any other exception wil be threated again
# instance.log('Delayed removal of %s has failed: %s. Will retry later', vmId, e)
logger.error('Delayed removal of %s failed: %s', i, e)
logger.error('Delayed removal of %s failed: %s', data, e)
logger.debug('Deferred removal for proxmox finished')

View File

@ -28,13 +28,14 @@
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from datetime import datetime
import datetime
import time
import logging
import typing
from django.utils.translation import gettext as _
from uds.core import services, types
from uds.core import types
from uds.core.services.specializations.dynamic_machine.dynamic_publication import DynamicPublication
from uds.core.util import autoserializable
# Not imported at runtime, just for type checking
@ -44,17 +45,15 @@ if typing.TYPE_CHECKING:
logger = logging.getLogger(__name__)
class ProxmoxPublication(services.Publication, autoserializable.AutoSerializable):
class ProxmoxPublication(DynamicPublication, autoserializable.AutoSerializable):
suggested_delay = 20
_name = autoserializable.StringField(default='')
_vmid = autoserializable.StringField(default='')
# Some customization fields
# If must wait untill finish queue for destroying the machine
wait_until_finish_to_destroy = True
_task = autoserializable.StringField(default='')
_state = autoserializable.StringField(default='')
_operation = autoserializable.StringField(default='')
_destroy_after = autoserializable.BoolField(default=False)
_reason = autoserializable.StringField(default='')
# Utility overrides for type checking...
def service(self) -> 'ProxmoxServiceLinked':
@ -74,104 +73,63 @@ class ProxmoxPublication(services.Publication, autoserializable.AutoSerializable
self._name,
self._vmid,
self._task,
self._state,
self._operation,
_state,
_operation,
destroy_after,
self._reason,
) = vals[1:]
else:
raise ValueError('Invalid data format')
self._destroy_after = destroy_after != ''
self._queue = (
# If removing
[
types.services.Operation.REMOVE,
types.services.Operation.REMOVE_COMPLETED,
types.services.Operation.FINISH,
]
if _operation == 'd'
# If publishing, must have finished for sure
else [types.services.Operation.FINISH]
)
self._is_flagged_for_destroy = destroy_after != ''
self.mark_for_upgrade() # Flag so manager can save it again with new format
def op_create(self) -> None:
# First we should create a full clone, so base machine do not get fullfilled with "garbage" delta disks...
comments = _('UDS Publication for {0} created at {1}').format(
self.servicepool_name(), str(datetime.datetime.now()).split('.')[0]
)
task = self.service().clone_machine(self._name, comments)
self._vmid = str(task.vmid)
self._task = ','.join((task.upid.node, task.upid.upid))
def publish(self) -> types.states.TaskState:
"""
If no space is available, publication will fail with an error
"""
try:
# First we should create a full clone, so base machine do not get fullfilled with "garbage" delta disks...
self._name = 'UDS ' + _('Publication') + ' ' + self.servicepool_name() + "-" + str(self.revision())
comments = _('UDS Publication for {0} created at {1}').format(
self.servicepool_name(), str(datetime.now()).split('.')[0]
)
task = self.service().clone_machine(self._name, comments)
self._vmid = str(task.vmid)
self._task = ','.join((task.upid.node, task.upid.upid))
self._state = types.states.TaskState.RUNNING
self._operation = 'p' # Publishing
self._destroy_after = False
return types.states.TaskState.RUNNING
except Exception as e:
logger.exception('Caught exception %s', e)
self._reason = str(e)
return types.states.TaskState.ERROR
def check_state(self) -> types.states.TaskState:
if self._state != types.states.TaskState.RUNNING:
return types.states.TaskState.from_str(self._state)
def op_create_checker(self) -> types.states.TaskState:
node, upid = self._task.split(',')
try:
task = self.service().provider().get_task_info(node, upid)
if task.is_running():
return types.states.TaskState.RUNNING
except Exception as e:
logger.exception('Proxmox publication')
self._state = types.states.TaskState.ERROR
self._reason = str(e)
return self._state
task = self.service().provider().get_task_info(node, upid)
if task.is_running():
return types.states.TaskState.RUNNING
if task.is_errored():
self._reason = task.exitstatus
self._state = types.states.TaskState.ERROR
else: # Finished
if self._destroy_after:
return self.destroy()
self._state = types.states.TaskState.FINISHED
if self._operation == 'p': # not Destroying
# Disable Protection (removal)
self.service().provider().set_protection(int(self._vmid), protection=False)
time.sleep(0.5) # Give some tome to proxmox. We have observed some concurrency issues
# And add it to HA if
self.service().enable_machine_ha(int(self._vmid))
time.sleep(0.5)
# Mark vm as template
self.service().provider().create_template(int(self._vmid))
return self._error(task.exitstatus)
# This seems to cause problems on Proxmox
# makeTemplate --> setProtection (that calls "config"). Seems that the HD dissapears...
# Seems a concurrency problem?
return self._state
def finish(self) -> None:
self._task = ''
self._destroy_after = False
def destroy(self) -> types.states.TaskState:
if (
self._state == types.states.TaskState.RUNNING and self._destroy_after is False
): # If called destroy twice, will BREAK STOP publication
self._destroy_after = True
return types.states.TaskState.RUNNING
self._state = types.states.TaskState.RUNNING
self._operation = 'd'
self._destroy_after = False
try:
task = self.service().remove_machine(self.machine())
self._task = ','.join((task.node, task.upid))
return types.states.TaskState.RUNNING
except Exception as e:
self._reason = str(e) # Store reason of error
logger.warning(
'Problem destroying publication %s: %s. Please, check machine state On Proxmox',
self.machine(),
e,
)
return types.states.TaskState.ERROR
return types.states.TaskState.FINISHED
def op_create_completed(self) -> None:
# Complete the creation, disabling ha protection and adding to HA and marking as template
self.service().provider().set_protection(int(self._vmid), protection=False)
time.sleep(0.5) # Give some tome to proxmox. We have observed some concurrency issues
# And add it to HA if needed (decided by service configuration)
self.service().enable_machine_ha(int(self._vmid))
# Wait a bit, if too fast, proxmox fails.. (Have not tested on 8.x, but previous versions failed if too fast..)
time.sleep(0.5)
# Mark vm as template
self.service().provider().create_template(int(self._vmid))
def op_remove(self) -> None:
self.service().remove_machine(self, self._vmid)
def cancel(self) -> types.states.TaskState:
return self.destroy()

View File

@ -33,11 +33,14 @@ import re
import typing
from django.utils.translation import gettext_noop as _
from uds.core import services, types
from uds.core import types
from uds.core.services.specializations.dynamic_machine.dynamic_publication import DynamicPublication
from uds.core.services.specializations.dynamic_machine.dynamic_service import DynamicService
from uds.core.services.specializations.dynamic_machine.dynamic_userservice import DynamicUserService
from uds.core.ui import gui
from uds.core.util import validators, log, fields
from . import helpers
from . import helpers, jobs
from .deployment_linked import ProxmoxUserserviceLinked
from .publication import ProxmoxPublication
@ -45,11 +48,14 @@ from .publication import ProxmoxPublication
if typing.TYPE_CHECKING:
from . import client
from .provider import ProxmoxProvider
from uds.core.services.specializations.dynamic_machine.dynamic_publication import DynamicPublication
from uds.core.services.specializations.dynamic_machine.dynamic_service import DynamicService
from uds.core.services.specializations.dynamic_machine.dynamic_userservice import DynamicUserService
logger = logging.getLogger(__name__)
class ProxmoxServiceLinked(services.Service): # pylint: disable=too-many-public-methods
class ProxmoxServiceLinked(DynamicService):
"""
Proxmox Linked clones service. This is based on creating a template from selected vm, and then use it to
"""
@ -115,9 +121,9 @@ class ProxmoxServiceLinked(services.Service): # pylint: disable=too-many-public
tooltip=_('Select if HA is enabled and HA group for machines of this service'),
readonly=True,
)
try_soft_shutdown = fields.soft_shutdown_field()
machine = gui.ChoiceField(
label=_("Base Machine"),
order=110,
@ -153,12 +159,12 @@ class ProxmoxServiceLinked(services.Service): # pylint: disable=too-many-public
tab=_('Machine'),
required=True,
)
basename = fields.basename_field(order=115)
lenname = fields.lenname_field(order=116)
basename = DynamicService.basename
lenname = DynamicService.lenname
prov_uuid = gui.HiddenField(value=None)
def initialize(self, values: 'types.core.ValuesType') -> None:
if values:
self.basename.value = validators.validate_basename(
@ -230,7 +236,7 @@ class ProxmoxServiceLinked(services.Service): # pylint: disable=too-many-public
config = self.provider().get_machine_configuration(vmid)
return config.networks[0].mac.lower()
def remove_machine(self, vmid: int) -> 'client.types.UPID':
def xremove_machine(self, vmid: int) -> 'client.types.UPID':
# First, remove from HA if needed
try:
self.disable_machine_ha(vmid)
@ -269,10 +275,34 @@ class ProxmoxServiceLinked(services.Service): # pylint: disable=too-many-public
def try_graceful_shutdown(self) -> bool:
return self.try_soft_shutdown.as_bool()
def get_console_connection(
self, machine_id: str
) -> typing.Optional[types.services.ConsoleConnectionInfo]:
def get_console_connection(self, machine_id: str) -> typing.Optional[types.services.ConsoleConnectionInfo]:
return self.provider().get_console_connection(machine_id)
def is_avaliable(self) -> bool:
return self.provider().is_available()
def get_machine_ip(self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str) -> str:
return self.provider().get_guest_ip_address(int(machine_id))
def get_machine_mac(self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str) -> str:
return self.get_nic_mac(int(machine_id))
def start_machine(self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str) -> None:
self.provider().start_machine(int(machine_id))
def stop_machine(self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str) -> None:
self.provider().stop_machine(int(machine_id))
def is_machine_running(
self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str
) -> bool:
# Raise an exception if fails to get machine info
vminfo = self.get_machine_info(int(machine_id))
return vminfo.status != 'stopped'
def remove_machine(self, caller_instance: 'DynamicUserService | DynamicPublication', machine_id: str) -> None:
# All removals are deferred, so we can do it async
# Try to stop it if already running... Hard stop
self.stop_machine(caller_instance, machine_id)
jobs.ProxmoxDeferredRemoval.remove(self.provider(), int(machine_id), self.try_graceful_shutdown())

View File

@ -0,0 +1,38 @@
#
# Copyright (c) 2012-2023 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
"""
# pyright: reportUnusedImport=false
from uds.core import managers
from .provider import ProxmoxProvider
from .jobs import ProxmoxDeferredRemovalOrig, ProxmoxVmidReleaserOrig
# Scheduled task to do clean processes
for cls in (ProxmoxDeferredRemovalOrig, ProxmoxVmidReleaserOrig):
managers.task_manager().register_job(cls)

View File

@ -0,0 +1,802 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2019-2021 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.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
import collections.abc
import logging
import re
import time
import typing
import urllib.parse
import requests
from uds.core import consts, types as core_types
from uds.core.util import security
from uds.core.util.decorators import cached, ensure_connected
from . import types
# DEFAULT_PORT = 8006
CACHE_DURATION: typing.Final[int] = consts.cache.DEFAULT_CACHE_TIMEOUT
CACHE_INFO_DURATION: typing.Final[int] = consts.cache.SHORT_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
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core.util.cache import Cache
logger = logging.getLogger(__name__)
class ProxmoxError(Exception):
pass
class ProxmoxConnectionError(ProxmoxError):
pass
class ProxmoxAuthError(ProxmoxError):
pass
class ProxmoxNotFound(ProxmoxError):
pass
class ProxmoxNodeUnavailableError(ProxmoxConnectionError):
pass
class ProxmoxNoGPUError(ProxmoxError):
pass
# caching helper
def caching_key_helper(obj: 'ProxmoxClient') -> str:
return obj._host # pylint: disable=protected-access
class ProxmoxClient:
_host: str
_port: int
_credentials: tuple[tuple[str, str], tuple[str, str]]
_url: str
_validate_cert: bool
_timeout: int
_ticket: str
_csrf: str
cache: typing.Optional['Cache']
def __init__(
self,
host: str,
port: int,
username: str,
password: str,
timeout: int = 5,
validate_certificate: bool = False,
cache: typing.Optional['Cache'] = None,
) -> None:
self._host = host
self._port = port
self._credentials = (('username', username), ('password', password))
self._validate_cert = validate_certificate
self._timeout = timeout
self._url = 'https://{}:{}/api2/json/'.format(self._host, self._port)
self.cache = cache
self._ticket = ''
self._csrf = ''
@property
def headers(self) -> dict[str, str]:
return {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'CSRFPreventionToken': self._csrf,
}
def ensure_correct(self, response: 'requests.Response', *, node: typing.Optional[str]) -> typing.Any:
if not response.ok:
logger.debug('Error on request %s: %s', response.status_code, response.content)
error_message = 'Status code {}'.format(response.status_code)
if response.status_code == 595:
raise ProxmoxNodeUnavailableError()
if response.status_code == 403:
raise ProxmoxAuthError()
if response.status_code == 400:
try:
error_message = 'Errors on request: {}'.format(response.json()['errors'])
except Exception: # nosec: No joson or no errors, use default msg
pass
if response.status_code == 500 and node:
# Try to get from journal
try:
journal = [x for x in filter(lambda x: 'failed' in x, self.journal(node, 4))]
logger.error('Proxmox error 500:')
for line in journal:
logger.error(' * %s', line)
error_message = f'Error 500 on request: {" ## ".join(journal)}'
except Exception:
pass # If we can't get journal, just use default message
raise ProxmoxError(error_message)
return response.json()
def _compose_url_for(self, path: str) -> str:
return self._url + path
def _get(self, path: str, *, node: typing.Optional[str] = None) -> typing.Any:
try:
result = security.secure_requests_session(verify=self._validate_cert).get(
self._compose_url_for(path),
headers=self.headers,
cookies={'PVEAuthCookie': self._ticket},
timeout=self._timeout,
)
logger.debug('GET result to %s: %s -- %s', path, result.status_code, result.content)
except requests.ConnectionError as e:
raise ProxmoxConnectionError(e)
return self.ensure_correct(result, node=node)
def _post(
self,
path: str,
data: typing.Optional[collections.abc.Iterable[tuple[str, str]]] = None,
*,
node: typing.Optional[str] = None,
) -> typing.Any:
try:
result = security.secure_requests_session(verify=self._validate_cert).post(
self._compose_url_for(path),
data=data, # type: ignore
headers=self.headers,
cookies={'PVEAuthCookie': self._ticket},
timeout=self._timeout,
)
logger.debug('POST result to %s: %s -- %s', path, result.status_code, result.content)
except requests.ConnectionError as e:
raise ProxmoxConnectionError(e)
return self.ensure_correct(result, node=node)
def _delete(
self,
path: str,
data: typing.Optional[collections.abc.Iterable[tuple[str, str]]] = None,
*,
node: typing.Optional[str] = None,
) -> typing.Any:
try:
result = security.secure_requests_session(verify=self._validate_cert).delete(
self._compose_url_for(path),
data=data, # type: ignore
headers=self.headers,
cookies={'PVEAuthCookie': self._ticket},
timeout=self._timeout,
)
logger.debug(
'DELETE result to %s: %s -- %s -- %s',
path,
result.status_code,
result.content,
result.headers,
)
except requests.ConnectionError as e:
raise ProxmoxConnectionError(e)
return self.ensure_correct(result, node=node)
def connect(self, force: bool = False) -> None:
if self._ticket:
return # Already connected
# we could cache this for a while, we know that at least for 30 minutes
if self.cache and not force:
dc = self.cache.get(self._host + 'conn')
if dc: # Stored on cache
self._ticket, self._csrf = dc
return
try:
result = security.secure_requests_session(verify=self._validate_cert).post(
url=self._compose_url_for('access/ticket'),
data=self._credentials,
headers=self.headers,
timeout=self._timeout,
)
if not result.ok:
raise ProxmoxAuthError()
data = result.json()['data']
self._ticket = data['ticket']
self._csrf = data['CSRFPreventionToken']
if self.cache:
self.cache.put(self._host + 'conn', (self._ticket, self._csrf), validity=1800) # 30 minutes
except requests.RequestException as e:
raise ProxmoxConnectionError from e
def test(self) -> bool:
try:
self.connect()
except Exception:
# logger.error('Error testing proxmox: %s', e)
return False
return True
@ensure_connected
@cached('cluster', CACHE_DURATION, key_helper=caching_key_helper)
def get_cluster_info(self, **kwargs: typing.Any) -> types.ClusterInfo:
return types.ClusterInfo.from_dict(self._get('cluster/status'))
@ensure_connected
def get_next_vmid(self) -> int:
return int(self._get('cluster/nextid')['data'])
@ensure_connected
def is_vmid_available(self, vmid: int) -> bool:
try:
self._get(f'cluster/nextid?vmid={vmid}')
except Exception: # Not available
return False
return True
@ensure_connected
@cached('nodeNets', CACHE_DURATION, args=1, kwargs=['node'], key_helper=caching_key_helper)
def get_node_networks(self, node: str, **kwargs: typing.Any) -> typing.Any:
return self._get(f'nodes/{node}/network', node=node)['data']
# pylint: disable=unused-argument
@ensure_connected
@cached('nodeGpuDevices', CACHE_DURATION_LONG, key_helper=caching_key_helper)
def list_node_gpu_devices(self, node: str, **kwargs: typing.Any) -> list[str]:
return [
device['id']
for device in self._get(f'nodes/{node}/hardware/pci', node=node)['data']
if device.get('mdev')
]
@ensure_connected
def list_node_vgpus(self, node: str, **kwargs: typing.Any) -> list[types.VGPUInfo]:
return [
types.VGPUInfo.from_dict(gpu)
for device in self.list_node_gpu_devices(node)
for gpu in self._get(f'nodes/{node}/hardware/pci/{device}/mdev', node=node)['data']
]
@ensure_connected
def node_has_vgpus_available(self, node: str, vgpu_type: typing.Optional[str], **kwargs: typing.Any) -> bool:
return any(
gpu.available and (vgpu_type is None or gpu.type == vgpu_type) for gpu in self.list_node_vgpus(node)
)
@ensure_connected
def get_best_node_for_machine(
self,
min_memory: int = 0,
must_have_vgpus: typing.Optional[bool] = None,
mdev_type: typing.Optional[str] = None,
) -> typing.Optional[types.NodeStats]:
'''
Returns the best node to create a VM on
Args:
minMemory (int, optional): Minimum memory required. Defaults to 0.
mustHaveVGPUS (typing.Optional[bool], optional): If the node must have VGPUS. True, False or None (don't care). Defaults to None.
'''
best = types.NodeStats.empty()
node: types.NodeStats
# Function to calculate the weight of a node
def calc_weight(x: types.NodeStats) -> float:
return (x.mem / x.maxmem) + (x.cpu / x.maxcpu) * 1.3
# Offline nodes are not "the best"
for node in filter(lambda x: x.status == 'online', self.get_node_stats()):
if min_memory and node.mem < min_memory + 512000000: # 512 MB reserved
continue # Skips nodes with not enouhg memory
if must_have_vgpus is not None and must_have_vgpus != bool(self.list_node_gpu_devices(node.name)):
continue # Skips nodes without VGPUS if vGPUS are required
if mdev_type and not self.node_has_vgpus_available(node.name, mdev_type):
continue # Skips nodes without free vGPUS of required type if a type is required
# Get best node using our simple weight function (basically, the less used node, but with a little more weight on CPU)
if calc_weight(node) < calc_weight(best):
best = node
# logger.debug('Node values for best: %s %f %f', node.name, node.mem / node.maxmem * 100, node.cpu)
return best if best.status == 'online' else None
@ensure_connected
def clone_machine(
self,
vmid: int,
new_vmid: int,
name: str,
description: typing.Optional[str],
as_linked_clone: bool,
use_node: typing.Optional[str] = None,
use_storage: typing.Optional[str] = None,
use_pool: typing.Optional[str] = None,
must_have_vgpus: typing.Optional[bool] = None,
) -> types.VmCreationResult:
vmInfo = self.get_machine_info(vmid)
src_node = vmInfo.node
if not use_node:
logger.debug('Selecting best node')
# If storage is not shared, must be done on same as origin
if use_storage and self.get_storage(use_storage, vmInfo.node).shared:
node = self.get_best_node_for_machine(
min_memory=-1, must_have_vgpus=must_have_vgpus, mdev_type=vmInfo.vgpu_type
)
if node is None:
raise ProxmoxError(
f'No switable node available for new vm {name} on Proxmox (check memory and VGPUS, space...)'
)
use_node = node.name
else:
use_node = src_node
# Check if mustHaveVGPUS is compatible with the node
if must_have_vgpus is not None and must_have_vgpus != bool(self.list_node_gpu_devices(use_node)):
raise ProxmoxNoGPUError(f'Node "{use_node}" does not have VGPUS and they are required')
if self.node_has_vgpus_available(use_node, vmInfo.vgpu_type):
raise ProxmoxNoGPUError(
f'Node "{use_node}" does not have free VGPUS of type {vmInfo.vgpu_type} (requred by VM {vmInfo.name})'
)
# From normal vm, disable "linked cloning"
if as_linked_clone and not vmInfo.template:
as_linked_clone = False
params: list[tuple[str, str]] = [
('newid', str(new_vmid)),
('name', name),
('target', use_node),
('full', str(int(not as_linked_clone))),
]
if description:
params.append(('description', description))
if use_storage and as_linked_clone is False:
params.append(('storage', use_storage))
if use_pool:
params.append(('pool', use_pool))
if as_linked_clone is False:
params.append(('format', 'qcow2')) # Ensure clone for templates is on qcow2 format
logger.debug('PARAMS: %s', params)
return types.VmCreationResult(
node=use_node,
vmid=new_vmid,
upid=types.UPID.from_dict(self._post(f'nodes/{src_node}/qemu/{vmid}/clone', data=params, node=src_node)),
)
@ensure_connected
@cached('hagrps', CACHE_DURATION, key_helper=caching_key_helper)
def list_ha_groups(self, **kwargs: typing.Any) -> list[str]:
return [g['group'] for g in self._get('cluster/ha/groups')['data']]
@ensure_connected
def enable_machine_ha(self, vmid: int, started: bool = False, group: typing.Optional[str] = None) -> None:
"""
Enable high availability for a virtual machine.
Args:
vmid (int): The ID of the virtual machine.
started (bool, optional): Whether the virtual machine should be started. Defaults to False.
group (str, optional): The group to which the virtual machine belongs. Defaults to None.
"""
self._post(
'cluster/ha/resources',
data=[
('sid', f'vm:{vmid}'),
('comment', 'UDS HA VM'),
('state', 'started' if started else 'stopped'),
('max_restart', '4'),
('max_relocate', '4'),
]
+ ([('group', group)] if group else []), # Append ha group if present
)
@ensure_connected
def disable_machine_ha(self, vmid: int) -> None:
try:
self._delete(f'cluster/ha/resources/vm%3A{vmid}')
except Exception:
logger.exception('removeFromHA')
@ensure_connected
def set_protection(self, vmid: int, node: typing.Optional[str] = None, protection: bool = False) -> None:
params: list[tuple[str, str]] = [
('protection', str(int(protection))),
]
node = node or self.get_machine_info(vmid).node
self._post(f'nodes/{node}/qemu/{vmid}/config', data=params, node=node)
@ensure_connected
def get_guest_ip_address(
self, vmid: int, node: typing.Optional[str], ip_version: typing.Literal['4', '6', ''] = ''
) -> str:
"""Returns the guest ip address of the specified machine"""
try:
node = node or self.get_machine_info(vmid).node
ifaces_list: list[dict[str, typing.Any]] = self._get(
f'nodes/{node}/qemu/{vmid}/agent/network-get-interfaces',
node=node,
)['data']['result']
# look for first non-localhost interface with an ip address
for iface in ifaces_list:
if iface['name'] != 'lo' and 'ip-addresses' in iface:
for ip in iface['ip-addresses']:
if ip['ip-address'].startswith('127.'):
continue
if ip_version == '4' and ip.get('ip-address-type') != 'ipv4':
continue
elif ip_version == '6' and ip.get('ip-address-type') != 'ipv6':
continue
return ip['ip-address']
except Exception as e:
logger.info('Error getting guest ip address for machine %s: %s', vmid, e)
raise ProxmoxError(f'No ip address for vm {vmid}: {e}')
raise ProxmoxError('No ip address found for vm {}'.format(vmid))
@ensure_connected
def remove_machine(self, vmid: int, node: typing.Optional[str] = None, purge: bool = True) -> types.UPID:
node = node or self.get_machine_info(vmid).node
return types.UPID.from_dict(self._delete(f'nodes/{node}/qemu/{vmid}?purge=1', node=node))
@ensure_connected
def list_snapshots(self, vmid: int, node: typing.Optional[str] = None) -> list[types.SnapshotInfo]:
node = node or self.get_machine_info(vmid).node
try:
return [
types.SnapshotInfo.from_dict(s)
for s in self._get(f'nodes/{node}/qemu/{vmid}/snapshot', node=node)['data']
]
except Exception:
return [] # If we can't get snapshots, just return empty list
@ensure_connected
@cached('snapshots', CACHE_DURATION, key_helper=caching_key_helper)
def supports_snapshot(self, vmid: int, node: typing.Optional[str] = None) -> bool:
# If machine uses tpm, snapshots are not supported
return not self.get_machine_configuration(vmid, node).tpmstate0
@ensure_connected
def create_snapshot(
self,
vmid: int,
node: 'str|None' = None,
name: typing.Optional[str] = None,
description: typing.Optional[str] = None,
) -> types.UPID:
if self.supports_snapshot(vmid, node) is False:
raise ProxmoxError('Machine does not support snapshots')
node = node or self.get_machine_info(vmid).node
# Compose a sanitized name, without spaces and with a timestamp
name = name or f'UDS-{time.time()}'
params: list[tuple[str, str]] = [
('snapname', name),
('description', description or f'UDS Snapshot created at {time.strftime("%c")}'),
]
params.append(('snapname', name or ''))
return types.UPID.from_dict(self._post(f'nodes/{node}/qemu/{vmid}/snapshot', data=params, node=node))
@ensure_connected
def remove_snapshot(
self, vmid: int, node: 'str|None' = None, name: typing.Optional[str] = None
) -> types.UPID:
node = node or self.get_machine_info(vmid).node
if name is None:
raise ProxmoxError('Snapshot name is required')
return types.UPID.from_dict(self._delete(f'nodes/{node}/qemu/{vmid}/snapshot/{name}', node=node))
@ensure_connected
def restore_snapshot(
self, vmid: int, node: 'str|None' = None, name: typing.Optional[str] = None
) -> types.UPID:
node = node or self.get_machine_info(vmid).node
if name is None:
raise ProxmoxError('Snapshot name is required')
return types.UPID.from_dict(self._post(f'nodes/{node}/qemu/{vmid}/snapshot/{name}/rollback', node=node))
@ensure_connected
def get_task(self, node: str, upid: str) -> types.TaskStatus:
return types.TaskStatus.from_dict(
self._get(f'nodes/{node}/tasks/{urllib.parse.quote(upid)}/status', node=node)
)
@ensure_connected
@cached('vms', CACHE_DURATION, key_helper=caching_key_helper)
def list_machines(
self, node: typing.Union[None, str, collections.abc.Iterable[str]] = None, **kwargs: typing.Any
) -> list[types.VMInfo]:
node_list: collections.abc.Iterable[str]
if node is None:
node_list = [n.name for n in self.get_cluster_info().nodes if n.online]
elif isinstance(node, str):
node_list = [node]
else:
node_list = node
result: list[types.VMInfo] = []
for node_name in node_list:
for vm in self._get(f'nodes/{node_name}/qemu', node=node_name)['data']:
vm['node'] = node_name
result.append(types.VMInfo.from_dict(vm))
return sorted(result, key=lambda x: '{}{}'.format(x.node, x.name))
@ensure_connected
@cached('vmip', CACHE_INFO_DURATION, key_helper=caching_key_helper)
def get_machine_pool_info(self, vmid: int, poolid: typing.Optional[str], **kwargs: typing.Any) -> types.VMInfo:
# try to locate machine in pool
node = None
if poolid:
try:
for i in self._get(f'pools/{poolid}', node=node)['data']['members']:
try:
if i['vmid'] == vmid:
node = i['node']
break
except Exception: # nosec: # If vmid is not present, just try next node
pass
except Exception: # nosec: # If pool is not present, just use default getVmInfo
pass
return self.get_machine_info(vmid, node, **kwargs)
@ensure_connected
@cached('vmin', CACHE_INFO_DURATION, key_helper=caching_key_helper)
def get_machine_info(self, vmid: int, node: typing.Optional[str] = None, **kwargs: typing.Any) -> types.VMInfo:
nodes = [types.Node(node, False, False, 0, '', '', '')] if node else self.get_cluster_info().nodes
any_node_is_down = False
for n in nodes:
try:
vm = self._get(f'nodes/{n.name}/qemu/{vmid}/status/current', node=node)['data']
vm['node'] = n.name
return types.VMInfo.from_dict(vm)
except ProxmoxConnectionError:
any_node_is_down = True # There is at least one node down when we are trying to get info
except ProxmoxAuthError:
raise
except ProxmoxError:
pass # Any other error, ignore this node (not found in that node)
if any_node_is_down:
raise ProxmoxNodeUnavailableError()
raise ProxmoxNotFound()
@ensure_connected
def get_machine_configuration(
self, vmid: int, node: typing.Optional[str] = None, **kwargs: typing.Any
) -> types.VMConfiguration:
node = node or self.get_machine_info(vmid).node
return types.VMConfiguration.from_dict(self._get(f'nodes/{node}/qemu/{vmid}/config', node=node)['data'])
@ensure_connected
def set_machine_mac(
self,
vmid: int,
mac: str,
netid: typing.Optional[str] = None,
node: typing.Optional[str] = None,
) -> None:
node = node or self.get_machine_info(vmid).node
# First, read current configuration and extract network configuration
config = self._get(f'nodes/{node}/qemu/{vmid}/config', node=node)['data']
if netid not in config:
# Get first network interface (netX where X is a number)
netid = next((k for k in config if k.startswith('net') and k[3:].isdigit()), None)
if not netid:
raise ProxmoxError('No network interface found')
netdata = config[netid]
# Update mac address, that is the first field <model>=<mac>,<other options>
netdata = re.sub(r'^([^=]+)=([^,]+),', r'\1={},'.format(mac), netdata)
logger.debug('Updating mac address for VM %s: %s=%s', vmid, netid, netdata)
self._post(
f'nodes/{node}/qemu/{vmid}/config',
data=[(netid, netdata)],
node=node,
)
@ensure_connected
def start_machine(self, vmid: int, node: typing.Optional[str] = None) -> types.UPID:
# if exitstatus is "OK" or contains "already running", all is fine
node = node or self.get_machine_info(vmid).node
return types.UPID.from_dict(self._post(f'nodes/{node}/qemu/{vmid}/status/start', node=node))
@ensure_connected
def stop_machine(self, vmid: int, node: typing.Optional[str] = None) -> types.UPID:
node = node or self.get_machine_info(vmid).node
return types.UPID.from_dict(self._post(f'nodes/{node}/qemu/{vmid}/status/stop', node=node))
@ensure_connected
def reset_machine(self, vmid: int, node: typing.Optional[str] = None) -> types.UPID:
node = node or self.get_machine_info(vmid).node
return types.UPID.from_dict(self._post(f'nodes/{node}/qemu/{vmid}/status/reset', node=node))
@ensure_connected
def suspend_machine(self, vmid: int, node: typing.Optional[str] = None) -> types.UPID:
# if exitstatus is "OK" or contains "already running", all is fine
node = node or self.get_machine_info(vmid).node
return types.UPID.from_dict(self._post(f'nodes/{node}/qemu/{vmid}/status/suspend', node=node))
@ensure_connected
def shutdown_machine(self, vmid: int, node: typing.Optional[str] = None) -> types.UPID:
# if exitstatus is "OK" or contains "already running", all is fine
node = node or self.get_machine_info(vmid).node
return types.UPID.from_dict(self._post(f'nodes/{node}/qemu/{vmid}/status/shutdown', node=node))
@ensure_connected
def convert_to_template(self, vmid: int, node: typing.Optional[str] = None) -> None:
node = node or self.get_machine_info(vmid).node
self._post(f'nodes/{node}/qemu/{vmid}/template', node=node)
# Ensure cache is reset for this VM (as it is now a template)
self.get_machine_info(vmid, force=True)
# proxmox has a "resume", but start works for suspended vm so we use it
resume_machine = start_machine
@ensure_connected
@cached('storage', CACHE_DURATION, key_helper=caching_key_helper)
def get_storage(self, storage: str, node: str, **kwargs: typing.Any) -> types.StorageInfo:
return types.StorageInfo.from_dict(
self._get(f'nodes/{node}/storage/{urllib.parse.quote(storage)}/status', node=node)['data']
)
@ensure_connected
@cached('storages', CACHE_DURATION, key_helper=caching_key_helper)
def list_storages(
self,
node: typing.Union[None, str, collections.abc.Iterable[str]] = None,
content: typing.Optional[str] = None,
**kwargs: typing.Any,
) -> list[types.StorageInfo]:
"""We use a list for storage instead of an iterator, so we can cache it..."""
nodes: collections.abc.Iterable[str]
if node is None:
nodes = [n.name for n in self.get_cluster_info().nodes if n.online]
elif isinstance(node, str):
nodes = [node]
else:
nodes = node
params = '' if not content else '?content={}'.format(urllib.parse.quote(content))
result: list[types.StorageInfo] = []
for node_name in nodes:
for storage in self._get(f'nodes/{node_name}/storage{params}', node=node_name)['data']:
storage['node'] = node_name
storage['content'] = storage['content'].split(',')
result.append(types.StorageInfo.from_dict(storage))
return result
@ensure_connected
@cached('nodeStats', CACHE_INFO_DURATION, key_helper=caching_key_helper)
def get_node_stats(self, **kwargs: typing.Any) -> list[types.NodeStats]:
return [
types.NodeStats.from_dict(nodeStat) for nodeStat in self._get('cluster/resources?type=node')['data']
]
@ensure_connected
@cached('pools', CACHE_DURATION // 6, key_helper=caching_key_helper)
def list_pools(self, **kwargs: typing.Any) -> list[types.PoolInfo]:
return [types.PoolInfo.from_dict(poolInfo) for poolInfo in self._get('pools')['data']]
@ensure_connected
@cached('pool', CACHE_DURATION, key_helper=caching_key_helper)
def get_pool_info(self, pool_id: str, retrieve_vm_names: bool = False, **kwargs: typing.Any) -> types.PoolInfo:
pool_info = types.PoolInfo.from_dict(self._get(f'pools/{pool_id}')['data'])
if retrieve_vm_names:
for i in range(len(pool_info.members)):
try:
pool_info.members[i] = pool_info.members[i]._replace(
vmname=self.get_machine_info(pool_info.members[i].vmid).name or ''
)
except Exception:
pool_info.members[i] = pool_info.members[i]._replace(
vmname=f'VM-{pool_info.members[i].vmid}'
)
return pool_info
@ensure_connected
def get_console_connection(
self, vmid: int, node: typing.Optional[str] = None
) -> typing.Optional[core_types.services.ConsoleConnectionInfo]:
"""
Gets the connetion info for the specified machine
"""
node = node or self.get_machine_info(vmid).node
res: dict[str, typing.Any] = self._post(f'nodes/{node}/qemu/{vmid}/spiceproxy', node=node)['data']
return core_types.services.ConsoleConnectionInfo(
type=res['type'],
proxy=res['proxy'],
address=res['host'],
port=res.get('port', None),
secure_port=res['tls-port'],
cert_subject=res['host-subject'],
ticket=core_types.services.ConsoleConnectionTicket(value=res['password']),
ca=res.get('ca', None),
)
# Sample data:
# 'data': {'proxy': 'http://pvealone.dkmon.com:3128',
# 'release-cursor': 'Ctrl+Alt+R',
# 'host': 'pvespiceproxy:63489cf9:101:pvealone::c934cf7f7570012bbebab9e1167402b6471aae16',
# 'delete-this-file': 1,
# 'secure-attention': 'Ctrl+Alt+Ins',
# 'title': 'VM 101 - VM-1',
# 'password': '31a189dd71ce859867e28dd68ba166a701e77eed',
# 'type': 'spice',
# 'toggle-fullscreen': 'Shift+F11',
# 'host-subject': 'OU=PVE Cluster Node,O=Proxmox Virtual Environment,CN=pvealone.dkmon.com',
# 'tls-port': 61000,
# 'ca': '-----BEGIN CERTIFICATE-----\\n......\\n-----END CERTIFICATE-----\\n'}}
@ensure_connected
def journal(self, node: str, lastentries: int = 4, **kwargs: typing.Any) -> list[str]:
try:
return self._get(f'nodes/{node}/journal?lastentries={lastentries}')['data']
except Exception:
return []

View File

@ -0,0 +1,334 @@
import datetime
import re
import typing
import collections.abc
NETWORK_RE: typing.Final[typing.Pattern[str]] = re.compile(r'([a-zA-Z0-9]+)=([^,]+)') # May have vla id at end
# Conversor from dictionary to NamedTuple
CONVERSORS: typing.Final[dict[typing.Any, collections.abc.Callable[[typing.Type[typing.Any]], typing.Any]]] = {
str: lambda x: str(x or ''),
typing.Optional[str]: lambda x: str(x) if x is not None else None, # pyright: ignore
bool: lambda x: bool(x),
typing.Optional[bool]: lambda x: bool(x) if x is not None else None, # pyright: ignore
int: lambda x: int(x or '0'), # type: ignore
typing.Optional[int]: lambda x: int(x or '0') if x is not None else None, # type: ignore
float: lambda x: float(x or '0'), # type: ignore
typing.Optional[float]: lambda x: float(x or '0') if x is not None else None, # type: ignore
datetime.datetime: lambda x: datetime.datetime.fromtimestamp(int(x)), # type: ignore
typing.Optional[datetime.datetime]: lambda x: (
datetime.datetime.fromtimestamp(int(x)) if x is not None else None # type: ignore
),
}
def _from_dict(
type: type[typing.NamedTuple],
dictionary: collections.abc.MutableMapping[str, typing.Any],
extra: typing.Optional[collections.abc.Mapping[str, typing.Any]] = None,
) -> typing.Any:
extra = extra or {}
return type(
**{
k: typing.cast(typing.Callable[..., typing.Any], CONVERSORS.get(type.__annotations__.get(k, str), lambda x: x))(
dictionary.get(k, extra.get(k, None))
)
for k in type._fields # pyright: ignore # _fields is a NamedTuple attribute that contains fields
}
)
# Need to be "NamedTuple"s because we use _fields attribute
class Cluster(typing.NamedTuple):
name: str
version: str
id: str
nodes: int
quorate: int
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'Cluster':
return _from_dict(Cluster, dictionary)
class Node(typing.NamedTuple):
name: str
online: bool
local: bool
nodeid: int
ip: str
level: str
id: str
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'Node':
return _from_dict(Node, dictionary)
class NodeStats(typing.NamedTuple):
name: str
status: str
uptime: int
disk: int
maxdisk: int
level: str
id: str
mem: int
maxmem: int
cpu: float
maxcpu: int
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'NodeStats':
dictionary['name'] = dictionary['node']
return _from_dict(NodeStats, dictionary)
@staticmethod
def empty() -> 'NodeStats':
return NodeStats(
name='',
status='offline',
uptime=0,
disk=0,
maxdisk=0,
level='',
id='',
mem=1,
maxmem=1,
cpu=1,
maxcpu=1,
)
class ClusterInfo(typing.NamedTuple):
cluster: typing.Optional[Cluster]
nodes: list[Node]
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'ClusterInfo':
nodes: list[Node] = []
cluster: typing.Optional[Cluster] = None
for i in dictionary['data']:
if i['type'] == 'cluster':
cluster = Cluster.from_dict(i)
else:
nodes.append(Node.from_dict(i))
return ClusterInfo(cluster=cluster, nodes=nodes)
class UPID(typing.NamedTuple):
node: str
pid: int
pstart: int
starttime: datetime.datetime
type: str
vmid: int
user: str
upid: str
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'UPID':
upid = dictionary['data']
d = upid.split(':')
return UPID(
node=d[1],
pid=int(d[2], 16),
pstart=int(d[3], 16),
starttime=datetime.datetime.fromtimestamp(int(d[4], 16)),
type=d[5],
vmid=int(d[6]),
user=d[7],
upid=upid,
)
class TaskStatus(typing.NamedTuple):
node: str
pid: int
pstart: int
starttime: datetime.datetime
type: str
status: str
exitstatus: str
user: str
upid: str
id: str
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'TaskStatus':
return _from_dict(TaskStatus, dictionary['data'])
def is_running(self) -> bool:
return self.status == 'running'
def is_finished(self) -> bool:
return self.status == 'stopped'
def is_completed(self) -> bool:
return self.is_finished() and self.exitstatus == 'OK'
def is_errored(self) -> bool:
return self.is_finished() and not self.is_completed()
class NetworkConfiguration(typing.NamedTuple):
net: str
type: str
mac: str
@staticmethod
def from_str(net: str, value: str) -> 'NetworkConfiguration':
v = NETWORK_RE.match(value)
type = mac = ''
if v:
type, mac = v.group(1), v.group(2)
return NetworkConfiguration(net=net, type=type, mac=mac)
class VMInfo(typing.NamedTuple):
status: str
vmid: int
node: str
template: bool
agent: typing.Optional[str]
cpus: typing.Optional[int]
lock: typing.Optional[str] # if suspended, lock == "suspended" & qmpstatus == "stopped"
disk: typing.Optional[int]
maxdisk: typing.Optional[int]
mem: typing.Optional[int]
maxmem: typing.Optional[int]
name: typing.Optional[str]
pid: typing.Optional[int]
qmpstatus: typing.Optional[str] # stopped, running, paused (in memory)
tags: typing.Optional[str]
uptime: typing.Optional[int]
netin: typing.Optional[int]
netout: typing.Optional[int]
diskread: typing.Optional[int]
diskwrite: typing.Optional[int]
vgpu_type: typing.Optional[str]
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'VMInfo':
vgpu_type = None
# Look for vgpu type if present
for k, v in dictionary.items():
if k.startswith('hostpci'):
for i in v.split(','):
if i.startswith('mdev='):
vgpu_type = i[5:]
break # found it, stop looking
if vgpu_type is not None:
break # Already found it, stop looking
data = _from_dict(VMInfo, dictionary, {'vgpu_type': vgpu_type})
return data
class VMConfiguration(typing.NamedTuple):
name: str
vga: str
sockets: int
cores: int
vmgenid: str
digest: str
networks: list[NetworkConfiguration]
tpmstate0: typing.Optional[str]
template: bool
@staticmethod
def from_dict(src: collections.abc.MutableMapping[str, typing.Any]) -> 'VMConfiguration':
nets: list[NetworkConfiguration] = []
for k in src.keys():
if k[:3] == 'net':
nets.append(NetworkConfiguration.from_str(k, src[k]))
src['networks'] = nets
return _from_dict(VMConfiguration, src)
class VmCreationResult(typing.NamedTuple):
node: str
vmid: int
upid: UPID
class StorageInfo(typing.NamedTuple):
node: str
storage: str
content: tuple[str, ...]
type: str
shared: bool
active: bool
used: int
avail: int
total: int
used_fraction: float
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'StorageInfo':
return _from_dict(StorageInfo, dictionary)
class PoolMemberInfo(typing.NamedTuple):
id: str
node: str
storage: str
type: str
vmid: int
vmname: str
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'PoolMemberInfo':
return _from_dict(PoolMemberInfo, dictionary)
class PoolInfo(typing.NamedTuple):
poolid: str
comments: str
members: list[PoolMemberInfo]
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'PoolInfo':
if 'members' in dictionary:
members: list[PoolMemberInfo] = [PoolMemberInfo.from_dict(i) for i in dictionary['members']]
else:
members = []
dictionary['comments'] = dictionary.get('comments', '')
dictionary['members'] = members
return _from_dict(PoolInfo, dictionary=dictionary)
class SnapshotInfo(typing.NamedTuple):
name: str
description: str
parent: typing.Optional[str]
snaptime: typing.Optional[int]
vmstate: typing.Optional[bool]
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'SnapshotInfo':
return _from_dict(SnapshotInfo, dictionary)
class VGPUInfo(typing.NamedTuple):
name: str
description: str
device: str
available: bool
type: str
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'VGPUInfo':
return _from_dict(VGPUInfo, dictionary)

View File

@ -0,0 +1,148 @@
# -*- 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 logging
import typing
from uds.core import types
from uds.core.services.specializations.fixed_machine.fixed_userservice import FixedUserService, Operation
from uds.core.util import autoserializable
from . import client
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from . import service_fixed
logger = logging.getLogger(__name__)
class ProxmoxUserServiceFixed(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
def _store_task(self, upid: 'client.types.UPID') -> None:
self._task = '\t'.join([upid.node, upid.upid])
def _retrieve_task(self) -> tuple[str, str]:
vals = self._task.split('\t')
return (vals[0], vals[1])
# Utility overrides for type checking...
def service(self) -> 'service_fixed.ProxmoxServiceFixed':
return typing.cast('service_fixed.ProxmoxServiceFixed', super().service())
def set_ready(self) -> types.states.TaskState:
if self.cache.get('ready') == '1':
return types.states.TaskState.FINISHED
try:
vminfo = self.service().get_machine_info(int(self._vmid))
except client.ProxmoxConnectionError:
raise # If connection fails, let it fail on parent
except Exception as e:
return self._error(f'Machine not found: {e}')
if vminfo.status == 'stopped':
self._queue = [Operation.START, Operation.FINISH]
return self._execute_queue()
self.cache.put('ready', '1')
return types.states.TaskState.FINISHED
def reset(self) -> types.states.TaskState:
"""
o Proxmox, reset operation just shutdowns it until v3 support is removed
"""
if self._vmid != '':
try:
self.service().provider().reset_machine(int(self._vmid))
except Exception: # nosec: if cannot reset, ignore it
pass # If could not reset, ignore it...
return types.states.TaskState.FINISHED
def process_ready_from_os_manager(self, data: typing.Any) -> types.states.TaskState:
return types.states.TaskState.FINISHED
def error(self, reason: str) -> types.states.TaskState:
return self._error(reason)
def start_machine(self) -> None:
try:
vminfo = self.service().get_machine_info(int(self._vmid))
except client.ProxmoxConnectionError:
self._retry_later()
return
except Exception as e:
raise Exception('Machine not found on start machine') from e
if vminfo.status == 'stopped':
self._store_task(self.service().provider().start_machine(int(self._vmid)))
# Check methods
def _check_task_finished(self) -> types.states.TaskState:
if self._task == '':
return types.states.TaskState.FINISHED
node, upid = self._retrieve_task()
try:
task = self.service().provider().get_task_info(node, upid)
except client.ProxmoxConnectionError:
return types.states.TaskState.RUNNING # Try again later
if task.is_errored():
return self._error(task.exitstatus)
if task.is_completed():
return types.states.TaskState.FINISHED
return types.states.TaskState.RUNNING
# Check methods
def start_checker(self) -> types.states.TaskState:
"""
Checks if machine has started
"""
return self._check_task_finished()

View File

@ -0,0 +1,721 @@
# -*- 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, consts, types
from uds.core.managers.userservice import UserServiceManager
from uds.core.util import log, autoserializable
from uds.core.util.model import sql_stamp_seconds
from .jobs import ProxmoxDeferredRemovalOrig
from . import client
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds import models
from .service_linked import ProxmoxServiceLinked
from .publication import ProxmoxPublication
logger = logging.getLogger(__name__)
class Operation(enum.IntEnum):
"""
Operation codes for Proxmox deployment
"""
CREATE = 0
START = 1
STOP = 2
SHUTDOWN = 3
REMOVE = 4
WAIT = 5
ERROR = 6
FINISH = 7
RETRY = 8
GET_MAC = 9
GRACEFUL_STOP = 10
opUnknown = 99
@staticmethod
def from_int(value: int) -> 'Operation':
try:
return Operation(value)
except ValueError:
return Operation.opUnknown
# The difference between "SHUTDOWN" and "GRACEFUL_STOP" is that the first one
# is used to "best try to stop" the machine to move to L2 (that is, if it cannot be stopped,
# it will be moved to L2 anyway, but keeps running), and the second one is used to "best try to stop"
# the machine when destoying it (that is, if it cannot be stopped, it will be destroyed anyway after a
# timeout of at most GUEST_SHUTDOWN_WAIT seconds)
# UP_STATES = ('up', 'reboot_in_progress', 'powering_up', 'restoring_state')
class ProxmoxUserserviceLinked(services.UserService, 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 Proxmox deployments (user machines in this case) is here.
"""
# : Recheck every this seconds by default (for task methods)
suggested_delay = 12
_name = autoserializable.StringField(default='')
_ip = autoserializable.StringField(default='')
_mac = autoserializable.StringField(default='')
_task = autoserializable.StringField(default='')
_vmid = autoserializable.StringField(default='')
_reason = autoserializable.StringField(default='')
_queue = autoserializable.ListField[Operation]()
# own vars
# _name: str
# _ip: str
# _mac: str
# _task: str
# _vmid: str
# _reason: str
# _queue: list[int]
# Utility overrides for type checking...
def service(self) -> 'ProxmoxServiceLinked':
return typing.cast('ProxmoxServiceLinked', super().service())
def publication(self) -> 'ProxmoxPublication':
pub = super().publication()
if pub is None:
raise Exception('No publication for this element!')
return typing.cast('ProxmoxPublication', pub)
def unmarshal(self, data: bytes) -> None:
"""
Does nothing here also, all data are keeped at environment storage
"""
if not data.startswith(b'v'):
return super().unmarshal(data)
vals = data.split(b'\1')
if vals[0] == b'v1':
self._name = vals[1].decode('utf8')
self._ip = vals[2].decode('utf8')
self._mac = vals[3].decode('utf8')
self._task = vals[4].decode('utf8')
self._vmid = vals[5].decode('utf8')
self._reason = vals[6].decode('utf8')
self._queue = [Operation.from_int(i) for i in pickle.loads(vals[7])] # nosec: controled data
self.mark_for_upgrade() # Flag so manager can save it again with new format
def get_name(self) -> str:
if self._name == '':
try:
self._name = self.name_generator().get(
self.service().get_basename(), self.service().get_lenname()
)
except KeyError:
return consts.NO_MORE_NAMES
return self._name
def set_ip(self, ip: str) -> None:
logger.debug('Setting IP to %s', ip)
self._ip = ip
def get_unique_id(self) -> str:
"""
Return and unique identifier for this service.
In our case, we will generate a mac name, that can be also as sample
of 'mac' generator use, and probably will get used something like this
at some services.
The get method of a mac generator takes one param, that is the mac range
to use to get an unused mac.
"""
if self._mac == '':
self._mac = self.mac_generator().get(self.service().get_macs_range())
return self._mac
def get_ip(self) -> str:
return self._ip
def set_ready(self) -> types.states.TaskState:
if self.cache.get('ready') == '1':
return types.states.TaskState.FINISHED
try:
vmInfo = self.service().get_machine_info(int(self._vmid))
except client.ProxmoxConnectionError as e:
return self._error(f'Could not connect to Proxmox: {e}')
except Exception as e:
return self._error(f'Machine not found: {e}')
if vmInfo.status == 'stopped':
self._queue = [Operation.START, Operation.FINISH]
return self._execute_queue()
self.cache.put('ready', '1')
return types.states.TaskState.FINISHED
def reset(self) -> types.states.TaskState:
"""
o Proxmox, reset operation just shutdowns it until v3 support is removed
"""
if self._vmid != '':
try:
self.service().provider().reset_machine(int(self._vmid))
except Exception: # nosec: if cannot reset, ignore it
pass # If could not reset, ignore it...
return types.states.TaskState.FINISHED
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 = '', # pylint: disable=unused-argument
) -> None:
script = f'''import sys
if sys.platform == 'win32':
from uds import operations
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)
dbService = self.db_obj()
if dbService:
try:
UserServiceManager().send_script(dbService, script)
except Exception as e:
logger.info('Exception sending loggin to %s: %s', dbService, e)
def process_ready_from_os_manager(self, data: typing.Any) -> types.states.TaskState:
# Here we will check for suspending the VM (when full ready)
logger.debug('Checking if cache 2 for %s', self._name)
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_queue()
# Do not need to go to level 2 (opWait is in fact "waiting for moving machine to cache level 2)
return types.states.TaskState.FINISHED
def deploy_for_user(self, user: 'models.User') -> types.states.TaskState:
"""
Deploys an service instance for an user.
"""
logger.debug('Deploying for user')
self._init_queue_for_deploy(False)
return self._execute_queue()
def deploy_for_cache(self, level: types.services.CacheLevel) -> types.states.TaskState:
"""
Deploys an service instance for cache
"""
self._init_queue_for_deploy(level == types.services.CacheLevel.L2)
return self._execute_queue()
def _init_queue_for_deploy(self, cache_l2: bool = False) -> None:
if cache_l2 is False:
self._queue = [Operation.CREATE, Operation.GET_MAC, Operation.START, Operation.FINISH]
else:
self._queue = [
Operation.CREATE,
Operation.GET_MAC,
Operation.START,
Operation.WAIT,
Operation.SHUTDOWN,
Operation.FINISH,
]
def _store_task(self, upid: 'client.types.UPID') -> None:
self._task = ','.join([upid.node, upid.upid])
def _retrieve_task(self) -> tuple[str, str]:
vals = self._task.split(',')
return (vals[0], vals[1])
def _get_current_op(self) -> Operation:
if not self._queue:
return Operation.FINISH
return self._queue[0]
def _pop_current_op(self) -> Operation:
if not self._queue:
return Operation.FINISH
res = self._queue.pop(0)
return res
def _push_front_op(self, op: Operation) -> None:
self._queue.insert(0, op)
def _retry_later(self) -> str:
self._push_front_op(Operation.RETRY)
return types.states.TaskState.RUNNING
def _error(self, reason: typing.Union[str, Exception]) -> types.states.TaskState:
"""
Internal method to set object as error state
Returns:
types.states.TaskState.ERROR, so we can do "return self.__error(reason)"
"""
reason = str(reason)
logger.debug('Setting error state, reason: %s', reason)
self.do_log(log.LogLevel.ERROR, reason)
if self._vmid != '': # Powers off
ProxmoxDeferredRemovalOrig.remove(self.service().provider(), int(self._vmid))
self._queue = [Operation.ERROR]
self._reason = reason
return types.states.TaskState.ERROR
def _execute_queue(self) -> types.states.TaskState:
self._debug('executeQueue')
op = self._get_current_op()
if op == Operation.ERROR:
return types.states.TaskState.ERROR
if op == Operation.FINISH:
return types.states.TaskState.FINISHED
try:
operation_executor = _EXECUTORS.get(op, None)
if operation_executor is None:
return self._error(f'Unknown operation found at execution queue ({op})')
operation_executor(self)
return types.states.TaskState.RUNNING
except Exception as e:
return self._error(e)
# Queue execution methods
def _retry(self) -> None:
"""
Used to retry an operation
In fact, this will not be never invoked, unless we push it twice, because
check_state method will "pop" first item when a check operation returns types.states.TaskState.FINISHED
At executeQueue this return value will be ignored, and it will only be used at check_state
"""
pass
def _retry_checker(self) -> types.states.TaskState:
"""
This method is not used, because retry operation is never used
"""
return types.states.TaskState.FINISHED
def _wait(self) -> None:
"""
Executes opWait, it simply waits something "external" to end
"""
pass
def _wait_checker(self) -> types.states.TaskState:
"""
Wait checker waits forever, until something external wakes it up
"""
return types.states.TaskState.RUNNING
def _create(self) -> None:
"""
Deploys a machine from template for user/cache
"""
template_id = self.publication().machine()
name = self.get_name()
if name == consts.NO_MORE_NAMES:
raise Exception(
'No more names available for this service. (Increase digits for this service to fix)'
)
comments = 'UDS Linked clone'
task_result = self.service().clone_machine(name, comments, template_id)
self._store_task(task_result.upid)
self._vmid = str(task_result.vmid)
def _remove(self) -> None:
"""
Removes a machine from system
"""
try:
vm_info = self.service().get_machine_info(int(self._vmid))
except Exception as e:
raise Exception('Machine not found on remove machine') from e
if vm_info.status != 'stopped':
logger.debug('Info status: %s', vm_info)
self._queue = [Operation.STOP, Operation.REMOVE, Operation.FINISH]
self._execute_queue()
self._store_task(self.service().remove_machine(int(self._vmid)))
def _start_machine(self) -> None:
try:
vm_info = self.service().get_machine_info(int(self._vmid))
except client.ProxmoxConnectionError:
self._retry_later()
return
except Exception as e:
raise Exception('Machine not found on start machine') from e
if vm_info.status == 'stopped':
self._store_task(self.service().provider().start_machine(int(self._vmid)))
def _stop_machine(self) -> None:
try:
vm_info = self.service().get_machine_info(int(self._vmid))
except client.ProxmoxConnectionError:
self._retry_later()
return
except Exception as e:
raise Exception('Machine not found on stop machine') from e
if vm_info.status != 'stopped':
logger.debug('Stopping machine %s', vm_info)
self._store_task(self.service().provider().stop_machine(int(self._vmid)))
def _shutdown_machine(self) -> None:
try:
vm_info = self.service().get_machine_info(int(self._vmid))
except client.ProxmoxConnectionError:
self._retry_later()
return
except Exception as e:
raise Exception('Machine not found or suspended machine') from e
if vm_info.status != 'stopped':
self._store_task(self.service().provider().shutdown_machine(int(self._vmid)))
def _gracely_stop(self) -> None:
"""
Tries to stop machine using qemu guest tools
If it takes too long to stop, or qemu guest tools are not installed,
will use "power off" "a las bravas"
"""
self._task = ''
shutdown = -1 # Means machine already stopped
try:
vm_info = self.service().get_machine_info(int(self._vmid))
except client.ProxmoxConnectionError:
self._retry_later()
return
except Exception as e:
raise Exception('Machine not found on stop machine') from e
if vm_info.status != 'stopped':
self._store_task(self.service().provider().shutdown_machine(int(self._vmid)))
shutdown = sql_stamp_seconds()
logger.debug('Stoped vm using guest tools')
self.storage.save_pickled('shutdown', shutdown)
def _update_machine_mac_and_ha(self) -> None:
try:
# Note: service will only enable ha if it is configured to do so
self.service().enable_machine_ha(int(self._vmid), True) # Enable HA before continuing here
# Set vm mac address now on first interface
self.service().provider().set_machine_mac(int(self._vmid), self.get_unique_id())
except client.ProxmoxConnectionError:
self._retry_later()
return
except Exception as e:
logger.exception('Setting HA and MAC on proxmox')
raise Exception(f'Error setting MAC and HA on proxmox: {e}') from e
# Check methods
def _check_task_finished(self) -> types.states.TaskState:
if self._task == '':
return types.states.TaskState.FINISHED
node, upid = self._retrieve_task()
try:
task = self.service().provider().get_task_info(node, upid)
except client.ProxmoxConnectionError:
return types.states.TaskState.RUNNING # Try again later
if task.is_errored():
return self._error(task.exitstatus)
if task.is_completed():
return types.states.TaskState.FINISHED
return types.states.TaskState.RUNNING
def _create_checker(self) -> types.states.TaskState:
"""
Checks the state of a deploy for an user or cache
"""
return self._check_task_finished()
def _start_checker(self) -> types.states.TaskState:
"""
Checks if machine has started
"""
return self._check_task_finished()
def _stop_checker(self) -> types.states.TaskState:
"""
Checks if machine has stoped
"""
return self._check_task_finished()
def _shutdown_checker(self) -> types.states.TaskState:
"""
Check if the machine has suspended
"""
return self._check_task_finished()
def _graceful_stop_checker(self) -> types.states.TaskState:
"""
Check if the machine has gracely stopped (timed shutdown)
"""
shutdown_start = self.storage.read_pickled('shutdown')
logger.debug('Shutdown start: %s', shutdown_start)
if shutdown_start < 0: # Was already stopped
# Machine is already stop
logger.debug('Machine WAS stopped')
return types.states.TaskState.FINISHED
if shutdown_start == 0: # Was shut down a las bravas
logger.debug('Macine DO NOT HAVE guest tools')
return self._stop_checker()
logger.debug('Checking State')
# Check if machine is already stopped
if self.service().get_machine_info(int(self._vmid)).status == 'stopped':
return types.states.TaskState.FINISHED # It's stopped
logger.debug('State is running')
if sql_stamp_seconds() - shutdown_start > consts.os.MAX_GUEST_SHUTDOWN_WAIT:
logger.debug('Time is consumed, falling back to stop')
self.do_log(
log.LogLevel.ERROR,
f'Could not shutdown machine using soft power off in time ({consts.os.MAX_GUEST_SHUTDOWN_WAIT} seconds). Powering off.',
)
# Not stopped by guest in time, but must be stopped normally
self.storage.save_pickled('shutdown', 0)
self._stop_machine() # Launch "hard" stop
return types.states.TaskState.RUNNING
def _remove_checker(self) -> types.states.TaskState:
"""
Checks if a machine has been removed
"""
return self._check_task_finished()
def _mac_checker(self) -> types.states.TaskState:
"""
Checks if change mac operation has finished.
Changing nic configuration is 1-step operation, so when we check it here, it is already done
"""
return types.states.TaskState.FINISHED
def check_state(self) -> types.states.TaskState:
"""
Check what operation is going on, and acts acordly to it
"""
self._debug('check_state')
op = self._get_current_op()
if op == Operation.ERROR:
return types.states.TaskState.ERROR
if op == Operation.FINISH:
return types.states.TaskState.FINISHED
try:
operation_checker = _CHECKERS.get(op, None)
if operation_checker is None:
return self._error(f'Unknown operation found at check queue ({op})')
state = operation_checker(self)
if state == types.states.TaskState.FINISHED:
self._pop_current_op() # Remove runing op
return self._execute_queue()
return state
except Exception as e:
return self._error(e)
def move_to_cache(self, level: types.services.CacheLevel) -> types.states.TaskState:
"""
Moves machines between cache levels
"""
if Operation.REMOVE in self._queue:
return types.states.TaskState.RUNNING
if level == types.services.CacheLevel.L1:
self._queue = [Operation.START, Operation.FINISH]
else:
self._queue = [Operation.START, Operation.SHUTDOWN, Operation.FINISH]
return self._execute_queue()
def error_reason(self) -> str:
"""
Returns the reason of the error.
Remember that the class is responsible of returning this whenever asked
for it, and it will be asked everytime it's needed to be shown to the
user (when the administation asks for it).
"""
return self._reason
def destroy(self) -> types.states.TaskState:
"""
Invoked for destroying a deployed service
"""
self._debug('destroy')
if self._vmid == '':
self._queue = []
self._reason = "canceled"
return types.states.TaskState.FINISHED
# If executing something, wait until finished to remove it
# We simply replace the execution queue
op = self._get_current_op()
if op == Operation.ERROR:
return self._error('Machine is already in error state!')
lst: list[Operation] = [] if not self.service().try_graceful_shutdown() else [Operation.GRACEFUL_STOP]
queue = lst + [Operation.STOP, Operation.REMOVE, Operation.FINISH]
if op in (Operation.FINISH, Operation.WAIT):
self._queue[:] = queue
return self._execute_queue()
self._queue = [op] + queue
# Do not execute anything.here, just continue normally
return types.states.TaskState.RUNNING
def cancel(self) -> types.states.TaskState:
"""
This is a task method. As that, the excepted return values are
State values RUNNING, FINISHED or ERROR.
This can be invoked directly by an administration or by the clean up
of the deployed service (indirectly).
When administrator requests it, the cancel is "delayed" and not
invoked directly.
"""
return self.destroy()
@staticmethod
def _op2str(op: Operation) -> str:
return {
Operation.CREATE: 'create',
Operation.START: 'start',
Operation.STOP: 'stop',
Operation.SHUTDOWN: 'shutdown',
Operation.GRACEFUL_STOP: 'gracely stop',
Operation.REMOVE: 'remove',
Operation.WAIT: 'wait',
Operation.ERROR: 'error',
Operation.FINISH: 'finish',
Operation.RETRY: 'retry',
Operation.GET_MAC: 'getting mac',
}.get(op, '????')
def _debug(self, txt: str) -> None:
logger.debug(
'State at %s: name: %s, ip: %s, mac: %s, vmid:%s, queue: %s',
txt,
self._name,
self._ip,
self._mac,
self._vmid,
[ProxmoxUserserviceLinked._op2str(op) for op in self._queue],
)
_EXECUTORS: typing.Final[
collections.abc.Mapping[
Operation, typing.Optional[collections.abc.Callable[[ProxmoxUserserviceLinked], None]]
]
] = {
Operation.CREATE: ProxmoxUserserviceLinked._create,
Operation.RETRY: ProxmoxUserserviceLinked._retry,
Operation.START: ProxmoxUserserviceLinked._start_machine,
Operation.STOP: ProxmoxUserserviceLinked._stop_machine,
Operation.GRACEFUL_STOP: ProxmoxUserserviceLinked._gracely_stop,
Operation.SHUTDOWN: ProxmoxUserserviceLinked._shutdown_machine,
Operation.WAIT: ProxmoxUserserviceLinked._wait,
Operation.REMOVE: ProxmoxUserserviceLinked._remove,
Operation.GET_MAC: ProxmoxUserserviceLinked._update_machine_mac_and_ha,
}
_CHECKERS: dict[
Operation, typing.Optional[collections.abc.Callable[[ProxmoxUserserviceLinked], types.states.TaskState]]
] = {
Operation.CREATE: ProxmoxUserserviceLinked._create_checker,
Operation.RETRY: ProxmoxUserserviceLinked._retry_checker,
Operation.WAIT: ProxmoxUserserviceLinked._wait_checker,
Operation.START: ProxmoxUserserviceLinked._start_checker,
Operation.STOP: ProxmoxUserserviceLinked._stop_checker,
Operation.GRACEFUL_STOP: ProxmoxUserserviceLinked._graceful_stop_checker,
Operation.SHUTDOWN: ProxmoxUserserviceLinked._shutdown_checker,
Operation.REMOVE: ProxmoxUserserviceLinked._remove_checker,
Operation.GET_MAC: ProxmoxUserserviceLinked._mac_checker,
}

View File

@ -0,0 +1,95 @@
#
# Copyright (c) 2012-2023 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 as _
from uds.core import types
from uds.core.ui.user_interface import gui
from uds import models
logger = logging.getLogger(__name__)
def get_storage(parameters: typing.Any) -> types.ui.CallbackResultType:
from .provider import ProxmoxProvider # pylint: disable=import-outside-toplevel
logger.debug('Parameters received by getResources Helper: %s', parameters)
provider = typing.cast(
ProxmoxProvider, models.Provider.objects.get(uuid=parameters['prov_uuid']).get_instance()
)
# Obtains datacenter from cluster
try:
vm_info = provider.get_machine_info(int(parameters['machine']))
except Exception:
return []
res: list[types.ui.ChoiceItem] = []
# Get storages for that datacenter
for storage in sorted(provider.list_storages(vm_info.node), key=lambda x: int(not x.shared)):
if storage.type in ('lvm', 'iscsi', 'iscsidirect'):
continue
space, free = (
storage.avail / 1024 / 1024 / 1024,
(storage.avail - storage.used) / 1024 / 1024 / 1024,
)
extra = _(' shared') if storage.shared else _(' (bound to {})').format(vm_info.node)
res.append(
gui.choice_item(storage.storage, f'{storage.storage} ({space:4.2f} GB/{free:4.2f} GB){extra}')
)
data: types.ui.CallbackResultType = [{'name': 'datastore', 'choices': res}]
logger.debug('return data: %s', data)
return data
def get_machines(parameters: typing.Any) -> types.ui.CallbackResultType:
from .provider import ProxmoxProvider # pylint: disable=import-outside-toplevel
logger.debug('Parameters received by getResources Helper: %s', parameters)
provider = typing.cast(
ProxmoxProvider, models.Provider.objects.get(uuid=parameters['prov_uuid']).get_instance()
)
# Obtains datacenter from cluster
try:
pool_info = provider.get_pool_info(parameters['pool'], retrieve_vm_names=True)
except Exception:
return []
return [
{
'name': 'machines',
'choices': [gui.choice_item(member.vmid, member.vmname) for member in pool_info.members],
}
]

View File

@ -0,0 +1,149 @@
#
# Copyright (c) 2012-2023 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 time
import logging
import typing
from uds.core import jobs
from uds.models import Provider
from uds.core.util.model import sql_stamp_seconds
from uds.core.util.unique_id_generator import UniqueIDGenerator
from . import provider
from . import client
MAX_VMID_LIFE_SECS = 365 * 24 * 60 * 60 * 3 # 3 years for "reseting"
logger = logging.getLogger(__name__)
class ProxmoxDeferredRemovalOrig(jobs.Job):
frecuency = 60 * 5 # Once every NN minutes
friendly_name = 'Proxmox removal'
counter = 0
@staticmethod
def remove(providerInstance: 'provider.ProxmoxProvider', vmId: int) -> None:
logger.debug(
'Adding %s from %s to defeffed removal process', vmId, providerInstance
)
ProxmoxDeferredRemovalOrig.counter += 1
try:
# First check state & stop machine if needed
vmInfo = providerInstance.get_machine_info(vmId)
if vmInfo.status == 'running':
# If running vm, simply stops it and wait for next
ProxmoxDeferredRemovalOrig.waitForTaskFinish(
providerInstance, providerInstance.stop_machine(vmId)
)
ProxmoxDeferredRemovalOrig.waitForTaskFinish(
providerInstance, providerInstance.remove_machine(vmId)
)
except client.ProxmoxNotFound:
return # Machine does not exists
except Exception as e:
providerInstance.storage.save_to_db('tr' + str(vmId), str(vmId), attr1='tRm')
logger.info(
'Machine %s could not be removed right now, queued for later: %s',
vmId,
e,
)
@staticmethod
def waitForTaskFinish(
providerInstance: 'provider.ProxmoxProvider',
upid: 'client.types.UPID',
maxWait: int = 30,
) -> bool:
counter = 0
while (
providerInstance.get_task_info(upid.node, upid.upid).is_running()
and counter < maxWait
):
time.sleep(0.3)
counter += 1
return counter < maxWait
def run(self) -> None:
dbProvider: Provider
# Look for Providers of type proxmox
for dbProvider in Provider.objects.filter(
maintenance_mode=False, data_type=provider.ProxmoxProvider.type_type
):
logger.debug('Provider %s if os type proxmox', dbProvider)
storage = dbProvider.get_environment().storage
instance: provider.ProxmoxProvider = typing.cast(
provider.ProxmoxProvider, dbProvider.get_instance()
)
for i in storage.filter('tRm'):
vmId = int(i[1].decode())
try:
vmInfo = instance.get_machine_info(vmId)
logger.debug('Found %s for removal %s', vmId, i)
# If machine is powered on, tries to stop it
# tries to remove in sync mode
if vmInfo.status == 'running':
ProxmoxDeferredRemovalOrig.waitForTaskFinish(
instance, instance.stop_machine(vmId)
)
return
if (
vmInfo.status == 'stopped'
): # Machine exists, try to remove it now
ProxmoxDeferredRemovalOrig.waitForTaskFinish(
instance, instance.remove_machine(vmId)
)
# It this is reached, remove check
storage.remove('tr' + str(vmId))
except client.ProxmoxNotFound:
storage.remove('tr' + str(vmId)) # VM does not exists anymore
except Exception as e: # Any other exception wil be threated again
# instance.log('Delayed removal of %s has failed: %s. Will retry later', vmId, e)
logger.error('Delayed removal of %s failed: %s', i, e)
logger.debug('Deferred removal for proxmox finished')
class ProxmoxVmidReleaserOrig(jobs.Job):
frecuency = 60 * 60 * 24 * 30 # Once a month
friendly_name = 'Proxmox maintenance'
def run(self) -> None:
logger.debug('Proxmox Vmid releader running')
gen = UniqueIDGenerator('proxmoxvmid', 'proxmox')
gen.release_older_than(sql_stamp_seconds() - MAX_VMID_LIFE_SECS)

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,350 @@
#
# 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 logging
import typing
from django.utils.translation import gettext_noop as _
from uds.core import services, types, consts
from uds.core.ui import gui
from uds.core.util import validators, fields
from uds.core.util.decorators import cached
from uds.core.util.unique_id_generator import UniqueIDGenerator
from . import client
from .service_linked import ProxmoxServiceLinked
from .service_fixed import ProxmoxServiceFixed
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core import environment
logger = logging.getLogger(__name__)
MAX_VMID: typing.Final[int] = 999999999
def cache_key_helper(self: 'ProxmoxProvider') -> str:
"""
Helper function to generate cache keys for the ProxmoxProvider class
"""
return f'{self.host.value}-{self.port.as_int()}'
class ProxmoxProvider(services.ServiceProvider):
type_name = _('Proxmox Platform Provider')
type_type = 'ProxmoxPlatformOrig'
type_description = _('Proxmox platform service provider')
icon_file = 'provider.png'
offers = [ProxmoxServiceLinked, ProxmoxServiceFixed]
host = gui.TextField(
length=64,
label=_('Host'),
order=1,
tooltip=_('Proxmox Server IP or Hostname'),
required=True,
)
port = gui.NumericField(
length=5,
label=_('Port'),
order=2,
tooltip=_('Proxmox API port (default is 8006)'),
required=True,
default=8006,
)
username = gui.TextField(
length=32,
label=_('Username'),
order=3,
tooltip=_('User with valid privileges on Proxmox, (use "user@authenticator" form)'),
required=True,
default='root@pam',
)
password = gui.PasswordField(
length=32,
label=_('Password'),
order=4,
tooltip=_('Password of the user of Proxmox'),
required=True,
)
concurrent_creation_limit = fields.concurrent_creation_limit_field()
concurrent_removal_limit = fields.concurrent_removal_limit_field()
timeout = fields.timeout_field()
start_vmid = gui.NumericField(
length=3,
label=_('Starting VmId'),
default=10000,
min_value=10000,
max_value=100000,
order=91,
tooltip=_('Starting machine id on proxmox'),
required=True,
readonly=True,
tab=types.ui.Tab.ADVANCED,
old_field_name='startVmId',
)
macs_range = fields.macs_range_field(default='52:54:00:00:00:00-52:54:00:FF:FF:FF')
# Own variables
_cached_api: typing.Optional[client.ProxmoxClient] = None
_vmid_generator: UniqueIDGenerator
def _api(self) -> client.ProxmoxClient:
"""
Returns the connection API object
"""
if self._cached_api is None:
self._cached_api = client.ProxmoxClient(
self.host.value,
self.port.as_int(),
self.username.value,
self.password.value,
self.timeout.as_int(),
False,
self.cache,
)
return self._cached_api
# There is more fields type, but not here the best place to cover it
def initialize(self, values: 'types.core.ValuesType') -> None:
"""
We will use the "autosave" feature for form fields
"""
# Just reset _api connection variable
self._cached_api = None
if values is not None:
self.timeout.value = validators.validate_timeout(self.timeout.value)
logger.debug(self.host.value)
# All proxmox use same UniqueId generator, even if they are different servers
self._vmid_generator = UniqueIDGenerator('proxmoxvmid', 'proxmox')
def test_connection(self) -> bool:
"""
Test that conection to Proxmox server is fine
Returns
True if all went fine, false if id didn't
"""
return self._api().test()
def list_machines(self, force: bool = False) -> list[client.types.VMInfo]:
return self._api().list_machines(force=force)
def get_machine_info(self, vmid: int, poolid: typing.Optional[str] = None) -> client.types.VMInfo:
return self._api().get_machine_pool_info(vmid, poolid, force=True)
def get_machine_configuration(self, vmid: int) -> client.types.VMConfiguration:
return self._api().get_machine_configuration(vmid, force=True)
def get_storage_info(self, storageid: str, node: str, force: bool = False) -> client.types.StorageInfo:
return self._api().get_storage(storageid, node, force=force)
def list_storages(
self, node: typing.Optional[str] = None, force: bool = False
) -> list[client.types.StorageInfo]:
return self._api().list_storages(node=node, content='images', force=force)
def list_pools(self, force: bool = False) -> list[client.types.PoolInfo]:
return self._api().list_pools(force=force)
def get_pool_info(
self, pool_id: str, retrieve_vm_names: bool = False, force: bool = False
) -> client.types.PoolInfo:
return self._api().get_pool_info(pool_id, retrieve_vm_names=retrieve_vm_names, force=force)
def create_template(self, vmid: int) -> None:
self._api().convert_to_template(vmid)
def clone_machine(
self,
vmid: int,
name: str,
description: typing.Optional[str],
as_linked_clone: bool,
target_node: typing.Optional[str] = None,
target_storage: typing.Optional[str] = None,
target_pool: typing.Optional[str] = None,
must_have_vgpus: typing.Optional[bool] = None,
) -> client.types.VmCreationResult:
return self._api().clone_machine(
vmid,
self.get_new_vmid(),
name,
description,
as_linked_clone,
target_node,
target_storage,
target_pool,
must_have_vgpus,
)
def start_machine(self, vmid: int) -> client.types.UPID:
return self._api().start_machine(vmid)
def stop_machine(self, vmid: int) -> client.types.UPID:
return self._api().stop_machine(vmid)
def reset_machine(self, vmid: int) -> client.types.UPID:
return self._api().reset_machine(vmid)
def suspend_machine(self, vmId: int) -> client.types.UPID:
return self._api().suspend_machine(vmId)
def shutdown_machine(self, vmid: int) -> client.types.UPID:
return self._api().shutdown_machine(vmid)
def remove_machine(self, vmid: int) -> client.types.UPID:
return self._api().remove_machine(vmid)
def get_task_info(self, node: str, upid: str) -> client.types.TaskStatus:
return self._api().get_task(node, upid)
def enable_machine_ha(self, vmid: int, started: bool = False, group: typing.Optional[str] = None) -> None:
self._api().enable_machine_ha(vmid, started, group)
def set_machine_mac(self, vmid: int, macAddress: str) -> None:
self._api().set_machine_mac(vmid, macAddress)
def disable_machine_ha(self, vmid: int) -> None:
self._api().disable_machine_ha(vmid)
def set_protection(self, vmid: int, node: typing.Optional[str] = None, protection: bool = False) -> None:
self._api().set_protection(vmid, node, protection)
def list_ha_groups(self) -> list[str]:
return self._api().list_ha_groups()
def get_console_connection(
self,
machine_id: str,
node: typing.Optional[str] = None,
) -> typing.Optional[types.services.ConsoleConnectionInfo]:
return self._api().get_console_connection(int(machine_id), node)
def get_new_vmid(self) -> int:
MAX_RETRIES: typing.Final[int] = 512 # So we don't loop forever, just in case...
vmid = 0
for _ in range(MAX_RETRIES):
vmid = self._vmid_generator.get(self.start_vmid.as_int(), MAX_VMID)
if self._api().is_vmid_available(vmid):
return vmid
# All assigned vmid will be left as unusable on UDS until released by time (3 years)
# This is not a problem at all, in the rare case that a machine id is released from uds db
# if it exists when we try to create a new one, we will simply try to get another one
raise client.ProxmoxError(f'Could not get a new vmid!!: last tried {vmid}')
def get_guest_ip_address(self, vmid: int, node: typing.Optional[str] = None, ip_version: typing.Literal['4', '6', ''] = '') -> str:
return self._api().get_guest_ip_address(vmid, node, ip_version)
def supports_snapshot(self, vmid: int, node: typing.Optional[str] = None) -> bool:
return self._api().supports_snapshot(vmid, node)
def get_current_snapshot(
self, vmid: int, node: typing.Optional[str] = None
) -> typing.Optional[client.types.SnapshotInfo]:
return (
sorted(
filter(lambda x: x.snaptime, self._api().list_snapshots(vmid, node)),
key=lambda x: x.snaptime or 0,
reverse=True,
)
+ [None]
)[0]
def create_snapshot(
self,
vmid: int,
node: typing.Optional[str] = None,
name: typing.Optional[str] = None,
description: typing.Optional[str] = None,
) -> client.types.UPID:
return self._api().create_snapshot(vmid, node, name, description)
def restore_snapshot(
self, vmid: int, node: typing.Optional[str] = None, name: typing.Optional[str] = None
) -> client.types.UPID:
"""
In fact snapshot is not optional, but node is and want to keep the same signature as the api
"""
return self._api().restore_snapshot(vmid, node, name)
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT, key_helper=cache_key_helper)
def is_available(self) -> bool:
return self._api().test()
def get_macs_range(self) -> str:
return self.macs_range.value
@staticmethod
def test(env: 'environment.Environment', data: 'types.core.ValuesType') -> 'types.core.TestResult':
"""
Test Proxmox Connectivity
Args:
env: environment passed for testing (temporal environment passed)
data: data passed for testing (data obtained from the form
definition)
Returns:
Array of two elements, first is True of False, depending on test
(True is all right, false is error),
second is an String with error, preferably internacionalizated..
"""
# try:
# # We instantiate the provider, but this may fail...
# instance = Provider(env, data)
# logger.debug('Methuselah has {0} years and is {1} :-)'
# .format(instance.methAge.value, instance.methAlive.value))
# except exceptions.ValidationException as e:
# # If we say that meth is alive, instantiation will
# return [False, str(e)]
# except Exception as e:
# logger.exception("Exception caugth!!!")
# return [False, str(e)]
# return [True, _('Nothing tested, but all went fine..')]
prox = ProxmoxProvider(env, data)
if prox.test_connection() is True:
return types.core.TestResult(True, _('Test passed'))
return types.core.TestResult(False, _('Connection failed. Check connection params'))

View File

@ -0,0 +1,182 @@
#
# 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
"""
from datetime import datetime
import time
import logging
import typing
from django.utils.translation import gettext as _
from uds.core import services, types
from uds.core.util import autoserializable
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from .service_linked import ProxmoxServiceLinked
logger = logging.getLogger(__name__)
class ProxmoxPublication(services.Publication, autoserializable.AutoSerializable):
suggested_delay = 20
_name = autoserializable.StringField(default='')
_vmid = autoserializable.StringField(default='')
_task = autoserializable.StringField(default='')
_state = autoserializable.StringField(default='')
_operation = autoserializable.StringField(default='')
_destroy_after = autoserializable.BoolField(default=False)
_reason = autoserializable.StringField(default='')
# Utility overrides for type checking...
def service(self) -> 'ProxmoxServiceLinked':
return typing.cast('ProxmoxServiceLinked', super().service())
def unmarshal(self, data: bytes) -> None:
"""
deserializes the data and loads it inside instance.
"""
if not data.startswith(b'v'):
return super().unmarshal(data)
logger.debug('Data: %s', data)
vals = data.decode('utf8').split('\t')
if vals[0] == 'v1':
(
self._name,
self._vmid,
self._task,
self._state,
self._operation,
destroy_after,
self._reason,
) = vals[1:]
else:
raise ValueError('Invalid data format')
self._destroy_after = destroy_after != ''
self.mark_for_upgrade() # Flag so manager can save it again with new format
def publish(self) -> types.states.TaskState:
"""
If no space is available, publication will fail with an error
"""
try:
# First we should create a full clone, so base machine do not get fullfilled with "garbage" delta disks...
self._name = 'UDS ' + _('Publication') + ' ' + self.servicepool_name() + "-" + str(self.revision())
comments = _('UDS Publication for {0} created at {1}').format(
self.servicepool_name(), str(datetime.now()).split('.')[0]
)
task = self.service().clone_machine(self._name, comments)
self._vmid = str(task.vmid)
self._task = ','.join((task.upid.node, task.upid.upid))
self._state = types.states.TaskState.RUNNING
self._operation = 'p' # Publishing
self._destroy_after = False
return types.states.TaskState.RUNNING
except Exception as e:
logger.exception('Caught exception %s', e)
self._reason = str(e)
return types.states.TaskState.ERROR
def check_state(self) -> types.states.TaskState:
if self._state != types.states.TaskState.RUNNING:
return types.states.TaskState.from_str(self._state)
node, upid = self._task.split(',')
try:
task = self.service().provider().get_task_info(node, upid)
if task.is_running():
return types.states.TaskState.RUNNING
except Exception as e:
logger.exception('Proxmox publication')
self._state = types.states.TaskState.ERROR
self._reason = str(e)
return self._state
if task.is_errored():
self._reason = task.exitstatus
self._state = types.states.TaskState.ERROR
else: # Finished
if self._destroy_after:
return self.destroy()
self._state = types.states.TaskState.FINISHED
if self._operation == 'p': # not Destroying
# Disable Protection (removal)
self.service().provider().set_protection(int(self._vmid), protection=False)
time.sleep(0.5) # Give some tome to proxmox. We have observed some concurrency issues
# And add it to HA if
self.service().enable_machine_ha(int(self._vmid))
time.sleep(0.5)
# Mark vm as template
self.service().provider().create_template(int(self._vmid))
# This seems to cause problems on Proxmox
# makeTemplate --> setProtection (that calls "config"). Seems that the HD dissapears...
# Seems a concurrency problem?
return self._state
def finish(self) -> None:
self._task = ''
self._destroy_after = False
def destroy(self) -> types.states.TaskState:
if (
self._state == types.states.TaskState.RUNNING and self._destroy_after is False
): # If called destroy twice, will BREAK STOP publication
self._destroy_after = True
return types.states.TaskState.RUNNING
self._state = types.states.TaskState.RUNNING
self._operation = 'd'
self._destroy_after = False
try:
task = self.service().remove_machine(self.machine())
self._task = ','.join((task.node, task.upid))
return types.states.TaskState.RUNNING
except Exception as e:
self._reason = str(e) # Store reason of error
logger.warning(
'Problem destroying publication %s: %s. Please, check machine state On Proxmox',
self.machine(),
e,
)
return types.states.TaskState.ERROR
def cancel(self) -> types.states.TaskState:
return self.destroy()
def error_reason(self) -> str:
return self._reason
def machine(self) -> int:
return int(self._vmid)

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,227 @@
#
# 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 collections.abc
import logging
import typing
from django.utils.translation import gettext_noop as _
from uds.core import services, types
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 log
from . import helpers
from .deployment_fixed import ProxmoxUserServiceFixed
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds import models
from . import client
from .provider import ProxmoxProvider
logger = logging.getLogger(__name__)
class ProxmoxServiceFixed(FixedService): # pylint: disable=too-many-public-methods
"""
Proxmox fixed machines service.
"""
type_name = _('Proxmox Fixed Machines')
type_type = 'ProxmoxFixedServiceOrig'
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 = ProxmoxUserServiceFixed
allowed_protocols = types.transports.Protocol.generic_vdi(types.transports.Protocol.SPICE)
services_type_provided = types.services.ServiceType.VDI
# Gui
token = FixedService.token
pool = gui.ChoiceField(
label=_("Resource Pool"),
readonly=False,
order=20,
fills={
'callback_name': 'pmFillMachinesFromResource',
'function': helpers.get_machines,
'parameters': ['prov_uuid', 'pool'],
},
tooltip=_('Resource Pool containing base machines'),
required=True,
tab=_('Machines'),
old_field_name='resourcePool',
)
machines = FixedService.machines
use_snapshots = FixedService.use_snapshots
prov_uuid = gui.HiddenField(value=None)
# Uses default FixedService.initialize
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
# Log with call stack
self.prov_uuid.value = self.provider().get_uuid()
self.pool.set_choices(
[gui.choice_item('', _('None'))]
+ [gui.choice_item(p.poolid, p.poolid) for p in self.provider().list_pools()]
)
def provider(self) -> 'ProxmoxProvider':
return typing.cast('ProxmoxProvider', super().provider())
def get_machine_info(self, vmId: int) -> 'client.types.VMInfo':
return self.provider().get_machine_info(vmId, self.pool.value.strip())
def is_avaliable(self) -> bool:
return self.provider().is_available()
def enumerate_assignables(self) -> collections.abc.Iterable[types.ui.ChoiceItem]:
# Obtain machines names and ids for asignables
# Only machines that already exists on proxmox and are not already assigned
vms: dict[int, str] = {}
for member in self.provider().get_pool_info(self.pool.value.strip(), retrieve_vm_names=True).members:
vms[member.vmid] = member.vmname
with self._assigned_machines_access() as assigned_vms:
return [
gui.choice_item(k, vms[int(k)])
for k in self.machines.as_list()
if k not in assigned_vms
and int(k) in vms # Only machines not assigned, and that exists on provider will be available
]
def assign_from_assignables(
self, assignable_id: str, user: 'models.User', userservice_instance: 'services.UserService'
) -> types.states.TaskState:
proxmox_service_instance = typing.cast(ProxmoxUserServiceFixed, userservice_instance)
with self._assigned_machines_access() as assigned_vms:
if assignable_id not in assigned_vms:
assigned_vms.add(assignable_id)
return proxmox_service_instance.assign(assignable_id)
return proxmox_service_instance.error('VM not available!')
def process_snapshot(self, remove: bool, userservice_instance: FixedUserService) -> None:
userservice_instance = typing.cast(ProxmoxUserServiceFixed, userservice_instance)
if self.use_snapshots.as_bool():
vmid = int(userservice_instance._vmid)
if remove:
try:
# try to revert to snapshot
snapshot = self.provider().get_current_snapshot(vmid)
if snapshot:
userservice_instance._store_task(
self.provider().restore_snapshot(vmid, name=snapshot.name)
)
except Exception as e:
self.do_log(log.LogLevel.WARNING, 'Could not restore SNAPSHOT for this VM. ({})'.format(e))
else:
logger.debug('Using snapshots')
# If no snapshot exists for this vm, try to create one for it on background
# Lauch an snapshot. We will not wait for it to finish, but instead let it run "as is"
try:
if not self.provider().get_current_snapshot(vmid):
logger.debug('No current snapshot')
self.provider().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))
def get_and_assign_machine(self) -> str:
found_vmid: typing.Optional[str] = None
try:
with self._assigned_machines_access() as assigned_vms:
for checking_vmid in self.machines.as_list():
if checking_vmid not in assigned_vms: # Not already assigned
try:
# Invoke to check it exists, do not need to store the result
self.provider().get_machine_info(int(checking_vmid), self.pool.value.strip())
found_vmid = checking_vmid
break
except Exception: # Notifies on log, but skipt it
self.provider().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(found_vmid)
except Exception as e: #
logger.debug('Error getting machine: %s', e)
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:
config = self.provider().get_machine_configuration(int(vmid))
return config.networks[0].mac.lower()
def get_guest_ip_address(self, vmid: str) -> str:
return self.provider().get_guest_ip_address(int(vmid))
def get_machine_name(self, vmid: str) -> str:
return self.provider().get_machine_info(int(vmid)).name or ''
def remove_and_free_machine(self, vmid: str) -> str:
try:
with self._assigned_machines_access() as assigned_vms:
assigned_vms.remove(vmid)
return types.states.State.FINISHED
except Exception as e:
logger.warning('Cound not save assigned machines on fixed pool: %s', e)
raise

View File

@ -0,0 +1,278 @@
#
# 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 re
import typing
from django.utils.translation import gettext_noop as _
from uds.core import services, types
from uds.core.ui import gui
from uds.core.util import validators, log, fields
from . import helpers
from .deployment_linked import ProxmoxUserserviceLinked
from .publication import ProxmoxPublication
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from . import client
from .provider import ProxmoxProvider
logger = logging.getLogger(__name__)
class ProxmoxServiceLinked(services.Service): # pylint: disable=too-many-public-methods
"""
Proxmox Linked clones service. This is based on creating a template from selected vm, and then use it to
"""
# : 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 used internally to identify this provider, must not be modified once created
type_type = 'ProxmoxLinkedServiceOrig'
# : Description shown at administration interface for this provider
type_description = _('Proxmox Services based on templates and COW')
# : 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)
icon_file = 'service.png'
# Functional related data
# : If we need to generate "cache" for this service, so users can access the
# : provided services faster. Is uses_cache is True, you will need also
# : set publication_type, do take care about that!
uses_cache = True
# : Tooltip shown to user when this item is pointed at admin interface, none
# : because we don't use it
cache_tooltip = _('Number of desired machines to keep running waiting for a user')
# : If we need to generate a "Level 2" cache for this service (i.e., L1
# : could be running machines and L2 suspended machines)
uses_cache_l2 = True
# : Tooltip shown to user when this item is pointed at admin interface, None
# : also because we don't use it
cache_tooltip_l2 = _('Number of desired VMs to keep stopped waiting for use')
# : If the service needs a s.o. manager (managers are related to agents
# : provided by services itselfs, i.e. virtual machines with actors)
needs_osmanager = True
# : If true, the system can't do an automatic assignation of a deployed user
# : service from this service
must_assign_manually = False
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 = ProxmoxPublication
# : Types of deploys (services in cache and/or assigned to users)
user_service_type = ProxmoxUserserviceLinked
allowed_protocols = types.transports.Protocol.generic_vdi(types.transports.Protocol.SPICE)
services_type_provided = types.services.ServiceType.VDI
pool = gui.ChoiceField(
label=_("Pool"),
order=1,
tooltip=_('Pool that will contain UDS created vms'),
# tab=_('Machine'),
# required=True,
default='',
)
ha = gui.ChoiceField(
label=_('HA'),
order=2,
tooltip=_('Select if HA is enabled and HA group for machines of this service'),
readonly=True,
)
try_soft_shutdown = fields.soft_shutdown_field()
machine = gui.ChoiceField(
label=_("Base Machine"),
order=110,
fills={
'callback_name': 'pmFillResourcesFromMachine',
'function': helpers.get_storage,
'parameters': ['machine', 'prov_uuid'],
},
tooltip=_('Service base machine'),
tab=_('Machine'),
required=True,
)
datastore = gui.ChoiceField(
label=_("Storage"),
readonly=False,
order=111,
tooltip=_('Storage for publications & machines.'),
tab=_('Machine'),
required=True,
)
gpu = gui.ChoiceField(
label=_("GPU Availability"),
readonly=False,
order=112,
choices={
'0': _('Do not check'),
'1': _('Only if available'),
'2': _('Only if NOT available'),
},
tooltip=_('Storage for publications & machines.'),
tab=_('Machine'),
required=True,
)
basename = fields.basename_field(order=115)
lenname = fields.lenname_field(order=116)
prov_uuid = gui.HiddenField(value=None)
def initialize(self, values: 'types.core.ValuesType') -> None:
if values:
self.basename.value = validators.validate_basename(
self.basename.value, length=self.lenname.as_int()
)
# if int(self.memory.value) < 128:
# raise exceptions.ValidationException(_('The minimum allowed memory is 128 Mb'))
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.provider().db_obj().uuid
# This is not the same case, values is not the "value" of the field, but
# the list of values shown because this is a "ChoiceField"
self.machine.set_choices(
[
gui.choice_item(str(m.vmid), f'{m.node}\\{m.name or m.vmid} ({m.vmid})')
for m in self.provider().list_machines()
if m.name and m.name[:3] != 'UDS'
]
)
self.pool.set_choices(
[gui.choice_item('', _('None'))]
+ [gui.choice_item(p.poolid, p.poolid) for p in self.provider().list_pools()]
)
self.ha.set_choices(
[gui.choice_item('', _('Enabled')), gui.choice_item('__', _('Disabled'))]
+ [gui.choice_item(group, group) for group in self.provider().list_ha_groups()]
)
def provider(self) -> 'ProxmoxProvider':
return typing.cast('ProxmoxProvider', super().provider())
def sanitized_name(self, name: str) -> str:
"""
Proxmox only allows machine names with [a-zA-Z0-9_-]
"""
return re.sub("[^a-zA-Z0-9_-]", "-", name)
def clone_machine(self, name: str, description: str, vmid: int = -1) -> 'client.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_machine(
self.machine.as_int(),
name,
description,
as_linked_clone=False,
target_storage=self.datastore.value,
target_pool=pool,
)
return self.provider().clone_machine(
vmid,
name,
description,
as_linked_clone=True,
target_storage=self.datastore.value,
target_pool=pool,
must_have_vgpus={'1': True, '2': False}.get(self.gpu.value, None),
)
def get_machine_info(self, vmid: int) -> 'client.types.VMInfo':
return self.provider().get_machine_info(vmid, self.pool.value.strip())
def get_nic_mac(self, vmid: int) -> str:
config = self.provider().get_machine_configuration(vmid)
return config.networks[0].mac.lower()
def remove_machine(self, vmid: int) -> 'client.types.UPID':
# First, remove from HA if needed
try:
self.disable_machine_ha(vmid)
except Exception as e:
logger.warning('Exception disabling HA for vm %s: %s', vmid, e)
self.do_log(level=log.LogLevel.WARNING, message=f'Exception disabling HA for vm {vmid}: {e}')
# And remove it
return self.provider().remove_machine(vmid)
def enable_machine_ha(self, vmid: int, started: bool = False) -> None:
if self.ha.value == '__':
return
self.provider().enable_machine_ha(vmid, started, self.ha.value or None)
def disable_machine_ha(self, vmid: int) -> None:
if self.ha.value == '__':
return
self.provider().disable_machine_ha(vmid)
def get_basename(self) -> str:
return self.basename.value
def get_lenname(self) -> int:
return int(self.lenname.value)
def get_macs_range(self) -> str:
"""
Returns de selected mac range
"""
return self.provider().get_macs_range()
def is_ha_enabled(self) -> bool:
return self.ha.value != '__'
def try_graceful_shutdown(self) -> bool:
return self.try_soft_shutdown.as_bool()
def get_console_connection(
self, machine_id: str
) -> typing.Optional[types.services.ConsoleConnectionInfo]:
return self.provider().get_console_connection(machine_id)
def is_avaliable(self) -> bool:
return self.provider().is_available()

View File