Python3: resources and libraries
[csit.git] / resources / tools / wrk / wrk_traffic_profile_parser.py
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
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:
30     """The wrk traffic profile.
31     """
32
33     MANDATORY_PARAMS = (
34         u"urls",
35         u"first-cpu",
36         u"cpus",
37         u"duration",
38         u"nr-of-threads",
39         u"nr-of-connections"
40     )
41
42     INTEGER_PARAMS = (
43         (u"cpus", 1),
44         (u"first-cpu", 0),
45         (u"duration", 1),
46         (u"nr-of-threads", 1),
47         (u"nr-of-connections", 1)
48     )
49
50     def __init__(self, profile_name):
51         """Read the traffic profile from the yaml file.
52
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.
56         """
57
58         self._profile_name = None
59         self._traffic_profile = None
60
61         self.profile_name = profile_name
62
63         try:
64             with open(self.profile_name, "r") as profile_file:
65                 self.traffic_profile = load(profile_file)
66         except IOError as err:
67             raise WrkError(
68                 msg=f"An error occurred while opening the file "
69                 f"'{self.profile_name}'.", details=str(err)
70             )
71         except YAMLError as err:
72             raise WrkError(
73                 msg=f"An error occurred while parsing the traffic profile "
74                 f"'{self.profile_name}'.", details=str(err)
75             )
76
77         self._validate_traffic_profile()
78
79         if self.traffic_profile:
80             logger.debug(
81                 f"\nThe wrk traffic profile '{self.profile_name}' is valid.\n"
82             )
83             logger.debug(f"wrk traffic profile '{self.profile_name}':")
84             logger.debug(pformat(self.traffic_profile))
85         else:
86             logger.debug(
87                 f"\nThe wrk traffic profile '{self.profile_name}' is invalid.\n"
88             )
89             raise WrkError(
90                 f"\nThe wrk traffic profile '{self.profile_name}' is invalid.\n"
91             )
92
93     def __repr__(self):
94         return pformat(self.traffic_profile)
95
96     def __str__(self):
97         return pformat(self.traffic_profile)
98
99     def _validate_traffic_profile(self):
100         """Validate the traffic profile.
101
102         The specification, the structure and the rules are described in
103         doc/wrk_lld.rst
104         """
105
106         logger.debug(
107             f"\nValidating the wrk traffic profile '{self.profile_name}'...\n"
108         )
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
114
115     def _validate_mandatory_structure(self):
116         """Validate presence of mandatory parameters in trafic profile dict
117
118         :returns: whether mandatory structure is followed by the profile
119         :rtype: bool
120         """
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.")
124             return False
125
126         # Level 2: Check if all mandatory parameters are present:
127         is_valid = True
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.")
131                 is_valid = False
132         return is_valid
133
134     def _validate_mandatory_values(self):
135         """Validate that mandatory profile values satisfy their constraints
136
137         :returns: whether mandatory values are acceptable
138         :rtype: bool
139         """
140         # Level 3: Mandatory params: Check if urls is a list:
141         is_valid = True
142         if not isinstance(self.traffic_profile[u"urls"], list):
143             logger.error(u"The parameter 'urls' must be a list.")
144             is_valid = False
145
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):
149                 is_valid = False
150         return is_valid
151
152     def _validate_optional_values(self):
153         """Validate values for optional parameters, if present
154
155         :returns: whether present optional values are acceptable
156         :rtype: bool
157         """
158         is_valid = True
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")
164                 is_valid = False
165             else:
166                 if not isfile(script):
167                     logger.error(f"The file '{script}' does not exist.")
168                     is_valid = False
169         else:
170             self.traffic_profile[u"script"] = None
171             logger.debug(
172                 u"The optional parameter 'LuaJIT script' is not defined. "
173                 u"No problem."
174             )
175
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):
180                 header = u", ".join(
181                     f"{0}: {1}".format(*item) for item in header.items()
182                 )
183                 self.traffic_profile[u"header"] = header
184             elif not isinstance(header, str):
185                 logger.error(u"The parameter 'header' type is not valid.")
186                 is_valid = False
187
188             if not header:
189                 logger.error(u"The parameter 'header' is defined but empty.")
190                 is_valid = False
191         else:
192             self.traffic_profile[u"header"] = None
193             logger.debug(
194                 u"The optional parameter 'header' is not defined. No problem."
195             )
196
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.")
202                 is_valid = False
203         else:
204             self.traffic_profile[u"latency"] = False
205             logger.debug(
206                 u"The optional parameter 'latency' is not defined. No problem."
207             )
208
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):
212                 is_valid = False
213         else:
214             self.traffic_profile[u"timeout"] = None
215             logger.debug(
216                 u"The optional parameter 'timeout' is not defined. No problem."
217             )
218
219         return is_valid
220
221     def _validate_dependencies(self):
222         """Validate dependencies between parameters
223
224         :returns: whether dependencies between parameters are acceptable
225         :rtype: bool
226         """
227         # Level 5: Check urls and cpus:
228         if self.traffic_profile[u"cpus"] % len(self.traffic_profile[u"urls"]):
229             logger.error(
230                 u"The number of CPUs must be a multiple of the number of URLs."
231             )
232             return False
233         return True
234
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.
238
239         :param param: Name of a traffic profile parameter
240         :param minimum: The minimum value for the named parameter
241         :type param: str
242         :type minimum: int
243         :returns: whether param is set to an int of at least minimum value
244         :rtype: bool
245         """
246         value = self._traffic_profile[param]
247         if isinstance(value, str):
248             if value.isdigit():
249                 value = int(value)
250             else:
251                 value = minimum - 1
252         if isinstance(value, int) and value >= minimum:
253             self.traffic_profile[param] = value
254             return True
255         logger.error(
256             f"The parameter '{param}' must be an integer and at least {minimum}"
257         )
258         return False
259
260     @property
261     def profile_name(self):
262         """Getter - Profile name.
263
264         :returns: The traffic profile file path
265         :rtype: str
266         """
267         return self._profile_name
268
269     @profile_name.setter
270     def profile_name(self, profile_name):
271         """
272
273         :param profile_name:
274         :type profile_name: str
275         """
276         self._profile_name = profile_name
277
278     @property
279     def traffic_profile(self):
280         """Getter: Traffic profile.
281
282         :returns: The traffic profile.
283         :rtype: dict
284         """
285         return self._traffic_profile
286
287     @traffic_profile.setter
288     def traffic_profile(self, profile):
289         """Setter - Traffic profile.
290
291         :param profile: The new traffic profile.
292         :type profile: dict
293         """
294         self._traffic_profile = profile