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@…>, 15 years ago

Proxy key additions working

  • Property mode set to 100644
File size: 11.6 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 parse_detail
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.addpubkey = config.get('access', 'addpubkey', 
53                '/usr/testbed/sbin/taddpubkey')
54        self.grantnodetype = config.get('access', 'grantnodetype', 
55                '/usr/testbed/sbin/grantnodetype')
56        self.log = logging.getLogger("fedd.allocate.local")
57        set_log_level(config, "access", self.log)
58
59        # Internal services are SOAP only
60        self.soap_services = {\
61                "AllocateProject": make_soap_handler(\
62                AllocateProjectRequestMessage.typecode,\
63                self.dynamic_project, AllocateProjectResponseMessage,\
64                "AllocateProjectResponseBody"), 
65                "StaticProject": make_soap_handler(\
66                StaticProjectRequestMessage.typecode,\
67                self.static_project, StaticProjectResponseMessage,\
68                "StaticProjectResponseBody")\
69                }
70        self.xmlrpc_services = { }
71
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
96    def dynamic_project(self, req, fedid=None):
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
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")
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
124                uname = uid.get('localname', None) or \
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:
133                ssh = access[0].get('sshPubkey', None)
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:
190            self.log.debug("[dynamic_project]: %s" % ' '.join(cmd))
191            if not self.debug:
192                try:
193                    rc = subprocess.call(cmd)
194                except OSerror, e:
195                    raise service_error(service_error.internal,
196                            "Dynamic project subprocess creation error "+ \
197                                    "[%s] (%s)" %  (cmd[1], e.strerror))
198
199            if rc != 0: 
200                raise service_error(service_error.internal,
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': {\
208                'name': { 'localname': name }, 
209                'user' : [ {\
210                    'userID': { 'localname' : uname },
211                    'access': [ { 'sshPubkey' : ssh } ],
212                } ]\
213            }\
214        }
215        return rv
216
217    def static_project(self, req, fedid=None):
218        """
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.
221        """
222
223        cmds =  []
224
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))
245       
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):
284        """
285        Send req on to a remote project instantiator.
286
287        Req is just the message to be sent.  This function re-wraps it.
288        It also rethrows any faults.
289        """
290
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
300        loc = feddInternalServiceLocator();
301        port = loc.getfeddInternalPortType(self.url,
302                transport=M2Crypto.httpslib.HTTPSConnection, 
303                transdict={ 'ssl_context' : ctx })
304
305        if req.has_key(req_name):
306            req = req[req_name]
307        else:
308            raise service_error(service_error.req, "Bad formated request");
309
310        # Reconstruct the full request message
311        msg = req_alloc()
312        set_elem = getattr(msg, "set_element_%s" % req_name)
313        set_elem(pack_soap(msg, req_name, req))
314        try:
315            mattr = getattr(port, method)
316            resp = mattr(msg)
317        except ZSI.ParseException, e:
318            raise service_error(service_error.proxy,
319                    "Bad format message (XMLRPC??): %s" % str(e))
320        except ZSI.FaultException, e:
321            resp = e.fault.detail[0]
322
323        r = unpack_soap(resp)
324
325        if r.has_key(resp_name):
326            return r[resp_name]
327        else:
328            raise service_error(service_error.proxy, "Bad proxy response")
329    # NB: end of proxy function definition     
330    return proxy
331
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.