source: fedd/fedd_allocate_project.py @ 058f58e

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

Unify the code for calling SOAP and XMLRPC services into a couple classes.
Before there were slightly different semantics everywhere.

Also make the handlers classes rather than the output of stub compiling
functions.

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