diff options
Diffstat (limited to 'resources/tools/wrk')
-rw-r--r-- | resources/tools/wrk/__init__.py | 16 | ||||
-rw-r--r-- | resources/tools/wrk/doc/wrk_lld.rst | 293 | ||||
-rw-r--r-- | resources/tools/wrk/wrk.py | 291 | ||||
-rw-r--r-- | resources/tools/wrk/wrk_errors.py | 55 | ||||
-rw-r--r-- | resources/tools/wrk/wrk_traffic_profile_parser.py | 286 | ||||
-rwxr-xr-x | resources/tools/wrk/wrk_utils.sh | 290 |
6 files changed, 1231 insertions, 0 deletions
diff --git a/resources/tools/wrk/__init__.py b/resources/tools/wrk/__init__.py new file mode 100644 index 0000000000..977169c00f --- /dev/null +++ b/resources/tools/wrk/__init__.py @@ -0,0 +1,16 @@ +# 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. + +""" +__init__ file for directory tools/wrk +""" diff --git a/resources/tools/wrk/doc/wrk_lld.rst b/resources/tools/wrk/doc/wrk_lld.rst new file mode 100644 index 0000000000..1437fd8948 --- /dev/null +++ b/resources/tools/wrk/doc/wrk_lld.rst @@ -0,0 +1,293 @@ +Onboarding of wrk as a http traffic generator in CSIT +----------------------------------------------------- + +wrk is a modern HTTP benchmarking tool capable of generating significant +load when run on a single multi-core CPU. + +An optional LuaJIT script can perform HTTP request generation, response +processing, and custom reporting. + + +wrk installation on TG node +''''''''''''''''''''''''''' + +**Procedure** + + #. Check if wrk is installed on the TG node. + #. If not, install it. + +**wrk installation** + +:: + + # Install pre-requisites: + sudo apt-get install build-essential libssl-dev git -y + + # Get the specified version: + wget ${WRK_DWNLD_PATH}/${WRK_TAR} + tar xzf ${WRK_TAR} + cd wrk-${WRK_VERSION} + + # Build the wrk: + cd wrk + make + + # Move the executable to somewhere in the PATH, e.q: + sudo cp wrk /usr/local/bin + + +wrk traffic profile +''''''''''''''''''' + +**The traffic profile can include these items:** + + - List of URLs - mandatory, + - The first CPU used to run wrk - mandatory, + - Number of CPUs used for wrk - mandatory, + - Test duration - mandatory, + - Number of threads - mandatory, + - Number of connections - mandatory, + - LuaJIT script - optional, defaults to no script, + - HTTP header - optional, defaults to no header, + - Latency - optional, defaults to False, + - Timeout - optional, defaults to wrk default. + +**List of URLs** + +List of URLs for requests. Each URL is requested in a separate instance of wrk. +Type: list + +*Example:* + +:: + + urls: + - "http://192.168.1.1/1kB.bin" + - "http://192.168.1.2/1kB.bin" + - "http://192.168.1.3/1kB.bin" + +**The first CPU used to run wrk** +The first CPU used to run wrk. The other CPUs follow this one. +Type: integer + +*Example:* + +:: + + first-cpu: 1 + +**Number of CPUs used for wrk** + +The number of CPUs used for wrk. The number of CPUs must be a multiplication +of the number of URLs. +Type: integer + +*Example:* + +:: + + cpus: 6 + +.. note:: + + The combinations of URLs and a number of CPUs create following use cases: + + - One URL and one CPU - One instance of wrk sends one request (URL) via + one NIC + - One URL and n CPUs - n instances of wrk send the same request (URL) + via one or more NICs + - n URLs and n CPUs - n instances of wrk send n requests (URL) via one + or more NICs + - n URLs and m CPUs, m = a * n - m instances of wrk send n requests + (URL) via one or more NICs + +**Test duration** + +Duration of the test in seconds. +Type: integer + +*Example:* + +:: + + duration: 30 + +**Number of threads** + +Total number of threads to use by wrk to send traffic. +Type: integer + +*Example:* + +:: + + nr-of-threads: 1 + +**Number of connections** + +Total number of HTTP connections to keep open with each thread handling +N = connections / threads. +Type: integer + +*Example:* + +:: + + nr-of-connections: 50 + +**LuaJIT script** + +Path to LuaJIT script. +Type: string + +For more information see: https://github.com/wg/wrk/blob/master/SCRIPTING + +*Example:* + +:: + + script: "scripts/report.lua" + +**HTTP header** + +HTTP header to add to request. +Type: string (taken as it is) or dictionary + +*Example:* + +:: + + # Dictionary: + header: + Connection: "close" + +or + +:: + + # String: + header: "Connection: close" + +**Latency** + +Print detailed latency statistics. +Type: boolean + +*Example:* + +:: + + latency: False + +**Timeout** + +Record a timeout if a response is not received within this amount of time. +Type: integer + +:: + + timeout: 5 + +**Examples of a wrk traffic profile** + +*Get the number of connections per second:* + +- Use 3 CPUs to send 3 different requests via 3 NICs. +- The test takes 30 seconds. +- wrk sends traffic in one thread per CPU. +- There will be open max 50 connection at the same time. +- The header is set to 'Connection: "close"' so wrk opens separate connection + for each request. Then the number of requests equals to the number of + connections. +- Timeout for responses from the server is set to 5 seconds. + +:: + + urls: + - "http://192.168.1.1/0B.bin" + - "http://192.168.1.2/0B.bin" + - "http://192.168.1.3/0B.bin" + cpus: 3 + duration: 30 + nr-of-threads: 1 + nr-of-connections: 50 + header: + Connection: "close" + timeout: 5 + +*Get the number of requests per second:* + +- Use 3 CPUs to send 3 different requests via 3 NICs. +- The test takes 30 seconds. +- wrk sends traffic in one thread per CPU. +- There will be max 50 concurrent open connections. + +:: + + urls: + - "http://192.168.1.1/1kB.bin" + - "http://192.168.1.2/1kB.bin" + - "http://192.168.1.3/1kB.bin" + cpus: 3 + duration: 30 + nr-of-threads: 1 + nr-of-connections: 50 + +*Get the bandwidth:* + +- Use 3 CPUs to send 3 different requests via 3 NICs. +- The test takes 30 seconds. +- wrk sends traffic in one thread per CPU. +- There will be open max 50 connection at the same time. +- Timeout for responses from the server is set to 5 seconds. + +:: + + urls: + - "http://192.168.1.1/1MB.bin" + - "http://192.168.1.2/1MB.bin" + - "http://192.168.1.3/1MB.bin" + cpus: 3 + duration: 30 + nr-of-threads: 1 + nr-of-connections: 50 + timeout: 5 + + +Running wrk +''''''''''' + +**Suite setup phase** + +CSIT framework checks if wrk is installed on the TG node. If not, or if the +installation is forced, it installs it on the TG node. + +*Procedure:* + + #. Make sure TRex is stopped. + #. Bind used TG interfaces to corresponding drivers (defined in the topology + file). + #. If the wrk installation is forced: + + - Destroy existing wrk + + #. If the wrk installation is not forced: + + - Check if wrk is installed. + - If installed, exit. + + #. Clone wrk from git (https://github.com/wg/wrk.git) + #. Build wrk. + #. Copy the executable to /usr/local/bin so it is in the PATH. + +**Test phase** + +*Procedure:* + +#. Read the wrk traffic profile. +#. Verify the profile. +#. Use the information from the profile to set the wrk parameters. +#. Run wrk. +#. Read the output. +#. Evaluate and log the output. + diff --git a/resources/tools/wrk/wrk.py b/resources/tools/wrk/wrk.py new file mode 100644 index 0000000000..33cfd08174 --- /dev/null +++ b/resources/tools/wrk/wrk.py @@ -0,0 +1,291 @@ +# 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 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): + """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 + :type profile_name: str + :type tg_node: dict + :type tg_numa: int + :type test_type: str + :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"])) + ] + 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"])) + ] + 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"])) + ] + args = " ".join(params) + + ssh = SSH() + ssh.connect(tg_node) + + 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}cps\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 diff --git a/resources/tools/wrk/wrk_errors.py b/resources/tools/wrk/wrk_errors.py new file mode 100644 index 0000000000..3173dd4223 --- /dev/null +++ b/resources/tools/wrk/wrk_errors.py @@ -0,0 +1,55 @@ +# 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. + +"""Implementation of exceptions used in the wrk traffic generator. +""" + + +from robot.api import logger + + +class WrkError(Exception): + """Exception(s) raised by the wrk traffic generator. + + When raising this exception, put this information to the message in this + order: + - short description of the encountered problem (parameter msg), + - relevant messages if there are any collected, e.g., from caught + exception (optional parameter details), + - relevant data if there are any collected (optional parameter details). + """ + + def __init__(self, msg, details=''): + """Sets the exception message and the level. + + :param msg: Short description of the encountered problem. + :param details: Relevant messages if there are any collected, e.g.: + from caught exception (optional parameter details), or relevant data if + there are any collected (optional parameter details). + :type msg: str + :type details: str + """ + + super(WrkError, self).__init__() + self._msg = msg + self._details = details + + logger.error(self._msg) + if self._details: + logger.error(self._details) + + def __repr__(self): + return repr(self._msg) + + def __str__(self): + return str(self._msg) diff --git a/resources/tools/wrk/wrk_traffic_profile_parser.py b/resources/tools/wrk/wrk_traffic_profile_parser.py new file mode 100644 index 0000000000..e1f8365345 --- /dev/null +++ b/resources/tools/wrk/wrk_traffic_profile_parser.py @@ -0,0 +1,286 @@ +# 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 traffic profile parser. + +See LLD for the structure of a wrk traffic profile. +""" + + +from os.path import isfile +from pprint import pformat + +from yaml import load, YAMLError +from robot.api import logger + +from resources.tools.wrk.wrk_errors import WrkError + + +class WrkTrafficProfile(object): + """The wrk traffic profile. + """ + + MANDATORY_PARAMS = ("urls", + "first-cpu", + "cpus", + "duration", + "nr-of-threads", + "nr-of-connections") + + def __init__(self, profile_name): + """Read the traffic profile from the yaml file. + + :param profile_name: Path to the yaml file with the profile. + :type profile_name: str + :raises: WrkError if it is not possible to parse the profile. + """ + + self._profile_name = None + self._traffic_profile = None + + self.profile_name = profile_name + + try: + with open(self.profile_name, 'r') as profile_file: + self.traffic_profile = load(profile_file) + except IOError as err: + raise WrkError(msg="An error occurred while opening the file '{0}'." + .format(self.profile_name), + details=str(err)) + except YAMLError as err: + raise WrkError(msg="An error occurred while parsing the traffic " + "profile '{0}'.".format(self.profile_name), + details=str(err)) + + self._validate_traffic_profile() + + if self.traffic_profile: + logger.debug("\nThe wrk traffic profile '{0}' is valid.\n". + format(self.profile_name)) + logger.debug("wrk traffic profile '{0}':".format(self.profile_name)) + logger.debug(pformat(self.traffic_profile)) + else: + logger.debug("\nThe wrk traffic profile '{0}' is invalid.\n". + format(self.profile_name)) + raise WrkError("\nThe wrk traffic profile '{0}' is invalid.\n". + format(self.profile_name)) + + def __repr__(self): + return pformat(self.traffic_profile) + + def __str__(self): + return pformat(self.traffic_profile) + + def _validate_traffic_profile(self): + """Validate the traffic profile. + + The specification, the structure and the rules are described in + doc/wrk_lld.rst + """ + + logger.debug("\nValidating the wrk traffic profile '{0}'...\n". + format(self.profile_name)) + + # Level 1: Check if the profile is a dictionary: + if not isinstance(self.traffic_profile, dict): + logger.error("The wrk traffic profile must be a dictionary.") + self.traffic_profile = None + return + + # Level 2: Check if all mandatory parameters are present: + is_valid = True + for param in self.MANDATORY_PARAMS: + if self.traffic_profile.get(param, None) is None: + logger.error("The parameter '{0}' in mandatory.".format(param)) + is_valid = False + if not is_valid: + self.traffic_profile = None + return + + # Level 3: Mandatory params: Check if urls is a list: + is_valid = True + if not isinstance(self.traffic_profile["urls"], list): + logger.error("The parameter 'urls' must be a list.") + is_valid = False + + # Level 3: Mandatory params: Check if cpus is a valid integer: + try: + cpus = int(self.traffic_profile["cpus"]) + if cpus < 1: + raise ValueError + self.traffic_profile["cpus"] = cpus + except ValueError: + logger.error("The parameter 'cpus' must be an integer greater than " + "1.") + is_valid = False + + # Level 3: Mandatory params: Check if first-cpu is a valid integer: + try: + first_cpu = int(self.traffic_profile["first-cpu"]) + if first_cpu < 0: + raise ValueError + self.traffic_profile["first-cpu"] = first_cpu + except ValueError: + logger.error("The parameter 'first-cpu' must be an integer greater " + "than 1.") + is_valid = False + + # Level 3: Mandatory params: Check if duration is a valid integer: + try: + duration = int(self.traffic_profile["duration"]) + if duration < 1: + raise ValueError + self.traffic_profile["duration"] = duration + except ValueError: + logger.error("The parameter 'duration' must be an integer " + "greater than 1.") + is_valid = False + + # Level 3: Mandatory params: Check if nr-of-threads is a valid integer: + try: + nr_of_threads = int(self.traffic_profile["nr-of-threads"]) + if nr_of_threads < 1: + raise ValueError + self.traffic_profile["nr-of-threads"] = nr_of_threads + except ValueError: + logger.error("The parameter 'nr-of-threads' must be an integer " + "greater than 1.") + is_valid = False + + # Level 3: Mandatory params: Check if nr-of-connections is a valid + # integer: + try: + nr_of_connections = int(self.traffic_profile["nr-of-connections"]) + if nr_of_connections < 1: + raise ValueError + self.traffic_profile["nr-of-connections"] = nr_of_connections + except ValueError: + logger.error("The parameter 'nr-of-connections' must be an integer " + "greater than 1.") + is_valid = False + + # Level 4: Optional params: Check if script is present: + script = self.traffic_profile.get("script", None) + if script is not None: + if not isinstance(script, str): + logger.error("The path to LuaJIT script in invalid") + is_valid = False + else: + if not isfile(script): + logger.error("The file '{0}' in not present.". + format(script)) + is_valid = False + else: + self.traffic_profile["script"] = None + logger.debug("The optional parameter 'LuaJIT script' is not " + "defined. No problem.") + + # Level 4: Optional params: Check if header is present: + header = self.traffic_profile.get("header", None) + if header: + if not (isinstance(header, dict) or isinstance(header, str)): + logger.error("The parameter 'header' is not valid.") + is_valid = False + else: + if isinstance(header, dict): + header_lst = list() + for key, val in header.items(): + header_lst.append("{0}: {1}".format(key, val)) + if header_lst: + self.traffic_profile["header"] = ", ".join(header_lst) + else: + logger.error("The parameter 'header' is defined but " + "empty.") + is_valid = False + else: + self.traffic_profile["header"] = None + logger.debug("The optional parameter 'header' is not defined. " + "No problem.") + + # Level 4: Optional params: Check if latency is present: + latency = self.traffic_profile.get("latency", None) + if latency is not None: + try: + latency = bool(latency) + self.traffic_profile["latency"] = latency + except ValueError: + logger.error("The parameter 'latency' must be boolean.") + is_valid = False + else: + self.traffic_profile["latency"] = False + logger.debug("The optional parameter 'latency' is not defined. " + "No problem.") + + # Level 4: Optional params: Check if timeout is present: + timeout = self.traffic_profile.get("timeout", None) + if timeout: + try: + timeout = int(timeout) + if timeout < 1: + raise ValueError + self.traffic_profile["timeout"] = timeout + except ValueError: + logger.error("The parameter 'timeout' must be integer greater " + "than 1.") + is_valid = False + else: + self.traffic_profile["timeout"] = None + logger.debug("The optional parameter 'timeout' is not defined. " + "No problem.") + + if not is_valid: + self.traffic_profile = None + return + + # Level 5: Check dependencies between parameters: + # Level 5: Check urls and cpus: + if self.traffic_profile["cpus"] % len(self.traffic_profile["urls"]): + logger.error("The number of CPUs must be a multiplication of the " + "number of URLs.") + self.traffic_profile = None + + @property + def profile_name(self): + """Getter - Profile name. + + :returns: The traffic profile file path + :rtype: str + """ + return self._profile_name + + @profile_name.setter + def profile_name(self, profile_name): + """ + + :param profile_name: + :type profile_name: str + """ + self._profile_name = profile_name + + @property + def traffic_profile(self): + """Getter: Traffic profile. + + :returns: The traffic profile. + :rtype: dict + """ + return self._traffic_profile + + @traffic_profile.setter + def traffic_profile(self, profile): + """Setter - Traffic profile. + + :param profile: The new traffic profile. + :type profile: dict + """ + self._traffic_profile = profile diff --git a/resources/tools/wrk/wrk_utils.sh b/resources/tools/wrk/wrk_utils.sh new file mode 100755 index 0000000000..2b9f6cf296 --- /dev/null +++ b/resources/tools/wrk/wrk_utils.sh @@ -0,0 +1,290 @@ +#!/bin/bash +# 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. + +set -x + +WRK_VERSION="4.0.2" +WRK_TAR=${WRK_VERSION}".tar.gz" +WRK_DWNLD_PATH="https://github.com/wg/wrk/archive" +WRK_TARGET="/opt" +WRK_INSTALL_DIR=${WRK_TARGET}/wrk-${WRK_VERSION} + +function wrk_utils.install { + # Install wrk + + # Directory for wrk: + dir=${1} + # Force the installation: + force=${2:-false} + + # Check if wrk is installed: + if [ "${force}" = true ]; then + wrk_utils.destroy + else + which wrk + if [ $? -eq 0 ]; then + test -d ${dir}/${WRK_INSTALL_DIR} && echo "WRK already installed: ${dir}/${WRK_INSTALL_DIR}" && exit 0 + fi + fi + + # Install pre-requisites: + apt-get update + apt-get install build-essential libssl-dev -y + + # Remove previous installation: + wrk_utils.destroy + + # Change the directory: + cd ${WRK_TARGET} + + # Get the specified version: + wget ${WRK_DWNLD_PATH}/${WRK_TAR} + tar xzf ${WRK_TAR} + rm ${WRK_TAR} + cd ${WRK_INSTALL_DIR} + + # Build the wrk: + make + + # Move the executable to somewhere in the PATH: + cp wrk /usr/local/bin +} + +function wrk_utils.destroy { + # Remove wrk + + sudo rm /usr/local/bin/wrk || true + sudo rm -rf ${WRK_INSTALL_DIR} || true +} + +function wrk_utils.traffic_1_url_1_core { + # Send traffic + # - to n URL (NIC) + # - using n instances of wrk, each on separate core. + + # The CPU used for wrk + cpu=${1} + # Total number of threads to use by one instance of wrk to send traffic. + threads=${2} + # Total number of HTTP connections to keep open with each thread handling + # N = connections / threads. + connections=${3} + # Duration of the test. + duration=${4} + # HTTP header to add to request. + header=${5} + # Record a timeout if a response is not received within this amount of time. + timeout=${6} + # Path to LuaJIT script. + script=${7} + # Print detailed latency statistics. + latency=${8} + # URL to send the traffic to. + url=${9} + + if [ "${timeout}" != "None" ]; then + timeout="--timeout ${timeout}" + else + timeout="" + fi + + if [ "${latency}" = "True" ]; then + latency="--latency" + else + latency="" + fi + + if [ "${script}" != "None" ]; then + script="--script '${script}'" + else + script="" + fi + + if [ "${header}" != "None" ]; then + header="${header}" + else + header="" + fi + + taskset --cpu-list ${cpu} \ + wrk --threads ${threads} \ + --connections ${connections} \ + --duration ${duration} \ + --header "${header}" \ + ${timeout} \ + ${script} \ + ${latency} \ + ${url} +} + +function wrk_utils.traffic_n_urls_n_cores { + # Send traffic + # - to n URL (NIC) + # - using n instances of wrk, each on separate core. + + # The first CPU used for wrk + first_cpu=${1} + # Total number of threads to use by one instance of wrk to send traffic. + threads=${2} + # Total number of HTTP connections to keep open with each thread handling + # N = connections / threads. + connections=${3} + # Duration of the test. + duration=${4} + # HTTP header to add to request. + header=${5} + # Record a timeout if a response is not received within this amount of time. + timeout=${6} + # Path to LuaJIT script. + script=${7} + # Print detailed latency statistics. + latency=${8} + # URL to send the traffic to. + urls=${9} + + if [ "${timeout}" != "None" ]; then + timeout="--timeout ${timeout}" + else + timeout="" + fi + + if [ "${latency}" = "True" ]; then + latency="--latency" + else + latency="" + fi + + if [ "${script}" != "None" ]; then + script="--script '${script}'" + else + script="" + fi + + if [ "${header}" != "None" ]; then + header="${header}" + else + header="" + fi + + urls=$(echo ${urls} | tr ";" "\n") + cpu=${first_cpu} + for url in ${urls}; do + taskset --cpu-list ${cpu} \ + wrk --threads ${threads} \ + --connections ${connections} \ + --duration ${duration} \ + --header "${header}" \ + ${timeout} \ + ${script} \ + ${latency} \ + ${url} & + cpu=$((cpu+1)) + done + + sleep ${duration} + sleep 2 +} + +function wrk_utils.traffic_n_urls_m_cores { + # Send traffic + # - to n URL (NIC) + # - using m instances of wrk, each on separate core. + + # The first CPU used for wrk + first_cpu=${1} + # The last CPU used for wrk + cpus_per_url=${2} + # Total number of threads to use by one instance of wrk to send traffic. + threads=${3} + # Total number of HTTP connections to keep open with each thread handling + # N = connections / threads. + connections=${4} + # Duration of the test. + duration=${5} + # HTTP header to add to request. + header=${6} + # Record a timeout if a response is not received within this amount of time. + timeout=${7} + # Path to LuaJIT script. + script=${8} + # Print detailed latency statistics. + latency=${9} + # URL to send the traffic to. + urls=${10} + + if [ "${timeout}" != "None" ]; then + timeout="--timeout ${timeout}" + else + timeout="" + fi + + if [ "${latency}" = "True" ]; then + latency="--latency" + else + latency="" + fi + + if [ "${script}" != "None" ]; then + script="--script '${script}'" + else + script="" + fi + + if [ "${header}" != "None" ]; then + header="${header}" + else + header="" + fi + + urls=$(echo ${urls} | tr ";" "\n") + + cpu=${first_cpu} + for i in `seq 1 ${cpus_per_url}`; do + for url in ${urls}; do + taskset --cpu-list ${cpu} \ + wrk --threads ${threads} \ + --connections ${connections} \ + --duration ${duration} \ + --header "${header}" \ + ${timeout} \ + ${script} \ + ${latency} \ + ${url} & + cpu=$((cpu+1)) + done + done + + sleep ${duration} + sleep 2 +} + +args=("$@") +case ${1} in + install) + force=${2} + wrk_utils.install ${force} + ;; + destroy) + wrk_utils.destroy + ;; + traffic_1_url_1_core) + wrk_utils.traffic_1_url_1_core "${args[@]:1}" + ;; + traffic_n_urls_n_cores) + wrk_utils.traffic_n_urls_n_cores "${args[@]:1}" + ;; + traffic_n_urls_m_cores) + wrk_utils.traffic_n_urls_m_cores "${args[@]:1}" + ;; +esac |