documentation: Installation prerequisites
[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 restore_from_dict(self, conf):
193         """
194         Restore ``settings`` with values found in ``conf``.
195
196         Method will drop all configuration options and restore their
197         values from conf dictionary
198         """
199         self.__dict__.clear()
200         tmp_conf = copy.deepcopy(conf)
201         for key in tmp_conf:
202             self.setValue(key, tmp_conf[key])
203
204     def load_from_env(self):
205         """
206         Update ``settings`` with values found in the environment.
207         """
208         for key in os.environ:
209             setattr(self, key, os.environ[key])
210
211     def check_test_params(self):
212         """
213         Check all parameters defined inside TEST_PARAMS for their
214         existence. In case that non existing vsperf parmeter name
215         is detected, then VSPER will raise a runtime error.
216         """
217         unknown_keys = []
218         for key in settings.getValue('TEST_PARAMS'):
219             if key == 'TEST_PARAMS':
220                 raise RuntimeError('It is not allowed to define TEST_PARAMS '
221                                    'as a test parameter')
222             if key not in self.__dict__ and key not in _EXTRA_TEST_PARAMS:
223                 unknown_keys.append(key)
224
225         if len(unknown_keys):
226             raise RuntimeError('Test parameters contain unknown configuration '
227                                'parameter(s): {}'.format(', '.join(unknown_keys)))
228
229     def check_vm_settings(self, vm_number):
230         """
231         Check all VM related settings starting with GUEST_ prefix.
232         If it is not available for defined number of VMs, then vsperf
233         will try to expand it automatically. Expansion is performed
234         also in case that first list item contains a macro.
235         """
236         for key in self.__dict__:
237             if key.startswith('GUEST_'):
238                 value = self.getValue(key)
239                 if isinstance(value, str) and str(value).find('#') >= 0:
240                     self._expand_vm_settings(key, 1)
241
242                 if isinstance(value, list):
243                     if len(value) < vm_number or str(value[0]).find('#') >= 0:
244                         # expand configuration for all VMs
245                         self._expand_vm_settings(key, vm_number)
246
247     def _expand_vm_settings(self, key, vm_number):
248         """
249         Expand VM option with given key for given number of VMs
250         """
251         tmp_value = self.getValue(key)
252         if isinstance(tmp_value, str):
253             scalar = True
254             master_value = tmp_value
255             tmp_value = [tmp_value]
256         else:
257             scalar = False
258             master_value = tmp_value[0]
259
260         master_value_str = str(master_value)
261         if master_value_str.find('#') >= 0:
262             self.__dict__[key] = []
263             for vmindex in range(vm_number):
264                 value = master_value_str.replace('#VMINDEX', str(vmindex))
265                 for macro, args, param, _, step in re.findall(_PARSE_PATTERN, value):
266                     multi = int(step) if len(step) and int(step) else 1
267                     if macro == '#EVAL':
268                         # pylint: disable=eval-used
269                         tmp_result = str(eval(param))
270                     elif macro == '#MAC':
271                         mac_value = netaddr.EUI(param).value
272                         mac = netaddr.EUI(mac_value + vmindex * multi)
273                         mac.dialect = netaddr.mac_unix_expanded
274                         tmp_result = str(mac)
275                     elif macro == '#IP':
276                         ip_value = netaddr.IPAddress(param).value
277                         tmp_result = str(netaddr.IPAddress(ip_value + vmindex * multi))
278                     else:
279                         raise RuntimeError('Unknown configuration macro {} in {}'.format(macro, key))
280
281                     value = value.replace("{}{}".format(macro, args), tmp_result)
282
283                 # retype value to original type if needed
284                 if not isinstance(master_value, str):
285                     value = ast.literal_eval(value)
286                 self.__dict__[key].append(value)
287         else:
288             for vmindex in range(len(tmp_value), vm_number):
289                 self.__dict__[key].append(master_value)
290
291         if scalar:
292             self.__dict__[key] = self.__dict__[key][0]
293
294         _LOGGER.debug("Expanding option: %s = %s", key, self.__dict__[key])
295
296     def __str__(self):
297         """Provide settings as a human-readable string.
298
299         This can be useful for debug.
300
301         Returns:
302             A human-readable string.
303         """
304         tmp_dict = {}
305         for key in self.__dict__:
306             tmp_dict[key] = self.getValue(key)
307
308         return pprint.pformat(tmp_dict)
309
310     #
311     # validation methods used by step driven testcases
312     #
313     def validate_getValue(self, result, attr):
314         """Verifies, that correct value was returned
315         """
316         # getValue must be called to expand macros and apply
317         # values from TEST_PARAM option
318         assert result == self.getValue(attr)
319         return True
320
321     def validate_setValue(self, dummy_result, name, value):
322         """Verifies, that value was correctly set
323         """
324         assert value == self.__dict__[name]
325         return True
326
327 settings = Settings()
328
329 def get_test_param(key, default=None):
330     """Retrieve value for test param ``key`` if available.
331
332     :param key: Key to retrieve from test params.
333     :param default: Default to return if key not found.
334
335     :returns: Value for ``key`` if found, else ``default``.
336     """
337     test_params = settings.getValue('TEST_PARAMS')
338     return test_params.get(key, default) if test_params else default
339
340 def merge_spec(orig, new):
341     """Merges ``new`` dict with ``orig`` dict, and returns orig.
342
343     This takes into account nested dictionaries. Example:
344
345         >>> old = {'foo': 1, 'bar': {'foo': 2, 'bar': 3}}
346         >>> new = {'foo': 6, 'bar': {'foo': 7}}
347         >>> merge_spec(old, new)
348         {'foo': 6, 'bar': {'foo': 7, 'bar': 3}}
349
350     You'll notice that ``bar.bar`` is not removed. This is the desired result.
351     """
352     for key in orig:
353         if key not in new:
354             continue
355
356         # Not allowing derived dictionary types for now
357         # pylint: disable=unidiomatic-typecheck
358         if type(orig[key]) == dict:
359             orig[key] = merge_spec(orig[key], new[key])
360         else:
361             orig[key] = new[key]
362
363     for key in new:
364         if key not in orig:
365             orig[key] = new[key]
366
367     return orig