From ce918406223d58d6121008203cbac9f64b02cbcb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adolfo=20G=C3=B3mez=20Garc=C3=ADa?= <dkmaster@dkmon.com>
Date: Thu, 7 Nov 2019 14:42:18 +0100
Subject: [PATCH] upgrading OpenNebula connector

---
 .../src/uds/services/OpenNebula/Provider.py   |  13 +-
 .../uds/services/OpenNebula/on/__init__.py    | 301 +---------------
 .../src/uds/services/OpenNebula/on/client.py  | 325 ++++++++++++++++++
 .../src/uds/services/OpenNebula/on/common.py  |  11 -
 .../src/uds/services/OpenNebula/on/storage.py |  11 +-
 .../uds/services/OpenNebula/on/template.py    |  49 +--
 .../src/uds/services/OpenNebula/on/types.py   | 110 ++++++
 server/src/uds/services/OpenNebula/on/vm.py   |  35 +-
 8 files changed, 494 insertions(+), 361 deletions(-)
 create mode 100644 server/src/uds/services/OpenNebula/on/client.py
 create mode 100644 server/src/uds/services/OpenNebula/on/types.py

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):