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

Adding fixed pool of machines to ProxMox

This commit is contained in:
Adolfo Gómez García 2024-01-31 00:43:27 +01:00
parent 06f3487d2c
commit 8ec94e0cdf
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
20 changed files with 624 additions and 332 deletions

View File

@ -137,7 +137,7 @@ class Providers(ModelHandler):
try:
perm = permissions.effective_permissions(self._user, s)
if perm >= uds.core.types.permissions.PermissionType.READ:
yield DetailServices.serviceToDict(s, perm, True)
yield DetailServices.service_to_dict(s, perm, True)
except Exception:
logger.exception('Passed service cause type is unknown')
@ -149,7 +149,7 @@ class Providers(ModelHandler):
service = Service.objects.get(uuid=self._args[1])
self.ensure_has_access(service.provider, uds.core.types.permissions.PermissionType.READ)
perm = self.get_permissions(service.provider)
return DetailServices.serviceToDict(service, perm, True)
return DetailServices.service_to_dict(service, perm, True)
except Exception:
# logger.exception('Exception')
return {}

View File

@ -63,10 +63,10 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
Detail handler for Services, whose parent is a Provider
"""
custom_methods = ['servicesPools']
custom_methods = ['servicepools']
@staticmethod
def serviceInfo(item: models.Service) -> dict[str, typing.Any]:
def service_info(item: models.Service) -> dict[str, typing.Any]:
info = item.get_type()
return {
@ -86,7 +86,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
}
@staticmethod
def serviceToDict(
def service_to_dict(
item: models.Service, perm: int, full: bool = False
) -> dict[str, typing.Any]:
"""
@ -113,7 +113,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
'permission': perm,
}
if full:
retVal['info'] = Services.serviceInfo(item)
retVal['info'] = Services.service_info(item)
return retVal
@ -123,9 +123,9 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
perm = permissions.effective_permissions(self._user, parent)
try:
if item is None:
return [Services.serviceToDict(k, perm) for k in parent.services.all()]
return [Services.service_to_dict(k, perm) for k in parent.services.all()]
k = parent.services.get(uuid=process_uuid(item))
val = Services.serviceToDict(k, perm, full=True)
val = Services.service_to_dict(k, perm, full=True)
return self.fill_instance_fields(k, val)
except Exception as e:
logger.error('Error getting services for %s: %s', parent, e)
@ -334,7 +334,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
except Exception:
raise self.invalid_item_response() from None
def servicesPools(self, parent: 'Model', item: str) -> typing.Any:
def servicepools(self, parent: 'Model', item: str) -> typing.Any:
parent = ensure.is_instance(parent, models.Provider)
service = parent.services.get(uuid=process_uuid(item))
logger.debug('Got parameters for servicepools: %s, %s', parent, item)

View File

@ -253,7 +253,7 @@ class ServicesPools(ModelHandler):
val['tags'] = [tag.tag for tag in item.tags.all()]
val['restrained'] = restrained
val['permission'] = permissions.effective_permissions(self._user, item)
val['info'] = Services.serviceInfo(item.service) # type: ignore
val['info'] = Services.service_info(item.service) # type: ignore
val['pool_group_id'] = poolGroupId
val['pool_group_name'] = poolGroupName
val['pool_group_thumb'] = poolGroupThumb

View File

@ -110,7 +110,7 @@ class FieldPatternType(enum.StrEnum):
class CallbackResultItem(typing.TypedDict):
# data = [{'name': 'datastore', 'choices': res}]
# {'name': 'datastore', 'choices': res}
name: str
choices: typing.List['ChoiceItem']

View File

@ -131,24 +131,24 @@ def get_server_group_from_field(fld: ui.gui.ChoiceField) -> models.ServerGroup:
# Ticket validity time field (for http related tunnels)
def tunnel_ticket_validity_field() -> ui.gui.NumericField:
def tunnel_ticket_validity_field(order: int = 90, tab: 'types.ui.Tab|None' = types.ui.Tab.ADVANCED) -> ui.gui.NumericField:
return ui.gui.NumericField(
length=3,
label=_('Ticket Validity'),
default=60,
order=90,
order=order,
tooltip=_(
'Allowed time, in seconds, for HTML5 client to reload data from UDS Broker. The default value of 60 is recommended.'
),
required=True,
min_value=60,
tab=types.ui.Tab.ADVANCED,
tab=tab,
old_field_name='ticketValidity',
)
# Tunnel wait time (for uds client related tunnels)
def tunnel_wait_time_field(order: int = 2) -> ui.gui.NumericField:
def tunnel_wait_time_field(order: int = 2, tab: 'types.ui.Tab|None' = types.ui.Tab.TUNNEL) -> ui.gui.NumericField:
return ui.gui.NumericField(
length=3,
label=_('Tunnel wait time'),
@ -158,7 +158,7 @@ def tunnel_wait_time_field(order: int = 2) -> ui.gui.NumericField:
order=order,
tooltip=_('Maximum time, in seconds, to wait before disable new connections on client tunnel listener'),
required=True,
tab=types.ui.Tab.TUNNEL,
tab=tab,
old_field_name='tunnelWait',
)
@ -192,7 +192,7 @@ def get_certificates_from_field(
def timeout_field(
default: int = 3,
order: int = 90,
tab: 'types.ui.Tab|str|None|bool' = None,
tab: 'types.ui.Tab|str|None' = types.ui.Tab.ADVANCED,
old_field_name: typing.Optional[str] = None,
) -> ui.gui.NumericField:
return ui.gui.NumericField(
@ -203,7 +203,7 @@ def timeout_field(
tooltip=_('Timeout in seconds for network connections'),
required=True,
min_value=1,
tab=None if tab is False else None if tab is None else types.ui.Tab.ADVANCED,
tab=tab,
old_field_name=old_field_name,
)
@ -226,26 +226,26 @@ def verify_ssl_field(
# Basename field
def basename_field(order: int = 32, tab: 'types.ui.Tab|str|None|bool' = None) -> ui.gui.TextField:
def basename_field(order: int = 32, tab: 'types.ui.Tab|str|None' = None) -> ui.gui.TextField:
return ui.gui.TextField(
label=_('Base Name'),
order=order,
tooltip=_('Base name for clones from this service'),
tab=None if tab is False else None if tab is None else types.ui.Tab.ADVANCED,
tab=tab,
required=True,
old_field_name='baseName',
)
# Length of name field
def lenname_field(order: int = 33, tab: 'types.ui.Tab|str|None|bool' = None) -> ui.gui.NumericField:
def lenname_field(order: int = 33, tab: 'types.ui.Tab|str|None' = None) -> ui.gui.NumericField:
return ui.gui.NumericField(
length=1,
label=_('Name Length'),
default=3,
order=order,
tooltip=_('Size of numeric part for the names derived from this service'),
tab=None if tab is False else None if tab is None else types.ui.Tab.ADVANCED,
tab=tab,
required=True,
old_field_name='lenName',
)
@ -253,7 +253,8 @@ def lenname_field(order: int = 33, tab: 'types.ui.Tab|str|None|bool' = None) ->
# Max preparing services field
def concurrent_creation_limit_field(
order: int = 50, tab: 'types.ui.Tab|str|None|bool' = None
order: int = 50,
tab: 'types.ui.Tab|str|None' = types.ui.Tab.ADVANCED,
) -> ui.gui.NumericField:
# Advanced tab
return ui.gui.NumericField(
@ -265,13 +266,14 @@ def concurrent_creation_limit_field(
order=order,
tooltip=_('Maximum number of concurrently creating VMs'),
required=True,
tab=None if tab is False else None if tab is None else types.ui.Tab.ADVANCED,
tab=tab,
old_field_name='maxPreparingServices',
)
def concurrent_removal_limit_field(
order: int = 51, tab: 'types.ui.Tab|str|None|bool' = None
order: int = 51,
tab: 'types.ui.Tab|str|None' = types.ui.Tab.ADVANCED,
) -> ui.gui.NumericField:
return ui.gui.NumericField(
length=3,
@ -282,25 +284,27 @@ def concurrent_removal_limit_field(
order=order,
tooltip=_('Maximum number of concurrently removing VMs'),
required=True,
tab=None if tab is False else None if tab is None else types.ui.Tab.ADVANCED,
tab=tab,
old_field_name='maxRemovingServices',
)
def remove_duplicates_field(order: int = 102, tab: 'types.ui.Tab|str|None|bool' = None) -> ui.gui.CheckBoxField:
def remove_duplicates_field(
order: int = 102, tab: 'types.ui.Tab|str|None' = types.ui.Tab.ADVANCED
) -> ui.gui.CheckBoxField:
return ui.gui.CheckBoxField(
label=_('Remove found duplicates'),
default=True,
order=order,
tooltip=_('If active, found duplicates vApps for this service will be removed'),
tab=None if tab is False else None if tab is None else types.ui.Tab.ADVANCED,
tab=tab,
old_field_name='removeDuplicates',
)
def soft_shutdown_field(
order: int = 103,
tab: 'types.ui.Tab|str|None|bool' = None,
tab: 'types.ui.Tab|str|None' = None,
old_field_name: typing.Optional[str] = None,
) -> ui.gui.CheckBoxField:
return ui.gui.CheckBoxField(
@ -308,16 +312,16 @@ def soft_shutdown_field(
default=False,
order=order,
tooltip=_(
'If active, UDS will try to shutdown (soft) the machine using Nutanix ACPI. Will delay 30 seconds the power off of hanged machines.'
'If active, UDS will try to shutdown (soft) the machine. Will delay 90 seconds the power off of hanged machines.'
),
tab=None if tab is False else None if tab is None else types.ui.Tab.ADVANCED,
tab=tab,
old_field_name=old_field_name,
)
def keep_on_access_error_field(
order: int = 104,
tab: 'types.ui.Tab|str|None|bool' = None,
tab: 'types.ui.Tab|str|None' = types.ui.Tab.ADVANCED,
old_field_name: typing.Optional[str] = None,
) -> ui.gui.CheckBoxField:
return ui.gui.CheckBoxField(
@ -325,7 +329,7 @@ def keep_on_access_error_field(
value=False,
order=order,
tooltip=_('If active, access errors found on machine will not be considered errors.'),
tab=None if tab is False else None if tab is None else types.ui.Tab.ADVANCED,
tab=tab,
old_field_name=old_field_name,
)
@ -333,7 +337,7 @@ def keep_on_access_error_field(
def macs_range_field(
default: str,
order: int = 91,
tab: 'types.ui.Tab|str|None|bool' = None,
tab: 'types.ui.Tab|str|None' = types.ui.Tab.ADVANCED,
readonly: bool = False,
) -> ui.gui.TextField:
return ui.gui.TextField(
@ -346,12 +350,12 @@ def macs_range_field(
default=default
),
required=True,
tab=None if tab is False else None if tab is None else types.ui.Tab.ADVANCED,
tab=tab,
old_field_name='macsRange',
)
def mfa_attr_field(order: int = 20, tab: 'types.ui.Tab|str|None|bool' = None) -> ui.gui.TextField:
def mfa_attr_field(order: int = 20, tab: 'types.ui.Tab|str|None' = types.ui.Tab.MFA) -> ui.gui.TextField:
return ui.gui.TextField(
length=2048,
lines=2,
@ -359,15 +363,17 @@ def mfa_attr_field(order: int = 20, tab: 'types.ui.Tab|str|None|bool' = None) ->
order=order,
tooltip=_('Attribute from where to extract the MFA code'),
required=False,
tab=None if tab is False else None if tab is None else types.ui.Tab.MFA,
tab=tab,
old_field_name='mfaAttr',
)
def on_logout_field(order: int = 10, tab: 'types.ui.Tab|str|None|bool' = False) -> ui.gui.ChoiceField:
def on_logout_field(
order: int = 10, tab: 'types.ui.Tab|str|None' = types.ui.Tab.ADVANCED
) -> ui.gui.ChoiceField:
return ui.gui.ChoiceField(
label=_('Logout Action'),
order=10,
order=order,
readonly=True,
tooltip=_('What to do when user logs out from service'),
choices=[
@ -375,15 +381,18 @@ def on_logout_field(order: int = 10, tab: 'types.ui.Tab|str|None|bool' = False)
ui.gui.choice_item('remove', _('Remove service')),
ui.gui.choice_item('keep-always', _('Keep service assigned even on new publication')),
],
tab=None if tab is False else None if tab is None else types.ui.Tab.ADVANCED,
tab=tab,
default='keep',
)
def onlogout_field_is_persistent(fld: ui.gui.ChoiceField) -> bool:
return fld.value == 'keep-always'
def onlogout_field_is_removable(fld: ui.gui.ChoiceField) -> bool:
return fld.value == 'remove'
def onlogout_field_is_keep(fld: ui.gui.ChoiceField) -> bool:
return fld.value == 'keep'
return fld.value == 'keep'

View File

@ -75,9 +75,9 @@ class Client:
_password: str
_timeout: int
_cache: 'Cache'
_needsUsbFix = True
_needs_usb_fix = True
def __getKey(self, prefix: str = '') -> str:
def _generate_key(self, prefix: str = '') -> str:
"""
Creates a key for the cache, using the prefix indicated as part of it
@ -88,7 +88,7 @@ class Client:
prefix, self._host, self._username, self._password, self._timeout
)
def __getApi(self) -> ovirt.Connection:
def _api(self) -> ovirt.Connection:
"""
Gets the api connection.
@ -97,7 +97,7 @@ class Client:
Must be accesed "locked", so we can safely alter cached_api and cached_api_key
"""
aKey = self.__getKey('o-host')
the_key = self._generate_key('o-host')
# if cached_api_key == aKey:
# return cached_api
@ -108,7 +108,7 @@ class Client:
# Nothing happens, may it was already disconnected
pass
try:
Client.cached_api_key = aKey
Client.cached_api_key = the_key
Client.cached_api = ovirt.Connection(
url='https://' + self._host + '/ovirt-engine/api',
username=self._username,
@ -137,25 +137,25 @@ class Client:
self._password = password
self._timeout = int(timeout)
self._cache = cache
self._needsUsbFix = True
self._needs_usb_fix = True
def test(self) -> bool:
try:
lock.acquire(True)
return self.__getApi().test()
return self._api().test()
except Exception as e:
logger.error('Testing Server failed for oVirt: %s', e)
return False
finally:
lock.release()
def isFullyFunctionalVersion(self) -> tuple[bool, str]:
def is_fully_functional_version(self) -> tuple[bool, str]:
"""
'4.0 version is always functional (right now...)
"""
return True, 'Test successfully passed'
def getVms(
def list_machines(
self, force: bool = False
) -> list[collections.abc.MutableMapping[str, typing.Any]]:
"""
@ -172,7 +172,7 @@ class Client:
'cluster_id'
"""
vmsKey = self.__getKey('o-vms')
vmsKey = self._generate_key('o-vms')
val: typing.Optional[typing.Any] = self._cache.get(vmsKey)
if val is not None and force is False:
@ -181,7 +181,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
vms: collections.abc.Iterable[typing.Any] = api.system_service().vms_service().list() # type: ignore
@ -210,7 +210,7 @@ class Client:
finally:
lock.release()
def getClusters(
def list_clusters(
self, force: bool = False
) -> list[collections.abc.MutableMapping[str, typing.Any]]:
"""
@ -228,7 +228,7 @@ class Client:
'datacenter_id'
"""
clsKey = self.__getKey('o-clusters')
clsKey = self._generate_key('o-clusters')
val: typing.Optional[typing.Any] = self._cache.get(clsKey)
if val and not force:
@ -237,7 +237,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
clusters: list[typing.Any] = api.system_service().clusters_service().list() # type: ignore
@ -254,7 +254,7 @@ class Client:
}
# Updates cache info for every single cluster
clKey = self.__getKey('o-cluster' + cluster.id)
clKey = self._generate_key('o-cluster' + cluster.id)
self._cache.put(clKey, val)
if dc is not None:
@ -267,7 +267,7 @@ class Client:
finally:
lock.release()
def getClusterInfo(
def get_cluster_info(
self, clusterId: str, force: bool = False
) -> collections.abc.MutableMapping[str, typing.Any]:
"""
@ -285,7 +285,7 @@ class Client:
'id'
'datacenter_id'
"""
clKey = self.__getKey('o-cluster' + clusterId)
clKey = self._generate_key('o-cluster' + clusterId)
val = self._cache.get(clKey)
if val and not force:
@ -294,7 +294,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
c: typing.Any = api.system_service().clusters_service().service(clusterId).get() # type: ignore
@ -309,7 +309,7 @@ class Client:
finally:
lock.release()
def getDatacenterInfo(
def get_datacenter_info(
self, datacenterId: str, force: bool = False
) -> collections.abc.MutableMapping[str, typing.Any]:
"""
@ -336,7 +336,7 @@ class Client:
'active' -> True or False
"""
dcKey = self.__getKey('o-dc' + datacenterId)
dcKey = self._generate_key('o-dc' + datacenterId)
val = self._cache.get(dcKey)
if val is not None and force is False:
@ -345,7 +345,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
datacenter_service = (
api.system_service().data_centers_service().service(datacenterId)
@ -383,7 +383,7 @@ class Client:
finally:
lock.release()
def getStorageInfo(
def get_storage_info(
self, storageId: str, force: bool = False
) -> collections.abc.MutableMapping[str, typing.Any]:
"""
@ -405,7 +405,7 @@ class Client:
# 'active' -> True or False --> This is not provided by api?? (api.storagedomains.get)
"""
sdKey = self.__getKey('o-sd' + storageId)
sdKey = self._generate_key('o-sd' + storageId)
val = self._cache.get(sdKey)
if val and not force:
@ -414,7 +414,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
dd: typing.Any = api.system_service().storage_domains_service().service(storageId).get() # type: ignore
@ -431,7 +431,7 @@ class Client:
finally:
lock.release()
def makeTemplate(
def create_template(
self,
name: str,
comments: str,
@ -467,7 +467,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
# cluster = ov.clusters_service().service('00000002-0002-0002-0002-0000000002e4') # .get()
# vm = ov.vms_service().service('e7ff4e00-b175-4e80-9c1f-e50a5e76d347') # .get()
@ -506,7 +506,7 @@ class Client:
finally:
lock.release()
def getTemplateState(self, templateId: str) -> str:
def get_template_state(self, templateId: str) -> str:
"""
Returns current template state.
This method do not uses cache at all (it always tries to get template state from oVirt server)
@ -521,7 +521,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
try:
template: typing.Any = (
@ -538,16 +538,16 @@ class Client:
finally:
lock.release()
def deployFromTemplate(
def deploy_from_template(
self,
name: str,
comments: str,
templateId: str,
clusterId: str,
displayType: str,
usbType: str,
memoryMB: int,
guaranteedMB: int,
template_id: str,
cluster_id: str,
display_type: str,
usb_type: str,
memory_mb: int,
guaranteed_mb: int,
) -> str:
"""
Deploys a virtual machine on selected cluster from selected template
@ -567,24 +567,24 @@ class Client:
logger.debug(
'Deploying machine with name "%s" from template %s at cluster %s with display %s and usb %s, memory %s and guaranteed %s',
name,
templateId,
clusterId,
displayType,
usbType,
memoryMB,
guaranteedMB,
template_id,
cluster_id,
display_type,
usb_type,
memory_mb,
guaranteed_mb,
)
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
logger.debug('Deploying machine %s', name)
cluster = ovirt.types.Cluster(id=clusterId)
template = ovirt.types.Template(id=templateId)
cluster = ovirt.types.Cluster(id=cluster_id)
template = ovirt.types.Template(id=template_id)
if self._needsUsbFix is False and usbType in (
if self._needs_usb_fix is False and usb_type in (
'native',
): # Removed 'legacy', from 3.6 is not used anymore, and from 4.0 not available
usb = ovirt.types.Usb(enabled=True, type=ovirt.types.UsbType.NATIVE)
@ -592,7 +592,7 @@ class Client:
usb = ovirt.types.Usb(enabled=False)
memoryPolicy = ovirt.types.MemoryPolicy(
guaranteed=guaranteedMB * 1024 * 1024
guaranteed=guaranteed_mb * 1024 * 1024
)
par = ovirt.types.Vm(
name=name,
@ -600,7 +600,7 @@ class Client:
template=template,
description=comments,
type=ovirt.types.VmType.DESKTOP,
memory=memoryMB * 1024 * 1024,
memory=memory_mb * 1024 * 1024,
memory_policy=memoryPolicy,
usb=usb,
) # display=display,
@ -610,7 +610,7 @@ class Client:
finally:
lock.release()
def removeTemplate(self, templateId: str) -> None:
def remove_template(self, templateId: str) -> None:
"""
Removes a template from ovirt server
@ -619,14 +619,14 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
api.system_service().templates_service().service(templateId).remove() # type: ignore
# This returns nothing, if it fails it raises an exception
finally:
lock.release()
def getMachineState(self, machineId: str) -> str:
def get_machine_state(self, machineId: str) -> str:
"""
Returns current state of a machine (running, suspended, ...).
This method do not uses cache at all (it always tries to get machine state from oVirt server)
@ -645,7 +645,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
try:
vm = api.system_service().vms_service().service(machineId).get() # type: ignore
@ -660,7 +660,7 @@ class Client:
finally:
lock.release()
def startMachine(self, machineId: str) -> None:
def start_machine(self, machineId: str) -> None:
"""
Tries to start a machine. No check is done, it is simply requested to oVirt.
@ -674,7 +674,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
vmService: typing.Any = (
api.system_service().vms_service().service(machineId)
@ -688,7 +688,7 @@ class Client:
finally:
lock.release()
def stopMachine(self, machineId: str) -> None:
def stop_machine(self, machineId: str) -> None:
"""
Tries to start a machine. No check is done, it is simply requested to oVirt
@ -700,7 +700,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
vmService: typing.Any = (
api.system_service().vms_service().service(machineId)
@ -714,7 +714,7 @@ class Client:
finally:
lock.release()
def suspendMachine(self, machineId: str) -> None:
def suspend_machine(self, machineId: str) -> None:
"""
Tries to start a machine. No check is done, it is simply requested to oVirt
@ -726,7 +726,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
vmService: typing.Any = (
api.system_service().vms_service().service(machineId)
@ -740,7 +740,7 @@ class Client:
finally:
lock.release()
def removeMachine(self, machineId: str) -> None:
def remove_machine(self, machineId: str) -> None:
"""
Tries to delete a machine. No check is done, it is simply requested to oVirt
@ -752,7 +752,7 @@ class Client:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
vmService: typing.Any = (
api.system_service().vms_service().service(machineId)
@ -766,14 +766,14 @@ class Client:
finally:
lock.release()
def updateMachineMac(self, machineId: str, macAddres: str) -> None:
def update_machine_mac(self, machineId: str, macAddres: str) -> None:
"""
Changes the mac address of first nic of the machine to the one specified
"""
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
vmService: typing.Any = (
api.system_service().vms_service().service(machineId)
@ -794,13 +794,13 @@ class Client:
finally:
lock.release()
def fixUsb(self, machineId: str) -> None:
def fix_usb(self, machineId: str) -> None:
# Fix for usb support
if self._needsUsbFix:
if self._needs_usb_fix:
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
usb = ovirt.types.Usb(enabled=True, type=ovirt.types.UsbType.NATIVE)
vms: typing.Any = api.system_service().vms_service().service(machineId)
vmu = ovirt.types.Vm(usb=usb)
@ -808,7 +808,7 @@ class Client:
finally:
lock.release()
def getConsoleConnection(
def get_console_connection(
self, machineId: str
) -> typing.Optional[collections.abc.MutableMapping[str, typing.Any]]:
"""
@ -816,7 +816,7 @@ class Client:
"""
try:
lock.acquire(True)
api = self.__getApi()
api = self._api()
vmService: typing.Any = (
api.system_service().vms_service().service(machineId)

View File

@ -31,7 +31,7 @@ def get_resources(parameters: typing.Any) -> types.ui.CallbackResultType:
provider.deserialize(parameters['ov'])
# Obtains datacenter from cluster
ci = provider.getClusterInfo(parameters['cluster'])
ci = provider.get_cluster_info(parameters['cluster'])
res: list[types.ui.ChoiceItem] = []
# Get storages for that datacenter

View File

@ -89,7 +89,6 @@ class OVirtDeferredRemoval(jobs.Job):
logger.debug('Looking for deferred vm removals')
provider: Provider
# Look for Providers of type Ovirt
for provider in Provider.objects.filter(
maintenance_mode=False, data_type=OVirtProvider.type_type

View File

@ -37,7 +37,7 @@ 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
from uds.core.util import validators, fields
from uds.core.util.cache import Cache
from uds.core.util.decorators import cached
@ -95,7 +95,7 @@ class OVirtProvider(
# but used for sample purposes
# If we don't indicate an order, the output order of fields will be
# "random"
ovirtVersion = gui.ChoiceField(
ovirt_version = gui.ChoiceField(
order=1,
label=_('oVirt Version'),
tooltip=_('oVirt Server Version'),
@ -106,6 +106,7 @@ class OVirtProvider(
gui.choice_item('4', '4.x'),
],
default='4', # Default value is the ID of the choicefield
old_field_name='ovirtVersion',
)
host = gui.TextField(
@ -131,30 +132,9 @@ class OVirtProvider(
required=True,
)
concurrent_creation_limit = gui.NumericField(
length=3,
label=_('Creation concurrency'),
default=10,
min_value=1,
max_value=65536,
order=50,
tooltip=_('Maximum number of concurrently creating VMs'),
required=True,
tab=types.ui.Tab.ADVANCED,
old_field_name='maxPreparingServices',
)
concurrent_removal_limit = gui.NumericField(
length=3,
label=_('Removal concurrency'),
default=5,
min_value=1,
max_value=65536,
order=51,
tooltip=_('Maximum number of concurrently removing VMs'),
required=True,
tab=types.ui.Tab.ADVANCED,
old_field_name='maxRemovingServices',
)
concurrent_creation_limit = fields.concurrent_creation_limit_field()
concurrent_removal_limit = fields.concurrent_removal_limit_field()
timeout = fields.timeout_field()
timeout = gui.NumericField(
length=3,
@ -227,7 +207,7 @@ class OVirtProvider(
"""
Checks that this version of ovirt if "fully functional" and does not needs "patchs'
"""
return list(self.__getApi().isFullyFunctionalVersion())
return list(self.__getApi().is_fully_functional_version())
def getMachines(
self, force: bool = False
@ -247,7 +227,7 @@ class OVirtProvider(
'cluster_id'
"""
return self.__getApi().getVms(force)
return self.__getApi().list_machines(force)
def getClusters(
self, force: bool = False
@ -267,9 +247,9 @@ class OVirtProvider(
'datacenter_id'
"""
return self.__getApi().getClusters(force)
return self.__getApi().list_clusters(force)
def getClusterInfo(
def get_cluster_info(
self, clusterId: str, force: bool = False
) -> collections.abc.MutableMapping[str, typing.Any]:
"""
@ -287,7 +267,7 @@ class OVirtProvider(
'id'
'datacenter_id'
"""
return self.__getApi().getClusterInfo(clusterId, force)
return self.__getApi().get_cluster_info(clusterId, force)
def getDatacenterInfo(
self, datacenterId: str, force: bool = False
@ -317,7 +297,7 @@ class OVirtProvider(
'active' -> True or False
"""
return self.__getApi().getDatacenterInfo(datacenterId, force)
return self.__getApi().get_datacenter_info(datacenterId, force)
def getStorageInfo(
self, storageId: str, force: bool = False
@ -341,7 +321,7 @@ class OVirtProvider(
# 'active' -> True or False --> This is not provided by api?? (api.storagedomains.get)
"""
return self.__getApi().getStorageInfo(storageId, force)
return self.__getApi().get_storage_info(storageId, force)
def makeTemplate(
self,
@ -366,7 +346,7 @@ class OVirtProvider(
Returns
Raises an exception if operation could not be acomplished, or returns the id of the template being created.
"""
return self.__getApi().makeTemplate(
return self.__getApi().create_template(
name, comments, machineId, clusterId, storageId, displayType
)
@ -381,7 +361,7 @@ class OVirtProvider(
(don't know if ovirt returns something more right now, will test what happens when template can't be published)
"""
return self.__getApi().getTemplateState(templateId)
return self.__getApi().get_template_state(templateId)
def getMachineState(self, machineId: str) -> str:
"""
@ -399,7 +379,7 @@ class OVirtProvider(
suspended, image_illegal, image_locked or powering_down
Also can return'unknown' if Machine is not known
"""
return self.__getApi().getMachineState(machineId)
return self.__getApi().get_machine_state(machineId)
def removeTemplate(self, templateId: str) -> None:
"""
@ -407,7 +387,7 @@ class OVirtProvider(
Returns nothing, and raises an Exception if it fails
"""
return self.__getApi().removeTemplate(templateId)
return self.__getApi().remove_template(templateId)
def deployFromTemplate(
self,
@ -435,7 +415,7 @@ class OVirtProvider(
Returns:
Id of the machine being created form template
"""
return self.__getApi().deployFromTemplate(
return self.__getApi().deploy_from_template(
name,
comments,
templateId,
@ -457,7 +437,7 @@ class OVirtProvider(
Returns:
"""
self.__getApi().startMachine(machineId)
self.__getApi().start_machine(machineId)
def stopMachine(self, machineId: str) -> None:
"""
@ -468,7 +448,7 @@ class OVirtProvider(
Returns:
"""
self.__getApi().stopMachine(machineId)
self.__getApi().stop_machine(machineId)
def suspendMachine(self, machineId: str) -> None:
"""
@ -479,7 +459,7 @@ class OVirtProvider(
Returns:
"""
self.__getApi().suspendMachine(machineId)
self.__getApi().suspend_machine(machineId)
def removeMachine(self, machineId: str) -> None:
"""
@ -490,16 +470,16 @@ class OVirtProvider(
Returns:
"""
self.__getApi().removeMachine(machineId)
self.__getApi().remove_machine(machineId)
def updateMachineMac(self, machineId: str, macAddres: str) -> None:
"""
Changes the mac address of first nic of the machine to the one specified
"""
self.__getApi().updateMachineMac(machineId, macAddres)
self.__getApi().update_machine_mac(machineId, macAddres)
def fixUsb(self, machineId: str) -> None:
self.__getApi().fixUsb(machineId)
self.__getApi().fix_usb(machineId)
def getMacRange(self) -> str:
return self.macsRange.value
@ -507,7 +487,7 @@ class OVirtProvider(
def getConsoleConnection(
self, machineId: str
) -> typing.Optional[collections.abc.MutableMapping[str, typing.Any]]:
return self.__getApi().getConsoleConnection(machineId)
return self.__getApi().get_console_connection(machineId)
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT)
def is_available(self) -> bool:

View File

@ -32,7 +32,6 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import collections.abc
import logging
from re import T
import typing
from datetime import datetime

View File

@ -294,7 +294,9 @@ class ProxmoxClient:
key_fnc=caching_key_helper,
)
def list_node_gpu_devices(self, node: str, **kwargs) -> list[str]:
return [device['id'] for device in self._get(f'nodes/{node}/hardware/pci')['data'] if device.get('mdev')]
return [
device['id'] for device in self._get(f'nodes/{node}/hardware/pci')['data'] if device.get('mdev')
]
@ensure_connected
def list_node_vgpus(self, node: str, **kwargs) -> list[typing.Any]:
@ -387,9 +389,11 @@ class ProxmoxClient:
# 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})')
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:
@ -424,7 +428,7 @@ class ProxmoxClient:
@ensure_connected
@cached('hagrps', CACHE_DURATION, key_fnc=caching_key_helper)
def list_ha_groups(self) -> list[str]:
def list_ha_groups(self, **kwargs) -> list[str]:
return [g['group'] for g in self._get('cluster/ha/groups')['data']]
@ensure_connected
@ -475,7 +479,9 @@ class ProxmoxClient:
kwargs='node',
key_fnc=caching_key_helper,
)
def list_machines(self, node: typing.Union[None, str, collections.abc.Iterable[str]] = None) -> list[types.VMInfo]:
def list_machines(
self, node: typing.Union[None, str, collections.abc.Iterable[str]] = None, **kwargs
) -> list[types.VMInfo]:
nodeList: collections.abc.Iterable[str]
if node is None:
nodeList = [n.name for n in self.get_cluster_info().nodes if n.online]
@ -672,8 +678,20 @@ class ProxmoxClient:
@ensure_connected
@cached('pools', CACHE_DURATION // 6, key_fnc=caching_key_helper)
def list_pools(self) -> list[types.PoolInfo]:
return [types.PoolInfo.from_dict(nodeStat) for nodeStat in self._get('pools')['data']]
def list_pools(self, **kwargs) -> list[types.PoolInfo]:
return [types.PoolInfo.from_dict(poolInfo) for poolInfo in self._get('pools')['data']]
@ensure_connected
@cached('pool', CACHE_DURATION, args=[1, 2], kwargs=['pool_id', 'retrieve_vm_names'], key_fnc=caching_key_helper)
def get_pool_info(self, pool_id: str, retrieve_vm_names: bool = False, **kwargs) -> 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(

View File

@ -1,14 +1,13 @@
import datetime
import re
import typing
import dataclasses
import collections.abc
NETWORK_RE: typing.Final[typing.Pattern] = re.compile(r'([a-zA-Z0-9]+)=([^,]+)') # May have vla id at end
# Conversor from dictionary to NamedTuple
CONVERSORS: typing.Final[collections.abc.MutableMapping[typing.Type, collections.abc.Callable]] = {
str: lambda x: str(x),
str: lambda x: str(x or ''),
bool: lambda x: bool(x),
int: lambda x: int(x or '0'),
float: lambda x: float(x or '0'),
@ -17,7 +16,7 @@ CONVERSORS: typing.Final[collections.abc.MutableMapping[typing.Type, collections
def _from_dict(
type: type[typing.Any],
type: type[typing.NamedTuple],
dictionary: collections.abc.MutableMapping[str, typing.Any],
extra: typing.Optional[collections.abc.Mapping[str, typing.Any]] = None,
) -> typing.Any:
@ -32,8 +31,8 @@ def _from_dict(
)
@dataclasses.dataclass(slots=True)
class Cluster:
# Need to be "NamedTuple"s because we use _fields attribute
class Cluster(typing.NamedTuple):
name: str
version: str
id: str
@ -45,8 +44,7 @@ class Cluster:
return _from_dict(Cluster, dictionary)
@dataclasses.dataclass(slots=True)
class Node:
class Node(typing.NamedTuple):
name: str
online: bool
local: bool
@ -60,8 +58,7 @@ class Node:
return _from_dict(Node, dictionary)
@dataclasses.dataclass(slots=True)
class NodeStats:
class NodeStats(typing.NamedTuple):
name: str
status: str
uptime: int
@ -96,8 +93,7 @@ class NodeStats:
)
@dataclasses.dataclass(slots=True)
class ClusterStatus:
class ClusterStatus(typing.NamedTuple):
cluster: typing.Optional[Cluster]
nodes: list[Node]
@ -115,8 +111,7 @@ class ClusterStatus:
return ClusterStatus(cluster=cluster, nodes=nodes)
@dataclasses.dataclass(slots=True)
class UPID:
class UPID(typing.NamedTuple):
node: str
pid: int
pstart: int
@ -142,8 +137,7 @@ class UPID:
)
@dataclasses.dataclass(slots=True)
class TaskStatus:
class TaskStatus(typing.NamedTuple):
node: str
pid: int
pstart: int
@ -172,8 +166,7 @@ class TaskStatus:
return self.is_finished() and not self.is_completed()
@dataclasses.dataclass(slots=True)
class NetworkConfiguration:
class NetworkConfiguration(typing.NamedTuple):
net: str
type: str
mac: str
@ -188,8 +181,7 @@ class NetworkConfiguration:
return NetworkConfiguration(net=net, type=type, mac=mac)
@dataclasses.dataclass(slots=True)
class VMInfo:
class VMInfo(typing.NamedTuple):
status: str
vmid: int
node: str
@ -231,8 +223,7 @@ class VMInfo:
return data
@dataclasses.dataclass(slots=True)
class VMConfiguration:
class VMConfiguration(typing.NamedTuple):
name: str
vga: str
sockets: int
@ -254,15 +245,13 @@ class VMConfiguration:
return _from_dict(VMConfiguration, src)
@dataclasses.dataclass(slots=True)
class VmCreationResult:
class VmCreationResult(typing.NamedTuple):
node: str
vmid: int
upid: UPID
@dataclasses.dataclass(slots=True)
class StorageInfo:
class StorageInfo(typing.NamedTuple):
node: str
storage: str
content: tuple[str, ...]
@ -280,11 +269,32 @@ class StorageInfo:
return _from_dict(StorageInfo, dictionary)
@dataclasses.dataclass(slots=True)
class PoolInfo:
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':
return _from_dict(PoolInfo, dictionary)
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)

View File

@ -27,17 +27,18 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing
import collections.abc
from uds.core import types
from uds.core.environment import Environment
import logging
from multiprocessing import pool
import typing
from django.utils.translation import gettext as _
from uds.core import types
from uds.core.environment import Environment
from uds.core.ui.user_interface import gui
from uds import models
from uds.services.OpenNebula.on import vm
logger = logging.getLogger(__name__)
@ -46,9 +47,9 @@ 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)
env = Environment(parameters['ev'])
provider: ProxmoxProvider = ProxmoxProvider(env)
provider.deserialize(parameters['ov'])
provider = typing.cast(
ProxmoxProvider, models.Provider.objects.get(uuid=parameters['prov_uuid']).get_instance()
)
# Obtains datacenter from cluster
try:
@ -58,26 +59,41 @@ def get_storage(parameters: typing.Any) -> types.ui.CallbackResultType:
res = []
# Get storages for that datacenter
for storage in sorted(
provider.listStorages(vm_info.node), key=lambda x: int(not x.shared)
):
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)
)
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}'
)
gui.choice_item(storage.storage, f'{storage.storage} ({space:4.2f} GB/{free:4.2f} GB){extra}')
)
data: types.ui.CallbackResultType = [{'name': 'datastore', 'choices': res}]
logger.debug('return data: %s', data)
return data
def get_machines(parameters: typing.Any) -> types.ui.CallbackResultType:
from .provider import ProxmoxProvider # pylint: disable=import-outside-toplevel
logger.debug('Parameters received by getResources Helper: %s', parameters)
provider = typing.cast(
ProxmoxProvider, models.Provider.objects.get(uuid=parameters['prov_uuid']).get_instance()
)
# Obtains datacenter from cluster
try:
pool_info = provider.get_pool_info(parameters['pool'], retrieve_vm_names=True)
except Exception:
return []
return [
{
'name': 'machines',
'choices': [gui.choice_item(member.vmid, member.vmname) for member in pool_info.members],
}
]

View File

@ -42,6 +42,7 @@ from uds.core.util.unique_mac_generator import UniqueMacGenerator
from . import client
from .service import ProxmoxLinkedService
from .service_fixed import ProxmoxFixedService
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
@ -53,13 +54,14 @@ logger = logging.getLogger(__name__)
MAX_VM_ID: typing.Final[int] = 999999999
class ProxmoxProvider(services.ServiceProvider): # pylint: disable=too-many-public-methods
offers = [ProxmoxLinkedService]
class ProxmoxProvider(services.ServiceProvider):
type_name = _('Proxmox Platform Provider')
type_type = 'ProxmoxPlatform'
type_description = _('Proxmox platform service provider')
icon_file = 'provider.png'
offers = [ProxmoxLinkedService, ProxmoxFixedService]
host = gui.TextField(
length=64,
label=_('Host'),
@ -113,15 +115,15 @@ class ProxmoxProvider(services.ServiceProvider): # pylint: disable=too-many-pub
macs_range = fields.macs_range_field(default='52:54:00:00:00:00-52:54:00:FF:FF:FF')
# Own variables
_api: typing.Optional[client.ProxmoxClient] = None
_cached_api: typing.Optional[client.ProxmoxClient] = None
_vmid_generator: UniqueIDGenerator
def _getApi(self) -> client.ProxmoxClient:
def _api(self) -> client.ProxmoxClient:
"""
Returns the connection API object
"""
if self._api is None:
self._api = client.ProxmoxClient(
if self._cached_api is None:
self._cached_api = client.ProxmoxClient(
self.host.value,
self.port.as_int(),
self.username.value,
@ -131,7 +133,7 @@ class ProxmoxProvider(services.ServiceProvider): # pylint: disable=too-many-pub
self.cache,
)
return self._api
return self._cached_api
# There is more fields type, but not here the best place to cover it
def initialize(self, values: 'Module.ValuesType') -> None:
@ -140,7 +142,7 @@ class ProxmoxProvider(services.ServiceProvider): # pylint: disable=too-many-pub
"""
# Just reset _api connection variable
self._api = None
self._cached_api = None
if values is not None:
self.timeout.value = validators.validate_timeout(self.timeout.value)
@ -158,97 +160,100 @@ class ProxmoxProvider(services.ServiceProvider): # pylint: disable=too-many-pub
True if all went fine, false if id didn't
"""
return self._getApi().test()
return self._api().test()
def listMachines(self) -> list[client.types.VMInfo]:
return self._getApi().list_machines()
def list_machines(self) -> list[client.types.VMInfo]:
return self._api().list_machines()
def get_machine_info(self, vmId: int, poolId: typing.Optional[str] = None) -> client.types.VMInfo:
return self._getApi().get_machines_pool_info(vmId, poolId, force=True)
return self._api().get_machines_pool_info(vmId, poolId, force=True)
def get_machine_configuration(self, vmId: int) -> client.types.VMConfiguration:
return self._getApi().get_machine_configuration(vmId, force=True)
return self._api().get_machine_configuration(vmId, force=True)
def getStorageInfo(self, storageId: str, node: str) -> client.types.StorageInfo:
return self._getApi().get_storage(storageId, node)
def get_storage_info(self, storageId: str, node: str) -> client.types.StorageInfo:
return self._api().get_storage(storageId, node)
def listStorages(self, node: typing.Optional[str]) -> list[client.types.StorageInfo]:
return self._getApi().list_storages(node=node, content='images')
def list_storages(self, node: typing.Optional[str]) -> list[client.types.StorageInfo]:
return self._api().list_storages(node=node, content='images')
def listPools(self) -> list[client.types.PoolInfo]:
return self._getApi().list_pools()
def list_pools(self) -> list[client.types.PoolInfo]:
return self._api().list_pools()
def get_pool_info(self, pool_id: str, retrieve_vm_names: bool = False) -> client.types.PoolInfo:
return self._api().get_pool_info(pool_id, retrieve_vm_names=retrieve_vm_names)
def make_template(self, vmId: int) -> None:
return self._getApi().convertToTemplate(vmId)
def create_template(self, vmId: int) -> None:
return self._api().convertToTemplate(vmId)
def cloneMachine(
def clone_machine(
self,
vmId: int,
vmid: int,
name: str,
description: typing.Optional[str],
linkedClone: bool,
toNode: typing.Optional[str] = None,
toStorage: typing.Optional[str] = None,
toPool: typing.Optional[str] = None,
mustHaveVGPUS: typing.Optional[bool] = None,
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._getApi().clone_machine(
vmId,
return self._api().clone_machine(
vmid,
self.get_new_vmid(),
name,
description,
linkedClone,
toNode,
toStorage,
toPool,
mustHaveVGPUS,
as_linked_clone,
target_node,
target_storage,
target_pool,
must_have_vgpus,
)
def start_machine(self, vmId: int) -> client.types.UPID:
return self._getApi().start_machine(vmId)
return self._api().start_machine(vmId)
def stop_machine(self, vmid: int) -> client.types.UPID:
return self._getApi().stop_machine(vmid)
return self._api().stop_machine(vmid)
def reset_machine(self, vmid: int) -> client.types.UPID:
return self._getApi().reset_machine(vmid)
return self._api().reset_machine(vmid)
def suspend_machine(self, vmId: int) -> client.types.UPID:
return self._getApi().suspend_machine(vmId)
return self._api().suspend_machine(vmId)
def shutdown_machine(self, vmId: int) -> client.types.UPID:
return self._getApi().shutdown_machine(vmId)
return self._api().shutdown_machine(vmId)
def remove_machine(self, vmid: int) -> client.types.UPID:
return self._getApi().remove_machine(vmid)
return self._api().remove_machine(vmid)
def get_task_info(self, node: str, upid: str) -> client.types.TaskStatus:
return self._getApi().get_task(node, upid)
return self._api().get_task(node, upid)
def enable_ha(self, vmId: int, started: bool = False, group: typing.Optional[str] = None) -> None:
self._getApi().enable_machine_ha(vmId, started, group)
self._api().enable_machine_ha(vmId, started, group)
def set_machine_mac(self, vmId: int, macAddress: str) -> None:
self._getApi().set_machine_ha(vmId, macAddress)
self._api().set_machine_ha(vmId, macAddress)
def disable_ha(self, vmid: int) -> None:
self._getApi().disable_machine_ha(vmid)
self._api().disable_machine_ha(vmid)
def set_protection(self, vmId: int, node: typing.Optional[str] = None, protection: bool = False) -> None:
self._getApi().set_protection(vmId, node, protection)
self._api().set_protection(vmId, node, protection)
def list_ha_groups(self) -> list[str]:
return self._getApi().list_ha_groups()
return self._api().list_ha_groups()
def get_console_connection(
self, machineId: str
) -> typing.Optional[collections.abc.MutableMapping[str, typing.Any]]:
return self._getApi().get_console_connection(machineId)
return self._api().get_console_connection(machineId)
def get_new_vmid(self) -> int:
while True: # look for an unused VmId
vmid = self._vmid_generator.get(self.start_vmid.as_int(), MAX_VM_ID)
if self._getApi().is_vmid_available(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
@ -256,7 +261,7 @@ class ProxmoxProvider(services.ServiceProvider): # pylint: disable=too-many-pub
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT)
def is_available(self) -> bool:
return self._getApi().test()
return self._api().test()
def get_macs_range(self) -> str:
return self.macs_range.value

View File

@ -36,7 +36,7 @@ import collections.abc
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, log
from uds.core.util import validators, log, fields
from uds.core.util.cache import Cache
from uds.core.util.decorators import cached
@ -66,7 +66,7 @@ class ProxmoxLinkedService(services.Service): # pylint: disable=too-many-public
# : Type used internally to identify this provider
type_type = 'ProxmoxLinkedService'
# : Description shown at administration interface for this provider
type_description = _('Proxmox Services based on templates and COW (experimental)')
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)
@ -105,8 +105,6 @@ class ProxmoxLinkedService(services.Service): # pylint: disable=too-many-public
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,
@ -123,23 +121,16 @@ class ProxmoxLinkedService(services.Service): # pylint: disable=too-many-public
readonly=True,
)
guestShutdown = gui.CheckBoxField(
label=_('Try SOFT Shutdown first'),
default=False,
order=103,
tooltip=_(
'If active, UDS will try to shutdown (soft) the machine using VMWare Guest Tools. Will delay 30 seconds the power off of hanged machines.'
),
old_field_name='guestShutdown',
)
soft_shutdown_field = fields.soft_shutdown_field()
machine = gui.ChoiceField(
label=_("Base Machine"),
order=110,
fills={
'callback_name': 'pmFillResourcesFromMachine',
'function': helpers.get_storage,
'parameters': ['machine', 'ov', 'ev'],
'parameters': ['machine', 'prov_uuid'],
},
tooltip=_('Service base machine'),
tab=_('Machine'),
@ -168,37 +159,16 @@ class ProxmoxLinkedService(services.Service): # pylint: disable=too-many-public
tab=_('Machine'),
required=True,
)
basename = fields.basename_field(order=115)
lenname = fields.lenname_field(order=116)
baseName = gui.TextField(
label=_('Machine Names'),
readonly=False,
order=115,
tooltip=_('Base name for clones from this machine'),
tab=_('Machine'),
required=True,
old_field_name='baseName',
)
lenName = gui.NumericField(
length=1,
label=_('Name Length'),
default=5,
order=116,
tooltip=_('Size of numeric part for the names of these machines'),
tab=_('Machine'),
required=True,
old_field_name='lenName',
)
ov = gui.HiddenField(value=None)
ev = gui.HiddenField(
value=None
) # We need to keep the env so we can instantiate the Provider
prov_uuid = gui.HiddenField(value=None)
def initialize(self, values: 'Module.ValuesType') -> None:
if values:
self.baseName.value = validators.validate_basename(
self.baseName.value, length=self.lenName.as_int()
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'))
@ -207,23 +177,20 @@ class ProxmoxLinkedService(services.Service): # pylint: disable=too-many-public
# 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.ov.value = self.parent().serialize()
self.ev.value = self.parent().env.key
self.prov_uuid.value = self.parent().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.parent().listMachines()
gui.choice_item(str(m.vmid), f'{m.node}\\{m.name or m.vmid} ({m.vmid})')
for m in self.parent().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.parent().listPools()]
+ [gui.choice_item(p.poolid, p.poolid) for p in self.parent().list_pools()]
)
self.ha.set_choices(
[gui.choice_item('', _('Enabled')), gui.choice_item('__', _('Disabled'))]
@ -240,31 +207,29 @@ class ProxmoxLinkedService(services.Service): # pylint: disable=too-many-public
return re.sub("[^a-zA-Z0-9_-]", "-", name)
def make_template(self, vmId: int) -> None:
self.parent().make_template(vmId)
self.parent().create_template(vmId)
def clone_machine(
self, name: str, description: str, vmId: int = -1
) -> 'client.types.VmCreationResult':
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.parent().cloneMachine(
return self.parent().clone_machine(
self.machine.value,
name,
description,
linkedClone=False,
toStorage=self.datastore.value,
toPool=pool,
as_linked_clone=False,
target_storage=self.datastore.value,
target_pool=pool,
)
return self.parent().cloneMachine(
return self.parent().clone_machine(
vmId,
name,
description,
linkedClone=True,
toStorage=self.datastore.value,
toPool=pool,
mustHaveVGPUS={'1': True, '2': False}.get(self.gpu.value, None),
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':
@ -299,7 +264,7 @@ class ProxmoxLinkedService(services.Service): # pylint: disable=too-many-public
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.parent().remove_machine(vmId)
@ -313,19 +278,17 @@ class ProxmoxLinkedService(services.Service): # pylint: disable=too-many-public
return
self.parent().disable_ha(vmId)
def set_protection(
self, vmId: int, node: typing.Optional[str] = None, protection: bool = False
) -> None:
def set_protection(self, vmId: int, node: typing.Optional[str] = None, protection: bool = False) -> None:
self.parent().set_protection(vmId, node, protection)
def set_machine_mac(self, vmId: int, mac: str) -> None:
self.parent().set_machine_mac(vmId, mac)
def get_basename(self) -> str:
return self.baseName.value
return self.basename.value
def get_lenname(self) -> int:
return int(self.lenName.value)
return int(self.lenname.value)
def get_macs_range(self) -> str:
"""
@ -337,7 +300,7 @@ class ProxmoxLinkedService(services.Service): # pylint: disable=too-many-public
return self.ha.value != '__'
def try_graceful_shutdown(self) -> bool:
return self.guestShutdown.as_bool()
return self.soft_shutdown_field.as_bool()
def get_console_connection(
self, machineId: str

View File

@ -0,0 +1,292 @@
#
# 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
import collections.abc
from django.utils.translation import gettext_noop as _, gettext
from uds.core import services, types, consts, exceptions
from uds.core.ui import gui
from uds.core.util import validators, log
from uds.core.util.cache import Cache
from uds.core.util.decorators import cached
from uds.core.workers import initialize
from . import helpers
from .deployment import ProxmoxDeployment
from .publication import ProxmoxPublication
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core.module import Module
from uds import models
from . import client
from .provider import ProxmoxProvider
logger = logging.getLogger(__name__)
class ProxmoxFixedService(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 Fixed Machines')
# : Type used internally to identify this provider
type_type = 'ProxmoxFixedService'
# : Description shown at administration interface for this provider
type_description = _('Proxmox Services based on fixed machines')
# : 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_manager = 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 = ProxmoxDeployment
allowed_protocols = types.transports.Protocol.generic_vdi(types.transports.Protocol.SPICE)
services_type_provided = types.services.ServiceType.VDI
# Gui
token = gui.TextField(
order=1,
label=_('Service Token'),
length=16,
tooltip=_(
'Service token that will be used by actors to communicate with service. Leave empty for persistent assignation.'
),
default='',
required=False,
readonly=False,
)
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',
)
# Keep name as "machine" so we can use VCHelpers.getMachines
machines = gui.MultiChoiceField(
label=_("Machines"),
order=21,
tooltip=_('Machines for this service'),
required=True,
tab=_('Machines'),
rows=10,
)
use_snapshots = gui.CheckBoxField(
label=_('Use snapshots'),
default=False,
order=22,
tooltip=_('If active, UDS will try to create an snapshot on VM use and recover if on exit.'),
tab=_('Machines'),
old_field_name='useSnapshots',
)
prov_uuid = gui.HiddenField(value=None)
def _get_assigned_machines(self) -> typing.Set[int]:
vals = self.storage.get_unpickle('vms')
logger.debug('Got storage VMS: %s', vals)
return vals or set()
def _save_assigned_machines(self, vals: typing.Set[int]) -> None:
logger.debug('Saving storage VMS: %s', vals)
self.storage.put_pickle('vms', vals)
def initialize(self, values: 'Module.ValuesType') -> None:
"""
Loads the assigned machines from storage
"""
if values:
if not self.machines.value:
raise exceptions.ui.ValidationError(gettext('We need at least a machine'))
self.storage.put_pickle('maxDeployed', len(self.machines.as_list()))
# Remove machines not in values from "assigned" set
self._save_assigned_machines(self._get_assigned_machines() & set(self.machines.as_list()))
self.token.value = self.token.value.strip()
self.userservices_limit = self.storage.get_unpickle('maxDeployed')
def init_gui(self) -> None:
# Here we have to use "default values", cause values aren't used at form initialization
# This is that value is always '', so if we want to change something, we have to do it
# at defValue
self.prov_uuid.value = self.parent().get_uuid()
self.pool.set_choices(
[gui.choice_item('', _('None'))]
+ [gui.choice_item(p.poolid, p.poolid) for p in self.parent().list_pools()]
)
def parent(self) -> 'ProxmoxProvider':
return typing.cast('ProxmoxProvider', super().parent())
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 get_machine_info(self, vmId: int) -> 'client.types.VMInfo':
return self.parent().get_machine_info(vmId, self.pool.value.strip())
def get_nic_mac(self, vmid: int) -> str:
config = self.parent().get_machine_configuration(vmid)
return config.networks[0].mac.lower()
def get_task_info(self, node: str, upid: str) -> 'client.types.TaskStatus':
return self.parent().get_task_info(node, upid)
def start_machine(self, vmId: int) -> 'client.types.UPID':
return self.parent().start_machine(vmId)
def stop_machine(self, vmId: int) -> 'client.types.UPID':
return self.parent().stop_machine(vmId)
def reset_machine(self, vmId: int) -> 'client.types.UPID':
return self.parent().reset_machine(vmId)
def suspend_machine(self, vmId: int) -> 'client.types.UPID':
return self.parent().suspend_machine(vmId)
def shutdown_machine(self, vmId: int) -> 'client.types.UPID':
return self.parent().shutdown_machine(vmId)
def get_machine_from_pool(self) -> int:
found_vmid: typing.Optional[int] = None
try:
assignedVmsSet = self._get_assigned_machines()
for k in self.machines.as_list():
checking_vmid = int(k)
if found_vmid not in assignedVmsSet: # Not assigned
# Check that the machine exists...
try:
vm_info = self.parent().get_machine_info(checking_vmid, self.pool.value.strip())
found_vmid = checking_vmid
break
except Exception: # Notifies on log, but skipt it
self.parent().do_log(
log.LogLevel.WARNING, 'Machine {} not accesible'.format(found_vmid)
)
logger.warning(
'The service has machines that cannot be checked on vmware (connection error or machine has been deleted): %s',
found_vmid,
)
if found_vmid:
assignedVmsSet.add(found_vmid)
self._save_assigned_machines(assignedVmsSet)
except Exception: #
raise Exception('No machine available')
if not found_vmid:
raise Exception('All machines from list already assigned.')
return found_vmid
def release_machine_from_pool(self, vmid: int) -> None:
try:
self._save_assigned_machines(self._get_assigned_machines() - {vmid}) # Sets operation
except Exception as e:
logger.warn('Cound not save assigned machines on vmware fixed pool: %s', e)
def enumerate_assignables(self) -> list[tuple[str, str]]:
# Obtain machines names and ids for asignables
vms: dict[int, str] = {}
for member in self.parent().get_pool_info(self.pool.value.strip()).members:
vms[member.vmid] = member.vmname
assignedVmsSet = self._get_assigned_machines()
k: str
return [
(k, vms.get(int(k), 'Unknown!')) for k in self.machines.as_list() if int(k) not in assignedVmsSet
]
def assign_from_assignables(
self, assignable_id: str, user: 'models.User', user_deployment: 'services.UserService'
) -> str:
userservice_instance: ProxmoxDeployment = typing.cast(ProxmoxDeployment, user_deployment)
assignedVmsSet = self._get_assigned_machines()
if assignable_id not in assignedVmsSet:
assignedVmsSet.add(int(assignable_id))
self._save_assigned_machines(assignedVmsSet)
return userservice_instance.assign(assignable_id)
return userservice_instance.error('VM not available!')
@cached('reachable', consts.cache.SHORT_CACHE_TIMEOUT)
def is_avaliable(self) -> bool:
return self.parent().is_available()

File diff suppressed because one or more lines are too long

View File

@ -102,6 +102,6 @@
</svg>
</div>
</uds-root>
<script src="/uds/res/admin/runtime.js?stamp=1706231732" type="module"></script><script src="/uds/res/admin/polyfills.js?stamp=1706231732" type="module"></script><script src="/uds/res/admin/main.js?stamp=1706231732" type="module"></script></body>
<script src="/uds/res/admin/runtime.js?stamp=1706650006" type="module"></script><script src="/uds/res/admin/polyfills.js?stamp=1706650006" type="module"></script><script src="/uds/res/admin/main.js?stamp=1706650006" type="module"></script></body>
</html>

View File

@ -135,8 +135,9 @@ class HTML5SSHTransport(transports.Transport):
)
ssh_host_key = gui.TextField(
label=_('SSH Host Key'),
length=512,
order=34,
tooltip=_('Host key of the SSH server. If not provided, no verification of host identity is done.'),
tooltip=_('Host key of the SSH server. If not provided, no verification of host identity is done. (as the line in known_hosts file)'),
tab=types.ui.Tab.PARAMETERS,
old_field_name='sshHostKey'
)