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
'''
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

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

View File

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

View File

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

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