diff --git a/server/src/uds/services/OpenNebula/Provider.py b/server/src/uds/services/OpenNebula/Provider.py index dadd2482..c5aaf5f5 100644 --- a/server/src/uds/services/OpenNebula/Provider.py +++ b/server/src/uds/services/OpenNebula/Provider.py @@ -31,6 +31,7 @@ .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com ''' import logging +import typing from django.utils.translation import ugettext_noop as _ from uds.core.services import ServiceProvider @@ -43,7 +44,7 @@ from . import on logger = logging.getLogger(__name__) -class Provider(ServiceProvider): +class Provider(ServiceProvider): # pylint: disable=too-many-public-methods ''' This class represents the sample services provider @@ -96,7 +97,7 @@ class Provider(ServiceProvider): timeout = gui.NumericField(length=3, label=_('Timeout'), defvalue='10', order=90, tooltip=_('Timeout in seconds of connection to OpenNebula'), required=True, tab=gui.ADVANCED_TAB) # Own variables - _api = None + _api: typing.Optional[on.client.OpenNebulaClient] = None def initialize(self, values=None): ''' @@ -106,18 +107,18 @@ class Provider(ServiceProvider): # Just reset _api connection variable self._api = None - if values is not None: + if values: self.timeout.value = validators.validateTimeout(self.timeout.value) logger.debug('Endpoint: %s', self.endpoint) @property - def endpoint(self): + def endpoint(self) -> str: return 'http{}://{}:{}/RPC2'.format('s' if self.ssl.isTrue() else '', self.host.value, self.port.value) @property - def api(self): + def api(self) -> on.client.OpenNebulaClient: if self._api is None: - self._api = on.OpenNebulaClient(self.username.value, self.password.value, self.endpoint) + self._api = on.client.OpenNebulaClient(self.username.value, self.password.value, self.endpoint) logger.debug('Api: %s', self._api) return self._api diff --git a/server/src/uds/services/OpenNebula/on/__init__.py b/server/src/uds/services/OpenNebula/on/__init__.py index 8115538a..cef08cf0 100644 --- a/server/src/uds/services/OpenNebula/on/__init__.py +++ b/server/src/uds/services/OpenNebula/on/__init__.py @@ -1,304 +1,7 @@ -# -*- 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. 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 -""" - -# pylint: disable=maybe-no-member -import types -import xmlrpc.client -import logging - -from uds.core.util import xml2dict - from . import storage from . import template from . import vm +from . import client -# Import submodules +# Import common from .common import VmState, ImageState, sanitizeName - -logger = logging.getLogger(__name__) - - -# Decorator -def ensureConnected(fnc): - def inner(*args, **kwargs): - args[0].connect() - return fnc(*args, **kwargs) - return inner - - -# Result checker -def checkResult(lst, parseResult=True): - if not lst[0]: - raise Exception('OpenNebula error {}: "{}"'.format(lst[2], lst[1])) - if parseResult: - return xml2dict.parse(lst[1]) - - return lst[1] - - -def asList(element): - if isinstance(element, (tuple, list)): - return element - return (element,) - - -# noinspection PyShadowingNames -class OpenNebulaClient: - def __init__(self, username, password, endpoint): - self.username = username - self.password = password - self.endpoint = endpoint - self.connection = None - self.cachedVersion = None - - @property - def sessionString(self): - return '{}:{}'.format(self.username, self.password) - - @property # type: ignore - @ensureConnected - def version(self): - if self.cachedVersion is None: - # Retrieve Version & keep it - result = self.connection.one.system.version(self.sessionString) - self.cachedVersion = checkResult(result, parseResult=False).split('.') - return self.cachedVersion - - def connect(self): - if self.connection is not None: - return - - self.connection = xmlrpc.client.ServerProxy(self.endpoint) # @UndefinedVariable - - @ensureConnected - def enumStorage(self, storageType=0): - storageType = str(storageType) # Ensure it is an string - # Invoke datastore pools info, no parameters except connection string - result = self.connection.one.datastorepool.info(self.sessionString) - result = checkResult(result) - for ds in asList(result['DATASTORE_POOL']['DATASTORE']): - if ds['TYPE'] == storageType: - yield(ds['ID'], ds['NAME'], ds['TOTAL_MB'], ds['FREE_MB']) - - @ensureConnected - def enumTemplates(self): - """ - Invoke templates pools info, with this parameters: - 1.- Session string - 2.- Filter flag - < = -3: Connected user’s resources - -2: All resources - -1: Connected user’s and his group’s resources - > = 0: UID User’s Resources - 3.- When the next parameter is >= -1 this is the Range start ID. Can be -1. For smaller values this is the offset used for pagination. - 4.- For values >= -1 this is the Range end ID. Can be -1 to get until the last ID. For values < -1 this is the page size used for pagination. - """ - result = checkResult(self.connection.one.templatepool.info(self.sessionString, -1, -1, -1)) - for ds in asList(result['VMTEMPLATE_POOL']['VMTEMPLATE']): - try: - yield(ds['ID'], ds['NAME'], ds['TEMPLATE']['MEMORY']) - except Exception: # Maybe no memory? (then template is not usable) - pass - - @ensureConnected - def enumImages(self): - """ - Invoke images pools info, with this parameters: - 1.- Session string - 2.- Filter flag - < = -3: Connected user’s resources - -2: All resources - -1: Connected user’s and his group’s resources - > = 0: UID User’s Resources - 3.- When the next parameter is >= -1 this is the Range start ID. Can be -1. For smaller values this is the offset used for pagination. - 4.- For values >= -1 this is the Range end ID. Can be -1 to get until the last ID. For values < -1 this is the page size used for pagination. - """ - result = self.connection.one.imagepool.info(self.sessionString, -1, -1, -1) - result = checkResult(result) - for ds in asList(result['IMAGE_POOL']['IMAGE']): - yield(ds['ID'], ds['NAME']) - - @ensureConnected - def templateInfo(self, templateId, extraInfo=False): - """ - Returns a list - first element is a dictionary (built from XML) - second is original XML - """ - result = self.connection.one.template.info(self.sessionString, int(templateId), extraInfo) - res = checkResult(result) - return res, result[1] - - @ensureConnected - def instantiateTemplate(self, templateId, vmName, createHold=False, templateToMerge='', privatePersistent=False): - """ - Instantiates a template (compatible with open nebula 4 & 5) - 1.- Session string - 2.- ID Of the template to instantiate - 3.- Name of the vm. If empty, open nebula will assign one - 4.- False to create machine on pending (default), True to create it on hold - 5.- A string containing an extra template to be merged with the one being instantiated. It can be empty. Syntax can be the usual attribute=value or XML. - 6.- true to create a private persistent copy of the template plus any image defined in DISK, and instantiate that copy. - Note: This parameter is ignored on version 4, it is new for version 5. - """ - if self.version[0] == '4': # Version 4 has one less parameter than version 5 - result = self.connection.one.template.instantiate(self.sessionString, int(templateId), vmName, createHold, templateToMerge) - else: - result = self.connection.one.template.instantiate(self.sessionString, int(templateId), vmName, createHold, templateToMerge, privatePersistent) - - return checkResult(result, parseResult=False) - - @ensureConnected - def updateTemplate(self, templateId, templateData, updateType=0): - """ - Updates the template with the templateXml - 1.- Session string - 2.- Object ID (integer) - 3.- The new template contents. Syntax can be the usual attribute=value or XML. - 4.- Update type. 0 replace the whole template, 1 merge with the existing one - """ - result = self.connection.one.template.update(self.sessionString, int(templateId), templateData, int(updateType)) - return checkResult(result, parseResult=False) - - @ensureConnected - def cloneTemplate(self, templateId, name): - """ - Clones the template - """ - if self.version[0] == '4': - result = self.connection.one.template.clone(self.sessionString, int(templateId), name) - else: - result = self.connection.one.template.clone(self.sessionString, int(templateId), name, False) # This works as previous version clone - - return checkResult(result, parseResult=False) - - @ensureConnected - def deleteTemplate(self, templateId): - """ - Deletes the template (not images) - """ - result = self.connection.one.template.delete(self.sessionString, int(templateId)) - return checkResult(result, parseResult=False) - - @ensureConnected - def cloneImage(self, srcId, name, datastoreId=-1): - """ - Clones the image. - """ - result = self.connection.one.image.clone(self.sessionString, int(srcId), name, int(datastoreId)) - return checkResult(result, parseResult=False) - - @ensureConnected - def makePersistentImage(self, imageId, persistent=False): - """ - Clones the image. - """ - result = self.connection.one.image.persistent(self.sessionString, int(imageId), persistent) - return checkResult(result, parseResult=False) - - @ensureConnected - def deleteImage(self, imageId): - """ - Deletes an image - """ - result = self.connection.one.image.delete(self.sessionString, int(imageId)) - return checkResult(result, parseResult=False) - - @ensureConnected - def imageInfo(self, imageInfo): - """ - Returns a list - first element is a dictionary (built from XML) - second is original XML - """ - result = self.connection.one.image.info(self.sessionString, int(imageInfo)) - res = checkResult(result) - return res, result[1] - - @ensureConnected - def enumVMs(self): - """ - Invoke vm pools info, with this parameters: - 1.- Session string - 2.- Filter flag - < = -3: Connected user’s resources - -2: All resources - -1: Connected user’s and his group’s resources - > = 0: UID User’s Resources - 3.- When the next parameter is >= -1 this is the Range start ID. Can be -1. For smaller values this is the offset used for pagination. - 4.- For values >= -1 this is the Range end ID. Can be -1 to get until the last ID. For values < -1 this is the page size used for pagination. - 5.- VM state to filter by. (-2 = any state including DONE, -1 = any state EXCEPT DONE) - """ - result = self.connection.one.vmpool.info(self.sessionString, -1, -1, -1, -1) - result = checkResult(result) - for ds in asList(result['VM_POOL']['VM']): - yield(ds['ID'], ds['NAME']) - - @ensureConnected - def VMInfo(self, vmId): - """ - Returns a list - first element is a dictionary (built from XML) - second is original XML - """ - result = self.connection.one.vm.info(self.sessionString, int(vmId)) - res = checkResult(result) - return res, result[1] - - @ensureConnected - def deleteVM(self, vmId): - """ - Deletes an vm - """ - if self.version[0] == '4': - return self.VMAction(vmId, 'delete') - else: - # Version 5 - return self.VMAction(vmId, 'terminate-hard') - - @ensureConnected - def getVMState(self, vmId): - """ - Returns the VM State - """ - result = self.connection.one.vm.info(self.sessionString, int(vmId)) - return int(checkResult(result)['VM']['STATE']) - - @ensureConnected - def getVMSubstate(self, vmId): - """ - Returns the VM State - """ - result = self.connection.one.vm.info(self.sessionString, int(vmId)) - r = checkResult(result) - try: - if int(r['VM']['STATE']) == VmState.ACTIVE: - return int(r['VM']['LCM_STATE']) - # Substate is not available if VM state is not active - return -1 - except Exception: - logger.exception('getVMSubstate') - return -1 - - @ensureConnected - def VMAction(self, vmId, action): - result = self.connection.one.vm.action(self.sessionString, action, int(vmId)) - return checkResult(result, parseResult=False) diff --git a/server/src/uds/services/OpenNebula/on/client.py b/server/src/uds/services/OpenNebula/on/client.py new file mode 100644 index 00000000..03618b64 --- /dev/null +++ b/server/src/uds/services/OpenNebula/on/client.py @@ -0,0 +1,325 @@ +# -*- 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. 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 +""" + +# pylint: disable=maybe-no-member +import xmlrpc.client +import logging +import typing + +from uds.core.util import xml2dict + +from . import types + +logger = logging.getLogger(__name__) + +RT = typing.TypeVar('RT') + +# Decorator +def ensureConnected(fnc: typing.Callable[..., RT]) -> typing.Callable[..., RT]: + def inner(*args, **kwargs) -> RT: + args[0].connect() + return fnc(*args, **kwargs) + return inner + + +# Result checker +def checkResultRaw(lst: typing.List) -> str: + # Openebula response is always this way: + # [Boolean, String, ErrorCode] + # First is True if ok, False if not + # Second is Result String if was ok + # Third is error code if first is False + if not lst[0]: + raise Exception('OpenNebula error {}: "{}"'.format(lst[2], lst[1])) + + return lst[1] + + +def checkResult(lst: typing.List) -> typing.Tuple[typing.Dict, str]: + return xml2dict.parse(checkResultRaw(lst)), lst[1] + + +def asIterable(element: RT) -> typing.Iterable[RT]: + if isinstance(element, (tuple, list)): + return element + return (element,) + + +class OpenNebulaClient: # pylint: disable=too-many-public-methods + username: str + password: str + endpoint: str + connection: xmlrpc.client.ServerProxy + cachedVersion: typing.Optional[typing.List[str]] + + def __init__(self, username: str, password: str, endpoint: str) -> None: + self.username = username + self.password = password + self.endpoint = endpoint + self.connection = None + self.cachedVersion = None + + @property + def sessionString(self): + return '{}:{}'.format(self.username, self.password) + + @property # type: ignore + @ensureConnected + def version(self) -> typing.List[str]: + if self.cachedVersion is None: + # Retrieve Version & keep it + result = self.connection.one.system.version(self.sessionString) + self.cachedVersion = checkResultRaw(result).split('.') + return self.cachedVersion + + def connect(self) -> None: + if self.connection is not None: + return + + self.connection = xmlrpc.client.ServerProxy(self.endpoint) + + @ensureConnected + def enumStorage(self, storageType: int = 0) -> typing.Iterable[types.StorageType]: + sstorageType = str(storageType) # Ensure it is an string + # Invoke datastore pools info, no parameters except connection string + result, _ = checkResult(self.connection.one.datastorepool.info(self.sessionString)) + for ds in asIterable(result['DATASTORE_POOL']['DATASTORE']): + if ds['TYPE'] == sstorageType: + yield types.StorageType(ds['ID'], ds['NAME'], int(ds['TOTAL_MB']), int(ds['FREE_MB']), None) + + @ensureConnected + def enumTemplates(self) -> typing.Iterable[types.TemplateType]: + """ + Invoke templates pools info, with this parameters: + 1.- Session string + 2.- Filter flag - < = -3: Connected user’s resources - -2: All resources - -1: Connected user’s and his group’s resources - > = 0: UID User’s Resources + 3.- When the next parameter is >= -1 this is the Range start ID. Can be -1. For smaller values this is the offset used for pagination. + 4.- For values >= -1 this is the Range end ID. Can be -1 to get until the last ID. For values < -1 this is the page size used for pagination. + """ + result, _ = checkResult(self.connection.one.templatepool.info(self.sessionString, -1, -1, -1)) + for ds in asIterable(result['VMTEMPLATE_POOL']['VMTEMPLATE']): + try: + yield types.TemplateType(ds['ID'], ds['NAME'], int(ds['TEMPLATE']['MEMORY']), None) + except Exception: # Maybe no memory? (then template is not usable) + pass + + @ensureConnected + def enumImages(self) -> typing.Iterable[types.ImageType]: + """ + Invoke images pools info, with this parameters: + 1.- Session string + 2.- Filter flag - < = -3: Connected user’s resources - -2: All resources - -1: Connected user’s and his group’s resources - > = 0: UID User’s Resources + 3.- When the next parameter is >= -1 this is the Range start ID. Can be -1. For smaller values this is the offset used for pagination. + 4.- For values >= -1 this is the Range end ID. Can be -1 to get until the last ID. For values < -1 this is the page size used for pagination. + """ + result, _ = checkResult(self.connection.one.imagepool.info(self.sessionString, -1, -1, -1)) + for ds in asIterable(result['IMAGE_POOL']['IMAGE']): + yield types.ImageType( + ds['ID'], + ds['NAME'], + int(ds.get('SIZE', -1)), + ds.get('PERSISTENT', '0') != '0', + int(ds.get('RUNNING_VMS', '0')), + types.ImageState.fromState(ds['STATE']), + None + ) + + @ensureConnected + def templateInfo(self, templateId: str, extraInfo: bool = False) -> types.TemplateType: + """ + Returns a list + first element is a dictionary (built from XML) + second is original XML + """ + result = self.connection.one.template.info(self.sessionString, int(templateId), extraInfo) + ds, xml = checkResult(result) + return types.TemplateType(ds['VMTEMPLATE']['ID'], ds['VMTEMPLATE']['NAME'], int(ds['VMTEMPLATE']['TEMPLATE']['MEMORY']), xml) + + @ensureConnected + def instantiateTemplate(self, templateId: str, vmName: str, createHold: bool = False, templateToMerge: str = '', privatePersistent: bool = False) -> str: + """ + Instantiates a template (compatible with open nebula 4 & 5) + 1.- Session string + 2.- ID Of the template to instantiate + 3.- Name of the vm. If empty, open nebula will assign one + 4.- False to create machine on pending (default), True to create it on hold + 5.- A string containing an extra template to be merged with the one being instantiated. It can be empty. Syntax can be the usual attribute=value or XML. + 6.- true to create a private persistent copy of the template plus any image defined in DISK, and instantiate that copy. + Note: This parameter is ignored on version 4, it is new for version 5. + """ + if self.version[0] == '4': # Version 4 has one less parameter than version 5 + result = self.connection.one.template.instantiate(self.sessionString, int(templateId), vmName, createHold, templateToMerge) + else: + result = self.connection.one.template.instantiate(self.sessionString, int(templateId), vmName, createHold, templateToMerge, privatePersistent) + + return checkResultRaw(result) + + @ensureConnected + def updateTemplate(self, templateId: str, templateData: str, updateType: int = 0) -> str: + """ + Updates the template with the templateXml + 1.- Session string + 2.- Object ID (integer) + 3.- The new template contents. Syntax can be the usual attribute=value or XML. + 4.- Update type. 0 replace the whole template, 1 merge with the existing one + """ + result = self.connection.one.template.update(self.sessionString, int(templateId), templateData, int(updateType)) + return checkResultRaw(result) + + @ensureConnected + def cloneTemplate(self, templateId: str, name: str) -> str: + """ + Clones the template + """ + if self.version[0] == '4': + result = self.connection.one.template.clone(self.sessionString, int(templateId), name) + else: + result = self.connection.one.template.clone(self.sessionString, int(templateId), name, False) # This works as previous version clone + + return checkResultRaw(result) + + @ensureConnected + def deleteTemplate(self, templateId: str) -> str: + """ + Deletes the template (not images) + """ + result = self.connection.one.template.delete(self.sessionString, int(templateId)) + return checkResultRaw(result) + + @ensureConnected + def cloneImage(self, srcId: str, name: str, datastoreId: typing.Union[str, int] = -1) -> str: + """ + Clones the image. + """ + result = self.connection.one.image.clone(self.sessionString, int(srcId), name, int(datastoreId)) + return checkResultRaw(result) + + @ensureConnected + def makePersistentImage(self, imageId: str, persistent: bool = False) -> str: + """ + Clones the image. + """ + result = self.connection.one.image.persistent(self.sessionString, int(imageId), persistent) + return checkResultRaw(result) + + @ensureConnected + def deleteImage(self, imageId: str) -> str: + """ + Deletes an image + """ + result = self.connection.one.image.delete(self.sessionString, int(imageId)) + return checkResultRaw(result) + + @ensureConnected + def imageInfo(self, imageInfo) -> types.ImageType: + """ + Returns a list + first element is a dictionary (built from XML) + second is original XML + """ + result, xml = checkResult(self.connection.one.image.info(self.sessionString, int(imageInfo))) + ds = result['IMAGE'] + return types.ImageType( + ds['ID'], + ds['NAME'], + int(ds.get('SIZE', -1)), + ds.get('PERSISTENT', '0') != '0', + int(ds.get('RUNNING_VMS', '0')), + types.ImageState.fromState(ds['STATE']), + xml + ) + + @ensureConnected + def enumVMs(self) -> typing.Iterable[types.VirtualMachineType]: + """ + Invoke vm pools info, with this parameters: + 1.- Session string + 2.- Filter flag - < = -3: Connected user’s resources - -2: All resources - -1: Connected user’s and his group’s resources - > = 0: UID User’s Resources + 3.- When the next parameter is >= -1 this is the Range start ID. Can be -1. For smaller values this is the offset used for pagination. + 4.- For values >= -1 this is the Range end ID. Can be -1 to get until the last ID. For values < -1 this is the page size used for pagination. + 5.- VM state to filter by. (-2 = any state including DONE, -1 = any state EXCEPT DONE) + """ + result, _ = checkResult(self.connection.one.vmpool.info(self.sessionString, -1, -1, -1, -1)) + if result['VM_POOL']: + for ds in asIterable(result['VM_POOL'].get('VM', [])): + yield types.VirtualMachineType(ds['ID'], ds['NAME'], int(ds.get('MEMORY', '0')), types.VmState.fromState(ds['STATE']), None) + + @ensureConnected + def VMInfo(self, vmId: str) -> types.VirtualMachineType: + """ + Returns a list + first element is a dictionary (built from XML) + second is original XML + """ + result, xml = checkResult(self.connection.one.vm.info(self.sessionString, int(vmId))) + ds = result['VM'] + return types.VirtualMachineType(ds['ID'], ds['NAME'], int(ds.get('MEMORY', '0')), types.VmState.fromState(ds['STATE']), xml) + + @ensureConnected + def deleteVM(self, vmId: str) -> str: + """ + Deletes an vm + """ + if self.version[0] == '4': + return self.VMAction(vmId, 'delete') + + # Version 5 + return self.VMAction(vmId, 'terminate-hard') + + @ensureConnected + def getVMState(self, vmId: str) -> types.VmState: + """ + Returns the VM State + """ + return self.VMInfo(vmId).state + + @ensureConnected + def getVMSubstate(self, vmId: str) -> int: + """ + Returns the VM State + """ + result = self.connection.one.vm.info(self.sessionString, int(vmId)) + r, _ = checkResult(result) + try: + if int(r['VM']['STATE']) == types.VmState.ACTIVE.value: + return int(r['VM']['LCM_STATE']) + # Substate is not available if VM state is not active + return -1 + except Exception: + logger.exception('getVMSubstate') + return -1 + + @ensureConnected + def VMAction(self, vmId: str, action: str) -> str: + result = self.connection.one.vm.action(self.sessionString, action, int(vmId)) + return checkResultRaw(result) diff --git a/server/src/uds/services/OpenNebula/on/common.py b/server/src/uds/services/OpenNebula/on/common.py index 7fcb6707..cfe2a2dd 100644 --- a/server/src/uds/services/OpenNebula/on/common.py +++ b/server/src/uds/services/OpenNebula/on/common.py @@ -32,7 +32,6 @@ """ # import sys -import types import re import logging @@ -40,16 +39,6 @@ import logging logger = logging.getLogger(__name__) # module = sys.modules[__name__] -VmState = types.ModuleType('VmState') -ImageState = types.ModuleType('ImageState') - -for i in enumerate(['INIT', 'PENDING', 'HOLD', 'ACTIVE', 'STOPPED', 'SUSPENDED', 'DONE', 'FAILED', 'POWEROFF', 'UNDEPLOYED', 'UNKNOWN']): - setattr(VmState, i[1], i[0]) - -for i in enumerate(['INIT', 'READY', 'USED', 'DISABLED', 'LOCKED', 'ERROR', 'CLONE', 'DELETE', 'USED_PERS', 'LOCKED_USED', 'LOCKED_USED_PERS']): - setattr(ImageState, i[1], i[0]) - - def sanitizeName(name): """ machine names with [a-zA-Z0-9_-] diff --git a/server/src/uds/services/OpenNebula/on/storage.py b/server/src/uds/services/OpenNebula/on/storage.py index f72210f4..d5b2557f 100644 --- a/server/src/uds/services/OpenNebula/on/storage.py +++ b/server/src/uds/services/OpenNebula/on/storage.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - # # Copyright (c) 2012-2019 Virtual Cable S.L. # All rights reserved. @@ -30,14 +29,18 @@ """ .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com """ - import logging +import typing +# Not imported at runtime, just for type checking +if typing.TYPE_CHECKING: + from . import client + from . import types logger = logging.getLogger(__name__) -def enumerateDatastores(api, datastoreType=0): +def enumerateDatastores(api: 'client.OpenNebulaClient', datastoreType: int = 0) -> typing.Iterable['types.StorageType']: """ 0 seems to be images datastore """ - return api.enumStorage(datastoreType) + yield from api.enumStorage(datastoreType) diff --git a/server/src/uds/services/OpenNebula/on/template.py b/server/src/uds/services/OpenNebula/on/template.py index 190afff1..d3166d86 100644 --- a/server/src/uds/services/OpenNebula/on/template.py +++ b/server/src/uds/services/OpenNebula/on/template.py @@ -32,22 +32,26 @@ """ import logging +import typing from defusedxml import minidom -# Python bindings for OpenNebula + +from . import types from .common import sanitizeName +# Not imported at runtime, just for type checking +if typing.TYPE_CHECKING: + from . import client logger = logging.getLogger(__name__) -def getTemplates(api, force=False): - +def getTemplates(api: 'client.OpenNebulaClient', force: bool = False) -> typing.Iterable[types.TemplateType]: for t in api.enumTemplates(): - if t[1][:4] != 'UDSP': # 0 = id, 1 = name + if t.name[:4] != 'UDSP': yield t -def create(api, fromTemplateId, name, toDataStore): +def create(api: 'client.OpenNebulaClient', fromTemplateId: str, name: str, toDataStore: str) -> str: """ Publish the machine (makes a template from it so we can create COWs) and returns the template id of the creating machine @@ -69,15 +73,13 @@ def create(api, fromTemplateId, name, toDataStore): templateId = api.cloneTemplate(fromTemplateId, name) # Now copy cloned images if possible - imgs = dict(((i[1], i[0]) for i in api.enumImages())) + imgs = {i.name: i.id for i in api.enumImages()} - info = api.templateInfo(templateId)[1] + info = api.templateInfo(templateId).xml template = minidom.parseString(info).getElementsByTagName('TEMPLATE')[0] logger.debug('XML: %s', template.toxml()) - counter = 0 - for dsk in template.getElementsByTagName('DISK'): - counter += 1 + for counter, dsk in enumerate(template.getElementsByTagName('DISK')): imgIds = dsk.getElementsByTagName('IMAGE_ID') if not imgIds: fromId = False @@ -114,7 +116,7 @@ def create(api, fromTemplateId, name, toDataStore): # api.call('template.update', templateId, template.toxml()) api.updateTemplate(templateId, template.toxml()) - return str(templateId) + return templateId except Exception as e: logger.exception('Creating template on OpenNebula') try: @@ -124,7 +126,7 @@ def create(api, fromTemplateId, name, toDataStore): raise e -def remove(api, templateId): +def remove(api: 'client.OpenNebulaClient', templateId: str) -> None: """ Removes a template from ovirt server @@ -134,9 +136,9 @@ def remove(api, templateId): # First, remove Images (wont be possible if there is any images already in use, but will try) # Now copy cloned images if possible try: - imgs = dict(((i[1], i[0]) for i in api.enumImages())) + imgs = {i.name: i.id for i in api.enumImages()} - info = api.templateInfo(templateId)[1] + info = api.templateInfo(templateId).xml template = minidom.parseString(info).getElementsByTagName('TEMPLATE')[0] logger.debug('XML: %s', template.toxml()) @@ -163,7 +165,7 @@ def remove(api, templateId): except Exception: logger.error('Removing template on OpenNebula') -def deployFrom(api, templateId, name): +def deployFrom(api: 'client.OpenNebulaClient', templateId: str, name: str) -> str: """ Deploys a virtual machine on selected cluster from selected template @@ -176,16 +178,16 @@ def deployFrom(api, templateId, name): Id of the machine being created form template """ vmId = api.instantiateTemplate(templateId, name, False, '', False) # api.call('template.instantiate', int(templateId), name, False, '') - return str(vmId) + return vmId -def checkPublished(api, templateId): +def checkPublished(api: 'client.OpenNebulaClient', templateId): """ checks if the template is fully published (images are ready...) """ try: - imgs = dict(((i[1], i[0]) for i in api.enumImages())) + imgs = {i.name: i.id for i in api.enumImages()} - info = api.templateInfo(templateId)[1] + info = api.templateInfo(templateId).xml template = minidom.parseString(info).getElementsByTagName('TEMPLATE')[0] logger.debug('XML: %s', template.toxml()) @@ -203,12 +205,13 @@ def checkPublished(api, templateId): logger.debug('Found %s for checking', imgId) - state = api.imageInfo(imgId)[0]['IMAGE']['STATE'] - if state in ('0', '4'): + state = api.imageInfo(imgId).state + if state in (types.ImageState.INIT, types.ImageState.LOCKED): return False - if state != '1': # If error is not READY + if state != types.ImageState.READY: # If error is not READY raise Exception('Error publishing. Image is in an invalid state. (Check it and delete it if not needed anymore)') - # Ensure image is non persistent. This may be invoked more than once, but idoes not matters + + # Ensure image is non persistent. This may be invoked more than once, but it does not matters api.makePersistentImage(imgId, False) except Exception: diff --git a/server/src/uds/services/OpenNebula/on/types.py b/server/src/uds/services/OpenNebula/on/types.py new file mode 100644 index 00000000..879f0a6a --- /dev/null +++ b/server/src/uds/services/OpenNebula/on/types.py @@ -0,0 +1,110 @@ +# -*- 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. 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 enum +import typing + +class VmState(enum.Enum): # pylint: disable=too-few-public-methods + INIT = 0 + PENDING = 1 + HOLD = 2 + ACTIVE = 3 + STOPPED = 4 + SUSPENDED = 5 + DONE = 6 + FAILED = 7 + POWEROFF = 8 + UNDEPLOYED = 9 + + UNKNOWN = 99 + + @staticmethod + def fromState(state: str) -> 'VmState': + try: + return VmState(int(state)) + except Exception: + return VmState.UNKNOWN + + +class ImageState(enum.Enum): # pylint: disable=too-few-public-methods + INIT = 0 + READY = 1 + USED = 2 + DISABLED = 3 + LOCKED = 4 + ERROR = 5 + CLONE = 6 + DELETE = 7 + USED_PERS = 8 + LOCKED_USED = 9 + LOCKED_USED_PERS = 10 + + UNKNOWN = 99 + + @staticmethod + def fromState(state: str) -> 'ImageState': + try: + return ImageState(int(state)) + except Exception: + return ImageState.UNKNOWN + + +class StorageType(typing.NamedTuple): + id: str + name: str + total: int # In Megabytes + free: int # In Megabytes + xml: typing.Optional[str] + + +class TemplateType(typing.NamedTuple): + id: str + name: str + memory: int + xml: typing.Optional[str] + + +class ImageType(typing.NamedTuple): + id: str + name: str + size: int # In Megabytes + persistent: bool + running_vms: int + state: ImageState + xml: typing.Optional[str] + + +class VirtualMachineType(typing.NamedTuple): + id: str + name: str + memory: int + state: VmState + xml: typing.Optional[str] diff --git a/server/src/uds/services/OpenNebula/on/vm.py b/server/src/uds/services/OpenNebula/on/vm.py index 301d3ffe..76388925 100644 --- a/server/src/uds/services/OpenNebula/on/vm.py +++ b/server/src/uds/services/OpenNebula/on/vm.py @@ -30,19 +30,21 @@ ''' .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com ''' - import logging -# import oca +import typing from defusedxml import minidom -# Python bindings for OpenNebula -from .common import VmState +from . import types + +# Not imported at runtime, just for type checking +if typing.TYPE_CHECKING: + from . import client logger = logging.getLogger(__name__) -def getMachineState(api, machineId): +def getMachineState(api: 'client.OpenNebulaClient', machineId: str) -> types.VmState: ''' Returns the state of the machine This method do not uses cache at all (it always tries to get machine state from OpenNebula server) @@ -54,17 +56,14 @@ def getMachineState(api, machineId): one of the on.VmState Values ''' try: - # vm = oca.VirtualMachine.new_with_id(api, int(machineId)) - # vm.info() - # return vm.state return api.getVMState(machineId) except Exception as e: logger.error('Error obtaining machine state for %s on OpenNebula: %s', machineId, e) - return VmState.UNKNOWN + return types.VmState.UNKNOWN -def getMachineSubstate(api, machineId): +def getMachineSubstate(api: 'client.OpenNebulaClient', machineId: str) -> int: ''' Returns the lcm_state ''' @@ -73,10 +72,10 @@ def getMachineSubstate(api, machineId): except Exception as e: logger.error('Error obtaining machine substate for %s on OpenNebula: %s', machineId, e) - return VmState.UNKNOWN + return types.VmState.UNKNOWN.value -def startMachine(api, machineId): +def startMachine(api: 'client.OpenNebulaClient', machineId: str) -> None: ''' Tries to start a machine. No check is done, it is simply requested to OpenNebula. @@ -94,7 +93,7 @@ def startMachine(api, machineId): pass -def stopMachine(api, machineId): +def stopMachine(api: 'client.OpenNebulaClient', machineId: str) -> None: ''' Tries to start a machine. No check is done, it is simply requested to OpenNebula @@ -109,7 +108,7 @@ def stopMachine(api, machineId): logger.error('Error powering off %s on OpenNebula: %s', machineId, e) -def suspendMachine(api, machineId): +def suspendMachine(api: 'client.OpenNebulaClient', machineId: str) -> None: ''' Tries to suspend a machine. No check is done, it is simply requested to OpenNebula @@ -124,7 +123,7 @@ def suspendMachine(api, machineId): logger.error('Error suspending %s on OpenNebula: %s', machineId, e) -def resetMachine(api, machineId): +def resetMachine(api: 'client.OpenNebulaClient', machineId: str) -> None: ''' Tries to suspend a machine. No check is done, it is simply requested to OpenNebula @@ -139,7 +138,7 @@ def resetMachine(api, machineId): logger.error('Error reseting %s on OpenNebula: %s', machineId, e) -def removeMachine(api, machineId): +def removeMachine(api: 'client.OpenNebulaClient', machineId: str) -> None: ''' Tries to delete a machine. No check is done, it is simply requested to OpenNebula @@ -158,7 +157,7 @@ def removeMachine(api, machineId): raise Exception(err) -def enumerateMachines(api): +def enumerateMachines(api: 'client.OpenNebulaClient') -> typing.Iterable[types.VirtualMachineType]: ''' Obtains the list of machines inside OpenNebula. Machines starting with UDS are filtered out @@ -173,7 +172,7 @@ def enumerateMachines(api): 'id' 'cluster_id' ''' - return api.enumVMs() + yield from api.enumVMs() def getNetInfo(api, machineId, networkId=None):