source: fedd/fedd_allocate_project.py @ 9460b1e

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

move remote_service out of fedd_util

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