source: fedd/fedd/allocate_project.py @ ec4fb42

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

Clean up some names that start with fedd_ that are ugly with the new package
structure. A couple other bugs cleaned up, too.

  • Property mode set to 100644
File size: 15.1 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 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 service_error
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 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 service_error(service_error.req, "No ID for user");
171
172            access = user.get('access', None)
173            if access != None:
174                ssh = access[0].get('sshPubkey', None)
175                if ssh == None:
176                    raise service_error(service_error.req, 
177                            "No ssh key for user");
178        else:
179            raise service_error(service_error.req, 
180                    "No access information for project");
181
182        # uname, name and ssh are set
183        user_fields = [
184                ("name", "Federation User %s" % uname),
185                ("email", "%s-fed@isi.deterlab.net" % uname),
186                ("password", self.random_string("", 8)),
187                ("login", uname),
188                ("address", "4676 Admiralty"),
189                ("city", "Marina del Rey"),
190                ("state", "CA"),
191                ("zip", "90292"),
192                ("country", "USA"),
193                ("phone", "310-448-9190"),
194                ("title", "None"),
195                ("affiliation", "USC/ISI"),
196                ("pubkey", ssh)
197        ]
198
199        proj_fields = [
200                ("name", name),
201                ("short description", "dynamic federated project"),
202                ("URL", "http://www.isi.edu/~faber"),
203                ("funders", "USC/USU"),
204                ("long description", "Federation access control"),
205                ("public", "1"),
206                ("num_pcs", "100"),
207                ("linkedtous", "1"),
208                ("newuser_xml", userfile)
209        ]
210       
211
212        # Write out the files
213        self.write_attr_xml(uf, "user", user_fields)
214        self.write_attr_xml(pf, "project", proj_fields)
215
216        # Generate the commands (only grantnodetype's are dynamic)
217        cmds = [
218                (self.wap, self.newproj, projfile),
219                (self.wap, self.mkproj, name)
220                ]
221
222        # Add commands to grant access to any resources in the request.
223        for nt in [ h for r in req.get('resources', []) \
224                if r.has_key('node') and r['node'].has_key('hardware')\
225                    for h in r['node']['hardware'] ] :
226            cmds.append((self.wap, self.grantnodetype, '-p', name, nt))
227
228        # Create the projects
229        rc = 0
230        for cmd in cmds:
231            self.log.debug("[dynamic_project]: %s" % ' '.join(cmd))
232            if not self.debug:
233                try:
234                    rc = subprocess.call(cmd)
235                except OSerror, e:
236                    raise service_error(service_error.internal,
237                            "Dynamic project subprocess creation error "+ \
238                                    "[%s] (%s)" %  (cmd[1], e.strerror))
239
240            if rc != 0: 
241                raise service_error(service_error.internal,
242                        "Dynamic project subprocess error " +\
243                                "[%s] (%d)" % (cmd[1], rc))
244        # Clean up tempfiles
245        os.unlink(userfile)
246        os.unlink(projfile)
247        rv = {\
248            'project': {\
249                'name': { 'localname': name }, 
250                'user' : [ {\
251                    'userID': { 'localname' : uname },
252                    'access': [ { 'sshPubkey' : ssh } ],
253                } ]\
254            }\
255        }
256        return rv
257
258    def static_project(self, req, fedid=None):
259        """
260        Be certain that the local project in the request has access to the
261        proper resources and users have correct keys.  Add them if necessary.
262        """
263
264        cmds =  []
265
266        # While we should be more careful about this, for the short term, add
267        # the keys to the specified users.
268
269        try:
270            users = req['StaticProjectRequestBody']['project']['user']
271            pname = req['StaticProjectRequestBody']['project']\
272                    ['name']['localname']
273            resources = req['StaticProjectRequestBody'].get('resources', [])
274        except KeyError:
275            raise service_error(service_error.req, "Badly formed request")
276
277
278        for u in users:
279            try:
280                name = u['userID']['localname']
281            except KeyError:
282                raise service_error(service_error.req, "Badly formed user")
283            for sk in [ k['sshPubkey'] for k in u.get('access', []) \
284                    if k.has_key('sshPubkey')]:
285                if self.allocation_level >= self.dynamic_keys:
286                    cmds.append((self.wap, self.addpubkey, '-w', \
287                            '-u', name, '-k', sk))
288                elif self.allocation_level >= self.confirm_keys:
289                    cmds.append((self.wap, self.confirmkey, '-C', \
290                            '-u', name, '-k', sk))
291                else:
292                    self.log.warning("[static_project] no checking of " + \
293                            "static keys")
294       
295
296        # Add commands to grant access to any resources in the request.  The
297        # list comprehension pulls out the hardware types in the node entries
298        # in the resources list.
299        for nt in [ h for r in resources \
300                if r.has_key('node') and r['node'].has_key('hardware')\
301                    for h in r['node']['hardware'] ] :
302            if self.allocation_level >= self.confirm_keys:
303                cmds.append((self.wap, self.grantnodetype, '-p', pname, nt))
304
305        # Run the commands
306        rc = 0
307        for cmd in cmds:
308            self.log.debug("[static_project]: %s" % ' '.join(cmd))
309            if not self.debug:
310                try:
311                    rc = subprocess.call(cmd)
312                except OSError, e:
313                    raise service_error(service_error.internal,
314                            "Static project subprocess creation error "+ \
315                                    "[%s] (%s)" %  (cmd[0], e.strerror))
316
317            if rc != 0: 
318                raise service_error(service_error.internal,
319                        "Static project subprocess error " +\
320                                "[%s] (%d)" % (cmd[0], rc))
321
322        return { 'project': req['StaticProjectRequestBody']['project']}
323
324    def release_project(self, req, fedid=None):
325        """
326        Remove user keys from users and delete dynamic projects.
327
328        Only keys not in the set of fixed keys are deleted. and there are
329        similar protections for projects.
330        """
331
332        cmds = []
333        pname = None
334        users = []
335
336        try:
337            if req['ReleaseProjectRequestBody']['project'].has_key('name'):
338                pname = req['ReleaseProjectRequestBody']['project']\
339                        ['name']['localname']
340            if req['ReleaseProjectRequestBody']['project'].has_key('user'):
341                users = req['ReleaseProjectRequestBody']['project']['user']
342        except KeyError:
343            raise service_error(service_error.req, "Badly formed request")
344
345        for u in users:
346            try:
347                name = u['userID']['localname']
348            except KeyError:
349                raise service_error(service_error.req, "Badly formed user")
350            for sk in [ k['sshPubkey'] for k in u.get('access', []) \
351                    if k.has_key('sshPubkey')]:
352                if (name.rstrip(), sk.rstrip()) not in self.fixed_keys:
353                    if self.allocation_level >= self.dynamic_keys:
354                        cmds.append((self.wap, self.addpubkey, '-R', '-w', \
355                                '-u', name, '-k', sk))
356        if pname and pname not in self.fixed_projects and \
357                self.allocation_level >= self.dynamic_projects:
358            cmds.append((self.wap, self.rmproj, pname))
359
360        # Run the commands
361        rc = 0
362        for cmd in cmds:
363            self.log.debug("[release_project]: %s" % ' '.join(cmd))
364            if not self.debug:
365                try:
366                    rc = subprocess.call(cmd)
367                except OSError, e:
368                    raise service_error(service_error.internal,
369                            "Release project subprocess creation error "+ \
370                                    "[%s] (%s)" %  (cmd[0], e.strerror))
371
372            if rc != 0: 
373                raise service_error(service_error.internal,
374                        "Release project subprocess error " +\
375                                "[%s] (%d)" % (cmd[0], rc))
376
377        return { 'project': req['ReleaseProjectRequestBody']['project']}
378
379class allocate_project_remote:
380    """
381    Allocate projects on a remote machine using the internal SOAP interface
382    """
383    class proxy(service_caller):
384        """
385        This class is a proxy functor (callable) that has the same signature as
386        a function called by soap_handler or xmlrpc_handler, but that used the
387        service_caller class to call the function remotely.
388        """
389
390        def __init__(self, url, cert_file, cert_pwd, trusted_certs, 
391                method, req_name, req_alloc, resp_name):
392            service_caller.__init__(self, method, 'getfeddInternalPortType',
393                    feddInternalServiceLocator, req_alloc, req_name)
394            self.url = url
395            self.cert_file = cert_file
396            self.cert_pwd = cert_pwd
397            self.trusted_certs = trusted_certs
398            self.resp_name = resp_name
399            # Calling the proxy object directly invokes the proxy_call method,
400            # not the service_call method.
401            self.__call__ = self.proxy_call
402           
403
404        # Define the proxy, NB, the parameters to make_proxy are visible to the
405        # definition of proxy.
406        def proxy_call(self, req, fedid=None):
407            """
408            Send req on to a remote project instantiator.
409
410            Req is just the message to be sent.  This function re-wraps it.
411            It also rethrows any faults.
412            """
413
414            if req.has_key(self.request_body_name):
415                req = req[self.request_body_name]
416            else:
417                raise service_error(service_error.req, "Bad formated request");
418
419            r = self.call_service(self.url, req, self.cert_file, self.cert_pwd,
420                    self.trusted_certs)
421            if r.has_key(self.resp_name):
422                return r[self.resp_name]
423            else:
424                raise service_error(service_error.protocol, 
425                        "Bad proxy response")
426
427    # back to defining the allocate_project_remote class
428    def __init__(self, config, auth=None):
429        """
430        Initializer.  Parses a configuration if one is given.
431        """
432
433        self.debug = config.get("access", "debug_project", False)
434        self.url = config.get("access", "project_allocation_uri", "")
435
436        self.cert_file = config.get("access", "cert_file", None)
437        self.cert_pwd = config.get("access", "cert_pwd", None)
438        self.trusted_certs = config.get("access", "trusted_certs", None)
439
440        # Certs are promoted from the generic to the specific, so without a if
441        # no dynamic project certificates, then proxy certs are used, and if
442        # none of those the main certs.
443
444        if config.has_option("globals", "proxy_cert_file"):
445            if not self.cert_file:
446                self.cert_file = config.get("globals", "proxy_cert_file")
447                if config.has_option("globals", "proxy_cert_pwd"):
448                    self.cert_pwd = config.get("globals", "proxy_cert_pwd")
449
450        if config.has_option("globals", "proxy_trusted_certs") and \
451                not self.trusted_certs:
452                self.trusted_certs = \
453                        config.get("globals", "proxy_trusted_certs")
454
455        if config.has_option("globals", "cert_file"):
456            has_pwd = config.has_option("globals", "cert_pwd")
457            if not self.cert_file:
458                self.cert_file = config.get("globals", "cert_file")
459                if has_pwd: 
460                    self.cert_pwd = config.get("globals", "cert_pwd")
461
462        if config.get("globals", "trusted_certs") and not self.trusted_certs:
463                self.trusted_certs = \
464                        config.get("globals", "trusted_certs")
465
466        self.soap_services = { }
467        self.xmlrpc_services = { }
468        self.log = logging.getLogger("fedd.allocate.remote")
469        set_log_level(config, "access", self.log)
470
471        # The specializations of the proxy functions
472        self.dynamic_project = self.proxy(self.url, self.cert_file, 
473                self.cert_pwd, self.trusted_certs, "AllocateProject",
474                "AllocateProjectRequestBody", AllocateProjectRequestMessage,
475                "AllocateProjectResponseBody")
476        self.static_project = self.proxy(self.url, self.cert_file, 
477                self.cert_pwd, self.trusted_certs, "StaticProject",
478                "StaticProjectRequestBody", StaticProjectRequestMessage,
479                "StaticProjectResponseBody")
480        self.release_project = self.proxy(self.url, self.cert_file,
481                self.cert_pwd, self.trusted_certs, "ReleaseProject",
482                "ReleaseProjectRequestBody", ReleaseProjectRequestMessage,
483                "ReleaseProjectResponseBody")
484
Note: See TracBrowser for help on using the repository browser.