-# Copyright (c) 2018 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.
-
-"""wrk implementation into CSIT framework.
-"""
-
-import re
-
-from copy import deepcopy
-from time import sleep
-
-from robot.api import logger
-
-from resources.libraries.python.ssh import SSH
-from resources.libraries.python.topology import NodeType
-from resources.libraries.python.CpuUtils import CpuUtils
-from resources.libraries.python.constants import Constants
-
-from resources.tools.wrk.wrk_traffic_profile_parser import WrkTrafficProfile
-from resources.tools.wrk.wrk_errors import WrkError
-
-
-REGEX_LATENCY_STATS = \
- r"Latency\s*" \
- r"(\d*\.*\d*\S*)\s*" \
- r"(\d*\.*\d*\S*)\s*" \
- r"(\d*\.*\d*\S*)\s*" \
- r"(\d*\.*\d*\%)"
-REGEX_RPS_STATS = \
- r"Req/Sec\s*" \
- r"(\d*\.*\d*\S*)\s*" \
- r"(\d*\.*\d*\S*)\s*" \
- r"(\d*\.*\d*\S*)\s*" \
- r"(\d*\.*\d*\%)"
-REGEX_RPS = r"Requests/sec:\s*" \
- r"(\d*\.*\S*)"
-REGEX_BW = r"Transfer/sec:\s*" \
- r"(\d*\.*\S*)"
-REGEX_LATENCY_DIST = \
- r"Latency Distribution\n" \
- r"\s*50\%\s*(\d*\.*\d*\D*)\n" \
- r"\s*75\%\s*(\d*\.*\d*\D*)\n" \
- r"\s*90\%\s*(\d*\.*\d*\D*)\n" \
- r"\s*99\%\s*(\d*\.*\d*\D*)\n"
-
-# Split number and multiplicand, e.g. 14.25k --> 14.25 and k
-REGEX_NUM = r"(\d*\.*\d*)(\D*)"
-
-
-def install_wrk(tg_node):
- """Install wrk on the TG node.
-
- :param tg_node: Traffic generator node.
- :type tg_node: dict
- :raises: RuntimeError if the given node is not a TG node or if the
- installation fails.
- """
-
- if tg_node['type'] != NodeType.TG:
- raise RuntimeError('Node type is not a TG.')
-
- ssh = SSH()
- ssh.connect(tg_node)
-
- ret, _, _ = ssh.exec_command(
- "sudo -E "
- "sh -c '{0}/resources/tools/wrk/wrk_utils.sh install false'".
- format(Constants.REMOTE_FW_DIR), timeout=1800)
- if int(ret) != 0:
- raise RuntimeError('Installation of wrk on TG node failed.')
-
-
-def destroy_wrk(tg_node):
- """Destroy wrk on the TG node.
-
- :param tg_node: Traffic generator node.
- :type tg_node: dict
- :raises: RuntimeError if the given node is not a TG node or the removal of
- wrk failed.
- """
-
- if tg_node['type'] != NodeType.TG:
- raise RuntimeError('Node type is not a TG.')
-
- ssh = SSH()
- ssh.connect(tg_node)
-
- ret, _, _ = ssh.exec_command(
- "sudo -E "
- "sh -c '{0}/resources/tools/wrk/wrk_utils.sh destroy'".
- format(Constants.REMOTE_FW_DIR), timeout=1800)
- if int(ret) != 0:
- raise RuntimeError('Removal of wrk from the TG node failed.')
-
-
-def run_wrk(tg_node, profile_name, tg_numa, test_type, warm_up=False):
- """Send the traffic as defined in the profile.
-
- :param tg_node: Traffic generator node.
- :param profile_name: The name of wrk traffic profile.
- :param tg_numa: Numa node on which wrk will run.
- :param test_type: The type of the tests: cps, rps, bw
- :param warm_up: If True, warm-up traffic is generated before test traffic.
- :type profile_name: str
- :type tg_node: dict
- :type tg_numa: int
- :type test_type: str
- :type warm_up: bool
- :returns: Message with measured data.
- :rtype: str
- :raises: RuntimeError if node type is not a TG.
- """
-
- if tg_node['type'] != NodeType.TG:
- raise RuntimeError('Node type is not a TG.')
-
- # Parse and validate the profile
- profile_path = ("resources/traffic_profiles/wrk/{0}.yaml".
- format(profile_name))
- profile = WrkTrafficProfile(profile_path).traffic_profile
-
- cores = CpuUtils.cpu_list_per_node(tg_node, tg_numa)
- first_cpu = cores[profile["first-cpu"]]
-
- if len(profile["urls"]) == 1 and profile["cpus"] == 1:
- params = [
- "traffic_1_url_1_core",
- str(first_cpu),
- str(profile["nr-of-threads"]),
- str(profile["nr-of-connections"]),
- "{0}s".format(profile["duration"]),
- "'{0}'".format(profile["header"]),
- str(profile["timeout"]),
- str(profile["script"]),
- str(profile["latency"]),
- "'{0}'".format(" ".join(profile["urls"]))
- ]
- if warm_up:
- warm_up_params = deepcopy(params)
- warm_up_params[4] = "10s"
- elif len(profile["urls"]) == profile["cpus"]:
- params = [
- "traffic_n_urls_n_cores",
- str(first_cpu),
- str(profile["nr-of-threads"]),
- str(profile["nr-of-connections"]),
- "{0}s".format(profile["duration"]),
- "'{0}'".format(profile["header"]),
- str(profile["timeout"]),
- str(profile["script"]),
- str(profile["latency"]),
- "'{0}'".format(" ".join(profile["urls"]))
- ]
- if warm_up:
- warm_up_params = deepcopy(params)
- warm_up_params[4] = "10s"
- else:
- params = [
- "traffic_n_urls_m_cores",
- str(first_cpu),
- str(profile["cpus"] / len(profile["urls"])),
- str(profile["nr-of-threads"]),
- str(profile["nr-of-connections"]),
- "{0}s".format(profile["duration"]),
- "'{0}'".format(profile["header"]),
- str(profile["timeout"]),
- str(profile["script"]),
- str(profile["latency"]),
- "'{0}'".format(" ".join(profile["urls"]))
- ]
- if warm_up:
- warm_up_params = deepcopy(params)
- warm_up_params[5] = "10s"
-
- args = " ".join(params)
-
- ssh = SSH()
- ssh.connect(tg_node)
-
- if warm_up:
- warm_up_args = " ".join(warm_up_params)
- ret, _, _ = ssh.exec_command(
- "{0}/resources/tools/wrk/wrk_utils.sh {1}".
- format(Constants.REMOTE_FW_DIR, warm_up_args), timeout=1800)
- if int(ret) != 0:
- raise RuntimeError('wrk runtime error.')
- sleep(60)
-
- ret, stdout, _ = ssh.exec_command(
- "{0}/resources/tools/wrk/wrk_utils.sh {1}".
- format(Constants.REMOTE_FW_DIR, args), timeout=1800)
- if int(ret) != 0:
- raise RuntimeError('wrk runtime error.')
-
- stats = _parse_wrk_output(stdout)
-
- log_msg = "\nMeasured values:\n"
- if test_type == "cps":
- log_msg += "Connections/sec: Avg / Stdev / Max / +/- Stdev\n"
- for item in stats["rps-stats-lst"]:
- log_msg += "{0} / {1} / {2} / {3}\n".format(*item)
- log_msg += "Total cps: {0}cps\n".format(stats["rps-sum"])
- elif test_type == "rps":
- log_msg += "Requests/sec: Avg / Stdev / Max / +/- Stdev\n"
- for item in stats["rps-stats-lst"]:
- log_msg += "{0} / {1} / {2} / {3}\n".format(*item)
- log_msg += "Total rps: {0}rps\n".format(stats["rps-sum"])
- elif test_type == "bw":
- log_msg += "Transfer/sec: {0}Bps".format(stats["bw-sum"])
-
- logger.info(log_msg)
-
- return log_msg
-
-
-def _parse_wrk_output(msg):
- """Parse the wrk stdout with the results.
-
- :param msg: stdout of wrk.
- :type msg: str
- :returns: Parsed results.
- :rtype: dict
- :raises: WrkError if the message does not include the results.
- """
-
- if "Thread Stats" not in msg:
- raise WrkError("The output of wrk does not include the results.")
-
- msg_lst = msg.splitlines(False)
-
- stats = {
- "latency-dist-lst": list(),
- "latency-stats-lst": list(),
- "rps-stats-lst": list(),
- "rps-lst": list(),
- "bw-lst": list(),
- "rps-sum": 0,
- "bw-sum": None
- }
-
- for line in msg_lst:
- if "Latency Distribution" in line:
- # Latency distribution - 50%, 75%, 90%, 99%
- pass
- elif "Latency" in line:
- # Latency statistics - Avg, Stdev, Max, +/- Stdev
- pass
- elif "Req/Sec" in line:
- # rps statistics - Avg, Stdev, Max, +/- Stdev
- stats["rps-stats-lst"].append((
- _evaluate_number(re.search(REGEX_RPS_STATS, line).group(1)),
- _evaluate_number(re.search(REGEX_RPS_STATS, line).group(2)),
- _evaluate_number(re.search(REGEX_RPS_STATS, line).group(3)),
- _evaluate_number(re.search(REGEX_RPS_STATS, line).group(4))))
- elif "Requests/sec:" in line:
- # rps (cps)
- stats["rps-lst"].append(
- _evaluate_number(re.search(REGEX_RPS, line).group(1)))
- elif "Transfer/sec:" in line:
- # BW
- stats["bw-lst"].append(
- _evaluate_number(re.search(REGEX_BW, line).group(1)))
-
- for item in stats["rps-stats-lst"]:
- stats["rps-sum"] += item[0]
- stats["bw-sum"] = sum(stats["bw-lst"])
-
- return stats
-
-
-def _evaluate_number(num):
- """Evaluate the numeric value of the number with multiplicands, e.g.:
- 12.25k --> 12250
-
- :param num: Number to evaluate.
- :type num: str
- :returns: Evaluated number.
- :rtype: float
- :raises: WrkError if it is not possible to evaluate the given number.
- """
-
- val = re.search(REGEX_NUM, num)
- try:
- val_num = float(val.group(1))
- except ValueError:
- raise WrkError("The output of wrk does not include the results "
- "or the format of results has changed.")
- val_mul = val.group(2).lower()
- if val_mul:
- if "k" in val_mul:
- val_num *= 1000
- elif "m" in val_mul:
- val_num *= 1000000
- elif "g" in val_mul:
- val_num *= 1000000000
- elif "b" in val_mul:
- pass
- elif "%" in val_mul:
- pass
- elif "" in val_mul:
- pass
- else:
- raise WrkError("The multiplicand {0} is not defined.".
- format(val_mul))
- return val_num