source: fedd/federation/allocate_project.py @ 5d3f239

axis_examplecompt_changesinfo-opsversion-1.30version-2.00version-3.01version-3.02
Last change on this file since 5d3f239 was 5d3f239, checked in by Ted Faber <faber@…>, 15 years ago

change package name to avoid conflicts with fedd on install

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