From a869b6b1e66606b97fc654001275bb50b9f63206 Mon Sep 17 00:00:00 2001 From: Thierry Parmentelat Date: Tue, 1 Jun 2010 07:46:28 +0000 Subject: [PATCH] renamed files used at runtime (see post-install script in specfile) curlwrapper now uses pycurl needs testing --- Makefile | 2 +- NodeManager.spec | 34 ++++++++++--- api.py | 14 +++--- api_calls.py | 44 +++++++++-------- bwmon.py | 2 +- curlwrapper.py | 91 ++++++++++++++++++++++------------- database.py | 47 +++++++++++++----- forward_api_calls.c | 2 +- logger.py | 4 +- logrotate/{nm => nodemanager} | 2 +- nm.py | 6 ++- plcapi.py | 2 +- sliver_vs.py | 4 +- ticket.py | 10 ++-- tools.py | 4 +- 15 files changed, 173 insertions(+), 95 deletions(-) rename logrotate/{nm => nodemanager} (78%) diff --git a/Makefile b/Makefile index 4b130d5..f5996ee 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ clean: ########## tags: - find . '(' -name '*.py' -o -name '*.c' -o -name '*.spec' ')' | xargs etags + (find . '(' -name '*.py' -o -name '*.c' -o -name '*.spec' ')' ; ls initscripts/*) | xargs etags .PHONY: tags diff --git a/NodeManager.spec b/NodeManager.spec index 8711e79..d58342c 100644 --- a/NodeManager.spec +++ b/NodeManager.spec @@ -41,9 +41,6 @@ Requires: util-vserver-python > 0.3-16 # Signed tickets Requires: gnupg -# Contact API server -Requires: curl - # Uses function decorators Requires: python >= 2.4 @@ -73,9 +70,33 @@ mkdir -p $RPM_BUILD_ROOT/%{_initrddir}/ rsync -av initscripts/ $RPM_BUILD_ROOT/%{_initrddir}/ chmod 755 $RPM_BUILD_ROOT/%{_initrddir}/* -install -D -m 644 logrotate/nm $RPM_BUILD_ROOT/%{_sysconfdir}/logrotate.d/nm +install -d -m 755 $RPM_BUILD_ROOT/var/lib/nodemanager + +install -D -m 644 logrotate/nodemanager $RPM_BUILD_ROOT/%{_sysconfdir}/logrotate.d/nodemanager +########## %post +# tmp - handle file renamings; old names are from 2.0-8 +renamings=" +/var/lib/misc/bwmon.dat@/var/lib/nodemanager/bwmon.dat +/root/sliver_mgr_db.pickle@/var/lib/nodemanager/nodemanager.pickle +/var/log/getslivers.txt@/var/lib/nodemanager/getslivers.txt +/var/log/nm@/var/log/nodemanager +/var/log/nm.daemon@/var/log/nodemanager.daemon +/var/run/nm.pid@/var/run/nodemanager.pid +/tmp/sliver_mgr.api@/tmp/nodemanager.api +/etc/logrotate.d/nm@/etc/logrotate.d/nodemanager +" +for renaming in $renamings; do + old=$(echo $renaming | cut -d@ -f1) + new=$(echo $renaming | cut -d@ -f2) + newdir=$(dirname $new) + if [ -e "$old" -a ! -e "$new" ] ; then + mkdir -p $newdir + mv -f $old $new + fi +done +# chkconfig --add conf_files chkconfig conf_files on chkconfig --add nm @@ -87,7 +108,7 @@ if [ "$PL_BOOTCD" != "1" ] ; then service fuse-pl restart fi - +########## %preun # 0 = erase, 1 = upgrade if [ $1 -eq 0 ] ; then @@ -107,7 +128,8 @@ rm -rf $RPM_BUILD_ROOT %{_datadir}/NodeManager/ %{_bindir}/forward_api_calls %{_initrddir}/ -%{_sysconfdir}/logrotate.d/nm +%{_sysconfdir}/logrotate.d/nodemanager +/var/lib/ %changelog * Fri May 14 2010 Talip Baris Metin - NodeManager-2.0-8 diff --git a/api.py b/api.py index ef279b8..a6cae30 100644 --- a/api.py +++ b/api.py @@ -39,7 +39,7 @@ except: logger.log("api: Warning: admin slice prefix set to %s" %(PLC_SLICE_PREFIX), 2) API_SERVER_PORT = 812 -UNIX_ADDR = '/tmp/sliver_mgr.api' +UNIX_ADDR = '/tmp/nodemanager.api' class APIRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): # overriding _dispatch to achieve this effect is officially deprecated, @@ -68,12 +68,15 @@ class APIRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): ucred = self.request.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, sizeof_struct_ucred) xid = struct.unpack('3i', ucred)[1] caller_name = pwd.getpwuid(xid)[0] - # Special case the genicw + # Special case : the sfa component manager if caller_name == PLC_SLICE_PREFIX+"_sfacm": try: result = method(*args) except Exception, err: raise xmlrpclib.Fault(104, 'Error in call: %s' %err) # Anyone can call these functions - elif method_name not in ('Help', 'Ticket', 'GetXIDs', 'GetSSHKeys'): + elif method_name in ('Help', 'Ticket', 'GetXIDs', 'GetSSHKeys'): + try: result = method(*args) + except Exception, err: raise xmlrpclib.Fault(104, 'Error in call: %s' %err) + else: # Execute anonymous call. # Authenticate the caller if not in the above fncts. if method_name == "GetRecord": target_name = caller_name @@ -83,7 +86,7 @@ class APIRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): # Gather target slice's object. target_rec = database.db.get(target_name) - # only work on slivers or self. Sannity check. + # only work on slivers or self. Sanity check. if not (target_rec and target_rec['type'].startswith('sliver.')): raise xmlrpclib.Fault(102, \ 'Invalid argument: the first argument must be a sliver name.') @@ -94,9 +97,6 @@ class APIRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): except Exception, err: raise xmlrpclib.Fault(104, 'Error in call: %s' %err) else: raise xmlrpclib.Fault(108, '%s: Permission denied.' % caller_name) - else: # Execute anonymous call. - try: result = method(*args) - except Exception, err: raise xmlrpclib.Fault(104, 'Error in call: %s' %err) if result == None: result = 1 return result diff --git a/api_calls.py b/api_calls.py index 5b99a08..1e350c7 100644 --- a/api_calls.py +++ b/api_calls.py @@ -33,8 +33,8 @@ import logger # TODO: These try/excepts are a hack to allow doc/DocBookLocal.py to # import this file in order to extract the documentation from each -# exported function. A better approach will involve more extensive code -# splitting, I think. +# exported function. +# A better approach will involve more extensive code splitting, I think. try: import database except: import logger as database try: import sliver_vs @@ -100,7 +100,10 @@ def export_to_docbook(**kwargs): @export_to_api(0) def Help(): """Get a list of functions currently supported by the Node Manager API""" - return ''.join([method.__doc__ + '\n' for method in api_method_dict.itervalues()]) + names=api_method_dict.keys() + names.sort() + return ''.join(['**** ' + api_method_dict[name].__name__ + '\n' + api_method_dict[name].__doc__ + '\n' + for name in names]) @export_to_docbook(roles=['self'], accepts=[Parameter(str, 'A ticket returned from GetSliceTicket()')], @@ -112,8 +115,7 @@ def Ticket(ticket): actions are performed on a delegated slice (such as creation), a controller slice must deliver a valid slice ticket to NM. - This ticket is the value retured by PLC's GetSliceTicket() API call, - """ + This ticket is the value retured by PLC's GetSliceTicket() API call.""" try: data = ticket_module.verify(ticket) name = data['slivers'][0]['name'] @@ -129,8 +131,7 @@ def Ticket(ticket): returns=Parameter(int, '1 if successful')) @export_to_api(1) def AdminTicket(ticket): - """Admin interface to create slivers based on ticket returned by GetSlivers(). - """ + """Admin interface to create slivers based on ticket returned by GetSlivers().""" try: data, = xmlrpclib.loads(ticket)[0] name = data['slivers'][0]['name'] @@ -257,17 +258,20 @@ def GetLoans(sliver_name): rec = sliver_name return rec.get('_loans', [])[:] -def validate_loans(obj): - """Check that is a valid loan specification.""" - def validate_loan(obj): return (type(obj)==list or type(obj)==tuple) and len(obj)==3 and type(obj[0])==str and type(obj[1])==str and obj[1] in database.LOANABLE_RESOURCES and type(obj[2])==int and obj[2]>=0 - return type(obj)==list and False not in map(validate_loan, obj) +def validate_loans(loans): + """Check that is a list of valid loan specifications.""" + def validate_loan(loan): + return (type(loan)==list or type(loan)==tuple) and len(loan)==3 \ + and type(loan[0])==str and type(loan[1])==str and loan[1] in database.LOANABLE_RESOURCES and type(loan[2])==int and loan[2]>=0 + return type(loans)==list and False not in [validate_loan(load) for loan in loans] + @export_to_docbook(roles=['nm-controller', 'self'], - accepts=[ Parameter(str, 'A sliver/slice name.'), - [Mixed(Parameter(str, 'recipient slice name'), - Parameter(str, 'resource name'), - Parameter(int, 'resource amount'))] ], - returns=Parameter(int, '1 if successful')) + accepts=[ Parameter(str, 'A sliver/slice name.'), + [Mixed(Parameter(str, 'recipient slice name'), + Parameter(str, 'resource name'), + Parameter(int, 'resource amount'))], ], + returns=Parameter(int, '1 if successful')) @export_to_api(2) def SetLoans(sliver_name, loans): """Overwrite the list of loans made by the specified sliver. @@ -276,15 +280,15 @@ def SetLoans(sliver_name, loans): RSpec is handed out, but it will silently discard those loans that would put it over capacity. This behavior may be replaced with error semantics in the future. As well, there is currently no asynchronous notification - of loss of resources. - """ + of loss of resources.""" rec = sliver_name - if not validate_loans(loans): raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification') + if not validate_loans(loans): + raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification') rec['_loans'] = loans database.db.sync() @export_to_docbook(roles=['nm-controller', 'self'], - returns=Parameter(dict, 'Record dictionary')) + returns=Parameter(dict, 'Record dictionary')) @export_to_api(0) def GetRecord(sliver_name): """Return sliver record""" diff --git a/bwmon.py b/bwmon.py index dad2a86..1d7081a 100644 --- a/bwmon.py +++ b/bwmon.py @@ -41,7 +41,7 @@ DEBUG = False # Set ENABLE to False to setup buckets, but not limit. ENABLE = True -datafile = "/var/lib/misc/bwmon.dat" +datafile = "/var/lib/nodemanager/bwmon.dat" try: sys.path.append("/etc/planetlab") diff --git a/curlwrapper.py b/curlwrapper.py index 81c9c14..da5810c 100644 --- a/curlwrapper.py +++ b/curlwrapper.py @@ -1,39 +1,64 @@ -# $Id$ -# $URL$ - -from subprocess import PIPE, Popen -from select import select -# raise xmplrpclib.ProtocolError -import xmlrpclib -import signal import os +import xmlrpclib +import urllib +import pycurl +from cStringIO import StringIO + import logger -class Sopen(Popen): - def kill(self, signal = signal.SIGTERM): - os.kill(self.pid, signal) +# a pycurl-based replacement for the previous version that relied on forking curl def retrieve(url, cacert=None, postdata=None, timeout=90): -# options = ('/usr/bin/curl', '--fail', '--silent') - options = ('/usr/bin/curl', '--fail', ) - if cacert: options += ('--cacert', cacert) - if postdata: options += ('--data', '@-') + curl= pycurl.Curl() + curl.setopt(pycurl.URL,url) + + # reproduce --fail from the previous version + curl.setopt(pycurl.FAILONERROR,1) + # don't want curl sending any signals + curl.setopt(pycurl.NOSIGNAL, 1) + + # do not follow location when attempting to download a file + # curl.setopt(pycurl.FOLLOWLOCATION, 0) + + # store result on the fly + buffer=StringIO() + curl.setopt(pycurl.WRITEFUNCTION,buffer.write) + + # set timeout if timeout: - options += ('--max-time', str(timeout)) - options += ('--connect-timeout', str(timeout)) - p = Sopen(options + (url,), stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) - if postdata: p.stdin.write(postdata) - p.stdin.close() - sout, sin, serr = select([p.stdout,p.stderr],[],[], timeout) - if len(sout) == 0 and len(sin) == 0 and len(serr) == 0: - logger.verbose("curlwrapper: timed out after %s" % timeout) - p.kill(signal.SIGKILL) - data = p.stdout.read() - err = p.stderr.read() - rc = p.wait() - if rc != 0: - # when this triggers, the error sometimes doesn't get printed - logger.log ("curlwrapper: retrieve, got stderr <%s>"%err) - raise xmlrpclib.ProtocolError(url, rc, err, postdata) - else: - return data + curl.setopt(pycurl.CONNECTTIMEOUT, timeout) + curl.setopt(pycurl.TIMEOUT, timeout) + + # set cacert + if cacert: + curl.setopt(pycurl.CAINFO, cacert) + curl.setopt(pycurl.SSL_VERIFYPEER, 2) + else: + curl.setopt(pycurl.SSL_VERIFYPEER, 0) + + # set postdata + if postdata: + if isinstance(postdata,dict): + postfields = urllib.urlencode(postdata) + else: + postfields=postdata + curl.setopt(pycurl.POSTFIELDS, postfields) + + # go + try: + curl.perform() + + errcode = curl.getinfo(pycurl.HTTP_CODE) + curl.close() + + # check the code, return 1 if successfull + if errcode == 60: + raise xmlrpclib.ProtocolError (url,errcode, "SSL certificate validation failed", postdata) + elif errcode != 200: + raise xmlrpclib.ProtocolError (url,errcode, "http error %d"%errcode, postdata) + + except pycurl.error, err: + errno, errstr = err + raise xmlrpclib.ProtocolError(url, errno, "curl error %d: '%s'\n" %(errno,errstr),postdata ) + + return buffer.getvalue() diff --git a/database.py b/database.py index ab40824..c35042a 100644 --- a/database.py +++ b/database.py @@ -26,10 +26,17 @@ import bwmon # We enforce minimum allocations to keep the clueless from hosing their slivers. # Disallow disk loans because there's currently no way to punish slivers over quota. -MINIMUM_ALLOCATION = {'cpu_pct': 0, 'cpu_share': 1, 'net_min_rate': 0, 'net_max_rate': 8, 'net_i2_min_rate': 0, 'net_i2_max_rate': 8, 'net_share': 1} +MINIMUM_ALLOCATION = {'cpu_pct': 0, + 'cpu_share': 1, + 'net_min_rate': 0, + 'net_max_rate': 8, + 'net_i2_min_rate': 0, + 'net_i2_max_rate': 8, + 'net_share': 1, + } LOANABLE_RESOURCES = MINIMUM_ALLOCATION.keys() -DB_FILE = '/root/sliver_mgr_db.pickle' +DB_FILE = '/var/lib/nodemanager/nodemanager.pickle' # database object and associated lock @@ -57,7 +64,13 @@ class Database(dict): self._min_timestamp = 0 def _compute_effective_rspecs(self): - """Calculate the effects of loans and store the result in field _rspec. At the moment, we allow slivers to loan only those resources that they have received directly from PLC. In order to do the accounting, we store three different rspecs: field 'rspec', which is the resources given by PLC; field '_rspec', which is the actual amount of resources the sliver has after all loans; and variable resid_rspec, which is the amount of resources the sliver has after giving out loans but not receiving any.""" + """Calculate the effects of loans and store the result in field _rspec. +At the moment, we allow slivers to loan only those resources that they have received directly from PLC. +In order to do the accounting, we store three different rspecs: + * field 'rspec', which is the resources given by PLC; + * field '_rspec', which is the actual amount of resources the sliver has after all loans; + * and variable resid_rspec, which is the amount of resources the sliver + has after giving out loans but not receiving any.""" slivers = {} for name, rec in self.iteritems(): if 'rspec' in rec: @@ -66,14 +79,17 @@ class Database(dict): for rec in slivers.itervalues(): eff_rspec = rec['_rspec'] resid_rspec = rec['rspec'].copy() - for target, resname, amt in rec.get('_loans', []): - if target in slivers and amt <= resid_rspec[resname] - MINIMUM_ALLOCATION[resname]: - eff_rspec[resname] -= amt - resid_rspec[resname] -= amt - slivers[target]['_rspec'][resname] += amt + for target, resource_name, amount in rec.get('_loans', []): + if target in slivers and amount <= resid_rspec[resource_name] - MINIMUM_ALLOCATION[resource_name]: + eff_rspec[resource_name] -= amount + resid_rspec[resource_name] -= amount + slivers[target]['_rspec'][resource_name] += amount def deliver_record(self, rec): - """A record is simply a dictionary with 'name' and 'timestamp' keys. We keep some persistent private data in the records under keys that start with '_'; thus record updates should not displace such keys.""" + """A record is simply a dictionary with 'name' and 'timestamp' +keys. We keep some persistent private data in the records under keys +that start with '_'; thus record updates should not displace such +keys.""" if rec['timestamp'] < self._min_timestamp: return name = rec['name'] old_rec = self.get(name) @@ -84,13 +100,18 @@ class Database(dict): old_rec.update(rec) def set_min_timestamp(self, ts): - """The ._min_timestamp member is the timestamp on the last comprehensive update. We use it to determine if a record is stale. This method should be called whenever new GetSlivers() data comes in.""" + """The ._min_timestamp member is the timestamp on the last comprehensive update. +We use it to determine if a record is stale. +This method should be called whenever new GetSlivers() data comes in.""" self._min_timestamp = ts for name, rec in self.items(): if rec['timestamp'] < ts: del self[name] def sync(self): - """Synchronize reality with the database contents. This method does a lot of things, and it's currently called after every single batch of database changes (a GetSlivers(), a loan, a record). It may be necessary in the future to do something smarter.""" + """Synchronize reality with the database contents. This +method does a lot of things, and it's currently called after every +single batch of database changes (a GetSlivers(), a loan, a record). +It may be necessary in the future to do something smarter.""" # delete expired records now = time.time() @@ -137,7 +158,9 @@ class Database(dict): def start(): - """The database dumper daemon. When it starts up, it populates the database with the last dumped database. It proceeds to handle dump requests forever.""" + """The database dumper daemon. +When it starts up, it populates the database with the last dumped database. +It proceeds to handle dump requests forever.""" def run(): global dump_requested while True: diff --git a/forward_api_calls.c b/forward_api_calls.c index 330bd3e..98fc01d 100644 --- a/forward_api_calls.c +++ b/forward_api_calls.c @@ -18,7 +18,7 @@ */ static const int TIMEOUT_SECS = 120; -const char *API_addr = "/tmp/sliver_mgr.api"; +const char *API_addr = "/tmp/nodemanager.api"; static const char *Header = "POST / HTTP/1.0\r\n" diff --git a/logger.py b/logger.py index c4f6ce6..b56c0e1 100644 --- a/logger.py +++ b/logger.py @@ -9,8 +9,8 @@ import traceback import subprocess import select -LOG_FILE = '/var/log/nm' -LOG_SLIVERS = '/var/log/getslivers.txt' +LOG_FILE = '/var/log/nodemanager' +LOG_SLIVERS = '/var/lib/nodemanager/getslivers.txt' # Thierry - trying to debug this for 4.2 # basically define 3 levels diff --git a/logrotate/nm b/logrotate/nodemanager similarity index 78% rename from logrotate/nm rename to logrotate/nodemanager index c685711..0ff3627 100644 --- a/logrotate/nm +++ b/logrotate/nodemanager @@ -1,4 +1,4 @@ -/var/log/nm { +/var/log/nodemanager { copytruncate compress daily diff --git a/nm.py b/nm.py index f421769..30ee0b7 100755 --- a/nm.py +++ b/nm.py @@ -188,10 +188,12 @@ def run(): logger.log("nm: Checking Auth.") while plc.check_authentication() != True: try: +# import pdb +# pdb.set_trace() plc.update_session() logger.log("nm: Authentication Failure. Retrying") - except: - logger.log("nm: Retry Failed. Waiting") + except Exception,e: + logger.log("nm: Retry Failed. (%r); Waiting.."%e) time.sleep(iperiod) logger.log("nm: Authentication Succeeded!") diff --git a/plcapi.py b/plcapi.py index 6ed3cac..ca411df 100644 --- a/plcapi.py +++ b/plcapi.py @@ -20,7 +20,7 @@ class PLCAPI: session => SessionAuth To authenticate using the Boot Manager authentication method, or - the new session-based method. + the new session-based method, respectively. """ def __init__(self, uri, cacert, auth, timeout = 90, **kwds): diff --git a/sliver_vs.py b/sliver_vs.py index 4186225..878579d 100644 --- a/sliver_vs.py +++ b/sliver_vs.py @@ -57,7 +57,7 @@ class Sliver_VS(accounts.Account, vserver.VServer): logger.verbose ('sliver_vs: %s init'%name) try: logger.log("sliver_vs: %s: first chance..."%name) - vserver.VServer.__init__(self, name,logfile='/var/log/nm') + vserver.VServer.__init__(self, name,logfile='/var/log/nodemanager') except Exception, err: if not isinstance(err, vserver.NoSuchVServer): # Probably a bad vserver or vserver configuration file @@ -66,7 +66,7 @@ class Sliver_VS(accounts.Account, vserver.VServer): self.destroy(name) self.create(name, rec['vref']) logger.log("sliver_vs: %s: second chance..."%name) - vserver.VServer.__init__(self, name,logfile='/var/log/nm') + vserver.VServer.__init__(self, name,logfile='/var/log/nodemanager') self.keys = '' self.rspec = {} diff --git a/ticket.py b/ticket.py index 0292cfb..aa24b88 100644 --- a/ticket.py +++ b/ticket.py @@ -12,6 +12,10 @@ from xmlrpclib import dumps, loads GPG = '/usr/bin/gpg' +def _popen_gpg(*args): + """Return a Popen object to GPG.""" + return Popen((GPG, '--batch', '--no-tty') + args, + stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) def sign(data): """Return signed with the default GPG key.""" @@ -33,11 +37,9 @@ def verify(signed_msg): msg = p.stdout.read() p.stdout.close() p.stderr.close() - if p.wait(): return None # verification failed + if p.wait(): + return None # verification failed else: data, = loads(msg)[0] return data -def _popen_gpg(*args): - """Return a Popen object to GPG.""" - return Popen((GPG, '--batch', '--no-tty') + args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) diff --git a/tools.py b/tools.py index 80685de..07946bf 100644 --- a/tools.py +++ b/tools.py @@ -13,7 +13,7 @@ import subprocess import logger -PID_FILE = '/var/run/nm.pid' +PID_FILE = '/var/run/nodemanager.pid' #################### def get_default_if(): @@ -64,7 +64,7 @@ def daemon(): devnull = os.open(os.devnull, os.O_RDWR) os.dup2(devnull, 0) # xxx fixme - this is just to make sure that nothing gets stupidly lost - should use devnull - crashlog = os.open('/var/log/nm.daemon', os.O_RDWR | os.O_APPEND | os.O_CREAT, 0644) + crashlog = os.open('/var/log/nodemanager.daemon', os.O_RDWR | os.O_APPEND | os.O_CREAT, 0644) os.dup2(crashlog, 1) os.dup2(crashlog, 2) -- 2.43.0