First version. Most definitely a work in progress.
[nodemanager.git] / api.py
diff --git a/api.py b/api.py
new file mode 100644 (file)
index 0000000..73f38e7
--- /dev/null
+++ b/api.py
@@ -0,0 +1,156 @@
+import SimpleXMLRPCServer
+import SocketServer
+import cPickle
+import errno
+import os
+import pwd
+import socket
+import struct
+import threading
+import xmlrpclib
+
+from config import *
+import accounts
+import database
+import logger
+import tools
+
+
+api_method_dict = {}
+nargs_dict = {}
+
+def export_to_api(nargs):
+    def export(method):
+        nargs_dict[method.__name__] = nargs
+        api_method_dict[method.__name__] = method
+        return method
+    return export
+
+@export_to_api(0)
+def DumpDatabase():
+    """DumpDatabase(): return the entire node manager DB, pickled"""
+    return cPickle.dumps(dict(database._db), 0)
+
+@export_to_api(0)
+def Help():
+    """Help(): get help"""
+    return ''.join([method.__doc__ + '\n' for method in api_method_dict.itervalues()])
+
+@export_to_api(1)
+def CreateSliver(rec):
+    """CreateSliver(sliver_name): set up a non-PLC-instantiated sliver"""
+    if not rec['plc_instantiated']:
+        accounts.get(rec['name']).ensure_created(rec)
+
+@export_to_api(1)
+def DeleteSliver(rec):
+    """DeleteSliver(sliver_name): tear down a non-PLC-instantiated sliver"""
+    if not rec['plc_instantiated']:
+        accounts.get(rec['name']).ensure_destroyed()
+
+@export_to_api(1)
+def Start(rec):
+    """Start(sliver_name): run start scripts belonging to the specified sliver"""
+    accounts.get(rec['name']).start()
+
+@export_to_api(1)
+def Stop(rec):
+    """Stop(sliver_name): kill all processes belonging to the specified sliver"""
+    accounts.get(rec['name']).stop()
+
+@export_to_api(1)
+def GetEffectiveRSpec(rec):
+    """GetEffectiveRSpec(sliver_name): return the RSpec allocated to the specified sliver, including loans"""
+    return tools.deepcopy(rec.get('eff_rspec', {}))
+
+@export_to_api(1)
+def GetRSpec(rec):
+    """GetRSpec(sliver_name): return the RSpec allocated to the specified sliver, excluding loans"""
+    return tools.deepcopy(rec.get('rspec', {}))
+
+@export_to_api(1)
+def GetLoans(rec):
+    """GetLoans(sliver_name): return the list of loans made by the specified sliver"""
+    return tools.deepcopy(rec.get('loans', []))
+
+def validate_loans(obj):
+    """Check that <obj> 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 LOANABLE_RESOURCES and \
+               type(obj[2])==int and obj[2]>0
+    return type(obj)==list and False not in map(validate_loan, obj)
+
+@export_to_api(2)
+def SetLoans(rec, loans):
+    """SetLoans(sliver_name, loans): overwrite the list of loans made by the specified sliver"""
+    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.deliver_records([rec])
+
+api_method_list = api_method_dict.keys()
+api_method_list.sort()
+
+
+class APIRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
+    # overriding _dispatch to achieve this effect is officially deprecated,
+    # but I can't figure out how to get access to .request
+    # without duplicating SimpleXMLRPCServer code here,
+    # which is more likely to change than the deprecated behavior
+    # is to be broken
+
+    @database.synchronized
+    def _dispatch(self, method_name, args):
+        method_name = str(method_name)
+        try: method = api_method_dict[method_name]
+        except KeyError:
+            raise xmlrpclib.Fault(100, 'Invalid API method %s.  Valid choices are %s' % (method_name, ', '.join(api_method_list)))
+
+        expected_nargs = nargs_dict[method_name]
+        if len(args) != nargs_dict[method_name]:
+            raise xmlrpclib.Fault(101, 'Invalid argument count: got %d, expecting %d.' % (len(args), expected_nargs))
+        else:
+            # XXX - these ought to be imported directly from some .h file
+            SO_PEERCRED = 17
+            sizeof_struct_ucred = 12
+            ucred = self.request.getsockopt(socket.SOL_SOCKET, SO_PEERCRED,
+                                            sizeof_struct_ucred)
+            xid = struct.unpack('3i', ucred)[2]
+            caller_name = pwd.getpwuid(xid)[0]
+
+            if expected_nargs >= 1:
+                target_name = args[0]
+                target_rec = database.get_sliver(target_name)
+                if not target_rec: raise xmlrpclib.Fault(102, 'Invalid argument: the first argument must be a sliver name.')
+
+                if caller_name not in (args[0], 'root') and \
+                       (caller_name, method_name) not in target_rec['delegations']:
+                    raise xmlrpclib.Fault(108, 'Permission denied.')
+                result = method(target_rec, *args[1:])
+            else:
+                if method_name == 'DumpDatabase' and caller_name != 'root':
+                    raise xmlrpclib.Fault(108, 'Permission denied.')
+                result = method()
+            if result == None: result = 1
+            return result
+
+class APIServer_INET(SocketServer.ThreadingMixIn,
+                     SimpleXMLRPCServer.SimpleXMLRPCServer):
+    allow_reuse_address = True
+
+class APIServer_UNIX(APIServer_INET): address_family = socket.AF_UNIX
+
+def start():
+    serv1 = APIServer_INET(('127.0.0.1', API_SERVER_PORT),
+                           requestHandler=APIRequestHandler, logRequests=0)
+    tools.as_daemon_thread(serv1.serve_forever)
+    unix_addr = '/tmp/node_mgr_api'
+    try: os.unlink(unix_addr)
+    except OSError, e:
+        if e.errno != errno.ENOENT: raise
+    serv2 = APIServer_UNIX(unix_addr,
+                           requestHandler=APIRequestHandler, logRequests=0)
+    tools.as_daemon_thread(serv2.serve_forever)
+    os.chmod(unix_addr, 0666)