1 # Copyright (c) 2019 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 safe_load, YAMLError
24 from robot.api import logger
26 from resources.tools.wrk.wrk_errors import WrkError
29 class WrkTrafficProfile:
30 """The wrk traffic profile.
46 (u"nr-of-threads", 1),
47 (u"nr-of-connections", 1)
50 def __init__(self, profile_name):
51 """Read the traffic profile from the yaml file.
53 :param profile_name: Path to the yaml file with the profile.
54 :type profile_name: str
55 :raises: WrkError if it is not possible to parse the profile.
58 self._profile_name = None
59 self._traffic_profile = None
61 self.profile_name = profile_name
64 with open(self.profile_name, u"rt") as profile_file:
65 self.traffic_profile = safe_load(profile_file)
66 except IOError as err:
68 msg=f"An error occurred while opening the file "
69 f"'{self.profile_name}'.", details=str(err)
71 except YAMLError as err:
73 msg=f"An error occurred while parsing the traffic profile "
74 f"'{self.profile_name}'.", details=str(err)
77 self._validate_traffic_profile()
79 if self.traffic_profile:
81 f"\nThe wrk traffic profile '{self.profile_name}' is valid.\n"
83 logger.debug(f"wrk traffic profile '{self.profile_name}':")
84 logger.debug(pformat(self.traffic_profile))
87 f"\nThe wrk traffic profile '{self.profile_name}' is invalid.\n"
90 f"\nThe wrk traffic profile '{self.profile_name}' is invalid.\n"
94 return pformat(self.traffic_profile)
97 return pformat(self.traffic_profile)
99 def _validate_traffic_profile(self):
100 """Validate the traffic profile.
102 The specification, the structure and the rules are described in
107 f"\nValidating the wrk traffic profile '{self.profile_name}'...\n"
109 if not (self._validate_mandatory_structure()
110 and self._validate_mandatory_values()
111 and self._validate_optional_values()
112 and self._validate_dependencies()):
113 self.traffic_profile = None
115 def _validate_mandatory_structure(self):
116 """Validate presence of mandatory parameters in trafic profile dict
118 :returns: whether mandatory structure is followed by the profile
121 # Level 1: Check if the profile is a dictionary:
122 if not isinstance(self.traffic_profile, dict):
123 logger.error(u"The wrk traffic profile must be a dictionary.")
126 # Level 2: Check if all mandatory parameters are present:
128 for param in self.MANDATORY_PARAMS:
129 if self.traffic_profile.get(param, None) is None:
130 logger.error(f"The parameter '{param}' in mandatory.")
134 def _validate_mandatory_values(self):
135 """Validate that mandatory profile values satisfy their constraints
137 :returns: whether mandatory values are acceptable
140 # Level 3: Mandatory params: Check if urls is a list:
142 if not isinstance(self.traffic_profile[u"urls"], list):
143 logger.error(u"The parameter 'urls' must be a list.")
146 # Level 3: Mandatory params: Check if integers are not below minimum
147 for param, minimum in self.INTEGER_PARAMS:
148 if not self._validate_int_param(param, minimum):
152 def _validate_optional_values(self):
153 """Validate values for optional parameters, if present
155 :returns: whether present optional values are acceptable
159 # Level 4: Optional params: Check if script is present:
160 script = self.traffic_profile.get(u"script", None)
161 if script is not None:
162 if not isinstance(script, str):
163 logger.error(u"The path to LuaJIT script in invalid")
166 if not isfile(script):
167 logger.error(f"The file '{script}' does not exist.")
170 self.traffic_profile[u"script"] = None
172 u"The optional parameter 'LuaJIT script' is not defined. "
176 # Level 4: Optional params: Check if header is present:
177 header = self.traffic_profile.get(u"header", None)
178 if header is not None:
179 if isinstance(header, dict):
181 f"{0}: {1}".format(*item) for item in header.items()
183 self.traffic_profile[u"header"] = header
184 elif not isinstance(header, str):
185 logger.error(u"The parameter 'header' type is not valid.")
189 logger.error(u"The parameter 'header' is defined but empty.")
192 self.traffic_profile[u"header"] = None
194 u"The optional parameter 'header' is not defined. No problem."
197 # Level 4: Optional params: Check if latency is present:
198 latency = self.traffic_profile.get(u"latency", None)
199 if latency is not None:
200 if not isinstance(latency, bool):
201 logger.error(u"The parameter 'latency' must be boolean.")
204 self.traffic_profile[u"latency"] = False
206 u"The optional parameter 'latency' is not defined. No problem."
209 # Level 4: Optional params: Check if timeout is present:
210 if u"timeout" in self.traffic_profile:
211 if not self._validate_int_param(u"timeout", 1):
214 self.traffic_profile[u"timeout"] = None
216 u"The optional parameter 'timeout' is not defined. No problem."
221 def _validate_dependencies(self):
222 """Validate dependencies between parameters
224 :returns: whether dependencies between parameters are acceptable
227 # Level 5: Check urls and cpus:
228 if self.traffic_profile[u"cpus"] % len(self.traffic_profile[u"urls"]):
230 u"The number of CPUs must be a multiple of the number of URLs."
235 def _validate_int_param(self, param, minimum):
236 """Validate that an int parameter is set acceptably
237 If it is not an int already but a string, convert and store it as int.
239 :param param: Name of a traffic profile parameter
240 :param minimum: The minimum value for the named parameter
243 :returns: whether param is set to an int of at least minimum value
246 value = self._traffic_profile[param]
247 if isinstance(value, str):
252 if isinstance(value, int) and value >= minimum:
253 self.traffic_profile[param] = value
256 f"The parameter '{param}' must be an integer and at least {minimum}"
261 def profile_name(self):
262 """Getter - Profile name.
264 :returns: The traffic profile file path
267 return self._profile_name
270 def profile_name(self, profile_name):
274 :type profile_name: str
276 self._profile_name = profile_name
279 def traffic_profile(self):
280 """Getter: Traffic profile.
282 :returns: The traffic profile.
285 return self._traffic_profile
287 @traffic_profile.setter
288 def traffic_profile(self, profile):
289 """Setter - Traffic profile.
291 :param profile: The new traffic profile.
294 self._traffic_profile = profile