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