source: fedd/fedd_allocate_project.py @ 7583a62

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

another checkpoint

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