Carving away at func some more to just get down to cert items, still lots
[certmaster.git] / certmaster / minion / server.py
diff --git a/certmaster/minion/server.py b/certmaster/minion/server.py
new file mode 100755 (executable)
index 0000000..f1b827f
--- /dev/null
@@ -0,0 +1,285 @@
+"""
+func
+
+Copyright 2007, Red Hat, Inc
+see AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+# standard modules
+import SimpleXMLRPCServer
+import string
+import sys
+import traceback
+import socket
+import fnmatch
+
+from gettext import textdomain
+I18N_DOMAIN = "func"
+
+
+from func.config import read_config
+from func.commonconfig import FuncdConfig
+from func import logger
+from func import certs
+import func.jobthing as jobthing
+import utils
+
+# our modules
+import AuthedXMLRPCServer
+import codes
+import module_loader
+import func.utils as futils
+
+
+
+class XmlRpcInterface(object):
+
+    def __init__(self):
+
+        """
+        Constructor.
+        """
+
+        config_file = '/etc/func/minion.conf'
+        self.config = read_config(config_file, FuncdConfig)
+        self.logger = logger.Logger().logger
+        self.audit_logger = logger.AuditLogger()
+        self.__setup_handlers()
+        
+        # need a reference so we can log ip's, certs, etc
+        # self.server = server
+
+    def __setup_handlers(self):
+
+        """
+        Add RPC functions from each class to the global list so they can be called.
+        """
+
+        self.handlers = {}
+        for x in self.modules.keys():
+            try:
+                self.modules[x].register_rpc(self.handlers, x)
+                self.logger.debug("adding %s" % x)
+            except AttributeError, e:
+                self.logger.warning("module %s not loaded, missing register_rpc method" % self.modules[x])
+
+
+        # internal methods that we do instead of spreading internal goo
+        # all over the modules. For now, at lest -akl
+
+
+        # system.listMethods os a quasi stanard xmlrpc method, so
+        # thats why it has a odd looking name
+        self.handlers["system.listMethods"] = self.list_methods
+        self.handlers["system.list_methods"] = self.list_methods
+        self.handlers["system.list_modules"] = self.list_modules
+
+    def list_modules(self):
+        modules = self.modules.keys()
+        modules.sort()
+        return modules
+
+    def list_methods(self):
+        methods = self.handlers.keys()
+        methods.sort()
+        return methods
+
+    def get_dispatch_method(self, method):
+
+        if method in self.handlers:
+            return FuncApiMethod(self.logger, method, self.handlers[method])
+
+        else:
+            self.logger.info("Unhandled method call for method: %s " % method)
+            raise codes.InvalidMethodException
+
+
+class FuncApiMethod:
+
+    """
+    Used to hold a reference to all of the registered functions.
+    """
+
+    def __init__(self, logger, name, method):
+
+        self.logger = logger
+        self.__method = method
+        self.__name = name
+
+    def __log_exc(self):
+
+        """
+        Log an exception.
+        """
+
+        (t, v, tb) = sys.exc_info()
+        self.logger.info("Exception occured: %s" % t )
+        self.logger.info("Exception value: %s" % v)
+        self.logger.info("Exception Info:\n%s" % string.join(traceback.format_list(traceback.extract_tb(tb))))
+
+    def __call__(self, *args):
+
+        self.logger.debug("(X) -------------------------------------------")
+
+        try:
+            rc = self.__method(*args)
+        except codes.FuncException, e:
+            self.__log_exc()
+            (t, v, tb) = sys.exc_info()
+            rc = futils.nice_exception(t,v,tb)
+        except:
+            self.__log_exc()
+            (t, v, tb) = sys.exc_info()
+            rc = futils.nice_exception(t,v,tb)
+        self.logger.debug("Return code for %s: %s" % (self.__name, rc))
+
+        return rc
+
+
+def serve():
+
+    """
+    Code for starting the XMLRPC service.
+    """
+    server =FuncSSLXMLRPCServer(('', 51234))
+    server.logRequests = 0 # don't print stuff to console
+    server.serve_forever()
+
+
+
+class FuncXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer, XmlRpcInterface):
+
+    def __init__(self, args):
+
+        self.allow_reuse_address = True
+
+        self.modules = module_loader.load_modules()
+        SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, args)
+        XmlRpcInterface.__init__(self)
+
+
+class FuncSSLXMLRPCServer(AuthedXMLRPCServer.AuthedSSLXMLRPCServer,
+                          XmlRpcInterface):
+    def __init__(self, args):
+        self.allow_reuse_address = True
+        self.modules = module_loader.load_modules()
+
+        XmlRpcInterface.__init__(self)
+        hn = utils.get_hostname()
+        self.key = "%s/%s.pem" % (self.config.cert_dir, hn)
+        self.cert = "%s/%s.cert" % (self.config.cert_dir, hn)
+        self.ca = "%s/ca.cert" % self.config.cert_dir
+        
+        self._our_ca = certs.retrieve_cert_from_file(self.ca)
+        
+        AuthedXMLRPCServer.AuthedSSLXMLRPCServer.__init__(self, ("", 51234),
+                                                          self.key, self.cert,
+                                                          self.ca)
+
+    def _dispatch(self, method, params):
+
+        """
+        the SimpleXMLRPCServer class will call _dispatch if it doesn't
+        find a handler method
+        """
+        # take _this_request and hand it off to check out the acls of the method
+        # being called vs the requesting host
+        
+        if not hasattr(self, '_this_request'):
+            raise codes.InvalidMethodException
+            
+        r,a = self._this_request
+        peer_cert = r.get_peer_certificate()
+        ip = a[0]
+        
+
+        # generally calling conventions are:  hardware.info
+        # async convention is async.hardware.info
+        # here we parse out the async to decide how to invoke it.
+        # see the async docs on the Wiki for further info.
+        async_dispatch = False
+        if method.startswith("async."):
+            async_dispatch = True
+            method = method.replace("async.","",1)
+
+        if not self._check_acl(peer_cert, ip, method, params):
+            raise codes.AccessToMethodDenied
+            
+        # Recognize ipython's tab completion calls
+        if method == 'trait_names' or method == '_getAttributeNames':
+            return self.handlers.keys()
+
+        cn = peer_cert.get_subject().CN
+        sub_hash = peer_cert.subject_name_hash()
+        self.audit_logger.log_call(ip, cn, sub_hash, method, params)
+
+        try:
+            if not async_dispatch:
+                return self.get_dispatch_method(method)(*params)
+            else:
+                return jobthing.minion_async_run(self.get_dispatch_method, method, params)
+        except:
+            (t, v, tb) = sys.exc_info()
+            rc = futils.nice_exception(t, v, tb)
+            return rc
+
+    def auth_cb(self, request, client_address):
+        peer_cert = request.get_peer_certificate()
+        return peer_cert.get_subject().CN
+    
+    def _check_acl(self, cert, ip, method, params):
+        acls = utils.get_acls_from_config(acldir=self.config.acl_dir)
+        
+        # certmaster always gets to run things
+        ca_cn = self._our_ca.get_subject().CN
+        ca_hash = self._our_ca.subject_name_hash()
+        ca_key = '%s-%s' % (ca_cn, ca_hash)
+        acls[ca_key] = ['*']
+
+        cn = cert.get_subject().CN
+        sub_hash = cert.subject_name_hash()
+        if acls:
+            allow_list = []
+            hostkey = '%s-%s' % (cn, sub_hash)
+            # search all the keys, match to 'cn-subhash'
+            for hostmatch in acls.keys():
+                if fnmatch.fnmatch(hostkey, hostmatch):
+                    allow_list.extend(acls[hostmatch])
+            # go through the allow_list and make sure this method is in there
+            for methodmatch in allow_list:
+                if fnmatch.fnmatch(method, methodmatch):
+                    return True
+                    
+        return False
+
+
+def main(argv):
+
+    """
+    Start things up.
+    """
+
+    if "daemon" in sys.argv or "--daemon" in sys.argv:
+        futils.daemonize("/var/run/funcd.pid")
+    else:
+        print "serving...\n"
+
+    try:
+        utils.create_minion_keys()
+        serve()
+    except codes.FuncException, e:
+        print >> sys.stderr, 'error: %s' % e
+        sys.exit(1)
+
+
+# ======================================================================================
+if __name__ == "__main__":
+    textdomain(I18N_DOMAIN)
+    main(sys.argv)