diff options
author | Deepak S <deepak.s@linux.intel.com> | 2016-12-30 09:21:22 -0800 |
---|---|---|
committer | Deepak S <deepak.s@linux.intel.com> | 2017-01-19 08:27:31 +0530 |
commit | a8075ff20b2046f847b91085aa27f2b336823337 (patch) | |
tree | 035dbed36ffb23ac4ced52bd06394c4fb159db03 /yardstick/network_services | |
parent | 6f8fbe8acb94448ef2eb188193e9bea534021e89 (diff) |
Introducing Generic framework to do pre-deployment VNF & Network service testing
This patch introduces the framework which is aligned with ETSI-TST001
This patch adds:
1. NetworkServiceTestCase introduces following functions
--> setup
--> Verify if infrastructure mapping can meet topology
--> Load VNF models
--> Fill traffic profile with information from topology
--> Provision VNFs
--> Run experiment (traffic)
--> run -> Yardstick calls run() at intervals defined in the yaml
and produces timestamped samples
--> teardown --> Stop VNFs
2. TrafficProfile is a generic class to get traffic profile for a given
testcase and select the traffic generator for testcase.
3. QueueFileWrapper is a class to send/recive cmds to vnf
4. GenericVNF is a generic class to instantiate VNF
5. GenericTrafficGen is a generic class to run/listen/verify traffic.
JIRA: YARDSTICK-483
Change-Id: Ic453c917d34dcb508a7f3afb459011da85f6402e
Signed-off-by: Deepak S <deepak.s@linux.intel.com>
Diffstat (limited to 'yardstick/network_services')
6 files changed, 462 insertions, 0 deletions
diff --git a/yardstick/network_services/traffic_profile/__init__.py b/yardstick/network_services/traffic_profile/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/yardstick/network_services/traffic_profile/__init__.py diff --git a/yardstick/network_services/traffic_profile/base.py b/yardstick/network_services/traffic_profile/base.py new file mode 100644 index 000000000..906498586 --- /dev/null +++ b/yardstick/network_services/traffic_profile/base.py @@ -0,0 +1,64 @@ +# Copyright (c) 2016-2017 Intel Corporation +# +# 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. +""" Base class for the generic traffic profile implementation """ + +from __future__ import absolute_import +from yardstick.common.utils import import_modules_from_package, itersubclasses + + +class TrafficProfile(object): + """ + This class defines the behavior + + """ + + @staticmethod + def get(tp_config): + """Get the traffic profile instance for the given traffic type + + :param tp_config: loaded YAML file + :return: + """ + profile_class = tp_config["traffic_profile"]["traffic_type"] + import_modules_from_package( + "yardstick.network_services.traffic_profile") + try: + return next(c for c in itersubclasses(TrafficProfile) + if c.__name__ == profile_class)(tp_config) + except StopIteration: + raise RuntimeError("No implementation for %s", profile_class) + + def __init__(self, tp_config): + # e.g. RFC2544 start_ip, stop_ip, drop_rate, + # IMIX = {"10K": 0.1, "100M": 0.5} + self.params = tp_config + + def execute(self, traffic_generator): + """ This methods defines the behavior of the traffic generator. + It will be called in a loop until the traffic generator exits. + + :param traffic_generator: TrafficGen instance + :return: None + """ + raise NotImplementedError() + + +class DummyProfile(TrafficProfile): + """ + This is an empty TrafficProfile implementation - if it is used, + the traffic will be completely handled by the Traffic Generator + implementation with no regard for the Traffic Profile. + """ + def execute(self, traffic_generator): + pass diff --git a/yardstick/network_services/vnf_generic/__init__.py b/yardstick/network_services/vnf_generic/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/yardstick/network_services/vnf_generic/__init__.py diff --git a/yardstick/network_services/vnf_generic/vnf/__init__.py b/yardstick/network_services/vnf_generic/vnf/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/yardstick/network_services/vnf_generic/vnf/__init__.py diff --git a/yardstick/network_services/vnf_generic/vnf/base.py b/yardstick/network_services/vnf_generic/vnf/base.py new file mode 100644 index 000000000..1d770f724 --- /dev/null +++ b/yardstick/network_services/vnf_generic/vnf/base.py @@ -0,0 +1,327 @@ +# Copyright (c) 2016-2017 Intel Corporation +# +# 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. +""" Base class implementation for generic vnf implementation """ + +from __future__ import absolute_import +import logging +import ipaddress +import six + +from yardstick.network_services.utils import get_nsb_option + +LOG = logging.getLogger(__name__) + + +class QueueFileWrapper(object): + """ Class providing file-like API for talking with SSH connection """ + + def __init__(self, q_in, q_out, prompt): + self.q_in = q_in + self.q_out = q_out + self.closed = False + self.buf = [] + self.bufsize = 20 + self.prompt = prompt + + def read(self, size): + """ read chunk from input queue """ + if self.q_in.qsize() > 0 and size: + in_data = self.q_in.get() + return in_data + + def write(self, chunk): + """ write chunk to output queue """ + self.buf.append(chunk) + # flush on prompt or if we exceed bufsize + + size = sum(len(c) for c in self.buf) + if self.prompt in chunk or size > self.bufsize: + out = ''.join(self.buf) + self.buf = [] + self.q_out.put(out) + + def close(self): + """ close multiprocessing queue """ + pass + + def clear(self): + """ clear queue """ + while self.q_out.qsize() > 0: + self.q_out.get() + + +class GenericVNF(object): + """ Class providing file-like API for generic VNF implementation """ + def __init__(self, vnfd): + super(GenericVNF, self).__init__() + self.vnfd = vnfd # fixme: parse this into a structure + # List of statistics we can obtain from this VNF + # - ETSI MANO 6.3.1.1 monitoring_parameter + self.kpi = self._get_kpi_definition(vnfd) + # Standard dictionary containing params like thread no, buffer size etc + self.config = {} + self.runs_traffic = False + self.name = "vnf__1" # name in topology file + self.bin_path = get_nsb_option("bin_path", "") + + @classmethod + def _get_kpi_definition(cls, vnfd): + """ Get list of KPIs defined in VNFD + + :param vnfd: + :return: list of KPIs, e.g. ['throughput', 'latency'] + """ + return vnfd['benchmark']['kpi'] + + @classmethod + def get_ip_version(cls, ip_addr): + """ get ip address version v6 or v4 """ + try: + address = ipaddress.ip_address(six.text_type(ip_addr)) + except ValueError: + LOG.error(ip_addr, " is not valid") + return + else: + return address.version + + def _ip_to_hex(self, ip_addr): + ip_to_convert = ip_addr.split(".") + ip_x = ip_addr + if self.get_ip_version(ip_addr) == 4: + ip_to_convert = ip_addr.split(".") + ip_octect = [int(octect) for octect in ip_to_convert] + ip_x = "{0[0]:02X}{0[1]:02X}{0[2]:02X}{0[3]:02X}".format(ip_octect) + return ip_x + + def _get_dpdk_port_num(self, name): + for intf in self.vnfd['vdu'][0]['external-interface']: + if name == intf['name']: + return intf['virtual-interface']['dpdk_port_num'] + + def _append_routes(self, ip_pipeline_cfg): + if 'routing_table' in self.vnfd['vdu'][0]: + routing_table = self.vnfd['vdu'][0]['routing_table'] + + where = ip_pipeline_cfg.find("arp_route_tbl") + link = ip_pipeline_cfg[:where] + route_add = ip_pipeline_cfg[where:] + + tmp = route_add.find('\n') + route_add = route_add[tmp:] + + cmds = "arp_route_tbl =" + + for route in routing_table: + net = self._ip_to_hex(route['network']) + net_nm = self._ip_to_hex(route['netmask']) + net_gw = self._ip_to_hex(route['gateway']) + port = self._get_dpdk_port_num(route['if']) + cmd = \ + " ({port0_local_ip_hex},{port0_netmask_hex},{dpdk_port},"\ + "{port1_local_ip_hex})".format(port0_local_ip_hex=net, + port0_netmask_hex=net_nm, + dpdk_port=port, + port1_local_ip_hex=net_gw) + cmds += cmd + + cmds += '\n' + ip_pipeline_cfg = link + cmds + route_add + + return ip_pipeline_cfg + + def _append_nd_routes(self, ip_pipeline_cfg): + if 'nd_route_tbl' in self.vnfd['vdu'][0]: + routing_table = self.vnfd['vdu'][0]['nd_route_tbl'] + + where = ip_pipeline_cfg.find("nd_route_tbl") + link = ip_pipeline_cfg[:where] + route_nd = ip_pipeline_cfg[where:] + + tmp = route_nd.find('\n') + route_nd = route_nd[tmp:] + + cmds = "nd_route_tbl =" + + for route in routing_table: + net = route['network'] + net_nm = route['netmask'] + net_gw = route['gateway'] + port = self._get_dpdk_port_num(route['if']) + cmd = \ + " ({port0_local_ip_hex},{port0_netmask_hex},{dpdk_port},"\ + "{port1_local_ip_hex})".format(port0_local_ip_hex=net, + port0_netmask_hex=net_nm, + dpdk_port=port, + port1_local_ip_hex=net_gw) + cmds += cmd + + cmds += '\n' + ip_pipeline_cfg = link + cmds + route_nd + + return ip_pipeline_cfg + + def _get_port0localip6(self): + return_value = "" + if 'nd_route_tbl' in self.vnfd['vdu'][0]: + routing_table = self.vnfd['vdu'][0]['nd_route_tbl'] + + inc = 0 + for route in routing_table: + inc += 1 + if inc == 1: + return_value = route['network'] + LOG.info("_get_port0localip6 : %s", return_value) + return return_value + + def _get_port1localip6(self): + return_value = "" + if 'nd_route_tbl' in self.vnfd['vdu'][0]: + routing_table = self.vnfd['vdu'][0]['nd_route_tbl'] + + inc = 0 + for route in routing_table: + inc += 1 + if inc == 2: + return_value = route['network'] + LOG.info("_get_port1localip6 : %s", return_value) + return return_value + + def _get_port0prefixlen6(self): + return_value = "" + if 'nd_route_tbl' in self.vnfd['vdu'][0]: + routing_table = self.vnfd['vdu'][0]['nd_route_tbl'] + + inc = 0 + for route in routing_table: + inc += 1 + if inc == 1: + return_value = route['netmask'] + LOG.info("_get_port0prefixlen6 : %s", return_value) + return return_value + + def _get_port1prefixlen6(self): + return_value = "" + if 'nd_route_tbl' in self.vnfd['vdu'][0]: + routing_table = self.vnfd['vdu'][0]['nd_route_tbl'] + + inc = 0 + for route in routing_table: + inc += 1 + if inc == 2: + return_value = route['netmask'] + LOG.info("_get_port1prefixlen6 : %s", return_value) + return return_value + + def _get_port0gateway6(self): + return_value = "" + if 'nd_route_tbl' in self.vnfd['vdu'][0]: + routing_table = self.vnfd['vdu'][0]['nd_route_tbl'] + + inc = 0 + for route in routing_table: + inc += 1 + if inc == 1: + return_value = route['network'] + LOG.info("_get_port0gateway6 : %s", return_value) + return return_value + + def _get_port1gateway6(self): + return_value = "" + if 'nd_route_tbl' in self.vnfd['vdu'][0]: + routing_table = self.vnfd['vdu'][0]['nd_route_tbl'] + + inc = 0 + for route in routing_table: + inc += 1 + if inc == 2: + return_value = route['network'] + LOG.info("_get_port1gateway6 : %s", return_value) + return return_value + + def instantiate(self, scenario_cfg, context_cfg): + """ Prepare VNF for operation and start the VNF process/VM + + :param scenario_cfg: + :param context_cfg: + :return: True/False + """ + raise NotImplementedError() + + def terminate(self): + """ Kill all VNF processes + + :return: + """ + raise NotImplementedError() + + def scale(self, flavor=""): + """ + + :param flavor: + :return: + """ + raise NotImplementedError() + + def collect_kpi(self): + """This method should return a dictionary containing the + selected KPI at a given point of time. + + :return: {"kpi": value, "kpi2": value} + """ + raise NotImplementedError() + + +class GenericTrafficGen(GenericVNF): + """ Class providing file-like API for generic traffic generator """ + + def __init__(self, vnfd): + super(GenericTrafficGen, self).__init__(vnfd) + self.runs_traffic = True + self.traffic_finished = False + self.name = "tgen__1" # name in topology file + + def run_traffic(self, traffic_profile): + """ Generate traffic on the wire according to the given params. + Method is non-blocking, returns immediately when traffic process + is running. Mandatory. + + :param traffic_profile: + :return: True/False + """ + raise NotImplementedError() + + def listen_traffic(self, traffic_profile): + """ Listen to traffic with the given parameters. + Method is non-blocking, returns immediately when traffic process + is running. Optional. + + :param traffic_profile: + :return: True/False + """ + pass + + def verify_traffic(self, traffic_profile): + """ Verify captured traffic after it has ended. Optional. + + :param traffic_profile: + :return: dict + """ + pass + + def terminate(self): + """ After this method finishes, all traffic processes should stop. Mandatory. + + :return: True/False + """ + raise NotImplementedError() diff --git a/yardstick/network_services/vnf_generic/vnfdgen.py b/yardstick/network_services/vnf_generic/vnfdgen.py new file mode 100644 index 000000000..9a02050a2 --- /dev/null +++ b/yardstick/network_services/vnf_generic/vnfdgen.py @@ -0,0 +1,71 @@ +# Copyright (c) 2016-2017 Intel Corporation +# +# 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. +""" Generic file to map and build vnf discriptor """ + +from __future__ import absolute_import +import collections +import yaml +import six + +from yardstick.common.task_template import TaskTemplate + + +def generate_vnfd(vnf_model, node): + """ + + :param vnf_model: VNF definition template, e.g. tg_ping_tpl.yaml + :param node: node configuration taken from pod.yaml + :return: Complete VNF Descriptor that will be taken + as input for GenericVNF.__init__ + """ + node["get"] = get + rendered_vnfd = TaskTemplate.render(vnf_model, **node) + # This is done to get rid of issues with serializing node + del node["get"] + filled_vnfd = yaml.load(rendered_vnfd) + return filled_vnfd + + +def dict_key_flatten(data): + """ Convert nested dict structure to dotted key + (e.g. {"a":{"b":1}} -> {"a.b":1} + + :param data: nested dictionary + :return: flat dicrionary + """ + next_data = {} + + if not any((isinstance(v, collections.Iterable) and not isinstance(v, str)) + for v in data.values()): + return data + + for key, val in six.iteritems(data): + if isinstance(val, collections.Mapping): + for n_k, n_v in six.iteritems(val): + next_data["%s.%s" % (key, n_k)] = n_v + elif isinstance(val, collections.Iterable) and not isinstance(val, + str): + for index, item in enumerate(val): + next_data["%s%d" % (key, index)] = item + else: + next_data[key] = val + + return dict_key_flatten(next_data) + + +def get(obj, key, *args): + """ Get template key from dictionary, get default value or raise an exception + """ + data = dict_key_flatten(obj) + return data.get(key, *args) |