Merge "cli: Modify configuration via CLI"
[vswitchperf.git] / conf / __init__.py
1 # Copyright 2015-2016 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 os
24 import re
25 import logging
26 import pprint
27 import ast
28 import netaddr
29
30 _LOGGER = logging.getLogger(__name__)
31
32 # Special test parameters which are not part of standard VSPERF configuration
33 _EXTRA_TEST_PARAMS = ['bidirectional', 'traffic_type', 'iload', 'tunnel_type',
34                       'multistream', 'stream_type', 'pre-installed_flows']
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 getValue(self, attr):
53         """Return a settings item value
54         """
55         if attr in self.__dict__:
56             if attr == 'TEST_PARAMS':
57                 return getattr(self, attr)
58             else:
59                 master_value = getattr(self, attr)
60                 # Check if parameter value was overridden by CLI option
61                 cli_value = get_test_param(attr, None)
62                 return cli_value if cli_value else master_value
63         else:
64             raise AttributeError("%r object has no attribute %r" %
65                                  (self.__class__, attr))
66
67     def __setattr__(self, name, value):
68         """Set a value
69         """
70         # skip non-settings. this should exclude built-ins amongst others
71         if not name.isupper():
72             return
73
74         # we can assume all uppercase keys are valid settings
75         super(Settings, self).__setattr__(name, value)
76
77     def setValue(self, name, value):
78         """Set a value
79         """
80         if name is not None and value is not None:
81             super(Settings, self).__setattr__(name, value)
82
83     def load_from_file(self, path):
84         """Update ``settings`` with values found in module at ``path``.
85         """
86         import imp
87
88         custom_settings = imp.load_source('custom_settings', path)
89
90         for key in dir(custom_settings):
91             if getattr(custom_settings, key) is not None:
92                 setattr(self, key, getattr(custom_settings, key))
93
94     def load_from_dir(self, dir_path):
95         """Update ``settings`` with contents of the .conf files at ``path``.
96
97         Each file must be named Nfilename.conf, where N is a single or
98         multi-digit decimal number.  The files are loaded in ascending order of
99         N - so if a configuration item exists in more that one file the setting
100         in the file with the largest value of N takes precedence.
101
102         :param dir_path: The full path to the dir from which to load the .conf
103             files.
104
105         :returns: None
106         """
107         regex = re.compile("^(?P<digit_part>[0-9]+).*.conf$")
108
109         def get_prefix(filename):
110             """
111             Provide a suitable function for sort's key arg
112             """
113             match_object = regex.search(os.path.basename(filename))
114             return int(match_object.group('digit_part'))
115
116         # get full file path to all files & dirs in dir_path
117         file_paths = os.listdir(dir_path)
118         file_paths = [os.path.join(dir_path, x) for x in file_paths]
119
120         # filter to get only those that are a files, with a leading
121         # digit and end in '.conf'
122         file_paths = [x for x in file_paths if os.path.isfile(x) and
123                       regex.search(os.path.basename(x))]
124
125         # sort ascending on the leading digits
126         file_paths.sort(key=get_prefix)
127
128         # load settings from each file in turn
129         for filepath in file_paths:
130             self.load_from_file(filepath)
131
132     def load_from_dict(self, conf):
133         """
134         Update ``settings`` with values found in ``conf``.
135
136         Unlike the other loaders, this is case insensitive.
137         """
138         for key in conf:
139             if conf[key] is not None:
140                 setattr(self, key.upper(), conf[key])
141
142     def load_from_env(self):
143         """
144         Update ``settings`` with values found in the environment.
145         """
146         for key in os.environ:
147             setattr(self, key, os.environ[key])
148
149     def check_test_params(self):
150         """
151         Check all parameters defined inside TEST_PARAMS for their
152         existence. In case that non existing vsperf parmeter name
153         is detected, then VSPER will raise a runtime error.
154         """
155         unknown_keys = []
156         for key in settings.getValue('TEST_PARAMS'):
157             if key == 'TEST_PARAMS':
158                 raise RuntimeError('It is not allowed to define TEST_PARAMS '
159                                    'as a test parameter')
160             if key not in self.__dict__ and key not in _EXTRA_TEST_PARAMS:
161                 unknown_keys.append(key)
162
163         if len(unknown_keys):
164             raise RuntimeError('Test parameters contain unknown configuration '
165                                'parameter(s): {}'.format(', '.join(unknown_keys)))
166
167     def check_vm_settings(self, vm_number):
168         """
169         Check all VM related settings starting with GUEST_ prefix.
170         If it is not available for defined number of VMs, then vsperf
171         will try to expand it automatically. Expansion is performed
172         also in case that first list item contains a macro.
173         """
174         for key in self.__dict__:
175             if key.startswith('GUEST_'):
176                 value = self.getValue(key)
177                 if isinstance(value, str) and value.find('#') >= 0:
178                     self._expand_vm_settings(key, 1)
179
180                 if isinstance(value, list):
181                     if len(value) < vm_number or str(value[0]).find('#') >= 0:
182                         # expand configuration for all VMs
183                         self._expand_vm_settings(key, vm_number)
184
185     def _expand_vm_settings(self, key, vm_number):
186         """
187         Expand VM option with given key for given number of VMs
188         """
189         tmp_value = self.getValue(key)
190         if isinstance(tmp_value, str):
191             scalar = True
192             master_value = tmp_value
193             tmp_value = [tmp_value]
194         else:
195             scalar = False
196             master_value = tmp_value[0]
197
198         master_value_str = str(master_value)
199         if master_value_str.find('#') >= 0:
200             self.__dict__[key] = []
201             for vmindex in range(vm_number):
202                 value = master_value_str.replace('#VMINDEX', str(vmindex))
203                 for macro, args, param, _, step in re.findall(_PARSE_PATTERN, value):
204                     multi = int(step) if len(step) and int(step) else 1
205                     if macro == '#EVAL':
206                         # pylint: disable=eval-used
207                         tmp_result = str(eval(param))
208                     elif macro == '#MAC':
209                         mac_value = netaddr.EUI(param).value
210                         mac = netaddr.EUI(mac_value + vmindex * multi)
211                         mac.dialect = netaddr.mac_unix_expanded
212                         tmp_result = str(mac)
213                     elif macro == '#IP':
214                         ip_value = netaddr.IPAddress(param).value
215                         tmp_result = str(netaddr.IPAddress(ip_value + vmindex * multi))
216                     else:
217                         raise RuntimeError('Unknown configuration macro {} in {}'.format(macro, key))
218
219                     value = value.replace("{}{}".format(macro, args), tmp_result)
220
221                 # retype value to original type if needed
222                 if not isinstance(master_value, str):
223                     value = ast.literal_eval(value)
224                 self.__dict__[key].append(value)
225         else:
226             for vmindex in range(len(tmp_value), vm_number):
227                 self.__dict__[key].append(master_value)
228
229         if scalar:
230             self.__dict__[key] = self.__dict__[key][0]
231
232         _LOGGER.debug("Expanding option: %s = %s", key, self.__dict__[key])
233
234     def __str__(self):
235         """Provide settings as a human-readable string.
236
237         This can be useful for debug.
238
239         Returns:
240             A human-readable string.
241         """
242         tmp_dict = {}
243         for key in self.__dict__:
244             tmp_dict[key] = self.getValue(key)
245
246         return pprint.pformat(tmp_dict)
247
248     #
249     # validation methods used by step driven testcases
250     #
251     def validate_getValue(self, result, attr):
252         """Verifies, that correct value was returned
253         """
254         assert result == self.__dict__[attr]
255         return True
256
257     def validate_setValue(self, dummy_result, name, value):
258         """Verifies, that value was correctly set
259         """
260         assert value == self.__dict__[name]
261         return True
262
263 settings = Settings()
264
265 def get_test_param(key, default=None):
266     """Retrieve value for test param ``key`` if available.
267
268     :param key: Key to retrieve from test params.
269     :param default: Default to return if key not found.
270
271     :returns: Value for ``key`` if found, else ``default``.
272     """
273     test_params = settings.getValue('TEST_PARAMS')
274     if key in test_params:
275         if not isinstance(test_params.get(key), str):
276             return test_params.get(key)
277         else:
278             # values are passed inside string from CLI, so we must retype them accordingly
279             try:
280                 return ast.literal_eval(test_params.get(key))
281             except ValueError:
282                 # for backward compatibility, we have to accept strings without quotes
283                 _LOGGER.warning("Adding missing quotes around string value: %s = %s",
284                                 key, str(test_params.get(key)))
285                 return str(test_params.get(key))
286     else:
287         return default