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