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