-#!/usr/bin/python2.7
-
-import os
-import sys
-import time
-import logging
-import argparse
-import unittest
-import requests
-import xml.dom.minidom as md
-from xml.etree import ElementTree as ET
-from netaddr import IPNetwork
-from string import lower
-
-import mininet.node
-import mininet.topo
-import mininet.net
-import mininet.util
-
-from mininet.node import RemoteController
-from mininet.node import OVSKernelSwitch
-
-
-def create_network(controller_ip, controller_port):
- """Create topology and mininet network."""
- topo = mininet.topo.Topo()
-
- topo.addSwitch('s1')
- topo.addHost('h1')
- topo.addHost('h2')
-
- topo.addLink('h1', 's1')
- topo.addLink('h2', 's1')
-
- switch = mininet.util.customConstructor(
- {'ovsk': OVSKernelSwitch}, 'ovsk,protocols=OpenFlow13')
-
- controller = mininet.util.customConstructor(
- {'remote': RemoteController}, 'remote,ip=%s:%s' % (controller_ip,
- controller_port))
-
- net = mininet.net.Mininet(topo=topo, switch=switch, controller=controller)
-
- return net
-
-
-def get_flows(net):
- """Get list of flows from network's first switch.
-
- Return list of all flows on switch, sorted by duration (newest first)
- One flow is a dictionary with all flow's attribute:value pairs. Matches
- are stored under 'matches' key as another dictionary.
- Example:
-
- {
- 'actions': 'drop',
- 'cookie': '0xa,',
- 'duration': '3.434s,',
- 'hard_timeout': '12,',
- 'idle_timeout': '34,',
- 'matches': {
- 'ip': None,
- 'nw_dst': '10.0.0.0/24'
- },
- 'n_bytes': '0,',
- 'n_packets': '0,',
- 'priority': '2',
- 'table': '1,'
- }
-
- """
- log = logging.getLogger(__name__)
-
- def parse_matches(flow, matches):
- flow['matches'] = {}
-
- for match in matches:
- split_match = match.split('=', 1)
- if len(split_match) == 1:
- flow['matches'][split_match[0]] = None
- else:
- flow['matches'][split_match[0]] = split_match[1].rstrip(',')
-
- switch = net.switches[0]
- output = switch.cmdPrint(
- 'ovs-ofctl -O OpenFlow13 dump-flows %s' % switch.name)
-# output = switch.cmdPrint(
-# 'ovs-ofctl -F openflow10 dump-flows %s' % switch.name)
-
- log.debug('switch flow table: {}'.format(output))
-
- flows = []
-
- for line in output.splitlines()[1:]:
- flow = {}
- for word in line.split():
- word.rstrip(',')
- try:
- key, value = word.split('=', 1)
- except ValueError:
- # TODO: need to figure out what to do here?
- continue
-
- if key == 'priority':
- values = value.split(',')
- flow[key] = values[0]
- parse_matches(flow, values[1:])
- else:
- flow[key] = value.rstrip(',')
-
- flows.append(flow)
-
- # sort by duration
- return sorted(flows, key=lambda x: x['duration'].rstrip('s'))
-
-
-def translate_to_flow(flow, name, dictionary):
- switch_flow_name = dictionary[name]
-
- key_err = '{} needs to be present in flow definition. Flow definition ' \
- 'was: {}.'.format(switch_flow_name, flow)
- assert switch_flow_name in flow, key_err
- return switch_flow_name
-
-
-def get_text_value(element):
- return element.childNodes[0].nodeValue
-
-
-def compare_elements(expected_match, actual_match, kw, comparators, default):
- for child in expected_match.childNodes:
- if child.nodeType is expected_match.TEXT_NODE:
- continue
-
- comparator = comparators.get(child.nodeName, default)
- comparator(child, actual_match, kw)
-
-
-def fallback_comparator(xml_element, switch_flow, kw):
- # print 'fallback_comparator-xml_element', xml_element.toxml()
- # print 'fallback_comparator: switch_flow', switch_flow
- # print 'fallback_comparator: kw', kws
-
- name = translate_to_flow(switch_flow, xml_element.nodeName, kw)
-
- actual = switch_flow[name]
- expected = xml_element.childNodes[0].nodeValue
-
- data = xml_element.toxml(), name, actual
- # print 'fallback_comparator: data', data
-
- assert expected == actual, 'xml part: %s && switch %s=%s' % data
-
-
-def default_comparator(xml_element, switch_flow):
- fallback_comparator(xml_element, switch_flow, keywords)
-
-
-def integer_comparator(expected, actual, kw, base):
- expected_value = int(expected.childNodes[0].data)
-
- name = kw.get(expected.nodeName)
- actual_value = int(actual[name], base)
-
- data = expected.toxml(), name, actual
- assert expected_value == actual_value, \
- 'xml value: %s && actual value %s=%s' % data
-
-
-def cookie_comparator(cookie, switch_flow):
- integer_comparator(cookie, switch_flow, keywords, 16)
-
-
-def ethernet_address_comparator(child, actual_match, kw):
- expected_address = child.getElementsByTagName("address")[0].childNodes[0].data
- actual_address = actual_match[kw.get(child.nodeName)]
-
- data = child.toxml(), kw.get(child.nodeName), actual_address
-
- assert lower(expected_address) == lower(actual_address), \
- 'xml address: %s && actual address %s=%s' % data
-
-
-def vlan_match_comparator(expected_match, actual_match, kw):
-
- def compare_vlan_pcp(expected_match, actual_match, kw):
- integer_comparator(expected_match, actual_match, kw, 10)
-
- def compare_vlan_id(expected_match, actual_match, kw):
- integer_comparator(expected_match.getElementsByTagName('vlan-id')[0],
- actual_match, kw, 10)
-
- VLAN_COMPARATORS = {
- 'vlan-pcp': compare_vlan_pcp,
- 'vlan-id': compare_vlan_id,
- }
-
- # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
- # print 'ethernet_match_comparator-actual_match:', actual_match
-
- compare_elements(expected_match, actual_match, kw,
- VLAN_COMPARATORS, fallback_comparator)
-
-
-def ethernet_match_comparator(expected_match, actual_match, kw):
- def compare_etype(child, actual_match, kw):
- expected_etype = int(child.getElementsByTagName("type")[0].childNodes[0].data)
- name = kw.get(child.nodeName)
- data = child.toxml(), name, actual_match
-
- if expected_etype == 2048: # IP
- assert ((actual_match.get('ip', 'IP Not-present') is None) or
- (actual_match.get('tcp', 'TCP Not-present') is None) or
- (actual_match.get('sctp', 'SCTP Not-present') is None) or
- (actual_match.get('udp', 'UDP Not-present') is None)), 'Expected etype %s && actual etype %s=%s' % data # noqa
-
- elif expected_etype == 2054: # ARP
- assert actual_match.get('arp', 'ARP Not-present') is None, \
- 'Expected etype %s && actual etype %s=%s' % data
-
- else:
- actual_etype = int(actual_match[name], 16)
-
- assert expected_etype == actual_etype, 'xml etype: %s && actual etype %s=%s' % data
-
- ETH_COMPARATORS = {
- 'ethernet-type': compare_etype,
- 'ethernet-source': ethernet_address_comparator,
- 'ethernet-destination': ethernet_address_comparator,
- }
-
- # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
- # print 'ethernet_match_comparator-actual_match:', actual_match
-
- compare_elements(expected_match, actual_match, kw,
- ETH_COMPARATORS, fallback_comparator)
-
-
-def ipv4_comparator(expected_match, actual_match, kw):
- # print 'ip_v4_comparator:', expected_match.toxml(), actual_match
- # print 'ip_v4_comparator-actual_match:', actual_match
-
- expected_value = expected_match.childNodes[0].data
- actual_value = actual_match[kw.get(expected_match.nodeName)]
-
- data = expected_match.toxml(), kw.get(expected_match.nodeName), actual_value
-
- assert IPNetwork(expected_value) == IPNetwork(actual_value),\
- 'xml part: %s && address %s=%s' % data
-
-
-def ip_match_comparator(expected_match, actual_match, kw):
- def compare_proto(child, actual_match, kw):
- print 'compare_proto:', child.toxml(), actual_match
- expected_proto = int(child.childNodes[0].data)
-
- name = child.nodeName
- data = expected_match.toxml(), name, actual_match
-
- if expected_proto == 6: # TCP
- assert actual_match.get('tcp', 'TCP Not-present') is None, 'ip protocol type: expected %s, actual %s=%s' % data # noqa
-
- elif expected_proto == 17: # UDP
- assert actual_match.get('udp', 'UDP Not-present') is None, 'ip protocol type: expected %s, actual %s=%s' % data # noqa
-
- elif expected_proto == 132: # SCTP
- assert actual_match.get('sctp', 'SCTP Not-present') is None, 'ip protocol type: expected %s, actual %s=%s' % data # noqa
-
- else:
- fallback_comparator(child, actual_match, kw)
-
- def compare_dscp(child, actual_match, kw):
- # print 'compare_dscp:', child.toxml(), actual_match
-
- expected_dscp = int(child.childNodes[0].data)
- name = kw.get(child.nodeName)
- actual_dscp = int(actual_match[name])
-
- data = child.toxml(), name, actual_match
-
- assert (expected_dscp * 4) == actual_dscp, 'dscp: expected %s, actual %s=%s' % data
-
- IP_MATCH_COMPARATORS = {
- 'ip-protocol': compare_proto,
- 'ip-dscp': compare_dscp,
- }
-
- # print 'ip_match_comparator:', expected_match.toxml(), actual_match
- compare_elements(expected_match, actual_match, kw,
- IP_MATCH_COMPARATORS, fallback_comparator)
-
-
-def match_comparator(expected_match, switch_flow):
- MATCH_COMPARATORS = {
- 'arp-source-hardware-address': ethernet_address_comparator,
- 'arp-target-hardware-address': ethernet_address_comparator,
- 'vlan-match': vlan_match_comparator,
- 'ethernet-match': ethernet_match_comparator,
- 'ip-match': ip_match_comparator,
- 'ipv4-destination': ipv4_comparator,
- 'ipv4-source': ipv4_comparator,
- }
-
- actual_match = switch_flow['matches']
-
- # print 'match_comparator-expected_match:', expected_match.toxml()
- # print 'match_comparator-actual_match:', actual_match
- # print 'match_comparator: keywords', keywords
-
- compare_elements(expected_match, actual_match, match_keywords,
- MATCH_COMPARATORS, fallback_comparator)
-
-
-def actions_comparator(actions, switch_flow):
- # print 'actions_comparator:', actions, switch_flow
-
- actual_actions = switch_flow['actions'].split(",")
- # print 'actions_comparator:', actual_actions
-
- for action in actions.childNodes:
- if action.nodeType is actions.TEXT_NODE:
- continue
-
- action_name = action.childNodes[3].nodeName
- expected_action = action_keywords.get(action_name)
-
- data = action.toxml(), expected_action
- # print 'actions_comparator:', data
-
- assert expected_action in actual_actions, 'xml part:\n%s\n expected action: %s' % data
-
-
-def null_comparator(element, switch_flow):
- pass
-
-
-def instructions_comparator(instructions_element, switch_flow):
- INSTRUCTION_COMPARATORS = {
- 'apply-actions': actions_comparator,
- 'default': null_comparator,
- }
- # print 'instructions_comparator:', instructions_element, switch_flow
-
- instructions = instructions_element.childNodes # noqa
-
- for instruction in instructions_element.childNodes:
- if instruction.nodeType is instructions_element.TEXT_NODE:
- continue
-
- for itype in instruction.childNodes:
- if itype.nodeType is itype.TEXT_NODE:
- continue
-
- comparator = INSTRUCTION_COMPARATORS.get(itype.nodeName,
- INSTRUCTION_COMPARATORS['default'])
- comparator(itype, switch_flow)
-
-
-COMPARATORS = {
- 'cookie': cookie_comparator,
- 'instructions': instructions_comparator,
- 'match': match_comparator,
- 'default': default_comparator,
-}
-
-
-def all_nodes(xml_root):
- """
- Generates every non-text nodes.
- """
- current_nodes = [xml_root]
- next_nodes = []
-
- while len(current_nodes) > 0:
- for node in current_nodes:
- if node.nodeType != xml_root.TEXT_NODE:
- yield node
- next_nodes.extend(node.childNodes)
-
- current_nodes, next_nodes = next_nodes, []
-
-
-def check_elements(xmlstr, keywords):
- # namespace = 'urn:opendaylight:flow:inventory'
- tree = md.parseString(xmlstr)
-
- for element in all_nodes(tree.documentElement):
- # switch flow object contains only some data from xml
- if element.nodeName not in keywords:
- # print 'check_elements: element.nodeName', element.nodeName, 'NOT in keywords'
- continue
-
- yield element
-
- raise StopIteration()
-
-
-class TestOpenFlowXMLs(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
- cls.net = create_network(cls.host, cls.mn_port)
- cls.net.start()
- time.sleep(15)
-
- @classmethod
- def tearDownClass(cls):
- cls.net.stop()
-
-
-def get_values(node, *tags):
- result = dict((tag, None) for tag in tags)
- for node in all_nodes(node):
- if node.nodeName in result and len(node.childNodes) > 0:
- result[node.nodeName] = node.childNodes[0].nodeValue
- return result
-
-
-def generate_tests_from_xmls(path, xmls=None):
- # generate test function from path to request xml
- def generate_test(path_to_xml):
- xml_string = ''
- with open(path_to_xml) as f:
- xml_string = f.read()
-
- tree = md.parseString(xml_string)
- ids = get_values(tree.documentElement, 'table_id', 'id')
-
- def new_test(self):
- log = logging.getLogger(__name__)
- # send request throught RESTCONF
- data = (self.host, self.port, ids['table_id'], ids['id'])
- url = 'http://%s:%d/restconf/config/opendaylight-inventory:nodes' \
- '/node/openflow:1/table/%s/flow/%s' % data
- headers = {
- 'Content-Type': 'application/xml',
- 'Accept': 'application/xml',
- }
- log.info('sending request to url: {}'.format(url))
- rsp = requests.put(url, auth=('admin', 'admin'), data=xml_string,
- headers=headers)
- log.info('received status code: {}'.format(rsp.status_code))
- log.debug('received content: {}'.format(rsp.text))
- assert rsp.status_code == 204 or rsp.status_code == 200, 'Status' \
- ' code returned %d' % rsp.status_code
-
- # check request content against restconf's datastore
- response = requests.get(url, auth=('admin', 'admin'),
- headers={'Accept': 'application/xml'})
- assert response.status_code == 200
- req = ET.tostring(ET.fromstring(xml_string))
- res = ET.tostring(ET.fromstring(response.text))
- assert req == res, 'uploaded and stored xml, are not the same\n' \
- 'uploaded: %s\nstored:%s' % (req, res)
-
- # collect flow table state on switch
- switch_flows = get_flows(self.net)
- assert len(switch_flows) > 0
-
- # compare requested object and flow table state
- for important_element in check_elements(xml_string, keywords):
- # log.info('important element: {}'.format(important_element.nodeName))
- comparator = COMPARATORS.get(important_element.nodeName,
- COMPARATORS['default'])
-
- comparator(important_element, switch_flows[0])
-
- return new_test
-
- # generate list of available xml requests
- xmlfiles = None
- if xmls is not None:
- xmlfiles = ('f%d.xml' % fid for fid in xmls)
- else:
- xmlfiles = (xml for xml in os.listdir(path) if xml.endswith('.xml'))
-
- # define key getter for sorting
- def get_test_number(test_name):
- return int(test_name[1:-4])
-
- for xmlfile in xmlfiles:
- test_name = 'test_xml_%04d' % get_test_number(xmlfile)
- setattr(TestOpenFlowXMLs,
- test_name,
- generate_test(os.path.join(path, xmlfile)))
-
-
-if __name__ == '__main__':
- # set up logging
- logging.basicConfig(level=logging.DEBUG)
-
- # parse cmdline arguments
- parser = argparse.ArgumentParser(description='Run switch <-> ODL tests '
- 'defined by xmls.')
- parser.add_argument('--odlhost', default='127.0.0.1', help='host where '
- 'odl controller is running')
- parser.add_argument('--odlport', type=int, default=8080, help='port on '
- 'which odl\'s RESTCONF is listening')
- parser.add_argument('--mnport', type=int, default=6653, help='port on '
- 'which odl\'s controller is listening')
- parser.add_argument('--xmls', default=None, help='generete tests only '
- 'from some xmls (i.e. 1,3,34) ')
- args = parser.parse_args()
-
- # set host and port of ODL controller for test cases
- TestOpenFlowXMLs.port = args.odlport
- TestOpenFlowXMLs.host = args.odlhost
- TestOpenFlowXMLs.mn_port = args.mnport
-
- keywords = None
- with open('keywords.csv') as f:
- keywords = dict(line.strip().split(';') for line in f
- if not line.startswith('#'))
-
- match_keywords = None
- with open('match-keywords.csv') as f:
- match_keywords = dict(line.strip().split(';') for line in f
- if not line.startswith('#'))
-
- action_keywords = None
- with open('action-keywords.csv') as f:
- action_keywords = dict(line.strip().split(';') for line in f
- if not line.startswith('#'))
-
- # fix arguments for unittest
- del sys.argv[1:]
-
- # generate tests for TestOpenFlowXMLs
- if args.xmls is not None:
- xmls = map(int, args.xmls.split(','))
- generate_tests_from_xmls('xmls', xmls)
- else:
- generate_tests_from_xmls('xmls')
-
- # run all tests
- unittest.main()