Merge "tests: L3, L4 and VxLAN tests for OVS & VPP"
[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) or isinstance(param, 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 load_from_file(self, path):
128         """Update ``settings`` with values found in module at ``path``.
129         """
130         import imp
131
132         custom_settings = imp.load_source('custom_settings', path)
133
134         for key in dir(custom_settings):
135             if getattr(custom_settings, key) is not None:
136                 setattr(self, key, getattr(custom_settings, key))
137
138     def load_from_dir(self, dir_path):
139         """Update ``settings`` with contents of the .conf files at ``path``.
140
141         Each file must be named Nfilename.conf, where N is a single or
142         multi-digit decimal number.  The files are loaded in ascending order of
143         N - so if a configuration item exists in more that one file the setting
144         in the file with the largest value of N takes precedence.
145
146         :param dir_path: The full path to the dir from which to load the .conf
147             files.
148
149         :returns: None
150         """
151         regex = re.compile("^(?P<digit_part>[0-9]+)(?P<alfa_part>[a-z]?)_.*.conf$")
152
153         def get_prefix(filename):
154             """
155             Provide a suitable function for sort's key arg
156             """
157             match_object = regex.search(os.path.basename(filename))
158             return [int(match_object.group('digit_part')),
159                     match_object.group('alfa_part')]
160
161         # get full file path to all files & dirs in dir_path
162         file_paths = os.listdir(dir_path)
163         file_paths = [os.path.join(dir_path, x) for x in file_paths]
164
165         # filter to get only those that are a files, with a leading
166         # digit and end in '.conf'
167         file_paths = [x for x in file_paths if os.path.isfile(x) and
168                       regex.search(os.path.basename(x))]
169
170         # sort ascending on the leading digits and afla (e.g. 03_, 05a_)
171         file_paths.sort(key=get_prefix)
172
173         # load settings from each file in turn
174         for filepath in file_paths:
175             self.load_from_file(filepath)
176
177     def load_from_dict(self, conf):
178         """
179         Update ``settings`` with values found in ``conf``.
180
181         Unlike the other loaders, this is case insensitive.
182         """
183         for key in conf:
184             if conf[key] is not None:
185                 if isinstance(conf[key], dict):
186                     # recursively update dict items, e.g. TEST_PARAMS
187                     setattr(self, key.upper(),
188                             merge_spec(getattr(self, key.upper()), conf[key]))
189                 else:
190                     setattr(self, key.upper(), conf[key])
191
192     def load_from_env(self):
193         """
194         Update ``settings`` with values found in the environment.
195         """
196         for key in os.environ:
197             setattr(self, key, os.environ[key])
198
199     def check_test_params(self):
200         """
201         Check all parameters defined inside TEST_PARAMS for their
202         existence. In case that non existing vsperf parmeter name
203         is detected, then VSPER will raise a runtime error.
204         """
205         unknown_keys = []
206         for key in settings.getValue('TEST_PARAMS'):
207             if key == 'TEST_PARAMS':
208                 raise RuntimeError('It is not allowed to define TEST_PARAMS '
209                                    'as a test parameter')
210             if key not in self.__dict__ and key not in _EXTRA_TEST_PARAMS:
211                 unknown_keys.append(key)
212
213         if len(unknown_keys):
214             raise RuntimeError('Test parameters contain unknown configuration '
215                                'parameter(s): {}'.format(', '.join(unknown_keys)))
216
217     def check_vm_settings(self, vm_number):
218         """
219         Check all VM related settings starting with GUEST_ prefix.
220         If it is not available for defined number of VMs, then vsperf
221         will try to expand it automatically. Expansion is performed
222         also in case that first list item contains a macro.
223         """
224         for key in self.__dict__:
225             if key.startswith('GUEST_'):
226                 value = self.getValue(key)
227                 if isinstance(value, str) and str(value).find('#') >= 0:
228                     self._expand_vm_settings(key, 1)
229
230                 if isinstance(value, list):
231                     if len(value) < vm_number or str(value[0]).find('#') >= 0:
232                         # expand configuration for all VMs
233                         self._expand_vm_settings(key, vm_number)
234
235     def _expand_vm_settings(self, key, vm_number):
236         """
237         Expand VM option with given key for given number of VMs
238         """
239         tmp_value = self.getValue(key)
240         if isinstance(tmp_value, str):
241             scalar = True
242             master_value = tmp_value
243             tmp_value = [tmp_value]
244         else:
245             scalar = False
246             master_value = tmp_value[0]
247
248         master_value_str = str(master_value)
249         if master_value_str.find('#') >= 0:
250             self.__dict__[key] = []
251             for vmindex in range(vm_number):
252                 value = master_value_str.replace('#VMINDEX', str(vmindex))
253                 for macro, args, param, _, step in re.findall(_PARSE_PATTERN, value):
254                     multi = int(step) if len(step) and int(step) else 1
255                     if macro == '#EVAL':
256                         # pylint: disable=eval-used
257                         tmp_result = str(eval(param))
258                     elif macro == '#MAC':
259                         mac_value = netaddr.EUI(param).value
260                         mac = netaddr.EUI(mac_value + vmindex * multi)
261                         mac.dialect = netaddr.mac_unix_expanded
262                         tmp_result = str(mac)
263                     elif macro == '#IP':
264                         ip_value = netaddr.IPAddress(param).value
265                         tmp_result = str(netaddr.IPAddress(ip_value + vmindex * multi))
266                     else:
267                         raise RuntimeError('Unknown configuration macro {} in {}'.format(macro, key))
268
269                     value = value.replace("{}{}".format(macro, args), tmp_result)
270
271                 # retype value to original type if needed
272                 if not isinstance(master_value, str):
273                     value = ast.literal_eval(value)
274                 self.__dict__[key].append(value)
275         else:
276             for vmindex in range(len(tmp_value), vm_number):
277                 self.__dict__[key].append(master_value)
278
279         if scalar:
280             self.__dict__[key] = self.__dict__[key][0]
281
282         _LOGGER.debug("Expanding option: %s = %s", key, self.__dict__[key])
283
284     def __str__(self):
285         """Provide settings as a human-readable string.
286
287         This can be useful for debug.
288
289         Returns:
290             A human-readable string.
291         """
292         tmp_dict = {}
293         for key in self.__dict__:
294             tmp_dict[key] = self.getValue(key)
295
296         return pprint.pformat(tmp_dict)
297
298     #
299     # validation methods used by step driven testcases
300     #
301     def validate_getValue(self, result, attr):
302         """Verifies, that correct value was returned
303         """
304         # getValue must be called to expand macros and apply
305         # values from TEST_PARAM option
306         assert result == self.getValue(attr)
307         return True
308
309     def validate_setValue(self, dummy_result, name, value):
310         """Verifies, that value was correctly set
311         """
312         assert value == self.__dict__[name]
313         return True
314
315 settings = Settings()
316
317 def get_test_param(key, default=None):
318     """Retrieve value for test param ``key`` if available.
319
320     :param key: Key to retrieve from test params.
321     :param default: Default to return if key not found.
322
323     :returns: Value for ``key`` if found, else ``default``.
324     """
325     test_params = settings.getValue('TEST_PARAMS')
326     return test_params.get(key, default) if test_params else default
327
328 def merge_spec(orig, new):
329     """Merges ``new`` dict with ``orig`` dict, and returns orig.
330
331     This takes into account nested dictionaries. Example:
332
333         >>> old = {'foo': 1, 'bar': {'foo': 2, 'bar': 3}}
334         >>> new = {'foo': 6, 'bar': {'foo': 7}}
335         >>> merge_spec(old, new)
336         {'foo': 6, 'bar': {'foo': 7, 'bar': 3}}
337
338     You'll notice that ``bar.bar`` is not removed. This is the desired result.
339     """
340     for key in orig:
341         if key not in new:
342             continue
343
344         # Not allowing derived dictionary types for now
345         # pylint: disable=unidiomatic-typecheck
346         if type(orig[key]) == dict:
347             orig[key] = merge_spec(orig[key], new[key])
348         else:
349             orig[key] = new[key]
350
351     for key in new:
352         if key not in orig:
353             orig[key] = new[key]
354
355     return orig