diff options
Diffstat (limited to 'resources/tools/wrk/wrk_traffic_profile_parser.py')
-rw-r--r-- | resources/tools/wrk/wrk_traffic_profile_parser.py | 286 |
1 files changed, 286 insertions, 0 deletions
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 |