blob: 84ed286fe87fcb3d39a3077b5207df43ae9ee2c0 [file] [log] [blame]
Dennis Dmitriev6f59add2016-10-18 13:45:27 +03001# Copyright 2016 Mirantis, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# 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, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15# TODO(slebedev): implement unit tests
16
dis2b2d8632016-12-08 17:56:57 +020017import collections
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030018import copy
dis2b2d8632016-12-08 17:56:57 +020019import os
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030020import re
21
dis2b2d8632016-12-08 17:56:57 +020022#from devops.helpers import templates
23from devops import error
24import json
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030025import yaml
26
27from tcp_tests.helpers import exceptions
dis2b2d8632016-12-08 17:56:57 +020028from tcp_tests.helpers import utils
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030029from tcp_tests import logger
30
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030031LOG = logger.logger
32
33
34class DevopsConfigMissingKey(KeyError):
35 def __init__(self, key, keypath):
36 super(DevopsConfigMissingKey, self).__init__()
37 self.key = key
38 self.keypath
39
40 def __str__(self):
41 return "Key '{0}' by keypath '{1}' is missing".format(
42 self.key,
43 self.keypath
44 )
45
46
47def fail_if_obj(x):
48 if not isinstance(x, int):
49 raise TypeError("Expecting int value!")
50
51
52def fix_devops_config(config):
53 """Function for get correct structure of config
54
55 :param config: dict
56 :returns: config dict
57 """
58 if not isinstance(config, dict):
59 raise exceptions.DevopsConfigTypeError(
60 type_name=type(config).__name__
61 )
62 if 'template' in config:
63 return copy.deepcopy(config)
64 else:
65 return {
66 "template": {
67 "devops_settings": copy.deepcopy(config)
68 }
69 }
70
71
72def list_update(obj, indexes, value):
73 """Procedure for setting value into list (nested too), need
74 in some functions where we are not able to set value directly.
75
76 e.g.: we want to change element in nested list.
77
78 obj = [12, 34, [3, 5, [0, 4], 3], 85]
79 list_update(obj, [2, 2, 1], 50) => obj[2][2][1] = 50
80 print(obj) => [12, 34, [3, 5, [0, 50], 3], 85]
81
82 :param obj: source list
83 :param indexes: list with indexes for recursive process
84 :param value: some value for setting
85 """
86 def check_obj(obj):
87 if not isinstance(obj, list):
88 raise TypeError("obj must be a list instance!")
89 check_obj(obj)
90 if len(indexes) > 0:
91 cur = obj
92 last_index = indexes[-1]
93 fail_if_obj(last_index)
94 for i in indexes[:-1]:
95 fail_if_obj(i)
96 check_obj(cur[i])
97 cur = cur[i]
98 cur[last_index] = value
99
100
101def return_obj(indexes=[]):
102 """Function returns dict() or list() object given nesting, it needs by
103 set_value_for_dict_by_keypath().
104
105 Examples:
106 return_obj() => {}
107 return_obj([0]) => [{}]
108 return_obj([-1]) => [{}]
109 return_obj([-1, 1, -2]) => [[None, [{}, None]]]
110 return_obj([2]) => [None, None, {}]
111 return_obj([1,3]) => [None, [None, None, None, {}]]
112 """
113 if not isinstance(indexes, list):
114 raise TypeError("indexes must be a list!")
115 if len(indexes) > 0:
116 # Create resulting initial object with 1 element
117 result = [None]
118 # And save it's ref
119 cur = result
120 # lambda for extending list elements
121 li = (lambda x: [None] * x)
122 # lambda for nesting of list
123 nesting = (lambda x: x if x >= 0 else abs(x) - 1)
124 # save last index
125 last_index = indexes[-1]
126 fail_if_obj(last_index)
127 # loop from first till penultimate elements of indexes
128 # we must create nesting list and set current position to
129 # element at next index in indexes list
130 for i in indexes[:-1]:
131 fail_if_obj(i)
132 cur.extend(li(nesting(i)))
133 cur[i] = [None]
134 cur = cur[i]
135 # Perform last index
136 cur.extend(li(nesting(last_index)))
137 cur[last_index] = {}
138 return result
139 else:
140 return dict()
141
142
143def keypath(paths):
144 """Function to make string keypath from list of paths"""
145 return ".".join(list(paths))
146
147
148def disassemble_path(path):
149 """Func for disassembling path into key and indexes list (if needed)
150
151 :param path: string
152 :returns: key string, indexes list
153 """
154 pattern = re.compile("\[([0-9]*)\]")
155 # find all indexes of possible list object in path
156 indexes = (lambda x: [int(r) for r in pattern.findall(x)]
157 if pattern.search(x) else [])
158 # get key
159 base_key = (lambda x: re.sub(pattern, '', x))
160 return base_key(path), indexes(path)
161
162
163def set_value_for_dict_by_keypath(source, paths, value, new_on_missing=True):
164 """Procedure for setting specific value by keypath in dict
165
166 :param source: dict
167 :param paths: string
168 :param value: value to set by keypath
169 """
170 paths = paths.lstrip(".").split(".")
171 walked_paths = []
172 # Store the last path
173 last_path = paths.pop()
174 data = source
175 # loop to go through dict
176 while len(paths) > 0:
177 path = paths.pop(0)
178 key, indexes = disassemble_path(path)
179 walked_paths.append(key)
180 if key not in data:
181 if new_on_missing:
182 # if object is missing, we create new one
183 data[key] = return_obj(indexes)
184 else:
185 raise DevopsConfigMissingKey(key, keypath(walked_paths[:-1]))
186
187 data = data[key]
188
189 # if we can not get element in list, we should
190 # throw an exception with walked path
191 for i in indexes:
192 try:
193 tmp = data[i]
194 except IndexError as err:
195 LOG.error(
196 "Couldn't access {0} element of '{1}' keypath".format(
197 i, keypath(walked_paths)
198 )
199 )
200 LOG.error(
201 "Dump of '{0}':\n{1}".format(
202 keypath(walked_paths),
203 json.dumps(data)
204 )
205 )
206 raise type(err)(
207 "Can't access '{0}' element of '{1}' object! "
208 "'{2}' object found!".format(
209 i,
210 keypath(walked_paths),
211 data
212 )
213 )
214 data = tmp
215 walked_paths[-1] += "[{0}]".format(i)
216
217 key, indexes = disassemble_path(last_path)
218 i_count = len(indexes)
219 if key not in data:
220 if new_on_missing:
221 data[key] = return_obj(indexes)
222 else:
223 raise DevopsConfigMissingKey(key, keypath(walked_paths))
224 elif i_count > 0 and not isinstance(data[key], list):
225 raise TypeError(
226 ("Key '{0}' by '{1}' keypath expected as list "
227 "but '{3}' obj found").format(
228 key, keypath(walked_paths), type(data[key]).__name__
229 )
230 )
231 if i_count == 0:
232 data[key] = value
233 else:
234 try:
235 list_update(data[key], indexes, value)
236 except (IndexError, TypeError) as err:
237 LOG.error(
238 "Error while setting by '{0}' key of '{1}' keypath".format(
239 last_path,
240 keypath(walked_paths)
241 )
242 )
243 LOG.error(
244 "Dump of object by '{0}' keypath:\n{1}".format(
245 keypath(walked_paths),
246 json.dumps(data)
247 )
248 )
249 raise type(err)(
250 "Couldn't set value by '{0}' key of '{1}' keypath'".format(
251 last_path,
252 keypath(walked_paths)
253 )
254 )
255
256
257class EnvironmentConfig(object):
258 def __init__(self):
259 super(EnvironmentConfig, self).__init__()
260 self._config = None
261
262 @property
263 def config(self):
264 return self._config
265
266 @config.setter
267 def config(self, config):
268 """Setter for config
269
270 :param config: dict
271 """
272 self._config = fix_devops_config(config)
273
274 def __getitem__(self, key):
275 if self._config is not None:
276 conf = self._config['template']['devops_settings']
277 return copy.deepcopy(conf.get(key, None))
278 else:
279 return None
280
281 @logger.logwrap
282 def set_value_by_keypath(self, keypath, value):
283 """Function for set value of devops settings by keypath.
284
285 It's forbidden to set value of self.config directly, so
286 it's possible simply set value by keypath
287 """
288 if self.config is None:
289 raise exceptions.DevopsConfigIsNone()
290 conf = self._config['template']['devops_settings']
291 set_value_for_dict_by_keypath(conf, keypath, value)
292
293 def save(self, filename):
294 """Dump current config into given file
295
296 :param filename: string
297 """
298 if self._config is None:
299 raise exceptions.DevopsConfigIsNone()
300 with open(filename, 'w') as f:
301 f.write(
302 yaml.dump(
303 self._config, default_flow_style=False
304 )
305 )
306
307 def load_template(self, filename):
308 """Method for reading file with devops config
309
310 :param filename: string
311 """
312 if filename is not None:
313 LOG.debug(
314 "Preparing to load config from template '{0}'".format(
315 filename
316 )
317 )
dis2b2d8632016-12-08 17:56:57 +0200318
319 #self.config = templates.yaml_template_load(filename)
320 self.config = yaml_template_load(filename)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300321 else:
322 LOG.error("Template filename is not set, loading config " +
323 "from template aborted.")
dis2b2d8632016-12-08 17:56:57 +0200324
325
326def yaml_template_load(config_file):
327 """Temporary moved from fuel_devops to use jinja2"""
328 dirname = os.path.dirname(config_file)
329
330 class TemplateLoader(yaml.Loader):
331 pass
332
333 def yaml_include(loader, node):
334 file_name = os.path.join(dirname, node.value)
335 if not os.path.isfile(file_name):
336 raise error.DevopsError(
337 "Cannot load the environment template {0} : include file {1} "
338 "doesn't exist.".format(dirname, file_name))
339 inputfile = utils.render_template(file_name)
340 return yaml.load(inputfile, TemplateLoader)
341
342 def yaml_get_env_variable(loader, node):
343 if not node.value.strip():
344 raise error.DevopsError(
345 "Environment variable is required after {tag} in "
346 "{filename}".format(tag=node.tag, filename=loader.name))
347 node_value = node.value.split(',', 1)
348 # Get the name of environment variable
349 env_variable = node_value[0].strip()
350
351 # Get the default value for environment variable if it exists in config
352 if len(node_value) > 1:
353 default_val = node_value[1].strip()
354 else:
355 default_val = None
356
357 value = os.environ.get(env_variable, default_val)
358 if value is None:
359 raise error.DevopsError(
360 "Environment variable {var} is not set from shell"
361 " environment! No default value provided in file "
362 "{filename}".format(var=env_variable, filename=loader.name))
363
364 return yaml.load(value, TemplateLoader)
365
366 def construct_mapping(loader, node):
367 loader.flatten_mapping(node)
368 return collections.OrderedDict(loader.construct_pairs(node))
369
370 if not os.path.isfile(config_file):
371 raise error.DevopsError(
372 "Cannot load the environment template {0} : file "
373 "doesn't exist.".format(config_file))
374
375 TemplateLoader.add_constructor("!include", yaml_include)
376 TemplateLoader.add_constructor("!os_env", yaml_get_env_variable)
377 TemplateLoader.add_constructor(
378 yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, construct_mapping)
379
380 f = utils.render_template(config_file)
381 return yaml.load(f, TemplateLoader)