Package gofer :: Package rmi :: Module dispatcher
[hide private]
[frames] | no frames]

Source Code for Module gofer.rmi.dispatcher

  1  # 
  2  # Copyright (c) 2011 Red Hat, Inc. 
  3  # 
  4  # This software is licensed to you under the GNU Lesser General Public 
  5  # License as published by the Free Software Foundation; either version 
  6  # 2 of the License (LGPLv2) or (at your option) any later version. 
  7  # There is NO WARRANTY for this software, express or implied, 
  8  # including the implied warranties of MERCHANTABILITY, 
  9  # NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should 
 10  # have received a copy of LGPLv2 along with this software; if not, see 
 11  # http://www.gnu.org/licenses/old-licenses/lgpl-2.0.txt. 
 12  # 
 13  # Jeff Ortel <jortel@redhat.com> 
 14  # 
 15   
 16  """ 
 17  Provides RMI dispatcher classes. 
 18  """ 
 19   
 20  import sys 
 21  import inspect 
 22  import traceback as tb 
 23   
 24  from gofer import NAME 
 25  from gofer.messaging import * 
 26  from gofer.pam import PAM 
 27   
 28  from logging import getLogger 
 29   
 30   
 31  log = getLogger(__name__) 
32 33 34 # --- Exceptions ------------------------------------------------------------- 35 36 37 -class DispatchError(Exception):
38 pass
39
40 41 -class ClassNotFound(DispatchError):
42 """ 43 Target class not found. 44 """ 45
46 - def __init__(self, classname):
47 DispatchError.__init__(self, classname)
48
49 50 -class MethodNotFound(DispatchError):
51 """ 52 Target method not found. 53 """ 54
55 - def __init__(self, classname, method):
56 message = '%s.%s(), not found' % (classname, method) 57 DispatchError.__init__(self, message)
58
59 60 -class NotPermitted(DispatchError):
61 """ 62 Called method not decorated as I{remote}. 63 """ 64
65 - def __init__(self, method):
66 message = '%s(), not permitted' % method.name 67 DispatchError.__init__(self, message)
68
69 70 -class NotAuthorized(DispatchError):
71 """ 72 Not authorized. 73 """ 74 pass
75
76 77 -class AuthMethod(NotAuthorized):
78 """ 79 Authentication method not supported. 80 """ 81
82 - def __init__(self, method, name):
83 message = \ 84 '%s(), auth (%s) not supported' % (method.name, name) 85 NotAuthorized.__init__(self, message)
86
87 88 -class NotShared(NotAuthorized):
89 """ 90 Method not shared between UUIDs. 91 """ 92
93 - def __init__(self, method):
94 message = '%s(), not shared' % method.name 95 DispatchError.__init__(self, message)
96
97 98 -class SecretRequired(NotAuthorized):
99 """ 100 Shared secret required and not passed. 101 """ 102
103 - def __init__(self, method):
104 message = '%s(), secret required' % method.name 105 NotAuthorized.__init__(self, message)
106
107 108 -class UserRequired(NotAuthorized):
109 """ 110 User (name) required and not passed. 111 """ 112
113 - def __init__(self, method):
114 message = '%s(), user (name) required' % method.name 115 NotAuthorized.__init__(self, message)
116
117 118 -class PasswordRequired(NotAuthorized):
119 """ 120 Password required and not passed. 121 """ 122
123 - def __init__(self, method):
124 message = '%s(), password required' % method.name 125 NotAuthorized.__init__(self, message)
126
127 128 -class NotAuthenticated(NotAuthorized):
129 """ 130 Not authenticated, user/password failed 131 PAM authentication. 132 """ 133
134 - def __init__(self, method, user):
135 message = '%s(), user "%s" not authenticted' % (method.name, user) 136 NotAuthorized.__init__(self, message)
137
138 139 -class UserNotAuthorized(NotAuthorized):
140 """ 141 The specified user is not authorized to invoke the RMI. 142 """ 143
144 - def __init__(self, method, expected, passed):
145 message = '%s(), user must be: %s, passed: %s' \ 146 % (method.name, 147 expected, 148 passed) 149 NotAuthorized.__init__(self, message)
150
151 152 -class SecretNotMatched(NotAuthorized):
153 """ 154 Specified secret, not matched. 155 """ 156
157 - def __init__(self, method, expected, passed):
158 message = '%s(), secret: %s not in: %s' \ 159 % (method.name, 160 passed, 161 expected) 162 NotAuthorized.__init__(self, message)
163
164 165 -class RemoteException(Exception):
166 """ 167 The re-raised (propagated) exception base class. 168 """ 169 170 @classmethod
171 - def instance(cls, reply):
172 classname = reply.xclass 173 mod = reply.xmodule 174 state = reply.xstate 175 args = reply.xargs 176 try: 177 C = globals().get(classname) 178 if not C: 179 mod = __import__(mod, {}, {}, [classname,]) 180 C = getattr(mod, classname) 181 inst = cls.__new(C) 182 inst.__dict__.update(state) 183 if isinstance(inst, Exception): 184 inst.args = args 185 except: 186 inst = RemoteException(reply.exval) 187 return inst
188 189 @classmethod
190 - def __new(cls, C):
191 try: 192 import new 193 return new.instance(C) 194 except: 195 pass 196 return Exception.__new__(C)
197
198 199 # --- RMI Classes ------------------------------------------------------------ 200 201 202 -class Reply(Envelope):
203 """ 204 Envelope for examining replies. 205 """ 206
207 - def succeeded(self):
208 """ 209 Test whether the reply indicates success. 210 @return: True when indicates success. 211 @rtype: bool 212 """ 213 return ( self.result and 'retval' in self.result )
214 215
216 - def failed(self):
217 """ 218 Test whether the reply indicates failure. 219 @return: True when indicates failure. 220 @rtype: bool 221 """ 222 return ( self.result and 'exval' in self.result )
223
224 - def started(self):
225 """ 226 Test whether the reply indicates status (started). 227 @return: True when indicates started. 228 @rtype: bool 229 """ 230 return ( self.status == 'started' )
231
232 - def progress(self):
233 """ 234 Test whether the reply indicates status (progress). 235 @return: True when indicates progress. 236 @rtype: bool 237 """ 238 return ( self.status == 'progress' )
239
240 241 -class Return(Envelope):
242 """ 243 Return envelope. 244 """ 245 246 @classmethod
247 - def succeed(cls, x):
248 """ 249 Return successful 250 @param x: The returned value. 251 @type x: any 252 @return: A return envelope. 253 @rtype: L{Return} 254 """ 255 inst = Return(retval=x) 256 inst.dump() # validate 257 return inst
258 259 @classmethod
260 - def exception(cls):
261 """ 262 Return raised exception. 263 @return: A return envelope. 264 @rtype: L{Return} 265 """ 266 try: 267 return cls.__exception() 268 except TypeError: 269 return cls.__exception()
270
271 - def succeeded(self):
272 """ 273 Test whether the return indicates success. 274 @return: True when indicates success. 275 @rtype: bool 276 """ 277 return ( 'retval' in self )
278
279 - def failed(self):
280 """ 281 Test whether the return indicates failure. 282 @return: True when indicates failure. 283 @rtype: bool 284 """ 285 return ( 'exval' in self )
286 287 @classmethod
288 - def __exception(cls):
289 """ 290 Return raised exception. 291 @return: A return envelope. 292 @rtype: L{Return} 293 """ 294 info = sys.exc_info() 295 inst = info[1] 296 xclass = inst.__class__ 297 exval = '\n'.join(tb.format_exception(*info)) 298 mod = inspect.getmodule(xclass) 299 if mod: 300 mod = mod.__name__ 301 args = None 302 if issubclass(xclass, Exception): 303 args = inst.args 304 state = dict(inst.__dict__) 305 state['trace'] = exval 306 inst = Return(exval=exval, 307 xmodule=mod, 308 xclass=xclass.__name__, 309 xstate=state, 310 xargs=args) 311 inst.dump() # validate 312 return inst
313
314 315 -class Request(Envelope):
316 """ 317 An RMI request envelope. 318 """ 319 pass
320
321 322 -class RMI(object):
323 """ 324 The RMI object performs the invocation. 325 @ivar request: The request envelope. 326 @type request: L{Request} 327 @ivar catalog: A dict of class mappings. 328 @type catalog: dict 329 """ 330
331 - def __init__(self, request, auth, catalog):
332 """ 333 @param request: The request envelope. 334 @type request: L{Request} 335 @param auth: Authentication properties. 336 @type auth: L{Options} 337 @param catalog: A dict of class mappings. 338 @type catalog: dict 339 """ 340 self.name = '.'.join((request.classname, request.method)) 341 self.request = request 342 self.auth = auth 343 self.inst = self.getclass(request, catalog) 344 self.method = self.getmethod(request, self.inst) 345 self.args = request.args 346 self.kwargs = request.kws
347 348 @staticmethod
349 - def getclass(request, catalog):
350 """ 351 Get an instance of the class or module specified in 352 the request using the catalog. 353 @param request: The request envelope. 354 @type request: L{Request} 355 @param catalog: A dict of class mappings. 356 @type catalog: dict 357 @return: An instance. 358 @rtype: (class|module) 359 """ 360 key = request.classname 361 inst = catalog.get(key, None) 362 if inst is None: 363 raise ClassNotFound(key) 364 if inspect.isclass(inst): 365 args, keywords = RMI.constructor(request) 366 return inst(*args, **keywords) 367 else: 368 return inst
369 370 @staticmethod
371 - def getmethod(request, inst):
372 """ 373 Get method of the class specified in the request. 374 Ensures that remote invocation is permitted. 375 @param request: The request envelope. 376 @type request: L{Request} 377 @param inst: A class or module object. 378 @type inst: (class|module) 379 @return: The requested method. 380 @rtype: (method|function) 381 """ 382 cn, fn = (request.classname, request.method) 383 if hasattr(inst, fn): 384 return getattr(inst, fn) 385 else: 386 raise MethodNotFound(cn, fn)
387 388 @staticmethod
389 - def __fn(method):
390 """ 391 Return the method's function (if a method) or 392 the I{method} assuming it's a function. 393 @param method: An instance method. 394 @type method: instancemethod 395 @return: The function 396 @rtype: function 397 """ 398 if inspect.ismethod(method): 399 fn = method.im_func 400 else: 401 fn = method 402 return fn
403 404 @staticmethod
405 - def __fninfo(method):
406 """ 407 Get the I{gofer} metadata embedded in the function 408 by the @remote decorator. 409 @param method: An instance method. 410 @type method: instancemethod 411 @return: The I{gofer} attribute. 412 @rtype: L{Options} 413 """ 414 try: 415 return getattr(RMI.__fn(method), NAME) 416 except: 417 pass
418 419 @staticmethod
420 - def constructor(request):
421 """ 422 Get (optional) constructor arguments. 423 @return: cntr: ([],{}) 424 """ 425 cntr = request.cntr 426 if not cntr: 427 cntr = ([],{}) 428 return cntr
429
430 - def __shared(self, fninfo):
431 """ 432 Validate the method is either marked as I{shared} 433 or that the request was received on the method's 434 contributing plugin UUID. 435 @param fninfo: The decorated function info. 436 @type fninfo: L{Options} 437 @raise NotShared: On sharing violation. 438 """ 439 if fninfo.shared: 440 return 441 uuid = fninfo.plugin.getuuid() 442 if not uuid: 443 return 444 log.debug('match uuid: "%s" = "%s"', self.auth.uuid, uuid) 445 if self.auth.uuid == uuid: 446 return 447 raise NotShared(self)
448
449 - def permitted(self):
450 """ 451 Check whether remote invocation of the specified method is permitted. 452 Applies security model using L{Security}. 453 """ 454 fninfo = RMI.__fninfo(self.method) 455 if fninfo is None: 456 raise NotPermitted(self) 457 self.__shared(fninfo) 458 security = Security(self, fninfo) 459 security.apply(self.auth)
460
461 - def __call__(self):
462 """ 463 Invoke the method. 464 @return: The invocation result. 465 @rtype: L{Return} 466 """ 467 try: 468 self.permitted() 469 retval = self.method(*self.args, **self.kwargs) 470 return Return.succeed(retval) 471 except Exception: 472 log.exception(str(self.method)) 473 return Return.exception()
474
475 - def __str__(self):
476 return str(self.request)
477
478 - def __repr__(self):
479 return str(self)
480
481 482 # --- Security classes ------------------------------------------------------- 483 484 485 -class Security:
486 """ 487 Layered Security. 488 @ivar method: The method name. 489 @type method: str 490 @ivar stack: The security stack; list of auth specifications defined by decorators. 491 @type stack: list 492 """ 493
494 - def __init__(self, method, fninfo):
495 """ 496 @param method: The method name. 497 @type method: str 498 @param fninfo: The decorated function info. 499 @type fninfo: L{Options} 500 """ 501 self.method = method 502 self.stack = fninfo.security
503
504 - def apply(self, passed):
505 """ 506 Apply auth specifications. 507 @param passed: The request's I{auth} info passed. 508 @type passed: L{Options}. 509 @raise SecretRequired: On secret required and not passed. 510 @raise SecretNotMatched: On not matched. 511 @raise UserRequired: On user required and not passed. 512 @raise PasswordRequired: On password required and not passed. 513 @raise UserNotAuthorized: On user not authorized. 514 @raise NotAuthenticated: On PAM auth failed. 515 """ 516 failed = [] 517 for name, required in self.stack: 518 try: 519 fn = self.impl(name) 520 return fn(required, passed) 521 except NotAuthorized, e: 522 log.debug(e) 523 failed.append(e) 524 if failed: 525 raise failed[-1]
526
527 - def impl(self, name):
528 """ 529 Find auth implementation by name. 530 @param name: auth type (name) 531 @type name: str 532 @return: The implementation method 533 @rtype: instancemethod 534 """ 535 try: 536 return getattr(self, name) 537 except AttributeError: 538 raise AuthMethod(self.method, name)
539
540 - def secret(self, required, passed):
541 """ 542 Perform shared secret auth. 543 @param required: Method specific auth specification. 544 @type required: L{Options} 545 @param passed: The credentials passed. 546 @type passed: L{Options} 547 @raise SecretRequired: On secret required and not passed. 548 @raise SecretNotMatched: On not matched. 549 """ 550 secret = required.secret 551 if callable(secret): 552 secret = secret() 553 if not secret: 554 return 555 if not isinstance(secret, (list,tuple)): 556 secret = (secret,) 557 if not passed.secret: 558 raise SecretRequired(self.method) 559 if passed.secret in secret: 560 return 561 raise SecretNotMatched(self.method, passed.secret, secret)
562
563 - def pam(self, required, passed):
564 """ 565 Perform PAM authentication. 566 @param required: Method specific auth specification. 567 @type required: L{Options} 568 @param passed: The credentials passed. 569 @type passed: L{Options} 570 @raise UserRequired: On user required and not passed. 571 @raise PasswordRequired: On password required and not passed. 572 @raise UserNotAuthorized: On user not authorized. 573 @raise NotAuthenticated: On PAM auth failed. 574 """ 575 if passed.pam: 576 passed = Options(passed.pam) 577 else: 578 passed = Options() 579 if not passed.user: 580 raise UserRequired(self.method) 581 if not passed.password: 582 raise PasswordRequired(self.method) 583 if passed.user != required.user: 584 raise UserNotAuthorized(self.method, required.user, passed.user) 585 pam = PAM() 586 try: 587 pam.authenticate(passed.user, passed.password, required.service) 588 except Exception: 589 raise NotAuthenticated(self.method, passed.user)
590
591 592 # --- Dispatcher ------------------------------------------------------------- 593 594 595 -class Dispatcher:
596 """ 597 The remote invocation dispatcher. 598 @ivar __catalog: The (catalog) of target classes. 599 @type __catalog: dict 600 """ 601 602 @staticmethod
603 - def auth(envelope):
604 return Options( 605 uuid=envelope.routing[-1], 606 secret=envelope.secret, 607 pam=envelope.pam,)
608
609 - def __init__(self, classes):
610 """ 611 @param classes: The (catalog) of target classes. 612 @type classes: list 613 """ 614 self.catalog = \ 615 dict([(c.__name__, c) for c in classes])
616
617 - def provides(self, name):
618 return ( name in self.catalog )
619
620 - def dispatch(self, envelope):
621 """ 622 Dispatch the requested RMI. 623 @param envelope: A request envelope. 624 @type envelope: L{Envelope} 625 @return: The result. 626 @rtype: any 627 """ 628 try: 629 auth = self.auth(envelope) 630 request = Request(envelope.request) 631 log.info('request: %s', request) 632 method = RMI(request, auth, self.catalog) 633 log.debug('method: %s', method) 634 return method() 635 except Exception: 636 log.exception(str(envelope)) 637 return Return.exception()
638