From d2d13994295cf1368a93dd6228a089a4e4224313 Mon Sep 17 00:00:00 2001 From: David Morel Date: Sat, 9 Aug 2025 11:31:59 +0200 Subject: [PATCH] Rework traffic-rules Previous implementation did not take how VLANs are configured into account, this lead to a pretty hefty change. VLANs use a fake bridge, which is then included in a parent bridge. While packets remain within this bridge, they are not tagged, so the vlanid cannot be used to match packets in the OVS datapath. The only workaround is to create rules for each port. However, this means that rules for untagged traffic, as previously implemented, will also apply to VLAN ports. Therefore, we must apply rules to each matching port in all cases, and create a rule on the uplink ports that matches accordingly. Changes summary: - Refactored to improve clarity and error handling - Added update_args_from_ovs() to dynamically gather bridge, VLAN, and port info from OVS. - Changed ip_range to ipRange that XO plugin is using - Replaced single rule building with per-port rule, as well as matching on VLAN for uplink ports - Updated tests to match the refactor - Added tests for update_args_from_ovs() - Mocked OVS command calls in tests for more accurate validation. Signed-off-by: David Morel --- README.md | 12 +- SOURCES/etc/xapi.d/plugins/sdncontroller.py | 288 +++++++-- tests/sdncontroller_test_cases/functions.py | 675 ++++++++++++++------ tests/sdncontroller_test_cases/parser.py | 648 ++++++++----------- tests/test_sdn-controller.py | 136 ++-- 5 files changed, 1071 insertions(+), 688 deletions(-) diff --git a/README.md b/README.md index f23bfc6..8461e8a 100644 --- a/README.md +++ b/README.md @@ -350,9 +350,9 @@ Parameters for adding a rule: - *priority* (optional): A number between 0 and 65535 for the rule priority. - *mac* (optional): The MAC address of the VIF to create the rule for, if not specified, a network-wide rule will be created. -- *iprange*: An IP or range of IPs in CIDR notation, for example `192.168.1.0/24`. +- *ipRange*: An IP or range of IPs in CIDR notation, for example `192.168.1.0/24`. - *direction*: can be **from**, **to** or **from/to** - - *to*: means the parameters for **port** and **iprange** are to be used as destination + - *to*: means the parameters for **port** and **ipRange** are to be used as destination - *from*: means they will be use as source - *from/to*: 2 rules will be created, one per direction - *protocol*: IP, TCP, UDP, ICMP or ARP @@ -365,7 +365,7 @@ $ xe host-call-plugin host-uuid plugin=sdncontroller.py \ args:bridge="xenbr0" \ args:priority="100" \ args:mac="6e:0b:9e:72:ab:c6" \ - args:iprange="192.168.1.0/24" \ + args:ipRange="192.168.1.0/24" \ args:direction="from/to" \ args:protocol="tcp" \ args:port="22" \ @@ -377,9 +377,9 @@ $ xe host-call-plugin host-uuid plugin=sdncontroller.py \ Parameters for removing a rule: - *bridge* : The name of the bridge to delete the rule from. - *mac* (optional): The MAC address of the VIF to delete the rule for. -- *iprange*: An IP or range of IPs in CIDR notation, for example `192.168.1.0/24`. +- *ipRange*: An IP or range of IPs in CIDR notation, for example `192.168.1.0/24`. - *direction*: can be **from**, **to** or **from/to** - - *to*: means the parameters for **port** and **iprange** are to be used as destination + - *to*: means the parameters for **port** and **ipRange** are to be used as destination - *from*: means they will be use as source - *from/to*: 2 rules will be created, one per direction - *protocol*: IP, TCP, UDP, ICMP or ARP @@ -390,7 +390,7 @@ $ xe host-call-plugin host-uuid plugin=sdncontroller.py \ fn=del-rule \ args:bridge="xenbr0" \ args:mac="6e:0b:9e:72:ab:c6" \ - args:iprange="192.168.1.0/24" \ + args:ipRange="192.168.1.0/24" \ args:direction="from/to" \ args:protocol="tcp" \ args:port="22" diff --git a/SOURCES/etc/xapi.d/plugins/sdncontroller.py b/SOURCES/etc/xapi.d/plugins/sdncontroller.py index 4f24ce7..6efe672 100755 --- a/SOURCES/etc/xapi.d/plugins/sdncontroller.py +++ b/SOURCES/etc/xapi.d/plugins/sdncontroller.py @@ -9,19 +9,27 @@ OPENFLOW_PROTOCOL = "OpenFlow11" OVS_OFCTL_CMD = "ovs-ofctl" +OVS_VSCTL_CMD = "ovs-vsctl" VALID_PROTOCOLS = {"tcp", "udp", "icmp", "ip", "arp"} PROTOCOLS_WITH_PORTS = {"tcp", "udp"} +OFPVID_NONE = "0xffff" # error codes E_PARSER = 1 E_PARAMS = 2 -E_BUILDER = 3 -E_OVSCALL = 4 +E_OVSCALL = 3 +E_PORTS = 4 + +# rules names per direction +TO = 0 +FROM = 1 + def log_and_raise_error(code, desc): _LOGGER.error(desc) raise_plugin_error(code, desc) + # All functions starting with `parse_` are helper functions compatible with the `Parser` class. # Each should accept `args` as input and return either (result, None) on success, # or (None, error_message) on failure. @@ -39,7 +47,9 @@ def parse_bridge(self): log_and_raise_error(E_PARSER, "bridge parameter is missing") if not BRIDGE_REGEX.match(bridge): - log_and_raise_error(E_PARSER, "'{}' is not a valid bridge name".format(bridge)) + log_and_raise_error( + E_PARSER, "'{}' is not a valid bridge name".format(bridge) + ) return bridge @@ -50,7 +60,10 @@ def parse_mac(self): if mac_addr is None: return None - if not MAC_REGEX.match(mac_addr) or MAC_REGEX.match(mac_addr).group(0) != mac_addr: + if ( + not MAC_REGEX.match(mac_addr) + or MAC_REGEX.match(mac_addr).group(0) != mac_addr + ): log_and_raise_error(E_PARSER, "'{}' is not a valid MAC".format(mac_addr)) return mac_addr @@ -60,13 +73,15 @@ def parse_iprange(self): r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}" r"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(/\d{1,2})?$" ) - ip_range = self.args.get("iprange") + ip_range = self.args.get("ipRange") if ip_range is None: - log_and_raise_error(E_PARSER, "ip range parameter is missing") + log_and_raise_error(E_PARSER, "ipRange parameter is missing") if not IPRANGE_REGEX.match(ip_range): - log_and_raise_error(E_PARSER, "'{}' is not a valid IP range".format(ip_range)) + log_and_raise_error( + E_PARSER, "'{}' is not a valid IP range".format(ip_range) + ) return ip_range @@ -81,7 +96,9 @@ def parse_direction(self): has_from = "from" in dir if not (has_to or has_from): - log_and_raise_error(E_PARSER, "'{}' is not a valid direction".format(direction)) + log_and_raise_error( + E_PARSER, "'{}' is not a valid direction".format(direction) + ) return (has_to, has_from) @@ -94,7 +111,9 @@ def parse_protocol(self): protocol = protocol.lower() if protocol not in VALID_PROTOCOLS: - log_and_raise_error(E_PARSER, "'{}' is not a supported protocol".format(protocol)) + log_and_raise_error( + E_PARSER, "'{}' is not a supported protocol".format(protocol) + ) return protocol @@ -117,7 +136,10 @@ def parse_allow(self): log_and_raise_error(E_PARSER, "allow parameter is missing") if allow.lower() not in ["true", "false"]: - log_and_raise_error(E_PARSER, "allow parameter should be true or false, not '{}'".format(allow)) + log_and_raise_error( + E_PARSER, + "allow parameter should be true or false, not '{}'".format(allow), + ) if allow.lower() == "true": return True @@ -135,7 +157,9 @@ def parse_priority(self): raise ValueError return priority except ValueError: - log_and_raise_error(E_PARSER, "'{}' is not a valid priority".format(priority)) + log_and_raise_error( + E_PARSER, "'{}' is not a valid priority".format(priority) + ) def read(self, key, parse_fn, dests=None): # parse_fn can return a single value or a tuple of values. @@ -147,13 +171,17 @@ def read(self, key, parse_fn, dests=None): if not isinstance(val, tuple): log_and_raise_error( E_PARSER, - "Internal error: parse {}: doesn't return a tuple while dests is set".format(key) + "Internal error: parse {}: doesn't return a tuple while dests is set".format( + key + ), ) if len(dests) != len(val): log_and_raise_error( E_PARSER, - "Internal error: parse {}: dests is {} while {} was expected".format(key, len(dests), len(val)) + "Internal error: parse {}: dests is {} while {} was expected".format( + key, len(dests), len(val) + ), ) for d, v in zip(dests, val): @@ -163,6 +191,7 @@ def read(self, key, parse_fn, dests=None): self.values[key] = val + def json_error(name, desc): return json.dumps( { @@ -173,40 +202,147 @@ def json_error(name, desc): } ) -def build_rules_string(args): + +def run_vsctl_cmd(args): + vsctl_cmd = [OVS_VSCTL_CMD] + args + cmd = run_command(vsctl_cmd, check=False) + if cmd["returncode"] != 0: + log_and_raise_error( + E_OVSCALL, + "Error running ovs-vsctl command: %s: %s" + % (format(vsctl_cmd), cmd["stderr"]), + ) + return cmd["stdout"] + + +def update_args_from_ovs(args): + # get parent bridge to apply rules to + args["parent-bridge"] = run_vsctl_cmd(["br-to-parent", args["bridge"]]).rstrip() + + # get ports names for our actual bridge, be it fake (vlan) or real (no vlan) + ifs_in_bridge = run_vsctl_cmd(["list-ports", args["bridge"]]).split() + ifs_in_parent_bridge = run_vsctl_cmd(["list-ports", args["parent-bridge"]]).split() + + # get vlanid if any + vlanid = run_vsctl_cmd(["get", "port", args["bridge"], "tag"]).rstrip() + if vlanid != "[]": + args["vlanid"] = vlanid + + # get the list of all ports and filter them with the ports names to get all interfaces + ports_j = json.loads(run_vsctl_cmd(["--format=json", "list", "port"])) + ports = [dict(zip(ports_j["headings"], row)) for row in ports_j["data"]] + interfaces = [] + parent_ifaces = [] + for port in ports: + name = port["name"] + if name in ifs_in_bridge: + interfaces.append(port["interfaces"]) + if name in ifs_in_parent_bridge: + parent_ifaces.append(port["interfaces"]) + if len(interfaces) == 0: + return + + # get the list of all interfaces, filter with what we found previously and get their ofports number + if_j = json.loads(run_vsctl_cmd(["--format=json", "list", "interface"])) + ifs = [dict(zip(if_j["headings"], row)) for row in if_j["data"]] + ofports = [] + for interface in ifs: + # port 65534 is the internal port for the bridge, we don't want to use it + if interface["_uuid"] in interfaces and interface["ofport"] != 65534: + ofports.append(interface["ofport"]) + + # second pass on interfaces, find uplinks ofports, that can be: + # - physical port ethX + # - physical ports ethX and Y of a bond + # - the port of a tunnel + uplinks = [] + for interface in ifs: + if interface["_uuid"] not in parent_ifaces: + continue + name = interface["name"] + # ignore irrelevant interfaces + if name.startswith("vif") or name.startswith("xen") or name.startswith("tap"): + continue + if ( + interface["type"] == "" + or interface["type"] == "gre" + or interface["type"] == "vxlan" + ): + uplinks.append(interface["ofport"]) + if interface["ofport"] in ofports: + ofports.remove(interface["ofport"]) + + args["uplinks"] = uplinks + args["ofports"] = ofports + + +def build_rules_strings(args): rules = [] + if args.get("has_to"): + if args.get("mac"): + rules.append(build_rule_string(TO, None, args)) + elif args.get("ofports"): + for ofport in args["ofports"]: + rules.append(build_rule_string(TO, ofport, args)) + for ofport in args["uplinks"]: + rules.append(build_rule_string(TO, ofport, args, uplink=True)) + + if args.get("has_from"): + if args.get("mac"): + rules.append(build_rule_string(FROM, None, args)) + elif args.get("ofports"): + for ofport in args["ofports"]: + rules.append(build_rule_string(FROM, ofport, args)) + for ofport in args["uplinks"]: + rules.append(build_rule_string(FROM, ofport, args, uplink=True)) + return rules - # tcp,dl_dst=26:bf:26:f0:4f:50,nw_src=192.168.38.0/24,tp_src=22 actions=NORMA - if args["has_from"]: - rule = "" - if "priority" in args and args["priority"]: - rule += "priority=" + args["priority"] + "," - rule += args["protocol"].lower() - if "mac" in args and args["mac"]: - rule += ",dl_dst=" + args["mac"] - rule += ",nw_src=" + args["iprange"] - if "protocol" in args and args["protocol"] in PROTOCOLS_WITH_PORTS: - rule += ",tp_src=" + args["port"] - if "allow" in args: - rule += ",actions=" + ("normal" if args["allow"] else "drop") - rules += [rule] - - # tcp,in_port=3,dl_src=26:bf:26:f0:4f:50,nw_dst=192.168.38.0/24,tp_dst=2 - if args["has_to"]: - rule = "" - if "priority" in args and args["priority"]: - rule += "priority=" + args["priority"] + "," - rule += args["protocol"].lower() - if "mac" in args and args["mac"]: - rule += ",dl_src=" + args["mac"] - rule += ",nw_dst=" + args["iprange"] - if "protocol" in args and args["protocol"] in PROTOCOLS_WITH_PORTS: - rule += ",tp_dst=" + args["port"] - if "allow" in args: - rule += ",actions=" + ("normal" if args["allow"] else "drop") - rules += [rule] - return rules +def build_rule_string(direction, ofport, args, uplink=False): + # To / From + rule_parts = { + "priority": ("priority", "priority"), + "protocol": (None, None), + "ofport": ("in_port", "in_port"), + "mac": ("dl_src", "dl_dst"), + "iprange": ("nw_dst", "nw_src"), + "port": ("tp_dst", "tp_src"), + "allow": ("actions", "actions"), + } + + rule = "" + vlanid = OFPVID_NONE + if args.get("vlanid"): + vlanid = args["vlanid"] + + if args.get("priority"): + rule += "priority={}".format(args["priority"]) + "," + rule += args["protocol"] + if uplink: + rule += ",dl_vlan={}".format(vlanid) + if ofport: + rule += "," + rule_parts["ofport"][direction] + "={}".format(ofport) + if args.get("mac"): + rule += "," + rule_parts["mac"][direction] + "={}".format(args["mac"]) + rule += "," + rule_parts["iprange"][direction] + "={}".format(args["iprange"]) + if args.get("port"): + rule += "," + rule_parts["port"][direction] + "={}".format(args["port"]) + if "allow" in args: # optional in case of del_rule + rule += ",actions={}".format("normal" if args["allow"] else "drop") + return rule + + +def run_ofctl_cmd(cmd, bridge, rule): + ofctl_cmd = [OVS_OFCTL_CMD, "-O", OPENFLOW_PROTOCOL, cmd, bridge, rule] + cmd = run_command(ofctl_cmd, check=False) + if cmd["returncode"] != 0: + log_and_raise_error( + E_OVSCALL, + "Error running ovs-ofctl command: %s: %s" + % (format(ofctl_cmd), cmd["stderr"]), + ) + _LOGGER.info("Applied rule: {}".format(ofctl_cmd)) + @error_wrapped def add_rule(_session, args): @@ -223,31 +359,39 @@ def add_rule(_session, args): parser.read("allow", parser.parse_allow) parser.read("priority", parser.parse_priority) except XenAPIPlugin.Failure as e: - log_and_raise_error(E_PARSER, "add_rule: Failed to get parameters: {}".format(e.params[1])) + log_and_raise_error( + E_PARSER, "add_rule: Failed to get parameters: {}".format(e.params[1]) + ) if parser.errors: - log_and_raise_error(E_PARSER, "add_rule: Failed to get parameters: {}".format(parser.errors)) + log_and_raise_error( + E_PARSER, "add_rule: Failed to get parameters: {}".format(parser.errors) + ) rule_args = parser.values - _LOGGER.info("successfully parsed: {}".format(rule_args)) # sanity check if rule_args["protocol"] in PROTOCOLS_WITH_PORTS and not rule_args["port"]: - log_and_raise_error(E_PARAMS, "add_rule: No port provided, tcp and udp requires one") + log_and_raise_error( + E_PARAMS, "add_rule: No port provided, tcp and udp requires one" + ) - ofctl_cmd = [OVS_OFCTL_CMD, "-O", OPENFLOW_PROTOCOL, "add-flow", rule_args["bridge"]] + # update vlanid first, as bridge could be replaced by parent bridge + update_args_from_ovs(rule_args) + if rule_args["ofports"] is None: + log_and_raise_error( + E_PORTS, "No ports found for bridge: {}".format(rule_args["bridge"]) + ) # We can now build the open flow rule - rules = build_rules_string(rule_args) + rules = build_rules_strings(rule_args) + _LOGGER.info("Built rules: {}".format(rules)) if not rules: - log_and_raise_error(E_BUILDER, "add_rule: No rules were build") + log_and_raise_error(E_PORTS, "add_rule: No rules were build") for rule in rules: - cmd = run_command(ofctl_cmd + [rule], check=False) - if cmd['returncode'] != 0: - log_and_raise_error(E_OVSCALL, "Error adding rule: %s: %s" % (format(ofctl_cmd + [rule]), cmd['stderr'])) - _LOGGER.info("Added rule: {}".format(ofctl_cmd + [rule])) + run_ofctl_cmd("add-flow", rule_args["parent-bridge"], rule) return json.dumps(True) @@ -265,28 +409,33 @@ def del_rule(_session, args): parser.read("iprange", parser.parse_iprange) parser.read("port", parser.parse_port) except XenAPIPlugin.Failure as e: - log_and_raise_error(E_PARSER, "del_rule: Failed to get parameters: {}".format(e.params[1])) + log_and_raise_error( + E_PARSER, "del_rule: Failed to get parameters: {}".format(e.params[1]) + ) if parser.errors: - log_and_raise_error(E_PARSER, "del_rule: Failed to get parameters: {}".format(parser.errors)) + log_and_raise_error( + E_PARSER, "del_rule: Failed to get parameters: {}".format(parser.errors) + ) rule_args = parser.values _LOGGER.info("successfully parsed: {}".format(rule_args)) # sanity check if rule_args["protocol"] in PROTOCOLS_WITH_PORTS and not rule_args["port"]: - log_and_raise_error(E_PARAMS, "del_rule: No port provided, tcp and udp requires one") + log_and_raise_error( + E_PARAMS, "del_rule: No port provided, tcp and udp requires one" + ) - ofctl_cmd = [OVS_OFCTL_CMD, "-O", OPENFLOW_PROTOCOL, "del-flows", rule_args["bridge"]] + update_args_from_ovs(rule_args) # We can now build the open flow rule - rules = build_rules_string(rule_args) + rules = build_rules_strings(rule_args) + _LOGGER.info("Built rules: {}".format(rules)) for rule in rules: - cmd = run_command(ofctl_cmd + [rule], check=False) - if cmd['returncode']: - log_and_raise_error(E_OVSCALL, "Error deleting rule: %s: %s" % (format(ofctl_cmd + [rule]), cmd['stderr'])) - _LOGGER.info("Deleted rule: {}".format(ofctl_cmd + [rule])) + run_ofctl_cmd("del-flows", rule_args["parent-bridge"], rule) + return json.dumps(True) @@ -298,12 +447,17 @@ def dump_flows(_session, args): try: bridge = parser.parse_bridge() except XenAPIPlugin.Failure as e: - log_and_raise_error(E_PARSER, "dump_flows: Failed to get parameters: {}".format(e.params[1])) + log_and_raise_error( + E_PARSER, "dump_flows: Failed to get parameters: {}".format(e.params[1]) + ) ofctl_cmd = [OVS_OFCTL_CMD, "-O", OPENFLOW_PROTOCOL, "dump-flows", bridge] cmd = run_command(ofctl_cmd, check=False) - if cmd['returncode']: - log_and_raise_error(E_OVSCALL, "Error dumping flows: %s: %s" % (format(ofctl_cmd), cmd['stderr'])) + if cmd["returncode"]: + log_and_raise_error( + E_OVSCALL, + "Error dumping flows: %s: %s" % (format(ofctl_cmd), cmd["stderr"]), + ) return json.dumps(cmd) diff --git a/tests/sdncontroller_test_cases/functions.py b/tests/sdncontroller_test_cases/functions.py index 205a30c..9381485 100644 --- a/tests/sdncontroller_test_cases/functions.py +++ b/tests/sdncontroller_test_cases/functions.py @@ -2,215 +2,504 @@ # PARAMS are in lists of test cases # IDS are just a description of the test for pytest, manual matching is needed import XenAPIPlugin +from mock import call -ADD_RULE_PARAMS = [ - { # no args - 'args': {}, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "add_rule: Failed to get parameters: bridge parameter is missing" - }, - 'cmd': { - 'returncode': 0, - 'stdout': '', - 'stderr': '' - }, - }, - { # ip drop - 'args': { - 'bridge': 'xenbr0', - 'direction': 'to', - 'protocol': 'ip', - 'iprange': '1.1.1.1', - 'allow': 'false', - }, - 'exception': None, - 'cmd': { - 'returncode': 0, - 'stdout': '', - 'stderr': '' - }, - }, - { # subnet tcp 4242 allow - 'args': { - 'bridge': 'xenbr0', - 'direction': 'to', - 'protocol': 'tcp', - 'port': '4242', - 'iprange': '1.1.1.1/24', - 'allow': 'false', - }, - 'exception': None, - 'cmd': { - 'returncode': 0, - 'stdout': '', - 'stderr': '' - }, - }, - { # tcp no port - 'args': { - 'bridge': 'xenbr0', - 'direction': 'to', - 'protocol': 'tcp', - 'iprange': '1.1.1.1/24', - 'allow': 'false', - }, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '2', - 'text': "add_rule: No port provided, tcp and udp requires one" - }, - 'cmd': { - 'returncode': 0, - 'stdout': '', - 'stderr': '' - }, - }, - { # failed ovs call - 'args': { - 'bridge': 'xenbr0', - 'direction': 'to', - 'protocol': 'ip', - 'iprange': '1.1.1.1/24', - 'allow': 'false', - }, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '4', - 'text': "Error adding rule: ['ovs-ofctl', '-O', 'OpenFlow11', 'add-flow', 'xenbr0', " - "'ip,nw_dst=1.1.1.1/24,actions=drop']: fake error" - }, - 'cmd': { - 'returncode': 1, - 'stdout': '', - 'stderr': 'fake error' +# List of ports in JSON format, as output by the `ovs-vsctl --format=json list port` command. +PORT_LIST_JSON = """ +{"data":[[["uuid","0de54408-a136-4d67-a258-dc6e6b894f72"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","e1b02bec-caec-45ba-a548-536a64806423"],["set",[]],["set",[]],"xapi31_port15",["map",[["xo:sdn-controller:private-network-uuid","364a15c4-3138-4ef6-930f-704c1711e92c"]]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","f245a8f8-3845-4870-b2de-57e102bb4d9a"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","15472a6b-4aa9-4dcf-b300-715b79377bf1"],["set",[]],["set",[]],"vif2.0",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","0e077dd0-df09-4864-a8c0-a6f485892921"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","fefec25f-1262-4acd-9d84-fe20f41d54b5"],["set",[]],["set",[]],"xapi9",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","fef17539-5477-4914-b68e-60381c36f79d"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","23c506aa-a061-4c8e-b6ce-35a46265c2aa"],["set",[]],["set",[]],"xapi16_port13",["map",[["xo:sdn-controller:private-network-uuid","25ea1e51-b662-4679-a71f-d0a9624b4832"]]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","473301b2-7ca8-4741-bf66-236122e1176d"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","1d12f735-b1ef-458a-a7c8-a6f6a5f86172"],["set",[]],["set",[]],"eth0",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","ec7b8cb2-9b65-4af0-9c67-f9bd8f5bef29"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","26ddebea-49d0-465e-991d-1b22a968f40e"],["set",[]],["set",[]],"xapi16",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","6a737693-f9aa-42a6-a05d-e254d743c3fc"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","72e590be-14ba-4f64-8ede-1a0f751c6c56"],["set",[]],["set",[]],"xapi31",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","7b52cff3-8b41-4e20-80db-62b927249b91"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","00314263-3096-4820-9a2f-509504fe9c1e"],["set",[]],["set",[]],"xapi9_port14",["map",[["xo:sdn-controller:private-network-uuid","b14c3cb2-2189-4086-b1c7-42d99d55f005"]]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","7f6cd65c-3382-4f2e-99c7-21637b198ffb"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","d76bc9ba-22c5-4fd6-9eaf-9502b59940ea"],["set",[]],["set",[]],"vif2.2",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","01409acd-750e-4f37-a091-1006ab9acfb0"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],true,["uuid","c3b73f81-0810-4fbe-b2b4-ff797f8abb22"],["set",[]],["set",[]],"xapi5",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],100,["set",[]],["set",[]]],[["uuid","bc222a76-b79c-4a43-b753-af91c9d10ff1"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","3a8c2af0-88ca-4a31-bbe3-b7b83668e52d"],["set",[]],["set",[]],"vif1.0",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","4bb3cc76-5a2e-41eb-b018-26d9ef934f21"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","e0d981dd-ae2c-4e30-a7b0-e96f1a6ae4bc"],["set",[]],["set",[]],"vif2.1",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],100,["set",[]],["set",[]]],[["uuid","6cda9ca7-f0f3-47dd-8fff-93016be3497e"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],false,["uuid","e60820dd-c660-4853-a78f-51a67a4c7b07"],["set",[]],["set",[]],"xenbr0",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],["set",[]],["set",[]],["set",[]]],[["uuid","7517d2d7-3679-4e90-8c79-4a52b585a582"],["set",[]],0,false,["set",[]],0,["set",[]],["map",[]],true,["uuid","191baed0-c6f5-470d-9aa6-e0c9ce960185"],["set",[]],["set",[]],"xapi0",["map",[]],false,["set",[]],["map",[]],["map",[]],["map",[]],["map",[]],42,["set",[]],["set",[]]]],"headings":["_uuid","bond_active_slave","bond_downdelay","bond_fake_iface","bond_mode","bond_updelay","cvlans","external_ids","fake_bridge","interfaces","lacp","mac","name","other_config","protected","qos","rstp_statistics","rstp_status","statistics","status","tag","trunks","vlan_mode"]} + +""" + +# List of interfaces in JSON format, as output by the `ovs-vsctl --format=json list interface` command. +INTERFACE_LIST_JSON = """ +{"data":[[["uuid","191baed0-c6f5-470d-9aa6-e0c9ce960185"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[]],22,0,0,0,0,["set",[]],1,["set",[]],"up",["map",[]],"f8:0d:ac:1a:da:de","f8:0d:ac:1a:da:de",1500,1500,"xapi0",9,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",0],["rx_crc_err",0],["rx_dropped",0],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",0],["rx_over_err",0],["rx_packets",0],["tx_bytes",0],["tx_dropped",0],["tx_errors",0],["tx_packets",0]]],["map",[["driver_name","openvswitch"]]],"internal"],[["uuid","3a8c2af0-88ca-4a31-bbe3-b7b83668e52d"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[["attached-mac","56:55:a4:a4:ce:71"],["xs-network-uuid","9334aa83-6960-62e5-a463-5acc05295af4"],["xs-vif-uuid","e41c690e-c0a1-f130-4e95-b9a505db2f3c"],["xs-vm-uuid","0ec00fdc-8127-8817-40d4-79d61797f87a"]]],5,0,0,0,0,["set",[]],3,["set",[]],"up",["map",[]],["set",[]],"fe:ff:ff:ff:ff:ff",1500,["set",[]],"vif1.0",2,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",950026692],["rx_crc_err",0],["rx_dropped",0],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",0],["rx_over_err",0],["rx_packets",8366385],["tx_bytes",101259026443],["tx_dropped",5],["tx_errors",0],["tx_packets",13563793]]],["map",[["driver_name","vif"],["driver_version",""],["firmware_version",""]]],""],[["uuid","c3b73f81-0810-4fbe-b2b4-ff797f8abb22"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[]],15,0,0,0,0,["set",[]],1,["set",[]],"up",["map",[]],"f8:0d:ac:1a:da:de","f8:0d:ac:1a:da:de",1500,1500,"xapi5",4,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",32756],["rx_crc_err",0],["rx_dropped",0],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",0],["rx_over_err",0],["rx_packets",732],["tx_bytes",0],["tx_dropped",0],["tx_errors",0],["tx_packets",0]]],["map",[["driver_name","openvswitch"]]],"internal"],[["uuid","23c506aa-a061-4c8e-b6ce-35a46265c2aa"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[]],11,0,0,0,0,["set",[]],0,["set",[]],"up",["map",[]],["set",[]],"2a:b8:2c:93:bc:fd",["set",[]],["set",[]],"xapi16_iface13",1,["set",[]],["map",[["key","12"],["remote_ip","192.168.1.221"]]],["map",[]],["map",[["rx_bytes",0],["rx_packets",0],["tx_bytes",0],["tx_packets",0]]],["map",[["tunnel_egress_iface","xenbr0"],["tunnel_egress_iface_carrier","up"]]],"gre"],[["uuid","26ddebea-49d0-465e-991d-1b22a968f40e"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[]],7,0,0,0,0,["set",[]],1,["set",[]],"up",["map",[]],["set",[]],"6e:54:d3:eb:1e:a4",1500,1500,"xapi16",65534,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",0],["rx_crc_err",0],["rx_dropped",0],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",0],["rx_over_err",0],["rx_packets",0],["tx_bytes",0],["tx_dropped",0],["tx_errors",0],["tx_packets",0]]],["map",[["driver_name","openvswitch"]]],"internal"],[["uuid","d76bc9ba-22c5-4fd6-9eaf-9502b59940ea"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[["attached-mac","0a:27:cf:e6:d2:9c"],["xs-network-uuid","3d809de7-f11b-db02-40ca-0f51330aae19"],["xs-vif-uuid","547e321d-88f2-c698-7f8d-d26630165078"],["xs-vm-uuid","e07d8464-9a05-e391-b4c3-5dc38be4c792"]]],16,0,0,0,0,["set",[]],3,["set",[]],"up",["map",[]],["set",[]],"fe:ff:ff:ff:ff:ff",1500,["set",[]],"vif2.2",2,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",12136],["rx_crc_err",0],["rx_dropped",0],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",0],["rx_over_err",0],["rx_packets",215],["tx_bytes",15146],["tx_dropped",0],["tx_errors",0],["tx_packets",215]]],["map",[["driver_name","vif"],["driver_version",""],["firmware_version",""]]],""],[["uuid","15472a6b-4aa9-4dcf-b300-715b79377bf1"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[["attached-mac","4e:01:39:a6:46:69"],["xs-network-uuid","9334aa83-6960-62e5-a463-5acc05295af4"],["xs-vif-uuid","e9a7914a-9518-82e2-7052-42cb16cc9724"],["xs-vm-uuid","e07d8464-9a05-e391-b4c3-5dc38be4c792"]]],17,0,0,0,0,["set",[]],1,["set",[]],"up",["map",[]],["set",[]],"fe:ff:ff:ff:ff:ff",1500,["set",[]],"vif2.0",6,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",1080132],["rx_crc_err",0],["rx_dropped",0],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",0],["rx_over_err",0],["rx_packets",32439],["tx_bytes",257625162],["tx_dropped",0],["tx_errors",0],["tx_packets",2379022]]],["map",[["driver_name","vif"],["driver_version",""],["firmware_version",""]]],""],[["uuid","00314263-3096-4820-9a2f-509504fe9c1e"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[]],13,0,0,0,0,["set",[]],0,["set",[]],"up",["map",[]],["set",[]],"ce:5a:a4:99:b0:e5",["set",[]],["set",[]],"xapi9_iface14",1,["set",[]],["map",[["key","5"],["remote_ip","192.168.1.221"]]],["map",[]],["map",[["rx_bytes",15146],["rx_packets",215],["tx_bytes",15146],["tx_packets",215]]],["map",[["tunnel_egress_iface","xenbr0"],["tunnel_egress_iface_carrier","up"]]],"vxlan"],[["uuid","e1b02bec-caec-45ba-a548-536a64806423"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[]],11,0,0,0,0,["set",[]],0,["set",[]],"up",["map",[]],["set",[]],"22:2f:77:14:8b:ea",["set",[]],["set",[]],"xapi31_iface15",1,["set",[]],["map",[["key","14"],["remote_ip","192.168.1.221"]]],["map",[]],["map",[["rx_bytes",0],["rx_packets",0],["tx_bytes",0],["tx_packets",0]]],["map",[["tunnel_egress_iface","xenbr0"],["tunnel_egress_iface_carrier","up"]]],"gre"],[["uuid","e0d981dd-ae2c-4e30-a7b0-e96f1a6ae4bc"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[["attached-mac","62:16:6e:15:38:c3"],["xs-network-uuid","3878b6de-84e8-a988-5341-d09cf8c821ba"],["xs-vif-uuid","75980193-aa29-3359-087c-2bd11cb04b24"],["xs-vm-uuid","e07d8464-9a05-e391-b4c3-5dc38be4c792"]]],18,0,0,0,0,["set",[]],3,["set",[]],"up",["map",[]],["set",[]],"fe:ff:ff:ff:ff:ff",1500,["set",[]],"vif2.1",5,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",20732],["rx_crc_err",0],["rx_dropped",0],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",0],["rx_over_err",0],["rx_packets",519],["tx_bytes",15364],["tx_dropped",0],["tx_errors",0],["tx_packets",218]]],["map",[["driver_name","vif"],["driver_version",""],["firmware_version",""]]],""],[["uuid","1d12f735-b1ef-458a-a7c8-a6f6a5f86172"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],"full",["set",[]],["map",[]],2,0,0,0,0,["set",[]],1,1000000000,"up",["map",[]],["set",[]],"f8:0d:ac:1a:da:de",1500,1500,"eth0",1,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",143676807514],["rx_crc_err",0],["rx_dropped",0],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",1130962],["rx_over_err",0],["rx_packets",198109882],["tx_bytes",50188460190],["tx_dropped",0],["tx_errors",0],["tx_packets",142255313]]],["map",[["driver_name","r8169"],["driver_version",""],["firmware_version",""]]],""],[["uuid","e60820dd-c660-4853-a78f-51a67a4c7b07"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[]],4,0,0,0,0,["set",[]],1,["set",[]],"up",["map",[]],["set",[]],"f8:0d:ac:1a:da:de",1500,1500,"xenbr0",65534,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",36745505785],["rx_crc_err",0],["rx_dropped",602217],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",0],["rx_over_err",0],["rx_packets",122886392],["tx_bytes",48280983892],["tx_dropped",0],["tx_errors",0],["tx_packets",121147512]]],["map",[["driver_name","openvswitch"]]],"internal"],[["uuid","72e590be-14ba-4f64-8ede-1a0f751c6c56"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[]],14,0,0,0,0,["set",[]],1,["set",[]],"up",["map",[]],["set",[]],"8a:7e:24:58:c9:21",1500,1500,"xapi31",65534,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",0],["rx_crc_err",0],["rx_dropped",0],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",0],["rx_over_err",0],["rx_packets",0],["tx_bytes",0],["tx_dropped",0],["tx_errors",0],["tx_packets",0]]],["map",[["driver_name","openvswitch"]]],"internal"],[["uuid","fefec25f-1262-4acd-9d84-fe20f41d54b5"],"up",["map",[]],["map",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["set",[]],["map",[]],12,0,0,0,0,["set",[]],1,["set",[]],"up",["map",[]],["set",[]],"62:98:0c:6c:2e:d1",1500,1500,"xapi9",65534,["set",[]],["map",[]],["map",[]],["map",[["collisions",0],["rx_bytes",24272],["rx_crc_err",0],["rx_dropped",0],["rx_errors",0],["rx_frame_err",0],["rx_missed_errors",0],["rx_multicast_packets",0],["rx_over_err",0],["rx_packets",430],["tx_bytes",0],["tx_dropped",0],["tx_errors",0],["tx_packets",0]]],["map",[["driver_name","openvswitch"]]],"internal"]],"headings":["_uuid","admin_state","bfd","bfd_status","cfm_fault","cfm_fault_status","cfm_flap_count","cfm_health","cfm_mpid","cfm_remote_mpids","cfm_remote_opstate","duplex","error","external_ids","ifindex","ingress_policing_burst","ingress_policing_kpkts_burst","ingress_policing_kpkts_rate","ingress_policing_rate","lacp_current","link_resets","link_speed","link_state","lldp","mac","mac_in_use","mtu","mtu_request","name","ofport","ofport_request","options","other_config","statistics","status","type"]} +""" + +UPDATE_ARGS_PARAMS = [ + { # no vlan + "args": {"bridge": "xenbr0"}, + "exception": None, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + { + "returncode": 0, + "stdout": "eth0\nvif1.0\nvif2.0\n", + "stderr": "", + }, # list-ports + { + "returncode": 0, + "stdout": "eth0\nvif1.0\nvif2.0\n", + "stderr": "", + }, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + { + "returncode": 0, + "stdout": INTERFACE_LIST_JSON, + "stderr": "", + }, # list-interfaces + ], + "ofports": [2, 6], + "uplinks": [1], + }, + { # with vlan + "args": {"bridge": "xapi5"}, + "exception": None, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + { + "returncode": 0, + "stdout": "eth0\nvif1.0\nvif2.0\n", + "stderr": "", + }, # list-ports parent + {"returncode": 0, "stdout": "100", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + { + "returncode": 0, + "stdout": INTERFACE_LIST_JSON, + "stderr": "", + }, # list-interfaces + ], + "ofports": [5], + "uplinks": [1], + }, + { # invalid bridge + "args": {"bridge": "abcd"}, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "3", + "text": "Error running ovs-vsctl command: ['ovs-vsctl', " + "'br-to-parent', 'abcd']: ovs-vsctl: no bridge named abcd", + }, + "cmd": [ + { + "returncode": 1, + "stdout": "", + "stderr": "ovs-vsctl: no bridge named abcd", + }, + ], + }, + { # error listing ports in bridge`` + "args": {"bridge": "xenbr0"}, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "3", + "text": "Error running ovs-vsctl command: ['ovs-vsctl', 'list-ports', 'xenbr0']: fake error", + }, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 1, "stdout": "", "stderr": "fake error"}, + ], + }, + { # error getting bridge tag + "args": {"bridge": "xenbr0"}, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "3", + "text": "Error running ovs-vsctl command: ['ovs-vsctl', 'get', 'port', 'xenbr0', 'tag']: fake error", + }, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + { + "returncode": 0, + "stdout": "eth0\nvif1.0\nvif2.0\n", + "stderr": "", + }, # list-ports parent + {"returncode": 1, "stdout": "", "stderr": "fake error"}, + ], + }, + { # error listing all ports + "args": {"bridge": "xenbr0"}, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "3", + "text": "Error running ovs-vsctl command: ['ovs-vsctl', '--format=json', 'list', 'port']: fake error", + }, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + { + "returncode": 0, + "stdout": "eth0\nvif1.0\nvif2.0\n", + "stderr": "", + }, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 1, "stdout": "", "stderr": "fake error"}, + ], + }, + { # error listing all interfaces + "args": {"bridge": "xenbr0"}, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "3", + "text": "Error running ovs-vsctl command: ['ovs-vsctl', '--format=json', 'list', 'interface']: fake error", }, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + { + "returncode": 0, + "stdout": "eth0\nvif1.0\nvif2.0\n", + "stderr": "", + }, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + {"returncode": 1, "stdout": "", "stderr": "fake error"}, + ], }, ] -ADD_RULE_IDS = ["no args", "ip drop", "subnet tcp 4242 allow", "tcp no port", - "failed ovs call"] +UPDATE_ARGS_IDS = [ + "no vlan", + "with vlan", + "invalid bridge", + "error listing ports in bridge", + "error getting bridge tag", + "error listing all ports", + "error listing all interfaces", +] +ADD_RULE_PARAMS = [ + { # no args + "args": {}, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "add_rule: Failed to get parameters: bridge parameter is missing", + }, + "cmd": [], + }, + { # ip drop + "args": { + "bridge": "xenbr0", + "direction": "to", + "protocol": "ip", + "ipRange": "1.1.1.1", + "allow": "false", + }, + "exception": None, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + { + "returncode": 0, + "stdout": INTERFACE_LIST_JSON, + "stderr": "", + }, # list-interfaces + ], + "calls": [ + call("add-flow", "xenbr0", "ip,in_port=5,nw_dst=1.1.1.1,actions=drop"), + ], + }, + { # subnet tcp 4242 allow + "args": { + "bridge": "xenbr0", + "direction": "to", + "protocol": "tcp", + "port": "4242", + "ipRange": "1.1.1.1/24", + "allow": "true", + }, + "exception": None, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + { + "returncode": 0, + "stdout": INTERFACE_LIST_JSON, + "stderr": "", + }, # list-interfaces + ], + "calls": [ + call( + "add-flow", + "xenbr0", + "tcp,in_port=5,nw_dst=1.1.1.1/24,tp_dst=4242,actions=normal", + ) + ], + }, + { # udp port from vif drop + "args": { + "bridge": "xenbr0", + "direction": "from", + "mac": "DE:AD:BE:EF:CA:FE", + "protocol": "udp", + "port": "2121", + "ipRange": "4.4.4.4", + "allow": "false", + }, + "exception": None, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + { + "returncode": 0, + "stdout": INTERFACE_LIST_JSON, + "stderr": "", + }, # list-interfaces + ], + "calls": [ + call( + "add-flow", + "xenbr0", + "udp,dl_dst=DE:AD:BE:EF:CA:FE,nw_src=4.4.4.4,tp_src=2121,actions=drop", + ), + ], + }, + { # tcp no port + "args": { + "bridge": "xenbr0", + "direction": "to", + "protocol": "tcp", + "ipRange": "1.1.1.1/24", + "allow": "false", + }, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "2", + "text": "add_rule: No port provided, tcp and udp requires one", + }, + "cmd": {"returncode": 0, "stdout": "", "stderr": ""}, + }, + { # failed ovs call + "args": { + "bridge": "xenbr0", + "direction": "to", + "protocol": "ip", + "ipRange": "1.1.1.1/24", + "allow": "false", + }, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "3", + "text": "Error running ovs-ofctl command: ['ovs-ofctl', '-O', 'OpenFlow11', 'add-flow', 'xenbr0', " + "'ip,in_port=5,nw_dst=1.1.1.1/24,actions=drop']: fake error", + }, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + { + "returncode": 0, + "stdout": INTERFACE_LIST_JSON, + "stderr": "", + }, # list-interfaces + {"returncode": 1, "stdout": "", "stderr": "fake error"}, # ofctl error + ], + }, +] +ADD_RULE_IDS = [ + "no args", + "ip drop", + "subnet tcp 4242 allow", + "udp port from vif drop", + "tcp no port", + "failed ovs call", +] DEL_RULE_PARAMS = [ { - 'args': {}, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "del_rule: Failed to get parameters: bridge parameter is missing" - }, - 'cmd': { - 'returncode': 0, - 'stdout': '', - 'stderr': '' - }, - }, - { # ip drop - 'args': { - 'bridge': 'xenbr0', - 'direction': 'to', - 'protocol': 'ip', - 'iprange': '1.1.1.1', - }, - 'exception': None, - 'cmd': { - 'returncode': 0, - 'stdout': '', - 'stderr': '' - }, - }, - { # tcp no port - 'args': { - 'bridge': 'xenbr0', - 'direction': 'to', - 'protocol': 'tcp', - 'iprange': '1.1.1.1/24', - 'allow': 'false', - }, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '2', - 'text': "del_rule: No port provided, tcp and udp requires one" - }, - 'cmd': { - 'returncode': 0, - 'stdout': '', - 'stderr': '' - }, - }, - { # failed ovs call - 'args': { - 'bridge': 'xenbr0', - 'direction': 'to', - 'protocol': 'ip', - 'iprange': '1.1.1.1/24', - }, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '4', - 'text': "Error deleting rule: ['ovs-ofctl', '-O', 'OpenFlow11', 'del-flows', 'xenbr0', " - "'ip,nw_dst=1.1.1.1/24']: fake error" - }, - 'cmd': { - 'returncode': 1, - 'stdout': '', - 'stderr': 'fake error' - }, - } + "args": {}, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "del_rule: Failed to get parameters: bridge parameter is missing", + }, + "cmd": {"returncode": 0, "stdout": "", "stderr": ""}, + }, + { # ip drop + "args": { + "bridge": "xenbr0", + "direction": "to", + "protocol": "ip", + "ipRange": "1.1.1.1", + "allow": "false", + }, + "exception": None, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + { + "returncode": 0, + "stdout": INTERFACE_LIST_JSON, + "stderr": "", + }, # list-interfaces + ], + "calls": [ + call("del-flows", "xenbr0", "ip,in_port=5,nw_dst=1.1.1.1"), + ], + }, + { # subnet tcp 4242 allow + "args": { + "bridge": "xenbr0", + "direction": "to", + "protocol": "tcp", + "port": "4242", + "ipRange": "1.1.1.1/24", + "allow": "true", + }, + "exception": None, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + { + "returncode": 0, + "stdout": INTERFACE_LIST_JSON, + "stderr": "", + }, # list-interfaces + ], + "calls": [ + call( + "del-flows", + "xenbr0", + "tcp,in_port=5,nw_dst=1.1.1.1/24,tp_dst=4242", + ) + ], + }, + { # udp port from vif drop + "args": { + "bridge": "xenbr0", + "direction": "from", + "mac": "DE:AD:BE:EF:CA:FE", + "protocol": "udp", + "port": "2121", + "ipRange": "4.4.4.4", + "allow": "false", + }, + "exception": None, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + { + "returncode": 0, + "stdout": INTERFACE_LIST_JSON, + "stderr": "", + }, # list-interfaces + ], + "calls": [ + call( + "del-flows", + "xenbr0", + "udp,dl_dst=DE:AD:BE:EF:CA:FE,nw_src=4.4.4.4,tp_src=2121", + ), + ], + }, + { # tcp no port + "args": { + "bridge": "xenbr0", + "direction": "to", + "protocol": "tcp", + "ipRange": "1.1.1.1/24", + "allow": "false", + }, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "2", + "text": "del_rule: No port provided, tcp and udp requires one", + }, + "cmd": {"returncode": 0, "stdout": "", "stderr": ""}, + }, + { # failed ovs call + "args": { + "bridge": "xenbr0", + "direction": "to", + "protocol": "ip", + "ipRange": "1.1.1.1/24", + "allow": "false", + }, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "3", + "text": "Error running ovs-ofctl command: ['ovs-ofctl', '-O', 'OpenFlow11', 'del-flows', 'xenbr0', " + "'ip,in_port=5,nw_dst=1.1.1.1/24']: fake error", + }, + "cmd": [ + {"returncode": 0, "stdout": "xenbr0", "stderr": ""}, # br-to-parent + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports + {"returncode": 0, "stdout": "vif2.1\n", "stderr": ""}, # list-ports parent + {"returncode": 0, "stdout": "[]", "stderr": ""}, # get port tag + {"returncode": 0, "stdout": PORT_LIST_JSON, "stderr": ""}, # list port + { + "returncode": 0, + "stdout": INTERFACE_LIST_JSON, + "stderr": "", + }, # list-interfaces + {"returncode": 1, "stdout": "", "stderr": "fake error"}, # ofctl error + ], + }, +] +DEL_RULE_IDS = [ + "no args", + "ip drop", + "subnet tcp 4242 allow", + "udp port from vif drop", + "tcp no port", + "failed ovs call", ] -DEL_RULE_IDS = ["no args", "ip drop", "tcp no port", "failed ovs call"] DUMP_FLOWS_PARAMS = [ - { # no args - 'args': {}, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "dump_flows: Failed to get parameters: bridge parameter is missing" - }, - 'cmd': { - 'returncode': 0, - 'stdout': '', - 'stderr': '' - }, - }, - { # invalid bridge - 'args': {'bridge': ''}, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "dump_flows: Failed to get parameters: '' is not a valid bridge name" - }, - 'cmd': { - 'returncode': 0, - 'stdout': '', - 'stderr': '' - }, - }, - { # xenbr0 - 'args': {'bridge': 'xenbr0'}, - 'exception': None, - 'cmd': { - 'returncode': 0, - 'stdout': '"OFPST_FLOW reply (OF1.1) (xid=0x2):\n cookie=0x0, duration=10553.194s, table=0, ' + { # no args + "args": {}, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "dump_flows: Failed to get parameters: bridge parameter is missing", + }, + "cmd": {"returncode": 0, "stdout": "", "stderr": ""}, + }, + { # invalid bridge + "args": {"bridge": ""}, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "dump_flows: Failed to get parameters: '' is not a valid bridge name", + }, + "cmd": {"returncode": 0, "stdout": "", "stderr": ""}, + }, + { # xenbr0 + "args": {"bridge": "xenbr0"}, + "exception": None, + "cmd": { + "returncode": 0, + "stdout": '"OFPST_FLOW reply (OF1.1) (xid=0x2):\n cookie=0x0, duration=10553.194s, table=0, ' 'n_packets=221808, n_bytes=84062096, priority=0 actions=NORMAL\n"', - 'stderr': '' + "stderr": "", }, }, - { # non-exsting bridge - 'args': {'bridge': 'xapi42'}, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '4', - 'text': "Error dumping flows: ['ovs-ofctl', '-O', 'OpenFlow11', 'dump-flows', 'xapi42']: " - "ovs-ofctl: xapi42 is not a bridge or a socket\n" + { # non-exsting bridge + "args": {"bridge": "xapi42"}, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "3", + "text": "Error dumping flows: ['ovs-ofctl', '-O', 'OpenFlow11', 'dump-flows', 'xapi42']: " + "ovs-ofctl: xapi42 is not a bridge or a socket\n", }, - 'cmd': { - 'returncode': 1, - 'stdout': '', - 'stderr': 'ovs-ofctl: xapi42 is not a bridge or a socket\n' + "cmd": { + "returncode": 1, + "stdout": "", + "stderr": "ovs-ofctl: xapi42 is not a bridge or a socket\n", }, }, ] diff --git a/tests/sdncontroller_test_cases/parser.py b/tests/sdncontroller_test_cases/parser.py index 7a82635..a5fdd42 100644 --- a/tests/sdncontroller_test_cases/parser.py +++ b/tests/sdncontroller_test_cases/parser.py @@ -4,463 +4,369 @@ import XenAPIPlugin BRIDGE_PARAMS = [ - { - 'input': {'bridge': 'xenbr0'}, - 'result': 'xenbr0', - 'exception': None - }, - { - 'input': {'bridge': 'xapi0'}, - 'result': 'xapi0', - 'exception': None + {"input": {"bridge": "xenbr0"}, "result": "xenbr0", "exception": None}, + {"input": {"bridge": "xapi0"}, "result": "xapi0", "exception": None}, + { + "input": {"bridge": ""}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'' is not a valid bridge name", + }, }, { - 'input': {'bridge': ''}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'' is not a valid bridge name" + "input": {"bridge": "test"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'test' is not a valid bridge name", }, }, { - 'input': {'bridge': 'test'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'test' is not a valid bridge name" + "input": {"bridge": "xenbr 0"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'xenbr 0' is not a valid bridge name", }, }, { - 'input': {'bridge': 'xenbr 0'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'xenbr 0' is not a valid bridge name" + "input": {}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "bridge parameter is missing", }, }, - { - 'input': {}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "bridge parameter is missing" - } - } ] -BRIDGE_IDS = ["xenbr0", "xapi0", "empty string", "no number", "with space", "no parameters"] +BRIDGE_IDS = [ + "xenbr0", + "xapi0", + "empty string", + "no number", + "with space", + "no parameters", +] MAC_PARAMS = [ { - 'input': {'mac': '72:7a:c0:ae:1b:a5'}, - 'result': '72:7a:c0:ae:1b:a5', - 'exception': None + "input": {"mac": "72:7a:c0:ae:1b:a5"}, + "result": "72:7a:c0:ae:1b:a5", + "exception": None, }, { - 'input': {'mac': '72:7A:C0:AE:1B:A5'}, - 'result': '72:7A:C0:AE:1B:A5', - 'exception': None + "input": {"mac": "72:7A:C0:AE:1B:A5"}, + "result": "72:7A:C0:AE:1B:A5", + "exception": None, }, { - 'input': {'mac': '72:7z:C0:AE:1B:A5'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'72:7z:C0:AE:1B:A5' is not a valid MAC" + "input": {"mac": "72:7z:C0:AE:1B:A5"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'72:7z:C0:AE:1B:A5' is not a valid MAC", }, }, { - 'input': {'mac': ''}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'' is not a valid MAC" + "input": {"mac": ""}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'' is not a valid MAC", }, }, - { - 'input': {}, - 'result': None, - 'exception': None - } + {"input": {}, "result": None, "exception": None}, ] MAC_IDS = ["mac", "MAC", "non hexa mac", "empty mac", "no parameters"] IPRANGE_PARAMS = [ - { - 'input': {'iprange': '1.1.1.1'}, - 'result': '1.1.1.1', - 'exception': None - }, - { - 'input': {'iprange': '1.1.1.0/24'}, - 'result': '1.1.1.0/24', - 'exception': None + {"input": {"ipRange": "1.1.1.1"}, "result": "1.1.1.1", "exception": None}, + {"input": {"ipRange": "1.1.1.0/24"}, "result": "1.1.1.0/24", "exception": None}, + { + "input": {"ipRange": "256.256.256.256"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'256.256.256.256' is not a valid IP range", + }, }, { - 'input': {'iprange': '256.256.256.256'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'256.256.256.256' is not a valid IP range" + "input": {"ipRange": ""}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'' is not a valid IP range", }, }, { - 'input': {'iprange': ''}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'' is not a valid IP range", + "input": {}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "ipRange parameter is missing", }, }, - { - 'input': {}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "ip range parameter is missing", - } - } ] IPRANGE_IDS = ["ip addr", "ip subnet", "invalid ip", "empty ip", "no parameters"] DIRECTION_PARAMS = [ - { - 'input': {'direction': 'to'}, - 'result': (True, False), - 'exception': None - }, - { - 'input': {'direction': 'TO'}, - 'result': (True, False), - 'exception': None - }, - { - 'input': {'direction': 'from'}, - 'result': (False, True), - 'exception': None - }, - { - 'input': {'direction': 'FROM'}, - 'result': (False, True), - 'exception': None - }, - { - 'input': {'direction': 'to/from'}, - 'result': (True, True), - 'exception': None - }, - { - 'input': {'direction': 'TO/from'}, - 'result': (True, True), - 'exception': None - }, - { - 'input': {'direction': 'to/FROM'}, - 'result': (True, True), - 'exception': None - }, - { - 'input': {'direction': 'from/to'}, - 'result': (True, True), - 'exception': None + {"input": {"direction": "to"}, "result": (True, False), "exception": None}, + {"input": {"direction": "TO"}, "result": (True, False), "exception": None}, + {"input": {"direction": "from"}, "result": (False, True), "exception": None}, + {"input": {"direction": "FROM"}, "result": (False, True), "exception": None}, + {"input": {"direction": "to/from"}, "result": (True, True), "exception": None}, + {"input": {"direction": "TO/from"}, "result": (True, True), "exception": None}, + {"input": {"direction": "to/FROM"}, "result": (True, True), "exception": None}, + {"input": {"direction": "from/to"}, "result": (True, True), "exception": None}, + { + "input": {"direction": ""}, + "result": (None, None), + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'' is not a valid direction", + }, }, { - 'input': {'direction': ''}, - 'result': (None, None), - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'' is not a valid direction" - } + "input": {"direction": "aoeui"}, + "result": (None, None), + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'aoeui' is not a valid direction", + }, }, { - 'input': {'direction': 'aoeui'}, - 'result': (None, None), - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'aoeui' is not a valid direction" - } + "input": {}, + "result": (None, None), + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "direction parameter is missing", + }, }, - { - 'input': {}, - 'result': (None, None), - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "direction parameter is missing" - } - } ] -DIRECTION_IDS = ["to", "TO", "from", "FROM", "to/from", "TO/from", "to/FROM", "from/to", - "empty direction", "invalid direction", "no parameters"] +DIRECTION_IDS = [ + "to", + "TO", + "from", + "FROM", + "to/from", + "TO/from", + "to/FROM", + "from/to", + "empty direction", + "invalid direction", + "no parameters", +] PROTOCOL_PARAMS = [ - { - 'input': {'protocol': 'ip'}, - 'result': 'ip', - 'exception': None - }, - { - 'input': {'protocol': 'IP'}, - 'result': 'ip', - 'exception': None - }, - { - 'input': {'protocol': 'tcp'}, - 'result': 'tcp', - 'exception': None - }, - { - 'input': {'protocol': 'TCP'}, - 'result': 'tcp', - 'exception': None - }, - { - 'input': {'protocol': 'udp'}, - 'result': 'udp', - 'exception': None - }, - { - 'input': {'protocol': 'UDP'}, - 'result': 'udp', - 'exception': None - }, - { - 'input': {'protocol': 'arp'}, - 'result': 'arp', - 'exception': None - }, - { - 'input': {'protocol': 'ARP'}, - 'result': 'arp', - 'exception': None - }, - { - 'input': {'protocol': 'icmp'}, - 'result': 'icmp', - 'exception': None - }, - { - 'input': {'protocol': 'ICMP'}, - 'result': 'icmp', - 'exception': None + {"input": {"protocol": "ip"}, "result": "ip", "exception": None}, + {"input": {"protocol": "IP"}, "result": "ip", "exception": None}, + {"input": {"protocol": "tcp"}, "result": "tcp", "exception": None}, + {"input": {"protocol": "TCP"}, "result": "tcp", "exception": None}, + {"input": {"protocol": "udp"}, "result": "udp", "exception": None}, + {"input": {"protocol": "UDP"}, "result": "udp", "exception": None}, + {"input": {"protocol": "arp"}, "result": "arp", "exception": None}, + {"input": {"protocol": "ARP"}, "result": "arp", "exception": None}, + {"input": {"protocol": "icmp"}, "result": "icmp", "exception": None}, + {"input": {"protocol": "ICMP"}, "result": "icmp", "exception": None}, + { + "input": {"protocol": ""}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'' is not a supported protocol", + }, }, { - 'input': {'protocol': ''}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'' is not a supported protocol", - } + "input": {"protocol": "aoeui"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'aoeui' is not a supported protocol", + }, }, { - 'input': {'protocol': 'aoeui'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'aoeui' is not a supported protocol", - } + "input": {}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "protocol parameter is missing", + }, }, - { - 'input': {}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "protocol parameter is missing", - } - } ] -PROTOCOL_IDS = ["ip", "IP", "tcp", "TCP", "udp", "UDP", "arp", "ARP", "icmp", - "ICMP", "empty string", "invalid protocol", "no parameters"] +PROTOCOL_IDS = [ + "ip", + "IP", + "tcp", + "TCP", + "udp", + "UDP", + "arp", + "ARP", + "icmp", + "ICMP", + "empty string", + "invalid protocol", + "no parameters", +] PORT_PARAMS = [ - { - 'input': {'port': '4242'}, - 'result': '4242', - 'exception': None - }, - { - 'input': {'port': '0'}, - 'result': '0', - 'exception': None - }, - { - 'input': {'port': '65535'}, - 'result': '65535', - 'exception': None - }, - { - 'input': {'port': '65536'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'65536' is not a valid port", - } + {"input": {"port": "4242"}, "result": "4242", "exception": None}, + {"input": {"port": "0"}, "result": "0", "exception": None}, + {"input": {"port": "65535"}, "result": "65535", "exception": None}, + { + "input": {"port": "65536"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'65536' is not a valid port", + }, }, { - 'input': {'port': '92142'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'92142' is not a valid port", - } + "input": {"port": "92142"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'92142' is not a valid port", + }, }, { - 'input': {'port': 'foo'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'foo' is not a valid port", - } + "input": {"port": "foo"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'foo' is not a valid port", + }, }, { - 'input': {'port': ''}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'' is not a valid port", - } + "input": {"port": ""}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'' is not a valid port", + }, }, - { - 'input': {}, - 'result': None, - 'exception': None - } + {"input": {}, "result": None, "exception": None}, +] +PORT_IDS = [ + "4242", + "0", + "65535", + "65536", + "92142", + "string", + "empty string", + "no parameters", ] -PORT_IDS = ["4242", "0", "65535", "65536", "92142", "string", "empty string", "no parameters"] ALLOW_PARAMS = [ - { - 'input': {'allow': 'true'}, - 'result': True, - 'exception': None - }, - { - 'input': {'allow': 'True'}, - 'result': True, - 'exception': None - }, - { - 'input': {'allow': 'TRUE'}, - 'result': True, - 'exception': None - }, - { - 'input': {'allow': 'false'}, - 'result': False, - 'exception': None - }, - { - 'input': {'allow': 'False'}, - 'result': False, - 'exception': None - }, - { - 'input': {'allow': 'FALSE'}, - 'result': False, - 'exception': None + {"input": {"allow": "true"}, "result": True, "exception": None}, + {"input": {"allow": "True"}, "result": True, "exception": None}, + {"input": {"allow": "TRUE"}, "result": True, "exception": None}, + {"input": {"allow": "false"}, "result": False, "exception": None}, + {"input": {"allow": "False"}, "result": False, "exception": None}, + {"input": {"allow": "FALSE"}, "result": False, "exception": None}, + { + "input": {"allow": "bar"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "allow parameter should be true or false, not 'bar'", + }, }, { - 'input': {'allow': 'bar'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "allow parameter should be true or false, not 'bar'", - } + "input": {"allow": ""}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "allow parameter should be true or false, not ''", + }, }, { - 'input': {'allow': ''}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "allow parameter should be true or false, not ''", - } + "input": {}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "allow parameter is missing", + }, }, - { - 'input': {}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "allow parameter is missing", - } - } ] -ALLOW_IDS = ["true", "True", "TRUE", "false", "False", "FALSE", "bar", "empty string", "no parameters"] +ALLOW_IDS = [ + "true", + "True", + "TRUE", + "false", + "False", + "FALSE", + "bar", + "empty string", + "no parameters", +] PRIORITY_PARAMS = [ - { - 'input': {'priority': '100'}, - 'result': '100', - 'exception': None - }, - { - 'input': {'priority': '0'}, - 'result': '0', - 'exception': None - }, - { - 'input': {'priority': '65535'}, - 'result': '65535', - 'exception': None + {"input": {"priority": "100"}, "result": "100", "exception": None}, + {"input": {"priority": "0"}, "result": "0", "exception": None}, + {"input": {"priority": "65535"}, "result": "65535", "exception": None}, + { + "input": {"priority": "65536"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'65536' is not a valid priority", + }, }, { - 'input': {'priority': '65536'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'65536' is not a valid priority", - } + "input": {"priority": "aoeui"}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'aoeui' is not a valid priority", + }, }, { - 'input': {'priority': 'aoeui'}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'aoeui' is not a valid priority", - } + "input": {"priority": ""}, + "result": None, + "exception": { + "type": XenAPIPlugin.Failure, + "code": "1", + "text": "'' is not a valid priority", + }, }, { - 'input': {'priority': ''}, - 'result': None, - 'exception': { - 'type': XenAPIPlugin.Failure, - 'code': '1', - 'text': "'' is not a valid priority", - } + "input": {}, + "result": None, + "exception": None, }, - { - 'input': {}, - 'result': None, - 'exception': None, - } ] PRIORITY_IDS = ["100", "0", "65535", "65536", "aoeui", "empty string", "no parameters"] diff --git a/tests/test_sdn-controller.py b/tests/test_sdn-controller.py index 53210aa..46a35d5 100644 --- a/tests/test_sdn-controller.py +++ b/tests/test_sdn-controller.py @@ -2,40 +2,50 @@ import pytest import XenAPIPlugin -from sdncontroller import ( - Parser, - add_rule, - del_rule, - dump_flows -) +from sdncontroller import Parser, update_args_from_ovs, add_rule, del_rule, dump_flows from sdncontroller_test_cases.parser import ( - BRIDGE_PARAMS, BRIDGE_IDS, - MAC_PARAMS, MAC_IDS, - IPRANGE_PARAMS, IPRANGE_IDS, - DIRECTION_PARAMS, DIRECTION_IDS, - PROTOCOL_PARAMS, PROTOCOL_IDS, - PORT_PARAMS, PORT_IDS, - ALLOW_PARAMS, ALLOW_IDS, - PRIORITY_PARAMS, PRIORITY_IDS + BRIDGE_PARAMS, + BRIDGE_IDS, + MAC_PARAMS, + MAC_IDS, + IPRANGE_PARAMS, + IPRANGE_IDS, + DIRECTION_PARAMS, + DIRECTION_IDS, + PROTOCOL_PARAMS, + PROTOCOL_IDS, + PORT_PARAMS, + PORT_IDS, + ALLOW_PARAMS, + ALLOW_IDS, + PRIORITY_PARAMS, + PRIORITY_IDS, ) from sdncontroller_test_cases.functions import ( - ADD_RULE_PARAMS, ADD_RULE_IDS, - DEL_RULE_PARAMS, DEL_RULE_IDS, - DUMP_FLOWS_PARAMS, DUMP_FLOWS_IDS + UPDATE_ARGS_PARAMS, + UPDATE_ARGS_IDS, + ADD_RULE_PARAMS, + ADD_RULE_IDS, + DEL_RULE_PARAMS, + DEL_RULE_IDS, + DUMP_FLOWS_PARAMS, + DUMP_FLOWS_IDS, ) + def parser_test(method, params): - exc = params['exception'] + exc = params["exception"] if exc: - with pytest.raises(exc['type']) as e: + with pytest.raises(exc["type"]) as e: ret = method() - assert e.value.params[0] == exc['code'] - assert e.value.params[1] == exc['text'] + assert e.value.params[0] == exc["code"] + assert e.value.params[1] == exc["text"] else: ret = method() - assert ret == params['result'] + assert ret == params["result"] + class TestSdnControllerParser: @pytest.fixture(params=BRIDGE_PARAMS, ids=BRIDGE_IDS) @@ -43,7 +53,7 @@ def bridge(self, request): return request.param def test_parse_bridge(self, bridge): - p = Parser(bridge['input']) + p = Parser(bridge["input"]) parser_test(p.parse_bridge, bridge) @pytest.fixture(params=MAC_PARAMS, ids=MAC_IDS) @@ -51,7 +61,7 @@ def mac(self, request): return request.param def test_parse_mac(self, mac): - p = Parser(mac['input']) + p = Parser(mac["input"]) parser_test(p.parse_mac, mac) @pytest.fixture(params=IPRANGE_PARAMS, ids=IPRANGE_IDS) @@ -59,7 +69,7 @@ def iprange(self, request): return request.param def test_parse_iprange(self, iprange): - p = Parser(iprange['input']) + p = Parser(iprange["input"]) parser_test(p.parse_iprange, iprange) @pytest.fixture(params=DIRECTION_PARAMS, ids=DIRECTION_IDS) @@ -67,7 +77,7 @@ def direction(self, request): return request.param def test_parse_direction(self, direction): - p = Parser(direction['input']) + p = Parser(direction["input"]) parser_test(p.parse_direction, direction) @pytest.fixture(params=PROTOCOL_PARAMS, ids=PROTOCOL_IDS) @@ -75,7 +85,7 @@ def protocol(self, request): return request.param def test_parse_protocol(self, protocol): - p = Parser(protocol['input']) + p = Parser(protocol["input"]) parser_test(p.parse_protocol, protocol) @pytest.fixture(params=PORT_PARAMS, ids=PORT_IDS) @@ -83,7 +93,7 @@ def port(self, request): return request.param def test_parse_port(self, port): - p = Parser(port['input']) + p = Parser(port["input"]) parser_test(p.parse_port, port) @pytest.fixture(params=ALLOW_PARAMS, ids=ALLOW_IDS) @@ -91,7 +101,7 @@ def allow(self, request): return request.param def test_parse_allow(self, allow): - p = Parser(allow['input']) + p = Parser(allow["input"]) parser_test(p.parse_allow, allow) @pytest.fixture(params=PRIORITY_PARAMS, ids=PRIORITY_IDS) @@ -99,52 +109,76 @@ def priority(self, request): return request.param def test_parse_priority(self, priority): - p = Parser(priority['input']) + p = Parser(priority["input"]) parser_test(p.parse_priority, priority) + @mock.patch("sdncontroller.run_command", autospec=True) class TestSdnControllerFunctions: + @pytest.fixture(params=UPDATE_ARGS_PARAMS, ids=UPDATE_ARGS_IDS) + def args_update(self, request): + return request.param + + def test_update_args_from_ovs(self, run_command, args_update): + run_command.side_effect = args_update["cmd"] + exc = args_update["exception"] + if exc: + with pytest.raises(exc["type"]) as e: + update_args_from_ovs(args_update["args"]) + assert e.value.params[0] == exc["code"] + assert e.value.params[1] == exc["text"] + else: + update_args_from_ovs(args_update["args"]) + assert args_update["args"]["ofports"] == args_update["ofports"] + assert args_update["args"]["uplinks"] == args_update["uplinks"] + @pytest.fixture(params=ADD_RULE_PARAMS, ids=ADD_RULE_IDS) def rule_to_add(self, request): return request.param def test_add_rule(self, run_command, rule_to_add): - run_command.return_value = rule_to_add['cmd'] - exc = rule_to_add['exception'] + run_command.side_effect = rule_to_add["cmd"] + exc = rule_to_add["exception"] if exc: - with pytest.raises(exc['type']) as e: - add_rule(None, rule_to_add['args']) - assert e.value.params[0] == exc['code'] - assert e.value.params[1] == exc['text'] + with pytest.raises(exc["type"]) as e: + add_rule(None, rule_to_add["args"]) + assert e.value.params[0] == exc["code"] + assert e.value.params[1] == exc["text"] else: - add_rule(None, rule_to_add['args']) + with mock.patch("sdncontroller.run_ofctl_cmd") as mock_func: + add_rule(None, rule_to_add["args"]) + assert mock_func.call_count == len(rule_to_add["calls"]) + mock_func.assert_has_calls(rule_to_add["calls"]) @pytest.fixture(params=DEL_RULE_PARAMS, ids=DEL_RULE_IDS) def rule_to_del(self, request): return request.param def test_del_rule(self, run_command, rule_to_del): - run_command.return_value = rule_to_del['cmd'] - exc = rule_to_del['exception'] + run_command.side_effect = rule_to_del["cmd"] + exc = rule_to_del["exception"] if exc: - with pytest.raises(exc['type']) as e: - del_rule(None, rule_to_del['args']) - assert e.value.params[0] == exc['code'] - assert e.value.params[1] == exc['text'] + with pytest.raises(exc["type"]) as e: + del_rule(None, rule_to_del["args"]) + assert e.value.params[0] == exc["code"] + assert e.value.params[1] == exc["text"] else: - del_rule(None, rule_to_del['args']) + with mock.patch("sdncontroller.run_ofctl_cmd") as mock_func: + del_rule(None, rule_to_del["args"]) + assert mock_func.call_count == len(rule_to_del["calls"]) + mock_func.assert_has_calls(rule_to_del["calls"]) @pytest.fixture(params=DUMP_FLOWS_PARAMS, ids=DUMP_FLOWS_IDS) def bridge_to_dump(self, request): return request.param def test_dump_flow(self, run_command, bridge_to_dump): - run_command.return_value = bridge_to_dump['cmd'] - exc = bridge_to_dump['exception'] + run_command.return_value = bridge_to_dump["cmd"] + exc = bridge_to_dump["exception"] if exc: - with pytest.raises(exc['type']) as e: - dump_flows(None, bridge_to_dump['args']) - assert e.value.params[0] == exc['code'] - assert e.value.params[1] == exc['text'] + with pytest.raises(exc["type"]) as e: + dump_flows(None, bridge_to_dump["args"]) + assert e.value.params[0] == exc["code"] + assert e.value.params[1] == exc["text"] else: - dump_flows(None, bridge_to_dump['args']) + dump_flows(None, bridge_to_dump["args"])