From 2c8cf005c899b7e0dc3e910f925d956e6e69d460 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 2 Dec 2014 14:24:43 +0800 Subject: [PATCH 01/58] Initial version of route model. --- .coveragerc | 26 +++ Makefile | 8 + __init__.py | 0 bundle.json | 22 ++ data/route.factory.json | 3 + ip/__init__.py | 2 + ip/addr.py | 208 +++++++++++++++++ ip/route.py | 84 +++++++ requirements.txt | 6 + route.py | 249 +++++++++++++++++++++ tests/data/route.factory.json | 3 + tests/test_e2e/bundle.json | 21 ++ tests/test_e2e/view_routes.py | 112 ++++++++++ tests/test_route.py | 409 ++++++++++++++++++++++++++++++++++ 14 files changed, 1153 insertions(+) create mode 100644 .coveragerc create mode 100644 Makefile create mode 100644 __init__.py create mode 100644 bundle.json create mode 100644 data/route.factory.json create mode 100644 ip/__init__.py create mode 100755 ip/addr.py create mode 100755 ip/route.py create mode 100644 requirements.txt create mode 100755 route.py create mode 100644 tests/data/route.factory.json create mode 100644 tests/test_e2e/bundle.json create mode 100755 tests/test_e2e/view_routes.py create mode 100644 tests/test_route.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..cef5b1c --- /dev/null +++ b/.coveragerc @@ -0,0 +1,26 @@ +# .coveragerc to control coverage.py +[run] +branch = True + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + +ignore_errors = True + +[html] +directory = coverage_html_report diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7b2326c --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +all: pylint test + +pylint: + flake8 -v --exclude=.git,__init__.py . +test: + nosetests --with-coverage --cover-erase --cover-package=route + +.PHONY: pylint test diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bundle.json b/bundle.json new file mode 100644 index 0000000..27d9d98 --- /dev/null +++ b/bundle.json @@ -0,0 +1,22 @@ +{ + "name": "route", + "version": "1.0", + "author": "Aeluin Chen", + "email": "aeluin.chen@moxa.com", + "description": "Handle the routing table", + "license": "MOXA", + "main": "route.py", + "argument": "", + "priority": 20, + "hook": ["ethernet", "cellular"], + "dependencies": {}, + "repository": "", + "role": "model", + "ttl": 10, + "resources": [ + { + "methods": ["get","put"], + "resource": "/network/routes" + } + ] +} diff --git a/data/route.factory.json b/data/route.factory.json new file mode 100644 index 0000000..cd61291 --- /dev/null +++ b/data/route.factory.json @@ -0,0 +1,3 @@ +{ + "interface": "eth0" +} diff --git a/ip/__init__.py b/ip/__init__.py new file mode 100644 index 0000000..83dc5af --- /dev/null +++ b/ip/__init__.py @@ -0,0 +1,2 @@ +import addr +import route diff --git a/ip/addr.py b/ip/addr.py new file mode 100755 index 0000000..25c9c32 --- /dev/null +++ b/ip/addr.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +import os +import sh +import ipcalc + +# https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net + +# Used python modules: +# setuptools +# https://pypi.python.org/pypi/setuptools +# +# ipcalc.py +# https://github.com/tehmaze/ipcalc/ +# +# sh.py +# https://pypi.python.org/pypi/sh + + +def interfaces(): + """List all interfaces. + + Returns: + A list of interface names. For example: + + ["eth0", "eth1", "wlan0"] + + Raises: + FIXME + """ + # ifaces=$(ip a show | grep -Eo "[0-9]: wlan[0-9]" | sed "s/.*wlan//g") + # ifaces=$(ip a show | grep -Eo '[0-9]: eth[0-9]' | awk '{print $2}') + try: + ifaces = os.listdir("/sys/class/net") + ifaces = [x for x in ifaces if not + (x.startswith("lo") or x.startswith("mon."))] + return ifaces + except Exception, e: + print "Cannot get interfaces: %s" % e + raise e + + +def ifaddresses(iface): + """Retrieve the detail information for an interface. + + Args: + iface: interface name. + + Returns: + A dict format data will be return. For example: + + {"mac": "", + "link": 1, + "inet": [{ + "ip": "", + "netmask": "", + "subnet": "", + "broadcast": ""}]} + + Raises: + ValueError + """ + info = dict() + try: + info["mac"] = open("/sys/class/net/%s/address" % iface).read() + info["mac"] = info["mac"][:-1] # remove '\n' + except: + info["mac"] = None + + try: + info["link"] = open("/sys/class/net/%s/operstate" % iface).read() + if "down" == info["link"][:-1]: + info["link"] = 0 + else: + info["link"] = open("/sys/class/net/%s/carrier" % iface).read() + info["link"] = int(info["link"][:-1]) # convert to int + except: + info["link"] = 0 + + # "ip addr show %s | grep inet | grep -v inet6 | awk '{print $2}'" + try: + output = sh.awk(sh.grep( + sh.ip("addr", "show", iface), "inet"), + "{print $2}") + except sh.ErrorReturnCode_1: + raise ValueError("Device \"%s\" does not exist." % iface) + except: + raise ValueError("Unknown error for \"%s\"." % iface) + + info["inet"] = list() + for ip in output.split(): + net = ipcalc.Network(ip) + item = dict() + item["ip"] = ip.split("/")[0] + item["netmask"] = net.netmask() + if 4 == net.version(): + item["subnet"] = net.network() + item["broadcast"] = net.broadcast() + info["inet"].append(item) + + return info + + +def ifupdown(iface, up): + """Set an interface to up or down status. + + Args: + iface: interface name. + up: status for the interface, True for up and False for down. + + Raises: + ValueError + """ + if not up: + try: + output = sh.awk( + sh.grep(sh.grep(sh.ps("ax"), iface), "dhclient"), + "{print $1}") + dhclients = output().split() + for dhclient in dhclients: + sh.kill(dhclient) + except: + pass + try: + sh.ip("link", "set", iface, "up" if up else "down") + except: + raise ValueError("Cannot update the link status for \"%s\"." + % iface) + + +def ifconfig(iface, dhcpc, ip="", netmask="24", gateway=""): + """Set the interface to static IP or dynamic IP (by dhcpclient). + + Args: + iface: interface name. + dhcpc: True for using dynamic IP and False for static. + ip: IP address for static IP + netmask: + gateway: + + Raises: + ValueError + """ + # TODO(aeluin) catch the exception? + # Check if interface exist + try: + sh.ip("addr", "show", iface) + except sh.ErrorReturnCode_1: + raise ValueError("Device \"%s\" does not exist." % iface) + except: + raise ValueError("Unknown error for \"%s\"." % iface) + + # Disable the dhcp client and flush interface + try: + dhclients = sh.awk( + sh.grep(sh.grep(sh.ps("ax"), iface), "dhclient"), + "{print $1}") + dhclients = dhclients.split() + if 1 == len(dhclients): + sh.dhclient("-x", iface) + elif len(dhclients) > 1: + for dhclient in dhclients: + sh.kill(dhclient) + except: + pass + + try: + sh.ip("-4", "addr", "flush", "label", iface) + except: + raise ValueError("Unknown error for \"%s\"." % iface) + + if dhcpc: + sh.dhclient(iface) + else: + if ip: + net = ipcalc.Network("%s/%s" % (ip, netmask)) + sh.ip("addr", "add", "%s/%s" % (ip, net.netmask()), "broadcast", + net.broadcast(), "dev", iface) + + +if __name__ == "__main__": + print interfaces() + + # ifconfig("eth0", True) + # time.sleep(10) + # ifconfig("eth1", False, "192.168.31.36") + eth0 = ifaddresses("eth0") + print eth0 + print "link: %d" % eth0["link"] + for ip in eth0["inet"]: + print "ip: %s" % ip["ip"] + print "netmask: %s" % ip["netmask"] + if "subnet" in ip: + print "subnet: %s" % ip["subnet"] + + ''' + ifupdown("eth1", True) + # ifconfig("eth1", True) + ifconfig("eth1", False, "192.168.31.39") + + eth1 = ifaddresses("eth1") + print "link: %d" % eth1["link"] + for ip in eth1["inet"]: + print "ip: %s" % ip["ip"] + print "netmask: %s" % ip["netmask"] + if "subnet" in ip: + print "subnet: %s" % ip["subnet"] + ''' diff --git a/ip/route.py b/ip/route.py new file mode 100755 index 0000000..a095e58 --- /dev/null +++ b/ip/route.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +import sh + + +def show(): + """List all routing rules. + + Returns: + A list of dict for each routing rule. + + [ + {"dest": "", + "src": "", + "dev": ""}, + {"default": "", + "dev": ""} + ] + """ + rules = [] + routes = sh.ip("route", "show") + for route in routes: + rule = dict() + route = route.split() + if "default" == route[0]: + rule["default"] = "" + if "via" in route: + rule["default"] = route[route.index("via")+1] + rule["dev"] = route[route.index("dev")+1] + else: + rule["dest"] = route[0] + rule["dev"] = route[route.index("dev")+1] + if "src" in route: + src = route.index("src") + elif "via" in route: + src = route.index("via") + else: + src = -1 + if -1 != src: + rule["src"] = route[src+1] + rules.append(rule) + return rules + + +def add(dest, dev="", src=""): + """Add a routing rule. + + Args: + dest: destination for the routing rule, default for default route. + dev: routing device, could be empty + src: source for the routing rule, fill "gateway" if dest is "default" + + Raises: + FIXME + """ + if "" == src: + sh.ip("route", "add", dest, "dev", dev) + elif "default" == dest: + if dev: + sh.ip("route", "add", dest, "dev", dev, "via", src) + else: + sh.ip("route", "add", dest, "via", src) + else: + sh.ip("route", "add", dest, "dev", dev, "proto", "kernel", "scope", + "link", "src", src) + + +def delete(network="default"): + """Delete a routing rule. + + Args: + network: destination of the routing rule to be delete + + Raises: + FIXME + """ + try: + sh.ip("route", "del", network) + except sh.ErrorReturnCode_2: + pass + + +if __name__ == "__main__": + print show() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2c0d97b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +flake8 +paho-mqtt +coveralls +sh +git+https://github.com/Sanji-IO/sanji.git#egg=sanji +git+https://github.com/lwindg/ezshell.git#egg=ezshell diff --git a/route.py b/route.py new file mode 100755 index 0000000..92f98a4 --- /dev/null +++ b/route.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +import os +import logging +from sanji.core import Sanji +from sanji.core import Route +from sanji.connection.mqtt import Mqtt +from sanji.model_initiator import ModelInitiator +from voluptuous import Schema +from voluptuous import Any, Extra, Optional + +import ip + + +# TODO: logger should be defined in sanji package? +logger = logging.getLogger() + + +class IPRoute(Sanji): + """ + A model to handle IP Route configuration. + + Attributes: + model: database with json format. + """ + def init(self, *args, **kwargs): + try: # pragma: no cover + self.bundle_env = kwargs["bundle_env"] + except KeyError: + self.bundle_env = os.getenv("BUNDLE_ENV", "debug") + + path_root = os.path.abspath(os.path.dirname(__file__)) + if self.bundle_env == "debug": # pragma: no cover + path_root = "%s/tests" % path_root + + try: + self.load(path_root) + except: + self.stop() + raise IOError("Cannot load any configuration.") + + self.cellular = None + + def load(self, path): + """ + Load the configuration. If configuration is not installed yet, + initialise them with default value. + + Args: + path: Path for the bundle, the configuration should be located + under "data" directory. + """ + self.model = ModelInitiator("route", path, backup_interval=-1) + if None == self.model.db: + raise IOError("Cannot load any configuration.") + self.save() + + def save(self): + """ + Save and backup the configuration. + """ + self.model.save_db() + self.model.backup_db() + + def cellular_connected(self, name, up=True): + """ + If cellular is connected, the default gateway should be set to + cellular interface. + + Args: + name: cellular's interface name + """ + default = dict() + if up: + self.cellular = name + default["interface"] = self.cellular + self.update_default(default) + else: + self.cellular = None + if name == self.model.db["interface"]: + self.update_default(default) + + def list_interfaces(self): + """ + List available interfaces. + """ + # retrieve all interfaces + try: + ifaces = ip.addr.interfaces() + except: + return None + """ + except Exception as e: + raise e + """ + + # list connected interfaces + data = [] + for iface in ifaces: + try: + iface_info = ip.addr.ifaddresses(iface) + except: + continue + if 1 == iface_info["link"]: + data.append(iface) + return data + + def update_default(self, default): + """ + Update default gateway. If updated failed, should recover to previous + one. + + Args: + default: dict format with "interface" required and "gateway" + optional. + """ + # change the default gateway + if "interface" in default: + ifaces = self.list_interfaces() + if default["interface"] not in ifaces: + raise ValueError("Interface should be UP.") + # FIXME: how to determine a interface is produced by cellular + # elif any("ppp" in s for s in ifaces): + elif self.cellular: + raise ValueError("Cellular is connected, the default gateway" + "cannot be changed.") + + try: + ip.route.delete("default") + if "gateway" in default: + ip.route.add("default", default["interface"], + default["gateway"]) + else: + ip.route.add("default", default["interface"]) + except Exception as e: + raise e + self.model.db["interface"] = default["interface"] + + # delete the default gateway + else: + try: + ip.route.delete("default") + except Exception as e: + raise e + if "interface" in self.model.db: + self.model.db.pop("interface") + + self.save() + + @Route(methods="get", resource="/network/routes/interfaces") + def get_interfaces(self, message, response): + """ + Get available interfaces. + """ + data = self.list_interfaces() + return response(data=data) + + @Route(methods="get", resource="/network/routes/default") + def get_default(self, message, response): + """ + Get default gateway. + """ + return response(data=self.model.db) + + put_default_schema = Schema({ + Optional("interface"): Any(str, unicode), + Extra: object}) + + @Route(methods="put", resource="/network/routes/default") + def put_default(self, message, response, schema=put_default_schema): + """ + Update the default gateway, delete default gateway if data is None or + empty. + """ + # TODO: should be removed when schema worked for unittest + try: + IPRoute.put_default_schema(message.data) + except Exception as e: + return response(code=400, + data={"message": "Invalid input: %s." % e}) + + # retrieve the default gateway + rules = ip.route.show() + default = None + for rule in rules: + if "default" in rule: + default = rule + break + + try: + self.update_default(message.data) + return response(data=self.model.db) + except Exception as e: + # recover the previous default gateway if any + try: + if default: + default["interface"] = default["dev"] + default["gateway"] = default["default"] + self.update_default(default) + except: + logger.info("Failed to recover the default gateway.") + logger.info("Update default gateway failed: %s" % e) + return response(code=404, + data={"message": + "Update default gateway failed: %s" + % e}) + + @Route(methods="put", resource="/network/ethernets/:id") + def hook_put_ethernet_by_id(self, message, response): + # check if the default gateway need to be modified + if message.data["name"] == self.model.db["interface"]: + default = dict() + default["interface"] = message.data["name"] + if "gateway" in message.data: + default["gateway"] = message.data["gateway"] + self.update_default(default) + return response(data=self.model.db) + return response(data=self.model.db) + + @Route(methods="put", resource="/network/ethernets") + def hook_put_ethernets(self, message, response): + for iface in message.data: + if iface["name"] == self.model.db["interface"]: + default = dict() + default["interface"] = iface["name"] + if "gateway" in iface: + default["gateway"] = iface["gateway"] + self.update_default(default) + return response(data=self.model.db) + return response(data=self.model.db) + + ''' Event can only received by view... + @Route(methods="put", resource="/network/cellulars") + def event_put_cellulars(self, message, response): + """ + Listen the cellular's event for interface connected or disconnected. + """ + pass + ''' + + +if __name__ == "__main__": + FORMAT = "%(asctime)s - %(levelname)s - %(lineno)s - %(message)s" + logging.basicConfig(level=0, format=FORMAT) + logger = logging.getLogger("IP Route") + + route = IPRoute(connection=Mqtt()) + route.start() diff --git a/tests/data/route.factory.json b/tests/data/route.factory.json new file mode 100644 index 0000000..cd61291 --- /dev/null +++ b/tests/data/route.factory.json @@ -0,0 +1,3 @@ +{ + "interface": "eth0" +} diff --git a/tests/test_e2e/bundle.json b/tests/test_e2e/bundle.json new file mode 100644 index 0000000..e3e1386 --- /dev/null +++ b/tests/test_e2e/bundle.json @@ -0,0 +1,21 @@ +{ + "name": "view-routes", + "version": "1.0", + "author": "Aeluin Chen", + "email": "aeluin.chen@moxa.com", + "description": "A test view for routes bundle", + "license": "MOXA", + "main": "view_routes.py", + "argument": "", + "priority": 20, + "hook": [], + "dependencies": {}, + "repository": "", + "role": "view", + "ttl": 10, + "resources": [ + { + "resource": "/network/routes" + } + ] +} diff --git a/tests/test_e2e/view_routes.py b/tests/test_e2e/view_routes.py new file mode 100755 index 0000000..2acc38e --- /dev/null +++ b/tests/test_e2e/view_routes.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +import logging +from time import sleep + +from sanji.core import Sanji +from sanji.connection.mqtt import Mqtt + + +REQ_RESOURCE = '/network/routes' +MANUAL_TEST = 0 + + +class View(Sanji): + + # This function will be executed after registered. + def run(self): + + for count in xrange(0, 100, 1): + # Normal CRUD Operation + # self.publish.[get, put, delete, post](...) + # One-to-One Messaging + # self.publish.direct.[get, put, delete, post](...) + # (if block=True return Message, else return mqtt mid number) + # Agruments + # (resource[, data=None, block=True, timeout=60]) + + # case 1: test GET available interfaces + resource = '%s/interfaces' % REQ_RESOURCE + print 'GET %s' % resource + res = self.publish.get(resource) + if res.code != 200: + print 'GET should be supported, code 200 is expected' + print res.to_json() + self.stop() + if 1 == MANUAL_TEST: + var = raw_input("Please enter any key to continue...") + + # case 2: test GET current default gateway setting + sleep(2) + resource = '%s/default' % REQ_RESOURCE + print 'GET %s' % resource + res = self.publish.get(resource) + if res.code != 200: + print 'GET should be supported, code 200 is expected' + print res.to_json() + self.stop() + if 1 == MANUAL_TEST: + var = raw_input("Please enter any key to continue...") + + # case 3: test PUT with no data (remove default gateway) + sleep(2) + resource = '%s/default' % REQ_RESOURCE + print 'PUT %s' % resource + res = self.publish.put(resource, None) + if res.code != 400: + print 'data is required, code 400 is expected' + print res.to_json() + self.stop() + if 1 == MANUAL_TEST: + var = raw_input("Please enter any key to continue...") + + # case 4: test PUT with empty data (remove default gateway) + sleep(2) + resource = '%s/default' % REQ_RESOURCE + print 'PUT %s' % resource + res = self.publish.put(resource, data={}) + if res.code != 200: + print 'data is not required, code 200 is expected' + print res.to_json() + self.stop() + if 1 == MANUAL_TEST: + var = raw_input("Please enter any key to continue...") + + # case 5: test PUT to update default gateway + sleep(2) + resource = '%s/default' % REQ_RESOURCE + print 'PUT %s' % resource + res = self.publish.put(resource, data={"interface": "eth0"}) + if res.code != 200: + print 'PUT with interface is supported, code 200 is expected' + print res.to_json() + self.stop() + if 1 == MANUAL_TEST: + var = raw_input("Please enter any key to continue...") + + # case 6: test PUT to update default gateway + sleep(2) + resource = '%s/default' % REQ_RESOURCE + print 'PUT %s' % resource + res = self.publish.put( + resource, + data={"interface": "eth0", "gateway": "192.168.31.254"}) + if res.code != 200: + print 'PUT with interface is supported, code 200 is expected' + print res.to_json() + self.stop() + if 1 == MANUAL_TEST: + print var + + # stop the test view + self.stop() + + +if __name__ == '__main__': + FORMAT = '%(asctime)s - %(levelname)s - %(lineno)s - %(message)s' + logging.basicConfig(level=0, format=FORMAT) + logger = logging.getLogger('IPRoute') + + view = View(connection=Mqtt()) + view.start() diff --git a/tests/test_route.py b/tests/test_route.py new file mode 100644 index 0000000..e5e2856 --- /dev/null +++ b/tests/test_route.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + + +import os +import sys +import logging +import unittest + +from mock import patch +from sanji.connection.mockup import Mockup +from sanji.message import Message + +try: + sys.path.append(os.path.dirname(os.path.realpath(__file__)) + '/../') + from route import IPRoute +except ImportError as e: + print os.path.dirname(os.path.realpath(__file__)) + '/../' + print sys.path + print e + print "Please check the python PATH for import test module. (%s)" \ + % __file__ + exit(1) + +dirpath = os.path.dirname(os.path.realpath(__file__)) + + +def mock_ip_addr_ifaddresses(iface): + if "eth0" == iface: + return {"mac": "78:ac:c0:c1:a8:fe", + "link": 1, + "inet": [{ + "broadcast": "192.168.31.255", + "ip": "192.168.31.36", + "netmask": "255.255.255.0", + "subnet": "192.168.31.0"}]} + elif "eth1" == iface: + return {"mac": "78:ac:c0:c1:a8:ff", + "link": 0, + "inet": [{ + "broadcast": "192.168.41.255", + "ip": "192.168.41.37", + "netmask": "255.255.255.0", + "subnet": "192.168.41.0"}]} + elif "ppp0" == iface: + return {"mac": "", + "link": 1, + "inet": [{ + "broadcast": "192.168.41.255", + "ip": "192.168.41.37", + "netmask": "255.255.255.0", + "subnet": "192.168.41.0"}]} + else: + raise ValueError + + +class TestIPRouteClass(unittest.TestCase): + + def setUp(self): + self.name = "route" + self.bundle = IPRoute(connection=Mockup()) + + def tearDown(self): + self.bundle.stop() + self.bundle = None + try: + os.remove("%s/data/%s.json" % (dirpath, self.name)) + except OSError: + pass + + try: + os.remove("%s/data/%s.backup.json" % (dirpath, self.name)) + except OSError: + pass + + def test_init_no_conf(self): + # case: no configuration file + with self.assertRaises(IOError): + with patch("route.ModelInitiator") as mock_modelinit: + mock_modelinit.side_effect = IOError + self.bundle.init() + + def test_load_current_conf(self): + # case: load current configuration + self.bundle.load(dirpath) + self.assertEqual("eth0", self.bundle.model.db["interface"]) + + def test_load_backup_conf(self): + # case: load backup configuration + os.remove("%s/data/%s.json" % (dirpath, self.name)) + self.bundle.load(dirpath) + self.assertEqual("eth0", self.bundle.model.db["interface"]) + + def test_load_no_conf(self): + # case: cannot load any configuration + with self.assertRaises(IOError): + self.bundle.load("%s/mock" % dirpath) + + def test_save(self): + # Already tested in init() + pass + + @patch.object(IPRoute, 'update_default') + def test_cellular_connected_up(self, mock_update_default): + # case: update default gateway when cellular connected + self.bundle.cellular_connected("ppp0") + self.assertEqual("ppp0", self.bundle.cellular) + + @patch.object(IPRoute, 'update_default') + def test_cellular_connected_down(self, mock_update_default): + # case: update default gateway when cellular disconnected + self.bundle.cellular_connected("ppp0", False) + self.assertEqual(None, self.bundle.cellular) + + @patch.object(IPRoute, 'update_default') + def test_cellular_connected_down_delete(self, mock_update_default): + # case: update default gateway when cellular disconnected + self.bundle.model.db["interface"] = "ppp0" + self.bundle.cellular_connected("ppp0", False) + self.assertEqual(None, self.bundle.cellular) + + @patch("route.ip.addr.ifaddresses") + @patch("route.ip.addr.interfaces") + def test_list_interfaces(self, mock_interfaces, mock_ifaddresses): + # case: list the available interfaces + mock_interfaces.return_value = ["eth0", "eth1", "ppp0"] + mock_ifaddresses.side_effect = mock_ip_addr_ifaddresses + + ifaces = self.bundle.list_interfaces() + self.assertEqual(2, len(ifaces)) + self.assertIn("eth0", ifaces) + self.assertIn("ppp0", ifaces) + + @patch("route.ip.addr.interfaces") + def test_list_interfaces_failed_get_ifaces(self, mock_interfaces): + # case: failed to list the available interfaces + mock_interfaces.side_effect = IOError + + ifaces = self.bundle.list_interfaces() + self.assertEqual(None, ifaces) + + @patch("route.ip.addr.ifaddresses") + @patch("route.ip.addr.interfaces") + def test_list_interfaces_failed_get_status(self, mock_interfaces, + mock_ifaddresses): + # case: cannot get some interface's status + def mock_ip_addr_ifaddresses_ppp0_failed(iface): + if "eth0" == iface: + return {"mac": "78:ac:c0:c1:a8:fe", + "link": 1, + "inet": [{ + "broadcast": "192.168.31.255", + "ip": "192.168.31.36", + "netmask": "255.255.255.0", + "subnet": "192.168.31.0"}]} + elif "eth1" == iface: + return {"mac": "78:ac:c0:c1:a8:ff", + "link": 0, + "inet": [{ + "broadcast": "192.168.41.255", + "ip": "192.168.41.37", + "netmask": "255.255.255.0", + "subnet": "192.168.41.0"}]} + else: + raise ValueError + + mock_interfaces.return_value = ["eth0", "eth1", "ppp0"] + mock_ifaddresses.side_effect = mock_ip_addr_ifaddresses_ppp0_failed + + ifaces = self.bundle.list_interfaces() + self.assertEqual(1, len(ifaces)) + self.assertIn("eth0", ifaces) + + @patch("route.ip.route.delete") + def test_update_default_delete(self, mock_route_delete): + # case: delete the default gateway + default = dict() + self.bundle.update_default(default) + self.assertNotIn("interface", self.bundle.model.db) + + @patch("route.ip.route.delete") + def test_update_default_delete_failed(self, mock_route_delete): + mock_route_delete.side_effect = IOError + + # case: failed to delete the default gateway + default = dict() + with self.assertRaises(IOError): + self.bundle.update_default(default) + self.assertIn("interface", self.bundle.model.db) + + @patch("route.ip.route.add") + @patch("route.ip.route.delete") + @patch.object(IPRoute, 'list_interfaces') + def test_update_default_add_without_gateway( + self, mock_list_interfaces, mock_route_delete, mock_route_add): + mock_list_interfaces.return_value = ["eth0", "eth1"] + + # case: add the default gateway + default = dict() + default["interface"] = "eth1" + self.bundle.update_default(default) + self.assertIn("eth1", self.bundle.model.db["interface"]) + + @patch("route.ip.route.add") + @patch("route.ip.route.delete") + @patch.object(IPRoute, 'list_interfaces') + def test_update_default_add_with_gateway( + self, mock_list_interfaces, mock_route_delete, mock_route_add): + mock_list_interfaces.return_value = ["eth0", "eth1"] + + # case: add the default gateway + default = dict() + default["interface"] = "eth1" + default["gateway"] = "192.168.4.254" + self.bundle.update_default(default) + self.assertIn("eth1", self.bundle.model.db["interface"]) + + @patch.object(IPRoute, 'list_interfaces') + def test_update_default_add_failed_iface_down(self, mock_list_interfaces): + mock_list_interfaces.return_value = ["eth0"] + + # case: fail to add the default gateway when indicated interface is + # down + default = dict() + default["interface"] = "eth1" + default["gateway"] = "192.168.4.254" + with self.assertRaises(ValueError): + self.bundle.update_default(default) + self.assertIn("interface", self.bundle.model.db) + + @patch.object(IPRoute, 'list_interfaces') + def test_update_default_add_failed_cellular_connected( + self, mock_list_interfaces): + mock_list_interfaces.return_value = ["eth0", "ppp0"] + self.bundle.cellular = "ppp0" + + # case: fail to add the default gateway when ppp is connected + default = dict() + default["interface"] = "eth0" + default["gateway"] = "192.168.3.254" + with self.assertRaises(ValueError): + self.bundle.update_default(default) + + @patch("route.ip.route.add") + @patch("route.ip.route.delete") + @patch.object(IPRoute, 'list_interfaces') + def test_update_default_add_failed( + self, mock_list_interfaces, mock_route_delete, mock_route_add): + mock_list_interfaces.return_value = ["eth0", "eth1"] + mock_route_add.side_effect = ValueError + + # case: fail to add the default gateway + default = dict() + default["interface"] = "eth0" + default["gateway"] = "192.168.3.254" + with self.assertRaises(ValueError): + self.bundle.update_default(default) + + @patch("route.ip.addr.ifaddresses") + @patch("route.ip.addr.interfaces") + def test_get_interfaces(self, mock_interfaces, mock_ifaddresses): + mock_interfaces.return_value = ["eth0", "eth1", "ppp0"] + mock_ifaddresses.side_effect = mock_ip_addr_ifaddresses + + # case: get supported interface list + message = Message({"data": {}, "query": {}, "param": {}}) + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.assertEqual(2, len(data)) + self.assertIn("eth0", data) + self.assertIn("ppp0", data) + self.bundle.get_interfaces(message=message, response=resp, test=True) + + @patch("route.ip.addr.interfaces") + def test_get_interfaces_failed(self, mock_interfaces): + mock_interfaces.side_effect = ValueError + + # case: fail to get supported interface list + message = Message({"data": {}, "query": {}, "param": {}}) + + def resp(code=404, data=None): + self.assertEqual(404, code) + self.assertEqual(None, data) + self.bundle.get_interfaces(message=message, response=resp, test=True) + + def test_get_default(self): + # case: get default gateway info. + message = Message({"data": {}, "query": {}, "param": {}}) + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.assertEqual("eth0", data["interface"]) + self.bundle.get_default(message=message, response=resp, test=True) + + def test_get_default_empty(self): + self.bundle.model.db = {} + + # case: no default gateway set + message = Message({"data": {}, "query": {}, "param": {}}) + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.assertEqual({}, data) + self.bundle.get_default(message=message, response=resp, test=True) + + def test_put_default_none(self): + # case: None data is not allowed + message = Message({"data": None, "query": {}, "param": {}}) + + def resp(code=200, data=None): + self.assertEqual(400, code) + self.bundle.put_default(message=message, response=resp, test=True) + + @patch.object(IPRoute, 'update_default') + def test_put_default_delete(self, mock_update_default): + # case: delete the default gateway + message = Message({"data": {}, "query": {}, "param": {}}) + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.bundle.put_default(message=message, response=resp, test=True) + + @patch.object(IPRoute, 'update_default') + def test_put_default_delete_failed(self, mock_update_default): + mock_update_default.side_effect = ValueError + + # case: failed to delete the default gateway + message = Message({"data": {}, "query": {}, "param": {}}) + + def resp(code=200, data=None): + self.assertEqual(404, code) + self.bundle.put_default(message=message, response=resp, test=True) + + @patch.object(IPRoute, "update_default") + def test_put_default_add(self, mock_update_default): + # case: add the default gateway + message = Message({"data": {}, "query": {}, "param": {}}) + message.data["interface"] = "eth1" + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.bundle.put_default(message=message, response=resp, test=True) + + """ TODO + @patch.object(IPRoute, "update_default") + def test_put_default_failed_recover_failed(self, mock_update_default): + pass + """ + + def test_hook_put_ethernet_by_id_no_change(self): + # case: test if default gateway changed + message = Message({"data": {}, "query": {}, "param": {}}) + message.data["name"] = "eth1" + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.assertEqual("eth0", data["interface"]) + self.bundle.hook_put_ethernet_by_id(message=message, response=resp, + test=True) + + @patch.object(IPRoute, "update_default") + def test_hook_put_ethernet_by_id(self, mock_update_default): + # case: test if default gateway changed + message = Message({"data": {}, "query": {}, "param": {}}) + message.data["name"] = "eth0" + message.data["gateway"] = "192.168.31.254" + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.assertEqual("eth0", data["interface"]) + self.bundle.hook_put_ethernet_by_id(message=message, response=resp, + test=True) + + def test_hook_put_ethernets_no_change(self): + # case: test if default gateway changed + message = Message({"data": [], "query": {}, "param": {}}) + iface = {"id": 2, "name": "eth1", "gateway": "192.168.4.254"} + message.data.append(iface) + iface = {"id": 3, "name": "eth2", "gateway": "192.168.5.254"} + message.data.append(iface) + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.assertEqual("eth0", data["interface"]) + self.bundle.hook_put_ethernets(message=message, response=resp, + test=True) + + @patch.object(IPRoute, "update_default") + def test_hook_put_ethernets(self, mock_update_default): + # case: test if default gateway changed + message = Message({"data": [], "query": {}, "param": {}}) + iface = {"id": 2, "name": "eth1", "gateway": "192.168.4.254"} + message.data.append(iface) + iface = {"id": 1, "name": "eth0", "gateway": "192.168.31.254"} + message.data.append(iface) + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.assertEqual("eth0", data["interface"]) + self.bundle.hook_put_ethernets(message=message, response=resp, + test=True) + + +if __name__ == "__main__": + FORMAT = '%(asctime)s - %(levelname)s - %(lineno)s - %(message)s' + logging.basicConfig(level=20, format=FORMAT) + logger = logging.getLogger('IPRoute Test') + unittest.main() From 5c260957eb47e90982f26ab67eb04b1360c83293 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 2 Dec 2014 14:26:28 +0800 Subject: [PATCH 02/58] Remove ezshell. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2c0d97b..8777725 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,3 @@ paho-mqtt coveralls sh git+https://github.com/Sanji-IO/sanji.git#egg=sanji -git+https://github.com/lwindg/ezshell.git#egg=ezshell From 9eb9e513f3e3510750eaebfd163be3310598e7a3 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 2 Dec 2014 14:48:39 +0800 Subject: [PATCH 03/58] Add travis file. --- .travis.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e94886e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: python +python: + - "2.7" +install: + - pip install -r requirements.txt +script: make +after_success: + coveralls +notifications: + slack: + rooms: + - sys:yeTvjm0bw1tX6MBWrfkVL5RG#travis From 6ef41e84a126d785f36eb4ef3786f29165a5aa50 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 4 Dec 2014 11:43:30 +0800 Subject: [PATCH 04/58] Add debian configuration files. --- .travis.yml | 1 + Makefile | 15 ++ debian.mk | 32 ++++ debian/README | 6 + debian/changelog | 5 + debian/compat | 1 + debian/control | 18 +++ debian/copyright | 340 +++++++++++++++++++++++++++++++++++++++++ debian/docs | 1 + debian/postinst | 43 ++++++ debian/rules | 13 ++ debian/source/format | 1 + requirements.txt | 6 +- tests/requirements.txt | 2 + 14 files changed, 480 insertions(+), 4 deletions(-) create mode 100644 debian.mk create mode 100644 debian/README create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/docs create mode 100644 debian/postinst create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 tests/requirements.txt diff --git a/.travis.yml b/.travis.yml index e94886e..ea06d90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - "2.7" install: - pip install -r requirements.txt + - pip install -r tests/requirements.txt script: make after_success: coveralls diff --git a/Makefile b/Makefile index 7b2326c..7ac82f1 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ + +FILES = README.md bundle.json route.py __init__.py requirements.txt +DIRS = data ip + all: pylint test pylint: @@ -5,4 +9,15 @@ pylint: test: nosetests --with-coverage --cover-erase --cover-package=route +deb: + mkdir -p deb + cp -a debian deb/ + cp -a debian.mk deb/Makefile + cp -a README.md $(FILES) $(DIRS) deb/ + (cd deb; \ + dpkg-buildpackage -us -uc -rfakeroot;) + +clean: + rm -rf deb + .PHONY: pylint test diff --git a/debian.mk b/debian.mk new file mode 100644 index 0000000..0bcf8d1 --- /dev/null +++ b/debian.mk @@ -0,0 +1,32 @@ + +# Where to put executable commands/icons/conf on 'make install'? +SANJI_VER = 1.0 +RESOURCE = network/route +LIBDIR = $(DESTDIR)/usr/lib/sanji-$(SANJI_VER)/$(RESOURCE) +TMPDIR = $(DESTDIR)/tmp + +FILES = bundle.json route.py +DIRS = data ip + + +all: + mkdir -p $(CURDIR)/packages + cp -a $(CURDIR)/requirements.txt $(CURDIR)/packages + pip install -r $(CURDIR)/packages/requirements.txt --download \ + $(CURDIR)/packages || true + +clean: + # do nothing + +distclean: clean + + +install: all + install -d $(LIBDIR) + install -d $(TMPDIR) + install $(FILES) $(LIBDIR) + cp -a $(DIRS) $(LIBDIR) + cp -a packages $(TMPDIR) + +uninstall: + -rm $(addprefix $(LIBDIR)/,$(FILES)) diff --git a/debian/README b/debian/README new file mode 100644 index 0000000..2999cb0 --- /dev/null +++ b/debian/README @@ -0,0 +1,6 @@ +The Debian Package sanji-bundle-route +---------------------------- + +Comments regarding the Package + + -- Aeluin Chen Tue, 02 Dec 2014 17:02:46 +0800 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..064ab9e --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +sanji-bundle-route (1.0.0) unstable; urgency=low + + * Initial Release. + + -- Aeluin Chen Tue, 02 Dec 2014 17:02:46 +0800 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..45a4fb7 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +8 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..f8f1ade --- /dev/null +++ b/debian/control @@ -0,0 +1,18 @@ +Source: sanji-bundle-route +Priority: extra +Maintainer: Aeluin Chen +Build-Depends: debhelper (>= 8.0.0) +Build-Depends-Indep: python (>= 2.7) +Standards-Version: 3.9.3 +Section: libs +Homepage: http://www.moxa.com +#Vcs-Git: git@github.com:lwindg/sanji-bundle-routes.git +#Vcs-Browser: https://github.com/lwindg/sanji-bundle-routes +X-Python-Version: >= 2.5 + +Package: sanji-bundle-route +Section: libs +Architecture: all +Depends: ${shlibs:Depends}, ${misc:Depends}, python2.7, python-pip +Description: A sanji library for configure the default route. + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..d6a9326 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,340 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/debian/docs @@ -0,0 +1 @@ +README.md diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..615db9a --- /dev/null +++ b/debian/postinst @@ -0,0 +1,43 @@ +#!/bin/sh +# postinst script for sanji-bundle-route +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + configure) + pip install -r /tmp/packages/requirements.txt || \ + pip install --no-index --find-links file:/tmp/packages \ + -r requirements.txt + rm -rf /tmp/packages + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..2ebce13 --- /dev/null +++ b/debian/rules @@ -0,0 +1,13 @@ +#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +%: + dh $@ --with python2 diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/requirements.txt b/requirements.txt index 8777725..3ad6eb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -flake8 -paho-mqtt -coveralls -sh +paho-mqtt==1.0 +sh==1.09 git+https://github.com/Sanji-IO/sanji.git#egg=sanji diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..7d0b1d0 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +flake8 +coveralls From 4194058fbcdebe4f2031caaaf06104cc1ff5f910 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 4 Dec 2014 14:39:17 +0800 Subject: [PATCH 05/58] Adjust for install python package from debian package installing. --- debian.mk | 3 ++- debian/postinst | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/debian.mk b/debian.mk index 0bcf8d1..4f229d6 100644 --- a/debian.mk +++ b/debian.mk @@ -11,9 +11,10 @@ DIRS = data ip all: mkdir -p $(CURDIR)/packages - cp -a $(CURDIR)/requirements.txt $(CURDIR)/packages pip install -r $(CURDIR)/packages/requirements.txt --download \ $(CURDIR)/packages || true + cp -a $(CURDIR)/requirements.txt \ + $(CURDIR)/packages/bundle-requirements.txt clean: # do nothing diff --git a/debian/postinst b/debian/postinst index 615db9a..dcbc2e2 100644 --- a/debian/postinst +++ b/debian/postinst @@ -20,9 +20,9 @@ set -e case "$1" in configure) - pip install -r /tmp/packages/requirements.txt || \ + pip install -r /tmp/packages/bundle-requirements.txt || \ pip install --no-index --find-links file:/tmp/packages \ - -r requirements.txt + -r /tmp/packages/bundle-requirements.txt rm -rf /tmp/packages ;; From 9a87ea71c7b6f6ac19d45aeecf0ef1958b418b0b Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 4 Dec 2014 17:15:00 +0800 Subject: [PATCH 06/58] Do not take the bundle as a package. --- __init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 __init__.py diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 From b698d7b29e13c15961040f40e580eeccecd3aeec Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 4 Dec 2014 17:22:58 +0800 Subject: [PATCH 07/58] Update nosetests command for debug. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7ac82f1..fce0723 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ all: pylint test pylint: flake8 -v --exclude=.git,__init__.py . test: - nosetests --with-coverage --cover-erase --cover-package=route + nosetests --with-coverage --cover-erase --cover-package=route -v deb: mkdir -p deb From a81c072423e16b20e83e1dfb24c69e3fedb746c6 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 4 Dec 2014 17:25:26 +0800 Subject: [PATCH 08/58] Add requirements. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 3ad6eb1..eb9e02d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ paho-mqtt==1.0 sh==1.09 +ipcalc git+https://github.com/Sanji-IO/sanji.git#egg=sanji From d5b885c95798b1cb51c2399b08a143dc934d1591 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 4 Dec 2014 17:53:44 +0800 Subject: [PATCH 09/58] Remove __init__.py from Makefile. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fce0723..e1c5b97 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -FILES = README.md bundle.json route.py __init__.py requirements.txt +FILES = README.md bundle.json route.py requirements.txt DIRS = data ip all: pylint test From a08746d6c8d4ae5101cfe6ee3c2bebc58a8477ce Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Fri, 5 Dec 2014 09:58:42 +0800 Subject: [PATCH 10/58] Empty interface to delete the default gateway. --- route.py | 2 +- tests/test_route.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/route.py b/route.py index 92f98a4..f50a076 100755 --- a/route.py +++ b/route.py @@ -116,7 +116,7 @@ def update_default(self, default): optional. """ # change the default gateway - if "interface" in default: + if "interface" in default and default["interface"]: ifaces = self.list_interfaces() if default["interface"] not in ifaces: raise ValueError("Interface should be UP.") diff --git a/tests/test_route.py b/tests/test_route.py index e5e2856..9a0251c 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -321,6 +321,16 @@ def resp(code=200, data=None): self.assertEqual(200, code) self.bundle.put_default(message=message, response=resp, test=True) + @patch.object(IPRoute, 'update_default') + def test_put_default_delete_with_empty_iface(self, mock_update_default): + # case: delete the default gateway + message = Message( + {"data": {"interface": ""}, "query": {}, "param": {}}) + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.bundle.put_default(message=message, response=resp, test=True) + @patch.object(IPRoute, 'update_default') def test_put_default_delete_failed(self, mock_update_default): mock_update_default.side_effect = ValueError From faf7a01c6b148931b72d4ef74ab10e5d1a9c5bfa Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Fri, 12 Dec 2014 13:24:17 +0800 Subject: [PATCH 11/58] Add interface for update the router information. Improve the get default gateway to get the gateway address if any. --- bundle.json | 4 + route.py | 101 +++++++++----- tests/test_e2e/bundle.json | 3 + tests/test_e2e/view_routes.py | 30 ++++- tests/test_route.py | 239 ++++++++++++++++++++++++---------- 5 files changed, 280 insertions(+), 97 deletions(-) diff --git a/bundle.json b/bundle.json index 27d9d98..9d9e055 100644 --- a/bundle.json +++ b/bundle.json @@ -17,6 +17,10 @@ { "methods": ["get","put"], "resource": "/network/routes" + }, + { + "methods": ["put"], + "resource": "/network/routers" } ] } diff --git a/route.py b/route.py index f50a076..d394e61 100755 --- a/route.py +++ b/route.py @@ -41,6 +41,7 @@ def init(self, *args, **kwargs): raise IOError("Cannot load any configuration.") self.cellular = None + self.interfaces = [] def load(self, path): """ @@ -106,6 +107,25 @@ def list_interfaces(self): data.append(iface) return data + def list_default(self): + """ + Retrieve the default gateway + + Return: + default: dict format with "interface" and/or "gateway" + """ + rules = ip.route.show() + default = dict() + for rule in rules: + if "default" in rule: + break + else: + return default + + default["interface"] = rule["dev"] + default["gateway"] = rule["default"] + return default + def update_default(self, default): """ Update default gateway. If updated failed, should recover to previous @@ -148,8 +168,36 @@ def update_default(self, default): self.save() + def update_interface_router(self, interface): + """ + Save the interface name with its gateway and update the default + gateway if needed. + + If gateway is not specified, use the previous value. Only delete the + gateway when gateway attribute is empty. + + Args: + interface: dict format with interface "name" and/or "gateway". + """ + # update the router information + for iface in self.interfaces: + if iface["interface"] == interface["name"]: + if "gateway" in interface: + iface["gateway"] = interface["gateway"] + break + else: + iface = dict() + iface["interface"] = interface["name"] + if "gateway" in interface: + iface["gateway"] = interface["gateway"] + self.interfaces.append(iface) + + # check if the default gateway need to be modified + if iface["interface"] == self.model.db["interface"]: + self.update_default(iface) + @Route(methods="get", resource="/network/routes/interfaces") - def get_interfaces(self, message, response): + def _get_interfaces(self, message, response): """ Get available interfaces. """ @@ -157,10 +205,14 @@ def get_interfaces(self, message, response): return response(data=data) @Route(methods="get", resource="/network/routes/default") - def get_default(self, message, response): + def _get_default(self, message, response): """ Get default gateway. """ + default = self.list_default() + if self.model.db and "interface" in self.model.db and default and \ + self.model.db["interface"] == default["interface"]: + return response(data=default) return response(data=self.model.db) put_default_schema = Schema({ @@ -168,7 +220,7 @@ def get_default(self, message, response): Extra: object}) @Route(methods="put", resource="/network/routes/default") - def put_default(self, message, response, schema=put_default_schema): + def _put_default(self, message, response, schema=put_default_schema): """ Update the default gateway, delete default gateway if data is None or empty. @@ -206,38 +258,29 @@ def put_default(self, message, response, schema=put_default_schema): "Update default gateway failed: %s" % e}) - @Route(methods="put", resource="/network/ethernets/:id") - def hook_put_ethernet_by_id(self, message, response): - # check if the default gateway need to be modified - if message.data["name"] == self.model.db["interface"]: - default = dict() - default["interface"] = message.data["name"] - if "gateway" in message.data: - default["gateway"] = message.data["gateway"] - self.update_default(default) - return response(data=self.model.db) + @Route(methods="put", resource="/network/routers") + def _put_router_info(self, message, response): + self.update_interface_router(message.data) return response(data=self.model.db) - @Route(methods="put", resource="/network/ethernets") - def hook_put_ethernets(self, message, response): - for iface in message.data: - if iface["name"] == self.model.db["interface"]: - default = dict() - default["interface"] = iface["name"] - if "gateway" in iface: - default["gateway"] = iface["gateway"] - self.update_default(default) - return response(data=self.model.db) + @Route(methods="put", resource="/network/ethernets/:id") + def _hook_put_ethernet_by_id(self, message, response): + """ + Save the interface name with its gateway and update the default + gateway if needed. + """ + self.update_interface_router(message.data) return response(data=self.model.db) - ''' Event can only received by view... - @Route(methods="put", resource="/network/cellulars") - def event_put_cellulars(self, message, response): + @Route(methods="put", resource="/network/ethernets") + def _hook_put_ethernets(self, message, response): """ - Listen the cellular's event for interface connected or disconnected. + Save the interface name with its gateway and update the default + gateway if needed. """ - pass - ''' + for iface in message.data: + self.update_interface_router(iface) + return response(data=self.model.db) if __name__ == "__main__": diff --git a/tests/test_e2e/bundle.json b/tests/test_e2e/bundle.json index e3e1386..259708e 100644 --- a/tests/test_e2e/bundle.json +++ b/tests/test_e2e/bundle.json @@ -16,6 +16,9 @@ "resources": [ { "resource": "/network/routes" + }, + { + "resource": "/network/routers" } ] } diff --git a/tests/test_e2e/view_routes.py b/tests/test_e2e/view_routes.py index 2acc38e..a6a21e8 100755 --- a/tests/test_e2e/view_routes.py +++ b/tests/test_e2e/view_routes.py @@ -91,7 +91,35 @@ def run(self): print 'PUT %s' % resource res = self.publish.put( resource, - data={"interface": "eth0", "gateway": "192.168.31.254"}) + data={"interface": "eth0", "gateway": "192.168.3.254"}) + if res.code != 200: + print 'PUT with interface is supported, code 200 is expected' + print res.to_json() + self.stop() + if 1 == MANUAL_TEST: + var = raw_input("Please enter any key to continue...") + + # case 7: test PUT to update interface router + sleep(2) + resource = '/network/routers' + print 'PUT %s' % resource + res = self.publish.put( + resource, + data={"name": "eth1", "gateway": "192.168.4.254"}) + if res.code != 200: + print 'PUT with interface is supported, code 200 is expected' + print res.to_json() + self.stop() + if 1 == MANUAL_TEST: + var = raw_input("Please enter any key to continue...") + + # case 8: test PUT to update interface router + sleep(2) + resource = '/network/routers' + print 'PUT %s' % resource + res = self.publish.put( + resource, + data={"name": "eth0", "gateway": "192.168.31.254"}) if res.code != 200: print 'PUT with interface is supported, code 200 is expected' print res.to_json() diff --git a/tests/test_route.py b/tests/test_route.py index 9a0251c..fcaa288 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -73,47 +73,47 @@ def tearDown(self): except OSError: pass - def test_init_no_conf(self): + def test__init__no_conf(self): # case: no configuration file with self.assertRaises(IOError): with patch("route.ModelInitiator") as mock_modelinit: mock_modelinit.side_effect = IOError self.bundle.init() - def test_load_current_conf(self): + def test__load__current_conf(self): # case: load current configuration self.bundle.load(dirpath) self.assertEqual("eth0", self.bundle.model.db["interface"]) - def test_load_backup_conf(self): + def test__load__backup_conf(self): # case: load backup configuration os.remove("%s/data/%s.json" % (dirpath, self.name)) self.bundle.load(dirpath) self.assertEqual("eth0", self.bundle.model.db["interface"]) - def test_load_no_conf(self): + def test__load__no_conf(self): # case: cannot load any configuration with self.assertRaises(IOError): self.bundle.load("%s/mock" % dirpath) - def test_save(self): + def test__save(self): # Already tested in init() pass @patch.object(IPRoute, 'update_default') - def test_cellular_connected_up(self, mock_update_default): + def test__cellular_connected__up(self, mock_update_default): # case: update default gateway when cellular connected self.bundle.cellular_connected("ppp0") self.assertEqual("ppp0", self.bundle.cellular) @patch.object(IPRoute, 'update_default') - def test_cellular_connected_down(self, mock_update_default): + def test__cellular_connected__down(self, mock_update_default): # case: update default gateway when cellular disconnected self.bundle.cellular_connected("ppp0", False) self.assertEqual(None, self.bundle.cellular) @patch.object(IPRoute, 'update_default') - def test_cellular_connected_down_delete(self, mock_update_default): + def test__cellular_connected__down_delete(self, mock_update_default): # case: update default gateway when cellular disconnected self.bundle.model.db["interface"] = "ppp0" self.bundle.cellular_connected("ppp0", False) @@ -121,7 +121,7 @@ def test_cellular_connected_down_delete(self, mock_update_default): @patch("route.ip.addr.ifaddresses") @patch("route.ip.addr.interfaces") - def test_list_interfaces(self, mock_interfaces, mock_ifaddresses): + def test__list_interfaces(self, mock_interfaces, mock_ifaddresses): # case: list the available interfaces mock_interfaces.return_value = ["eth0", "eth1", "ppp0"] mock_ifaddresses.side_effect = mock_ip_addr_ifaddresses @@ -132,7 +132,7 @@ def test_list_interfaces(self, mock_interfaces, mock_ifaddresses): self.assertIn("ppp0", ifaces) @patch("route.ip.addr.interfaces") - def test_list_interfaces_failed_get_ifaces(self, mock_interfaces): + def test__list_interfaces__failed_get_ifaces(self, mock_interfaces): # case: failed to list the available interfaces mock_interfaces.side_effect = IOError @@ -141,8 +141,8 @@ def test_list_interfaces_failed_get_ifaces(self, mock_interfaces): @patch("route.ip.addr.ifaddresses") @patch("route.ip.addr.interfaces") - def test_list_interfaces_failed_get_status(self, mock_interfaces, - mock_ifaddresses): + def test__list_interfaces__failed_get_status(self, mock_interfaces, + mock_ifaddresses): # case: cannot get some interface's status def mock_ip_addr_ifaddresses_ppp0_failed(iface): if "eth0" == iface: @@ -171,15 +171,42 @@ def mock_ip_addr_ifaddresses_ppp0_failed(iface): self.assertEqual(1, len(ifaces)) self.assertIn("eth0", ifaces) + @patch("route.ip.route.show") + def test__list_default(self, mock_route_show): + # case: get current default gateway + mock_route_show.return_value = [ + {"default": "192.168.3.254", "dev": "eth0"}, + {"src": "192.168.4.127", + "dev": "eth1", + "dest": "192.168.4.0/24"}] + + default = self.bundle.list_default() + self.assertEqual("eth0", default["interface"]) + self.assertEqual("192.168.3.254", default["gateway"]) + + @patch("route.ip.route.show") + def test__list_default__no_default(self, mock_route_show): + # case: no current default gateway + mock_route_show.return_value = [ + {"src": "192.168.3.127", + "dev": "eth0", + "dest": "192.168.3.0/24"}, + {"src": "192.168.4.127", + "dev": "eth1", + "dest": "192.168.4.0/24"}] + + default = self.bundle.list_default() + self.assertEqual({}, default) + @patch("route.ip.route.delete") - def test_update_default_delete(self, mock_route_delete): + def test__update_default__delete(self, mock_route_delete): # case: delete the default gateway default = dict() self.bundle.update_default(default) self.assertNotIn("interface", self.bundle.model.db) @patch("route.ip.route.delete") - def test_update_default_delete_failed(self, mock_route_delete): + def test__update_default__delete_failed(self, mock_route_delete): mock_route_delete.side_effect = IOError # case: failed to delete the default gateway @@ -191,7 +218,7 @@ def test_update_default_delete_failed(self, mock_route_delete): @patch("route.ip.route.add") @patch("route.ip.route.delete") @patch.object(IPRoute, 'list_interfaces') - def test_update_default_add_without_gateway( + def test__update_default__add_without_gateway( self, mock_list_interfaces, mock_route_delete, mock_route_add): mock_list_interfaces.return_value = ["eth0", "eth1"] @@ -204,7 +231,7 @@ def test_update_default_add_without_gateway( @patch("route.ip.route.add") @patch("route.ip.route.delete") @patch.object(IPRoute, 'list_interfaces') - def test_update_default_add_with_gateway( + def test__update_default__add_with_gateway( self, mock_list_interfaces, mock_route_delete, mock_route_add): mock_list_interfaces.return_value = ["eth0", "eth1"] @@ -216,7 +243,8 @@ def test_update_default_add_with_gateway( self.assertIn("eth1", self.bundle.model.db["interface"]) @patch.object(IPRoute, 'list_interfaces') - def test_update_default_add_failed_iface_down(self, mock_list_interfaces): + def test__update_default__add_failed_iface_down( + self, mock_list_interfaces): mock_list_interfaces.return_value = ["eth0"] # case: fail to add the default gateway when indicated interface is @@ -229,7 +257,7 @@ def test_update_default_add_failed_iface_down(self, mock_list_interfaces): self.assertIn("interface", self.bundle.model.db) @patch.object(IPRoute, 'list_interfaces') - def test_update_default_add_failed_cellular_connected( + def test__update_default__add_failed_cellular_connected( self, mock_list_interfaces): mock_list_interfaces.return_value = ["eth0", "ppp0"] self.bundle.cellular = "ppp0" @@ -244,7 +272,7 @@ def test_update_default_add_failed_cellular_connected( @patch("route.ip.route.add") @patch("route.ip.route.delete") @patch.object(IPRoute, 'list_interfaces') - def test_update_default_add_failed( + def test__update_default__add_failed( self, mock_list_interfaces, mock_route_delete, mock_route_add): mock_list_interfaces.return_value = ["eth0", "eth1"] mock_route_add.side_effect = ValueError @@ -256,9 +284,83 @@ def test_update_default_add_failed( with self.assertRaises(ValueError): self.bundle.update_default(default) + @patch.object(IPRoute, 'update_default') + def test__update_interface_router__update_interface( + self, mock_update_default): + # arrange + self.bundle.interfaces = [ + {"interface": "eth0", "gateway": "192.168.31.254"}, + {"interface": "eth1", "gateway": "192.168.4.254"}] + iface = {"name": "eth1", "gateway": "192.168.41.254"} + + # act + self.bundle.update_interface_router(iface) + + # assert + self.assertEqual(2, len(self.bundle.interfaces)) + self.assertIn({"interface": "eth0", "gateway": "192.168.31.254"}, + self.bundle.interfaces) + self.assertIn({"interface": "eth1", "gateway": "192.168.41.254"}, + self.bundle.interfaces) + + @patch.object(IPRoute, 'update_default') + def test__update_interface_router__add_interface_with_gateway( + self, mock_update_default): + # arrange + self.bundle.interfaces = [ + {"interface": "eth0", "gateway": "192.168.31.254"}] + iface = {"name": "eth1", "gateway": "192.168.41.254"} + + # act + self.bundle.update_interface_router(iface) + + # assert + self.assertEqual(2, len(self.bundle.interfaces)) + self.assertIn({"interface": "eth0", "gateway": "192.168.31.254"}, + self.bundle.interfaces) + self.assertIn({"interface": "eth1", "gateway": "192.168.41.254"}, + self.bundle.interfaces) + + @patch.object(IPRoute, 'update_default') + def test__update_interface_router__add_interface_without_gateway( + self, mock_update_default): + # arrange + self.bundle.interfaces = [ + {"interface": "eth0", "gateway": "192.168.31.254"}] + iface = {"name": "eth1"} + + # act + self.bundle.update_interface_router(iface) + + # assert + self.assertEqual(2, len(self.bundle.interfaces)) + self.assertIn({"interface": "eth0", "gateway": "192.168.31.254"}, + self.bundle.interfaces) + self.assertIn({"interface": "eth1"}, + self.bundle.interfaces) + + @patch.object(IPRoute, 'update_default') + def test__update_interface_router__update_default( + self, mock_update_default): + # arrange + self.bundle.interfaces = [ + {"interface": "eth0", "gateway": "192.168.3.254"}, + {"interface": "eth1", "gateway": "192.168.4.254"}] + iface = {"name": "eth0", "gateway": "192.168.31.254"} + + # act + self.bundle.update_interface_router(iface) + + # assert + self.assertEqual(2, len(self.bundle.interfaces)) + self.assertIn({"interface": "eth0", "gateway": "192.168.31.254"}, + self.bundle.interfaces) + self.assertIn({"interface": "eth1", "gateway": "192.168.4.254"}, + self.bundle.interfaces) + @patch("route.ip.addr.ifaddresses") @patch("route.ip.addr.interfaces") - def test_get_interfaces(self, mock_interfaces, mock_ifaddresses): + def test__get_interfaces(self, mock_interfaces, mock_ifaddresses): mock_interfaces.return_value = ["eth0", "eth1", "ppp0"] mock_ifaddresses.side_effect = mock_ip_addr_ifaddresses @@ -270,10 +372,10 @@ def resp(code=200, data=None): self.assertEqual(2, len(data)) self.assertIn("eth0", data) self.assertIn("ppp0", data) - self.bundle.get_interfaces(message=message, response=resp, test=True) + self.bundle._get_interfaces(message=message, response=resp, test=True) @patch("route.ip.addr.interfaces") - def test_get_interfaces_failed(self, mock_interfaces): + def test__get_interfaces__failed(self, mock_interfaces): mock_interfaces.side_effect = ValueError # case: fail to get supported interface list @@ -282,18 +384,36 @@ def test_get_interfaces_failed(self, mock_interfaces): def resp(code=404, data=None): self.assertEqual(404, code) self.assertEqual(None, data) - self.bundle.get_interfaces(message=message, response=resp, test=True) + self.bundle._get_interfaces(message=message, response=resp, test=True) + + @patch.object(IPRoute, 'list_default') + def test__get_default__match(self, mock_list_default): + mock_list_default.return_value = { + "interface": "eth0", "gateway": "192.168.3.254"} + + # case: get default gateway info. + message = Message({"data": {}, "query": {}, "param": {}}) + + def resp(code=200, data=None): + self.assertEqual(200, code) + self.assertEqual("eth0", data["interface"]) + self.assertEqual("192.168.3.254", data["gateway"]) + self.bundle._get_default(message=message, response=resp, test=True) + + @patch.object(IPRoute, 'list_default') + def test__get_default__different(self, mock_list_default): + mock_list_default.return_value = { + "interface": "eth1", "gateway": "192.168.4.254"} - def test_get_default(self): # case: get default gateway info. message = Message({"data": {}, "query": {}, "param": {}}) def resp(code=200, data=None): self.assertEqual(200, code) self.assertEqual("eth0", data["interface"]) - self.bundle.get_default(message=message, response=resp, test=True) + self.bundle._get_default(message=message, response=resp, test=True) - def test_get_default_empty(self): + def test__get_default__empty(self): self.bundle.model.db = {} # case: no default gateway set @@ -302,37 +422,37 @@ def test_get_default_empty(self): def resp(code=200, data=None): self.assertEqual(200, code) self.assertEqual({}, data) - self.bundle.get_default(message=message, response=resp, test=True) + self.bundle._get_default(message=message, response=resp, test=True) - def test_put_default_none(self): + def test__put_default__none(self): # case: None data is not allowed message = Message({"data": None, "query": {}, "param": {}}) def resp(code=200, data=None): self.assertEqual(400, code) - self.bundle.put_default(message=message, response=resp, test=True) + self.bundle._put_default(message=message, response=resp, test=True) @patch.object(IPRoute, 'update_default') - def test_put_default_delete(self, mock_update_default): + def test__put_default__delete(self, mock_update_default): # case: delete the default gateway message = Message({"data": {}, "query": {}, "param": {}}) def resp(code=200, data=None): self.assertEqual(200, code) - self.bundle.put_default(message=message, response=resp, test=True) + self.bundle._put_default(message=message, response=resp, test=True) @patch.object(IPRoute, 'update_default') - def test_put_default_delete_with_empty_iface(self, mock_update_default): + def test__put_default__delete_with_empty_iface(self, mock_update_default): # case: delete the default gateway message = Message( {"data": {"interface": ""}, "query": {}, "param": {}}) def resp(code=200, data=None): self.assertEqual(200, code) - self.bundle.put_default(message=message, response=resp, test=True) + self.bundle._put_default(message=message, response=resp, test=True) @patch.object(IPRoute, 'update_default') - def test_put_default_delete_failed(self, mock_update_default): + def test__put_default__delete_failed(self, mock_update_default): mock_update_default.side_effect = ValueError # case: failed to delete the default gateway @@ -340,17 +460,17 @@ def test_put_default_delete_failed(self, mock_update_default): def resp(code=200, data=None): self.assertEqual(404, code) - self.bundle.put_default(message=message, response=resp, test=True) + self.bundle._put_default(message=message, response=resp, test=True) @patch.object(IPRoute, "update_default") - def test_put_default_add(self, mock_update_default): + def test__put_default__add(self, mock_update_default): # case: add the default gateway message = Message({"data": {}, "query": {}, "param": {}}) message.data["interface"] = "eth1" def resp(code=200, data=None): self.assertEqual(200, code) - self.bundle.put_default(message=message, response=resp, test=True) + self.bundle._put_default(message=message, response=resp, test=True) """ TODO @patch.object(IPRoute, "update_default") @@ -358,46 +478,31 @@ def test_put_default_failed_recover_failed(self, mock_update_default): pass """ - def test_hook_put_ethernet_by_id_no_change(self): - # case: test if default gateway changed + @patch.object(IPRoute, "update_interface_router") + def test__put_router_info(self, mock_update_interface_router): + # case: update the router information by interface message = Message({"data": {}, "query": {}, "param": {}}) - message.data["name"] = "eth1" + message.data["interface"] = "eth1" + message.data["gateway"] = "192.168.41.254" def resp(code=200, data=None): self.assertEqual(200, code) - self.assertEqual("eth0", data["interface"]) - self.bundle.hook_put_ethernet_by_id(message=message, response=resp, - test=True) + self.bundle._put_router_info(message=message, response=resp, test=True) - @patch.object(IPRoute, "update_default") - def test_hook_put_ethernet_by_id(self, mock_update_default): + @patch.object(IPRoute, "update_interface_router") + def test__hook_put_ethernet_by_id(self, mock_update_interface_router): # case: test if default gateway changed message = Message({"data": {}, "query": {}, "param": {}}) - message.data["name"] = "eth0" - message.data["gateway"] = "192.168.31.254" - - def resp(code=200, data=None): - self.assertEqual(200, code) - self.assertEqual("eth0", data["interface"]) - self.bundle.hook_put_ethernet_by_id(message=message, response=resp, - test=True) - - def test_hook_put_ethernets_no_change(self): - # case: test if default gateway changed - message = Message({"data": [], "query": {}, "param": {}}) - iface = {"id": 2, "name": "eth1", "gateway": "192.168.4.254"} - message.data.append(iface) - iface = {"id": 3, "name": "eth2", "gateway": "192.168.5.254"} - message.data.append(iface) + message.data["name"] = "eth1" def resp(code=200, data=None): self.assertEqual(200, code) self.assertEqual("eth0", data["interface"]) - self.bundle.hook_put_ethernets(message=message, response=resp, - test=True) + self.bundle._hook_put_ethernet_by_id(message=message, response=resp, + test=True) - @patch.object(IPRoute, "update_default") - def test_hook_put_ethernets(self, mock_update_default): + @patch.object(IPRoute, "update_interface_router") + def test__hook_put_ethernets(self, mock_update_interface_router): # case: test if default gateway changed message = Message({"data": [], "query": {}, "param": {}}) iface = {"id": 2, "name": "eth1", "gateway": "192.168.4.254"} @@ -408,8 +513,8 @@ def test_hook_put_ethernets(self, mock_update_default): def resp(code=200, data=None): self.assertEqual(200, code) self.assertEqual("eth0", data["interface"]) - self.bundle.hook_put_ethernets(message=message, response=resp, - test=True) + self.bundle._hook_put_ethernets(message=message, response=resp, + test=True) if __name__ == "__main__": From acb99ff75799698a034979996a29d3ffcc396dbd Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Mon, 15 Dec 2014 15:08:22 +0800 Subject: [PATCH 12/58] Update bundle.json to meet the new feature of controller (1.6.0 beta). --- bundle.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bundle.json b/bundle.json index 9d9e055..bf6cc56 100644 --- a/bundle.json +++ b/bundle.json @@ -14,9 +14,13 @@ "role": "model", "ttl": 10, "resources": [ + { + "methods": ["get"], + "resource": "/network/routes/interfaces" + }, { "methods": ["get","put"], - "resource": "/network/routes" + "resource": "/network/routes/default" }, { "methods": ["put"], From 72913fa9d68274fdc76444e2b86e1f13475e280b Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Mon, 5 Jan 2015 10:34:51 +0800 Subject: [PATCH 13/58] Rename the configuration file for updated sanji SDK. --- data/{route.factory.json => route.json.factory} | 0 tests/data/{route.factory.json => route.json.factory} | 0 tests/test_route.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename data/{route.factory.json => route.json.factory} (100%) rename tests/data/{route.factory.json => route.json.factory} (100%) diff --git a/data/route.factory.json b/data/route.json.factory similarity index 100% rename from data/route.factory.json rename to data/route.json.factory diff --git a/tests/data/route.factory.json b/tests/data/route.json.factory similarity index 100% rename from tests/data/route.factory.json rename to tests/data/route.json.factory diff --git a/tests/test_route.py b/tests/test_route.py index fcaa288..dcbe081 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -69,7 +69,7 @@ def tearDown(self): pass try: - os.remove("%s/data/%s.backup.json" % (dirpath, self.name)) + os.remove("%s/data/%s.json.backup" % (dirpath, self.name)) except OSError: pass From c3781d5a1348ec75700c597e5940f2e22a94400c Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 6 Jan 2015 15:27:40 +0800 Subject: [PATCH 14/58] Modify /network/routers put message to /network/interfaces event message. --- bundle.json | 8 ++++---- route.py | 5 ++--- tests/test_route.py | 6 ++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/bundle.json b/bundle.json index bf6cc56..2e9e9d8 100644 --- a/bundle.json +++ b/bundle.json @@ -14,6 +14,10 @@ "role": "model", "ttl": 10, "resources": [ + { + "role": "view", + "resource": "/network/interfaces" + }, { "methods": ["get"], "resource": "/network/routes/interfaces" @@ -21,10 +25,6 @@ { "methods": ["get","put"], "resource": "/network/routes/default" - }, - { - "methods": ["put"], - "resource": "/network/routers" } ] } diff --git a/route.py b/route.py index d394e61..e6f3185 100755 --- a/route.py +++ b/route.py @@ -258,10 +258,9 @@ def _put_default(self, message, response, schema=put_default_schema): "Update default gateway failed: %s" % e}) - @Route(methods="put", resource="/network/routers") - def _put_router_info(self, message, response): + @Route(methods="put", resource="/network/interfaces") + def _event_router_info(self, message): self.update_interface_router(message.data) - return response(data=self.model.db) @Route(methods="put", resource="/network/ethernets/:id") def _hook_put_ethernet_by_id(self, message, response): diff --git a/tests/test_route.py b/tests/test_route.py index dcbe081..85bdfed 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -479,15 +479,13 @@ def test_put_default_failed_recover_failed(self, mock_update_default): """ @patch.object(IPRoute, "update_interface_router") - def test__put_router_info(self, mock_update_interface_router): + def test__event_router_info(self, mock_update_interface_router): # case: update the router information by interface message = Message({"data": {}, "query": {}, "param": {}}) message.data["interface"] = "eth1" message.data["gateway"] = "192.168.41.254" - def resp(code=200, data=None): - self.assertEqual(200, code) - self.bundle._put_router_info(message=message, response=resp, test=True) + self.bundle._event_router_info(message=message, test=True) @patch.object(IPRoute, "update_interface_router") def test__hook_put_ethernet_by_id(self, mock_update_interface_router): From b3e6805f2249df3add7c8b8b0d52bb3661778c14 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Mon, 12 Jan 2015 11:26:39 +0800 Subject: [PATCH 15/58] Do not specify the library's version during developing. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eb9e02d..4849006 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ paho-mqtt==1.0 -sh==1.09 +sh ipcalc git+https://github.com/Sanji-IO/sanji.git#egg=sanji From 2aca29184b7e1f9dc1175fa9928cbf0ae63869f7 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Mon, 12 Jan 2015 13:19:57 +0800 Subject: [PATCH 16/58] Use logger instead of print. --- ip/addr.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ip/addr.py b/ip/addr.py index 25c9c32..a57fcf0 100755 --- a/ip/addr.py +++ b/ip/addr.py @@ -3,6 +3,7 @@ import os import sh import ipcalc +import logging # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net @@ -17,6 +18,9 @@ # https://pypi.python.org/pypi/sh +logger = logging.getLogger() + + def interfaces(): """List all interfaces. @@ -36,7 +40,7 @@ def interfaces(): (x.startswith("lo") or x.startswith("mon."))] return ifaces except Exception, e: - print "Cannot get interfaces: %s" % e + logger.info("Cannot get interfaces: %s" % e) raise e From efd40af64eb0dd34897f1aa1203f60abea8db273 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 15 Jan 2015 13:30:05 +0800 Subject: [PATCH 17/58] Set the default gateway at the end of init(). Set the default gateway with gateway IP retrieved from "/network/interfaces" event. --- route.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/route.py b/route.py index e6f3185..13a004f 100755 --- a/route.py +++ b/route.py @@ -43,6 +43,8 @@ def init(self, *args, **kwargs): self.cellular = None self.interfaces = [] + self.update_default(self.model.db) + def load(self, path): """ Load the configuration. If configuration is not installed yet, @@ -146,6 +148,12 @@ def update_default(self, default): raise ValueError("Cellular is connected, the default gateway" "cannot be changed.") + # retrieve the default gateway + for iface in self.interfaces: + if iface["interface"] == default["interface"]: + default = iface + break + try: ip.route.delete("default") if "gateway" in default: From 9e3ee7f0785e9833c2fc98aac9bae7034ce17aef Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 15 Jan 2015 13:40:49 +0800 Subject: [PATCH 18/58] Add mock for setUp and init for unittest. --- tests/test_route.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_route.py b/tests/test_route.py index 85bdfed..986ac3d 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -56,7 +56,8 @@ def mock_ip_addr_ifaddresses(iface): class TestIPRouteClass(unittest.TestCase): - def setUp(self): + @patch.object(IPRoute, 'update_default') + def setUp(self, mock_update_default): self.name = "route" self.bundle = IPRoute(connection=Mockup()) @@ -73,7 +74,8 @@ def tearDown(self): except OSError: pass - def test__init__no_conf(self): + @patch.object(IPRoute, 'update_default') + def test__init__no_conf(self, mock_update_default): # case: no configuration file with self.assertRaises(IOError): with patch("route.ModelInitiator") as mock_modelinit: From a1a20bbcfed0aedee2ebc4f0cdef33c68ee0b40f Mon Sep 17 00:00:00 2001 From: Zack YL Shih Date: Mon, 11 May 2015 16:48:51 +0800 Subject: [PATCH 19/58] Change logger from root to sanji.route --- ip/addr.py | 4 ++-- route.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/ip/addr.py b/ip/addr.py index a57fcf0..a76c987 100755 --- a/ip/addr.py +++ b/ip/addr.py @@ -18,7 +18,7 @@ # https://pypi.python.org/pypi/sh -logger = logging.getLogger() +_logger = logging.getLogger("sanji.route.ip.addr") def interfaces(): @@ -40,7 +40,7 @@ def interfaces(): (x.startswith("lo") or x.startswith("mon."))] return ifaces except Exception, e: - logger.info("Cannot get interfaces: %s" % e) + _logger.info("Cannot get interfaces: %s" % e) raise e diff --git a/route.py b/route.py index 13a004f..da4f766 100755 --- a/route.py +++ b/route.py @@ -13,8 +13,7 @@ import ip -# TODO: logger should be defined in sanji package? -logger = logging.getLogger() +_logger = logging.getLogger("sanji.route") class IPRoute(Sanji): @@ -55,7 +54,7 @@ def load(self, path): under "data" directory. """ self.model = ModelInitiator("route", path, backup_interval=-1) - if None == self.model.db: + if self.model.db is None: raise IOError("Cannot load any configuration.") self.save() @@ -259,8 +258,8 @@ def _put_default(self, message, response, schema=put_default_schema): default["gateway"] = default["default"] self.update_default(default) except: - logger.info("Failed to recover the default gateway.") - logger.info("Update default gateway failed: %s" % e) + _logger.info("Failed to recover the default gateway.") + _logger.info("Update default gateway failed: %s" % e) return response(code=404, data={"message": "Update default gateway failed: %s" @@ -293,7 +292,7 @@ def _hook_put_ethernets(self, message, response): if __name__ == "__main__": FORMAT = "%(asctime)s - %(levelname)s - %(lineno)s - %(message)s" logging.basicConfig(level=0, format=FORMAT) - logger = logging.getLogger("IP Route") + _logger = logging.getLogger("sanji.route") route = IPRoute(connection=Mqtt()) route.start() From e0816f24c8cdc85305e4bc3b3fd61551964ec946 Mon Sep 17 00:00:00 2001 From: Zack YL Shih Date: Mon, 11 May 2015 16:49:41 +0800 Subject: [PATCH 20/58] Add .gitignore --- .gitignore | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db4561e --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ From b02c8d8608192f76bba10c014a29d2ec90780e7c Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 12 May 2015 18:02:05 +0800 Subject: [PATCH 21/58] Use "{}" instead of "None" to prevent compare error. --- route.py | 4 ++-- tests/test_route.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/route.py b/route.py index 13a004f..9c6f9b7 100755 --- a/route.py +++ b/route.py @@ -92,7 +92,7 @@ def list_interfaces(self): try: ifaces = ip.addr.interfaces() except: - return None + return {} """ except Exception as e: raise e @@ -140,7 +140,7 @@ def update_default(self, default): # change the default gateway if "interface" in default and default["interface"]: ifaces = self.list_interfaces() - if default["interface"] not in ifaces: + if not ifaces or default["interface"] not in ifaces: raise ValueError("Interface should be UP.") # FIXME: how to determine a interface is produced by cellular # elif any("ppp" in s for s in ifaces): diff --git a/tests/test_route.py b/tests/test_route.py index 986ac3d..a3b3952 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -139,7 +139,7 @@ def test__list_interfaces__failed_get_ifaces(self, mock_interfaces): mock_interfaces.side_effect = IOError ifaces = self.bundle.list_interfaces() - self.assertEqual(None, ifaces) + self.assertEqual({}, ifaces) @patch("route.ip.addr.ifaddresses") @patch("route.ip.addr.interfaces") @@ -385,7 +385,7 @@ def test__get_interfaces__failed(self, mock_interfaces): def resp(code=404, data=None): self.assertEqual(404, code) - self.assertEqual(None, data) + self.assertEqual({}, data) self.bundle._get_interfaces(message=message, response=resp, test=True) @patch.object(IPRoute, 'list_default') From 22882ccb751fa6ba28df99cd48a56335c950c121 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Wed, 13 May 2015 13:21:36 +0800 Subject: [PATCH 22/58] Ignore the exception when interface is not "UP". --- route.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/route.py b/route.py index 9c6f9b7..83462cc 100755 --- a/route.py +++ b/route.py @@ -43,7 +43,10 @@ def init(self, *args, **kwargs): self.cellular = None self.interfaces = [] - self.update_default(self.model.db) + try: + self.update_default(self.model.db) + except: + pass def load(self, path): """ From a86ccd7f0c0761a516bb100668ab29a51e9ccd09 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Wed, 13 May 2015 13:56:43 +0800 Subject: [PATCH 23/58] Do not throw exception if no interface for update. --- route.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/route.py b/route.py index 83462cc..11a6830 100755 --- a/route.py +++ b/route.py @@ -204,7 +204,8 @@ def update_interface_router(self, interface): self.interfaces.append(iface) # check if the default gateway need to be modified - if iface["interface"] == self.model.db["interface"]: + if iface["interface"] and \ + iface["interface"] == self.model.db["interface"]: self.update_default(iface) @Route(methods="get", resource="/network/routes/interfaces") From d276cc9a3547d6e7e686f23d9bb277fad4e20013 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Wed, 13 May 2015 14:22:50 +0800 Subject: [PATCH 24/58] Prevent exception. --- route.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/route.py b/route.py index 11a6830..51a92a0 100755 --- a/route.py +++ b/route.py @@ -204,9 +204,11 @@ def update_interface_router(self, interface): self.interfaces.append(iface) # check if the default gateway need to be modified - if iface["interface"] and \ - iface["interface"] == self.model.db["interface"]: - self.update_default(iface) + if iface["interface"] == self.model.db["interface"]: + try: + self.update_default(iface) + except: + pass @Route(methods="get", resource="/network/routes/interfaces") def _get_interfaces(self, message, response): From 64dc4427e49f996ff26361077e32667968da0175 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 14 May 2015 16:25:24 +0800 Subject: [PATCH 25/58] Remove hook (use event instead). --- bundle.json | 2 +- route.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bundle.json b/bundle.json index 2e9e9d8..825ae10 100644 --- a/bundle.json +++ b/bundle.json @@ -8,7 +8,7 @@ "main": "route.py", "argument": "", "priority": 20, - "hook": ["ethernet", "cellular"], + "hook": [], "dependencies": {}, "repository": "", "role": "model", diff --git a/route.py b/route.py index 51a92a0..870f269 100755 --- a/route.py +++ b/route.py @@ -276,6 +276,7 @@ def _put_default(self, message, response, schema=put_default_schema): def _event_router_info(self, message): self.update_interface_router(message.data) + ''' @Route(methods="put", resource="/network/ethernets/:id") def _hook_put_ethernet_by_id(self, message, response): """ @@ -294,6 +295,7 @@ def _hook_put_ethernets(self, message, response): for iface in message.data: self.update_interface_router(iface) return response(data=self.model.db) + ''' if __name__ == "__main__": From d1aff022db3ad0f3b530ffc96d52961ffd75e4aa Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 14 May 2015 16:25:24 +0800 Subject: [PATCH 26/58] Remove hook (use event instead). --- bundle.json | 2 +- route.py | 2 ++ tests/test_route.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bundle.json b/bundle.json index 2e9e9d8..825ae10 100644 --- a/bundle.json +++ b/bundle.json @@ -8,7 +8,7 @@ "main": "route.py", "argument": "", "priority": 20, - "hook": ["ethernet", "cellular"], + "hook": [], "dependencies": {}, "repository": "", "role": "model", diff --git a/route.py b/route.py index 51a92a0..870f269 100755 --- a/route.py +++ b/route.py @@ -276,6 +276,7 @@ def _put_default(self, message, response, schema=put_default_schema): def _event_router_info(self, message): self.update_interface_router(message.data) + ''' @Route(methods="put", resource="/network/ethernets/:id") def _hook_put_ethernet_by_id(self, message, response): """ @@ -294,6 +295,7 @@ def _hook_put_ethernets(self, message, response): for iface in message.data: self.update_interface_router(iface) return response(data=self.model.db) + ''' if __name__ == "__main__": diff --git a/tests/test_route.py b/tests/test_route.py index a3b3952..e34999e 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -489,6 +489,7 @@ def test__event_router_info(self, mock_update_interface_router): self.bundle._event_router_info(message=message, test=True) + """ @patch.object(IPRoute, "update_interface_router") def test__hook_put_ethernet_by_id(self, mock_update_interface_router): # case: test if default gateway changed @@ -515,6 +516,7 @@ def resp(code=200, data=None): self.assertEqual("eth0", data["interface"]) self.bundle._hook_put_ethernets(message=message, response=resp, test=True) + """ if __name__ == "__main__": From deac8a3d1a948a014d10bb7967f6d29be40204ce Mon Sep 17 00:00:00 2001 From: YuLun Shih Date: Thu, 14 May 2015 18:30:10 +0800 Subject: [PATCH 27/58] Upgrade paho-mqtt to v1.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4849006..201cdd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -paho-mqtt==1.0 +paho-mqtt==1.1 sh ipcalc git+https://github.com/Sanji-IO/sanji.git#egg=sanji From d86424128810e9f28496283963578ffe54405327 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 19 May 2015 14:28:25 +0800 Subject: [PATCH 28/58] 0.9.0 --- bundle.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle.json b/bundle.json index 825ae10..798ccf5 100644 --- a/bundle.json +++ b/bundle.json @@ -1,6 +1,6 @@ { "name": "route", - "version": "1.0", + "version": "0.9.0", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", From b77327a166051d02964472b29d95210975a91944 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 26 May 2015 15:09:42 +0800 Subject: [PATCH 29/58] Run before ethernet and concurrently. --- bundle.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bundle.json b/bundle.json index 798ccf5..138da96 100644 --- a/bundle.json +++ b/bundle.json @@ -1,13 +1,14 @@ { "name": "route", - "version": "0.9.0", + "version": "0.9.1", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", "license": "MOXA", "main": "route.py", "argument": "", - "priority": 20, + "priority": 19, + "concurrent": true, "hook": [], "dependencies": {}, "repository": "", From 7377e0fc9c70175b7fbc4dfb40b1ee3f9290ead6 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 26 May 2015 15:10:31 +0800 Subject: [PATCH 30/58] 0.9.2 --- bundle.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle.json b/bundle.json index 138da96..44bdbeb 100644 --- a/bundle.json +++ b/bundle.json @@ -1,6 +1,6 @@ { "name": "route", - "version": "0.9.1", + "version": "0.9.2", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", From a323b2bd96c3aac70cbe5637ec4c3161f13071aa Mon Sep 17 00:00:00 2001 From: YuLun Shih Date: Tue, 26 May 2015 16:25:24 +0800 Subject: [PATCH 31/58] Revert "Run before ethernet and concurrently." --- bundle.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bundle.json b/bundle.json index 44bdbeb..798ccf5 100644 --- a/bundle.json +++ b/bundle.json @@ -1,14 +1,13 @@ { "name": "route", - "version": "0.9.2", + "version": "0.9.0", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", "license": "MOXA", "main": "route.py", "argument": "", - "priority": 19, - "concurrent": true, + "priority": 20, "hook": [], "dependencies": {}, "repository": "", From 15516e5af5df53a48c871faf52728affd6fa260d Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 26 May 2015 16:31:27 +0800 Subject: [PATCH 32/58] Bug fixed: Run before ethernet and concurrently (concurrent should be false) --- bundle.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle.json b/bundle.json index 44bdbeb..d7c94ba 100644 --- a/bundle.json +++ b/bundle.json @@ -8,7 +8,7 @@ "main": "route.py", "argument": "", "priority": 19, - "concurrent": true, + "concurrent": false, "hook": [], "dependencies": {}, "repository": "", From 931386692400ed5fd1788f1aafe9bedeae5192e5 Mon Sep 17 00:00:00 2001 From: YuLun Shih Date: Tue, 26 May 2015 16:32:21 +0800 Subject: [PATCH 33/58] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 201cdd0..859f552 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ paho-mqtt==1.1 sh ipcalc -git+https://github.com/Sanji-IO/sanji.git#egg=sanji +sanji From 1b95182cb2eabf878c1abbac964b2a6ffdd3111c Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Wed, 27 May 2015 02:46:35 -0700 Subject: [PATCH 34/58] 0.9.2 --- bundle.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle.json b/bundle.json index 5fa663c..d7c94ba 100644 --- a/bundle.json +++ b/bundle.json @@ -1,6 +1,6 @@ { "name": "route", - "version": "0.9.0", + "version": "0.9.2", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", From b9a5c27cd549eca17da479059e1d6f40531261ca Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 28 May 2015 10:51:32 +0800 Subject: [PATCH 35/58] Convert IP() to str().. --- bundle.json | 2 +- ip/addr.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/bundle.json b/bundle.json index d7c94ba..26e3d28 100644 --- a/bundle.json +++ b/bundle.json @@ -1,6 +1,6 @@ { "name": "route", - "version": "0.9.2", + "version": "0.9.3", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", diff --git a/ip/addr.py b/ip/addr.py index a76c987..7399b23 100755 --- a/ip/addr.py +++ b/ip/addr.py @@ -82,24 +82,32 @@ def ifaddresses(iface): info["link"] = 0 # "ip addr show %s | grep inet | grep -v inet6 | awk '{print $2}'" + info["inet"] = list() try: + ''' output = sh.awk(sh.grep( sh.ip("addr", "show", iface), "inet"), "{print $2}") + ''' + output = sh.ip("addr", "show", iface) + try: + output = sh.awk(sh.grep(output, "inet"), "{print $2}") + except: + return info except sh.ErrorReturnCode_1: raise ValueError("Device \"%s\" does not exist." % iface) except: raise ValueError("Unknown error for \"%s\"." % iface) - info["inet"] = list() + # info["inet"] = list() for ip in output.split(): net = ipcalc.Network(ip) item = dict() - item["ip"] = ip.split("/")[0] - item["netmask"] = net.netmask() + item["ip"] = str(ip.split("/")[0]) + item["netmask"] = str(net.netmask()) if 4 == net.version(): - item["subnet"] = net.network() - item["broadcast"] = net.broadcast() + item["subnet"] = str(net.network()) + item["broadcast"] = str(net.broadcast()) info["inet"].append(item) return info From bcb2dbb67ec3edd3dedd7fc6565ca9dd7917cde6 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 2 Jun 2015 15:45:47 +0800 Subject: [PATCH 36/58] Use netifaces to speedup query time. --- bundle.json | 2 +- ip/addr.py | 53 ++++++++++++++------------------------------- requirements.txt | 1 + route.py | 28 +++++++++--------------- tests/test_route.py | 24 +++++++------------- 5 files changed, 36 insertions(+), 72 deletions(-) diff --git a/bundle.json b/bundle.json index 26e3d28..c0afe01 100644 --- a/bundle.json +++ b/bundle.json @@ -1,6 +1,6 @@ { "name": "route", - "version": "0.9.3", + "version": "0.9.4", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", diff --git a/ip/addr.py b/ip/addr.py index 7399b23..5b951b4 100755 --- a/ip/addr.py +++ b/ip/addr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: UTF-8 -*- -import os import sh +import netifaces import ipcalc import logging @@ -18,7 +18,7 @@ # https://pypi.python.org/pypi/sh -_logger = logging.getLogger("sanji.route.ip.addr") +_logger = logging.getLogger("sanji.ethernet.ip.addr") def interfaces(): @@ -35,7 +35,7 @@ def interfaces(): # ifaces=$(ip a show | grep -Eo "[0-9]: wlan[0-9]" | sed "s/.*wlan//g") # ifaces=$(ip a show | grep -Eo '[0-9]: eth[0-9]' | awk '{print $2}') try: - ifaces = os.listdir("/sys/class/net") + ifaces = netifaces.interfaces() ifaces = [x for x in ifaces if not (x.startswith("lo") or x.startswith("mon."))] return ifaces @@ -62,14 +62,12 @@ def ifaddresses(iface): "broadcast": ""}]} Raises: - ValueError + ValueError: You must specify a valid interface name. """ - info = dict() - try: - info["mac"] = open("/sys/class/net/%s/address" % iface).read() - info["mac"] = info["mac"][:-1] # remove '\n' - except: - info["mac"] = None + full = netifaces.ifaddresses(iface) + + info = {} + info["mac"] = full[netifaces.AF_LINK][0]['addr'] try: info["link"] = open("/sys/class/net/%s/operstate" % iface).read() @@ -81,33 +79,14 @@ def ifaddresses(iface): except: info["link"] = 0 - # "ip addr show %s | grep inet | grep -v inet6 | awk '{print $2}'" - info["inet"] = list() - try: - ''' - output = sh.awk(sh.grep( - sh.ip("addr", "show", iface), "inet"), - "{print $2}") - ''' - output = sh.ip("addr", "show", iface) - try: - output = sh.awk(sh.grep(output, "inet"), "{print $2}") - except: - return info - except sh.ErrorReturnCode_1: - raise ValueError("Device \"%s\" does not exist." % iface) - except: - raise ValueError("Unknown error for \"%s\"." % iface) - - # info["inet"] = list() - for ip in output.split(): - net = ipcalc.Network(ip) - item = dict() - item["ip"] = str(ip.split("/")[0]) - item["netmask"] = str(net.netmask()) - if 4 == net.version(): - item["subnet"] = str(net.network()) - item["broadcast"] = str(net.broadcast()) + info["inet"] = [] + for ip in full[netifaces.AF_INET]: + item = {} + item["ip"] = ip['addr'] + item["netmask"] = ip['netmask'] + item["broadcast"] = ip["broadcast"] + net = ipcalc.Network("%s/%s" % (ip['addr'], ip['netmask'])) + item["subnet"] = str(net.network()) info["inet"].append(item) return info diff --git a/requirements.txt b/requirements.txt index 859f552..dd55615 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ paho-mqtt==1.1 sh ipcalc +netifaces sanji diff --git a/route.py b/route.py index ccbb064..4fa6cbb 100755 --- a/route.py +++ b/route.py @@ -2,6 +2,7 @@ # -*- coding: UTF-8 -*- import os +import netifaces import logging from sanji.core import Sanji from sanji.core import Route @@ -76,7 +77,7 @@ def cellular_connected(self, name, up=True): Args: name: cellular's interface name """ - default = dict() + default = {} if up: self.cellular = name default["interface"] = self.cellular @@ -118,16 +119,15 @@ def list_default(self): Return: default: dict format with "interface" and/or "gateway" """ - rules = ip.route.show() - default = dict() - for rule in rules: - if "default" in rule: - break + gws = netifaces.gateways() + default = {} + if gws['default'] != {}: + gw = gws['default'][netifaces.AF_INET] else: return default - default["interface"] = rule["dev"] - default["gateway"] = rule["default"] + default["gateway"] = gw[0] + default["interface"] = gw[1] return default def update_default(self, default): @@ -196,7 +196,7 @@ def update_interface_router(self, interface): iface["gateway"] = interface["gateway"] break else: - iface = dict() + iface = {} iface["interface"] = interface["name"] if "gateway" in interface: iface["gateway"] = interface["gateway"] @@ -246,13 +246,7 @@ def _put_default(self, message, response, schema=put_default_schema): data={"message": "Invalid input: %s." % e}) # retrieve the default gateway - rules = ip.route.show() - default = None - for rule in rules: - if "default" in rule: - default = rule - break - + default = self.list_default() try: self.update_default(message.data) return response(data=self.model.db) @@ -260,8 +254,6 @@ def _put_default(self, message, response, schema=put_default_schema): # recover the previous default gateway if any try: if default: - default["interface"] = default["dev"] - default["gateway"] = default["default"] self.update_default(default) except: _logger.info("Failed to recover the default gateway.") diff --git a/tests/test_route.py b/tests/test_route.py index e34999e..b4a800a 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -173,29 +173,21 @@ def mock_ip_addr_ifaddresses_ppp0_failed(iface): self.assertEqual(1, len(ifaces)) self.assertIn("eth0", ifaces) - @patch("route.ip.route.show") - def test__list_default(self, mock_route_show): + @patch("route.netifaces.gateways") + def test__list_default(self, mock_gateways): # case: get current default gateway - mock_route_show.return_value = [ - {"default": "192.168.3.254", "dev": "eth0"}, - {"src": "192.168.4.127", - "dev": "eth1", - "dest": "192.168.4.0/24"}] + mock_gateways.return_value = { + 'default': {2: ('192.168.3.254', 'eth0')}, + 2: [('192.168.3.254', 'eth0', True)]} default = self.bundle.list_default() self.assertEqual("eth0", default["interface"]) self.assertEqual("192.168.3.254", default["gateway"]) - @patch("route.ip.route.show") - def test__list_default__no_default(self, mock_route_show): + @patch("route.netifaces.gateways") + def test__list_default__no_default(self, mock_gateways): # case: no current default gateway - mock_route_show.return_value = [ - {"src": "192.168.3.127", - "dev": "eth0", - "dest": "192.168.3.0/24"}, - {"src": "192.168.4.127", - "dev": "eth1", - "dest": "192.168.4.0/24"}] + mock_gateways.return_value = {'default': {}} default = self.bundle.list_default() self.assertEqual({}, default) From 77ebb5cb29945283d5f560f296a0035612b6fbb7 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Fri, 5 Jun 2015 18:26:07 +0800 Subject: [PATCH 37/58] Update ip.addr, add script for dhcp client. --- ip/addr.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ip/addr.py b/ip/addr.py index 5b951b4..e91b57e 100755 --- a/ip/addr.py +++ b/ip/addr.py @@ -119,7 +119,7 @@ def ifupdown(iface, up): % iface) -def ifconfig(iface, dhcpc, ip="", netmask="24", gateway=""): +def ifconfig(iface, dhcpc, ip="", netmask="24", gateway="", script=None): """Set the interface to static IP or dynamic IP (by dhcpclient). Args: @@ -161,7 +161,10 @@ def ifconfig(iface, dhcpc, ip="", netmask="24", gateway=""): raise ValueError("Unknown error for \"%s\"." % iface) if dhcpc: - sh.dhclient(iface) + if script: + sh.dhclient("-sf", script, iface) + else: + sh.dhclient(iface) else: if ip: net = ipcalc.Network("%s/%s" % (ip, netmask)) From fdbc6500a2a6bd98655dc6678ff4377c45585b0b Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 9 Jun 2015 16:32:33 +0800 Subject: [PATCH 38/58] 0.9.4-1 --- bundle.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle.json b/bundle.json index c0afe01..70ca585 100644 --- a/bundle.json +++ b/bundle.json @@ -1,6 +1,6 @@ { "name": "route", - "version": "0.9.4", + "version": "0.9.4-1", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", From a2dcb266836849888c154a633ccbae4cea9e3528 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Wed, 10 Jun 2015 01:53:13 -0700 Subject: [PATCH 39/58] Try and catch the exception of ip.addr. --- bundle.json | 2 +- ip/addr.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/bundle.json b/bundle.json index 70ca585..757994c 100644 --- a/bundle.json +++ b/bundle.json @@ -1,6 +1,6 @@ { "name": "route", - "version": "0.9.4-1", + "version": "0.9.4-2", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", diff --git a/ip/addr.py b/ip/addr.py index e91b57e..cde5f6a 100755 --- a/ip/addr.py +++ b/ip/addr.py @@ -3,6 +3,7 @@ import sh import netifaces import ipcalc +import copy import logging # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net @@ -67,7 +68,10 @@ def ifaddresses(iface): full = netifaces.ifaddresses(iface) info = {} - info["mac"] = full[netifaces.AF_LINK][0]['addr'] + try: + info["mac"] = full[netifaces.AF_LINK][0]['addr'] + except: + info["mac"] = "" try: info["link"] = open("/sys/class/net/%s/operstate" % iface).read() @@ -80,13 +84,15 @@ def ifaddresses(iface): info["link"] = 0 info["inet"] = [] + if netifaces.AF_INET not in full: + return info + for ip in full[netifaces.AF_INET]: - item = {} - item["ip"] = ip['addr'] - item["netmask"] = ip['netmask'] - item["broadcast"] = ip["broadcast"] - net = ipcalc.Network("%s/%s" % (ip['addr'], ip['netmask'])) - item["subnet"] = str(net.network()) + item = copy.deepcopy(ip) + if "addr" in item: + item["ip"] = item.pop("addr") + net = ipcalc.Network("%s/%s" % (item["ip"], item["netmask"])) + item["subnet"] = str(net.network()) info["inet"].append(item) return info From 499aa5213c569acfab247e53b31fcd176c149823 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Fri, 12 Jun 2015 16:58:44 +0800 Subject: [PATCH 40/58] Update building policy for debian package. --- .travis.yml | 4 +- Makefile | 74 ++++++++++++++++++---- build-deb/Makefile | 44 +++++++++++++ build-deb/debian/README | 6 ++ build-deb/debian/changelog | 17 +++++ {debian => build-deb/debian}/compat | 0 {debian => build-deb/debian}/control | 8 +-- {debian => build-deb/debian}/copyright | 0 {debian => build-deb/debian}/docs | 0 {debian => build-deb/debian}/postinst | 6 +- {debian => build-deb/debian}/rules | 16 ++++- {debian => build-deb/debian}/source/format | 0 debian.mk | 33 ---------- debian/README | 6 -- debian/changelog | 5 -- 15 files changed, 152 insertions(+), 67 deletions(-) create mode 100644 build-deb/Makefile create mode 100644 build-deb/debian/README create mode 100644 build-deb/debian/changelog rename {debian => build-deb/debian}/compat (100%) rename {debian => build-deb/debian}/control (68%) rename {debian => build-deb/debian}/copyright (100%) rename {debian => build-deb/debian}/docs (100%) rename {debian => build-deb/debian}/postinst (86%) rename {debian => build-deb/debian}/rules (53%) rename {debian => build-deb/debian}/source/format (100%) delete mode 100644 debian.mk delete mode 100644 debian/README delete mode 100644 debian/changelog diff --git a/.travis.yml b/.travis.yml index ea06d90..de2028b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,9 @@ python: install: - pip install -r requirements.txt - pip install -r tests/requirements.txt -script: make +script: + - make pylint + - make test after_success: coveralls notifications: diff --git a/Makefile b/Makefile index e1c5b97..8b85306 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,69 @@ +NAME = $(shell cat bundle.json | sed -n 's/"name"//p' | tr -d '", :') +VERSION = $(shell cat bundle.json | sed -n 's/"version"//p' | tr -d '", :') -FILES = README.md bundle.json route.py requirements.txt -DIRS = data ip +PROJECT = sanji-bundle-$(NAME) -all: pylint test +DISTDIR = $(PROJECT)-$(VERSION) +ARCHIVE = $(CURDIR)/$(DISTDIR).tar.gz + +SANJI_VER ?= 1.0 +INSTALL_DIR = $(DESTDIR)/usr/lib/sanji-$(SANJI_VER)/$(NAME) +STAGING_DIR = $(CURDIR)/staging +PROJECT_STAGING_DIR = $(STAGING_DIR)/$(DISTDIR) + +TARGET_FILES = \ + bundle.json \ + requirements.txt \ + route.py \ + data/route.json.factory \ + ip/__init__.py \ + ip/addr.py \ + ip/route.py +DIST_FILES= \ + $(TARGET_FILES) \ + README.md \ + Makefile \ + tests/requirements.txt \ + tests/test_route.py \ + tests/data/route.json.factory \ + tests/test_e2e/bundle.json \ + tests/test_e2e/view_routes.py +INSTALL_FILES=$(addprefix $(INSTALL_DIR)/,$(TARGET_FILES)) +STAGING_FILES=$(addprefix $(PROJECT_STAGING_DIR)/,$(DIST_FILES)) + + +all: + +clean: + rm -rf $(DISTDIR)*.tar.gz $(STAGING_DIR) + @rm -rf .coverage + @find ./ -name *.pyc | xargs rm -rf + +distclean: clean pylint: flake8 -v --exclude=.git,__init__.py . test: - nosetests --with-coverage --cover-erase --cover-package=route -v + nosetests --with-coverage --cover-erase --cover-package=$(NAME) -v -deb: - mkdir -p deb - cp -a debian deb/ - cp -a debian.mk deb/Makefile - cp -a README.md $(FILES) $(DIRS) deb/ - (cd deb; \ - dpkg-buildpackage -us -uc -rfakeroot;) +dist: $(ARCHIVE) -clean: - rm -rf deb +$(ARCHIVE): distclean $(STAGING_FILES) + @mkdir -p $(STAGING_DIR) + cd $(STAGING_DIR) && \ + tar zcf $@ $(DISTDIR) + +$(PROJECT_STAGING_DIR)/%: % + @mkdir -p $(dir $@) + @cp -a $< $@ + +install: $(INSTALL_FILES) + +$(INSTALL_DIR)/%: % + @mkdir -p $(dir $@) + @cp -a $< $@ + +uninstall: + -rm $(addprefix $(INSTALL_DIR)/,$(TARGET_FILES)) -.PHONY: pylint test +.PHONY: clean dist pylint test diff --git a/build-deb/Makefile b/build-deb/Makefile new file mode 100644 index 0000000..b99c73a --- /dev/null +++ b/build-deb/Makefile @@ -0,0 +1,44 @@ +PROJ_DIR = $(abspath ..) + +NAME = $(shell cat $(PROJ_DIR)/bundle.json | sed -n 's/"name"//p' | tr -d '", :') +VERSION = $(shell cat $(PROJ_DIR)/bundle.json | sed -n 's/"version"//p' | tr -d '", :') + +PROJECT = sanji-bundle-$(NAME) +DEBVERSION = 1 +DIST ?= unstable + +STAGING_DIR = $(abspath $(PROJECT)-$(VERSION)) +UPSTREAM_ARCHIVE = $(PROJECT)-$(VERSION).tar.gz + +FILES = \ + $(STAGING_DIR)/debian/changelog \ + $(STAGING_DIR)/debian/compat \ + $(STAGING_DIR)/debian/control \ + $(STAGING_DIR)/debian/copyright \ + $(STAGING_DIR)/debian/docs \ + $(STAGING_DIR)/debian/postinst \ + $(STAGING_DIR)/debian/README \ + $(STAGING_DIR)/debian/rules \ + $(STAGING_DIR)/debian/source/format + +.PHONY: all build + +all: build + +build: extract-upstream $(FILES) + cd $(STAGING_DIR) && \ + dpkg-buildpackage -us -uc -rfakeroot + +$(STAGING_DIR)/debian/%: $(PROJ_DIR)/build-deb/debian/% + mkdir -p $(dir $@) + cp $< $@ + +extract-upstream: + tar zxf $(PROJ_DIR)/$(UPSTREAM_ARCHIVE) + +changelog: + dch -v $(VERSION)-$(DEBVERSION) -D $(DIST) -M -u low \ + --release-heuristic log + +clean: + rm -rf $(STAGING_DIR) $(PROJECT)-* $(PROJECT)_* diff --git a/build-deb/debian/README b/build-deb/debian/README new file mode 100644 index 0000000..e8881f1 --- /dev/null +++ b/build-deb/debian/README @@ -0,0 +1,6 @@ +The Debian Package route +---------------------------- + +Comments regarding the Package + + -- Aeluin Chen Fri, 12 Jun 2015 16:48:57 +0800 diff --git a/build-deb/debian/changelog b/build-deb/debian/changelog new file mode 100644 index 0000000..6865e44 --- /dev/null +++ b/build-deb/debian/changelog @@ -0,0 +1,17 @@ +sanji-bundle-route (0.9.4-2) unstable; urgency=low + + * Update building policy for debian package. + + -- Aeluin Chen Fri, 12 Jun 2015 16:57:12 +0800 + +sanji-bundle-route (0.9.4-1) unstable; urgency=low + + * Use netifaces to speedup query time. + + -- Aeluin Chen Fri, 05 Jun 2015 18:32:58 +0800 + +sanji-bundle-route (0.9.0) unstable; urgency=low + + * Initial Release. + + -- Aeluin Chen Fri, 05 Jun 2015 18:27:19 +0800 diff --git a/debian/compat b/build-deb/debian/compat similarity index 100% rename from debian/compat rename to build-deb/debian/compat diff --git a/debian/control b/build-deb/debian/control similarity index 68% rename from debian/control rename to build-deb/debian/control index f8f1ade..786f5b0 100644 --- a/debian/control +++ b/build-deb/debian/control @@ -6,13 +6,13 @@ Build-Depends-Indep: python (>= 2.7) Standards-Version: 3.9.3 Section: libs Homepage: http://www.moxa.com -#Vcs-Git: git@github.com:lwindg/sanji-bundle-routes.git -#Vcs-Browser: https://github.com/lwindg/sanji-bundle-routes +#Vcs-Git: +#Vcs-Browser: X-Python-Version: >= 2.5 Package: sanji-bundle-route Section: libs Architecture: all Depends: ${shlibs:Depends}, ${misc:Depends}, python2.7, python-pip -Description: A sanji library for configure the default route. - +Description: Handle the routing table + diff --git a/debian/copyright b/build-deb/debian/copyright similarity index 100% rename from debian/copyright rename to build-deb/debian/copyright diff --git a/debian/docs b/build-deb/debian/docs similarity index 100% rename from debian/docs rename to build-deb/debian/docs diff --git a/debian/postinst b/build-deb/debian/postinst similarity index 86% rename from debian/postinst rename to build-deb/debian/postinst index dcbc2e2..8715f6a 100644 --- a/debian/postinst +++ b/build-deb/debian/postinst @@ -1,5 +1,5 @@ #!/bin/sh -# postinst script for sanji-bundle-route +# postinst script for route # # see: dh_installdeb(1) @@ -20,9 +20,9 @@ set -e case "$1" in configure) - pip install -r /tmp/packages/bundle-requirements.txt || \ + pip install -r /tmp/packages/requirements.txt || \ pip install --no-index --find-links file:/tmp/packages \ - -r /tmp/packages/bundle-requirements.txt + -r /tmp/packages/requirements.txt rm -rf /tmp/packages ;; diff --git a/debian/rules b/build-deb/debian/rules similarity index 53% rename from debian/rules rename to build-deb/debian/rules index 2ebce13..e746a10 100755 --- a/debian/rules +++ b/build-deb/debian/rules @@ -9,5 +9,19 @@ # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 +DEB_PACKAGE := $(strip $(shell dh_listpackages -i 2>/dev/null || dh_listpackages -i)) +DEB_DESTDIR := $(CURDIR)/debian/$(DEB_PACKAGE) + +DEB_PYPDIR := $(DEB_DESTDIR)/tmp/packages + + %: - dh $@ --with python2 + dh $@ + +override_dh_auto_test: + +override_dh_auto_install: + dh_auto_install + mkdir -p $(DEB_PYPDIR) + cp -a requirements.txt $(DEB_PYPDIR) + pip install -r requirements.txt --download $(DEB_PYPDIR) diff --git a/debian/source/format b/build-deb/debian/source/format similarity index 100% rename from debian/source/format rename to build-deb/debian/source/format diff --git a/debian.mk b/debian.mk deleted file mode 100644 index 4f229d6..0000000 --- a/debian.mk +++ /dev/null @@ -1,33 +0,0 @@ - -# Where to put executable commands/icons/conf on 'make install'? -SANJI_VER = 1.0 -RESOURCE = network/route -LIBDIR = $(DESTDIR)/usr/lib/sanji-$(SANJI_VER)/$(RESOURCE) -TMPDIR = $(DESTDIR)/tmp - -FILES = bundle.json route.py -DIRS = data ip - - -all: - mkdir -p $(CURDIR)/packages - pip install -r $(CURDIR)/packages/requirements.txt --download \ - $(CURDIR)/packages || true - cp -a $(CURDIR)/requirements.txt \ - $(CURDIR)/packages/bundle-requirements.txt - -clean: - # do nothing - -distclean: clean - - -install: all - install -d $(LIBDIR) - install -d $(TMPDIR) - install $(FILES) $(LIBDIR) - cp -a $(DIRS) $(LIBDIR) - cp -a packages $(TMPDIR) - -uninstall: - -rm $(addprefix $(LIBDIR)/,$(FILES)) diff --git a/debian/README b/debian/README deleted file mode 100644 index 2999cb0..0000000 --- a/debian/README +++ /dev/null @@ -1,6 +0,0 @@ -The Debian Package sanji-bundle-route ----------------------------- - -Comments regarding the Package - - -- Aeluin Chen Tue, 02 Dec 2014 17:02:46 +0800 diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index 064ab9e..0000000 --- a/debian/changelog +++ /dev/null @@ -1,5 +0,0 @@ -sanji-bundle-route (1.0.0) unstable; urgency=low - - * Initial Release. - - -- Aeluin Chen Tue, 02 Dec 2014 17:02:46 +0800 From 7242be67d756d24053a4999eb0d2f04ec0d34f89 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 3 Sep 2015 06:56:09 -0700 Subject: [PATCH 41/58] Add API for update router information API /network/routes/db for update database. --- bundle.json | 6 +++++- route.py | 30 ++++++++++++++++++++++++------ tests/test_route.py | 25 +++++++++++++++++++++---- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/bundle.json b/bundle.json index 757994c..1fcc3d1 100644 --- a/bundle.json +++ b/bundle.json @@ -17,12 +17,16 @@ "resources": [ { "role": "view", - "resource": "/network/interfaces" + "resource": "/network/interface" }, { "methods": ["get"], "resource": "/network/routes/interfaces" }, + { + "methods": ["get","put"], + "resource": "/network/routes/db" + }, { "methods": ["get","put"], "resource": "/network/routes/default" diff --git a/route.py b/route.py index 4fa6cbb..de80043 100755 --- a/route.py +++ b/route.py @@ -96,10 +96,6 @@ def list_interfaces(self): ifaces = ip.addr.interfaces() except: return {} - """ - except Exception as e: - raise e - """ # list connected interfaces data = [] @@ -263,8 +259,30 @@ def _put_default(self, message, response, schema=put_default_schema): "Update default gateway failed: %s" % e}) - @Route(methods="put", resource="/network/interfaces") - def _event_router_info(self, message): + def set_router_db(self, message, response): + """ + Update router database batch or by interface. + """ + if type(message.data) is list: + for iface in message.data: + self.update_interface_router(iface) + return response(data=self.interfaces) + elif type(message.data) is dict: + self.update_interface_router(message.data) + return response(data=message.data) + return response(code=400, + data={"message": "Wrong type of router database."}) + + @Route(methods="put", resource="/network/routes/db") + def _set_router_db(self, message, response): + return self.set_router_db(message, response) + + @Route(methods="get", resource="/network/routes/db") + def _get_router_db(self, message, response): + return response(data=self.interfaces) + + @Route(methods="put", resource="/network/interface") + def _event_router_db(self, message): self.update_interface_router(message.data) ''' diff --git a/tests/test_route.py b/tests/test_route.py index b4a800a..bbb7b80 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -8,6 +8,7 @@ import unittest from mock import patch +from mock import Mock from sanji.connection.mockup import Mockup from sanji.message import Message @@ -62,7 +63,7 @@ def setUp(self, mock_update_default): self.bundle = IPRoute(connection=Mockup()) def tearDown(self): - self.bundle.stop() + #self.bundle.stop() self.bundle = None try: os.remove("%s/data/%s.json" % (dirpath, self.name)) @@ -95,7 +96,7 @@ def test__load__backup_conf(self): def test__load__no_conf(self): # case: cannot load any configuration - with self.assertRaises(IOError): + with self.assertRaises(RuntimeError): self.bundle.load("%s/mock" % dirpath) def test__save(self): @@ -472,14 +473,30 @@ def test_put_default_failed_recover_failed(self, mock_update_default): pass """ + @patch.object(IPRoute, "update_default") + def test__set_router_db__add(self, mock_update_default): + """ + set_router_db: add one interface's router info to database + """ + # arrange + iface = {"name": "eth0", "gateway": "192.168.3.127"} + message = Message({"data": iface}) + mock_func = Mock(code=200, data=None) + + # act + self.bundle.set_router_db(message=message, response=mock_func) + + # assert + self.assertEqual(mock_func.call_args_list[0][1]["data"], iface) + @patch.object(IPRoute, "update_interface_router") - def test__event_router_info(self, mock_update_interface_router): + def test__event_router_db(self, mock_update_interface_router): # case: update the router information by interface message = Message({"data": {}, "query": {}, "param": {}}) message.data["interface"] = "eth1" message.data["gateway"] = "192.168.41.254" - self.bundle._event_router_info(message=message, test=True) + self.bundle._event_router_db(message=message, test=True) """ @patch.object(IPRoute, "update_interface_router") From 5220398722974ab60cba7403b4ca4fe94f0caf4b Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 3 Sep 2015 07:04:04 -0700 Subject: [PATCH 42/58] Change config key from "interface" to "default" --- data/route.json.factory | 2 +- route.py | 14 +++++++------- tests/data/route.json.factory | 2 +- tests/test_route.py | 16 ++++++++-------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/data/route.json.factory b/data/route.json.factory index cd61291..f0f970f 100644 --- a/data/route.json.factory +++ b/data/route.json.factory @@ -1,3 +1,3 @@ { - "interface": "eth0" + "default": "eth0" } diff --git a/route.py b/route.py index de80043..a385590 100755 --- a/route.py +++ b/route.py @@ -84,7 +84,7 @@ def cellular_connected(self, name, up=True): self.update_default(default) else: self.cellular = None - if name == self.model.db["interface"]: + if name == self.model.db["default"]: self.update_default(default) def list_interfaces(self): @@ -161,7 +161,7 @@ def update_default(self, default): ip.route.add("default", default["interface"]) except Exception as e: raise e - self.model.db["interface"] = default["interface"] + self.model.db["default"] = default["interface"] # delete the default gateway else: @@ -169,8 +169,8 @@ def update_default(self, default): ip.route.delete("default") except Exception as e: raise e - if "interface" in self.model.db: - self.model.db.pop("interface") + if "default" in self.model.db: + self.model.db.pop("default") self.save() @@ -199,7 +199,7 @@ def update_interface_router(self, interface): self.interfaces.append(iface) # check if the default gateway need to be modified - if iface["interface"] == self.model.db["interface"]: + if iface["interface"] == self.model.db["default"]: try: self.update_default(iface) except: @@ -219,8 +219,8 @@ def _get_default(self, message, response): Get default gateway. """ default = self.list_default() - if self.model.db and "interface" in self.model.db and default and \ - self.model.db["interface"] == default["interface"]: + if self.model.db and "default" in self.model.db and default and \ + self.model.db["default"] == default["interface"]: return response(data=default) return response(data=self.model.db) diff --git a/tests/data/route.json.factory b/tests/data/route.json.factory index cd61291..f0f970f 100644 --- a/tests/data/route.json.factory +++ b/tests/data/route.json.factory @@ -1,3 +1,3 @@ { - "interface": "eth0" + "default": "eth0" } diff --git a/tests/test_route.py b/tests/test_route.py index bbb7b80..1b51787 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -86,13 +86,13 @@ def test__init__no_conf(self, mock_update_default): def test__load__current_conf(self): # case: load current configuration self.bundle.load(dirpath) - self.assertEqual("eth0", self.bundle.model.db["interface"]) + self.assertEqual("eth0", self.bundle.model.db["default"]) def test__load__backup_conf(self): # case: load backup configuration os.remove("%s/data/%s.json" % (dirpath, self.name)) self.bundle.load(dirpath) - self.assertEqual("eth0", self.bundle.model.db["interface"]) + self.assertEqual("eth0", self.bundle.model.db["default"]) def test__load__no_conf(self): # case: cannot load any configuration @@ -198,7 +198,7 @@ def test__update_default__delete(self, mock_route_delete): # case: delete the default gateway default = dict() self.bundle.update_default(default) - self.assertNotIn("interface", self.bundle.model.db) + self.assertNotIn("default", self.bundle.model.db) @patch("route.ip.route.delete") def test__update_default__delete_failed(self, mock_route_delete): @@ -208,7 +208,7 @@ def test__update_default__delete_failed(self, mock_route_delete): default = dict() with self.assertRaises(IOError): self.bundle.update_default(default) - self.assertIn("interface", self.bundle.model.db) + self.assertIn("default", self.bundle.model.db) @patch("route.ip.route.add") @patch("route.ip.route.delete") @@ -221,7 +221,7 @@ def test__update_default__add_without_gateway( default = dict() default["interface"] = "eth1" self.bundle.update_default(default) - self.assertIn("eth1", self.bundle.model.db["interface"]) + self.assertIn("eth1", self.bundle.model.db["default"]) @patch("route.ip.route.add") @patch("route.ip.route.delete") @@ -235,7 +235,7 @@ def test__update_default__add_with_gateway( default["interface"] = "eth1" default["gateway"] = "192.168.4.254" self.bundle.update_default(default) - self.assertIn("eth1", self.bundle.model.db["interface"]) + self.assertIn("eth1", self.bundle.model.db["default"]) @patch.object(IPRoute, 'list_interfaces') def test__update_default__add_failed_iface_down( @@ -249,7 +249,7 @@ def test__update_default__add_failed_iface_down( default["gateway"] = "192.168.4.254" with self.assertRaises(ValueError): self.bundle.update_default(default) - self.assertIn("interface", self.bundle.model.db) + self.assertIn("default", self.bundle.model.db) @patch.object(IPRoute, 'list_interfaces') def test__update_default__add_failed_cellular_connected( @@ -405,7 +405,7 @@ def test__get_default__different(self, mock_list_default): def resp(code=200, data=None): self.assertEqual(200, code) - self.assertEqual("eth0", data["interface"]) + self.assertEqual("eth0", data["default"]) self.bundle._get_default(message=message, response=resp, test=True) def test__get_default__empty(self): From 60b0bd831e5ece89c2d4b92358d098bb69a8fa46 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 3 Sep 2015 07:17:01 -0700 Subject: [PATCH 43/58] Revert "Change config key from "interface" to "default"" This reverts commit 5220398722974ab60cba7403b4ca4fe94f0caf4b. --- data/route.json.factory | 2 +- route.py | 14 +++++++------- tests/data/route.json.factory | 2 +- tests/test_route.py | 16 ++++++++-------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/data/route.json.factory b/data/route.json.factory index f0f970f..cd61291 100644 --- a/data/route.json.factory +++ b/data/route.json.factory @@ -1,3 +1,3 @@ { - "default": "eth0" + "interface": "eth0" } diff --git a/route.py b/route.py index a385590..de80043 100755 --- a/route.py +++ b/route.py @@ -84,7 +84,7 @@ def cellular_connected(self, name, up=True): self.update_default(default) else: self.cellular = None - if name == self.model.db["default"]: + if name == self.model.db["interface"]: self.update_default(default) def list_interfaces(self): @@ -161,7 +161,7 @@ def update_default(self, default): ip.route.add("default", default["interface"]) except Exception as e: raise e - self.model.db["default"] = default["interface"] + self.model.db["interface"] = default["interface"] # delete the default gateway else: @@ -169,8 +169,8 @@ def update_default(self, default): ip.route.delete("default") except Exception as e: raise e - if "default" in self.model.db: - self.model.db.pop("default") + if "interface" in self.model.db: + self.model.db.pop("interface") self.save() @@ -199,7 +199,7 @@ def update_interface_router(self, interface): self.interfaces.append(iface) # check if the default gateway need to be modified - if iface["interface"] == self.model.db["default"]: + if iface["interface"] == self.model.db["interface"]: try: self.update_default(iface) except: @@ -219,8 +219,8 @@ def _get_default(self, message, response): Get default gateway. """ default = self.list_default() - if self.model.db and "default" in self.model.db and default and \ - self.model.db["default"] == default["interface"]: + if self.model.db and "interface" in self.model.db and default and \ + self.model.db["interface"] == default["interface"]: return response(data=default) return response(data=self.model.db) diff --git a/tests/data/route.json.factory b/tests/data/route.json.factory index f0f970f..cd61291 100644 --- a/tests/data/route.json.factory +++ b/tests/data/route.json.factory @@ -1,3 +1,3 @@ { - "default": "eth0" + "interface": "eth0" } diff --git a/tests/test_route.py b/tests/test_route.py index 1b51787..bbb7b80 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -86,13 +86,13 @@ def test__init__no_conf(self, mock_update_default): def test__load__current_conf(self): # case: load current configuration self.bundle.load(dirpath) - self.assertEqual("eth0", self.bundle.model.db["default"]) + self.assertEqual("eth0", self.bundle.model.db["interface"]) def test__load__backup_conf(self): # case: load backup configuration os.remove("%s/data/%s.json" % (dirpath, self.name)) self.bundle.load(dirpath) - self.assertEqual("eth0", self.bundle.model.db["default"]) + self.assertEqual("eth0", self.bundle.model.db["interface"]) def test__load__no_conf(self): # case: cannot load any configuration @@ -198,7 +198,7 @@ def test__update_default__delete(self, mock_route_delete): # case: delete the default gateway default = dict() self.bundle.update_default(default) - self.assertNotIn("default", self.bundle.model.db) + self.assertNotIn("interface", self.bundle.model.db) @patch("route.ip.route.delete") def test__update_default__delete_failed(self, mock_route_delete): @@ -208,7 +208,7 @@ def test__update_default__delete_failed(self, mock_route_delete): default = dict() with self.assertRaises(IOError): self.bundle.update_default(default) - self.assertIn("default", self.bundle.model.db) + self.assertIn("interface", self.bundle.model.db) @patch("route.ip.route.add") @patch("route.ip.route.delete") @@ -221,7 +221,7 @@ def test__update_default__add_without_gateway( default = dict() default["interface"] = "eth1" self.bundle.update_default(default) - self.assertIn("eth1", self.bundle.model.db["default"]) + self.assertIn("eth1", self.bundle.model.db["interface"]) @patch("route.ip.route.add") @patch("route.ip.route.delete") @@ -235,7 +235,7 @@ def test__update_default__add_with_gateway( default["interface"] = "eth1" default["gateway"] = "192.168.4.254" self.bundle.update_default(default) - self.assertIn("eth1", self.bundle.model.db["default"]) + self.assertIn("eth1", self.bundle.model.db["interface"]) @patch.object(IPRoute, 'list_interfaces') def test__update_default__add_failed_iface_down( @@ -249,7 +249,7 @@ def test__update_default__add_failed_iface_down( default["gateway"] = "192.168.4.254" with self.assertRaises(ValueError): self.bundle.update_default(default) - self.assertIn("default", self.bundle.model.db) + self.assertIn("interface", self.bundle.model.db) @patch.object(IPRoute, 'list_interfaces') def test__update_default__add_failed_cellular_connected( @@ -405,7 +405,7 @@ def test__get_default__different(self, mock_list_default): def resp(code=200, data=None): self.assertEqual(200, code) - self.assertEqual("eth0", data["default"]) + self.assertEqual("eth0", data["interface"]) self.bundle._get_default(message=message, response=resp, test=True) def test__get_default__empty(self): From 5303148f77d35192af97c5687ded044a3fb12071 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 3 Sep 2015 07:46:28 -0700 Subject: [PATCH 44/58] Add FIXME comments for next update Secondary default route will be supported. --- route.py | 32 +++++++++----------------------- tests/test_route.py | 2 +- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/route.py b/route.py index de80043..0f2d5f6 100755 --- a/route.py +++ b/route.py @@ -136,6 +136,8 @@ def update_default(self, default): optional. """ # change the default gateway + # FIXME: only "gateway" without interface is also available + # FIXME: add "secondary" default route rule if "interface" in default and default["interface"]: ifaces = self.list_interfaces() if not ifaces or default["interface"] not in ifaces: @@ -154,11 +156,15 @@ def update_default(self, default): try: ip.route.delete("default") - if "gateway" in default: + if "gateway" in default and "interface" in default: ip.route.add("default", default["interface"], default["gateway"]) - else: + elif "interface" in default: ip.route.add("default", default["interface"]) + elif "gateway" in default: + ip.route.add("default", "", default["gateway"]) + else: + raise ValueError("Invalid default route.") except Exception as e: raise e self.model.db["interface"] = default["interface"] @@ -219,6 +225,7 @@ def _get_default(self, message, response): Get default gateway. """ default = self.list_default() + # FIXME: show real time value instead of settings? if self.model.db and "interface" in self.model.db and default and \ self.model.db["interface"] == default["interface"]: return response(data=default) @@ -285,27 +292,6 @@ def _get_router_db(self, message, response): def _event_router_db(self, message): self.update_interface_router(message.data) - ''' - @Route(methods="put", resource="/network/ethernets/:id") - def _hook_put_ethernet_by_id(self, message, response): - """ - Save the interface name with its gateway and update the default - gateway if needed. - """ - self.update_interface_router(message.data) - return response(data=self.model.db) - - @Route(methods="put", resource="/network/ethernets") - def _hook_put_ethernets(self, message, response): - """ - Save the interface name with its gateway and update the default - gateway if needed. - """ - for iface in message.data: - self.update_interface_router(iface) - return response(data=self.model.db) - ''' - if __name__ == "__main__": FORMAT = "%(asctime)s - %(levelname)s - %(lineno)s - %(message)s" diff --git a/tests/test_route.py b/tests/test_route.py index bbb7b80..c80540f 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -63,7 +63,7 @@ def setUp(self, mock_update_default): self.bundle = IPRoute(connection=Mockup()) def tearDown(self): - #self.bundle.stop() + self.bundle.stop() self.bundle = None try: os.remove("%s/data/%s.json" % (dirpath, self.name)) From 8933f67cd83f32f5d923aa93f8aed193457f3065 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Fri, 4 Sep 2015 18:36:47 +0800 Subject: [PATCH 45/58] Change some method's name --- route.py | 16 +++++++------- tests/test_route.py | 52 ++++++++++++++++++++++----------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/route.py b/route.py index 0f2d5f6..29963cf 100755 --- a/route.py +++ b/route.py @@ -108,7 +108,7 @@ def list_interfaces(self): data.append(iface) return data - def list_default(self): + def get_default(self): """ Retrieve the default gateway @@ -180,7 +180,7 @@ def update_default(self, default): self.save() - def update_interface_router(self, interface): + def update_router(self, interface): """ Save the interface name with its gateway and update the default gateway if needed. @@ -224,7 +224,7 @@ def _get_default(self, message, response): """ Get default gateway. """ - default = self.list_default() + default = self.get_default() # FIXME: show real time value instead of settings? if self.model.db and "interface" in self.model.db and default and \ self.model.db["interface"] == default["interface"]: @@ -249,7 +249,7 @@ def _put_default(self, message, response, schema=put_default_schema): data={"message": "Invalid input: %s." % e}) # retrieve the default gateway - default = self.list_default() + default = self.get_default() try: self.update_default(message.data) return response(data=self.model.db) @@ -272,13 +272,13 @@ def set_router_db(self, message, response): """ if type(message.data) is list: for iface in message.data: - self.update_interface_router(iface) + self.update_router(iface) return response(data=self.interfaces) elif type(message.data) is dict: - self.update_interface_router(message.data) + self.update_router(message.data) return response(data=message.data) return response(code=400, - data={"message": "Wrong type of router database."}) + data={"message": "Wrong type of router database."}) @Route(methods="put", resource="/network/routes/db") def _set_router_db(self, message, response): @@ -290,7 +290,7 @@ def _get_router_db(self, message, response): @Route(methods="put", resource="/network/interface") def _event_router_db(self, message): - self.update_interface_router(message.data) + self.update_router(message.data) if __name__ == "__main__": diff --git a/tests/test_route.py b/tests/test_route.py index c80540f..a967252 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -96,7 +96,7 @@ def test__load__backup_conf(self): def test__load__no_conf(self): # case: cannot load any configuration - with self.assertRaises(RuntimeError): + with self.assertRaises(IOError): self.bundle.load("%s/mock" % dirpath) def test__save(self): @@ -175,22 +175,22 @@ def mock_ip_addr_ifaddresses_ppp0_failed(iface): self.assertIn("eth0", ifaces) @patch("route.netifaces.gateways") - def test__list_default(self, mock_gateways): + def test__get_default(self, mock_gateways): # case: get current default gateway mock_gateways.return_value = { 'default': {2: ('192.168.3.254', 'eth0')}, 2: [('192.168.3.254', 'eth0', True)]} - default = self.bundle.list_default() + default = self.bundle.get_default() self.assertEqual("eth0", default["interface"]) self.assertEqual("192.168.3.254", default["gateway"]) @patch("route.netifaces.gateways") - def test__list_default__no_default(self, mock_gateways): + def test__get_default__no_default(self, mock_gateways): # case: no current default gateway mock_gateways.return_value = {'default': {}} - default = self.bundle.list_default() + default = self.bundle.get_default() self.assertEqual({}, default) @patch("route.ip.route.delete") @@ -280,7 +280,7 @@ def test__update_default__add_failed( self.bundle.update_default(default) @patch.object(IPRoute, 'update_default') - def test__update_interface_router__update_interface( + def test__update_router__update_interface( self, mock_update_default): # arrange self.bundle.interfaces = [ @@ -289,7 +289,7 @@ def test__update_interface_router__update_interface( iface = {"name": "eth1", "gateway": "192.168.41.254"} # act - self.bundle.update_interface_router(iface) + self.bundle.update_router(iface) # assert self.assertEqual(2, len(self.bundle.interfaces)) @@ -299,7 +299,7 @@ def test__update_interface_router__update_interface( self.bundle.interfaces) @patch.object(IPRoute, 'update_default') - def test__update_interface_router__add_interface_with_gateway( + def test__update_router__add_interface_with_gateway( self, mock_update_default): # arrange self.bundle.interfaces = [ @@ -307,7 +307,7 @@ def test__update_interface_router__add_interface_with_gateway( iface = {"name": "eth1", "gateway": "192.168.41.254"} # act - self.bundle.update_interface_router(iface) + self.bundle.update_router(iface) # assert self.assertEqual(2, len(self.bundle.interfaces)) @@ -317,7 +317,7 @@ def test__update_interface_router__add_interface_with_gateway( self.bundle.interfaces) @patch.object(IPRoute, 'update_default') - def test__update_interface_router__add_interface_without_gateway( + def test__update_router__add_interface_without_gateway( self, mock_update_default): # arrange self.bundle.interfaces = [ @@ -325,7 +325,7 @@ def test__update_interface_router__add_interface_without_gateway( iface = {"name": "eth1"} # act - self.bundle.update_interface_router(iface) + self.bundle.update_router(iface) # assert self.assertEqual(2, len(self.bundle.interfaces)) @@ -335,7 +335,7 @@ def test__update_interface_router__add_interface_without_gateway( self.bundle.interfaces) @patch.object(IPRoute, 'update_default') - def test__update_interface_router__update_default( + def test__update_router__update_default( self, mock_update_default): # arrange self.bundle.interfaces = [ @@ -344,7 +344,7 @@ def test__update_interface_router__update_default( iface = {"name": "eth0", "gateway": "192.168.31.254"} # act - self.bundle.update_interface_router(iface) + self.bundle.update_router(iface) # assert self.assertEqual(2, len(self.bundle.interfaces)) @@ -381,9 +381,9 @@ def resp(code=404, data=None): self.assertEqual({}, data) self.bundle._get_interfaces(message=message, response=resp, test=True) - @patch.object(IPRoute, 'list_default') - def test__get_default__match(self, mock_list_default): - mock_list_default.return_value = { + @patch.object(IPRoute, 'get_default') + def test___get_default__match(self, mock_get_default): + mock_get_default.return_value = { "interface": "eth0", "gateway": "192.168.3.254"} # case: get default gateway info. @@ -395,9 +395,9 @@ def resp(code=200, data=None): self.assertEqual("192.168.3.254", data["gateway"]) self.bundle._get_default(message=message, response=resp, test=True) - @patch.object(IPRoute, 'list_default') - def test__get_default__different(self, mock_list_default): - mock_list_default.return_value = { + @patch.object(IPRoute, 'get_default') + def test___get_default__different(self, mock_get_default): + mock_get_default.return_value = { "interface": "eth1", "gateway": "192.168.4.254"} # case: get default gateway info. @@ -408,7 +408,7 @@ def resp(code=200, data=None): self.assertEqual("eth0", data["interface"]) self.bundle._get_default(message=message, response=resp, test=True) - def test__get_default__empty(self): + def test___get_default__empty(self): self.bundle.model.db = {} # case: no default gateway set @@ -489,8 +489,8 @@ def test__set_router_db__add(self, mock_update_default): # assert self.assertEqual(mock_func.call_args_list[0][1]["data"], iface) - @patch.object(IPRoute, "update_interface_router") - def test__event_router_db(self, mock_update_interface_router): + @patch.object(IPRoute, "update_router") + def test__event_router_db(self, mock_update_router): # case: update the router information by interface message = Message({"data": {}, "query": {}, "param": {}}) message.data["interface"] = "eth1" @@ -499,8 +499,8 @@ def test__event_router_db(self, mock_update_interface_router): self.bundle._event_router_db(message=message, test=True) """ - @patch.object(IPRoute, "update_interface_router") - def test__hook_put_ethernet_by_id(self, mock_update_interface_router): + @patch.object(IPRoute, "update_router") + def test__hook_put_ethernet_by_id(self, mock_update_router): # case: test if default gateway changed message = Message({"data": {}, "query": {}, "param": {}}) message.data["name"] = "eth1" @@ -511,8 +511,8 @@ def resp(code=200, data=None): self.bundle._hook_put_ethernet_by_id(message=message, response=resp, test=True) - @patch.object(IPRoute, "update_interface_router") - def test__hook_put_ethernets(self, mock_update_interface_router): + @patch.object(IPRoute, "update_router") + def test__hook_put_ethernets(self, mock_update_router): # case: test if default gateway changed message = Message({"data": [], "query": {}, "param": {}}) iface = {"id": 2, "name": "eth1", "gateway": "192.168.4.254"} From 9d0c3a6b2440b362452a033cb3718d0f76c75108 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 8 Sep 2015 13:55:08 +0800 Subject: [PATCH 46/58] Remove unused cellular part --- route.py | 43 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/route.py b/route.py index 29963cf..46562af 100755 --- a/route.py +++ b/route.py @@ -24,6 +24,9 @@ class IPRoute(Sanji): Attributes: model: database with json format. """ + + update_interval = 60 + def init(self, *args, **kwargs): try: # pragma: no cover self.bundle_env = kwargs["bundle_env"] @@ -40,13 +43,17 @@ def init(self, *args, **kwargs): self.stop() raise IOError("Cannot load any configuration.") - self.cellular = None - self.interfaces = [] - + """ update inside run() try: self.update_default(self.model.db) except: pass + """ + + def run(self): + while True: + self.update_default(self.model.db) + sleep(self.update_interval) def load(self, path): """ @@ -69,24 +76,6 @@ def save(self): self.model.save_db() self.model.backup_db() - def cellular_connected(self, name, up=True): - """ - If cellular is connected, the default gateway should be set to - cellular interface. - - Args: - name: cellular's interface name - """ - default = {} - if up: - self.cellular = name - default["interface"] = self.cellular - self.update_default(default) - else: - self.cellular = None - if name == self.model.db["interface"]: - self.update_default(default) - def list_interfaces(self): """ List available interfaces. @@ -142,11 +131,6 @@ def update_default(self, default): ifaces = self.list_interfaces() if not ifaces or default["interface"] not in ifaces: raise ValueError("Interface should be UP.") - # FIXME: how to determine a interface is produced by cellular - # elif any("ppp" in s for s in ifaces): - elif self.cellular: - raise ValueError("Cellular is connected, the default gateway" - "cannot be changed.") # retrieve the default gateway for iface in self.interfaces: @@ -224,12 +208,7 @@ def _get_default(self, message, response): """ Get default gateway. """ - default = self.get_default() - # FIXME: show real time value instead of settings? - if self.model.db and "interface" in self.model.db and default and \ - self.model.db["interface"] == default["interface"]: - return response(data=default) - return response(data=self.model.db) + return response(data=self.get_default()) put_default_schema = Schema({ Optional("interface"): Any(str, unicode), From c8cadfe30402ea7657627206cbf1404f3f94e84a Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 8 Sep 2015 18:31:20 +0800 Subject: [PATCH 47/58] Add secondary policy Add secondary default gateway policy. Unittest cases are not finished. --- data/route.json.factory | 2 +- route.py | 124 +++++++++++++++++------- tests/data/route.json.factory | 2 +- tests/test_route.py | 171 +++++++++++++++++++++++++--------- 4 files changed, 215 insertions(+), 84 deletions(-) diff --git a/data/route.json.factory b/data/route.json.factory index cd61291..f0f970f 100644 --- a/data/route.json.factory +++ b/data/route.json.factory @@ -1,3 +1,3 @@ { - "interface": "eth0" + "default": "eth0" } diff --git a/route.py b/route.py index 46562af..88dca7b 100755 --- a/route.py +++ b/route.py @@ -4,6 +4,7 @@ import os import netifaces import logging +from time import sleep from sanji.core import Sanji from sanji.core import Route from sanji.connection.mqtt import Mqtt @@ -43,16 +44,9 @@ def init(self, *args, **kwargs): self.stop() raise IOError("Cannot load any configuration.") - """ update inside run() - try: - self.update_default(self.model.db) - except: - pass - """ - def run(self): while True: - self.update_default(self.model.db) + self.try_update_default(self.model.db) sleep(self.update_interval) def load(self, path): @@ -124,20 +118,18 @@ def update_default(self, default): default: dict format with "interface" required and "gateway" optional. """ + # delete the default gateway + if not default or ("interface" not in default and \ + "gateway" not in default): + try: + ip.route.delete("default") + except Exception as e: + raise e + # change the default gateway # FIXME: only "gateway" without interface is also available # FIXME: add "secondary" default route rule - if "interface" in default and default["interface"]: - ifaces = self.list_interfaces() - if not ifaces or default["interface"] not in ifaces: - raise ValueError("Interface should be UP.") - - # retrieve the default gateway - for iface in self.interfaces: - if iface["interface"] == default["interface"]: - default = iface - break - + else: try: ip.route.delete("default") if "gateway" in default and "interface" in default: @@ -151,18 +143,45 @@ def update_default(self, default): raise ValueError("Invalid default route.") except Exception as e: raise e - self.model.db["interface"] = default["interface"] - # delete the default gateway + def try_update_default(self, routes): + """ + Try to update the default gateway. + + Args: + routes: dict format including default gateway interface and + secondary default gateway interface. + For example: + { + "default": "wwan0", + "secondary": "eth0" + } + """ + ifaces = self.list_interfaces() + if not ifaces: + raise ValueError("Interfaces should be UP.") + + default = {} + if routes["default"] in ifaces: + default["interface"] = routes["default"] + elif routes["secondary"] in ifaces: + default["interface"] = routes["secondary"] else: - try: - ip.route.delete("default") - except Exception as e: - raise e - if "interface" in self.model.db: - self.model.db.pop("interface") + return self.update_default({}) - self.save() + # find gateway by interface + for iface in self.interfaces: + if iface["interface"] == default["interface"]: + default = iface + break + + current = self.get_default() + try: + if current["interface"] != default["interface"] or \ + current["gateway"] != default["gateway"]: + self.update_default(default) + except: + self.update_default(default) def update_router(self, interface): """ @@ -189,12 +208,28 @@ def update_router(self, interface): self.interfaces.append(iface) # check if the default gateway need to be modified - if iface["interface"] == self.model.db["interface"]: + if iface["interface"] == self.model.db["default"]: try: - self.update_default(iface) + self.try_update_default(self.model.db) except: pass + def put(self, default, is_default=True): + """ + Update default / secondary gateway. + """ + if is_default: + def_type = "default" + else: + def_type = "secondary" + + # save the setting + if "interface" in default: + self.model.db[def_type] = default["interface"] + else: + self.model.db[def_type] = "" + self.save() + @Route(methods="get", resource="/network/routes/interfaces") def _get_interfaces(self, message, response): """ @@ -227,16 +262,17 @@ def _put_default(self, message, response, schema=put_default_schema): return response(code=400, data={"message": "Invalid input: %s." % e}) - # retrieve the default gateway - default = self.get_default() + self.put(message.data) + try: self.update_default(message.data) - return response(data=self.model.db) + # FIXME: return should be outside? + # return response(data=self.get_default()) except Exception as e: - # recover the previous default gateway if any + # try database if failed + _logger.info(e) try: - if default: - self.update_default(default) + self.try_update_default(self.model.db) except: _logger.info("Failed to recover the default gateway.") _logger.info("Update default gateway failed: %s" % e) @@ -244,6 +280,22 @@ def _put_default(self, message, response, schema=put_default_schema): data={"message": "Update default gateway failed: %s" % e}) + return response(data=self.get_default()) + + @Route(methods="put", resource="/network/routes/secondary") + def _put_secondary(self, message, response, schema=put_default_schema): + """ + Update the secondary default gateway, delete default gateway if data + is None or empty. + """ + # TODO: should be removed when schema worked for unittest + try: + IPRoute.put_default_schema(message.data) + except Exception as e: + return response(code=400, + data={"message": "Invalid input: %s." % e}) + + self.put(message.data, False) def set_router_db(self, message, response): """ diff --git a/tests/data/route.json.factory b/tests/data/route.json.factory index cd61291..f0f970f 100644 --- a/tests/data/route.json.factory +++ b/tests/data/route.json.factory @@ -1,3 +1,3 @@ { - "interface": "eth0" + "default": "eth0" } diff --git a/tests/test_route.py b/tests/test_route.py index a967252..a3f9eea 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -77,55 +77,49 @@ def tearDown(self): @patch.object(IPRoute, 'update_default') def test__init__no_conf(self, mock_update_default): - # case: no configuration file + """ + init: no configuration file + """ with self.assertRaises(IOError): with patch("route.ModelInitiator") as mock_modelinit: mock_modelinit.side_effect = IOError self.bundle.init() def test__load__current_conf(self): - # case: load current configuration + """ + load: load current configuration + """ self.bundle.load(dirpath) - self.assertEqual("eth0", self.bundle.model.db["interface"]) + self.assertEqual("eth0", self.bundle.model.db["default"]) def test__load__backup_conf(self): - # case: load backup configuration + """ + load: load backup configuration + """ os.remove("%s/data/%s.json" % (dirpath, self.name)) self.bundle.load(dirpath) - self.assertEqual("eth0", self.bundle.model.db["interface"]) + self.assertEqual("eth0", self.bundle.model.db["default"]) def test__load__no_conf(self): - # case: cannot load any configuration + """ + load: cannot load any configuration + """ with self.assertRaises(IOError): self.bundle.load("%s/mock" % dirpath) def test__save(self): + """ + save + """ # Already tested in init() pass - @patch.object(IPRoute, 'update_default') - def test__cellular_connected__up(self, mock_update_default): - # case: update default gateway when cellular connected - self.bundle.cellular_connected("ppp0") - self.assertEqual("ppp0", self.bundle.cellular) - - @patch.object(IPRoute, 'update_default') - def test__cellular_connected__down(self, mock_update_default): - # case: update default gateway when cellular disconnected - self.bundle.cellular_connected("ppp0", False) - self.assertEqual(None, self.bundle.cellular) - - @patch.object(IPRoute, 'update_default') - def test__cellular_connected__down_delete(self, mock_update_default): - # case: update default gateway when cellular disconnected - self.bundle.model.db["interface"] = "ppp0" - self.bundle.cellular_connected("ppp0", False) - self.assertEqual(None, self.bundle.cellular) - @patch("route.ip.addr.ifaddresses") @patch("route.ip.addr.interfaces") def test__list_interfaces(self, mock_interfaces, mock_ifaddresses): - # case: list the available interfaces + """ + list_interfaces: list the available interfaces + """ mock_interfaces.return_value = ["eth0", "eth1", "ppp0"] mock_ifaddresses.side_effect = mock_ip_addr_ifaddresses @@ -136,7 +130,9 @@ def test__list_interfaces(self, mock_interfaces, mock_ifaddresses): @patch("route.ip.addr.interfaces") def test__list_interfaces__failed_get_ifaces(self, mock_interfaces): - # case: failed to list the available interfaces + """ + list_interfaces: failed to list the available interfaces + """ mock_interfaces.side_effect = IOError ifaces = self.bundle.list_interfaces() @@ -146,7 +142,9 @@ def test__list_interfaces__failed_get_ifaces(self, mock_interfaces): @patch("route.ip.addr.interfaces") def test__list_interfaces__failed_get_status(self, mock_interfaces, mock_ifaddresses): - # case: cannot get some interface's status + """ + list_interfaces: cannot get some interface's status + """ def mock_ip_addr_ifaddresses_ppp0_failed(iface): if "eth0" == iface: return {"mac": "78:ac:c0:c1:a8:fe", @@ -176,7 +174,9 @@ def mock_ip_addr_ifaddresses_ppp0_failed(iface): @patch("route.netifaces.gateways") def test__get_default(self, mock_gateways): - # case: get current default gateway + """ + get_default: get current default gateway + """ mock_gateways.return_value = { 'default': {2: ('192.168.3.254', 'eth0')}, 2: [('192.168.3.254', 'eth0', True)]} @@ -187,28 +187,105 @@ def test__get_default(self, mock_gateways): @patch("route.netifaces.gateways") def test__get_default__no_default(self, mock_gateways): - # case: no current default gateway + """ + get_default: no current default gateway + """ mock_gateways.return_value = {'default': {}} default = self.bundle.get_default() self.assertEqual({}, default) @patch("route.ip.route.delete") - def test__update_default__delete(self, mock_route_delete): - # case: delete the default gateway - default = dict() - self.bundle.update_default(default) - self.assertNotIn("interface", self.bundle.model.db) + @patch("route.ip.route.add") + def test__update_default(self, mock_ip_route_add, mock_ip_route_del): + """ + update_default: update the default gateway with both interface and + gateway + """ + default = {} + default["interface"] = "eth1" + default["gateway"] = "192.168.4.254" + + try: + self.bundle.update_default(default) + except: + self.fail("update_default raised exception unexpectedly!") + + @patch("route.ip.route.delete") + @patch("route.ip.route.add") + def test__update_default__with_iface(self, mock_ip_route_add, + mock_ip_route_del): + """ + update_default: update the default gateway with interface + """ + default = {} + default["interface"] = "eth1" + + try: + self.bundle.update_default(default) + except: + self.fail("update_default raised exception unexpectedly!") @patch("route.ip.route.delete") - def test__update_default__delete_failed(self, mock_route_delete): - mock_route_delete.side_effect = IOError + @patch("route.ip.route.add") + def test__update_default__with_gateway(self, mock_ip_route_add, + mock_ip_route_del): + """ + update_default: update the default gateway with gateway + """ + default = {} + default["gateway"] = "192.168.4.254" + + try: + self.bundle.update_default(default) + except: + self.fail("update_default raised exception unexpectedly!") + + @patch("route.ip.route.delete") + @patch("route.ip.route.add") + def test__update_default__failed(self, mock_ip_route_add, + mock_ip_route_del): + """ + update_default: failed to update the default gateway + """ + mock_ip_route_add.side_effect = IOError + default = {} + default["gateway"] = "192.168.4.254" - # case: failed to delete the default gateway - default = dict() with self.assertRaises(IOError): self.bundle.update_default(default) - self.assertIn("interface", self.bundle.model.db) + + @patch("route.ip.route.delete") + def test__update_default__delete(self, mock_ip_route_del): + """ + update_default: delete the default gateway + """ + default = {} + + try: + self.bundle.update_default(default) + except: + self.fail("update_default raised exception unexpectedly!") + + @patch("route.ip.route.delete") + def test__update_default__delete_failed(self, mock_ip_route_del): + """ + update_default: failed delete the default gateway + """ + mock_ip_route_del.side_effect = IOError + default = {} + + with self.assertRaises(IOError): + self.bundle.update_default(default) + + def test__try_update_default(self): + """ + try_update_default: no interfaces + try_update_default: update by default + try_update_default: update by secondary + try_update_default: delete default gateway + """ + pass @patch("route.ip.route.add") @patch("route.ip.route.delete") @@ -221,7 +298,7 @@ def test__update_default__add_without_gateway( default = dict() default["interface"] = "eth1" self.bundle.update_default(default) - self.assertIn("eth1", self.bundle.model.db["interface"]) + self.assertIn("eth1", self.bundle.model.db["default"]) @patch("route.ip.route.add") @patch("route.ip.route.delete") @@ -235,7 +312,7 @@ def test__update_default__add_with_gateway( default["interface"] = "eth1" default["gateway"] = "192.168.4.254" self.bundle.update_default(default) - self.assertIn("eth1", self.bundle.model.db["interface"]) + self.assertIn("eth1", self.bundle.model.db["default"]) @patch.object(IPRoute, 'list_interfaces') def test__update_default__add_failed_iface_down( @@ -249,7 +326,7 @@ def test__update_default__add_failed_iface_down( default["gateway"] = "192.168.4.254" with self.assertRaises(ValueError): self.bundle.update_default(default) - self.assertIn("interface", self.bundle.model.db) + self.assertIn("default", self.bundle.model.db) @patch.object(IPRoute, 'list_interfaces') def test__update_default__add_failed_cellular_connected( @@ -400,15 +477,17 @@ def test___get_default__different(self, mock_get_default): mock_get_default.return_value = { "interface": "eth1", "gateway": "192.168.4.254"} - # case: get default gateway info. + # case: get current default gateway info. message = Message({"data": {}, "query": {}, "param": {}}) def resp(code=200, data=None): self.assertEqual(200, code) - self.assertEqual("eth0", data["interface"]) + self.assertEqual("eth1", data["interface"]) self.bundle._get_default(message=message, response=resp, test=True) - def test___get_default__empty(self): + @patch.object(IPRoute, 'get_default') + def test___get_default__empty(self, mock_get_default): + mock_get_default.return_value = {} self.bundle.model.db = {} # case: no default gateway set From 33547c068991555bfcf70bf6ee86807def80e6fb Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 8 Sep 2015 08:19:59 -0700 Subject: [PATCH 48/58] Update unittest Update unittest with secondary default gateway. --- route.py | 59 +++--- tests/test_route.py | 462 ++++++++++++++++++++++++-------------------- 2 files changed, 275 insertions(+), 246 deletions(-) diff --git a/route.py b/route.py index 88dca7b..1ff492c 100755 --- a/route.py +++ b/route.py @@ -38,6 +38,7 @@ def init(self, *args, **kwargs): if self.bundle_env == "debug": # pragma: no cover path_root = "%s/tests" % path_root + self.interfaces = [] try: self.load(path_root) except: @@ -119,8 +120,8 @@ def update_default(self, default): optional. """ # delete the default gateway - if not default or ("interface" not in default and \ - "gateway" not in default): + if not default or ("interface" not in default and + "gateway" not in default): try: ip.route.delete("default") except Exception as e: @@ -214,7 +215,7 @@ def update_router(self, interface): except: pass - def put(self, default, is_default=True): + def set_default(self, default, is_default=True): """ Update default / secondary gateway. """ @@ -224,19 +225,32 @@ def put(self, default, is_default=True): def_type = "secondary" # save the setting + # if no interface but has gateway, do not update anything if "interface" in default: self.model.db[def_type] = default["interface"] - else: + elif "gateway" not in default: self.model.db[def_type] = "" self.save() + try: + if is_default: + self.update_default(default) + except Exception as e: + # try database if failed + try: + self.try_update_default(self.model.db) + except: + _logger.info("Failed to recover the default gateway.") + error = "Update default gateway failed: %s" % e + _logger.error(error) + raise IOError(error) + @Route(methods="get", resource="/network/routes/interfaces") def _get_interfaces(self, message, response): """ Get available interfaces. """ - data = self.list_interfaces() - return response(data=data) + return response(data=self.list_interfaces()) @Route(methods="get", resource="/network/routes/default") def _get_default(self, message, response): @@ -255,31 +269,11 @@ def _put_default(self, message, response, schema=put_default_schema): Update the default gateway, delete default gateway if data is None or empty. """ - # TODO: should be removed when schema worked for unittest - try: - IPRoute.put_default_schema(message.data) - except Exception as e: - return response(code=400, - data={"message": "Invalid input: %s." % e}) - - self.put(message.data) - try: - self.update_default(message.data) - # FIXME: return should be outside? - # return response(data=self.get_default()) + self.set_default(message.data) except Exception as e: - # try database if failed - _logger.info(e) - try: - self.try_update_default(self.model.db) - except: - _logger.info("Failed to recover the default gateway.") - _logger.info("Update default gateway failed: %s" % e) return response(code=404, - data={"message": - "Update default gateway failed: %s" - % e}) + data={"message": e}) return response(data=self.get_default()) @Route(methods="put", resource="/network/routes/secondary") @@ -288,14 +282,7 @@ def _put_secondary(self, message, response, schema=put_default_schema): Update the secondary default gateway, delete default gateway if data is None or empty. """ - # TODO: should be removed when schema worked for unittest - try: - IPRoute.put_default_schema(message.data) - except Exception as e: - return response(code=400, - data={"message": "Invalid input: %s." % e}) - - self.put(message.data, False) + self.set_default(message.data, False) def set_router_db(self, message, response): """ diff --git a/tests/test_route.py b/tests/test_route.py index a3f9eea..324ac66 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -104,7 +104,7 @@ def test__load__no_conf(self): """ load: cannot load any configuration """ - with self.assertRaises(IOError): + with self.assertRaises(Exception): self.bundle.load("%s/mock" % dirpath) def test__save(self): @@ -199,7 +199,7 @@ def test__get_default__no_default(self, mock_gateways): @patch("route.ip.route.add") def test__update_default(self, mock_ip_route_add, mock_ip_route_del): """ - update_default: update the default gateway with both interface and + update_default: update the default gateway with both interface and gateway """ default = {} @@ -278,87 +278,153 @@ def test__update_default__delete_failed(self, mock_ip_route_del): with self.assertRaises(IOError): self.bundle.update_default(default) - def test__try_update_default(self): + @patch.object(IPRoute, "list_interfaces") + def test__try_update_default__no_iface(self, mock_list_interfaces): """ try_update_default: no interfaces + """ + mock_list_interfaces.return_value = [] + + with self.assertRaises(ValueError): + self.bundle.try_update_default(self.bundle.model.db) + + @patch.object(IPRoute, "update_default") + @patch.object(IPRoute, "get_default") + @patch.object(IPRoute, "list_interfaces") + def test__try_update_default__by_default( + self, + mock_list_interfaces, + mock_get_default, + mock_update_default): + """ try_update_default: update by default - try_update_default: update by secondary - try_update_default: delete default gateway """ - pass + mock_list_interfaces.return_value = ["eth0", "eth1", "wwan0"] + mock_get_default.return_value = { + "interface": "eth1", + "gateway": "192.168.4.254" + } - @patch("route.ip.route.add") - @patch("route.ip.route.delete") - @patch.object(IPRoute, 'list_interfaces') - def test__update_default__add_without_gateway( - self, mock_list_interfaces, mock_route_delete, mock_route_add): - mock_list_interfaces.return_value = ["eth0", "eth1"] + self.bundle.interfaces = [ + { + "interface": "eth0", + "gateway": "192.168.3.254" + }, + { + "interface": "eth1", + "gateway": "192.168.4.254" + } + ] + + routes = {} + routes["default"] = "eth0" + routes["secondary"] = "eth1" + + self.bundle.try_update_default(routes) + mock_update_default.assert_called_once_with(self.bundle.interfaces[0]) - # case: add the default gateway - default = dict() - default["interface"] = "eth1" - self.bundle.update_default(default) - self.assertIn("eth1", self.bundle.model.db["default"]) + @patch.object(IPRoute, "update_default") + @patch.object(IPRoute, "get_default") + @patch.object(IPRoute, "list_interfaces") + def test__try_update_default__by_default_with_current_value( + self, + mock_list_interfaces, + mock_get_default, + mock_update_default): + """ + try_update_default: update by default (same with current setting) + """ + mock_list_interfaces.return_value = ["eth0", "eth1", "wwan0"] + mock_get_default.return_value = { + "interface": "eth0", + "gateway": "192.168.3.254" + } - @patch("route.ip.route.add") - @patch("route.ip.route.delete") - @patch.object(IPRoute, 'list_interfaces') - def test__update_default__add_with_gateway( - self, mock_list_interfaces, mock_route_delete, mock_route_add): - mock_list_interfaces.return_value = ["eth0", "eth1"] + self.bundle.interfaces = [ + { + "interface": "eth0", + "gateway": "192.168.3.254" + }, + { + "interface": "eth1", + "gateway": "192.168.4.254" + } + ] + + routes = {} + routes["default"] = "eth0" + routes["secondary"] = "eth1" + + self.bundle.try_update_default(routes) + self.assertTrue(not mock_update_default.called) - # case: add the default gateway - default = dict() - default["interface"] = "eth1" - default["gateway"] = "192.168.4.254" - self.bundle.update_default(default) - self.assertIn("eth1", self.bundle.model.db["default"]) + @patch.object(IPRoute, "update_default") + @patch.object(IPRoute, "get_default") + @patch.object(IPRoute, "list_interfaces") + def test__try_update_default__by_secondary( + self, + mock_list_interfaces, + mock_get_default, + mock_update_default): + """ + try_update_default: update by secondary + """ + # arrange + mock_list_interfaces.return_value = ["eth1", "wwan0"] + mock_get_default.return_value = { + "interface": "wwan0", + "gateway": "192.168.4.254" + } - @patch.object(IPRoute, 'list_interfaces') - def test__update_default__add_failed_iface_down( - self, mock_list_interfaces): - mock_list_interfaces.return_value = ["eth0"] + self.bundle.interfaces = [ + { + "interface": "eth0", + "gateway": "192.168.3.254" + }, + { + "interface": "eth1", + "gateway": "192.168.4.254" + }, + { + "interface": "wwan0", + "gateway": "192.168.5.254" + } + ] + + routes = {} + routes["default"] = "eth0" + routes["secondary"] = "wwan0" - # case: fail to add the default gateway when indicated interface is - # down - default = dict() - default["interface"] = "eth1" - default["gateway"] = "192.168.4.254" - with self.assertRaises(ValueError): - self.bundle.update_default(default) - self.assertIn("default", self.bundle.model.db) - - @patch.object(IPRoute, 'list_interfaces') - def test__update_default__add_failed_cellular_connected( - self, mock_list_interfaces): - mock_list_interfaces.return_value = ["eth0", "ppp0"] - self.bundle.cellular = "ppp0" - - # case: fail to add the default gateway when ppp is connected - default = dict() - default["interface"] = "eth0" - default["gateway"] = "192.168.3.254" - with self.assertRaises(ValueError): - self.bundle.update_default(default) + # act + self.bundle.try_update_default(routes) - @patch("route.ip.route.add") - @patch("route.ip.route.delete") - @patch.object(IPRoute, 'list_interfaces') - def test__update_default__add_failed( - self, mock_list_interfaces, mock_route_delete, mock_route_add): - mock_list_interfaces.return_value = ["eth0", "eth1"] - mock_route_add.side_effect = ValueError - - # case: fail to add the default gateway - default = dict() - default["interface"] = "eth0" - default["gateway"] = "192.168.3.254" - with self.assertRaises(ValueError): - self.bundle.update_default(default) + # assert + mock_update_default.assert_called_once_with(self.bundle.interfaces[2]) + + @patch.object(IPRoute, "update_default") + @patch.object(IPRoute, "list_interfaces") + def test__try_update_default__delete( + self, + mock_list_interfaces, + mock_update_default): + """ + try_update_default: delete default gateway + """ + mock_list_interfaces.return_value = ["eth1"] + + routes = {} + routes["default"] = "wwan0" + routes["secondary"] = "eth0" + + self.bundle.try_update_default(routes) + mock_update_default.assert_called_once_with({}) @patch.object(IPRoute, 'update_default') def test__update_router__update_interface( self, mock_update_default): + """ + update_router: update router info by interface + """ # arrange self.bundle.interfaces = [ {"interface": "eth0", "gateway": "192.168.31.254"}, @@ -378,6 +444,9 @@ def test__update_router__update_interface( @patch.object(IPRoute, 'update_default') def test__update_router__add_interface_with_gateway( self, mock_update_default): + """ + update_router: add a new interface with gateway + """ # arrange self.bundle.interfaces = [ {"interface": "eth0", "gateway": "192.168.31.254"}] @@ -396,6 +465,9 @@ def test__update_router__add_interface_with_gateway( @patch.object(IPRoute, 'update_default') def test__update_router__add_interface_without_gateway( self, mock_update_default): + """ + update_router: add a new interface without gateway + """ # arrange self.bundle.interfaces = [ {"interface": "eth0", "gateway": "192.168.31.254"}] @@ -411,10 +483,18 @@ def test__update_router__add_interface_without_gateway( self.assertIn({"interface": "eth1"}, self.bundle.interfaces) + @patch.object(IPRoute, "get_default") @patch.object(IPRoute, 'update_default') def test__update_router__update_default( - self, mock_update_default): + self, mock_update_default, mock_get_default): + """ + update_router: default gateway should also be updated + """ # arrange + mock_get_default.return_value = { + "interface": "eth0", + "gateway": "192.168.3.254" + } self.bundle.interfaces = [ {"interface": "eth0", "gateway": "192.168.3.254"}, {"interface": "eth1", "gateway": "192.168.4.254"}] @@ -429,128 +509,128 @@ def test__update_router__update_default( self.bundle.interfaces) self.assertIn({"interface": "eth1", "gateway": "192.168.4.254"}, self.bundle.interfaces) + mock_update_default.assert_called_once_with(self.bundle.interfaces[0]) - @patch("route.ip.addr.ifaddresses") - @patch("route.ip.addr.interfaces") - def test__get_interfaces(self, mock_interfaces, mock_ifaddresses): - mock_interfaces.return_value = ["eth0", "eth1", "ppp0"] - mock_ifaddresses.side_effect = mock_ip_addr_ifaddresses - - # case: get supported interface list - message = Message({"data": {}, "query": {}, "param": {}}) - - def resp(code=200, data=None): - self.assertEqual(200, code) - self.assertEqual(2, len(data)) - self.assertIn("eth0", data) - self.assertIn("ppp0", data) - self.bundle._get_interfaces(message=message, response=resp, test=True) - - @patch("route.ip.addr.interfaces") - def test__get_interfaces__failed(self, mock_interfaces): - mock_interfaces.side_effect = ValueError - - # case: fail to get supported interface list - message = Message({"data": {}, "query": {}, "param": {}}) - - def resp(code=404, data=None): - self.assertEqual(404, code) - self.assertEqual({}, data) - self.bundle._get_interfaces(message=message, response=resp, test=True) - - @patch.object(IPRoute, 'get_default') - def test___get_default__match(self, mock_get_default): - mock_get_default.return_value = { - "interface": "eth0", "gateway": "192.168.3.254"} - - # case: get default gateway info. - message = Message({"data": {}, "query": {}, "param": {}}) - - def resp(code=200, data=None): - self.assertEqual(200, code) - self.assertEqual("eth0", data["interface"]) - self.assertEqual("192.168.3.254", data["gateway"]) - self.bundle._get_default(message=message, response=resp, test=True) - - @patch.object(IPRoute, 'get_default') - def test___get_default__different(self, mock_get_default): - mock_get_default.return_value = { - "interface": "eth1", "gateway": "192.168.4.254"} + @patch.object(IPRoute, "update_default") + def test__set_default__default(self, mock_update_default): + """ + set_default: update default gateway + """ + # arrange + self.bundle.model.db["default"] = "eth1" + default = { + "interface": "eth0", + "gateway": "192.168.3.254" + } - # case: get current default gateway info. - message = Message({"data": {}, "query": {}, "param": {}}) + # act + self.bundle.set_default(default) - def resp(code=200, data=None): - self.assertEqual(200, code) - self.assertEqual("eth1", data["interface"]) - self.bundle._get_default(message=message, response=resp, test=True) + # assert + self.assertEqual(self.bundle.model.db, {"default": "eth0"}) + mock_update_default.assert_called_once_with(default) - @patch.object(IPRoute, 'get_default') - def test___get_default__empty(self, mock_get_default): - mock_get_default.return_value = {} - self.bundle.model.db = {} + @patch.object(IPRoute, "try_update_default") + @patch.object(IPRoute, "update_default") + def test__set_default__update_default_failed( + self, + mock_update_default, + mock_try_update_default): + """ + set_default: update default gateway failed + """ + # arrange + mock_update_default.side_effect = IOError + self.bundle.model.db["default"] = "eth1" + default = { + "interface": "eth0", + "gateway": "192.168.3.254" + } - # case: no default gateway set - message = Message({"data": {}, "query": {}, "param": {}}) + # act + with self.assertRaises(IOError): + self.bundle.set_default(default) - def resp(code=200, data=None): - self.assertEqual(200, code) - self.assertEqual({}, data) - self.bundle._get_default(message=message, response=resp, test=True) + # assert + self.assertEqual(self.bundle.model.db, {"default": "eth0"}) + mock_update_default.assert_called_once_with(default) + mock_try_update_default.assert_called_once_with(self.bundle.model.db) - def test__put_default__none(self): - # case: None data is not allowed - message = Message({"data": None, "query": {}, "param": {}}) + @patch.object(IPRoute, "try_update_default") + @patch.object(IPRoute, "update_default") + def test__set_default__update_default_and_recovery_failed( + self, + mock_update_default, + mock_try_update_default): + """ + set_default: update default gateway failed and recovery failed + """ + # arrange + mock_update_default.side_effect = IOError + mock_try_update_default.side_effect = IOError + self.bundle.model.db["default"] = "eth1" + default = { + "interface": "eth0", + "gateway": "192.168.3.254" + } - def resp(code=200, data=None): - self.assertEqual(400, code) - self.bundle._put_default(message=message, response=resp, test=True) + # act + with self.assertRaises(IOError): + self.bundle.set_default(default) - @patch.object(IPRoute, 'update_default') - def test__put_default__delete(self, mock_update_default): - # case: delete the default gateway - message = Message({"data": {}, "query": {}, "param": {}}) + # assert + self.assertEqual(self.bundle.model.db, {"default": "eth0"}) + mock_update_default.assert_called_once_with(default) + mock_try_update_default.assert_called_once_with(self.bundle.model.db) - def resp(code=200, data=None): - self.assertEqual(200, code) - self.bundle._put_default(message=message, response=resp, test=True) + def test__set_default__secondary(self): + """ + set_default: update secondary default gateway + """ + # arrange + self.bundle.model.db["default"] = "eth1" + default = { + "interface": "eth0", + "gateway": "192.168.3.254" + } - @patch.object(IPRoute, 'update_default') - def test__put_default__delete_with_empty_iface(self, mock_update_default): - # case: delete the default gateway - message = Message( - {"data": {"interface": ""}, "query": {}, "param": {}}) + # act + self.bundle.set_default(default, False) - def resp(code=200, data=None): - self.assertEqual(200, code) - self.bundle._put_default(message=message, response=resp, test=True) + # assert + self.assertEqual(self.bundle.model.db, + {"default": "eth1", "secondary": "eth0"}) - @patch.object(IPRoute, 'update_default') - def test__put_default__delete_failed(self, mock_update_default): - mock_update_default.side_effect = ValueError + @patch("route.ip.route.delete") + def test__set_default__clear(self, mock_ip_route_del): + """ + set_default: clear default gateway + """ + # arrange + self.bundle.model.db["default"] = "eth1" + default = {} - # case: failed to delete the default gateway - message = Message({"data": {}, "query": {}, "param": {}}) + # act + self.bundle.set_default(default) - def resp(code=200, data=None): - self.assertEqual(404, code) - self.bundle._put_default(message=message, response=resp, test=True) + # assert + self.assertEqual(self.bundle.model.db, {"default": ""}) + mock_ip_route_del.assert_called_once_with("default") @patch.object(IPRoute, "update_default") - def test__put_default__add(self, mock_update_default): - # case: add the default gateway - message = Message({"data": {}, "query": {}, "param": {}}) - message.data["interface"] = "eth1" + def test__set_default__no_change(self, mock_update_default): + """ + set_default: default gateway doesn't change + """ + # arrange + self.bundle.model.db["default"] = "eth1" + default = {"gateway": "192.168.3.254"} - def resp(code=200, data=None): - self.assertEqual(200, code) - self.bundle._put_default(message=message, response=resp, test=True) + # act + self.bundle.set_default(default) - """ TODO - @patch.object(IPRoute, "update_default") - def test_put_default_failed_recover_failed(self, mock_update_default): - pass - """ + # assert + self.assertEqual(self.bundle.model.db, {"default": "eth1"}) @patch.object(IPRoute, "update_default") def test__set_router_db__add(self, mock_update_default): @@ -568,44 +648,6 @@ def test__set_router_db__add(self, mock_update_default): # assert self.assertEqual(mock_func.call_args_list[0][1]["data"], iface) - @patch.object(IPRoute, "update_router") - def test__event_router_db(self, mock_update_router): - # case: update the router information by interface - message = Message({"data": {}, "query": {}, "param": {}}) - message.data["interface"] = "eth1" - message.data["gateway"] = "192.168.41.254" - - self.bundle._event_router_db(message=message, test=True) - - """ - @patch.object(IPRoute, "update_router") - def test__hook_put_ethernet_by_id(self, mock_update_router): - # case: test if default gateway changed - message = Message({"data": {}, "query": {}, "param": {}}) - message.data["name"] = "eth1" - - def resp(code=200, data=None): - self.assertEqual(200, code) - self.assertEqual("eth0", data["interface"]) - self.bundle._hook_put_ethernet_by_id(message=message, response=resp, - test=True) - - @patch.object(IPRoute, "update_router") - def test__hook_put_ethernets(self, mock_update_router): - # case: test if default gateway changed - message = Message({"data": [], "query": {}, "param": {}}) - iface = {"id": 2, "name": "eth1", "gateway": "192.168.4.254"} - message.data.append(iface) - iface = {"id": 1, "name": "eth0", "gateway": "192.168.31.254"} - message.data.append(iface) - - def resp(code=200, data=None): - self.assertEqual(200, code) - self.assertEqual("eth0", data["interface"]) - self.bundle._hook_put_ethernets(message=message, response=resp, - test=True) - """ - if __name__ == "__main__": FORMAT = '%(asctime)s - %(levelname)s - %(lineno)s - %(message)s' From d8bc1caa1fe0c1b7eaf32db23ee3be13eacf0c35 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 8 Sep 2015 08:21:57 -0700 Subject: [PATCH 49/58] Fix pylint --- tests/test_route.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_route.py b/tests/test_route.py index 324ac66..891fcf0 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -533,9 +533,9 @@ def test__set_default__default(self, mock_update_default): @patch.object(IPRoute, "try_update_default") @patch.object(IPRoute, "update_default") def test__set_default__update_default_failed( - self, - mock_update_default, - mock_try_update_default): + self, + mock_update_default, + mock_try_update_default): """ set_default: update default gateway failed """ @@ -559,9 +559,9 @@ def test__set_default__update_default_failed( @patch.object(IPRoute, "try_update_default") @patch.object(IPRoute, "update_default") def test__set_default__update_default_and_recovery_failed( - self, - mock_update_default, - mock_try_update_default): + self, + mock_update_default, + mock_try_update_default): """ set_default: update default gateway failed and recovery failed """ From 23285c0a41b564ea1d750baee92c39dfee2a7027 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Tue, 8 Sep 2015 15:16:58 -0700 Subject: [PATCH 50/58] Fix unittest Mock try_update_default() instead of update_default(). --- tests/test_route.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_route.py b/tests/test_route.py index 891fcf0..39ab760 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -484,9 +484,9 @@ def test__update_router__add_interface_without_gateway( self.bundle.interfaces) @patch.object(IPRoute, "get_default") - @patch.object(IPRoute, 'update_default') + @patch.object(IPRoute, 'try_update_default') def test__update_router__update_default( - self, mock_update_default, mock_get_default): + self, mock_try_update_default, mock_get_default): """ update_router: default gateway should also be updated """ @@ -509,7 +509,7 @@ def test__update_router__update_default( self.bundle.interfaces) self.assertIn({"interface": "eth1", "gateway": "192.168.4.254"}, self.bundle.interfaces) - mock_update_default.assert_called_once_with(self.bundle.interfaces[0]) + mock_try_update_default.assert_called_once_with(self.bundle.model.db) @patch.object(IPRoute, "update_default") def test__set_default__default(self, mock_update_default): From ab4d542d9c3206c01e6642d0b0db2e819cfefc40 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Wed, 9 Sep 2015 15:16:07 +0800 Subject: [PATCH 51/58] Add DNS setting DNS should be updated is default gateway updated. --- route.py | 20 +++++++++++++++++++- tests/test_route.py | 7 +++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/route.py b/route.py index 1ff492c..cbea247 100755 --- a/route.py +++ b/route.py @@ -47,7 +47,10 @@ def init(self, *args, **kwargs): def run(self): while True: - self.try_update_default(self.model.db) + try: + self.try_update_default(self.model.db) + except: + pass sleep(self.update_interval) def load(self, path): @@ -110,6 +113,17 @@ def get_default(self): default["interface"] = gw[1] return default + def update_dns(self, interface): + """ + Update DNS according to default gateway's interface. + + Args: + default: interface name + """ + res = self.publish.put("/network/dns", data={"interface": interface}) + if res.code != 200: + raise RuntimeWarning(res.data["message"]) + def update_default(self, default): """ Update default gateway. If updated failed, should recover to previous @@ -142,6 +156,10 @@ def update_default(self, default): ip.route.add("default", "", default["gateway"]) else: raise ValueError("Invalid default route.") + + # update DNS + if "interface" in default: + self.update_dns(default["interface"]) except Exception as e: raise e diff --git a/tests/test_route.py b/tests/test_route.py index 39ab760..09504aa 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -195,9 +195,11 @@ def test__get_default__no_default(self, mock_gateways): default = self.bundle.get_default() self.assertEqual({}, default) + @patch.object(IPRoute, "update_dns") @patch("route.ip.route.delete") @patch("route.ip.route.add") - def test__update_default(self, mock_ip_route_add, mock_ip_route_del): + def test__update_default(self, mock_ip_route_add, mock_ip_route_del, + mock_update_dns): """ update_default: update the default gateway with both interface and gateway @@ -211,10 +213,11 @@ def test__update_default(self, mock_ip_route_add, mock_ip_route_del): except: self.fail("update_default raised exception unexpectedly!") + @patch.object(IPRoute, "update_dns") @patch("route.ip.route.delete") @patch("route.ip.route.add") def test__update_default__with_iface(self, mock_ip_route_add, - mock_ip_route_del): + mock_ip_route_del, mock_update_dns): """ update_default: update the default gateway with interface """ From 79c433f0972eb6a4d30a2c3c15c33c29027db478 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Wed, 9 Sep 2015 16:06:30 +0800 Subject: [PATCH 52/58] Add response for secondary default gateway setting --- route.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/route.py b/route.py index cbea247..4f4125a 100755 --- a/route.py +++ b/route.py @@ -300,7 +300,12 @@ def _put_secondary(self, message, response, schema=put_default_schema): Update the secondary default gateway, delete default gateway if data is None or empty. """ - self.set_default(message.data, False) + try: + self.set_default(message.data, False) + except Exception as e: + return response(code=404, + data={"message": e}) + return response(data=message.data) def set_router_db(self, message, response): """ From 70ba5b33953ac36720d345e518a9a5f111855a37 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Wed, 9 Sep 2015 16:20:39 +0800 Subject: [PATCH 53/58] Fix pylint --- route.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/route.py b/route.py index 4f4125a..2743296 100755 --- a/route.py +++ b/route.py @@ -117,7 +117,7 @@ def update_dns(self, interface): """ Update DNS according to default gateway's interface. - Args: + Args: default: interface name """ res = self.publish.put("/network/dns", data={"interface": interface}) From 4551b792aba75a35dfad699b891a214c30be59c9 Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Wed, 9 Sep 2015 16:57:17 +0800 Subject: [PATCH 54/58] Change temp package directory to avoid conflict Move temp package directory from /tmp/package to /tmp/[package name]-package. --- build-deb/debian/postinst | 8 ++++---- build-deb/debian/rules | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build-deb/debian/postinst b/build-deb/debian/postinst index 8715f6a..d032fb3 100644 --- a/build-deb/debian/postinst +++ b/build-deb/debian/postinst @@ -20,10 +20,10 @@ set -e case "$1" in configure) - pip install -r /tmp/packages/requirements.txt || \ - pip install --no-index --find-links file:/tmp/packages \ - -r /tmp/packages/requirements.txt - rm -rf /tmp/packages + pip install -r /tmp/sanji-bundle-route-packages/requirements.txt || \ + pip install --no-index --find-links file:/tmp/sanji-bundle-route-packages \ + -r /tmp/sanji-bundle-route-packages/requirements.txt + rm -rf /tmp/sanji-bundle-route-packages ;; abort-upgrade|abort-remove|abort-deconfigure) diff --git a/build-deb/debian/rules b/build-deb/debian/rules index e746a10..895eafd 100755 --- a/build-deb/debian/rules +++ b/build-deb/debian/rules @@ -12,7 +12,7 @@ DEB_PACKAGE := $(strip $(shell dh_listpackages -i 2>/dev/null || dh_listpackages -i)) DEB_DESTDIR := $(CURDIR)/debian/$(DEB_PACKAGE) -DEB_PYPDIR := $(DEB_DESTDIR)/tmp/packages +DEB_PYPDIR := $(DEB_DESTDIR)/tmp/$(DEB_PACKAGE)-packages %: From 26aa3ebb8874d3a70ab4ffb84319296c6641f45f Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 10 Sep 2015 14:33:25 +0800 Subject: [PATCH 55/58] Update source format from native to quilt --- build-deb/Makefile | 4 +++- build-deb/debian/source/format | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build-deb/Makefile b/build-deb/Makefile index b99c73a..a6fcaae 100644 --- a/build-deb/Makefile +++ b/build-deb/Makefile @@ -9,6 +9,7 @@ DIST ?= unstable STAGING_DIR = $(abspath $(PROJECT)-$(VERSION)) UPSTREAM_ARCHIVE = $(PROJECT)-$(VERSION).tar.gz +UPSTREAM_ORIG_ARCHIVE = $(PROJECT)_$(VERSION).orig.tar.gz FILES = \ $(STAGING_DIR)/debian/changelog \ @@ -34,7 +35,8 @@ $(STAGING_DIR)/debian/%: $(PROJ_DIR)/build-deb/debian/% cp $< $@ extract-upstream: - tar zxf $(PROJ_DIR)/$(UPSTREAM_ARCHIVE) + cp -a $(PROJ_DIR)/$(UPSTREAM_ARCHIVE) $(UPSTREAM_ORIG_ARCHIVE) + tar zxf $(UPSTREAM_ORIG_ARCHIVE) changelog: dch -v $(VERSION)-$(DEBVERSION) -D $(DIST) -M -u low \ diff --git a/build-deb/debian/source/format b/build-deb/debian/source/format index 89ae9db..163aaf8 100644 --- a/build-deb/debian/source/format +++ b/build-deb/debian/source/format @@ -1 +1 @@ -3.0 (native) +3.0 (quilt) From 2ece8312b4a6413cd4693c4ba3254874fd5a43ae Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Fri, 11 Sep 2015 13:35:30 +0800 Subject: [PATCH 56/58] Fix wrong version in bundle.json 0.9.4 instead of 0.9.4-2 --- bundle.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle.json b/bundle.json index 1fcc3d1..9e0999d 100644 --- a/bundle.json +++ b/bundle.json @@ -1,6 +1,6 @@ { "name": "route", - "version": "0.9.4-2", + "version": "0.9.4", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", From af744e2ae76c369085ed757ece0ef2dfb7b33c5c Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Fri, 23 Oct 2015 14:39:00 +0800 Subject: [PATCH 57/58] Bugfix: default gateway can't be updated sometimes --- build-deb/debian/changelog | 6 ++++++ bundle.json | 2 +- route.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/build-deb/debian/changelog b/build-deb/debian/changelog index 6865e44..ca256a1 100644 --- a/build-deb/debian/changelog +++ b/build-deb/debian/changelog @@ -1,3 +1,9 @@ +sanji-bundle-route (0.9.5-1) unstable; urgency=low + + * Bugfix: default gateway cannot be updated at some scenario. + + -- Aeluin Chen Fri, 23 Oct 2015 10:20:25 +0800 + sanji-bundle-route (0.9.4-2) unstable; urgency=low * Update building policy for debian package. diff --git a/bundle.json b/bundle.json index 9e0999d..11b3ce6 100644 --- a/bundle.json +++ b/bundle.json @@ -1,6 +1,6 @@ { "name": "route", - "version": "0.9.4", + "version": "0.9.5", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", diff --git a/route.py b/route.py index 2743296..12eb0ef 100755 --- a/route.py +++ b/route.py @@ -104,7 +104,7 @@ def get_default(self): """ gws = netifaces.gateways() default = {} - if gws['default'] != {}: + if gws['default'] != {} and netifaces.AF_INET in gws['default']: gw = gws['default'][netifaces.AF_INET] else: return default From 8e87fc64f3c76ce5f4daf0ebc17b785a3b0f5dcd Mon Sep 17 00:00:00 2001 From: Aeluin Chen Date: Thu, 5 Nov 2015 11:22:34 +0800 Subject: [PATCH 58/58] Add timeout to `grep` For preventing input without EOF. --- build-deb/debian/changelog | 6 ++++++ bundle.json | 2 +- ip/addr.py | 8 ++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/build-deb/debian/changelog b/build-deb/debian/changelog index ca256a1..29fbc9f 100644 --- a/build-deb/debian/changelog +++ b/build-deb/debian/changelog @@ -1,3 +1,9 @@ +sanji-bundle-route (0.9.6-1) unstable; urgency=low + + * Add timeout to `grep` for preventing input without EOF. + + -- Aeluin Chen Thu, 05 Nov 2015 11:22:22 +0800 + sanji-bundle-route (0.9.5-1) unstable; urgency=low * Bugfix: default gateway cannot be updated at some scenario. diff --git a/bundle.json b/bundle.json index 11b3ce6..af0dcdb 100644 --- a/bundle.json +++ b/bundle.json @@ -1,6 +1,6 @@ { "name": "route", - "version": "0.9.5", + "version": "0.9.6", "author": "Aeluin Chen", "email": "aeluin.chen@moxa.com", "description": "Handle the routing table", diff --git a/ip/addr.py b/ip/addr.py index cde5f6a..9684e9c 100755 --- a/ip/addr.py +++ b/ip/addr.py @@ -111,7 +111,9 @@ def ifupdown(iface, up): if not up: try: output = sh.awk( - sh.grep(sh.grep(sh.ps("ax"), iface), "dhclient"), + sh.grep( + sh.grep(sh.ps("ax"), iface, _timeout=5), + "dhclient", _timeout=5), "{print $1}") dhclients = output().split() for dhclient in dhclients: @@ -150,7 +152,9 @@ def ifconfig(iface, dhcpc, ip="", netmask="24", gateway="", script=None): # Disable the dhcp client and flush interface try: dhclients = sh.awk( - sh.grep(sh.grep(sh.ps("ax"), iface), "dhclient"), + sh.grep( + sh.grep(sh.ps("ax"), iface, _timeout=5), + "dhclient", _timeout=5), "{print $1}") dhclients = dhclients.split() if 1 == len(dhclients):