├── requirements.txt ├── README.rst ├── LICENSE ├── jsonubus.py └── netcli.py /requirements.txt: -------------------------------------------------------------------------------- 1 | jsonrpclib-pelix==0.4.2 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | netcli 2 | ~~~~~~ 3 | 4 | netcli is a simple example how to use the jsonrpc api of OpenWrt through `uhttpd-mod-ubus` + `rpcd` 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 by the Jinja Team, see AUTHORS for more details. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /jsonubus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import jsonrpclib 5 | import logging 6 | from datetime import datetime 7 | from datetime import timedelta 8 | from enum import Enum 9 | 10 | class MessageStatus(Enum): 11 | UBUS_STATUS_OK = 0 12 | UBUS_STATUS_INVALID_COMMAND = 1 13 | UBUS_STATUS_INVALID_ARGUMENT = 2 14 | UBUS_STATUS_METHOD_NOT_FOUND = 3 15 | UBUS_STATUS_NOT_FOUND = 4 16 | UBUS_STATUS_NO_DATA = 5 17 | UBUS_STATUS_PERMISSION_DENIED = 6 18 | UBUS_STATUS_TIMEOUT = 7 19 | UBUS_STATUS_NOT_SUPPORTED = 8 20 | 21 | class NotAuthenticatedError(RuntimeError): 22 | pass 23 | 24 | class Ubus(object): 25 | def list(self, path): 26 | raise NotImplementedError 27 | 28 | def call(self, path, func, **kwargs): 29 | raise NotImplementedError 30 | 31 | def subscribe(self, path): 32 | raise NotImplementedError 33 | 34 | class JsonUbus(Ubus): 35 | def __init__(self, url, user, password): 36 | self.url = url 37 | self._server = jsonrpclib.ServerProxy(self.url) 38 | self.__session = None 39 | self.__user = user 40 | self.__password = password 41 | self.__timeout = timedelta(seconds=1) 42 | self.__expires = None 43 | self.__lastcalled = datetime.now() 44 | self.__lastused = datetime(year=1970, month=1, day=1) 45 | self.logger = logging.getLogger('jsonubus') 46 | 47 | def session(self): 48 | if self.__session == None: 49 | ret = self._server.call("00000000000000000000000000000000", "session", "login", {"username": self.__user, "password": self.__password}) 50 | if ret[0] != 0: 51 | raise NotAuthenticatedError("Could not authenticate against ubus") 52 | self.__session = ret[1]['ubus_rpc_session'] 53 | self.__timeout = timedelta(seconds=ret[1]['timeout']) 54 | self.__expires = ret[1]['expires'] 55 | self.__lastused = datetime.now() 56 | self.logger.info('Connected with {}'.format(self.url)) 57 | return self.__session 58 | 59 | def list(self, *args): 60 | """ list() - returns all available paths 61 | list('system', 'network') -> {'system': {}, 'network': {}} 62 | returning dict only contains valid objects 63 | """ 64 | if len(args): 65 | return self._server.list(*args) 66 | else: 67 | return self._server.list() 68 | 69 | def _handle_session_timeout(self): 70 | self.logger.debug("Handle Session Timeout: {} + {} < {}".format( 71 | self.__lastused, self.__timeout, datetime.now())) 72 | if (self.__lastused + self.__timeout) < datetime.now(): 73 | self.__session = None 74 | 75 | def call(self, ubus_path, ubus_method, **kwargs): 76 | """ calls ubus method ubus_method and returns a list. 77 | Returning list contains at lease one element the return value or 78 | if success also a response 79 | """ 80 | self._handle_session_timeout() 81 | self.__lastused = datetime.now() 82 | self.logger.debug("call {} {} {} {}".format(self.session(), ubus_path, ubus_method, kwargs)) 83 | return self._server.call(self.session(), ubus_path, ubus_method, kwargs) 84 | 85 | def callp(self, ubus_path, ubus_method, **kwargs): 86 | """ returns a human printable ubus response """ 87 | response = self.call(ubus_path, ubus_method, **kwargs) 88 | if response[0] != 0: 89 | return "Fail {}".format(MessageStatus(response[0])) 90 | else: 91 | if len(response) > 1: 92 | return response[1] 93 | 94 | if __name__ == '__main__': 95 | js = JsonUbus(url="http://192.168.122.175/ubus", user='root', password='yipyip') 96 | print(js.call('uci', 'configs')) 97 | print(js.call('uci', 'get', config='tests')) 98 | print(js.list("system", "network")) 99 | -------------------------------------------------------------------------------- /netcli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pprint import pprint 4 | from jsonubus import JsonUbus 5 | import readline 6 | import logging 7 | import sys 8 | import argparse 9 | 10 | LOG = logging.getLogger('netcli') 11 | 12 | def convert_to_dict(argumentlist): 13 | """ convert a list into a dict 14 | e.g. ['help=me', 'foo'='bar'] => {'help': 'me', 'foo':'bar'} 15 | """ 16 | def gen_dict(keyval): 17 | pos = keyval.find('=') 18 | if pos == -1: 19 | raise RuntimeError("Invalid argument {}".format(keyval)) 20 | return {keyval[:pos]:keyval[pos+1:]} 21 | converted = {} 22 | for i in [gen_dict(part) for part in argumentlist if len(part) > 0]: 23 | converted.update(i) 24 | return converted 25 | 26 | class CliApp(object): 27 | def __init__(self): 28 | self.__prompt = "netcli#>" 29 | self.__command = {} 30 | self.__commands = [] 31 | self.__completer = [] 32 | self.register_command('help', self) 33 | self.register_command('?', self) 34 | self.register_command('verbose', self) 35 | readline.parse_and_bind("tab: complete") 36 | readline.set_completer(self.completer) 37 | 38 | def error(self, message): 39 | print(message) 40 | 41 | @property 42 | def prompt(self): 43 | return self.__prompt 44 | 45 | @prompt.setter 46 | def set_prompt(self, prompt): 47 | self.__prompt = prompt 48 | 49 | def input_loop(self): 50 | # ctrl D + exit 51 | line = '' 52 | while line != 'exit': 53 | try: 54 | line = input(self.prompt) 55 | except EOFError: 56 | # otherwise console will be on the same line 57 | print() 58 | sys.exit(0) 59 | except KeyboardInterrupt: 60 | print() 61 | sys.exit(0) 62 | 63 | self.dispatcher(line) 64 | 65 | def completer(self, text, state): 66 | # first complete commands 67 | # second delegate completer 68 | if state == 0: 69 | if type(text) == str: 70 | split = readline.get_line_buffer().split(' ') 71 | if len(split) <= 1: 72 | self.__completer = [s + " " for s in self.__commands if s.startswith(split[0])] 73 | else: 74 | if split[0] in self.__command: 75 | self.__completer = self.__command[split[0]].complete(split[1:]) 76 | else: 77 | return None 78 | else: 79 | self.__completer = self.__commands 80 | try: 81 | return self.__completer[state] 82 | except IndexError: 83 | return None 84 | 85 | def dispatcher(self, line): 86 | split = line.split(' ') 87 | cmd = split[0] 88 | argument = split[1:] 89 | if cmd in self.__command: 90 | self.__command[cmd].dispatch(cmd, argument) 91 | else: 92 | self.error("No such command. See help or ?") 93 | 94 | def register_command(self, name, cmdclass): 95 | # TODO: move this into class var of SubCommand 96 | self.__command[name] = cmdclass 97 | self.__commands.append(name) 98 | 99 | def dispatch(self, cmd, arg): 100 | """ self implemented commands """ 101 | if cmd == "help" or cmd == "?": 102 | self.help() 103 | elif cmd == "verbose": 104 | self.verbose() 105 | 106 | def help(self): 107 | print("available commands : %s" % self.__commands) 108 | 109 | def verbose(self): 110 | logging.basicConfig() 111 | 112 | class Cli(CliApp): 113 | def __init__(self, url, user, password): 114 | super().__init__() 115 | self.__url = url 116 | self.__user = user 117 | self.__password = password 118 | self.__ubus = JsonUbus(url, user, password) 119 | self.__ubus.list() 120 | self.register_command('ubus', Ubus(self.__ubus)) 121 | 122 | class SubCommand(object): 123 | # todo class variables 124 | def complete(self, text): 125 | """ returns an array of possible extensions 126 | text is "cmd f" 127 | return ["cmd foo", "cmd fun", "cmd far"] 128 | """ 129 | pass 130 | 131 | def dispatch(self, cmd, arguments): 132 | """ arguements is [] 133 | """ 134 | pass 135 | 136 | class Ubus(SubCommand): 137 | """ An interface to ubus """ 138 | _commands = ['call', 'list'] 139 | 140 | def __init__(self, ubus): 141 | self.__ubus = ubus 142 | self.__paths = {} 143 | 144 | def update_paths(self): 145 | paths = self.__ubus.list() 146 | for path in paths: 147 | self.__paths.update(self.__ubus.list(path)) 148 | 149 | def dispatch(self, cmd, arguments): 150 | # func obj 151 | argp = argparse.ArgumentParser(prog="ubus", description='Call ubus functions') 152 | argp.add_argument('func', nargs=1, type=str, help='list or call', choices=self._commands) 153 | argp.add_argument('path', nargs='?', type=str, help='ubus path') 154 | argp.add_argument('method', nargs='?', type=str, help='object function') 155 | self.update_paths() 156 | parsed, leftover = argp.parse_known_args(arguments) 157 | if parsed.func[0] == "call": 158 | if not parsed.path: 159 | print('Path is missing') 160 | elif parsed.path not in self.__paths: 161 | print('Unknown path %s' % parsed.path) 162 | elif not parsed.method: 163 | print('No method given!') 164 | else: 165 | pprint(self.__ubus.callp(parsed.path, parsed.method, **convert_to_dict(leftover))) 166 | elif parsed.func[0] == 'list': 167 | if parsed.path: 168 | print(self.__ubus.list(parsed.path)) 169 | else: 170 | print(self.__ubus.list()) 171 | else: 172 | return 'Unknown ubus method {}'.format(parsed.func) 173 | 174 | def complete(self, split): 175 | if not self.__paths: 176 | self.update_paths() 177 | 178 | if len(split) == 1: # call or list 179 | return [s + " " for s in self._commands if s.startswith(split[0])] 180 | elif len(split) > 1 and not split[0] in self._commands: 181 | return 182 | elif len(split) == 2: # e.g network or network.interface.lan 183 | return [s + " " for s in self.__paths if s.startswith(split[1])] 184 | elif len(split) > 2 and split[0] == 'list': # list only takes max 1 argument 185 | return 186 | elif len(split) == 3: # e.g. func of network -> status 187 | if split[1] in self.__paths: 188 | return [s + " " for s in self.__paths[split[1]] if s.startswith(split[2])] 189 | return 190 | elif len(split) > 3: # arguments of the func e.g. name=foooa 191 | arg = split[-1] 192 | if arg.find('=') == -1: 193 | # we extend the argument name 194 | return [s + "=" for s in self.__paths[split[1]][split[2]] if s.startswith(split[-1])] 195 | return 196 | 197 | class Uci(SubCommand): 198 | pass 199 | 200 | if __name__ == '__main__': 201 | Cli(url='http://127.0.0.1:8080/ubus', user='root', password='yipyip').input_loop() 202 | --------------------------------------------------------------------------------