# 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