source: fedd/fedd_allocate_project.py @ 0a20ef8

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

Explicit allocation levels for the different knods of allocations.

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