fully ported openstack provider to python 3.x

This commit is contained in:
Adolfo Gómez García 2019-09-03 12:37:11 +02:00
parent 75f244be2e
commit bada5ed2af
10 changed files with 302 additions and 229 deletions

View File

@ -79,7 +79,7 @@ class Publication(Environmentable, Serializable):
# : but full clone takes a lot, so we suggest that checks are done more steady.
# : This attribute is always accessed using an instance object, so you can
# : change suggestedTime in your implementation.
suggestedTime = 10
suggestedTime: int = 10
_osmanager: typing.Optional['osmanagers.OSManager']
_service: 'services.Service'

View File

@ -235,7 +235,7 @@ class Service(Module):
We will access the returned array in "name" basis. This means that the service will be assigned by "name", so be care that every single service
returned are not repeated... :-)
"""
raise NotImplementedError('The class {0} has been marked as manually asignable but no requestServicesForAssignetion provided!!!'.format(self.__class__.__name__))
raise Exception('The class {0} has been marked as manually asignable but no requestServicesForAssignetion provided!!!'.format(self.__class__.__name__))
def macGenerator(self) -> typing.Optional['UniqueMacGenerator']:
"""

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -30,16 +30,21 @@
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
import pickle
import logging
import typing
from uds.core.services import UserDeployment
from uds.core.util.state import State
from uds.core.util import log
from . import openStack
import pickle
import logging
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from .LiveService import LiveService
from .LivePublication import LivePublication
__updated__ = '2019-02-07'
logger = logging.getLogger(__name__)
@ -58,12 +63,12 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
The logic for managing ovirt deployments (user machines in this case) is here.
"""
_name = ''
_ip = ''
_mac = ''
_vmid = ''
_reason = ''
_queue = None
_name: str = ''
_ip: str = ''
_mac: str = ''
_vmid: str = ''
_reason: str = ''
_queue: typing.List[int] = []
# : Recheck every this seconds by default (for task methods)
suggestedTime = 20
@ -76,8 +81,16 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
self._reason = ''
self._queue = []
# For typing check only...
def service(self) -> 'LiveService':
return typing.cast('LiveService', super().service())
# For typing check only...
def publication(self) -> 'LivePublication':
return typing.cast('LivePublication', super().publication())
# Serializable needed methods
def marshal(self):
def marshal(self) -> bytes:
"""
Does nothing right here, we will use envoronment storage in this sample
"""
@ -91,7 +104,7 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
pickle.dumps(self._queue, protocol=0)
]).encode('utf8')
def unmarshal(self, data: bytes):
def unmarshal(self, data: bytes) -> None:
"""
Does nothing here also, all data are keeped at environment storage
"""
@ -144,7 +157,7 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
:note: This IP is the IP of the "consumed service", so the transport can
access it.
"""
logger.debug('Setting IP to {}'.format(ip))
logger.debug('Setting IP to %s', ip)
self._ip = ip
def getUniqueId(self):
@ -206,15 +219,9 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
if self._vmid != '':
self.service().resetMachine(self._vmid)
def getConsoleConnection(self):
return self.service().getConsoleConnection(self._vmid)
def desktopLogin(self, username, password, domain=''):
return self.service().desktopLogin(self._vmId, username, password, domain)
def notifyReadyFromOsManager(self, data):
# Here we will check for suspending the VM (when full ready)
logger.debug('Checking if cache 2 for {0}'.format(self._name))
logger.debug('Checking if cache 2 for %s', self._name)
if self.__getCurrentOp() == opWait:
logger.debug('Machine is ready. Moving to level 2')
self.__popCurrentOp() # Remove current state
@ -245,7 +252,7 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
self._queue = [opCreate, opWait, opSuspend, opFinish]
def __checkMachineState(self, chkState):
logger.debug('Checking that state of machine {} ({}) is {}'.format(self._vmid, self._name, chkState))
logger.debug('Checking that state of machine %s (%s) is %s', self._vmid, self._name, chkState)
status = self.service().getMachineState(self._vmid)
# If we want to check an state and machine does not exists (except in case that we whant to check this)
@ -253,24 +260,20 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
return self.__error('Machine not available. ({})'.format(status))
ret = State.RUNNING
if type(chkState) is list:
chkState = [chkState] if not isinstance(chkState, (list, tuple)) else chkState
if status in chkState:
ret = State.FINISHED
else:
if status == chkState:
ret = State.FINISHED
return ret
def __getCurrentOp(self):
if len(self._queue) == 0:
if not self._queue:
return opFinish
return self._queue[0]
def __popCurrentOp(self):
if len(self._queue) == 0:
if not self._queue:
return opFinish
res = self._queue.pop(0)
@ -313,7 +316,7 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
if op == opFinish:
return State.FINISHED
fncs = {
fncs: typing.Dict[int, typing.Callable[[], None]] = {
opCreate: self.__create,
opRetry: self.__retry,
opStart: self.__startMachine,
@ -323,12 +326,10 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
}
try:
execFnc = fncs.get(op, None)
if execFnc is None:
if op not in fncs:
return self.__error('Unknown operation found at execution queue ({0})'.format(op))
execFnc()
fncs[op]()
return State.RUNNING
except Exception as e:
@ -432,7 +433,7 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
if op == opFinish:
return State.FINISHED
fncs = {
fncs: typing.Dict[int, typing.Callable[[], str]] = {
opCreate: self.__checkCreate,
opRetry: self.__retry,
opWait: self.__wait,
@ -442,12 +443,10 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
}
try:
chkFnc = fncs.get(op, None)
if op not in fncs:
return self.__error('Unknown operation found at execution queue ({0})'.format(op))
if chkFnc is None:
return self.__error('Unknown operation found at check queue ({0})'.format(op))
state = chkFnc()
state = fncs[op]()
if state == State.FINISHED:
self.__popCurrentOp() # Remove runing op
return self.__executeQueue()
@ -462,7 +461,6 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
(No matter wether it is for cache or for an user)
"""
self.__debug('finish')
pass
def moveToCache(self, newLevel):
"""
@ -478,25 +476,6 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
return self.__executeQueue()
def userLoggedIn(self, user):
"""
This method must be available so os managers can invoke it whenever
an user get logged into a service.
The user provided is just an string, that is provided by actor.
"""
# We store the value at storage, but never get used, just an example
pass
def userLoggedOut(self, user):
"""
This method must be available so os managers can invoke it whenever
an user get logged out if a service.
The user provided is just an string, that is provided by actor.
"""
pass
def reasonOfError(self):
"""
Returns the reason of the error.
@ -553,8 +532,8 @@ class LiveDeployment(UserDeployment): # pylint: disable=too-many-public-methods
}.get(op, '????')
def __debug(self, txt):
logger.debug('_name {0}: {1}'.format(txt, self._name))
logger.debug('_ip {0}: {1}'.format(txt, self._ip))
logger.debug('_mac {0}: {1}'.format(txt, self._mac))
logger.debug('_vmid {0}: {1}'.format(txt, self._vmid))
logger.debug('Queue at {0}: {1}'.format(txt, [LiveDeployment.__op2str(op) for op in self._queue]))
logger.debug('_name %s: %s', txt, self._name)
logger.debug('_ip %s: %s', txt, self._ip)
logger.debug('_mac %s: %s', txt, self._mac)
logger.debug('_vmid %s: %s', txt, self._vmid)
logger.debug('Queue at %s: %s', txt, [LiveDeployment.__op2str(op) for op in self._queue])

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -30,17 +30,11 @@
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from django.utils.translation import ugettext as _
from uds.core.services import Publication
from uds.core.util.state import State
from datetime import datetime
import six
import logging
__updated__ = '2019-02-07'
from uds.core.services import Publication
from uds.core.util.state import State
logger = logging.getLogger(__name__)
@ -49,13 +43,13 @@ class LivePublication(Publication):
"""
This class provides the publication of a oVirtLinkedService
"""
_name = ''
_reason = ''
_templateId = ''
_state = 'r'
_destroyAfter = 'n'
_name: str = ''
_reason: str = ''
_templateId: str = ''
_state: str = 'r'
_destroyAfter: str = 'n'
suggestedTime = 20 # : Suggested recheck time if publication is unfinished in seconds
suggestedTime: int = 20 # : Suggested recheck time if publication is unfinished in seconds
def initialize(self):
"""
@ -73,13 +67,13 @@ class LivePublication(Publication):
self._state = 'r'
self._destroyAfter = 'n'
def marshal(self):
def marshal(self) -> bytes:
"""
returns data from an instance of Sample Publication serialized
"""
return '\t'.join(['v1', self._name, self._reason, self._templateId, self._state, self._destroyAfter]).encode('utf8')
def unmarshal(self, data):
def unmarshal(self, data: bytes) -> None:
"""
deserializes the data and loads it inside instance.
"""
@ -97,7 +91,7 @@ class LivePublication(Publication):
try:
res = self.service().makeTemplate(self._name)
logger.debug('Result: {}'.format(res))
logger.debug('Publication result: %s', res)
self._templateId = res['id']
self._state = res['status']
except Exception as e:
@ -128,7 +122,6 @@ class LivePublication(Publication):
"""
In our case, finish does nothing
"""
pass
def reasonOfError(self):
"""
@ -153,7 +146,7 @@ class LivePublication(Publication):
"""
# We do not do anything else to destroy this instance of publication
if self._state == 'error':
return # Nothing to cancel
return State.ERROR # Nothing to cancel
if self._state == 'creating':
self._destroyAfter = 'y'
@ -163,7 +156,7 @@ class LivePublication(Publication):
self.service().removeTemplate(self._templateId)
except Exception as e:
self._state = 'error'
self._reason = six.text_type(e)
self._reason = str(e)
return State.ERROR
return State.FINISHED

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -30,22 +30,25 @@
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from django.utils.translation import ugettext_noop as _, ugettext
import logging
import typing
from django.utils.translation import ugettext_noop as _
from uds.core.transports import protocols
from uds.core.services import Service, types as serviceTypes
from uds.core.ui import gui
from .LivePublication import LivePublication
from .LiveDeployment import LiveDeployment
from . import helpers
from uds.core.ui import gui
import six
import logging
__updated__ = '2018-10-22'
logger = logging.getLogger(__name__)
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from . import openStack
class LiveService(Service):
"""
@ -98,21 +101,29 @@ class LiveService(Service):
# Now the form part
region = gui.ChoiceField(label=_('Region'), order=1, tooltip=_('Service region'), required=True, rdonly=True)
project = gui.ChoiceField(label=_('Project'), order=2,
project = gui.ChoiceField(
label=_('Project'),
order=2,
fills={
'callbackName' : 'osFillResources',
'function' : helpers.getResources,
'parameters' : ['ov', 'ev', 'project', 'region', 'legacy']
},
tooltip=_('Project for this service'), required=True, rdonly=True
tooltip=_('Project for this service'),
required=True,
rdonly=True
)
availabilityZone = gui.ChoiceField(label=_('Availability Zones'), order=3,
availabilityZone = gui.ChoiceField(
label=_('Availability Zones'),
order=3,
fills={
'callbackName' : 'osFillVolumees',
'function' : helpers.getVolumes,
'parameters' : ['ov', 'ev', 'project', 'region', 'availabilityZone', 'legacy']
},
tooltip=_('Service availability zones'), required=True, rdonly=True
tooltip=_('Service availability zones'),
required=True,
rdonly=True
)
volume = gui.ChoiceField(label=_('Volume'), order=4, tooltip=_('Base volume for service (restricted by availability zone)'), required=True, tab=_('Machine'))
# volumeType = gui.ChoiceField(label=_('Volume Type'), order=5, tooltip=_('Volume type for service'), required=True)
@ -144,6 +155,8 @@ class LiveService(Service):
ev = gui.HiddenField(value=None)
legacy = gui.HiddenField(value=None) # We need to keep the env so we can instantiate the Provider
_api: typing.Optional['openStack.Client']
def initialize(self, values):
"""
We check here form values to see if they are valid.
@ -151,7 +164,7 @@ class LiveService(Service):
Note that we check them through FROM variables, that already has been
initialized by __init__ method of base class, before invoking this.
"""
if values is not None:
if values:
length = int(self.lenName.value)
if len(self.baseName.value) + length > 15:
raise Service.ValidationException(_('The length of basename plus length must not be greater than 15'))
@ -182,30 +195,30 @@ class LiveService(Service):
self.legacy.setDefValue(self.parent().legacy and 'true' or 'false')
@property
def api(self):
if self._api is None:
def api(self) -> 'openStack.Client':
if not self._api:
self._api = self.parent().api(projectId=self.project.value, region=self.region.value)
return self._api
def sanitizeVmName(self, name):
def sanitizeVmName(self, name: str) -> str:
return self.parent().sanitizeVmName(name)
def makeTemplate(self, templateName, description=None):
def makeTemplate(self, templateName: str, description: typing.Optional[str] = None):
# First, ensures that volume has not any running instances
# if self.api.getVolume(self.volume.value)['status'] != 'available':
# raise Exception('The Volume is in use right now. Ensure that there is no machine running before publishing')
description = 'UDS Template snapshot' if description is None else description
description = description or 'UDS Template snapshot'
return self.api.createVolumeSnapshot(self.volume.value, templateName, description)
def getTemplate(self, snapshotId):
def getTemplate(self, snapshotId: str):
"""
Checks current state of a template (an snapshot)
"""
return self.api.getSnapshot(snapshotId)
def deployFromTemplate(self, name, snapshotId):
def deployFromTemplate(self, name: str, snapshotId: str) -> str:
"""
Deploys a virtual machine on selected cluster from selected template
@ -217,22 +230,24 @@ class LiveService(Service):
Returns:
Id of the machine being created form template
"""
logger.debug('Deploying from template {0} machine {1}'.format(snapshotId, name))
logger.debug('Deploying from template %s machine %s', snapshotId, name)
# self.datastoreHasSpace()
return self.api.createServerFromSnapshot(snapshotId=snapshotId,
return self.api.createServerFromSnapshot(
snapshotId=snapshotId,
name=name,
availabilityZone=self.availabilityZone.value,
flavorId=self.flavor.value,
networkId=self.network.value,
securityGroupsIdsList=self.securityGroups.value)['id']
securityGroupsIdsList=self.securityGroups.value
)['id']
def removeTemplate(self, templateId):
def removeTemplate(self, templateId: str) -> None:
"""
invokes removeTemplate from parent provider
"""
self.api.deleteSnapshot(templateId)
def getMachineState(self, machineId):
def getMachineState(self, machineId: str) -> str:
"""
Invokes getServer from openstack client
@ -259,9 +274,12 @@ class LiveService(Service):
SUSPENDED. The server is suspended, either by request or necessity. This status appears for only the XenServer/XCP, KVM, and ESXi hypervisors. Administrative users can suspend an instance if it is infrequently used or to perform system maintenance. When you suspend an instance, its VM state is stored on disk, all memory is written to disk, and the virtual machine is stopped. Suspending an instance is similar to placing a device in hibernation; memory and vCPUs become available to create other instances.
VERIFY_RESIZE. System is awaiting confirmation that the server is operational after a move or resize.
"""
return self.api.getServer(machineId)['status']
server = self.api.getServer(machineId)
if server['status'] in ('ERROR', 'DELETED'):
logger.warning('Got server status %s for %s: %s', server['status'], machineId, server.get('fault'))
return server['status']
def startMachine(self, machineId):
def startMachine(self, machineId: str) -> None:
"""
Tries to start a machine. No check is done, it is simply requested to OpenStack.
@ -274,7 +292,7 @@ class LiveService(Service):
"""
self.api.startServer(machineId)
def stopMachine(self, machineId):
def stopMachine(self, machineId: str) -> None:
"""
Tries to stop a machine. No check is done, it is simply requested to OpenStack
@ -285,7 +303,7 @@ class LiveService(Service):
"""
self.api.stopServer(machineId)
def resetMachine(self, machineId):
def resetMachine(self, machineId: str) -> None:
"""
Tries to stop a machine. No check is done, it is simply requested to OpenStack
@ -296,7 +314,7 @@ class LiveService(Service):
"""
self.api.resetServer(machineId)
def suspendMachine(self, machineId):
def suspendMachine(self, machineId: str) -> None:
"""
Tries to suspend a machine. No check is done, it is simply requested to OpenStack
@ -307,7 +325,7 @@ class LiveService(Service):
"""
self.api.suspendServer(machineId)
def resumeMachine(self, machineId):
def resumeMachine(self, machineId: str) -> None:
"""
Tries to start a machine. No check is done, it is simply requested to OpenStack
@ -318,7 +336,7 @@ class LiveService(Service):
"""
self.api.resumeServer(machineId)
def removeMachine(self, machineId):
def removeMachine(self, machineId: str) -> None:
"""
Tries to delete a machine. No check is done, it is simply requested to OpenStack
@ -329,21 +347,22 @@ class LiveService(Service):
"""
self.api.deleteServer(machineId)
def getNetInfo(self, machineId):
def getNetInfo(self, machineId: str) -> typing.Tuple[str, str]:
"""
Gets the mac address of first nic of the machine
"""
net = self.api.getServer(machineId)['addresses']
vals = six.next(six.itervalues(net))[0] # Returns "any" mac address of any interface. We just need only one interface info
vals = next(iter(net.values()))[0] # Returns "any" mac address of any interface. We just need only one interface info
# vals = six.next(six.itervalues(net))[0]
return vals['OS-EXT-IPS-MAC:mac_addr'].upper(), vals['addr']
def getBaseName(self):
def getBaseName(self) -> str:
"""
Returns the base name
"""
return self.baseName.value
def getLenName(self):
def getLenName(self) -> int:
"""
Returns the length of numbers part
"""

View File

@ -34,7 +34,6 @@ import typing
import logging
from django.utils.translation import ugettext_noop as _
from uds.core import Module
from uds.core.services import ServiceProvider
from uds.core.ui import gui
from uds.core.util import validators
@ -42,6 +41,9 @@ from uds.core.util import validators
from .LiveService import LiveService
from . import openStack
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core import Module
logger = logging.getLogger(__name__)
@ -114,7 +116,7 @@ class Provider(ServiceProvider):
# Own variables
_api: typing.Optional[openStack.Client] = None
def initialize(self, values: Module.ValuesType = None):
def initialize(self, values: 'Module.ValuesType' = None):
"""
We will use the "autosave" feature for form fields
"""
@ -123,11 +125,14 @@ class Provider(ServiceProvider):
if values is not None:
self.timeout.value = validators.validateTimeout(self.timeout.value, returnAsInteger=False)
def api(self, projectId=None, region=None):
def api(self, projectId=None, region=None) -> openStack.Client:
if self._api is None:
self._api = openStack.Client(
self.endpoint.value, -1,
self.domain.value, self.username.value, self.password.value,
self.endpoint.value,
-1,
self.domain.value,
self.username.value,
self.password.value,
legacyVersion=False,
useSSL=None,
projectId=projectId,
@ -136,7 +141,7 @@ class Provider(ServiceProvider):
)
return self._api
def sanitizeVmName(self, name: str):
def sanitizeVmName(self, name: str) -> str:
return openStack.sanitizeName(name)
def testConnection(self):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -32,7 +32,8 @@ Created on Jun 22, 2012
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
import logging
import typing
from django.utils.translation import ugettext_noop as _
from uds.core.services import ServiceProvider
@ -42,9 +43,11 @@ from uds.core.util import validators
from .LiveService import LiveService
from . import openStack
import logging
__updated__ = '2018-10-22'
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core import Module
logger = logging.getLogger(__name__)
@ -117,9 +120,9 @@ class ProviderLegacy(ServiceProvider):
legacy = True
# Own variables
_api = None
_api: typing.Optional[openStack.Client] = None
def initialize(self, values=None):
def initialize(self, values: 'Module.ValuesType' = None):
'''
We will use the "autosave" feature for form fields
'''
@ -128,14 +131,19 @@ class ProviderLegacy(ServiceProvider):
if values is not None:
self.timeout.value = validators.validateTimeout(self.timeout.value, returnAsInteger=False)
def api(self, projectId=None, region=None):
return openStack.Client(self.host.value, self.port.value,
self.domain.value, self.username.value, self.password.value,
def api(self, projectId=None, region=None) -> openStack.Client:
return openStack.Client(
self.host.value,
self.port.value,
self.domain.value,
self.username.value,
self.password.value,
legacyVersion=True,
useSSL=self.ssl.isTrue(),
projectId=projectId,
region=region,
access=self.access.value)
access=self.access.value
)
def sanitizeVmName(self, name):
return openStack.sanitizeName(name)

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2016-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -29,4 +29,3 @@
from .ProviderLegacy import ProviderLegacy
from .Provider import Provider

View File

@ -1,21 +1,43 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2016-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.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import logging
import typing
from django.utils.translation import ugettext as _
import logging
from uds.core.ui import gui
logger = logging.getLogger(__name__)
def getResources(parameters):
def getResources(parameters: typing.Dict[str, str]) -> typing.List[typing.Dict[str, typing.Any]]:
'''
This helper is designed as a callback for Project Selector
'''
@ -24,7 +46,7 @@ def getResources(parameters):
else:
from .Provider import Provider
from uds.core.environment import Environment
logger.debug('Parameters received by getResources Helper: {0}'.format(parameters))
logger.debug('Parameters received by getResources Helper: %s', parameters)
env = Environment(parameters['ev'])
provider = Provider(env)
provider.unserialize(parameters['ov'])
@ -44,10 +66,10 @@ def getResources(parameters):
{'name': 'securityGroups', 'values': securityGroups},
{'name': 'volumeType', 'values': volumeTypes},
]
logger.debug('Return data: {}'.format(data))
logger.debug('Return data: %s', data)
return data
def getVolumes(parameters):
def getVolumes(parameters: typing.Dict[str, str]) -> typing.List[typing.Dict[str, typing.Any]]:
'''
This helper is designed as a callback for Zone Selector
'''
@ -56,17 +78,19 @@ def getVolumes(parameters):
else:
from .Provider import Provider
from uds.core.environment import Environment
logger.debug('Parameters received by getVolumes Helper: {0}'.format(parameters))
logger.debug('Parameters received by getVolumes Helper: %s', parameters)
env = Environment(parameters['ev'])
provider = Provider(env)
provider.unserialize(parameters['ov'])
api = provider.api(parameters['project'], parameters['region'])
volumes = [gui.choiceItem(v['id'], v['name']) for v in api.listVolumes() if v['name'] != '' and v['availability_zone'] == parameters['availabilityZone']]
# Source volumes are all available for us
# volumes = [gui.choiceItem(v['id'], v['name']) for v in api.listVolumes() if v['name'] != '' and v['availability_zone'] == parameters['availabilityZone']]
volumes = [gui.choiceItem(v['id'], v['name']) for v in api.listVolumes() if v['name'] != '']
data = [
{'name': 'volume', 'values': volumes},
]
logger.debug('Return data: {}'.format(data))
logger.debug('Return data: %s', data)
return data

View File

@ -32,12 +32,18 @@
"""
import logging
import json
import typing
import requests
# import dateutil.parser
from django.utils.translation import ugettext as _
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
# Required: Authentication v3
@ -51,11 +57,12 @@ VERIFY_SSL = False
# Helpers
def ensureResponseIsValid(response, errMsg=None):
def ensureResponseIsValid(response: requests.Response, errMsg: typing.Optional[str] = None) -> None:
if response.ok is False:
try:
_, err = response.json().popitem() # Extract any key, in case of error is expected to have only one top key so this will work
errMsg += ': {message}'.format(**err)
msg = ': {message}'.format(**err)
errMsg = errMsg + msg if errMsg else msg
except Exception:
pass # If error geting error message, simply ignore it (will be loged on service log anyway)
if errMsg is None:
@ -64,7 +71,14 @@ def ensureResponseIsValid(response, errMsg=None):
raise Exception(errMsg)
def getRecurringUrlJson(url, headers, key, params=None, errMsg=None, timeout=10):
def getRecurringUrlJson(
url: str,
headers: typing.Dict[str, str],
key: str,
params: typing.Dict[str, str] = None,
errMsg: str = None,
timeout: int = 10
) -> typing.Iterable[typing.Any]:
counter = 0
while True:
counter += 1
@ -86,8 +100,7 @@ def getRecurringUrlJson(url, headers, key, params=None, errMsg=None, timeout=10)
# Decorators
def authRequired(func):
def ensurer(obj, *args, **kwargs):
def ensurer(obj: 'Client', *args, **kwargs):
obj.ensureAuthenticated()
try:
return func(obj, *args, **kwargs)
@ -109,13 +122,41 @@ def authProjectRequired(func):
return ensurer
class Client(object):
class Client: # pylint: disable=too-many-public-methods
PUBLIC = 'public'
PRIVATE = 'private'
INTERNAL = 'url'
_authenticated: bool
_authenticatedProjectId: typing.Optional[str]
_authUrl: str
_tokenId: typing.Optional[str]
_catalog: typing.Optional[typing.List[typing.Dict[str, typing.Any]]]
_isLegacy: bool
_access: typing.Optional[str]
_domain: str
_username: str
_password: str
_userId: typing.Optional[str]
_projectId: typing.Optional[str]
_project: typing.Optional[str]
_region: typing.Optional[str]
_timeout: int
# Legacyversion is True for versions <= Ocata
def __init__(self, host, port, domain, username, password, legacyVersion=True, useSSL=False, projectId=None, region=None, access=None):
def __init__(
self,
host: str,
port: typing.Union[str, int],
domain: str,
username: str,
password: str,
legacyVersion: bool = True,
useSSL: bool = False,
projectId: typing.Optional[str] = None,
region: typing.Optional[str] = None,
access: typing.Optional[str] = None
):
self._authenticated = False
self._authenticatedProjectId = None
self._tokenId = None
@ -137,21 +178,22 @@ class Client(object):
if self._authUrl[-1] != '/':
self._authUrl += '/'
def _getEndpointFor(self, type_): # If no region is indicatad, first endpoint is returned
def _getEndpointFor(self, type_: str) -> str: # If no region is indicatad, first endpoint is returned
for i in self._catalog:
if i['type'] == type_:
for j in i['endpoints']:
if j['interface'] == self._access and (self._region is None or j['region'] == self._region):
return j['url']
raise Exception('No endpoint url found')
def _requestHeaders(self):
def _requestHeaders(self) -> typing.Dict[str, str]:
headers = {'content-type': 'application/json'}
if self._tokenId is not None:
if self._tokenId:
headers['X-Auth-Token'] = self._tokenId
return headers
def authPassword(self):
def authPassword(self) -> None:
# logger.debug('Authenticating...')
data = {
'auth': {
@ -189,11 +231,13 @@ class Client(object):
# logger.debug('Request data: {}'.format(data))
r = requests.post(self._authUrl + 'v3/auth/tokens',
r = requests.post(
self._authUrl + 'v3/auth/tokens',
data=json.dumps(data),
headers={'content-type': 'application/json'},
verify=VERIFY_SSL,
timeout=self._timeout)
timeout=self._timeout
)
ensureResponseIsValid(r, 'Invalid Credentials')
@ -211,12 +255,12 @@ class Client(object):
if self._projectId is not None:
self._catalog = token['catalog']
def ensureAuthenticated(self):
def ensureAuthenticated(self) -> None:
if self._authenticated is False or self._projectId != self._authenticatedProjectId:
self.authPassword()
@authRequired
def listProjects(self):
def listProjects(self) -> typing.Iterable[typing.Any]:
return getRecurringUrlJson(
self._authUrl + 'v3/users/{user_id}/projects'.format(user_id=self._userId),
headers=self._requestHeaders(),
@ -226,7 +270,7 @@ class Client(object):
)
@authRequired
def listRegions(self):
def listRegions(self) -> typing.Iterable[typing.Any]:
return getRecurringUrlJson(
self._authUrl + 'v3/regions/',
headers=self._requestHeaders(),
@ -236,7 +280,7 @@ class Client(object):
)
@authProjectRequired
def listServers(self, detail=False, params=None):
def listServers(self, detail: bool = False, params: bool = None) -> typing.Iterable[typing.Any]:
path = '/servers/' + 'detail' if detail is True else ''
return getRecurringUrlJson(
self._getEndpointFor('compute') + path,
@ -248,7 +292,7 @@ class Client(object):
)
@authProjectRequired
def listImages(self):
def listImages(self) -> typing.Iterable[typing.Any]:
return getRecurringUrlJson(
self._getEndpointFor('image') + '/v2/images?status=active',
headers=self._requestHeaders(),
@ -258,7 +302,7 @@ class Client(object):
)
@authProjectRequired
def listVolumeTypes(self):
def listVolumeTypes(self) -> typing.Iterable[typing.Any]:
return getRecurringUrlJson(
self._getEndpointFor('volumev2') + '/types',
headers=self._requestHeaders(),
@ -268,7 +312,7 @@ class Client(object):
)
@authProjectRequired
def listVolumes(self):
def listVolumes(self) -> typing.Iterable[typing.Any]:
# self._getEndpointFor('volumev2') + '/volumes'
return getRecurringUrlJson(
self._getEndpointFor('volumev2') + '/volumes/detail',
@ -279,7 +323,7 @@ class Client(object):
)
@authProjectRequired
def listVolumeSnapshots(self, volumeId=None):
def listVolumeSnapshots(self, volumeId: typing.Optional[typing.Dict[str, typing.Any]] = None) -> typing.Iterable[typing.Any]:
for s in getRecurringUrlJson(
self._getEndpointFor('volumev2') + '/snapshots',
headers=self._requestHeaders(),
@ -291,7 +335,7 @@ class Client(object):
yield s
@authProjectRequired
def listAvailabilityZones(self):
def listAvailabilityZones(self) -> typing.Iterable[typing.Any]:
for az in getRecurringUrlJson(
self._getEndpointFor('compute') + '/os-availability-zone',
headers=self._requestHeaders(),
@ -303,7 +347,7 @@ class Client(object):
yield az['zoneName']
@authProjectRequired
def listFlavors(self):
def listFlavors(self) -> typing.Iterable[typing.Any]:
return getRecurringUrlJson(
self._getEndpointFor('compute') + '/flavors',
headers=self._requestHeaders(),
@ -313,7 +357,7 @@ class Client(object):
)
@authProjectRequired
def listNetworks(self):
def listNetworks(self) -> typing.Iterable[typing.Any]:
return getRecurringUrlJson(
self._getEndpointFor('network') + '/v2.0/networks',
headers=self._requestHeaders(),
@ -323,7 +367,7 @@ class Client(object):
)
@authProjectRequired
def listPorts(self, networkId=None, ownerId=None):
def listPorts(self, networkId: typing.Optional[str] = None, ownerId: typing.Optional[str] = None) -> typing.Iterable[typing.Any]:
params = {}
if networkId is not None:
params['network_id'] = networkId
@ -340,7 +384,7 @@ class Client(object):
)
@authProjectRequired
def listSecurityGroups(self):
def listSecurityGroups(self) -> typing.Iterable[typing.Any]:
return getRecurringUrlJson(
self._getEndpointFor('compute') + '/os-security-groups',
headers=self._requestHeaders(),
@ -350,19 +394,18 @@ class Client(object):
)
@authProjectRequired
def getServer(self, serverId):
def getServer(self, serverId: str) -> typing.Dict[str, typing.Any]:
r = requests.get(
self._getEndpointFor('compute') + '/servers/{server_id}'.format(server_id=serverId),
headers=self._requestHeaders(),
verify=VERIFY_SSL,
timeout=self._timeout
)
ensureResponseIsValid(r, 'Get Server information')
return r.json()['server']
@authProjectRequired
def getVolume(self, volumeId):
def getVolume(self, volumeId: str) -> typing.Dict[str, typing.Any]:
r = requests.get(
self._getEndpointFor('volumev2') + '/volumes/{volume_id}'.format(volume_id=volumeId),
headers=self._requestHeaders(),
@ -372,12 +415,10 @@ class Client(object):
ensureResponseIsValid(r, 'Get Volume information')
v = r.json()['volume']
return v
return r.json()['volume']
@authProjectRequired
def getSnapshot(self, snapshotId):
def getSnapshot(self, snapshotId: str) -> typing.Dict[str, typing.Any]:
"""
States are:
creating, available, deleting, error, error_deleting
@ -391,19 +432,17 @@ class Client(object):
ensureResponseIsValid(r, 'Get Snaphost information')
v = r.json()['snapshot']
return v
return r.json()['snapshot']
@authProjectRequired
def updateSnapshot(self, snapshotId, name=None, description=None):
def updateSnapshot(self, snapshotId: str, name: typing.Optional[str] = None, description: typing.Optional[str] = None) -> typing.Dict[str, typing.Any]:
data = {
'snapshot': {}
}
if name is not None:
if name:
data['snapshot']['name'] = name
if description is not None:
if description:
data['snapshot']['description'] = description
r = requests.put(
@ -416,13 +455,11 @@ class Client(object):
ensureResponseIsValid(r, 'Update Snaphost information')
v = r.json()['snapshot']
return v
return r.json()['snapshot']
@authProjectRequired
def createVolumeSnapshot(self, volumeId, name, description=None):
description = 'UDS Snapshot' if description is None else description
def createVolumeSnapshot(self, volumeId: str, name: str, description: typing.Optional[str] = None) -> typing.Dict[str, typing.Any]:
description = description or 'UDS Snapshot'
data = {
'snapshot': {
'name': name,
@ -447,8 +484,8 @@ class Client(object):
return r.json()['snapshot']
@authProjectRequired
def createVolumeFromSnapshot(self, snapshotId, name, description=None):
description = 'UDS Volume' if description is None else description
def createVolumeFromSnapshot(self, snapshotId: str, name: str, description: typing.Optional[str] = None) -> typing.Dict[str, typing.Any]:
description = description or 'UDS Volume'
data = {
'volume': {
'name': name,
@ -468,10 +505,19 @@ class Client(object):
ensureResponseIsValid(r, 'Cannot create volume from snapshot.')
return r.json()
return r.json()['volume']
@authProjectRequired
def createServerFromSnapshot(self, snapshotId, name, availabilityZone, flavorId, networkId, securityGroupsIdsList, count=1):
def createServerFromSnapshot(
self,
snapshotId: str,
name: str,
availabilityZone: str,
flavorId: str,
networkId: str,
securityGroupsIdsList: typing.Iterable[str],
count: int = 1
) -> typing.Dict[str, typing.Any]:
data = {
'server': {
'name': name,
@ -510,7 +556,7 @@ class Client(object):
return r.json()['server']
@authProjectRequired
def deleteServer(self, serverId):
def deleteServer(self, serverId: str) -> None:
r = requests.post(
self._getEndpointFor('compute') + '/servers/{server_id}/action'.format(server_id=serverId),
data='{"forceDelete": null}',
@ -524,7 +570,7 @@ class Client(object):
# This does not returns anything
@authProjectRequired
def deleteSnapshot(self, snapshotId):
def deleteSnapshot(self, snapshotId: str) -> None:
r = requests.delete(
self._getEndpointFor('volumev2') + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshotId),
headers=self._requestHeaders(),
@ -537,7 +583,7 @@ class Client(object):
# Does not returns a message body
@authProjectRequired
def startServer(self, serverId):
def startServer(self, serverId: str) -> None:
r = requests.post(
self._getEndpointFor('compute') + '/servers/{server_id}/action'.format(server_id=serverId),
data='{"os-start": null}',
@ -551,7 +597,7 @@ class Client(object):
# This does not returns anything
@authProjectRequired
def stopServer(self, serverId):
def stopServer(self, serverId: str) -> None:
r = requests.post(
self._getEndpointFor('compute') + '/servers/{server_id}/action'.format(server_id=serverId),
data='{"os-stop": null}',
@ -563,7 +609,7 @@ class Client(object):
ensureResponseIsValid(r, 'Stoping server')
@authProjectRequired
def suspendServer(self, serverId):
def suspendServer(self, serverId: str) -> None:
r = requests.post(
self._getEndpointFor('compute') + '/servers/{server_id}/action'.format(server_id=serverId),
data='{"suspend": null}',
@ -575,7 +621,7 @@ class Client(object):
ensureResponseIsValid(r, 'Suspending server')
@authProjectRequired
def resumeServer(self, serverId):
def resumeServer(self, serverId: str) -> None:
r = requests.post(
self._getEndpointFor('compute') + '/servers/{server_id}/action'.format(server_id=serverId),
data='{"resume": null}',
@ -587,7 +633,7 @@ class Client(object):
ensureResponseIsValid(r, 'Resuming server')
@authProjectRequired
def resetServer(self, serverId):
def resetServer(self, serverId: str) -> None:
r = requests.post( # pylint: disable=unused-variable
self._getEndpointFor('compute') + '/servers/{server_id}/action'.format(server_id=serverId),
data='{"reboot":{"type":"HARD"}}',
@ -599,7 +645,7 @@ class Client(object):
# Ignore response for this...
# ensureResponseIsValid(r, 'Reseting server')
def testConnection(self):
def testConnection(self) -> bool:
# First, ensure requested api is supported
# We need api version 3.2 or greater
try: