diff --git a/server/src/uds/core/osmanagers/BaseOsManager.py b/server/src/uds/core/osmanagers/BaseOsManager.py index edd966a63..ff2645400 100644 --- a/server/src/uds/core/osmanagers/BaseOsManager.py +++ b/server/src/uds/core/osmanagers/BaseOsManager.py @@ -96,10 +96,10 @@ class OSManager(Module): pass # These methods must be overriden - def process(self, service, message, data, options=None): + def process(self, userService, message, data, options=None): ''' This method must be overriden so your so manager can manage requests and responses from agent. - @param service: Service that sends the request (virtual machine or whatever) + @param userService: Service that sends the request (virtual machine or whatever) @param message: message to process (os manager dependent) @param data: Data for this message ''' diff --git a/server/src/uds/core/services/BaseService.py b/server/src/uds/core/services/BaseService.py index 3df6d80af..0fdb94897 100644 --- a/server/src/uds/core/services/BaseService.py +++ b/server/src/uds/core/services/BaseService.py @@ -37,7 +37,7 @@ from uds.core import Module from uds.core.transports import protocols from . import types -__updated__ = '2015-05-28' +__updated__ = '2016-02-08' class Service(Module): @@ -171,6 +171,10 @@ class Service(Module): # : For example, VDI, VAPP, ... servicesTypeProvided = types.ALL + # : If the service can provide any other option on release appart of "delete" & "keep assigned" + # : Defaults to None (no any other options are provided) + actionsOnRelease = None + def __init__(self, environment, parent, values=None): ''' Do not forget to invoke this in your derived class using "super(self.__class__, self).__init__(environment, parent, values)". diff --git a/server/src/uds/models/UserService.py b/server/src/uds/models/UserService.py index c2f4b09cb..1099a8b33 100644 --- a/server/src/uds/models/UserService.py +++ b/server/src/uds/models/UserService.py @@ -55,7 +55,7 @@ from uds.models.Util import getSqlDatetime import logging -__updated__ = '2015-11-06' +__updated__ = '2016-02-08' logger = logging.getLogger(__name__) @@ -383,6 +383,12 @@ class UserService(UUIDModel): self.setState(State.REMOVABLE) self.save() + def release(self): + ''' + A much more convenient method that "remove" + ''' + self.remove() + def cancel(self): ''' Asks the UserServiceManager to cancel the current operation of this user deployed service. diff --git a/server/src/uds/osmanagers/LinuxOsManager/LinuxOsManager.py b/server/src/uds/osmanagers/LinuxOsManager/LinuxOsManager.py index e65b9313f..630d0ae49 100644 --- a/server/src/uds/osmanagers/LinuxOsManager/LinuxOsManager.py +++ b/server/src/uds/osmanagers/LinuxOsManager/LinuxOsManager.py @@ -132,7 +132,7 @@ class LinuxOsManager(osmanagers.OSManager): except Exception: log.doLog(service, log.ERROR, "do not understand {0}".format(data), origin) - def process(self, service, msg, data, options): + def process(self, userService, msg, data, options): ''' We understand this messages: * msg = info, data = None. Get information about name of machine (or domain, in derived WinDomainOsManager class), old method @@ -141,47 +141,47 @@ class LinuxOsManager(osmanagers.OSManager): * msg = logoff, data = Username, Informs that the username has logged out of the machine * msg = ready, data = None, Informs machine ready to be used ''' - logger.info("Invoked LinuxOsManager for {0} with params: {1},{2}".format(service, msg, data)) - # We get from storage the name for this service. If no name, we try to assign a new one + logger.info("Invoked LinuxOsManager for {0} with params: {1},{2}".format(userService, msg, data)) + # We get from storage the name for this userService. If no name, we try to assign a new one ret = "ok" notifyReady = False doRemove = False - state = service.os_state + state = userService.os_state # Old "info" state, will be removed in a near future if msg == "info": - ret = self.infoVal(service) + ret = self.infoVal(userService) state = State.PREPARING elif msg == "information": - ret = self.infoValue(service) + ret = self.infoValue(userService) state = State.PREPARING elif msg == "log": - self.doLog(service, data, log.ACTOR) + self.doLog(userService, data, log.ACTOR) elif msg == "login": - self.loggedIn(service, data, False) + self.loggedIn(userService, data, False) elif msg == "logout": - self.loggedOut(service, data, False) + self.loggedOut(userService, data, False) if self._onLogout == 'remove': doRemove = True elif msg == "ip": - # This ocurss on main loop inside machine, so service is usable + # This ocurss on main loop inside machine, so userService is usable state = State.USABLE - self.notifyIp(service.unique_id, service, data) + self.notifyIp(userService.unique_id, userService, data) elif msg == "ready": state = State.USABLE notifyReady = True - self.notifyIp(service.unique_id, service, data) + self.notifyIp(userService.unique_id, userService, data) - service.setOsState(state) + userService.setOsState(state) # If notifyReady is not true, save state, let UserServiceManager do it for us else if doRemove is True: - service.remove() + userService.release() else: if notifyReady is False: - service.save() + userService.save() else: - UserServiceManager.manager().notifyReadyFromOsManager(service, '') + UserServiceManager.manager().notifyReadyFromOsManager(userService, '') logger.debug('Returning {0}'.format(ret)) return ret diff --git a/server/src/uds/osmanagers/WindowsOsManager/WindowsOsManager.py b/server/src/uds/osmanagers/WindowsOsManager/WindowsOsManager.py index e3a44c2eb..e1ab14fde 100644 --- a/server/src/uds/osmanagers/WindowsOsManager/WindowsOsManager.py +++ b/server/src/uds/osmanagers/WindowsOsManager/WindowsOsManager.py @@ -138,7 +138,7 @@ class WindowsOsManager(osmanagers.OSManager): logger.exception('WindowsOs Manager message log: ') log.doLog(service, log.ERROR, "do not understand {0}".format(data), origin) - def process(self, service, msg, data, options): + def process(self, userService, msg, data, options): ''' We understand this messages: * msg = info, data = None. Get information about name of machine (or domain, in derived WinDomainOsManager class) (old method) @@ -147,51 +147,51 @@ class WindowsOsManager(osmanagers.OSManager): * msg = logoff, data = Username, Informs that the username has logged out of the machine * msg = ready, data = None, Informs machine ready to be used ''' - logger.info("Invoked WindowsOsManager for {0} with params: {1},{2}".format(service, msg, data)) - # We get from storage the name for this service. If no name, we try to assign a new one + logger.info("Invoked WindowsOsManager for {0} with params: {1},{2}".format(userService, msg, data)) + # We get from storage the name for this userService. If no name, we try to assign a new one ret = "ok" notifyReady = False doRemove = False - state = service.os_state + state = userService.os_state if msg == "info": - ret = self.infoVal(service) + ret = self.infoVal(userService) state = State.PREPARING elif msg == "information": - ret = self.infoValue(service) + ret = self.infoValue(userService) state = State.PREPARING elif msg == "log": - self.doLog(service, data, log.ACTOR) + self.doLog(userService, data, log.ACTOR) elif msg == "logon" or msg == 'login': if '\\' not in data: - self.loggedIn(service, data, False) - service.setInUse(True) - # We get the service logged hostname & ip and returns this - ip, hostname = service.getConnectionSource() + self.loggedIn(userService, data, False) + userService.setInUse(True) + # We get the userService logged hostname & ip and returns this + ip, hostname = userService.getConnectionSource() ret = "{0}\t{1}".format(ip, hostname) elif msg == "logoff" or msg == 'logout': - self.loggedOut(service, data, False) + self.loggedOut(userService, data, False) if self._onLogout == 'remove': doRemove = True elif msg == "ip": - # This ocurss on main loop inside machine, so service is usable + # This ocurss on main loop inside machine, so userService is usable state = State.USABLE - self.notifyIp(service.unique_id, service, data) + self.notifyIp(userService.unique_id, userService, data) elif msg == "ready": state = State.USABLE notifyReady = True - self.notifyIp(service.unique_id, service, data) + self.notifyIp(userService.unique_id, userService, data) - service.setOsState(state) + userService.setOsState(state) # If notifyReady is not true, save state, let UserServiceManager do it for us else if doRemove is True: - service.remove() + userService.release() else: if notifyReady is False: - service.save() + userService.save() else: logger.debug('Notifying ready') - UserServiceManager.manager().notifyReadyFromOsManager(service, '') + UserServiceManager.manager().notifyReadyFromOsManager(userService, '') logger.debug('Returning {} to {} message'.format(ret, msg)) if options is not None and options.get('scramble', True) is False: return ret diff --git a/server/src/uds/services/OpenNebula/LiveDeployment.py b/server/src/uds/services/OpenNebula/LiveDeployment.py new file mode 100644 index 000000000..aee506744 --- /dev/null +++ b/server/src/uds/services/OpenNebula/LiveDeployment.py @@ -0,0 +1,612 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2012 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 +''' +from uds.core.services import UserDeployment +from uds.core.util.State import State +from uds.core.util import log + +import pickle +import logging + +__updated__ = '2016-01-28' + + +logger = logging.getLogger(__name__) + +opCreate, opStart, opStop, opSuspend, opRemove, opWait, opError, opFinish, opRetry, opChangeMac = range(10) + +NO_MORE_NAMES = 'NO-NAME-ERROR' + + +class LiveDeployment(UserDeployment): + ''' + This class generates the user consumable elements of the service tree. + + After creating at administration interface an Deployed Service, UDS will + create consumable services for users using UserDeployment class as + provider of this elements. + + The logic for managing ovirt deployments (user machines in this case) is here. + + ''' + + # : Recheck every six seconds by default (for task methods) + suggestedTime = 6 + + def initialize(self): + self._name = '' + self._ip = '' + self._mac = '' + self._vmid = '' + self._reason = '' + self._queue = [] + + # Serializable needed methods + def marshal(self): + ''' + Does nothing right here, we will use envoronment storage in this sample + ''' + return '\1'.join(['v1', self._name, self._ip, self._mac, self._vmid, self._reason, pickle.dumps(self._queue)]) + + def unmarshal(self, str_): + ''' + Does nothing here also, all data are keeped at environment storage + ''' + vals = str_.split('\1') + if vals[0] == 'v1': + self._name, self._ip, self._mac, self._vmid, self._reason, queue = vals[1:] + self._queue = pickle.loads(queue) + + def getName(self): + ''' + We override this to return a name to display. Default inplementation + (in base class), returns getUniqueIde() value + This name will help user to identify elements, and is only used + at administration interface. + + We will use here the environment name provided generator to generate + a name for this element. + + The namaGenerator need two params, the base name and a length for a + numeric incremental part for generating unique names. This are unique for + all UDS names generations, that is, UDS will not generate this name again + until this name is freed, or object is removed, what makes its environment + to also get removed, that makes all uniques ids (names and macs right now) + to also get released. + + Every time get method of a generator gets called, the generator creates + a new unique name, so we keep the first generated name cached and don't + generate more names. (Generator are simple utility classes) + ''' + if self._name == '': + try: + self._name = self.nameGenerator().get(self.service().getBaseName(), self.service().getLenName()) + except KeyError: + return NO_MORE_NAMES + return self._name + + def setIp(self, ip): + ''' + In our case, there is no OS manager associated with this, so this method + will never get called, but we put here as sample. + + Whenever an os manager actor notifies the broker the state of the service + (mainly machines), the implementation of that os manager can (an probably will) + need to notify the IP of the deployed service. Remember that UDS treats with + IP services, so will probable needed in every service that you will create. + :note: This IP is the IP of the "consumed service", so the transport can + access it. + ''' + logger.debug('Setting IP to {}'.format(ip)) + self._ip = ip + + def getUniqueId(self): + ''' + Return and unique identifier for this service. + In our case, we will generate a mac name, that can be also as sample + of 'mac' generator use, and probably will get used something like this + at some services. + + The get method of a mac generator takes one param, that is the mac range + to use to get an unused mac. + ''' + if self._mac == '': + self._mac = self.macGenerator().get(self.service().getMacRange()) + return self._mac + + def getIp(self): + ''' + We need to implement this method, so we can return the IP for transports + use. If no IP is known for this service, this must return None + + If our sample do not returns an IP, IP transport will never work with + this service. Remember in real cases to return a valid IP address if + the service is accesible and you alredy know that (for example, because + the IP has been assigend via setIp by an os manager) or because + you get it for some other method. + + Storage returns None if key is not stored. + + :note: Keeping the IP address is responsibility of the User Deployment. + Every time the core needs to provide the service to the user, or + show the IP to the administrator, this method will get called + + ''' + return self._ip + + def setReady(self): + ''' + The method is invoked whenever a machine is provided to an user, right + before presenting it (via transport rendering) to the user. + ''' + if self.cache().get('ready') == '1': + return State.FINISHED + + state = self.service().getMachineState(self._vmid) + + if state == 'unknown': + return self.__error('Machine is not available anymore') + + if state not in ('up', 'powering_up', 'restoring_state'): + self._queue = [opStart, opFinish] + return self.__executeQueue() + + self.cache().put('ready', '1') + return State.FINISHED + + 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)) + if self.__getCurrentOp() == opWait: + logger.debug('Machine is ready. Moving to level 2') + self.__popCurrentOp() # Remove current state + return self.__executeQueue() + # Do not need to go to level 2 (opWait is in fact "waiting for moving machine to cache level 2) + return State.FINISHED + + def deployForUser(self, user): + ''' + Deploys an service instance for an user. + ''' + logger.debug('Deploying for user') + self.__initQueueForDeploy(False) + return self.__executeQueue() + + def deployForCache(self, cacheLevel): + ''' + Deploys an service instance for cache + ''' + self.__initQueueForDeploy(cacheLevel == self.L2_CACHE) + return self.__executeQueue() + + def __initQueueForDeploy(self, forLevel2=False): + + if forLevel2 is False: + self._queue = [opCreate, opChangeMac, opStart, opFinish] + else: + self._queue = [opCreate, opChangeMac, opStart, opWait, opSuspend, opFinish] + + def __checkMachineState(self, chkState): + logger.debug('Checking that state of machine {} ({}) is {}'.format(self._vmid, self._name, chkState)) + state = 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) + if state == 'unknown' and chkState != 'unknown': + return self.__error('Machine not found') + + ret = State.RUNNING + if type(chkState) is list: + for cks in chkState: + if state == cks: + ret = State.FINISHED + break + else: + if state == chkState: + ret = State.FINISHED + + return ret + + def __getCurrentOp(self): + if len(self._queue) == 0: + return opFinish + + return self._queue[0] + + def __popCurrentOp(self): + if len(self._queue) == 0: + return opFinish + + res = self._queue.pop(0) + return res + + def __pushFrontOp(self, op): + self._queue.insert(0, op) + + def __pushBackOp(self, op): + self._queue.append(op) + + def __error(self, reason): + ''' + Internal method to set object as error state + + Returns: + State.ERROR, so we can do "return self.__error(reason)" + ''' + logger.debug('Setting error state, reason: {0}'.format(reason)) + self.doLog(log.ERROR, reason) + + if self._vmid != '': # Powers off + try: + state = self.service().getMachineState(self._vmid) + if state in ('up', 'suspended'): + self.service().stopMachine(self._vmid) + except: + logger.debug('Can\t set machine state to stopped') + + self._queue = [opError] + self._reason = str(reason) + return State.ERROR + + def __executeQueue(self): + self.__debug('executeQueue') + op = self.__getCurrentOp() + + if op == opError: + return State.ERROR + + if op == opFinish: + return State.FINISHED + + fncs = { + opCreate: self.__create, + opRetry: self.__retry, + opStart: self.__startMachine, + opStop: self.__stopMachine, + opSuspend: self.__suspendMachine, + opWait: self.__wait, + opRemove: self.__remove, + opChangeMac: self.__changeMac + } + + try: + execFnc = fncs.get(op, None) + + if execFnc is None: + return self.__error('Unknown operation found at execution queue ({0})'.format(op)) + + execFnc() + + return State.RUNNING + except Exception as e: + return self.__error(e) + + # Queue execution methods + def __retry(self): + ''' + Used to retry an operation + In fact, this will not be never invoked, unless we push it twice, because + checkState method will "pop" first item when a check operation returns State.FINISHED + + At executeQueue this return value will be ignored, and it will only be used at checkState + ''' + return State.FINISHED + + def __wait(self): + ''' + Executes opWait, it simply waits something "external" to end + ''' + return State.RUNNING + + def __create(self): + ''' + Deploys a machine from template for user/cache + ''' + templateId = self.publication().getTemplateId() + name = self.getName() + if name == NO_MORE_NAMES: + raise Exception('No more names available for this service. (Increase digits for this service to fix)') + + name = self.service().sanitizeVmName(name) # oVirt don't let us to create machines with more than 15 chars!!! + comments = 'UDS Linked clone' + + self._vmid = self.service().deployFromTemplate(name, comments, templateId) + if self._vmid is None: + raise Exception('Can\'t create machine') + + def __remove(self): + ''' + Removes a machine from system + ''' + state = self.service().getMachineState(self._vmid) + + if state == 'unknown': + raise Exception('Machine not found') + + if state != 'down': + self.__pushFrontOp(opStop) + self.__executeQueue() + else: + self.service().removeMachine(self._vmid) + + def __startMachine(self): + ''' + Powers on the machine + ''' + state = self.service().getMachineState(self._vmid) + + if state == 'unknown': + raise Exception('Machine not found') + + if state == 'up': # Already started, return + return + + if state != 'down' and state != 'suspended': + self.__pushFrontOp(opRetry) # Will call "check Retry", that will finish inmediatly and again call this one + else: + self.service().startMachine(self._vmid) + + def __stopMachine(self): + ''' + Powers off the machine + ''' + state = self.service().getMachineState(self._vmid) + + if state == 'unknown': + raise Exception('Machine not found') + + if state == 'down': # Already stoped, return + return + + if state != 'up' and state != 'suspended': + self.__pushFrontOp(opRetry) # Will call "check Retry", that will finish inmediatly and again call this one + else: + self.service().stopMachine(self._vmid) + + def __suspendMachine(self): + ''' + Suspends the machine + ''' + state = self.service().getMachineState(self._vmid) + + if state == 'unknown': + raise Exception('Machine not found') + + if state == 'suspended': # Already suspended, return + return + + if state != 'up': + self.__pushFrontOp(opRetry) # Remember here, the return State.FINISH will make this retry be "poped" right ar return + else: + self.service().suspendMachine(self._vmid) + + def __changeMac(self): + ''' + Changes the mac of the first nic + ''' + self.service().updateMachineMac(self._vmid, self.getUniqueId()) + + # Check methods + def __checkCreate(self): + ''' + Checks the state of a deploy for an user or cache + ''' + return self.__checkMachineState('down') + + def __checkStart(self): + ''' + Checks if machine has started + ''' + return self.__checkMachineState('up') + + def __checkStop(self): + ''' + Checks if machine has stoped + ''' + return self.__checkMachineState('down') + + def __checkSuspend(self): + ''' + Check if the machine has suspended + ''' + return self.__checkMachineState('suspended') + + def __checkRemoved(self): + ''' + Checks if a machine has been removed + ''' + return self.__checkMachineState('unknown') + + def __checkMac(self): + ''' + Checks if change mac operation has finished. + + Changing nic configuration es 1-step operation, so when we check it here, it is already done + ''' + return State.FINISHED + + def checkState(self): + ''' + Check what operation is going on, and acts acordly to it + ''' + self.__debug('checkState') + op = self.__getCurrentOp() + + if op == opError: + return State.ERROR + + if op == opFinish: + return State.FINISHED + + fncs = { + opCreate: self.__checkCreate, + opRetry: self.__retry, + opWait: self.__wait, + opStart: self.__checkStart, + opStop: self.__checkStop, + opSuspend: self.__checkSuspend, + opRemove: self.__checkRemoved, + opChangeMac: self.__checkMac + } + + try: + chkFnc = fncs.get(op, None) + + if chkFnc is None: + return self.__error('Unknown operation found at check queue ({0})'.format(op)) + + state = chkFnc() + if state == State.FINISHED: + self.__popCurrentOp() # Remove runing op + return self.__executeQueue() + + return state + except Exception as e: + return self.__error(e) + + def finish(self): + ''' + Invoked when the core notices that the deployment of a service has finished. + (No matter wether it is for cache or for an user) + ''' + self.__debug('finish') + pass + + def assignToUser(self, user): + ''' + This method is invoked whenever a cache item gets assigned to an user. + This gives the User Deployment an oportunity to do whatever actions + are required so the service puts at a correct state for using by a service. + ''' + pass + + def moveToCache(self, newLevel): + ''' + Moves machines between cache levels + ''' + if opRemove in self._queue: + return State.RUNNING + + if newLevel == self.L1_CACHE: + self._queue = [opStart, opFinish] + else: + self._queue = [opStart, opSuspend, opFinish] + + 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. + + Remember that the class is responsible of returning this whenever asked + for it, and it will be asked everytime it's needed to be shown to the + user (when the administation asks for it). + ''' + return self._reason + + def destroy(self): + ''' + Invoked for destroying a deployed service + ''' + self.__debug('destroy') + # If executing something, wait until finished to remove it + # We simply replace the execution queue + op = self.__getCurrentOp() + + if op == opError: + return self.__error('Machine is already in error state!') + + if op == opFinish or op == opWait: + self._queue = [opStop, opRemove, opFinish] + return self.__executeQueue() + + self._queue = [op, opStop, opRemove, opFinish] + # Do not execute anything.here, just continue normally + return State.RUNNING + + def cancel(self): + ''' + This is a task method. As that, the excepted return values are + State values RUNNING, FINISHED or ERROR. + + This can be invoked directly by an administration or by the clean up + of the deployed service (indirectly). + When administrator requests it, the cancel is "delayed" and not + invoked directly. + ''' + return self.destroy() + + @staticmethod + def __op2str(op): + return { + opCreate: 'create', + opStart: 'start', + opStop: 'stop', + opSuspend: 'suspend', + opRemove: 'remove', + opWait: 'wait', + opError: 'error', + opFinish: 'finish', + opRetry: 'retry', + opChangeMac: 'changing mac' + }.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])) diff --git a/server/src/uds/services/OpenNebula/LivePublication.py b/server/src/uds/services/OpenNebula/LivePublication.py new file mode 100644 index 000000000..fbd6e8856 --- /dev/null +++ b/server/src/uds/services/OpenNebula/LivePublication.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2012 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 +''' + +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 logging + + +__updated__ = '2016-02-08' + + +logger = logging.getLogger(__name__) + + +class LivePublication(Publication): + ''' + This class provides the publication of a oVirtLinkedService + ''' + + suggestedTime = 2 # : Suggested recheck time if publication is unfinished in seconds + + def initialize(self): + ''' + This method will be invoked by default __init__ of base class, so it gives + us the oportunity to initialize whataver we need here. + + In our case, we setup a few attributes.. + ''' + + # We do not check anything at marshal method, so we ensure that + # default values are correctly handled by marshal. + self._name = '' + self._reason = '' + self._templateId = '' + self._state = 'r' + + def marshal(self): + ''' + returns data from an instance of Sample Publication serialized + ''' + return '\t'.join(['v1', self._name, self._reason, self._templateId, self._state]) + + def unmarshal(self, data): + ''' + deserializes the data and loads it inside instance. + ''' + logger.debug('Data: {0}'.format(data)) + vals = data.split('\t') + if vals[0] == 'v1': + self._name, self._reason, self._templateId, self._state = vals[1:] + + def publish(self): + ''' + Realizes the publication of the service + ''' + self._name = self.service().sanitizeVmName('UDSP ' + self.dsName() + "-" + str(self.revision())) + self._reason = '' # No error, no reason for it + self._state = 'ok' + + try: + self._templateId = self.service().makeTemplate(self._name) + except Exception as e: + self._state = 'error' + self._reason = str(e) + return State.ERROR + + return State.RUNNING + + def checkState(self): + ''' + Checks state of publication creation + ''' + if self._state == 'error': + return State.ERROR + + if self._state == 'ok': + return State.FINISHED + + self._state = 'ok' + return State.FINISHED + + def finish(self): + ''' + In our case, finish does nothing + ''' + pass + + def reasonOfError(self): + ''' + If a publication produces an error, here we must notify the reason why + it happened. This will be called just after publish or checkState + if they return State.ERROR + + Returns an string, in our case, set at checkState + ''' + return self._reason + + def destroy(self): + ''' + This is called once a publication is no more needed. + + This method do whatever needed to clean up things, such as + removing created "external" data (environment gets cleaned by core), + etc.. + + The retunred value is the same as when publishing, State.RUNNING, + State.FINISHED or State.ERROR. + ''' + # We do not do anything else to destroy this instance of publication + try: + self.service().removeTemplate(self._templateId) + except Exception as e: + self._state = 'error' + self._reason = str(e) + return State.ERROR + + return State.FINISHED + + def cancel(self): + ''' + Do same thing as destroy + ''' + return self.destroy() + + # Here ends the publication needed methods. + # Methods provided below are specific for this publication + # and will be used by user deployments that uses this kind of publication + + def getTemplateId(self): + ''' + Returns the template id associated with the publication + ''' + return self._templateId diff --git a/server/src/uds/services/OpenNebula/LiveService.py b/server/src/uds/services/OpenNebula/LiveService.py new file mode 100644 index 000000000..7505dd78e --- /dev/null +++ b/server/src/uds/services/OpenNebula/LiveService.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2012 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 +''' +from django.utils.translation import ugettext_noop as _, ugettext +from uds.core.transports import protocols +from uds.core.services import Service, types as serviceTypes +from .LivePublication import LivePublication +from .LiveDeployment import LiveDeployment +from . import Helpers + +from uds.core.ui import gui + +import logging + +__updated__ = '2016-02-08' + +logger = logging.getLogger(__name__) + + +class LiveService(Service): + ''' + Opennebula Live Service + ''' + # : Name to show the administrator. This string will be translated BEFORE + # : sending it to administration interface, so don't forget to + # : mark it as _ (using ugettext_noop) + typeName = _('OpenNebula Live Images') + # : Type used internally to identify this provider + typeType = 'openNebulaLiveService' + # : Description shown at administration interface for this provider + typeDescription = _('OpenNebula live images bases service') + # : Icon file used as icon for this provider. This string will be translated + # : BEFORE sending it to administration interface, so don't forget to + # : mark it as _ (using ugettext_noop) + iconFile = 'provider.png' + + # Functional related data + + # : If the service provides more than 1 "deployed user" (-1 = no limit, + # : 0 = ???? (do not use it!!!), N = max number to deploy + maxDeployed = -1 + # : If we need to generate "cache" for this service, so users can access the + # : provided services faster. Is usesCache is True, you will need also + # : set publicationType, do take care about that! + usesCache = True + # : Tooltip shown to user when this item is pointed at admin interface, none + # : because we don't use it + cacheTooltip = _('Number of desired machines to keep running waiting for a user') + + # : If the service needs a s.o. manager (managers are related to agents + # : provided by services itselfs, i.e. virtual machines with actors) + needsManager = True + # : If true, the system can't do an automatic assignation of a deployed user + # : service from this service + mustAssignManually = False + + # : Types of publications (preparated data for deploys) + # : In our case, we do no need a publication, so this is None + publicationType = LivePublication + # : Types of deploys (services in cache and/or assigned to users) + deployedType = LiveDeployment + + allowedProtocols = protocols.GENERIC + (protocols.SPICE,) + servicesTypeProvided = (serviceTypes.VDI,) + + # Now the form part + template = gui.ChoiceField(label=_("Base Template"), order=1, tooltip=_('Service base template'), required=True) + datastore = gui.ChoiceField(label=_("Datastore"), order=2, tooltip=_('Service clones datastore'), required=True) + + baseName = gui.TextField( + label=_('Machine Names'), + rdonly=False, + order=6, + tooltip=('Base name for clones from this machine'), + required=True + ) + + lenName = gui.NumericField( + length=1, + label=_('Name Length'), + defvalue=5, + order=7, + tooltip=_('Size of numeric part for the names of these machines (between 3 and 6)'), + required=True + ) + + def initialize(self, values): + ''' + We check here form values to see if they are valid. + + Note that we check them throught FROM variables, that already has been + initialized by __init__ method of base class, before invoking this. + ''' + if values is not None: + 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')) + if self.baseName.value.isdigit(): + raise Service.ValidationException(_('The machine name can\'t be only numbers')) + + def initGui(self): + ''' + Loads required values inside + ''' + + templates = self.parent().getTemplates() + vals = [] + for t in templates: + vals.append(gui.choiceItem(t.id, t.name)) + + # This is not the same case, values is not the "value" of the field, but + # the list of values shown because this is a "ChoiceField" + self.template.setValues(vals) + + datastores = self.parent().getDatastores() + vals = [] + for d in datastores: + vals.append(gui.choiceItem(d.id, d.name)) + + self.datastore.setValues(vals) + + def sanitizeVmName(self, name): + return self.parent().sanitizeVmName(name) + + def makeTemplate(self, templateName): + return self.parent().makeTemplate(self.template.value, templateName, self.datastore.value) + + def getTemplateState(self, templateId): + ''' + Invokes getTemplateState from parent provider + + Args: + templateId: templateId to remove + + Returns nothing + + Raises an exception if operation fails. + ''' + return self.parent().getTemplateState(templateId) + + def deployFromTemplate(self, name, comments, templateId): + ''' + Deploys a virtual machine on selected cluster from selected template + + Args: + name: Name (sanitized) of the machine + comments: Comments for machine + templateId: Id of the template to deploy from + displayType: 'vnc' or 'spice'. Display to use ad oVirt admin interface + memoryMB: Memory requested for machine, in MB + guaranteedMB: Minimum memory guaranteed for this machine + + Returns: + Id of the machine being created form template + ''' + logger.debug('Deploying from template {0} machine {1}'.format(templateId, name)) + self.datastoreHasSpace() + return self.parent().deployFromTemplate(name, comments, templateId, self.cluster.value, + self.display.value, int(self.memory.value), int(self.memoryGuaranteed.value)) + + def removeTemplate(self, templateId): + ''' + invokes removeTemplate from parent provider + ''' + return self.parent().removeTemplate(templateId) + + def getMachineState(self, machineId): + ''' + Invokes getMachineState from parent provider + (returns if machine is "active" or "inactive" + + Args: + machineId: If of the machine to get state + + Returns: + one of this values: + unassigned, down, up, powering_up, powered_down, + paused, migrating_from, migrating_to, unknown, not_responding, + wait_for_launch, reboot_in_progress, saving_state, restoring_state, + suspended, image_illegal, image_locked or powering_down + Also can return'unknown' if Machine is not known + ''' + return self.parent().getMachineState(machineId) + + def startMachine(self, machineId): + ''' + Tries to start a machine. No check is done, it is simply requested to oVirt. + + This start also "resume" suspended/paused machines + + Args: + machineId: Id of the machine + + Returns: + ''' + return self.parent().startMachine(machineId) + + def stopMachine(self, machineId): + ''' + Tries to start a machine. No check is done, it is simply requested to oVirt + + Args: + machineId: Id of the machine + + Returns: + ''' + return self.parent().stopMachine(machineId) + + def suspendMachine(self, machineId): + ''' + Tries to start a machine. No check is done, it is simply requested to oVirt + + Args: + machineId: Id of the machine + + Returns: + ''' + return self.parent().suspendMachine(machineId) + + def removeMachine(self, machineId): + ''' + Tries to delete a machine. No check is done, it is simply requested to oVirt + + Args: + machineId: Id of the machine + + Returns: + ''' + return self.parent().removeMachine(machineId) + + def updateMachineMac(self, machineId, macAddres): + ''' + Changes the mac address of first nic of the machine to the one specified + ''' + return self.parent().updateMachineMac(machineId, macAddres) + + def getMacRange(self): + ''' + Returns de selected mac range + ''' + return self.parent().getMacRange() + + def getBaseName(self): + ''' + Returns the base name + ''' + return self.baseName.value + + def getLenName(self): + ''' + Returns the length of numbers part + ''' + return int(self.lenName.value) + + def getDisplay(self): + ''' + Returns the selected display type (for created machines, for administration + ''' + return self.display.value + + def getConsoleConnection(self, machineId): + return self.parent().getConsoleConnection(machineId) + + def desktopLogin(self, machineId, username, password, domain): + return self.parent().desktopLogin(machineId, username, password, domain) diff --git a/server/src/uds/services/OpenNebula/Provider.py b/server/src/uds/services/OpenNebula/Provider.py new file mode 100644 index 000000000..9ebca4065 --- /dev/null +++ b/server/src/uds/services/OpenNebula/Provider.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2012 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. + +''' +Created on Jun 22, 2012 + +.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +from django.utils.translation import ugettext_noop as _ +from uds.core.services import ServiceProvider +from uds.core.ui import gui +from uds.core.util import validators +from defusedxml import minidom + +from .LiveService import LiveService + + +import logging +import six + +# Python bindings for OpenNebula +import oca + +__updated__ = '2016-02-08' + +logger = logging.getLogger(__name__) + +class Provider(ServiceProvider): + ''' + This class represents the sample services provider + + In this class we provide: + * The Provider functionality + * The basic configuration parameters for the provider + * The form fields needed by administrators to configure this provider + + :note: At class level, the translation must be simply marked as so + using ugettext_noop. This is so cause we will translate the string when + sent to the administration client. + + For this class to get visible at administration client as a provider type, + we MUST register it at package __init__. + + ''' + # : What kind of services we offer, this are classes inherited from Service + offers = [LiveService] + # : Name to show the administrator. This string will be translated BEFORE + # : sending it to administration interface, so don't forget to + # : mark it as _ (using ugettext_noop) + typeName = _('OpenNebula Platform Provider') + # : Type used internally to identify this provider + typeType = 'openNebulaPlatform' + # : Description shown at administration interface for this provider + typeDescription = _('OpenNebula platform service provider') + # : Icon file used as icon for this provider. This string will be translated + # : BEFORE sending it to administration interface, so don't forget to + # : mark it as _ (using ugettext_noop) + iconFile = 'provider.png' + + # now comes the form fields + # There is always two fields that are requested to the admin, that are: + # Service Name, that is a name that the admin uses to name this provider + # Description, that is a short description that the admin gives to this provider + # Now we are going to add a few fields that we need to use this provider + # Remember that these are "dummy" fields, that in fact are not required + # but used for sample purposes + # If we don't indicate an order, the output order of fields will be + # "random" + host = gui.TextField(length=64, label=_('Host'), order=1, tooltip=_('OpenNebula Host'), required=True) + port = gui.NumericField(length=5, label=_('Port'), defvalue='2633', order=2, tooltip=_('OpenNebula Port (default is 2633 for non ssl connection)'), required=True) + ssl = gui.CheckBoxField(label=_('Use SSL'), order=3, tooltip=_('If checked, the connection will be forced to be ssl (will not work if server is not providing ssl)')) + username = gui.TextField(length=32, label=_('Username'), order=4, tooltip=_('User with valid privileges on OpenNebula'), required=True, defvalue='oneadmin') + password = gui.PasswordField(lenth=32, label=_('Password'), order=5, tooltip=_('Password of the user of OpenNebula'), required=True) + timeout = gui.NumericField(length=3, label=_('Timeout'), defvalue='10', order=6, tooltip=_('Timeout in seconds of connection to OpenNebula'), required=True) + macsRange = gui.TextField(length=36, label=_('Macs range'), defvalue='52:54:00:00:00:00-52:54:00:FF:FF:FF', order=7, rdonly=True, + tooltip=_('Range of valid macs for UDS managed machines'), required=True) + + # Own variables + _api = None + + def initialize(self, values=None): + ''' + We will use the "autosave" feature for form fields + ''' + + # Just reset _api connection variable + self._api = None + + if values is not None: + self.macsRange.value = validators.validateMacRange(self.macsRange.value) + self.timeout.value = validators.validateTimeout(self.timeout.value, returnAsInteger=False) + logger.debug('Endpoint: {}'.format(self.endPoint)) + + @property + def endpoint(self): + return 'http{}://{}:{}/RPC2'.format('s' if self.ssl.isTrue() else '', self.host.value, self.port.value) + + + @property + def api(self): + if self._api is None: + self._api = oca.Client('{}:{}'.format(self.username.value, self.password.value), self.endpoint) + + return self._api + + def resetApi(self): + self._api = None + + def sanitizeVmName(self, name): + ''' + Ovirt only allows machine names with [a-zA-Z0-9_-] + ''' + import re + return re.sub("[^a-zA-Z0-9_-]", "_", name) + + def testConnection(self): + ''' + Test that conection to OpenNebula server is fine + + Returns + + True if all went fine, false if id didn't + ''' + + try: + if self.api.version() < '4.1': + return [False, 'OpenNebula version is not supported (required version 4.1 or newer)'] + except Exception as e: + return [False, '{}'.format(e)] + + return [True, _('Opennebula test connection passed')] + + + def getMachines(self, force=False): + ''' + Obtains the list of machines inside OpenNebula. + Machines starting with UDS are filtered out + + Args: + force: If true, force to update the cache, if false, tries to first + get data from cache and, if valid, return this. + + Returns + An array of dictionaries, containing: + 'name' + 'id' + 'cluster_id' + ''' + vmpool = oca.VirtualMachinePool(self.api) + vmpool.info() + + return vmpool + + def getDatastores(self, datastoreType=0): + ''' + 0 seems to be images datastore + ''' + datastores = oca.DatastorePool(self.api) + datastores.info() + + for ds in datastores: + if ds.type == datastoreType: + yield ds + + def getTemplates(self, force=False): + logger.debug('Api: {}'.format(self.api)) + templatesPool = oca.VmTemplatePool(self.api) + templatesPool.info() + + for t in templatesPool: + if t.name[:4] != 'UDSP': + yield t + + def makeTemplate(self, fromTemplateId, name, toDataStore): + ''' + Publish the machine (makes a template from it so we can create COWs) and returns the template id of + the creating machine + + Args: + fromTemplateId: id of the base template + name: Name of the machine (care, only ascii characters and no spaces!!!) + + Returns + Raises an exception if operation could not be acomplished, or returns the id of the template being created. + + Note: + Maybe we need to also clone the hard disk? + ''' + try: + # First, we clone the themplate itself + templateId = self.api.call('template.clone', int(fromTemplateId), name) + + # Now copy cloned images if possible + try: + imgs = oca.ImagePool(self.api) + imgs.info() + imgs = dict(((i.name, i.id) for i in imgs)) + + info = self.api.call('template.info', templateId) + template = minidom.parseString(info).getElementsByTagName('TEMPLATE')[0] + logger.debug('XML: {}'.format(template.toxml())) + + counter = 0 + for dsk in template.getElementsByTagName('DISK'): + counter += 1 + imgIds = dsk.getElementsByTagName('IMAGE_ID') + if len(imgIds) == 0: + fromId = False + node = dsk.getElementsByTagName('IMAGE')[0].childNodes[0] + imgName = node.data + # Locate + imgId = imgs[imgName] + else: + fromId = True + node = imgIds[0].childNodes[0] + imgId = node.data + + logger.debug('Found {} for cloning'.format(imgId)) + + # Now clone the image + imgName = self.sanitizeVmName(name + ' DSK ' + six.text_type(counter)) + newId = self.api.call('image.clone', int(imgId), imgName, int(toDataStore)) + if fromId is True: + node.data = six.text_type(newId) + else: + node.data = imgName + + # Now update the clone + self.api.call('template.update', templateId, template.toxml()) + except: + logger.exception('Exception cloning image') + + return six.text_type(templateId) + except Exception as e: + logger.error('Creating template on OpenNebula: {}'.format(e)) + raise + + def removeTemplate(self, templateId): + ''' + Removes a template from ovirt server + + Returns nothing, and raises an Exception if it fails + ''' + try: + # 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 = oca.ImagePool(self.api) + imgs.info() + imgs = dict(((i.name, i.id) for i in imgs)) + + info = self.api.call('template.info', int(templateId)) + template = minidom.parseString(info).getElementsByTagName('TEMPLATE')[0] + logger.debug('XML: {}'.format(template.toxml())) + + counter = 0 + for dsk in template.getElementsByTagName('DISK'): + imgIds = dsk.getElementsByTagName('IMAGE_ID') + if len(imgIds) == 0: + node = dsk.getElementsByTagName('IMAGE')[0].childNodes[0] + imgId = imgs[node.data] + else: + node = imgIds[0].childNodes[0] + imgId = node.data + + logger.debug('Found {} for cloning'.format(imgId)) + + # Now delete the image + self.api.call('image.delete', int(imgId)) + + except: + logger.exception('Exception cloning image') + + self.api.call('template.delete', int(templateId)) + except Exception as e: + logger.error('Creating template on OpenNebula: {}'.format(e)) + + def getMachineState(self, machineId): + ''' + Returns the state of the machine + This method do not uses cache at all (it always tries to get machine state from OpenNebula server) + + Args: + machineId: Id of the machine to get state + + Returns: + one of this values: + unassigned, down, up, powering_up, powered_down, + paused, migrating_from, migrating_to, unknown, not_responding, + wait_for_launch, reboot_in_progress, saving_state, restoring_state, + suspended, image_illegal, image_locked or powering_down + Also can return'unknown' if Machine is not known + ''' + return self.__getApi().getMachineState(machineId) + + def deployFromTemplate(self, name, comments, templateId, clusterId, displayType, memoryMB, guaranteedMB): + ''' + Deploys a virtual machine on selected cluster from selected template + + Args: + name: Name (sanitized) of the machine + comments: Comments for machine + templateId: Id of the template to deploy from + clusterId: Id of the cluster to deploy to + displayType: 'vnc' or 'spice'. Display to use ad OpenNebula admin interface + memoryMB: Memory requested for machine, in MB + guaranteedMB: Minimum memory guaranteed for this machine + + Returns: + Id of the machine being created form template + ''' + return self.__getApi().deployFromTemplate(name, comments, templateId, clusterId, displayType, memoryMB, guaranteedMB) + + def startMachine(self, machineId): + ''' + Tries to start a machine. No check is done, it is simply requested to OpenNebula. + + This start also "resume" suspended/paused machines + + Args: + machineId: Id of the machine + + Returns: + ''' + return self.__getApi().startMachine(machineId) + + def stopMachine(self, machineId): + ''' + Tries to start a machine. No check is done, it is simply requested to OpenNebula + + Args: + machineId: Id of the machine + + Returns: + ''' + return self.__getApi().stopMachine(machineId) + + def suspendMachine(self, machineId): + ''' + Tries to start a machine. No check is done, it is simply requested to OpenNebula + + Args: + machineId: Id of the machine + + Returns: + ''' + return self.__getApi().suspendMachine(machineId) + + def removeMachine(self, machineId): + ''' + Tries to delete a machine. No check is done, it is simply requested to OpenNebula + + Args: + machineId: Id of the machine + + Returns: + ''' + return self.__getApi().removeMachine(machineId) + + def updateMachineMac(self, machineId, macAddres): + ''' + Changes the mac address of first nic of the machine to the one specified + ''' + return self.__getApi().updateMachineMac(machineId, macAddres) + + def getMacRange(self): + return self.macsRange.value + + def getConsoleConnection(self, machineId): + return self.__getApi().getConsoleConnection(machineId) + + def desktopLogin(self, machineId, username, password, domain): + ''' + ''' + return self.__getApi().desktopLogin(machineId, username, password, domain) + + @staticmethod + def test(env, data): + ''' + Test ovirt Connectivity + + Args: + env: environment passed for testing (temporal environment passed) + + data: data passed for testing (data obtained from the form + definition) + + Returns: + Array of two elements, first is True of False, depending on test + (True is all right, false is error), + second is an String with error, preferably internacionalizated.. + + ''' + return Provider(env, data).testConnection() diff --git a/server/src/uds/services/OpenNebula/__init__.py b/server/src/uds/services/OpenNebula/__init__.py new file mode 100644 index 000000000..03243be33 --- /dev/null +++ b/server/src/uds/services/OpenNebula/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2012 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from .Provider import Provider + diff --git a/server/src/uds/services/OpenNebula/provider.png b/server/src/uds/services/OpenNebula/provider.png new file mode 100644 index 000000000..9e435e9b5 Binary files /dev/null and b/server/src/uds/services/OpenNebula/provider.png differ diff --git a/server/src/uds/static/adm/js/gui-d-servicespools.coffee b/server/src/uds/static/adm/js/gui-d-servicespools.coffee index 1eab5e83c..be1efe314 100644 --- a/server/src/uds/static/adm/js/gui-d-servicespools.coffee +++ b/server/src/uds/static/adm/js/gui-d-servicespools.coffee @@ -448,9 +448,10 @@ gui.servicesPools.link = (event) -> css: "disabled" disabled: true click: (val, value, btn, tbl, refreshFnc) -> - gui.promptModal gettext("Publish"), gettext("Cancel publication"), + gui.doLog val, val[0] + gui.promptModal gettext("Publish"), gettext("Cancel publication?"), onYes: -> - pubApi.invoke val.id + "/cancel", -> + pubApi.invoke val[0].id + "/cancel", -> refreshFnc() return