source: fedd/fedd_allocate_project.py @ 51cc9df

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

split fedid out

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