From a3b8b5de76dd8b5737b03233f97e1508a03f6e67 Mon Sep 17 00:00:00 2001 From: Tibor Frank Date: Thu, 14 Apr 2016 12:46:22 +0200 Subject: [PATCH] Honeycomb API keywords - add keywords to get, put post and delete configuration data using Honeycomb REST API - add keywords to get operational data using Honeycomb REST API Change-Id: If75221777e0e83a809c834addf82e95bbd540e21 Signed-off-by: Tibor Frank --- resources/libraries/python/HoneycombAPIKeywords.py | 543 +++++++++++++++++++++ resources/libraries/python/HoneycombUtil.py | 101 +++- .../templates/honeycomb/config_bridge_domain.url | 1 + .../honeycomb/config_network_topology.url | 1 + .../templates/honeycomb/config_vpp_interfaces.url | 1 + .../templates/honeycomb/oper_bridge_domains.url | 1 + .../templates/honeycomb/oper_network_topology.url | 1 + .../templates/honeycomb/oper_vpp_interfaces.url | 1 + resources/templates/honeycomb/poll_oper_data.url | 1 + 9 files changed, 625 insertions(+), 26 deletions(-) create mode 100644 resources/libraries/python/HoneycombAPIKeywords.py create mode 100644 resources/templates/honeycomb/config_bridge_domain.url create mode 100644 resources/templates/honeycomb/config_network_topology.url create mode 100644 resources/templates/honeycomb/config_vpp_interfaces.url create mode 100644 resources/templates/honeycomb/oper_bridge_domains.url create mode 100644 resources/templates/honeycomb/oper_network_topology.url create mode 100644 resources/templates/honeycomb/oper_vpp_interfaces.url create mode 100644 resources/templates/honeycomb/poll_oper_data.url diff --git a/resources/libraries/python/HoneycombAPIKeywords.py b/resources/libraries/python/HoneycombAPIKeywords.py new file mode 100644 index 0000000000..0369a32d5b --- /dev/null +++ b/resources/libraries/python/HoneycombAPIKeywords.py @@ -0,0 +1,543 @@ +# 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. + +"""Keywords used with Honeycomb. + +There are implemented keywords which work with: +- Honeycomb operations +- VPP Interfaces +- Bridge domains + +The keywords make possible to put and get configuration data and to get +operational data. +""" + +from json import dumps + +from robot.api import logger + +from resources.libraries.python.HTTPRequest import HTTPCodes +from resources.libraries.python.HoneycombSetup import HoneycombError +from resources.libraries.python.HoneycombUtil import HoneycombUtil as HcUtil +from resources.libraries.python.HoneycombUtil import DataRepresentation + + +class OperationsKeywords(object): + """Keywords which perform "operations" in Honeycomb. + + The keywords in this class are not a part of a specific area in Honeycomb, + e.g.: interfaces or bridge domains, but they perform "operations" in any + area of Honeycomb. + """ + + def __init__(self): + pass + + @staticmethod + def poll_oper_data(node): + """Poll operational data. + + You can use this keyword when you configure something in Honeycomb and + you want configuration data to make effect immediately, e.g.: + + | | Create Bridge Domain | .... + | | Add Bridge Domain To Interface | .... + | | Poll Oper Data | .... + | | ${br}= | Get Oper Info About Bridge Domain | .... + + ..note:: This is not very reliable way how to poll operational data. + This keyword is only temporary workaround and will be removed when this + problem is solved in Honeycomb. + :param node: Honeycomb node. + :type: dict + :raises HoneycombError: If it is not possible to poll operational data. + """ + + status_code, _ = HcUtil.\ + post_honeycomb_data(node, "poll_oper_data", data='', + data_representation=DataRepresentation.NO_DATA, + timeout=30) + if status_code != HTTPCodes.OK: + raise HoneycombError("It was not possible to poll operational data " + "on node {0}.".format(node['host'])) + + +class InterfaceKeywords(object): + """Keywords for Interface manipulation. + + Implements keywords which get configuration and operational data about + vpp interfaces and set the interface's parameters using Honeycomb REST API. + """ + + def __init__(self): + pass + + @staticmethod + def _configure_interface(node, interface, data, + data_representation=DataRepresentation.JSON): + """Send interface configuration data and check the response. + + :param node: Honeycomb node. + :param interface: The name of interface. + :param data: Configuration data to be sent in PUT request. + :param data_representation: How the data is represented. + :type node: dict + :type interface: str + :type data: str + :type data_representation: DataRepresentation + :return: Content of response. + :rtype: bytearray + :raises HoneycombError: If the status code in response on PUT is not + 200 = OK. + """ + + status_code, resp = HcUtil.\ + put_honeycomb_data(node, "config_vpp_interfaces", data, + data_representation=data_representation) + if status_code != HTTPCodes.OK: + raise HoneycombError( + "The configuration of interface '{0}' was not successful. " + "Status code: {1}.".format(interface, status_code)) + return resp + + @staticmethod + def get_all_interfaces_cfg_data(node): + """Get configuration data about all interfaces from Honeycomb. + + :param node: Honeycomb node. + :type node: dict + :return: Configuration data about all interfaces from Honeycomb. + :rtype: list + :raises HoneycombError: If it is not possible to get configuration data. + """ + + status_code, resp = HcUtil.get_honeycomb_data(node, + "config_vpp_interfaces") + if status_code != HTTPCodes.OK: + raise HoneycombError( + "Not possible to get configuration information about the " + "interfaces. Status code: {0}.".format(status_code)) + try: + intf = HcUtil.parse_json_response(resp, ("interfaces", "interface")) + return intf + except KeyError: + return [] + + @staticmethod + def get_interface_cfg_info(node, interface): + """Get configuration data about the given interface from Honeycomb. + + :param node: Honeycomb node. + :param interface: The name of interface. + :type node: dict + :type interface: str + :return: Configuration data about the given interface from Honeycomb. + :rtype: dict + """ + + intfs = InterfaceKeywords.get_all_interfaces_cfg_data(node) + for intf in intfs: + if intf["name"] == interface: + return intf + return {} + + @staticmethod + def get_all_interfaces_oper_info(node): + """Get operational data about all interfaces from Honeycomb. + + :param node: Honeycomb node. + :type node: dict + :return: Operational data about all interfaces from Honeycomb. + :rtype: list + :raises HoneycombError: If it is not possible to get operational data. + """ + + status_code, resp = HcUtil.get_honeycomb_data(node, + "oper_vpp_interfaces") + if status_code != HTTPCodes.OK: + raise HoneycombError( + "Not possible to get operational information about the " + "interfaces. Status code: {0}.".format(status_code)) + try: + intf = HcUtil.parse_json_response(resp, ("interfaces-state", + "interface")) + return intf + except KeyError: + return [] + + @staticmethod + def get_interface_oper_info(node, interface): + """Get operational data about the given interface from Honeycomb. + + :param node: Honeycomb node. + :param interface: The name of interface. + :type node: dict + :type interface: str + :return: Operational data about the given interface from Honeycomb. + :rtype: dict + """ + + intfs = InterfaceKeywords.get_all_interfaces_oper_info(node) + for intf in intfs: + if intf["name"] == interface: + return intf + return {} + + @staticmethod + def set_interface_state(node, interface, state="up"): + """Set VPP interface state. + + The keyword changes the administration state of interface to up or down + depending on the parameter "state". + + :param node: Honeycomb node. + :param interface: The name of interface. + :param state: The requested state, only "up" and "down" are valid + values. + :type node: dict + :type interface: str + :type state: str + :return: Content of response. + :rtype: bytearray + :raises KeyError: If the argument "state" is nor "up" or "down". + :raises HoneycombError: If the interface is not present on the node. + """ + + intf_state = {"up": "true", + "down": "false"} + intfs = InterfaceKeywords.get_all_interfaces_cfg_data(node) + for intf in intfs: + if intf["name"] == interface: + intf["enabled"] = intf_state[state.lower()] + new_intf = {"interfaces": {"interface": intfs}} + return InterfaceKeywords._configure_interface(node, interface, + dumps(new_intf)) + raise HoneycombError("The interface '{0}' is not present on node " + "'{1}'.".format(interface, node['host'])) + + @staticmethod + def set_interface_up(node, interface): + """Set the administration state of VPP interface to up. + + :param node: Honeycomb node. + :param interface: The name of interface. + :type node: dict + :type interface: str + :return: Content of response + :rtype: bytearray + """ + + return InterfaceKeywords.set_interface_state(node, interface, "up") + + @staticmethod + def set_interface_down(node, interface): + """Set the administration state of VPP interface to down. + + :param node: Honeycomb node. + :param interface: The name of interface. + :type node: dict + :type interface: str + :return: Content of response. + :rtype: bytearray + """ + + return InterfaceKeywords.set_interface_state(node, interface, "down") + + @staticmethod + def add_bridge_domain_to_interface(node, interface, bd_name, + split_horizon_group=None, bvi=None): + """Add a new bridge domain to an interface and set its parameters. + + :param node: Honeycomb node. + :param interface: The name of interface. + :param bd_name: Bridge domain name. + :param split_horizon_group: Split-horizon group name. + :param bvi: The bridged virtual interface. + :type node: dict + :type interface: str + :type bd_name: str + :type split_horizon_group: str + :type bvi: str + :return: Content of response. + :rtype: bytearray + :raises HoneycombError: If the interface is not present on the node. + """ + + intfs = InterfaceKeywords.get_all_interfaces_cfg_data(node) + v3po_l2 = {"bridge-domain": str(bd_name)} + if split_horizon_group: + v3po_l2["split-horizon-group"] = str(split_horizon_group) + if bvi: + v3po_l2["bridged-virtual-interface"] = str(bvi) + for intf in intfs: + if intf["name"] == interface: + intf["v3po:l2"] = v3po_l2 + new_intf = {"interfaces": {"interface": intfs}} + return InterfaceKeywords._configure_interface(node, interface, + dumps(new_intf)) + raise HoneycombError("The interface '{0}' is not present on node " + "'{1}'.".format(interface, node['host'])) + + +class BridgeDomainKeywords(object): + """Keywords for Bridge domain manipulation. + + Implements keywords which get configuration and operational data about + bridge domains and put the bridge domains' parameters using Honeycomb REST + API. + """ + + def __init__(self): + pass + + @staticmethod + def _create_json_bridge_domain_info(name, **kwargs): + """Generate bridge domain information in the structure as it is expected + by Honeycomb. + + The generated data structure is as follows: + { + "bridge-domains": { + "bridge-domain": [ + { + "name": "bd_name", + "flood": "false", + "forward": "false", + "learn": "false", + "unknown-unicast-flood": "false", + "arp-termination": "false" + } + ] + } + } + + :param name: The name of new bridge-domain. + :param kwargs: named arguments: + flood (bool): If True, flooding is enabled. + forward (bool): If True, packet forwarding is enabled. + learn (bool): If True, learning is enabled. + uu_flood (bool): If True, unknown unicast flooding is enabled. + arp_termination (bool): If True, ARP termination is enabled. + :type name: str + :type kwargs: dict + :return: Bridge domain information in format suitable for Honeycomb. + :rtype: dict + :raises KeyError: If at least one of kwargs items is missing. + """ + + brd_info = { + "bridge-domains": { + "bridge-domain": [ + {"name": name, + "flood": str(kwargs["flood"]).lower(), + "forward": str(kwargs["forward"]).lower(), + "learn": str(kwargs["learn"]).lower(), + "unknown-unicast-flood": str(kwargs["uu_flood"]).lower(), + "arp-termination": str(kwargs["arp_termination"]).lower()}, + ] + } + } + + return brd_info + + @staticmethod + def create_bridge_domain(node, name, flood=True, forward=True, learn=True, + uu_flood=True, arp_termination=False): + """Create a bridge domain using Honeycomb. + + This keyword adds a new bridge domain to the list of bridge domains and + sets its parameters. The existing bridge domains are untouched. + :param node: Node with Honeycomb where the bridge domain should be + created. + :param name: The name of new bridge-domain. + :param flood: If True, flooding is enabled. + :param forward: If True, packet forwarding is enabled. + :param learn: If True, learning is enabled. + :param uu_flood: If True, unknown unicast flooding is enabled. + :param arp_termination: If True, ARP termination is enabled. + :type node: dict + :type name: str + :type flood: bool + :type forward: bool + :type learn: bool + :type uu_flood: bool + :type arp_termination: bool + :raises HoneycombError: If the bridge domain already exists or it has + not been created. + """ + + existing_brds = BridgeDomainKeywords.\ + get_all_bds_cfg_data(node, ignore_404=True) + + for brd in existing_brds: + if brd["name"] == name: + raise HoneycombError("Bridge domain {0} already exists.". + format(name)) + + brd_info = BridgeDomainKeywords._create_json_bridge_domain_info( + name, flood=flood, forward=forward, learn=learn, uu_flood=uu_flood, + arp_termination=arp_termination) + for brd in existing_brds: + brd_info["bridge-domains"]["bridge-domain"].append(brd) + + status_code, _ = HcUtil.put_honeycomb_data(node, "config_bridge_domain", + dumps(brd_info)) + if status_code != HTTPCodes.OK: + raise HoneycombError( + "Bridge domain {0} was not created. " + "Status code: {01}.".format(name, status_code)) + + @staticmethod + def get_all_bds_oper_data(node): + """Get operational data about all bridge domains from Honeycomb. + + :param node: Honeycomb node. + :type node: dict + :return: Operational data about all bridge domains from Honeycomb. + :rtype: list + :raises HoneycombError: If it is not possible to get information about + the bridge domains. + """ + + status_code, resp = HcUtil.get_honeycomb_data(node, + "oper_bridge_domains") + if status_code != HTTPCodes.OK: + raise HoneycombError( + "Not possible to get information about the bridge domains. " + "Status code: {0}.".format(status_code)) + try: + br_domains = HcUtil.parse_json_response(resp, ("bridge-domains", + "bridge-domain")) + except KeyError: + return [] + return br_domains + + @staticmethod + def get_bd_oper_data(node, name): + """Get operational data about the given bridge domain from Honeycomb. + + :param node: Honeycomb node. + :param name: The name of bridge domain. + :type node: dict + :type name: str + :return: Operational data about the given bridge domain from Honeycomb. + :rtype: dict + """ + + br_domains = BridgeDomainKeywords.get_all_bds_oper_data(node) + for br_domain in br_domains: + if br_domain["name"] == name: + br_domain["name"] = br_domain["name"] + return br_domain + return {} + + @staticmethod + def get_all_bds_cfg_data(node, ignore_404=False): + """Get configuration data about all bridge domains from Honeycomb. + + :param node: Honeycomb node. + :param ignore_404: If True, the error 404 is ignored. + :type node: dict + :type ignore_404: bool + :return: Configuration data about all bridge domains from Honeycomb. + :rtype: list + :raises HoneycombError: If it is not possible to get information about + the bridge domains. + """ + + status_code, resp = HcUtil.get_honeycomb_data(node, + "config_bridge_domain") + if status_code != HTTPCodes.OK: + if ignore_404 and status_code == HTTPCodes.NOT_FOUND: + br_domains = list() + logger.debug("Error 404 ignored") + else: + raise HoneycombError( + "Not possible to get information about the bridge domains. " + "Status code: {0}.".format(status_code)) + else: + try: + br_domains = HcUtil.parse_json_response(resp, ("bridge-domains", + "bridge-domain")) + except KeyError: + return [] + return br_domains + + @staticmethod + def get_bd_cfg_data(node, name): + """Get configuration data about the given bridge domain from Honeycomb. + + :param node: Honeycomb node. + :param name: The name of bridge domain. + :type node: dict + :type name: str + :return: Configuration data about the given bridge domain from + Honeycomb. + :rtype: dict + """ + + br_domains = BridgeDomainKeywords.get_all_bds_cfg_data(node) + for br_domain in br_domains: + if br_domain["name"] == name: + return br_domain + return {} + + @staticmethod + def delete_all_bridge_domains(node): + """Delete all bridge domains on Honeycomb node. + + :param node: Honeycomb node. + :type node: dict + :return: Response from DELETE request. + :rtype: str + :raises HoneycombError: If it is not possible to delete all bridge + domains. + """ + + status_code, resp = HcUtil.delete_honeycomb_data(node, + "config_bridge_domain") + if status_code != HTTPCodes.OK: + raise HoneycombError( + "Not possible to delete all bridge domains. " + "Status code: {0}.".format(status_code)) + return resp + + @staticmethod + def remove_bridge_domain(node, name): + """Remove one bridge domain from Honeycomb. + + :param node: Honeycomb node. + :param name: Name of the bridge domain to be removed. + :type node: dict + :type name: str + :return: True if the bridge domain was removed. + :rtype: bool + :raises HoneycombError: If it is not possible to remove the bridge + domain. + """ + + br_domains = BridgeDomainKeywords.get_all_bds_cfg_data(node) + for br_domain in br_domains: + if br_domain["name"] == name: + br_domains.remove(br_domain) + brd_info = {"bridge-domains": {"bridge-domain": br_domains}} + status_code, _ = HcUtil.put_honeycomb_data( + node, "config_bridge_domain", dumps(brd_info)) + if status_code != HTTPCodes.OK: + raise HoneycombError( + "Bridge domain '{0}' was not deleted. " + "Status code: {1}.".format(name, status_code)) + return True + + raise HoneycombError("Not possible to delete bridge domain '{0}'. The " + "bridge domain was not found".format(name)) diff --git a/resources/libraries/python/HoneycombUtil.py b/resources/libraries/python/HoneycombUtil.py index 92b545d1fc..7ffa6430ff 100644 --- a/resources/libraries/python/HoneycombUtil.py +++ b/resources/libraries/python/HoneycombUtil.py @@ -23,6 +23,7 @@ HoneycombAPIKeywords instead. """ from json import loads +from enum import Enum, unique from robot.api import logger @@ -30,6 +31,30 @@ from resources.libraries.python.HTTPRequest import HTTPRequest from resources.libraries.python.constants import Constants as Const +@unique +class DataRepresentation(Enum): + """Representation of data sent by PUT and POST requests.""" + NO_DATA = 0 + JSON = 1 + XML = 2 + TXT = 3 + + +# Headers used in requests. Key - content representation, value - header. +HEADERS = {DataRepresentation.NO_DATA: + {}, # Must be empty. + DataRepresentation.JSON: + {"Content-Type": "application/json", + "Accept": "text/plain"}, + DataRepresentation.XML: + {"Content-Type": "application/xml", + "Accept": "text/plain"}, + DataRepresentation.TXT: + {"Content-Type": "text/plain", + "Accept": "text/plain"} + } + + class HoneycombError(Exception): """Exception(s) raised by methods working with Honeycomb. @@ -166,53 +191,77 @@ class HoneycombUtil(object): without extension, not the full path. :type node: dict :type url_file: str - :return: Requested information. - :rtype list + :return: Status code and content of response. + :rtype tuple """ path = HoneycombUtil.read_path_from_url_file(url_file) - status_code, resp = HTTPRequest.get(node, path) - return status_code, resp + return HTTPRequest.get(node, path) @staticmethod - def put_honeycomb_data(node, url_file, data, data_representation='json'): + def put_honeycomb_data(node, url_file, data, + data_representation=DataRepresentation.JSON): """Send configuration data using PUT request and return the status code - and response. + and response content. :param node: Honeycomb node. :param url_file: URL file. The argument contains only the name of file without extension, not the full path. :param data: Configuration data to be sent to Honeycomb. - :param data_representation: How the data is represented. Supported types - of representation are: json, xml and txt. + :param data_representation: How the data is represented. :type node: dict :type url_file: str :type data: str - :type data_representation: str + :type data_representation: DataRepresentation :return: Status code and content of response. :rtype: tuple + :raises HoneycombError: If the given data representation is not defined + in HEADERS. """ - headers = {'json': - {"Content-Type": "application/json", - 'Accept': 'text/plain'}, - 'xml': - {"Content-Type": "application/xml", - 'Accept': 'text/plain'}, - 'txt': - {"Content-Type": "text/plain", - 'Accept': 'text/plain'} - } try: - header = headers[data_representation] - except KeyError as err: - raise HoneycombError("Wrong data type: {0}.". + header = HEADERS[data_representation] + except AttributeError as err: + raise HoneycombError("Wrong data representation: {0}.". format(data_representation), repr(err)) path = HoneycombUtil.read_path_from_url_file(url_file) - status_code, resp = HTTPRequest.put(node=node, path=path, - headers=header, payload=data) - return status_code, resp + return HTTPRequest.put(node=node, path=path, headers=header, + payload=data) + + @staticmethod + def post_honeycomb_data(node, url_file, data=None, + data_representation=DataRepresentation.JSON, + timeout=10): + """Send a POST request and return the status code and response content. + + :param node: Honeycomb node. + :param url_file: URL file. The argument contains only the name of file + without extension, not the full path. + :param data: Configuration data to be sent to Honeycomb. + :param data_representation: How the data is represented. + :param timeout: How long to wait for the server to send data before + giving up. + :type node: dict + :type url_file: str + :type data: str + :type data_representation: DataRepresentation + :type timeout: int + :return: Status code and content of response. + :rtype: tuple + :raises HoneycombError: If the given data representation is not defined + in HEADERS. + """ + + try: + header = HEADERS[data_representation] + except AttributeError as err: + raise HoneycombError("Wrong data representation: {0}.". + format(data_representation), repr(err)) + + path = HoneycombUtil.read_path_from_url_file(url_file) + return HTTPRequest.post(node=node, path=path, headers=header, + payload=data, timeout=timeout) @staticmethod def delete_honeycomb_data(node, url_file): @@ -223,7 +272,7 @@ class HoneycombUtil(object): without extension, not the full path. :type node: dict :type url_file: str - :return: Status code and response. + :return: Status code and content of response. :rtype tuple """ diff --git a/resources/templates/honeycomb/config_bridge_domain.url b/resources/templates/honeycomb/config_bridge_domain.url new file mode 100644 index 0000000000..03d8f241cd --- /dev/null +++ b/resources/templates/honeycomb/config_bridge_domain.url @@ -0,0 +1 @@ +/restconf/config/v3po:vpp/bridge-domains \ No newline at end of file diff --git a/resources/templates/honeycomb/config_network_topology.url b/resources/templates/honeycomb/config_network_topology.url new file mode 100644 index 0000000000..fe1b9310c2 --- /dev/null +++ b/resources/templates/honeycomb/config_network_topology.url @@ -0,0 +1 @@ +/restconf/config/network-topology:network-topology \ No newline at end of file diff --git a/resources/templates/honeycomb/config_vpp_interfaces.url b/resources/templates/honeycomb/config_vpp_interfaces.url new file mode 100644 index 0000000000..3789f5e535 --- /dev/null +++ b/resources/templates/honeycomb/config_vpp_interfaces.url @@ -0,0 +1 @@ +/restconf/config/ietf-interfaces:interfaces \ No newline at end of file diff --git a/resources/templates/honeycomb/oper_bridge_domains.url b/resources/templates/honeycomb/oper_bridge_domains.url new file mode 100644 index 0000000000..7c8584326c --- /dev/null +++ b/resources/templates/honeycomb/oper_bridge_domains.url @@ -0,0 +1 @@ +/restconf/operational/v3po:vpp-state/bridge-domains \ No newline at end of file diff --git a/resources/templates/honeycomb/oper_network_topology.url b/resources/templates/honeycomb/oper_network_topology.url new file mode 100644 index 0000000000..e165d3f259 --- /dev/null +++ b/resources/templates/honeycomb/oper_network_topology.url @@ -0,0 +1 @@ +/restconf/operational/network-topology:network-topology \ No newline at end of file diff --git a/resources/templates/honeycomb/oper_vpp_interfaces.url b/resources/templates/honeycomb/oper_vpp_interfaces.url new file mode 100644 index 0000000000..de6f950c0d --- /dev/null +++ b/resources/templates/honeycomb/oper_vpp_interfaces.url @@ -0,0 +1 @@ +/restconf/operational/ietf-interfaces:interfaces-state \ No newline at end of file diff --git a/resources/templates/honeycomb/poll_oper_data.url b/resources/templates/honeycomb/poll_oper_data.url new file mode 100644 index 0000000000..2fe04386f3 --- /dev/null +++ b/resources/templates/honeycomb/poll_oper_data.url @@ -0,0 +1 @@ +/restconf/operations/v3po:vpp-poll-oper-data \ No newline at end of file -- 2.16.6