Commit 6501cf07 authored by David Johnson's avatar David Johnson

Pythonize the whole library so that PYTHONPATH and module paths are sane.

There are a couple bugfixes in here -- there was a bad call to
super().__init__ that passed self as the first arg.  I also removed the
config file manager_class and helper_class defaults.

Also updates the slurm module and adds important notes about subclassing
both an existing helper and manager inside of a single class... method
resolution order is important!  See the slurm module, or the new docs
for SimpleElasticSliceManager for that.
parent d538b17a
*~
*.pyc
build/
elasticslice.egg-info/
#!/usr/bin/python -i
##
## This is an example script that creates a dynamically expandable
## slice. All slices are "dynamically" expandable, of course, but
## usually that process involves generating a new request rspec and
## calling Update() to update your entire sliver. Instead, we create a
## slice, populate it with additional sliver info from an rspec, and
## then AddNodes() and DeleteNodes() as necessary. However, only a
## subset of node metadata can be set -- things like tarballs, OS, any
## LANs to join. You cannot add a new link or lan via this mechanism.
##
## If the slice doesn't exist, we create one, and create an initial
## rspec to populate the slice with all slivers it needs, and contact
## the necessary AM(s) to obtain the resources. During runtime, this
## script can add or remove nodes, according to its resource demands.
## It periodically renews the slice and its slivers.
##
## This script runs (at least) three threads. The main thread kicks off
## a management loop (ElasticSliceManager.manage); a
## ProtoGeniClientServer listens for incoming connections from ProtoGENI
## CM management scripts; and finally, the main thread "hangs around"
## (if you pass the -i option to python when invoking elasticslice!) so
## that you can manually manage your slice. For instance, if you pass
## the options that auto-start the threads (-A -M), you can stop them by
## typing stop() at the python prompt. Then you can issue manual
## management operations, like
##
## > server.renew_slice()
##
## or
##
## > server.delete_nodes(['node-10','node-11'])
##
## You can resume automatic management by typing start().
##
## You'll need to have geni-lib download and in your PYTHONPATH;
## otherwise, the import of protogeniclientlib will fail, since it tries
## to pull in geni-lib modules.
##
## You'll also need to have your Emulab encrypted SSL certificate;
## you'll want to place the passphrase for it in something like
## ~/.ssl/password (or specify an alternate location as a command-line
## argument to this script), otherwise you'll have to type the
## passphrase every time the key gets used! You'll also need the public
## bits of the CA certs for each CM you plan to use, so that the client
## server this script runs for you can verify incoming connections from
## CMs you are using.
##
## With the following command, all configuration options will be read
## from the specified config file:
##
## python elasticslice.py --config ../etc/main.conf
##
## Configuration options from the file will be overriden
## if specific command-line arguments are provided. To see
## the list of available arguments and their defaults, do:
##
## python elasticslice.py -h
##
## Here's a couple commands with command-line arguments:
##
## python -i elasticslice.py -d -D johnsond -U david -P foo -S djds \
## --default-cm clutah -C ~/.ssl/clutah-cacert.pem -A -M
##
## (this runs the elasticslice script in debug mode (and spews tons of
## output; you'll want to log it); creates a client server that
## authenticates incoming requests with HTTPS username/password
## authentication, with username 'david' and password 'foo'; operates
## on a slice with the HRN djds; uses the clutah (cloudlab utah) CM
## to create its sliver; and uses ~/.ssl/clutah-cacert.pem as the
## cacert for verification purposes. Finally, the -A option says to
## autostart the client server so that it is listening for incoming
## connections; and the -M option says to autostart the manager's
## infinite mangement loop (the thing that tries to add nodes if
## there are enough free resources).
##
## Here's a second command line that requires you to have a copy of the
## dynamic version of my openstack profile; not only is this script a
## geni-lib rspec generate (i.e., a Cloudlab geni-lib script) -- but it
## is also a ElasticsliceManagerHelper that provides the generateRspec
## and generateAddNodesArgs functions that a ElasticSliceManager relies
## on to bring semantics to a dynamic experiment.
##
## python -i elasticslice.py -d -D johnsond -U david -P foo -p 13333 -S djdos \
## --default-cm clutah -C ~/.ssl/clutah-cacert.pem \
## -H ~/g/openstack-build-ubuntu/osp-dynamic.py -A -M 2>&1 | tee log.1
##
## (this is much the same, but it uses -p to run the client server on
## a different port than the default (15243); uses a different slice
## HRN; and uses -H to specify a script that provides a
## ElasticSliceHelper (the openstack elasticslice helper.) Oh, and
## it also logs to a file.
##
import os
import getopt
import sys
import threading
import traceback
import logging
import protogeniclientlib
import manager as managerlib
from util import parse_options, configure_logging, Config
SECOND = 1
MINUTE = SECOND * 60
HOUR = MINUTE * 60
DAY = HOUR * 24
MONTH = DAY * 31
YEAR = DAY * 365
#logging.basicConfig()
LOG = logging.getLogger(__name__)
config = None
server = None
clientserver = None
manager = None
helper = None
(manager_classpath,manager_mod,manager_class) = (None,None,None)
(helper_classpath,helper_mod,helper_class) = (None,None,None)
(clientserver_classpath,clientserver_mod,clientserver_class) = (None,None,None)
client_endpoint_class = None
##
## Simple wrapper functions that are useful on the CLI for the administrator
## who might do things manually, interactively.
##
def createServer():
global server
if not server:
server = protogeniclientlib.ProtoGeniServer(config=config)
server.loadCache()
pass
return server
def createHelper():
global helper,helper_mod,helper_class
if not helper and helper_classpath:
if helper_classpath.find('.') > -1:
class_idx = helper_classpath.rfind('.')
classname = helper_classpath[class_idx+1:]
modpath = helper_classpath[:class_idx]
LOG.info("importing %s from %s as helper" % (classname,modpath))
helper_mod = __import__(modpath)
helper_class = getattr(helper_mod,classname)
helper = helper_class(server,config=config)
LOG.info("instantiated %s from %s as helper"
% (helper_class,helper_mod))
else:
LOG.error("helper_class must be a fully-qualified class name"
" (i.e., path.to.module.ClassName)")
return helper
pass
##elif not helper:
## helper = protogeniclientlib.SimpleElasticSliceManagerHelper()
elif helper:
LOG.warn("Helper already exists!")
else:
LOG.info("helper_class not specified; cannot create helper!")
pass
return helper
def createManager():
global manager,manager_mod,manager_class
if not manager and manager_classpath:
if manager_classpath.find('.') > -1:
class_idx = manager_classpath.rfind('.')
classname = manager_classpath[class_idx+1:]
modpath = manager_classpath[:class_idx]
LOG.info("importing %s from %s as manager" % (classname,modpath))
manager_mod = __import__(modpath)
manager_class = getattr(manager_mod,classname)
manager = manager_class(server,config=config)
LOG.info("instantiated %s from %s as manager"
% (manager_class,manager_mod))
else:
LOG.error("manager_class must be a fully-qualified class name"
" (i.e., path.to.module.ClassName)")
return manager
pass
##elif not manager:
## manager = protogeniclientlib.SimpleElasticSliceManagerManager()
elif manager:
LOG.warn("Manager already exists!")
else:
LOG.error("manager_class not specified; cannot create manager!")
pass
if manager:
set_helper = None
try:
set_helper = getattr(manager,'set_helper')
except:
pass
if set_helper:
try:
set_helper(helper)
except:
LOG.exception("manager.set_helper failed!")
pass
pass
pass
return manager
def startManager():
global manager
if not manager:
createManager()
pass
manager.start()
pass
def stopManager():
global manager
if manager:
manager.stop()
pass
def createClientServer():
global clientserver,clientserver_mod,clientserver_class,clientserver_endpoint
if not clientserver and clientserver_classpath:
if clientserver_classpath.find('.') > -1:
class_idx = clientserver_classpath.rfind('.')
classname = clientserver_classpath[class_idx+1:]
modpath = clientserver_classpath[:class_idx]
LOG.info("importing %s from %s as clientserver" % (classname,modpath))
clientserver_mod = __import__(modpath)
clientserver_class = getattr(clientserver,classname)
clientserver_endpoint = clientserver_class(server,config=config)
LOG.info("instantiated %s from %s as clientserver"
% (clientserver_class,clientserver_mod))
else:
LOG.error("clientserver_class must be a fully-qualified class name"
" (i.e., path.to.module.ClassName)")
return clientserver
pass
elif not clientserver \
and manager and isinstance(manager,managerlib.ElasticSliceClientEndpoint):
LOG.info("using manager as clientserver endpoint!")
clientserver_endpoint = manager
##elif not clientserver:
## clientserver = protogeniclientlib.SimpleElasticSliceManagerClientServer()
elif clientserver:
LOG.warn("ClientServer already exists!")
return clientserver
else:
LOG.info("clientserver_class not specified, and manager is not an"
" endpoint; cannot create clientserver!")
return None
clientserver_endpoint = manager
cskwargs = dict(modules={ 'dynslice':clientserver_endpoint })
clientserver = protogeniclientlib.ProtoGeniClientServer(**cskwargs)
#
# Also try to tell our server who we are.
#
try:
srv = createServer()
srv.set_client_endpoint(clientserver,config.all['node_delete_wait_time'])
except:
LOG.exception("could not set client endpoint!")
pass
return clientserver
def startClientServer():
global clientserver
if not clientserver:
createClientServer()
pass
clientserver.handle_requests_threaded()
pass
def stopClientServer():
global clientserver
if not clientserver:
print "Client Server not running!"
else:
clientserver.stop_handling()
pass
def stop():
stopClientServer()
stopManager()
pass
def start():
startClientServer()
startManager()
pass
def lookupSlice(name=None):
if not server: createServer()
return server.resolve_slice(name=name)
def createSlice(name=None):
if not server: createServer()
return server.create_slice(name=name)
def renewSlice(name=None,additional_seconds=protogeniclientlib.DAY,cmlist=None):
if not server: createServer()
return server.renew_slice(name=name,additional_seconds=additional_seconds,
cmlist=cmlist)
def deleteSlice(name=None,cmlist=None):
if not server: createServer()
return server.delete_slice(name=name,cmlist=cmlist)
def createSliver(rspec,cm=None,name=None):
if not server: createServer()
return server.create_sliver(rspec,cm=cm,name=name)
def deleteSliver(cm=None,name=None):
if not server: createServer()
return server.delete_sliver(name=name,cm=cm)
def renewSliver(cm=None,name=None):
if not server: createServer()
return server.renew_sliver(name=name,cm=cm)
def sliverStatus(name=None,cm=None):
if not server: createServer()
return server.sliver_status(name=name,cm=cm)
def renew(name=None,cmlist=None):
if not server: createServer()
retval1 = renewSlice()
retval2 = renewSliver()
return (retval1,retval2)
def addNodes(nodes,name=None,cm=None):
if not server: createServer()
return server.add_nodes(nodes=nodes,name=name,cm=cm)
def getResources(cm=None,available=True):
if not server: createServer()
return server.get_resources(cm=cm,available=available)
def deleteNodes(nodes,name=None,cm=None):
if not server: createServer()
return server.delete_nodes(nodes=nodes,name=name,cm=cm)
def verify_config(config):
if config.minthreshold > config.maxthreshold:
LOG.error("Min threshold (%s) is greater than max threshold (%s)"
% (str(config.minthreshold),str(config.maxthreshold)))
pass
if __name__ == "__main__":
(options, args) = parse_options()
configure_logging(options.debug)
config = Config(options)
verify_config(config)
manager_classpath = config.all['manager_class']
helper_classpath = config.all['helper_class']
clientserver_endpoint_classpath = config.all['clientserver_endpoint_class']
manager_class = config.all['manager_class'] \
or 'manager.SimpleElasticSliceManager'
helper_class = config.all['helper_class'] \
or 'manager.SimpleElasticSliceHelper'
# The default manager is an Endpoint, so for now don't force an
# Endpoint on the user.
clientserver_class = config.all['clientserver_endpoint_class'] or None
LOG.debug("Completed verification of the provided configuration options")
# TODO:
# # Always create the server handle object; both the manager and any
# # manager helper need it.
# LOG.debug("server args: %s" % (str(skwargs),))
createServer()
createHelper()
# Always create the manager. Do that after the helper,
createManager()
if config.automanage:
startManager()
pass
if config.autostart:
startClientServer()
pass
pass
import logging
from manager import SimpleElasticSliceManager, SimpleElasticSliceHelper
from util import ShellCommand
from elasticslice.managers.core import SimpleElasticSliceHelper, \
SimpleElasticSliceManager
from elasticslice.util.util import ShellCommand
SECOND = 1
MINUTE = SECOND * 60
......@@ -11,7 +12,15 @@ YEAR = DAY * 365
LOG = logging.getLogger(__name__)
class SlurmDynamicManager(SimpleElasticSliceManager, SimpleElasticSliceHelper):
#
# NB: it is very important that, as documented, SimpleElasticSliceHelper
# is the first class here, so that its methods override those of
# SimpleElasticSliceManager. Why? Because SimpleElasticSliceManager is
# also a helper -- it is a PluginElasticSliceHelper, which means it
# accepts a plugin helper, and doesn't have any functionality of its own
# (other than to call the the plugin helper object's methods).
#
class SlurmDynamicManager(SimpleElasticSliceHelper,SimpleElasticSliceManager):
DEF_ORDER = [ 'ensure_slice','ensure_sliver','update_sliver_status',
'renew','update_available','update_all','get_system_state',
......@@ -22,10 +31,10 @@ class SlurmDynamicManager(SimpleElasticSliceManager, SimpleElasticSliceHelper):
add_nodes=5 * MINUTE,delete_nodes=5 * MINUTE)
def __init__(self,server,config=None):
SimpleElasticSliceHelper.__init__(self,server,config=config)
SimpleElasticSliceManager.__init__(self,server,config=config,
manage_order=SlurmDynamicManager.DEF_ORDER,
manage_intervals=SlurmDynamicManager.DEF_INTERVALS)
SimpleElasticSliceHelper.__init__(self,server,config=config)
self.slurm = SlurmScheduler()
# Manager functions redefined:
......
......@@ -4,7 +4,7 @@ import os
import traceback
import time
import threading
from protogeniclientlib import ProtoGeniClientDefs, ProtoGeniServer, \
from elasticslice.rpc.protogeni import ProtoGeniClientDefs, ProtoGeniServer, \
ProtoGeniResponse, ProtoGeniManifestWrapper, \
ProtoGeniClientServerEndpoint, ProtoGeniClientServer, SENDMAIL
import geni.rspec.pgad as RSpecAd
......@@ -157,7 +157,7 @@ class SimpleElasticSliceHelper(ElasticSliceHelper):
num_pcs=1,image_urn=None,num_lans=0,multiplex_lans=False,
tarballs=[],startup_command=None,nodetype=None,
node_prefix="node",lan_prefix="lan"):
super(SimpleElasticSliceHelper,self).__init__(self,server)
super(SimpleElasticSliceHelper,self).__init__(server)
self.config = config
self.rspec = RSpec.Request()
......@@ -789,6 +789,29 @@ class SimpleElasticSliceManager(ElasticSliceManager,
ElasticSliceManager locks using a per-instance threading.RLock.
Thus, once one thread has the lock, they're good.
If you subclass SimpleElasticSliceManager, and want to replace its
helper functions, it is very important that you first subclass
whatever helper class you're extending, then
SimpleElasticSliceManager. For instance,
class FooManager(SimpleElasticSliceHelper,SimpleElasticSliceManager):
Why? Because that makes the method resolution order inside of FooManager
be FooManager, SimpleElasticSliceHelper, SimpleElasticSliceManager ... .
If you instead tried to do the reverse:
class FooManager(SimpleElasticSliceManager,SimpleElasticSliceHelper):
the method resolution order would be FooManager, SimpleElasticSliceManager,
SimpleElasticSliceHelper. Because SimpleElasticSliceManager provides an
implementation of each method in the ElasticSliceHelper interface, those
methods will be called, and those from SimpleElasticSliceHelper will never
be called (which defeats the point, that you hoped to re-use the helper
methods from SimpleElasticSliceHelper). So consider method resolution
order! Helpers are not mixins, because an ElasticSliceManager is also
an ElasticSliceHelper! This is an OO multiple inheritance style, not a
mixin style.
"""
def __init__(self,server,config=None,
minthreshold=DEF_MIN_THRESHOLD,maxthreshold=DEF_MAX_THRESHOLD,
......@@ -818,6 +841,8 @@ class SimpleElasticSliceManager(ElasticSliceManager,
self.server = server
self.config = config
LOG.debug("server = %s, config = %s" % (server,config))
self.minthreshold = (config and 'minthreshold' in config.all
and config.all['minthreshold']) or minthreshold
self.maxthreshold = (config and 'maxthreshold' in config.all
......
......@@ -127,9 +127,11 @@ def parse_options():
parser.add_option("--image_urn", dest="image_urn",
help="Set the default image URN, e.g. urn:publicid:IDN+emulab.net+image+emulab-ops:UBUNTU14-64-STD")
parser.add_option("--manager_class",dest="manager_class",
help="The class to instantiate and use as the Manager [default: %default]")
help="The class to instantiate and use as the Manager [default: elasticslice.managers.core.SimpleElasticSliceManager]")
# default="manager.SimpleElasticSliceManager")
parser.add_option("--helper_class",dest="helper_class",
help="The class to instantiate and use as the Helper [default: %default]")
help="The class to instantiate and use as the Helper [default: elasticslice.managers.core.SimpleElasticSliceHelper, if you also use the default manager_class]")
# default="manager.SimpleElasticSliceHelper")
parser.add_option("--clientserver_endpoint_class",
dest="clientserver_endpoint_class [default %default]",
help="The class to instantiate and use as the Client Server endpoint [default: %default]")
......
[metadata]
name = elasticslice
version = 0.1
summary = A library and tools to manage ProtoGeni experiments that dynamically change size
description-file =
README.rst
author = David M Johnson, Dmitry Duplyakin
author-email = johnsond@flux.utah.edu, dmitry.duplyakin@gmail.com
home-page = https://gitlab.flux.utah.edu/elasticslice/elasticslice
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
[files]
packages =
elasticslice
#data_files =
# etc/main.conf =
# etc/elasticslice.conf
[global]
setup-hooks =
pbr.hooks.setup_hook
[pbr]
warnerrors = true
#!/usr/bin/env python
import setuptools
setuptools.setup(
setup_requires=['pbr'],
pbr=True)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment