aboutsummaryrefslogtreecommitdiffstats
path: root/resources/tools/wrk
diff options
context:
space:
mode:
authorTibor Frank <tifrank@cisco.com>2017-12-04 16:41:57 +0100
committerTibor Frank <tifrank@cisco.com>2018-01-10 15:35:01 +0100
commita95c54b7821596402e0aa7136cd7d1de71a5b187 (patch)
treef1c941b06bb05069af1b3f587b5bcfc8bc22ef3a /resources/tools/wrk
parentec120d957cfec192d30e84a0d337198153214a70 (diff)
CSIT-866: wrk onboarding in CSIT
- CSIT-867: Low Level Description - CSIT-868: wrk traffic profile - parsing - CSIT-869: wrk implementation into CSIT Change-Id: I65e1037f5ae05b3a5b2020e4a6c54462766ae1b4 Signed-off-by: Tibor Frank <tifrank@cisco.com>
Diffstat (limited to 'resources/tools/wrk')
-rw-r--r--resources/tools/wrk/__init__.py16
-rw-r--r--resources/tools/wrk/doc/wrk_lld.rst293
-rw-r--r--resources/tools/wrk/wrk.py291
-rw-r--r--resources/tools/wrk/wrk_errors.py55
-rw-r--r--resources/tools/wrk/wrk_traffic_profile_parser.py286
-rwxr-xr-xresources/tools/wrk/wrk_utils.sh290
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