diff --git a/server/src/uds/core/util/xml2dict.py b/server/src/uds/core/util/xml2dict.py new file mode 100644 index 00000000..3e630db7 --- /dev/null +++ b/server/src/uds/core/util/xml2dict.py @@ -0,0 +1,61 @@ +''' +Created on Jul 11, 2016 + +@author: dkmaster +''' +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2013 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. + +from __future__ import unicode_literals +from collections import defaultdict +from xml.etree import cElementTree + +def etree_to_dict(t): + d = {t.tag: {} if t.attrib else None} + children = list(t) + if children: + dd = defaultdict(list) + for dc in map(etree_to_dict, children): + for k, v in dc.items(): + dd[k].append(v) + d = {t.tag: {k:v[0] if len(v) == 1 else v for k, v in dd.items()}} + if t.attrib: + d[t.tag].update(('@' + k, v) for k, v in t.attrib.items()) + if t.text: + text = t.text.strip() + if children or t.attrib: + if text: + d[t.tag]['#text'] = text + else: + d[t.tag] = text + return d + +def parse(xml_string): + return etree_to_dict(cElementTree.XML(xml_string)) # @UndefinedVariable + diff --git a/server/src/uds/services/OpenNebula/Provider.py b/server/src/uds/services/OpenNebula/Provider.py index c0fb5741..c327c79d 100644 --- a/server/src/uds/services/OpenNebula/Provider.py +++ b/server/src/uds/services/OpenNebula/Provider.py @@ -50,7 +50,7 @@ import six # Python bindings for OpenNebula import oca -__updated__ = '2016-04-25' +__updated__ = '2016-07-11' logger = logging.getLogger(__name__) @@ -129,7 +129,7 @@ class Provider(ServiceProvider): @property def api(self): if self._api is None: - self._api = oca.Client('{}:{}'.format(self.username.value, self.password.value), self.endpoint) + self._api = on.OpenNebulaClient(self.username.value, self.password.value, self.endpoint) return self._api diff --git a/server/src/uds/services/OpenNebula/on/__init__.py b/server/src/uds/services/OpenNebula/on/__init__.py index 2003ae60..37e2a446 100644 --- a/server/src/uds/services/OpenNebula/on/__init__.py +++ b/server/src/uds/services/OpenNebula/on/__init__.py @@ -38,12 +38,10 @@ import re import logging import six -from defusedxml import minidom +import xmlrpclib +from uds.core.util import xml2dict -# Python bindings for OpenNebula -import oca - -__updated__ = '2016-02-25' +__updated__ = '2016-07-11' logger = logging.getLogger(__name__) @@ -61,3 +59,79 @@ from . import template from . import vm from . import storage +# 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 lst[0] == False: + raise Exception('OpenNebula error {}: "{}"'.format(lst[2], lst[1])) + if parseResult: + return xml2dict.parse(lst[1]) + else: + return lst[1] + +def asList(element): + if isinstance(element, (tuple, list)): + return element + return (element,) + +class OpenNebulaClient(object): + def __init__(self, username, password, endpoint): + self.username = username + self.password = password + self.endpoint = endpoint + self.connection = None + + @property + def sessionString(self): + return '{}:{}'.format(self.username, self.password) + + + def connect(self): + if self.connection is not None: + return + + self.connection = xmlrpclib.ServerProxy(self.endpoint) + + @ensureConnected + def enumStorage(self, storageType=0): + storageType = six.text_type(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 = self.connection.one.templatepool.info(self.sessionString, -3, -1, -1) + result = checkResult(result) + for ds in asList(result['VMTEMPLATE_POOL']['VMTEMPLATE']): + yield(ds['ID'], ds['NAME'], ds['TEMPLATE']['MEMORY']) + + @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 cloneImage(self, srcId, name, datastoreId=-1): + result = self.connection.one.image.clone(self.sessionString, int(srcId), name, int(datastoreId)) + return checkResult(result, parseResult=False) diff --git a/server/src/uds/services/OpenNebula/on/storage.py b/server/src/uds/services/OpenNebula/on/storage.py index 7b39fc57..f65cabcd 100644 --- a/server/src/uds/services/OpenNebula/on/storage.py +++ b/server/src/uds/services/OpenNebula/on/storage.py @@ -32,11 +32,9 @@ ''' import logging -import six -import oca -__updated__ = '2016-02-09' +__updated__ = '2016-07-11' logger = logging.getLogger(__name__) @@ -44,9 +42,4 @@ def enumerateDatastores(api, datastoreType=0): ''' 0 seems to be images datastore ''' - datastores = oca.DatastorePool(api) - datastores.info() - - for ds in datastores: - if ds.type == datastoreType: - yield (ds.id, ds.name) + return api.enumStorage(datastoreType) diff --git a/server/src/uds/services/OpenNebula/on/template.py b/server/src/uds/services/OpenNebula/on/template.py index f56520a6..498b8896 100644 --- a/server/src/uds/services/OpenNebula/on/template.py +++ b/server/src/uds/services/OpenNebula/on/template.py @@ -39,20 +39,16 @@ from defusedxml import minidom # Python bindings for OpenNebula from .common import sanitizeName -__updated__ = '2016-02-09' +__updated__ = '2016-07-11' logger = logging.getLogger(__name__) def getTemplates(api, force=False): - logger.debug('Api: {}'.format(api)) - templatesPool = oca.VmTemplatePool(api) - templatesPool.info() - - for t in templatesPool: - if t.name[:4] != 'UDSP': - yield (t.id, t.name) + for t in api.enumTemplates(): + if t[1][:4] != 'UDSP': # 0 = id, 1 = name + yield t def create(api, fromTemplateId, name, toDataStore): '''