blob: 3ad9a36a46276b558ec340a742639063f3fd5040 [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
17import copy
18import json
19import re
20
21from devops.helpers import templates
22import yaml
23
24from tcp_tests.helpers import exceptions
25from tcp_tests import logger
26
27
28LOG = logger.logger
29
30
31class DevopsConfigMissingKey(KeyError):
32 def __init__(self, key, keypath):
33 super(DevopsConfigMissingKey, self).__init__()
34 self.key = key
35 self.keypath
36
37 def __str__(self):
38 return "Key '{0}' by keypath '{1}' is missing".format(
39 self.key,
40 self.keypath
41 )
42
43
44def fail_if_obj(x):
45 if not isinstance(x, int):
46 raise TypeError("Expecting int value!")
47
48
49def fix_devops_config(config):
50 """Function for get correct structure of config
51
52 :param config: dict
53 :returns: config dict
54 """
55 if not isinstance(config, dict):
56 raise exceptions.DevopsConfigTypeError(
57 type_name=type(config).__name__
58 )
59 if 'template' in config:
60 return copy.deepcopy(config)
61 else:
62 return {
63 "template": {
64 "devops_settings": copy.deepcopy(config)
65 }
66 }
67
68
69def list_update(obj, indexes, value):
70 """Procedure for setting value into list (nested too), need
71 in some functions where we are not able to set value directly.
72
73 e.g.: we want to change element in nested list.
74
75 obj = [12, 34, [3, 5, [0, 4], 3], 85]
76 list_update(obj, [2, 2, 1], 50) => obj[2][2][1] = 50
77 print(obj) => [12, 34, [3, 5, [0, 50], 3], 85]
78
79 :param obj: source list
80 :param indexes: list with indexes for recursive process
81 :param value: some value for setting
82 """
83 def check_obj(obj):
84 if not isinstance(obj, list):
85 raise TypeError("obj must be a list instance!")
86 check_obj(obj)
87 if len(indexes) > 0:
88 cur = obj
89 last_index = indexes[-1]
90 fail_if_obj(last_index)
91 for i in indexes[:-1]:
92 fail_if_obj(i)
93 check_obj(cur[i])
94 cur = cur[i]
95 cur[last_index] = value
96
97
98def return_obj(indexes=[]):
99 """Function returns dict() or list() object given nesting, it needs by
100 set_value_for_dict_by_keypath().
101
102 Examples:
103 return_obj() => {}
104 return_obj([0]) => [{}]
105 return_obj([-1]) => [{}]
106 return_obj([-1, 1, -2]) => [[None, [{}, None]]]
107 return_obj([2]) => [None, None, {}]
108 return_obj([1,3]) => [None, [None, None, None, {}]]
109 """
110 if not isinstance(indexes, list):
111 raise TypeError("indexes must be a list!")
112 if len(indexes) > 0:
113 # Create resulting initial object with 1 element
114 result = [None]
115 # And save it's ref
116 cur = result
117 # lambda for extending list elements
118 li = (lambda x: [None] * x)
119 # lambda for nesting of list
120 nesting = (lambda x: x if x >= 0 else abs(x) - 1)
121 # save last index
122 last_index = indexes[-1]
123 fail_if_obj(last_index)
124 # loop from first till penultimate elements of indexes
125 # we must create nesting list and set current position to
126 # element at next index in indexes list
127 for i in indexes[:-1]:
128 fail_if_obj(i)
129 cur.extend(li(nesting(i)))
130 cur[i] = [None]
131 cur = cur[i]
132 # Perform last index
133 cur.extend(li(nesting(last_index)))
134 cur[last_index] = {}
135 return result
136 else:
137 return dict()
138
139
140def keypath(paths):
141 """Function to make string keypath from list of paths"""
142 return ".".join(list(paths))
143
144
145def disassemble_path(path):
146 """Func for disassembling path into key and indexes list (if needed)
147
148 :param path: string
149 :returns: key string, indexes list
150 """
151 pattern = re.compile("\[([0-9]*)\]")
152 # find all indexes of possible list object in path
153 indexes = (lambda x: [int(r) for r in pattern.findall(x)]
154 if pattern.search(x) else [])
155 # get key
156 base_key = (lambda x: re.sub(pattern, '', x))
157 return base_key(path), indexes(path)
158
159
160def set_value_for_dict_by_keypath(source, paths, value, new_on_missing=True):
161 """Procedure for setting specific value by keypath in dict
162
163 :param source: dict
164 :param paths: string
165 :param value: value to set by keypath
166 """
167 paths = paths.lstrip(".").split(".")
168 walked_paths = []
169 # Store the last path
170 last_path = paths.pop()
171 data = source
172 # loop to go through dict
173 while len(paths) > 0:
174 path = paths.pop(0)
175 key, indexes = disassemble_path(path)
176 walked_paths.append(key)
177 if key not in data:
178 if new_on_missing:
179 # if object is missing, we create new one
180 data[key] = return_obj(indexes)
181 else:
182 raise DevopsConfigMissingKey(key, keypath(walked_paths[:-1]))
183
184 data = data[key]
185
186 # if we can not get element in list, we should
187 # throw an exception with walked path
188 for i in indexes:
189 try:
190 tmp = data[i]
191 except IndexError as err:
192 LOG.error(
193 "Couldn't access {0} element of '{1}' keypath".format(
194 i, keypath(walked_paths)
195 )
196 )
197 LOG.error(
198 "Dump of '{0}':\n{1}".format(
199 keypath(walked_paths),
200 json.dumps(data)
201 )
202 )
203 raise type(err)(
204 "Can't access '{0}' element of '{1}' object! "
205 "'{2}' object found!".format(
206 i,
207 keypath(walked_paths),
208 data
209 )
210 )
211 data = tmp
212 walked_paths[-1] += "[{0}]".format(i)
213
214 key, indexes = disassemble_path(last_path)
215 i_count = len(indexes)
216 if key not in data:
217 if new_on_missing:
218 data[key] = return_obj(indexes)
219 else:
220 raise DevopsConfigMissingKey(key, keypath(walked_paths))
221 elif i_count > 0 and not isinstance(data[key], list):
222 raise TypeError(
223 ("Key '{0}' by '{1}' keypath expected as list "
224 "but '{3}' obj found").format(
225 key, keypath(walked_paths), type(data[key]).__name__
226 )
227 )
228 if i_count == 0:
229 data[key] = value
230 else:
231 try:
232 list_update(data[key], indexes, value)
233 except (IndexError, TypeError) as err:
234 LOG.error(
235 "Error while setting by '{0}' key of '{1}' keypath".format(
236 last_path,
237 keypath(walked_paths)
238 )
239 )
240 LOG.error(
241 "Dump of object by '{0}' keypath:\n{1}".format(
242 keypath(walked_paths),
243 json.dumps(data)
244 )
245 )
246 raise type(err)(
247 "Couldn't set value by '{0}' key of '{1}' keypath'".format(
248 last_path,
249 keypath(walked_paths)
250 )
251 )
252
253
254class EnvironmentConfig(object):
255 def __init__(self):
256 super(EnvironmentConfig, self).__init__()
257 self._config = None
258
259 @property
260 def config(self):
261 return self._config
262
263 @config.setter
264 def config(self, config):
265 """Setter for config
266
267 :param config: dict
268 """
269 self._config = fix_devops_config(config)
270
271 def __getitem__(self, key):
272 if self._config is not None:
273 conf = self._config['template']['devops_settings']
274 return copy.deepcopy(conf.get(key, None))
275 else:
276 return None
277
278 @logger.logwrap
279 def set_value_by_keypath(self, keypath, value):
280 """Function for set value of devops settings by keypath.
281
282 It's forbidden to set value of self.config directly, so
283 it's possible simply set value by keypath
284 """
285 if self.config is None:
286 raise exceptions.DevopsConfigIsNone()
287 conf = self._config['template']['devops_settings']
288 set_value_for_dict_by_keypath(conf, keypath, value)
289
290 def save(self, filename):
291 """Dump current config into given file
292
293 :param filename: string
294 """
295 if self._config is None:
296 raise exceptions.DevopsConfigIsNone()
297 with open(filename, 'w') as f:
298 f.write(
299 yaml.dump(
300 self._config, default_flow_style=False
301 )
302 )
303
304 def load_template(self, filename):
305 """Method for reading file with devops config
306
307 :param filename: string
308 """
309 if filename is not None:
310 LOG.debug(
311 "Preparing to load config from template '{0}'".format(
312 filename
313 )
314 )
315 self.config = templates.yaml_template_load(filename)
316 else:
317 LOG.error("Template filename is not set, loading config " +
318 "from template aborted.")