upgrading OpenNebula connector

This commit is contained in:
Adolfo Gómez García 2019-11-07 14:42:18 +01:00
parent 99fe68608c
commit ce91840622
8 changed files with 494 additions and 361 deletions

View File

@ -31,6 +31,7 @@
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
import logging import logging
import typing
from django.utils.translation import ugettext_noop as _ from django.utils.translation import ugettext_noop as _
from uds.core.services import ServiceProvider from uds.core.services import ServiceProvider
@ -43,7 +44,7 @@ from . import on
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Provider(ServiceProvider): class Provider(ServiceProvider): # pylint: disable=too-many-public-methods
''' '''
This class represents the sample services provider 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) 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 # Own variables
_api = None _api: typing.Optional[on.client.OpenNebulaClient] = None
def initialize(self, values=None): def initialize(self, values=None):
''' '''
@ -106,18 +107,18 @@ class Provider(ServiceProvider):
# Just reset _api connection variable # Just reset _api connection variable
self._api = None self._api = None
if values is not None: if values:
self.timeout.value = validators.validateTimeout(self.timeout.value) self.timeout.value = validators.validateTimeout(self.timeout.value)
logger.debug('Endpoint: %s', self.endpoint) logger.debug('Endpoint: %s', self.endpoint)
@property @property
def endpoint(self): def endpoint(self) -> str:
return 'http{}://{}:{}/RPC2'.format('s' if self.ssl.isTrue() else '', self.host.value, self.port.value) return 'http{}://{}:{}/RPC2'.format('s' if self.ssl.isTrue() else '', self.host.value, self.port.value)
@property @property
def api(self): def api(self) -> on.client.OpenNebulaClient:
if self._api is None: 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) logger.debug('Api: %s', self._api)
return self._api return self._api

View File

@ -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 storage
from . import template from . import template
from . import vm from . import vm
from . import client
# Import submodules # Import common
from .common import VmState, ImageState, sanitizeName 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 users resources - -2: All resources - -1: Connected users and his groups resources - > = 0: UID Users 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 users resources - -2: All resources - -1: Connected users and his groups resources - > = 0: UID Users 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 users resources - -2: All resources - -1: Connected users and his groups resources - > = 0: UID Users 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)

View File

@ -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 users resources - -2: All resources - -1: Connected users and his groups resources - > = 0: UID Users 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 users resources - -2: All resources - -1: Connected users and his groups resources - > = 0: UID Users 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 users resources - -2: All resources - -1: Connected users and his groups resources - > = 0: UID Users 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)

View File

@ -32,7 +32,6 @@
""" """
# import sys # import sys
import types
import re import re
import logging import logging
@ -40,16 +39,6 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# module = sys.modules[__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): def sanitizeName(name):
""" """
machine names with [a-zA-Z0-9_-] machine names with [a-zA-Z0-9_-]

View File

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2012-2019 Virtual Cable S.L. # Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
@ -30,14 +29,18 @@
""" """
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
""" """
import logging 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__) 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 0 seems to be images datastore
""" """
return api.enumStorage(datastoreType) yield from api.enumStorage(datastoreType)

View File

@ -32,22 +32,26 @@
""" """
import logging import logging
import typing
from defusedxml import minidom from defusedxml import minidom
# Python bindings for OpenNebula
from . import types
from .common import sanitizeName from .common import sanitizeName
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from . import client
logger = logging.getLogger(__name__) 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(): for t in api.enumTemplates():
if t[1][:4] != 'UDSP': # 0 = id, 1 = name if t.name[:4] != 'UDSP':
yield t 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 Publish the machine (makes a template from it so we can create COWs) and returns the template id of
the creating machine the creating machine
@ -69,15 +73,13 @@ def create(api, fromTemplateId, name, toDataStore):
templateId = api.cloneTemplate(fromTemplateId, name) templateId = api.cloneTemplate(fromTemplateId, name)
# Now copy cloned images if possible # 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] template = minidom.parseString(info).getElementsByTagName('TEMPLATE')[0]
logger.debug('XML: %s', template.toxml()) logger.debug('XML: %s', template.toxml())
counter = 0 for counter, dsk in enumerate(template.getElementsByTagName('DISK')):
for dsk in template.getElementsByTagName('DISK'):
counter += 1
imgIds = dsk.getElementsByTagName('IMAGE_ID') imgIds = dsk.getElementsByTagName('IMAGE_ID')
if not imgIds: if not imgIds:
fromId = False fromId = False
@ -114,7 +116,7 @@ def create(api, fromTemplateId, name, toDataStore):
# api.call('template.update', templateId, template.toxml()) # api.call('template.update', templateId, template.toxml())
api.updateTemplate(templateId, template.toxml()) api.updateTemplate(templateId, template.toxml())
return str(templateId) return templateId
except Exception as e: except Exception as e:
logger.exception('Creating template on OpenNebula') logger.exception('Creating template on OpenNebula')
try: try:
@ -124,7 +126,7 @@ def create(api, fromTemplateId, name, toDataStore):
raise e raise e
def remove(api, templateId): def remove(api: 'client.OpenNebulaClient', templateId: str) -> None:
""" """
Removes a template from ovirt server 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) # First, remove Images (wont be possible if there is any images already in use, but will try)
# Now copy cloned images if possible # Now copy cloned images if possible
try: 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] template = minidom.parseString(info).getElementsByTagName('TEMPLATE')[0]
logger.debug('XML: %s', template.toxml()) logger.debug('XML: %s', template.toxml())
@ -163,7 +165,7 @@ def remove(api, templateId):
except Exception: except Exception:
logger.error('Removing template on OpenNebula') 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 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 Id of the machine being created form template
""" """
vmId = api.instantiateTemplate(templateId, name, False, '', False) # api.call('template.instantiate', int(templateId), name, False, '') 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...) checks if the template is fully published (images are ready...)
""" """
try: 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] template = minidom.parseString(info).getElementsByTagName('TEMPLATE')[0]
logger.debug('XML: %s', template.toxml()) logger.debug('XML: %s', template.toxml())
@ -203,12 +205,13 @@ def checkPublished(api, templateId):
logger.debug('Found %s for checking', imgId) logger.debug('Found %s for checking', imgId)
state = api.imageInfo(imgId)[0]['IMAGE']['STATE'] state = api.imageInfo(imgId).state
if state in ('0', '4'): if state in (types.ImageState.INIT, types.ImageState.LOCKED):
return False 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)') 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) api.makePersistentImage(imgId, False)
except Exception: except Exception:

View File

@ -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]

View File

@ -30,19 +30,21 @@
''' '''
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
import logging import logging
# import oca import typing
from defusedxml import minidom 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__) logger = logging.getLogger(__name__)
def getMachineState(api, machineId): def getMachineState(api: 'client.OpenNebulaClient', machineId: str) -> types.VmState:
''' '''
Returns the state of the machine Returns the state of the machine
This method do not uses cache at all (it always tries to get machine state from OpenNebula server) 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 one of the on.VmState Values
''' '''
try: try:
# vm = oca.VirtualMachine.new_with_id(api, int(machineId))
# vm.info()
# return vm.state
return api.getVMState(machineId) return api.getVMState(machineId)
except Exception as e: except Exception as e:
logger.error('Error obtaining machine state for %s on OpenNebula: %s', machineId, 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 Returns the lcm_state
''' '''
@ -73,10 +72,10 @@ def getMachineSubstate(api, machineId):
except Exception as e: except Exception as e:
logger.error('Error obtaining machine substate for %s on OpenNebula: %s', machineId, 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. Tries to start a machine. No check is done, it is simply requested to OpenNebula.
@ -94,7 +93,7 @@ def startMachine(api, machineId):
pass 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 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) 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 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) 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 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) 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 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) raise Exception(err)
def enumerateMachines(api): def enumerateMachines(api: 'client.OpenNebulaClient') -> typing.Iterable[types.VirtualMachineType]:
''' '''
Obtains the list of machines inside OpenNebula. Obtains the list of machines inside OpenNebula.
Machines starting with UDS are filtered out Machines starting with UDS are filtered out
@ -173,7 +172,7 @@ def enumerateMachines(api):
'id' 'id'
'cluster_id' 'cluster_id'
''' '''
return api.enumVMs() yield from api.enumVMs()
def getNetInfo(api, machineId, networkId=None): def getNetInfo(api, machineId, networkId=None):