• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1
2"""
3  Copyright (c) 2007 Jan-Klaas Kollhof
4
5  This file is part of jsonrpc.
6
7  jsonrpc is free software; you can redistribute it and/or modify
8  it under the terms of the GNU Lesser General Public License as published by
9  the Free Software Foundation; either version 2.1 of the License, or
10  (at your option) any later version.
11
12  This software is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  GNU Lesser General Public License for more details.
16
17  You should have received a copy of the GNU Lesser General Public License
18  along with this software; if not, write to the Free Software
19  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20"""
21
22import os
23import socket
24import subprocess
25import urllib
26import urllib2
27from autotest_lib.client.common_lib import error as exceptions
28from autotest_lib.client.common_lib import global_config
29
30from json import decoder
31
32from json import encoder as json_encoder
33json_encoder_class = json_encoder.JSONEncoder
34
35
36# Try to upgrade to the Django JSON encoder. It uses the standard json encoder
37# but can handle DateTime
38try:
39    # See http://crbug.com/418022 too see why the try except is needed here.
40    from django import conf as django_conf
41    # The serializers can't be imported if django isn't configured.
42    # Using try except here doesn't work, as test_that initializes it's own
43    # django environment (setup_django_lite_environment) which raises import
44    # errors if the django dbutils have been previously imported, as importing
45    # them leaves some state behind.
46    # This the variable name must not be undefined or empty string.
47    if os.environ.get(django_conf.ENVIRONMENT_VARIABLE, None):
48        from django.core.serializers import json as django_encoder
49        json_encoder_class = django_encoder.DjangoJSONEncoder
50except ImportError:
51    pass
52
53
54class JSONRPCException(Exception):
55    pass
56
57
58class ValidationError(JSONRPCException):
59    """Raised when the RPC is malformed."""
60    def __init__(self, error, formatted_message):
61        """Constructor.
62
63        @param error: a dict of error info like so:
64                      {error['name']: 'ErrorKind',
65                       error['message']: 'Pithy error description.',
66                       error['traceback']: 'Multi-line stack trace'}
67        @formatted_message: string representation of this exception.
68        """
69        self.problem_keys = eval(error['message'])
70        self.traceback = error['traceback']
71        super(ValidationError, self).__init__(formatted_message)
72
73
74def BuildException(error):
75    """Exception factory.
76
77    Given a dict of error info, determine which subclass of
78    JSONRPCException to build and return.  If can't determine the right one,
79    just return a JSONRPCException with a pretty-printed error string.
80
81    @param error: a dict of error info like so:
82                  {error['name']: 'ErrorKind',
83                   error['message']: 'Pithy error description.',
84                   error['traceback']: 'Multi-line stack trace'}
85    """
86    error_message = '%(name)s: %(message)s\n%(traceback)s' % error
87    for cls in JSONRPCException.__subclasses__():
88        if error['name'] == cls.__name__:
89            return cls(error, error_message)
90    for cls in (exceptions.CrosDynamicSuiteException.__subclasses__() +
91                exceptions.RPCException.__subclasses__()):
92        if error['name'] == cls.__name__:
93            return cls(error_message)
94    return JSONRPCException(error_message)
95
96
97class ServiceProxy(object):
98    def __init__(self, serviceURL, serviceName=None, headers=None):
99        """
100        @param serviceURL: The URL for the service we're proxying.
101        @param serviceName: Name of the REST endpoint to hit.
102        @param headers: Extra HTTP headers to include.
103        """
104        self.__serviceURL = serviceURL
105        self.__serviceName = serviceName
106        self.__headers = headers or {}
107
108        # TODO(pprabhu) We are reading this config value deep in the stack
109        # because we don't want to update all tools with a new command line
110        # argument. Once this has been proven to work, flip the switch -- use
111        # sso by default, and turn it off internally in the lab via
112        # shadow_config.
113        self.__use_sso_client = global_config.global_config.get_config_value(
114            'CLIENT', 'use_sso_client', type=bool, default=False)
115
116
117    def __getattr__(self, name):
118        if self.__serviceName is not None:
119            name = "%s.%s" % (self.__serviceName, name)
120        return ServiceProxy(self.__serviceURL, name, self.__headers)
121
122    def __call__(self, *args, **kwargs):
123        # Caller can pass in a minimum value of timeout to be used for urlopen
124        # call. Otherwise, the default socket timeout will be used.
125        min_rpc_timeout = kwargs.pop('min_rpc_timeout', None)
126        postdata = json_encoder_class().encode({'method': self.__serviceName,
127                                                'params': args + (kwargs,),
128                                                'id': 'jsonrpc'})
129        url_with_args = self.__serviceURL + '?' + urllib.urlencode({
130            'method': self.__serviceName})
131        if self.__use_sso_client:
132            respdata = _sso_request(url_with_args, self.__headers, postdata,
133                                    min_rpc_timeout)
134        else:
135            respdata = _raw_http_request(url_with_args, self.__headers,
136                                         postdata, min_rpc_timeout)
137
138        try:
139            resp = decoder.JSONDecoder().decode(respdata)
140        except ValueError:
141            raise JSONRPCException('Error decoding JSON reponse:\n' + respdata)
142        if resp['error'] is not None:
143            raise BuildException(resp['error'])
144        else:
145            return resp['result']
146
147
148def _raw_http_request(url_with_args, headers, postdata, timeout):
149    """Make a raw HTPP request.
150
151    @param url_with_args: url with the GET params formatted.
152    @headers: Any extra headers to include in the request.
153    @postdata: data for a POST request instead of a GET.
154    @timeout: timeout to use (in seconds).
155
156    @returns: the response from the http request.
157    """
158    request = urllib2.Request(url_with_args, data=postdata, headers=headers)
159    default_timeout = socket.getdefaulttimeout()
160    if not default_timeout:
161        # If default timeout is None, socket will never time out.
162        return urllib2.urlopen(request).read()
163    else:
164        return urllib2.urlopen(
165                request,
166                timeout=max(timeout, default_timeout),
167        ).read()
168
169
170def _sso_request(url_with_args, headers, postdata, timeout):
171    """Make an HTTP request via sso_client.
172
173    @param url_with_args: url with the GET params formatted.
174    @headers: Any extra headers to include in the request.
175    @postdata: data for a POST request instead of a GET.
176    @timeout: timeout to use (in seconds).
177
178    @returns: the response from the http request.
179    """
180    headers_str = '; '.join(['%s: %s' % (k, v) for k, v in headers.iteritems()])
181    cmd = [
182        'sso_client',
183        '-url', url_with_args,
184    ]
185    if headers_str:
186        cmd += [
187                '-header_sep', '";"',
188                '-headers', headers_str,
189        ]
190    if postdata:
191        cmd += [
192                '-method', 'POST',
193                '-data', postdata,
194        ]
195    if timeout:
196        cmd += ['-request_timeout', str(timeout)]
197    else:
198        # sso_client has a default timeout of 5 seconds. To mimick the raw
199        # behaviour of never timing out, we force a large timeout.
200        cmd += ['-request_timeout', '3600']
201
202    try:
203        return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
204    except subprocess.CalledProcessError as e:
205        if _sso_creds_error(e.output):
206            raise JSONRPCException('RPC blocked by uberproxy. Have your run '
207                                   '`prodaccess`')
208
209        raise JSONRPCException(
210                'Error (code: %s) retrieving url (%s): %s' %
211                (e.returncode, url_with_args, e.output)
212        )
213
214
215def _sso_creds_error(output):
216    return 'No user creds available' in output
217