mirror of
https://github.com/dkmstr/openuds.git
synced 2024-12-22 13:34:04 +03:00
* Important advances on OpenNebula service provider
* several semantic changes * Minor improvements & fixes
This commit is contained in:
parent
d024d74529
commit
de4aef3a5c
@ -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
|
||||
'''
|
||||
|
@ -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)".
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
612
server/src/uds/services/OpenNebula/LiveDeployment.py
Normal file
612
server/src/uds/services/OpenNebula/LiveDeployment.py
Normal file
@ -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]))
|
165
server/src/uds/services/OpenNebula/LivePublication.py
Normal file
165
server/src/uds/services/OpenNebula/LivePublication.py
Normal file
@ -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
|
294
server/src/uds/services/OpenNebula/LiveService.py
Normal file
294
server/src/uds/services/OpenNebula/LiveService.py
Normal file
@ -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)
|
422
server/src/uds/services/OpenNebula/Provider.py
Normal file
422
server/src/uds/services/OpenNebula/Provider.py
Normal file
@ -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()
|
32
server/src/uds/services/OpenNebula/__init__.py
Normal file
32
server/src/uds/services/OpenNebula/__init__.py
Normal file
@ -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
|
||||
|
BIN
server/src/uds/services/OpenNebula/provider.png
Normal file
BIN
server/src/uds/services/OpenNebula/provider.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user