source: fedd/fedd_allocate_project.py @ 4ed10ae

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

Proxy key additions working

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