1#!/usr/bin/env python3.4 2# 3# Copyright 2016- Google, Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17""" 18A simple JSON RPC client. 19""" 20import json 21import time 22from urllib import request 23 24class HTTPError(Exception): 25 pass 26 27class RemoteError(Exception): 28 pass 29 30def JSONCounter(): 31 """A counter that generates JSON RPC call IDs. 32 33 Follows the increasing integer sequence. Every time this function is 34 called, the next number in the sequence is returned. 35 """ 36 i = 0 37 while True: 38 yield i 39 i += 1 40 41class JSONRPCClient: 42 COUNTER = JSONCounter() 43 headers = {'content-type': 'application/json'} 44 def __init__(self, baseurl): 45 self._baseurl = baseurl 46 47 def call(self, path, methodname=None, *args): 48 """Wrapper for the internal _call method. 49 50 A retry is performed if the initial call fails to compensate for 51 unstable networks. 52 53 Params: 54 path: Path of the rpc service to be appended to the base url. 55 methodname: Method name of the RPC call. 56 args: A tuple of arguments for the RPC call. 57 58 Returns: 59 The returned message of the JSON RPC call from the server. 60 """ 61 try: 62 return self._call(path, methodname, *args) 63 except: 64 # Take five and try again 65 time.sleep(5) 66 return self._call(path, methodname, *args) 67 68 def _post_json(self, url, payload): 69 """Performs an HTTP POST request with a JSON payload. 70 71 Params: 72 url: The full URL to post the payload to. 73 payload: A JSON string to be posted to server. 74 75 Returns: 76 The HTTP response code and text. 77 """ 78 req = request.Request(url) 79 req.add_header('Content-Type', 'application/json') 80 resp = request.urlopen(req, data=payload.encode("utf-8")) 81 txt = resp.read() 82 return resp.code, txt.decode('utf-8') 83 84 def _call(self, path, methodname=None, *args): 85 """Performs a JSON RPC call and return the response. 86 87 Params: 88 path: Path of the rpc service to be appended to the base url. 89 methodname: Method name of the RPC call. 90 args: A tuple of arguments for the RPC call. 91 92 Returns: 93 The returned message of the JSON RPC call from the server. 94 95 Raises: 96 HTTPError: Raised if the http post return code is not 200. 97 RemoteError: Raised if server returned an error. 98 """ 99 jsonid = next(JSONRPCClient.COUNTER) 100 payload = json.dumps({"method": methodname, 101 "params": args, 102 "id": jsonid}) 103 url = self._baseurl + path 104 status_code, text = self._post_json(url, payload) 105 if status_code != 200: 106 raise HTTPError(text) 107 r = json.loads(text) 108 if r['error']: 109 raise RemoteError(r['error']) 110 return r['result'] 111 112 def sys(self, *args): 113 return self.call("sys", *args) 114 115 def __getattr__(self, name): 116 def rpc_call(*args): 117 return self.call('uci', name, *args) 118 return rpc_call 119