source: fedd/fedd_allocate_project.py @ 0b123ff

axis_examplecompt_changesinfo-opsversion-1.30version-2.00version-3.01version-3.02
Last change on this file since 0b123ff was 3f6bc5f, checked in by Ted Faber <faber@…>, 16 years ago

Initial move to general authorization framework. Currently integrated with Access stuff fully.

  • Property mode set to 100644
File size: 13.9 KB
RevLine 
[7da9da6]1#!/usr/local/bin/python
2
3import os,sys
4import re
5import random
6import string
7import subprocess
8import tempfile
9
10from fedd_services import *
[7aec37d]11from fedd_internal_services import *
[7da9da6]12from fedd_util import *
[51cc9df]13from fedid import fedid
[f8582c9]14from fixed_resource import read_key_db, read_project_db
[9460b1e]15from remote_service import xmlrpc_handler, soap_handler, service_caller
[ef36c1e]16from service_error import *
[11a08b0]17import logging
18
19
[0ea11af]20# Configure loggers to dump to /dev/null which avoids errors if calling classes
21# don't configure them.
[11a08b0]22class nullHandler(logging.Handler):
23    def emit(self, record): pass
24
25fl = logging.getLogger("fedd.allocate.local")
26fl.addHandler(nullHandler())
27fl = logging.getLogger("fedd.allocate.remote")
28fl.addHandler(nullHandler())
[7da9da6]29
[4ed10ae]30
[7da9da6]31class fedd_allocate_project_local:
[0ea11af]32    """
33    Allocate projects on this machine in response to an access request.
34    """
[3f6bc5f]35    def __init__(self, config, auth=None):
[7da9da6]36        """
37        Initializer.  Parses a configuration if one is given.
38        """
39
[4ed10ae]40        self.debug = config.get("access", "debug_project", False)
41        self.wap = config.get('access', 'wap', '/usr/testbed/sbin/wap')
42        self.newproj = config.get('access', 'newproj',
43                '/usr/testbed/sbin/newproj')
44        self.mkproj = config.get('access', 'mkproj', '/usr/testbed/sbin/mkproj')
[7583a62]45        self.rmproj = config.get('access', 'rmproj', '/usr/testbed/sbin/rmproj')
[4ed10ae]46        self.addpubkey = config.get('access', 'addpubkey', 
47                '/usr/testbed/sbin/taddpubkey')
48        self.grantnodetype = config.get('access', 'grantnodetype', 
49                '/usr/testbed/sbin/grantnodetype')
[11a08b0]50        self.log = logging.getLogger("fedd.allocate.local")
[4ed10ae]51        set_log_level(config, "access", self.log)
[7583a62]52        fixed_key_db = config.get("access", "fixed_keys", None)
53        fixed_project_db = config.get("access", "fixed_projects", None)
[f8582c9]54        self.fixed_keys = set()
55        self.fixed_projects = set()
56
57        # initialize the fixed resource sets
58        for db, rset, fcn in (\
59                (fixed_key_db, self.fixed_keys, read_key_db), \
60                (fixed_project_db, self.fixed_projects, read_project_db)):
61            if db:
62                try:
63                    rset.update(fcn(db))
64                except:
65                    self.log.debug("Can't read resources from %s" % db)
66       
[0ea11af]67        # Internal services are SOAP only
68        self.soap_services = {\
[058f58e]69                "AllocateProject": soap_handler(\
[0ea11af]70                AllocateProjectRequestMessage.typecode,\
71                self.dynamic_project, AllocateProjectResponseMessage,\
[4ed10ae]72                "AllocateProjectResponseBody"), 
[058f58e]73                "StaticProject": soap_handler(\
[4ed10ae]74                StaticProjectRequestMessage.typecode,\
75                self.static_project, StaticProjectResponseMessage,\
[7583a62]76                "StaticProjectResponseBody"),\
[058f58e]77                "ReleaseProject": soap_handler(\
[f8582c9]78                ReleaseProjectRequestMessage.typecode,\
79                self.release_project, ReleaseProjectResponseMessage,\
80                "ReleaseProjectResponseBody")\
[0ea11af]81                }
82        self.xmlrpc_services = { }
83
[7da9da6]84    def random_string(self, s, n=3):
85        """Append n random ASCII characters to s and return the string"""
86        rv = s
87        for i in range(0,n):
88            rv += random.choice(string.ascii_letters)
89        return rv
90
91    def write_attr_xml(self, file, root, lines):
92        """
93        Write an emulab config file for a dynamic project.
94
95        Format is <root><attribute name=lines[0]>lines[1]</attribute></root>
96        """
97        # Convert a pair to an attribute line
98        out_attr = lambda a,v : \
99                '<attribute name="%s"><value>%s</value></attribute>' % (a, v)
100
101        f = os.fdopen(file, "w")
102        f.write("<%s>\n" % root)
103        f.write("\n".join([out_attr(*l) for l in lines]))
104        f.write("</%s>\n" % root)
105        f.close()
106
107
[ef36c1e]108    def dynamic_project(self, req, fedid=None):
[7da9da6]109        """
110        Create a dynamic project with ssh access
111
112        Req includes the project and resources as a dictionary
113        """
114        # tempfiles for the parameter files
115        uf, userfile = tempfile.mkstemp(prefix="usr", suffix=".xml",
116                dir="/tmp")
117        pf, projfile = tempfile.mkstemp(prefix="proj", suffix=".xml",
118                dir="/tmp")
119
[ef36c1e]120        if req.has_key('AllocateProjectRequestBody') and \
121                req['AllocateProjectRequestBody'].has_key('project'):
122            proj = req['AllocateProjectRequestBody']['project']
123        else:
124            raise service_error(service_error.req, 
125                    "Badly formed allocation request")
[7da9da6]126        # Take the first user and ssh key
127        name = proj.get('name', None) or self.random_string("proj",4)
128        user = proj.get('user', None)
129        if user != None:
130            user = user[0]      # User is a list, take the first entry
131            if not user.has_key("userID"):
132                uname = self.random_string("user", 3)
133            else:
134                uid = proj['userID']
135                # XXX: fedid
[e40c7ee]136                uname = uid.get('localname', None) or \
[7da9da6]137                        uid.get('kerberosUsername', None) or \
138                        uid.get('uri', None)
139                if uname == None:
140                    raise fedd_proj.service_error(fedd_proj.service_error.req, 
141                            "No ID for user");
142
143            access = user.get('access', None)
144            if access != None:
[ef36c1e]145                ssh = access[0].get('sshPubkey', None)
[7da9da6]146                if ssh == None:
147                    raise fedd_proj.service_error(fedd_proj.service_error.req, 
148                            "No ssh key for user");
149        else:
150            raise fedd_proj.service_error(fedd_proj.service_error.req, 
151                    "No access information for project");
152
153        # uname, name and ssh are set
154        user_fields = [
155                ("name", "Federation User %s" % uname),
156                ("email", "%s-fed@isi.deterlab.net" % uname),
157                ("password", self.random_string("", 8)),
158                ("login", uname),
159                ("address", "4676 Admiralty"),
160                ("city", "Marina del Rey"),
161                ("state", "CA"),
162                ("zip", "90292"),
163                ("country", "USA"),
164                ("phone", "310-448-9190"),
165                ("title", "None"),
166                ("affiliation", "USC/ISI"),
167                ("pubkey", ssh)
168        ]
169
170        proj_fields = [
171                ("name", name),
172                ("short description", "dynamic federated project"),
173                ("URL", "http://www.isi.edu/~faber"),
174                ("funders", "USC/USU"),
175                ("long description", "Federation access control"),
176                ("public", "1"),
177                ("num_pcs", "100"),
178                ("linkedtous", "1"),
179                ("newuser_xml", userfile)
180        ]
181       
182
183        # Write out the files
184        self.write_attr_xml(uf, "user", user_fields)
185        self.write_attr_xml(pf, "project", proj_fields)
186
187        # Generate the commands (only grantnodetype's are dynamic)
188        cmds = [
189                (self.wap, self.newproj, projfile),
190                (self.wap, self.mkproj, name)
191                ]
192
193        # Add commands to grant access to any resources in the request.
194        for nt in [ h for r in req.get('resources', []) \
195                if r.has_key('node') and r['node'].has_key('hardware')\
196                    for h in r['node']['hardware'] ] :
197            cmds.append((self.wap, self.grantnodetype, '-p', name, nt))
198
199        # Create the projects
200        rc = 0
201        for cmd in cmds:
[11a08b0]202            self.log.debug("[dynamic_project]: %s" % ' '.join(cmd))
[4ed10ae]203            if not self.debug:
[7da9da6]204                try:
205                    rc = subprocess.call(cmd)
206                except OSerror, e:
[4ed10ae]207                    raise service_error(service_error.internal,
[7da9da6]208                            "Dynamic project subprocess creation error "+ \
209                                    "[%s] (%s)" %  (cmd[1], e.strerror))
210
211            if rc != 0: 
[4ed10ae]212                raise service_error(service_error.internal,
[7da9da6]213                        "Dynamic project subprocess error " +\
214                                "[%s] (%d)" % (cmd[1], rc))
215        # Clean up tempfiles
216        os.unlink(userfile)
217        os.unlink(projfile)
218        rv = {\
219            'project': {\
[e40c7ee]220                'name': { 'localname': name }, 
[7da9da6]221                'user' : [ {\
[e40c7ee]222                    'userID': { 'localname' : uname },
[7da9da6]223                    'access': [ { 'sshPubkey' : ssh } ],
224                } ]\
225            }\
226        }
227        return rv
[ef36c1e]228
[4ed10ae]229    def static_project(self, req, fedid=None):
[ef36c1e]230        """
[4ed10ae]231        Be certain that the local project in the request has access to the
232        proper resources and users have correct keys.  Add them if necessary.
[ef36c1e]233        """
234
[4ed10ae]235        cmds =  []
[ef36c1e]236
[4ed10ae]237        # While we should be more careful about this, for the short term, add
238        # the keys to the specified users.
239
240        try:
241            users = req['StaticProjectRequestBody']['project']['user']
242            pname = req['StaticProjectRequestBody']['project']\
243                    ['name']['localname']
244            resources = req['StaticProjectRequestBody'].get('resources', [])
245        except KeyError:
246            raise service_error(service_error.req, "Badly formed request")
247
248
249        for u in users:
250            try:
251                name = u['userID']['localname']
252            except KeyError:
253                raise service_error(service_error.req, "Badly formed user")
254            for sk in [ k['sshPubkey'] for k in u.get('access', []) \
255                    if k.has_key('sshPubkey')]:
[7583a62]256                cmds.append((self.wap, self.addpubkey, '-w', \
257                        '-u', name, '-k', sk))
[0ea11af]258       
[4ed10ae]259
260        # Add commands to grant access to any resources in the request.  The
261        # list comprehension pulls out the hardware types in the node entries
262        # in the resources list.
263        for nt in [ h for r in resources \
264                if r.has_key('node') and r['node'].has_key('hardware')\
265                    for h in r['node']['hardware'] ] :
266            cmds.append((self.wap, self.grantnodetype, '-p', pname, nt))
267
268        # Run the commands
269        rc = 0
270        for cmd in cmds:
271            self.log.debug("[static_project]: %s" % ' '.join(cmd))
272            if not self.debug:
273                try:
274                    rc = subprocess.call(cmd)
275                except OSError, e:
276                    raise service_error(service_error.internal,
277                            "Static project subprocess creation error "+ \
278                                    "[%s] (%s)" %  (cmd[0], e.strerror))
279
280            if rc != 0: 
281                raise service_error(service_error.internal,
282                        "Static project subprocess error " +\
283                                "[%s] (%d)" % (cmd[0], rc))
284
285        return { 'project': req['StaticProjectRequestBody']['project']}
286
[7583a62]287    def release_project(self, req, fedid=None):
288        """
289        Remove user keys from users and delete dynamic projects.
290
291        Only keys not in the set of fixed keys are deleted. and there are
292        similar protections for projects.
293        """
294
295        cmds = []
296        pname = None
297        users = []
298
299        try:
300            if req['ReleaseProjectRequestBody']['project'].has_key('name'):
301                pname = req['ReleaseProjectRequestBody']['project']\
302                        ['name']['localname']
303            if req['ReleaseProjectRequestBody']['project'].has_key('user'):
304                users = req['ReleaseProjectRequestBody']['project']['user']
305        except KeyError:
306            raise service_error(service_error.req, "Badly formed request")
307
308        for u in users:
309            try:
310                name = u['userID']['localname']
311            except KeyError:
312                raise service_error(service_error.req, "Badly formed user")
313            for sk in [ k['sshPubkey'] for k in u.get('access', []) \
314                    if k.has_key('sshPubkey')]:
[f8582c9]315                if (name.rstrip(), sk.rstrip()) not in self.fixed_keys:
[7583a62]316                    cmds.append((self.wap, self.addpubkey, '-R', '-w', \
317                            '-u', name, '-k', sk))
[f8582c9]318        if pname and pname not in self.fixed_projects:
[7583a62]319            cmds.append((self.wap, self.rmproj, pname))
320
321        # Run the commands
322        rc = 0
323        for cmd in cmds:
324            self.log.debug("[release_project]: %s" % ' '.join(cmd))
325            if not self.debug:
326                try:
327                    rc = subprocess.call(cmd)
328                except OSError, e:
329                    raise service_error(service_error.internal,
330                            "Release project subprocess creation error "+ \
331                                    "[%s] (%s)" %  (cmd[0], e.strerror))
332
333            if rc != 0: 
334                raise service_error(service_error.internal,
335                        "Release project subprocess error " +\
336                                "[%s] (%d)" % (cmd[0], rc))
337
338        return { 'project': req['ReleaseProjectRequestBody']['project']}
339
[058f58e]340class fedd_allocate_project_remote:
[4ed10ae]341    """
[058f58e]342    Allocate projects on a remote machine using the internal SOAP interface
[4ed10ae]343    """
[058f58e]344    class proxy(service_caller):
[ef36c1e]345        """
[058f58e]346        This class is a proxy functor (callable) that has the same signature as
347        a function called by soap_handler or xmlrpc_handler, but that used the
348        service_caller class to call the function remotely.
[ef36c1e]349        """
[4ed10ae]350
[058f58e]351        def __init__(self, url, cert_file, cert_pwd, trusted_certs, 
352                method, req_name, req_alloc, resp_name):
353            service_caller.__init__(self, method, 'getfeddInternalPortType',
354                    feddInternalServiceLocator, req_alloc, req_name)
355            self.url = url
356            self.cert_file = cert_file
357            self.cert_pwd = cert_pwd
358            self.trusted_certs = trusted_certs
359            self.resp_name = resp_name
360            # Calling the proxy object directly invokes the proxy_call method,
361            # not the service_call method.
362            self.__call__ = self.proxy_call
363           
364
365        # Define the proxy, NB, the parameters to make_proxy are visible to the
366        # definition of proxy.
367        def proxy_call(self, req, fedid=None):
368            """
369            Send req on to a remote project instantiator.
370
371            Req is just the message to be sent.  This function re-wraps it.
372            It also rethrows any faults.
373            """
374
375            if req.has_key(self.request_body_name):
376                req = req[self.request_body_name]
377            else:
378                raise service_error(service_error.req, "Bad formated request");
[ef36c1e]379
[058f58e]380            r = self.call_service(self.url, req, self.cert_file, self.cert_pwd,
381                    self.trusted_certs)
382            if r.has_key(self.resp_name):
383                return r[self.resp_name]
384            else:
385                raise service_error(service_error.protocol, 
386                        "Bad proxy response")
[4ed10ae]387
[058f58e]388    # back to defining the fedd_allocate_project_remote class
[3f6bc5f]389    def __init__(self, config, auth=None):
[4ed10ae]390        """
391        Initializer.  Parses a configuration if one is given.
392        """
393
394        self.debug = config.get("access", "debug_project", False)
395        self.url = config.get("access", "dynamic_projects_url", "")
396
397        self.cert_file = config.get("access", "cert_file", None)
398        self.cert_pwd = config.get("access", "cert_pwd", None)
399        self.trusted_certs = config.get("access", "trusted_certs", None)
400
401        # Certs are promoted from the generic to the specific, so without a if
402        # no dynamic project certificates, then proxy certs are used, and if
403        # none of those the main certs.
404
405        if config.has_option("globals", "proxy_cert_file"):
406            if not self.cert_file:
407                self.cert_file = config.get("globals", "proxy_cert_file")
408                if config.has_option("globals", "porxy_cert_pwd"):
409                    self.cert_pwd = config.get("globals", "proxy_cert_pwd")
410
411        if config.has_option("globals", "proxy_trusted_certs") and \
412                not self.trusted_certs:
413                self.trusted_certs = \
414                        config.get("globals", "proxy_trusted_certs")
415
416        if config.has_option("globals", "cert_file"):
417            has_pwd = config.has_option("globals", "cert_pwd")
418            if not self.cert_file:
419                self.cert_file = config.get("globals", "cert_file")
420                if has_pwd: 
421                    self.cert_pwd = config.get("globals", "cert_pwd")
422
423        if config.get("globals", "trusted_certs") and not self.trusted_certs:
424                self.trusted_certs = \
425                        config.get("globals", "trusted_certs")
426
427        self.soap_services = { }
428        self.xmlrpc_services = { }
429        self.log = logging.getLogger("fedd.allocate.remote")
430        set_log_level(config, "access", self.log)
[058f58e]431        # The specializations of the proxy functions
432        self.dynamic_project = self.proxy(self.url, self.cert_file, 
433                self.cert_pwd, self.trusted_certs, "AllocateProject",
434                "AllocateProjectRequestBody", AllocateProjectRequestMessage,
435                "AllocateProjectResponseBody")
436        self.static_project = self.proxy(self.url, self.cert_file, 
437                self.cert_pwd, self.trusted_certs, "StaticProject",
438                "StaticProjectRequestBody", StaticProjectRequestMessage,
439                "StaticProjectResponseBody")
440        self.release_project = self.proxy(self.url, self.cert_file,
441                self.cert_pwd, self.trusted_certs, "ReleaseProject",
442                "ReleaseProjectRequestBody", ReleaseProjectRequestMessage,
443                "ReleaseProjectResponseBody")
444
Note: See TracBrowser for help on using the repository browser.