1 # Copyright (c) 2018 Cisco and / or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
3 # use this file except in compliance with the License. You may obtain a copy
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions
12 # and limitations under the License.
14 """wrk traffic profile parser.
16 See LLD for the structure of a wrk traffic profile.
20 from os.path import isfile
21 from pprint import pformat
23 from yaml import load, YAMLError
24 from robot.api import logger
26 from resources.tools.wrk.wrk_errors import WrkError
29 class WrkTrafficProfile(object):
30 """The wrk traffic profile.
33 MANDATORY_PARAMS = ("urls",
40 INTEGER_PARAMS = (("cpus", 1),
44 ("nr-of-connections", 1))
46 def __init__(self, profile_name):
47 """Read the traffic profile from the yaml file.
49 :param profile_name: Path to the yaml file with the profile.
50 :type profile_name: str
51 :raises: WrkError if it is not possible to parse the profile.
54 self._profile_name = None
55 self._traffic_profile = None
57 self.profile_name = profile_name
60 with open(self.profile_name, 'r') as profile_file:
61 self.traffic_profile = load(profile_file)
62 except IOError as err:
63 raise WrkError(msg="An error occurred while opening the file '{0}'."
64 .format(self.profile_name),
66 except YAMLError as err:
67 raise WrkError(msg="An error occurred while parsing the traffic "
68 "profile '{0}'.".format(self.profile_name),
71 self._validate_traffic_profile()
73 if self.traffic_profile:
74 logger.debug("\nThe wrk traffic profile '{0}' is valid.\n".
75 format(self.profile_name))
76 logger.debug("wrk traffic profile '{0}':".format(self.profile_name))
77 logger.debug(pformat(self.traffic_profile))
79 logger.debug("\nThe wrk traffic profile '{0}' is invalid.\n".
80 format(self.profile_name))
81 raise WrkError("\nThe wrk traffic profile '{0}' is invalid.\n".
82 format(self.profile_name))
85 return pformat(self.traffic_profile)
88 return pformat(self.traffic_profile)
90 def _validate_traffic_profile(self):
91 """Validate the traffic profile.
93 The specification, the structure and the rules are described in
97 logger.debug("\nValidating the wrk traffic profile '{0}'...\n".
98 format(self.profile_name))
99 if not (self._validate_mandatory_structure()
100 and self._validate_mandatory_values()
101 and self._validate_optional_values()
102 and self._validate_dependencies()):
103 self.traffic_profile = None
105 def _validate_mandatory_structure(self):
106 """Validate presence of mandatory parameters in trafic profile dict
108 :returns: whether mandatory structure is followed by the profile
111 # Level 1: Check if the profile is a dictionary:
112 if not isinstance(self.traffic_profile, dict):
113 logger.error("The wrk traffic profile must be a dictionary.")
116 # Level 2: Check if all mandatory parameters are present:
118 for param in self.MANDATORY_PARAMS:
119 if self.traffic_profile.get(param, None) is None:
120 logger.error("The parameter '{0}' in mandatory.".format(param))
124 def _validate_mandatory_values(self):
125 """Validate that mandatory profile values satisfy their constraints
127 :returns: whether mandatory values are acceptable
130 # Level 3: Mandatory params: Check if urls is a list:
132 if not isinstance(self.traffic_profile["urls"], list):
133 logger.error("The parameter 'urls' must be a list.")
136 # Level 3: Mandatory params: Check if integers are not below minimum
137 for param, minimum in self.INTEGER_PARAMS:
138 if not self._validate_int_param(param, minimum):
142 def _validate_optional_values(self):
143 """Validate values for optional parameters, if present
145 :returns: whether present optional values are acceptable
149 # Level 4: Optional params: Check if script is present:
150 script = self.traffic_profile.get("script", None)
151 if script is not None:
152 if not isinstance(script, str):
153 logger.error("The path to LuaJIT script in invalid")
156 if not isfile(script):
157 logger.error("The file '{0}' does not exist.".
161 self.traffic_profile["script"] = None
162 logger.debug("The optional parameter 'LuaJIT script' is not "
163 "defined. No problem.")
165 # Level 4: Optional params: Check if header is present:
166 header = self.traffic_profile.get("header", None)
167 if header is not None:
168 if isinstance(header, dict):
169 header = ", ".join("{0}: {1}".format(*item)
170 for item in header.items())
171 self.traffic_profile["header"] = header
172 elif not isinstance(header, str):
173 logger.error("The parameter 'header' type is not valid.")
177 logger.error("The parameter 'header' is defined but "
181 self.traffic_profile["header"] = None
182 logger.debug("The optional parameter 'header' is not defined. "
185 # Level 4: Optional params: Check if latency is present:
186 latency = self.traffic_profile.get("latency", None)
187 if latency is not None:
188 if not isinstance(latency, bool):
189 logger.error("The parameter 'latency' must be boolean.")
192 self.traffic_profile["latency"] = False
193 logger.debug("The optional parameter 'latency' is not defined. "
196 # Level 4: Optional params: Check if timeout is present:
197 if 'timeout' in self.traffic_profile:
198 if not self._validate_int_param('timeout', 1):
201 self.traffic_profile["timeout"] = None
202 logger.debug("The optional parameter 'timeout' is not defined. "
207 def _validate_dependencies(self):
208 """Validate dependencies between parameters
210 :returns: whether dependencies between parameters are acceptable
213 # Level 5: Check urls and cpus:
214 if self.traffic_profile["cpus"] % len(self.traffic_profile["urls"]):
215 logger.error("The number of CPUs must be a multiple of the "
220 def _validate_int_param(self, param, minimum):
221 """Validate that an int parameter is set acceptably
222 If it is not an int already but a string, convert and store it as int.
224 :param param: Name of a traffic profile parameter
225 :param minimum: The minimum value for the named parameter
228 :returns: whether param is set to an int of at least minimum value
231 value = self._traffic_profile[param]
232 if isinstance(value, (str, unicode)):
237 if isinstance(value, int) and value >= minimum:
238 self.traffic_profile[param] = value
240 logger.error("The parameter '{param}' must be an integer and "
241 "at least {minimum}".format(param=param, minimum=minimum))
245 def profile_name(self):
246 """Getter - Profile name.
248 :returns: The traffic profile file path
251 return self._profile_name
254 def profile_name(self, profile_name):
258 :type profile_name: str
260 self._profile_name = profile_name
263 def traffic_profile(self):
264 """Getter: Traffic profile.
266 :returns: The traffic profile.
269 return self._traffic_profile
271 @traffic_profile.setter
272 def traffic_profile(self, profile):
273 """Setter - Traffic profile.
275 :param profile: The new traffic profile.
278 self._traffic_profile = profile