Autogen: Generate also NIC drivers.
[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 safe_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, u"rt") as profile_file:
65                 self.traffic_profile = safe_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