Refactor traffic profile verification
[csit.git] / resources / tools / wrk / wrk_traffic_profile_parser.py
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
4 # of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 """wrk traffic profile parser.
15
16 See LLD for the structure of a wrk traffic profile.
17 """
18
19
20 from os.path import isfile
21 from pprint import pformat
22
23 from yaml import load, YAMLError
24 from robot.api import logger
25
26 from resources.tools.wrk.wrk_errors import WrkError
27
28
29 class WrkTrafficProfile(object):
30     """The wrk traffic profile.
31     """
32
33     MANDATORY_PARAMS = ("urls",
34                         "first-cpu",
35                         "cpus",
36                         "duration",
37                         "nr-of-threads",
38                         "nr-of-connections")
39
40     INTEGER_PARAMS = (("cpus", 1),
41                       ("first-cpu", 0),
42                       ("duration", 1),
43                       ("nr-of-threads", 1),
44                       ("nr-of-connections", 1))
45
46     def __init__(self, profile_name):
47         """Read the traffic profile from the yaml file.
48
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.
52         """
53
54         self._profile_name = None
55         self._traffic_profile = None
56
57         self.profile_name = profile_name
58
59         try:
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),
65                            details=str(err))
66         except YAMLError as err:
67             raise WrkError(msg="An error occurred while parsing the traffic "
68                                "profile '{0}'.".format(self.profile_name),
69                            details=str(err))
70
71         self._validate_traffic_profile()
72
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))
78         else:
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))
83
84     def __repr__(self):
85         return pformat(self.traffic_profile)
86
87     def __str__(self):
88         return pformat(self.traffic_profile)
89
90     def _validate_traffic_profile(self):
91         """Validate the traffic profile.
92
93         The specification, the structure and the rules are described in
94         doc/wrk_lld.rst
95         """
96
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
104
105     def _validate_mandatory_structure(self):
106         """Validate presence of mandatory parameters in trafic profile dict
107
108         :returns: whether mandatory structure is followed by the profile
109         :rtype: bool
110         """
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.")
114             return False
115
116         # Level 2: Check if all mandatory parameters are present:
117         is_valid = True
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))
121                 is_valid = False
122         return is_valid
123
124     def _validate_mandatory_values(self):
125         """Validate that mandatory profile values satisfy their constraints
126
127         :returns: whether mandatory values are acceptable
128         :rtype: bool
129         """
130         # Level 3: Mandatory params: Check if urls is a list:
131         is_valid = True
132         if not isinstance(self.traffic_profile["urls"], list):
133             logger.error("The parameter 'urls' must be a list.")
134             is_valid = False
135
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):
139                 is_valid = False
140         return is_valid
141
142     def _validate_optional_values(self):
143         """Validate values for optional parameters, if present
144
145         :returns: whether present optional values are acceptable
146         :rtype: bool
147         """
148         is_valid = True
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")
154                 is_valid = False
155             else:
156                 if not isfile(script):
157                     logger.error("The file '{0}' does not exist.".
158                                  format(script))
159                     is_valid = False
160         else:
161             self.traffic_profile["script"] = None
162             logger.debug("The optional parameter 'LuaJIT script' is not "
163                          "defined. No problem.")
164
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.")
174                 is_valid = False
175
176             if not header:
177                 logger.error("The parameter 'header' is defined but "
178                              "empty.")
179                 is_valid = False
180         else:
181             self.traffic_profile["header"] = None
182             logger.debug("The optional parameter 'header' is not defined. "
183                          "No problem.")
184
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.")
190                 is_valid = False
191         else:
192             self.traffic_profile["latency"] = False
193             logger.debug("The optional parameter 'latency' is not defined. "
194                          "No problem.")
195
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):
199                 is_valid = False
200         else:
201             self.traffic_profile["timeout"] = None
202             logger.debug("The optional parameter 'timeout' is not defined. "
203                          "No problem.")
204
205         return is_valid
206
207     def _validate_dependencies(self):
208         """Validate dependencies between parameters
209
210         :returns: whether dependencies between parameters are acceptable
211         :rtype: bool
212         """
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 "
216                          "number of URLs.")
217             return False
218         return True
219
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.
223
224         :param param: Name of a traffic profile parameter
225         :param minimum: The minimum value for the named parameter
226         :type param: str
227         :type minimum: int
228         :returns: whether param is set to an int of at least minimum value
229         :rtype: bool
230         """
231         value = self._traffic_profile[param]
232         if isinstance(value, (str, unicode)):
233             if value.isdigit():
234                 value = int(value)
235             else:
236                 value = minimum - 1
237         if isinstance(value, int) and value >= minimum:
238             self.traffic_profile[param] = value
239             return True
240         logger.error("The parameter '{param}' must be an integer and "
241                      "at least {minimum}".format(param=param, minimum=minimum))
242         return False
243
244     @property
245     def profile_name(self):
246         """Getter - Profile name.
247
248         :returns: The traffic profile file path
249         :rtype: str
250         """
251         return self._profile_name
252
253     @profile_name.setter
254     def profile_name(self, profile_name):
255         """
256
257         :param profile_name:
258         :type profile_name: str
259         """
260         self._profile_name = profile_name
261
262     @property
263     def traffic_profile(self):
264         """Getter: Traffic profile.
265
266         :returns: The traffic profile.
267         :rtype: dict
268         """
269         return self._traffic_profile
270
271     @traffic_profile.setter
272     def traffic_profile(self, profile):
273         """Setter - Traffic profile.
274
275         :param profile: The new traffic profile.
276         :type profile: dict
277         """
278         self._traffic_profile = profile