diff options
Diffstat (limited to 'resources')
-rw-r--r-- | resources/libraries/python/honeycomb/BGP.py | 107 | ||||
-rw-r--r-- | resources/libraries/python/honeycomb/HoneycombSetup.py | 32 | ||||
-rw-r--r-- | resources/libraries/robot/honeycomb/bgp.robot | 86 | ||||
-rw-r--r-- | resources/test_data/honeycomb/bgp.py | 175 | ||||
-rwxr-xr-x | resources/traffic_scripts/honeycomb/bgp_open.py | 85 |
5 files changed, 466 insertions, 19 deletions
diff --git a/resources/libraries/python/honeycomb/BGP.py b/resources/libraries/python/honeycomb/BGP.py index ca50cc3120..75dce762ff 100644 --- a/resources/libraries/python/honeycomb/BGP.py +++ b/resources/libraries/python/honeycomb/BGP.py @@ -13,6 +13,7 @@ """Keywords to manipulate BGP configuration using Honeycomb REST API.""" +from resources.libraries.python.constants import Constants as Const from resources.libraries.python.HTTPRequest import HTTPCodes from resources.libraries.python.honeycomb.HoneycombSetup import HoneycombError from resources.libraries.python.honeycomb.HoneycombUtil \ @@ -27,6 +28,7 @@ class BGPKeywords(object): """ def __init__(self): + """Initializer.""" pass @staticmethod @@ -41,7 +43,7 @@ class BGPKeywords(object): :type data: dict :returns: Content of response. :rtype: bytearray - :raises HoneycombError: If the status code in response on PUT is not + :raises HoneycombError: If the status code in response to PUT is not 200 = OK or 201 = ACCEPTED. """ @@ -69,7 +71,7 @@ class BGPKeywords(object): :type data: dict :returns: Content of response. :rtype: bytearray - :raises HoneycombError: If the status code in response on PUT is not + :raises HoneycombError: If the status code in response to PUT is not 200 = OK or 201 = ACCEPTED. """ @@ -93,6 +95,7 @@ class BGPKeywords(object): :type node: dict :returns: BGP configuration data. :rtype: dict + :raises HoneycombError: If the status code in response is not 200 = OK. """ status_code, resp = HcUtil. \ @@ -104,21 +107,29 @@ class BGPKeywords(object): return resp @staticmethod - def get_bgp_peer(node, address): + def get_bgp_peer(node, address, datastore='config'): """Get BGP configuration of the specified peer from the node. :param node: Honeycomb node. :param address: IP address of the peer. + :param datastore: Get data from config or operational datastore. :type node: dict :type address: str + :type datastore; str :returns: BGP peer configuration data. :rtype: dict + :raises HoneycombError: If the status code in response is not 200 = OK. """ path = "bgp-openconfig-extensions:neighbors/" \ "neighbor/{0}".format(address) + if datastore != "operational": + url = "config_bgp_peer" + else: + url = "oper_bgp" + path = "peer/bgp:%2F%2F{0}".format(address) status_code, resp = HcUtil. \ - get_honeycomb_data(node, "config_bgp_peer", path) + get_honeycomb_data(node, url, path) if status_code != HTTPCodes.OK: raise HoneycombError( "Not possible to get configuration information about the BGP" @@ -211,7 +222,7 @@ class BGPKeywords(object): :type ip_version: str :returns: Content of response. :rtype: bytearray - :raises HoneycombError: If the operation fails. + :raises HoneycombError: If the status code in response is not 200 = OK. """ route_address = route_address.replace("/", "%2F") @@ -239,7 +250,7 @@ class BGPKeywords(object): def get_all_peer_routes(node, peer_address, ip_version): """Get all configured routes for the given BGP peer. - :param node: HOneycomb node. + :param node: Honeycomb node. :param peer_address: IP address of the peer. :param ip_version: IP protocol version. ipv4 or ipv6 :type node: dict @@ -247,7 +258,7 @@ class BGPKeywords(object): :type ip_version: str :returns: Content of response. :rtype: bytearray - :raises HoneycombError: If the operation fails. + :raises HoneycombError: If the status code in response is not 200 = OK. """ if ip_version.lower() == "ipv4": @@ -299,3 +310,85 @@ class BGPKeywords(object): .format(peer_address, route_address, index) return BGPKeywords._configure_bgp_route(node, path) + + @staticmethod + def get_bgp_local_rib(node): + """Get local RIB table from the Honeycomb node. + + :param node: Honeycomb node. + :type node: dict + :returns: RIB operational data. + :rtype: dict + :raises HoneycombError: If the status code in response is not 200 = OK. + """ + + path = "loc-rib" + + status_code, resp = HcUtil. \ + get_honeycomb_data(node, "oper_bgp", path) + + if status_code != HTTPCodes.OK: + raise HoneycombError( + "Not possible to get operational data from BGP local RIB." + " Status code: {0}.".format(status_code)) + + return resp + + @staticmethod + def configure_bgp_base(node, ip_address, port, as_number): + """Modify BGP config file. Requires a restart of Honeycomb to take + effect. + + :param node: Honeycomb node. + :param ip_address: BGP peer identifier/binding address. + :param port: BGP binding port. + :param as_number: Autonomous System ID number. + :type node: dict + :type ip_address: str + :type port: int + :type as_number: int + :raises HoneycombError: If modifying the configuration fails. + """ + + from resources.libraries.python.ssh import SSH + + config = { + '\\"bgp-binding-address\\"': '\\"{0}\\"'.format(ip_address), + '\\"bgp-port\\"': port, + '\\"bgp-as-number\\"': as_number} + + path = "{0}/config/bgp.json".format(Const.REMOTE_HC_DIR) + + for key, value in config.items(): + find = key + replace = '"{0}": "{1}",'.format(key, value) + + argument = '"/{0}/c\\ {1}"'.format(find, replace) + command = "sed -i {0} {1}".format(argument, path) + + ssh = SSH() + ssh.connect(node) + (ret_code, _, stderr) = ssh.exec_command_sudo(command) + if ret_code != 0: + raise HoneycombError("Failed to modify configuration on " + "node {0}, {1}".format(node, stderr)) + + @staticmethod + def compare_rib_tables(data, ref): + """Compare provided RIB table with reference. All reference entries must + be present in data. Data entries not present in reference are ignored. + + :param data: Data from Honeycomb node. + :param ref: Reference data to compare against. + :type data: dict + :type ref: dict + :raises HoneycombError: If the tables do not match.""" + + # Remove runtime attributes from data + for item in data: + item.pop("attributes", "") + + for item in ref: + if item not in data: + raise HoneycombError( + "RIB entry {0} not found in operational data.") diff --git a/resources/libraries/python/honeycomb/HoneycombSetup.py b/resources/libraries/python/honeycomb/HoneycombSetup.py index 7c3831ca04..d4175b13e4 100644 --- a/resources/libraries/python/honeycomb/HoneycombSetup.py +++ b/resources/libraries/python/honeycomb/HoneycombSetup.py @@ -61,12 +61,13 @@ class HoneycombSetup(object): HoneycombSetup.print_environment(nodes) - logger.console("\n(re)Starting Honeycomb service ...") - cmd = "sudo service honeycomb start" for node in nodes: if node['type'] == NodeType.DUT: + logger.console( + "\n(re)Starting Honeycomb service on node {0}".format( + node["host"])) ssh = SSH() ssh.connect(node) (ret_code, _, _) = ssh.exec_command_sudo(cmd) @@ -89,13 +90,15 @@ class HoneycombSetup(object): :type nodes: list :raises HoneycombError: If Honeycomb failed to stop. """ - logger.console("\nShutting down Honeycomb service ...") cmd = "sudo service honeycomb stop" errors = [] for node in nodes: if node['type'] == NodeType.DUT: + logger.console( + "\nShutting down Honeycomb service on node {0}".format( + node["host"])) ssh = SSH() ssh.connect(node) (ret_code, _, _) = ssh.exec_command_sudo(cmd) @@ -121,7 +124,8 @@ class HoneycombSetup(object): :raises HoneycombError: If Honeycomb fails to start. """ - logger.console("\n(re)Starting Honeycomb service ...") + logger.console( + "\n(re)Starting Honeycomb service on node {0}".format(node["host"])) cmd = "sudo service honeycomb restart" @@ -650,6 +654,26 @@ class HoneycombSetup(object): logger.info("ODL client service stopped.") + @staticmethod + def set_static_arp(node, ip_address, mac_address): + """Configure a static ARP entry using arp. + + :param node: Node in topology. + :param ip_address: IP address for the entry. + :param mac_address: MAC adddress for the entry. + :type node: dict + :type ip_address: str + :type mac_address: str + :raises RuntimeError: If the operation fails. + """ + + ssh = SSH() + ssh.connect(node) + ret_code, _, _ = ssh.exec_command_sudo("arp -s {0} {1}".format( + ip_address, mac_address)) + + if ret_code != 0: + raise RuntimeError("Failed to configure static ARP adddress.") class HoneycombStartupConfig(object): diff --git a/resources/libraries/robot/honeycomb/bgp.robot b/resources/libraries/robot/honeycomb/bgp.robot index d4c2d6b0a2..aa96cc6538 100644 --- a/resources/libraries/robot/honeycomb/bgp.robot +++ b/resources/libraries/robot/honeycomb/bgp.robot @@ -16,6 +16,24 @@ | Library | resources.libraries.python.honeycomb.HcAPIKwInterfaces.InterfaceKeywords *** Keywords *** +| Configure BGP module +| | [Documentation] | Edit Honeycomb's configuration file for the BGP feature.\ +| | ... | Honeycomb needs to be restarted for the changes to take effect. +| | ... +| | ... | *Arguments:* +| | ... | - node - information about a DUT node. Type: dictionary +| | ... | - ip_address - IP address to bind BGP listener to. Type: string +| | ... | - port - Port number to bind BGP listener to. Type: integer +| | ... | - as_number - Autonomous System (AS) ID number. Type: integer +| | ... +| | ... | *Example:* +| | ... +| | ... | \| Configure BGP module \| ${nodes['DUT1']} \| 192.168.0.1 \| ${179} \ +| | ... | \| ${65000} \| +| | ... +| | [Arguments] | ${node} | ${ip_address} | ${port} | ${as_number} +| | Configure BGP base | ${node} | ${ip_address} | ${port} | ${as_number} + | No BGP peers should be configured | | [Documentation] | Uses Honeycomb API to read BGP configuration and checks | | ... | if there ary BGP peers conffigured. @@ -50,7 +68,7 @@ | | Add BGP Peer | ${node} | ${address} | ${data} | BGP Peer From Honeycomb Should be -| | [Documentation] | Uses Honeycomb API to verify BGP peer operational data. +| | [Documentation] | Uses Honeycomb API to verify BGP peer config data. | | ... | | ... | *Arguments:* | | ... | - node - Information about a DUT node. Type: dictionary @@ -67,6 +85,24 @@ | | ${oper_data}= | Get BGP Peer | ${node} | ${address} | | Compare Data Structures | ${oper_data} | ${data} +| Peer Operational Data From Honeycomb Should be +| | [Documentation] | Uses Honeycomb API to verify BGP peer operational data. +| | ... +| | ... | *Arguments:* +| | ... | - node - Information about a DUT node. Type: dictionary +| | ... | - address - IP address of the peer. Type: string +| | ... | - data - Peer configuration data. Type: dictionary +| | ... +| | ... | *Example:* +| | ... +| | ... | \| BGP Peer From Honeycomb Should be \ +| | ... | \| ${nodes['DUT1']} \| 192.168.0.1 \| ${data} \| +| | ... +| | [Arguments] | ${node} | ${address} +| | ... +| | ${oper_data}= | Get BGP Peer | ${node} | ${address} | operational +| | Should be Equal | ${oper_data['peer'][0]['peer-id']} | bgp://${address} + | Honeycomb removes BGP peer | | [Documentation] | Uses Honeycomb API to add a BGP peer. | | ... @@ -171,3 +207,51 @@ | | ${oper_data}= | Get All Peer Routes | | ... | ${node} | ${peer_address} | ${ip_version} | | Should be Empty | ${oper_data['bgp-inet:${ip_version}-routes']} + +| BGP Loc-RIB table should include +| | [Documentation] | Uses Honeycomb API to retrieve local BGP RIB table\ +| | ... | And verifies that it contains the specified entry. +| | ... +| | ... | *Arguments:* +| | ... | - node - Information about a DUT node. Type: dictionary +| | ... | - data - RIB that should be present in operational data.\ +| | ... | Type: dictionary +| | ... +| | ... | *Example:* +| | ... +| | ... | \| BGP Loc-RIB table should include \| ${nodes['DUT1']} \| ${data} \| +| | ... +| | [Arguments] | ${node} | ${data} +| | ... +| | ${oper_data}= | Get BGP Local RIB | ${node} +| | ${oper_data}= | Set Variable | ${oper_data['loc-rib']['tables']} +| | ${data}= | Set Variable | ${data['loc-rib']['tables']} +| | Compare RIB Tables | ${oper_data} | ${data} + +| Receive BGP OPEN message +| | [Documentation] | Open a TCP listener on BGP port(179) and listen\ +| | ... | for BGP OPEN message. Verify ID and holdtime fields. +| | ... +| | ... | *Arguments:* +| | ... | - tg_node - Information about the TG node. Type: dictionary +| | ... | - rx_ip - IP address to listen on. Type: string +| | ... | - src_ip - IP address of the BGP speaker. Also acts as BGP peer ID.\ +| | ... | Type: string +| | ... | - holdtime - Expected value of HOLD_TIME field in received message.\ +| | ... | Type: integer +| | ... +| | ... | *Example:* +| | ... +| | ... | \| Receive BGP OPEN message \| ${node['TG']} \ +| | ... | \| 192.168.0.1 \| 192.168.0.2 \| ${0} +| | ... +| | [Arguments] | ${tg_node} | ${rx_ip} | ${src_ip} | ${port} | ${as_number} +| | ... | ${holdtime} +| | ... +| | ${args}= | Catenate | --rx_ip | ${rx_ip} +| | ... | --src_ip | ${src_ip} +| | ... | --rx_port | ${port} +| | ... | --as_number | ${as_number} +| | ... | --holdtime | ${holdtime} +| | Run Traffic Script On Node | honeycomb/bgp_open.py +| | ... | ${tg_node} | ${args} diff --git a/resources/test_data/honeycomb/bgp.py b/resources/test_data/honeycomb/bgp.py index f9dc4ab233..84c9431e6b 100644 --- a/resources/test_data/honeycomb/bgp.py +++ b/resources/test_data/honeycomb/bgp.py @@ -18,6 +18,7 @@ from copy import deepcopy # Internal BGP peers for CRUD tests address_internal = "192.168.0.2" address_internal2 = "192.168.0.3" +holdtime_internal = 60 peer_internal = { "bgp-openconfig-extensions:neighbor": [{ "neighbor-address": address_internal, @@ -26,13 +27,13 @@ peer_internal = { }, "timers": { "config": { - "connect-retry": 10, - "hold-time": 60 + "connect-retry": 3, + "hold-time": holdtime_internal } }, "transport": { "config": { - "remote-port": 17900, + "remote-port": 179, "passive-mode": False } }, @@ -55,12 +56,12 @@ peer_internal_update = { "timers": { "config": { "connect-retry": 5, - "hold-time": 120 + "hold-time": holdtime_internal*2 } }, "transport": { "config": { - "remote-port": 17901, + "remote-port": 180, "passive-mode": True } }, @@ -78,7 +79,7 @@ peer_internal2 = deepcopy(peer_internal) peer_internal2["bgp-openconfig-extensions:neighbor"][0]["neighbor-address"] = \ address_internal2 -# Application BGP peer for CRUD tests +# Application BGP peer for CRUD test address_application = "192.168.0.4" peer_application = { "bgp-openconfig-extensions:neighbor": [{ @@ -103,6 +104,7 @@ peer_application = { }] } +# IPv4 route for CRUD test route_address_ipv4 = "192.168.0.5/32" route_id_ipv4 = 0 route_data_ipv4 = { @@ -124,6 +126,7 @@ route_data_ipv4 = { }] } +# IPv4 route for testing Update operation route_data_ipv4_update = { "bgp-inet:ipv4-route": [{ "path-id": route_id_ipv4, @@ -143,6 +146,7 @@ route_data_ipv4_update = { }] } +# IPv4 route for testing multiple routes route_address_ipv4_2 = "192.168.0.6/32" route_id_ipv4_2 = 1 route_data_ipv4_2 = { @@ -164,6 +168,7 @@ route_data_ipv4_2 = { }] } +# IPv6 route for CRUD test route_address_ipv6 = "3ffe:62::1/64" route_id_ipv6 = 0 route_data_ipv6 = { @@ -185,8 +190,164 @@ route_data_ipv6 = { }] } +# IPv4 route operational data in routing table table1_oper = { "destination-prefix": route_address_ipv4, "next-hop": "192.168.1.1", "vpp-ipv4-route-state": {} -}
\ No newline at end of file +} + +# Peer configurations for traffic test +dut1_peer = { + "bgp-openconfig-extensions:neighbor": [{ + "neighbor-address": "192.168.1.1", + "config": { + "peer-type": "INTERNAL" + }, + "timers": { + "config": { + "connect-retry": 3, + "hold-time": 60 + } + }, + "transport": { + "config": { + "remote-port": 179, + "passive-mode": False + } + }, + "afi-safis": { + "afi-safi": [ + { + "afi-safi-name": "openconfig-bgp-types:IPV4-UNICAST", + "receive": True, + "send-max": 0}, + { + "afi-safi-name": "openconfig-bgp-types:IPV6-UNICAST", + "receive": True, + "send-max": 0}, + { + "afi-safi-name": "LINKSTATE" + } + ] + } + }] +} + +dut2_peer = { + "bgp-openconfig-extensions:neighbor": [{ + "neighbor-address": "192.168.1.2", + "config": { + "peer-type": "INTERNAL" + }, + "timers": { + "config": { + "connect-retry": 3, + "hold-time": 60 + } + }, + "transport": { + "config": { + "remote-port": 179, + "passive-mode": True + } + }, + "afi-safis": { + "afi-safi": [ + { + "afi-safi-name": "openconfig-bgp-types:IPV4-UNICAST", + "receive": True, + "send-max": 0}, + { + "afi-safi-name": "openconfig-bgp-types:IPV6-UNICAST", + "receive": True, + "send-max": 0}, + { + "afi-safi-name": "LINKSTATE" + } + ] + } + }] +} + +# IPv4 route for traffic test +dut1_route_address = "192.168.0.5/32" +dut1_route_id = 1 +dut1_route = { + "bgp-inet:ipv4-route": [{ + "path-id": dut1_route_id, + "prefix": dut1_route_address, + "attributes": { + "as-path": {}, + "origin": { + "value": "igp" + }, + "local-pref": { + "pref": 100 + }, + "ipv4-next-hop": { + "global": "192.168.1.3" + } + } + }] +} + +# IPv4 route in peer operational data +rib_operational = { + "loc-rib": {"tables": [ + { + "afi": "bgp-types:ipv4-address-family", + "safi": "bgp-types:unicast-subsequent-address-family", + "bgp-inet:ipv4-routes": { + "ipv4-route": dut1_route["bgp-inet:ipv4-route"] + } + } + ]} +} + +route_operational = { + "vpp-ipv4-route-state": {}, + "next-hop": "192.168.1.3", + "destination-prefix": dut1_route_address +} + +# IPv6 route for traffic test +dut1_route_ip6_address = "3ffe:62::1/64" +dut1_route_ip6_id = 1 +dut1_route_ip6 = { + "bgp-inet:ipv6-route": [{ + "path-id": dut1_route_ip6_id, + "prefix": dut1_route_ip6_address, + "attributes": { + "as-path": {}, + "origin": { + "value": "igp" + }, + "local-pref": { + "pref": 100 + }, + "ipv6-next-hop": { + "global": "3ffe:63::1" + } + } + }] +} + +# IPv6 route in peer operational data +rib_ip6_operational = { + "loc-rib": {"tables": [ + { + "afi": "bgp-types:ipv6-address-family", + "safi": "bgp-types:unicast-subsequent-address-family", + "bgp-inet:ipv6-routes": { + "ipv6-route": dut1_route_ip6["bgp-inet:ipv6-route"] + } + } + ]} +} + +route_ip6_operational = { + "vpp-ipv6-route-state": {}, + "next-hop": "3ffe:63::1", + "destination-prefix": dut1_route_ip6_address +} diff --git a/resources/traffic_scripts/honeycomb/bgp_open.py b/resources/traffic_scripts/honeycomb/bgp_open.py new file mode 100755 index 0000000000..e7bb7d841a --- /dev/null +++ b/resources/traffic_scripts/honeycomb/bgp_open.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Traffic script that listens for incoming BGP connections and verifies +received GBP Open message.""" + +import sys +import socket + +from scapy.main import load_contrib +from scapy.layers.inet import Raw +from scapy.contrib.bgp import BGPHeader, BGPOpen + +from resources.libraries.python.TrafficScriptArg import TrafficScriptArg + + +def main(): + """Open a TCP listener socket on the default BGP port. Accept an incoming + connection, receive data and verify if data is a valid BGP Open message.""" + args = TrafficScriptArg(['rx_ip', 'src_ip', 'rx_port', 'as_number', + 'holdtime']) + + rx_ip = args.get_arg('rx_ip') + src_ip = args.get_arg('src_ip') + rx_port = int(args.get_arg('rx_port')) + as_number = int(args.get_arg('as_number')) + holdtime = int(args.get_arg('holdtime')) + + load_contrib("bgp") + + soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + soc.bind((rx_ip, rx_port)) + soc.listen(1) + + print "Listener active, awaiting connection..." + soc.settimeout(8) + conn, addr = soc.accept() + print 'Connection established with peer:', addr + + data = conn.recv(256) + conn.close() + soc.close() + + bgp_layer = (BGPHeader(Raw(data).load)) + bgp_layer.show() + + if not bgp_layer.haslayer(BGPOpen): + raise RuntimeError("Received data is not a BGP OPEN message.") + bgp_open = bgp_layer.getlayer(BGPOpen) + if bgp_open.bgp_id != src_ip: + raise RuntimeError( + "BGP ID mismatch. Received {0} but should be {1}". + format(bgp_open.bgp_id, src_ip)) + else: + print "BGP ID matched." + + if bgp_open.AS != as_number: + raise RuntimeError( + "BGP AS number mismatch. Received {0} but should be {1}". + format(bgp_open.AS, as_number)) + else: + print "BGP AS number matched." + + if bgp_open.hold_time != holdtime: + raise RuntimeError( + "Hold Time parameter mismatch. Received {0} but should be {1}.". + format(bgp_layer.getlayer(BGPOpen).holdtime, holdtime)) + else: + print "BGP Hold Time parameter matched." + + sys.exit(0) + +if __name__ == "__main__": + main() |