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:
parent
4fb1da1554
commit
5c9bf779e3
@ -465,6 +465,11 @@ LOGGING = {
|
||||
'level': LOGLEVEL,
|
||||
'propagate': False,
|
||||
},
|
||||
'uds.core.services': {
|
||||
'handlers': ['servicesFile'],
|
||||
'level': LOGLEVEL,
|
||||
'propagate': False,
|
||||
},
|
||||
# Custom Auth log
|
||||
'authLog': {
|
||||
'handlers': ['authFile'],
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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
@ -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')
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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())
|
||||
|
38
server/src/uds/services/ProxmoxOrig/__init__.py
Normal file
38
server/src/uds/services/ProxmoxOrig/__init__.py
Normal 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)
|
802
server/src/uds/services/ProxmoxOrig/client/__init__.py
Normal file
802
server/src/uds/services/ProxmoxOrig/client/__init__.py
Normal 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 []
|
334
server/src/uds/services/ProxmoxOrig/client/types.py
Normal file
334
server/src/uds/services/ProxmoxOrig/client/types.py
Normal 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)
|
148
server/src/uds/services/ProxmoxOrig/deployment_fixed.py
Normal file
148
server/src/uds/services/ProxmoxOrig/deployment_fixed.py
Normal 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()
|
721
server/src/uds/services/ProxmoxOrig/deployment_linked.py
Normal file
721
server/src/uds/services/ProxmoxOrig/deployment_linked.py
Normal 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,
|
||||
}
|
95
server/src/uds/services/ProxmoxOrig/helpers.py
Normal file
95
server/src/uds/services/ProxmoxOrig/helpers.py
Normal 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],
|
||||
}
|
||||
]
|
149
server/src/uds/services/ProxmoxOrig/jobs.py
Normal file
149
server/src/uds/services/ProxmoxOrig/jobs.py
Normal 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)
|
BIN
server/src/uds/services/ProxmoxOrig/provider.png
Normal file
BIN
server/src/uds/services/ProxmoxOrig/provider.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
350
server/src/uds/services/ProxmoxOrig/provider.py
Normal file
350
server/src/uds/services/ProxmoxOrig/provider.py
Normal 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'))
|
182
server/src/uds/services/ProxmoxOrig/publication.py
Normal file
182
server/src/uds/services/ProxmoxOrig/publication.py
Normal 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)
|
BIN
server/src/uds/services/ProxmoxOrig/service.png
Normal file
BIN
server/src/uds/services/ProxmoxOrig/service.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
227
server/src/uds/services/ProxmoxOrig/service_fixed.py
Normal file
227
server/src/uds/services/ProxmoxOrig/service_fixed.py
Normal 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
|
278
server/src/uds/services/ProxmoxOrig/service_linked.py
Normal file
278
server/src/uds/services/ProxmoxOrig/service_linked.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user