e1f8365345754bd1f551714e36a95f47011e7ce8
[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     def __init__(self, profile_name):
41         """Read the traffic profile from the yaml file.
42
43         :param profile_name: Path to the yaml file with the profile.
44         :type profile_name: str
45         :raises: WrkError if it is not possible to parse the profile.
46         """
47
48         self._profile_name = None
49         self._traffic_profile = None
50
51         self.profile_name = profile_name
52
53         try:
54             with open(self.profile_name, 'r') as profile_file:
55                 self.traffic_profile = load(profile_file)
56         except IOError as err:
57             raise WrkError(msg="An error occurred while opening the file '{0}'."
58                            .format(self.profile_name),
59                            details=str(err))
60         except YAMLError as err:
61             raise WrkError(msg="An error occurred while parsing the traffic "
62                                "profile '{0}'.".format(self.profile_name),
63                            details=str(err))
64
65         self._validate_traffic_profile()
66
67         if self.traffic_profile:
68             logger.debug("\nThe wrk traffic profile '{0}' is valid.\n".
69                          format(self.profile_name))
70             logger.debug("wrk traffic profile '{0}':".format(self.profile_name))
71             logger.debug(pformat(self.traffic_profile))
72         else:
73             logger.debug("\nThe wrk traffic profile '{0}' is invalid.\n".
74                          format(self.profile_name))
75             raise WrkError("\nThe wrk traffic profile '{0}' is invalid.\n".
76                            format(self.profile_name))
77
78     def __repr__(self):
79         return pformat(self.traffic_profile)
80
81     def __str__(self):
82         return pformat(self.traffic_profile)
83
84     def _validate_traffic_profile(self):
85         """Validate the traffic profile.
86
87         The specification, the structure and the rules are described in
88         doc/wrk_lld.rst
89         """
90
91         logger.debug("\nValidating the wrk traffic profile '{0}'...\n".
92                      format(self.profile_name))
93
94         # Level 1: Check if the profile is a dictionary:
95         if not isinstance(self.traffic_profile, dict):
96             logger.error("The wrk traffic profile must be a dictionary.")
97             self.traffic_profile = None
98             return
99
100         # Level 2: Check if all mandatory parameters are present:
101         is_valid = True
102         for param in self.MANDATORY_PARAMS:
103             if self.traffic_profile.get(param, None) is None:
104                 logger.error("The parameter '{0}' in mandatory.".format(param))
105                 is_valid = False
106         if not is_valid:
107             self.traffic_profile = None
108             return
109
110         # Level 3: Mandatory params: Check if urls is a list:
111         is_valid = True
112         if not isinstance(self.traffic_profile["urls"], list):
113             logger.error("The parameter 'urls' must be a list.")
114             is_valid = False
115
116         # Level 3: Mandatory params: Check if cpus is a valid integer:
117         try:
118             cpus = int(self.traffic_profile["cpus"])
119             if cpus < 1:
120                 raise ValueError
121             self.traffic_profile["cpus"] = cpus
122         except ValueError:
123             logger.error("The parameter 'cpus' must be an integer greater than "
124                          "1.")
125             is_valid = False
126
127         # Level 3: Mandatory params: Check if first-cpu is a valid integer:
128         try:
129             first_cpu = int(self.traffic_profile["first-cpu"])
130             if first_cpu < 0:
131                 raise ValueError
132             self.traffic_profile["first-cpu"] = first_cpu
133         except ValueError:
134             logger.error("The parameter 'first-cpu' must be an integer greater "
135                          "than 1.")
136             is_valid = False
137
138         # Level 3: Mandatory params: Check if duration is a valid integer:
139         try:
140             duration = int(self.traffic_profile["duration"])
141             if duration < 1:
142                 raise ValueError
143             self.traffic_profile["duration"] = duration
144         except ValueError:
145             logger.error("The parameter 'duration' must be an integer "
146                          "greater than 1.")
147             is_valid = False
148
149         # Level 3: Mandatory params: Check if nr-of-threads is a valid integer:
150         try:
151             nr_of_threads = int(self.traffic_profile["nr-of-threads"])
152             if nr_of_threads < 1:
153                 raise ValueError
154             self.traffic_profile["nr-of-threads"] = nr_of_threads
155         except ValueError:
156             logger.error("The parameter 'nr-of-threads' must be an integer "
157                          "greater than 1.")
158             is_valid = False
159
160         # Level 3: Mandatory params: Check if nr-of-connections is a valid
161         # integer:
162         try:
163             nr_of_connections = int(self.traffic_profile["nr-of-connections"])
164             if nr_of_connections < 1:
165                 raise ValueError
166             self.traffic_profile["nr-of-connections"] = nr_of_connections
167         except ValueError:
168             logger.error("The parameter 'nr-of-connections' must be an integer "
169                          "greater than 1.")
170             is_valid = False
171
172         # Level 4: Optional params: Check if script is present:
173         script = self.traffic_profile.get("script", None)
174         if script is not None:
175             if not isinstance(script, str):
176                 logger.error("The path to LuaJIT script in invalid")
177                 is_valid = False
178             else:
179                 if not isfile(script):
180                     logger.error("The file '{0}' in not present.".
181                                  format(script))
182                     is_valid = False
183         else:
184             self.traffic_profile["script"] = None
185             logger.debug("The optional parameter 'LuaJIT script' is not "
186                          "defined. No problem.")
187
188         # Level 4: Optional params: Check if header is present:
189         header = self.traffic_profile.get("header", None)
190         if header:
191             if not (isinstance(header, dict) or isinstance(header, str)):
192                 logger.error("The parameter 'header' is not valid.")
193                 is_valid = False
194             else:
195                 if isinstance(header, dict):
196                     header_lst = list()
197                     for key, val in header.items():
198                         header_lst.append("{0}: {1}".format(key, val))
199                     if header_lst:
200                         self.traffic_profile["header"] = ", ".join(header_lst)
201                     else:
202                         logger.error("The parameter 'header' is defined but "
203                                      "empty.")
204                         is_valid = False
205         else:
206             self.traffic_profile["header"] = None
207             logger.debug("The optional parameter 'header' is not defined. "
208                          "No problem.")
209
210         # Level 4: Optional params: Check if latency is present:
211         latency = self.traffic_profile.get("latency", None)
212         if latency is not None:
213             try:
214                 latency = bool(latency)
215                 self.traffic_profile["latency"] = latency
216             except ValueError:
217                 logger.error("The parameter 'latency' must be boolean.")
218                 is_valid = False
219         else:
220             self.traffic_profile["latency"] = False
221             logger.debug("The optional parameter 'latency' is not defined. "
222                          "No problem.")
223
224         # Level 4: Optional params: Check if timeout is present:
225         timeout = self.traffic_profile.get("timeout", None)
226         if timeout:
227             try:
228                 timeout = int(timeout)
229                 if timeout < 1:
230                     raise ValueError
231                 self.traffic_profile["timeout"] = timeout
232             except ValueError:
233                 logger.error("The parameter 'timeout' must be integer greater "
234                              "than 1.")
235                 is_valid = False
236         else:
237             self.traffic_profile["timeout"] = None
238             logger.debug("The optional parameter 'timeout' is not defined. "
239                          "No problem.")
240
241         if not is_valid:
242             self.traffic_profile = None
243             return
244
245         # Level 5: Check dependencies between parameters:
246         # Level 5: Check urls and cpus:
247         if self.traffic_profile["cpus"] % len(self.traffic_profile["urls"]):
248             logger.error("The number of CPUs must be a multiplication of the "
249                          "number of URLs.")
250             self.traffic_profile = None
251
252     @property
253     def profile_name(self):
254         """Getter - Profile name.
255
256         :returns: The traffic profile file path
257         :rtype: str
258         """
259         return self._profile_name
260
261     @profile_name.setter
262     def profile_name(self, profile_name):
263         """
264
265         :param profile_name:
266         :type profile_name: str
267         """
268         self._profile_name = profile_name
269
270     @property
271     def traffic_profile(self):
272         """Getter: Traffic profile.
273
274         :returns: The traffic profile.
275         :rtype: dict
276         """
277         return self._traffic_profile
278
279     @traffic_profile.setter
280     def traffic_profile(self, profile):
281         """Setter - Traffic profile.
282
283         :param profile: The new traffic profile.
284         :type profile: dict
285         """
286         self._traffic_profile = profile