integration: fix ovsdpdk_mq_pvp_rxqs_testpmd
[vswitchperf.git] / conf / __init__.py
1 # Copyright 2015-2017 Intel Corporation.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #   http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """Settings and configuration handlers.
16
17 Settings will be loaded from several .conf files
18 and any user provided settings file.
19 """
20
21 # pylint: disable=invalid-name
22
23 import copy
24 import os
25 import re
26 import logging
27 import pprint
28 import ast
29 import netaddr
30
31 _LOGGER = logging.getLogger(__name__)
32
33 # Special test parameters which are not part of standard VSPERF configuration
34 _EXTRA_TEST_PARAMS = ['TUNNEL_TYPE']
35
36 # regex to parse configuration macros from 04_vnf.conf
37 # it will select all patterns starting with # sign
38 # and returns macro parameters and step
39 # examples of valid macros:
40 #   #VMINDEX
41 #   #MAC(AA:BB:CC:DD:EE:FF) or #MAC(AA:BB:CC:DD:EE:FF,2)
42 #   #IP(192.168.1.2) or #IP(192.168.1.2,2)
43 #   #EVAL(2*#VMINDEX)
44 _PARSE_PATTERN = r'(#[A-Z]+)(\(([^(),]+)(,([0-9]+))?\))?'
45
46 class Settings(object):
47     """Holding class for settings.
48     """
49     def __init__(self):
50         pass
51
52     def _eval_param(self, param):
53         # pylint: disable=invalid-name
54         """ Helper function for expansion of references to vsperf parameters
55         """
56         if isinstance(param, str):
57             # evaluate every #PARAM reference inside parameter itself
58             macros = re.findall(r'#PARAM\((([\w\-]+)(\[[\w\[\]\-\'\"]+\])*)\)', param)
59             if macros:
60                 for macro in macros:
61                     # pylint: disable=eval-used
62                     try:
63                         tmp_val = str(eval("self.getValue('{}'){}".format(macro[1], macro[2])))
64                         param = param.replace('#PARAM({})'.format(macro[0]), tmp_val)
65                     # silently ignore that option required by PARAM macro can't be evaluated;
66                     # It is possible, that referred parameter will be constructed during runtime
67                     # and re-read later.
68                     except IndexError:
69                         pass
70                     except AttributeError:
71                         pass
72             return param
73         elif isinstance(param, (list, tuple)):
74             tmp_list = []
75             for item in param:
76                 tmp_list.append(self._eval_param(item))
77             return tmp_list
78         elif isinstance(param, dict):
79             tmp_dict = {}
80             for (key, value) in param.items():
81                 tmp_dict[key] = self._eval_param(value)
82             return tmp_dict
83         else:
84             return param
85
86     def getValue(self, attr):
87         """Return a settings item value
88         """
89         if attr in self.__dict__:
90             if attr == 'TEST_PARAMS':
91                 return getattr(self, attr)
92             else:
93                 master_value = getattr(self, attr)
94                 # Check if parameter value was modified by CLI option
95                 cli_value = get_test_param(attr, None)
96                 if cli_value is not None:
97                     # TRAFFIC dictionary is not overridden by CLI option
98                     # but only updated by specified values
99                     if attr == 'TRAFFIC':
100                         tmp_value = copy.deepcopy(master_value)
101                         tmp_value = merge_spec(tmp_value, cli_value)
102                         return self._eval_param(tmp_value)
103                     else:
104                         return self._eval_param(cli_value)
105                 else:
106                     return self._eval_param(master_value)
107         else:
108             raise AttributeError("%r object has no attribute %r" %
109                                  (self.__class__, attr))
110
111     def __setattr__(self, name, value):
112         """Set a value
113         """
114         # skip non-settings. this should exclude built-ins amongst others
115         if not name.isupper():
116             return
117
118         # we can assume all uppercase keys are valid settings
119         super(Settings, self).__setattr__(name, value)
120
121     def setValue(self, name, value):
122         """Set a value
123         """
124         if name is not None and value is not None:
125             super(Settings, self).__setattr__(name, value)
126
127     def resetValue(self, attr):
128         """If parameter was overridden by TEST_PARAMS, then it will
129            be set to its original value.
130         """
131         if attr in self.__dict__['TEST_PARAMS']:
132             self.__dict__['TEST_PARAMS'].pop(attr)
133
134     def load_from_file(self, path):
135         """Update ``settings`` with values found in module at ``path``.
136         """
137         import imp
138
139         custom_settings = imp.load_source('custom_settings', path)
140
141         for key in dir(custom_settings):
142             if getattr(custom_settings, key) is not None:
143                 setattr(self, key, getattr(custom_settings, key))
144
145     def load_from_dir(self, dir_path):
146         """Update ``settings`` with contents of the .conf files at ``path``.
147
148         Each file must be named Nfilename.conf, where N is a single or
149         multi-digit decimal number.  The files are loaded in ascending order of
150         N - so if a configuration item exists in more that one file the setting
151         in the file with the largest value of N takes precedence.
152
153         :param dir_path: The full path to the dir from which to load the .conf
154             files.
155
156         :returns: None
157         """
158         regex = re.compile("^(?P<digit_part>[0-9]+)(?P<alfa_part>[a-z]?)_.*.conf$")
159
160         def get_prefix(filename):
161             """
162             Provide a suitable function for sort's key arg
163             """
164             match_object = regex.search(os.path.basename(filename))
165             return [int(match_object.group('digit_part')),
166                     match_object.group('alfa_part')]
167
168         # get full file path to all files & dirs in dir_path
169         file_paths = os.listdir(dir_path)
170         file_paths = [os.path.join(dir_path, x) for x in file_paths]
171
172         # filter to get only those that are a files, with a leading
173         # digit and end in '.conf'
174         file_paths = [x for x in file_paths if os.path.isfile(x) and
175                       regex.search(os.path.basename(x))]
176
177         # sort ascending on the leading digits and afla (e.g. 03_, 05a_)
178         file_paths.sort(key=get_prefix)
179
180         # load settings from each file in turn
181         for filepath in file_paths:
182             self.load_from_file(filepath)
183
184     def load_from_dict(self, conf):
185         """
186         Update ``settings`` with values found in ``conf``.
187
188         Unlike the other loaders, this is case insensitive.
189         """
190         for key in conf:
191             if conf[key] is not None:
192                 if isinstance(conf[key], dict):
193                     # recursively update dict items, e.g. TEST_PARAMS
194                     setattr(self, key.upper(),
195                             merge_spec(getattr(self, key.upper()), conf[key]))
196                 else:
197                     setattr(self, key.upper(), conf[key])
198
199     def restore_from_dict(self, conf):
200         """
201         Restore ``settings`` with values found in ``conf``.
202
203         Method will drop all configuration options and restore their
204         values from conf dictionary
205         """
206         self.__dict__.clear()
207         tmp_conf = copy.deepcopy(conf)
208         for key in tmp_conf:
209             self.setValue(key, tmp_conf[key])
210
211     def load_from_env(self):
212         """
213         Update ``settings`` with values found in the environment.
214         """
215         for key in os.environ:
216             setattr(self, key, os.environ[key])
217
218     def check_test_params(self):
219         """
220         Check all parameters defined inside TEST_PARAMS for their
221         existence. In case that non existing vsperf parmeter name
222         is detected, then VSPER will raise a runtime error.
223         """
224         unknown_keys = []
225         for key in settings.getValue('TEST_PARAMS'):
226             if key == 'TEST_PARAMS':
227                 raise RuntimeError('It is not allowed to define TEST_PARAMS '
228                                    'as a test parameter')
229             if key not in self.__dict__ and key not in _EXTRA_TEST_PARAMS:
230                 unknown_keys.append(key)
231
232         if unknown_keys:
233             raise RuntimeError('Test parameters contain unknown configuration '
234                                'parameter(s): {}'.format(', '.join(unknown_keys)))
235
236     def check_vm_settings(self, vm_number):
237         """
238         Check all VM related settings starting with GUEST_ prefix.
239         If it is not available for defined number of VMs, then vsperf
240         will try to expand it automatically. Expansion is performed
241         also in case that first list item contains a macro.
242         """
243         for key in self.__dict__:
244             if key.startswith('GUEST_'):
245                 value = self.getValue(key)
246                 if isinstance(value, str) and str(value).find('#') >= 0:
247                     self._expand_vm_settings(key, 1)
248
249                 if isinstance(value, list):
250                     if len(value) < vm_number or str(value[0]).find('#') >= 0:
251                         # expand configuration for all VMs
252                         self._expand_vm_settings(key, vm_number)
253
254     def _expand_vm_settings(self, key, vm_number):
255         """
256         Expand VM option with given key for given number of VMs
257         """
258         tmp_value = self.getValue(key)
259         # skip empty/not set value
260         if not tmp_value:
261             return
262         if isinstance(tmp_value, str):
263             scalar = True
264             master_value = tmp_value
265             tmp_value = [tmp_value]
266         else:
267             scalar = False
268             master_value = tmp_value[0]
269
270         master_value_str = str(master_value)
271         if master_value_str.find('#') >= 0:
272             self.__dict__[key] = []
273             for vmindex in range(vm_number):
274                 value = master_value_str.replace('#VMINDEX', str(vmindex))
275                 for macro, args, param, _, step in re.findall(_PARSE_PATTERN, value):
276                     multi = int(step) if step and int(step) else 1
277                     if macro == '#EVAL':
278                         # pylint: disable=eval-used
279                         tmp_result = str(eval(param))
280                     elif macro == '#MAC':
281                         mac_value = netaddr.EUI(param).value
282                         mac = netaddr.EUI(mac_value + vmindex * multi)
283                         mac.dialect = netaddr.mac_unix_expanded
284                         tmp_result = str(mac)
285                     elif macro == '#IP':
286                         ip_value = netaddr.IPAddress(param).value
287                         tmp_result = str(netaddr.IPAddress(ip_value + vmindex * multi))
288                     else:
289                         raise RuntimeError('Unknown configuration macro {} in {}'.format(macro, key))
290
291                     value = value.replace("{}{}".format(macro, args), tmp_result)
292
293                 # retype value to original type if needed
294                 if not isinstance(master_value, str):
295                     value = ast.literal_eval(value)
296                 self.__dict__[key].append(value)
297         else:
298             for vmindex in range(len(tmp_value), vm_number):
299                 self.__dict__[key].append(master_value)
300
301         if scalar:
302             self.__dict__[key] = self.__dict__[key][0]
303
304         _LOGGER.debug("Expanding option: %s = %s", key, self.__dict__[key])
305
306     def __str__(self):
307         """Provide settings as a human-readable string.
308
309         This can be useful for debug.
310
311         Returns:
312             A human-readable string.
313         """
314         tmp_dict = {}
315         for key in self.__dict__:
316             tmp_dict[key] = self.getValue(key)
317
318         return pprint.pformat(tmp_dict)
319
320     #
321     # validation methods used by step driven testcases
322     #
323     def validate_getValue(self, result, attr):
324         """Verifies, that correct value was returned
325         """
326         # getValue must be called to expand macros and apply
327         # values from TEST_PARAM option
328         assert result == self.getValue(attr)
329         return True
330
331     def validate_setValue(self, _dummy_result, name, value):
332         """Verifies, that value was correctly set
333         """
334         assert value == self.__dict__[name]
335         return True
336
337     def validate_resetValue(self, _dummy_result, attr):
338         """Verifies, that value was correctly reset
339         """
340         return 'TEST_PARAMS' not in self.__dict__ or \
341                attr not in self.__dict__['TEST_PARAMS']
342
343 settings = Settings()
344
345 def get_test_param(key, default=None):
346     """Retrieve value for test param ``key`` if available.
347
348     :param key: Key to retrieve from test params.
349     :param default: Default to return if key not found.
350
351     :returns: Value for ``key`` if found, else ``default``.
352     """
353     test_params = settings.getValue('TEST_PARAMS')
354     return test_params.get(key, default) if test_params else default
355
356 def merge_spec(orig, new):
357     """Merges ``new`` dict with ``orig`` dict, and returns orig.
358
359     This takes into account nested dictionaries. Example:
360
361         >>> old = {'foo': 1, 'bar': {'foo': 2, 'bar': 3}}
362         >>> new = {'foo': 6, 'bar': {'foo': 7}}
363         >>> merge_spec(old, new)
364         {'foo': 6, 'bar': {'foo': 7, 'bar': 3}}
365
366     You'll notice that ``bar.bar`` is not removed. This is the desired result.
367     """
368     for key in orig:
369         if key not in new:
370             continue
371
372         # Not allowing derived dictionary types for now
373         # pylint: disable=unidiomatic-typecheck
374         if type(orig[key]) == dict:
375             orig[key] = merge_spec(orig[key], new[key])
376         else:
377             orig[key] = new[key]
378
379     for key in new:
380         if key not in orig:
381             orig[key] = new[key]
382
383     return orig