| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 1 | #    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 |  | 
| dis | 2b2d863 | 2016-12-08 17:56:57 +0200 | [diff] [blame] | 17 | import collections | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 18 | import copy | 
| dis | 2b2d863 | 2016-12-08 17:56:57 +0200 | [diff] [blame] | 19 | import os | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 20 | import re | 
|  | 21 |  | 
| dis | 2b2d863 | 2016-12-08 17:56:57 +0200 | [diff] [blame] | 22 | from devops import error | 
|  | 23 | import json | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 24 | import yaml | 
|  | 25 |  | 
|  | 26 | from tcp_tests.helpers import exceptions | 
| dis | 2b2d863 | 2016-12-08 17:56:57 +0200 | [diff] [blame] | 27 | from tcp_tests.helpers import utils | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 28 | from tcp_tests import logger | 
|  | 29 |  | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 30 | LOG = logger.logger | 
|  | 31 |  | 
|  | 32 |  | 
|  | 33 | class 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 |  | 
|  | 46 | def fail_if_obj(x): | 
|  | 47 | if not isinstance(x, int): | 
|  | 48 | raise TypeError("Expecting int value!") | 
|  | 49 |  | 
|  | 50 |  | 
|  | 51 | def 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 |  | 
|  | 71 | def 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 |  | 
|  | 100 | def 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 |  | 
|  | 142 | def keypath(paths): | 
|  | 143 | """Function to make string keypath from list of paths""" | 
|  | 144 | return ".".join(list(paths)) | 
|  | 145 |  | 
|  | 146 |  | 
|  | 147 | def 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 |  | 
|  | 162 | def 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 |  | 
|  | 256 | class EnvironmentConfig(object): | 
|  | 257 | def __init__(self): | 
|  | 258 | super(EnvironmentConfig, self).__init__() | 
| Dennis Dmitriev | 2d60c8e | 2017-05-12 18:34:01 +0300 | [diff] [blame] | 259 | self.__config = None | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 260 |  | 
|  | 261 | @property | 
|  | 262 | def config(self): | 
| Dennis Dmitriev | 2d60c8e | 2017-05-12 18:34:01 +0300 | [diff] [blame] | 263 | return self.__config | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 264 |  | 
|  | 265 | @config.setter | 
|  | 266 | def config(self, config): | 
|  | 267 | """Setter for config | 
|  | 268 |  | 
|  | 269 | :param config: dict | 
|  | 270 | """ | 
| Dennis Dmitriev | 2d60c8e | 2017-05-12 18:34:01 +0300 | [diff] [blame] | 271 | self.__config = fix_devops_config(config) | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 272 |  | 
|  | 273 | def __getitem__(self, key): | 
| Dennis Dmitriev | 2d60c8e | 2017-05-12 18:34:01 +0300 | [diff] [blame] | 274 | if self.__config is not None: | 
|  | 275 | conf = self.__config['template']['devops_settings'] | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 276 | 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 Dmitriev | 2d60c8e | 2017-05-12 18:34:01 +0300 | [diff] [blame] | 289 | conf = self.__config['template']['devops_settings'] | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 290 | 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 Dmitriev | 2d60c8e | 2017-05-12 18:34:01 +0300 | [diff] [blame] | 297 | if self.__config is None: | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 298 | raise exceptions.DevopsConfigIsNone() | 
|  | 299 | with open(filename, 'w') as f: | 
|  | 300 | f.write( | 
|  | 301 | yaml.dump( | 
| Dennis Dmitriev | 2d60c8e | 2017-05-12 18:34:01 +0300 | [diff] [blame] | 302 | self.__config, default_flow_style=False | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 303 | ) | 
|  | 304 | ) | 
|  | 305 |  | 
| Artem Panchenko | db0a97f | 2017-06-27 19:09:13 +0300 | [diff] [blame] | 306 | def load_template(self, filename, options=None): | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 307 | """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 | ) | 
| dis | 2b2d863 | 2016-12-08 17:56:57 +0200 | [diff] [blame] | 317 |  | 
| Dina Belova | e6fdffb | 2017-09-19 13:58:34 -0700 | [diff] [blame^] | 318 | # self.config = templates.yaml_template_load(filename) | 
| Artem Panchenko | db0a97f | 2017-06-27 19:09:13 +0300 | [diff] [blame] | 319 | self.config = yaml_template_load(filename, options) | 
| Dennis Dmitriev | 6f59add | 2016-10-18 13:45:27 +0300 | [diff] [blame] | 320 | else: | 
|  | 321 | LOG.error("Template filename is not set, loading config " + | 
|  | 322 | "from template aborted.") | 
| dis | 2b2d863 | 2016-12-08 17:56:57 +0200 | [diff] [blame] | 323 |  | 
|  | 324 |  | 
| Artem Panchenko | db0a97f | 2017-06-27 19:09:13 +0300 | [diff] [blame] | 325 | def yaml_template_load(config_file, options=None): | 
| dis | 2b2d863 | 2016-12-08 17:56:57 +0200 | [diff] [blame] | 326 | """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 Panchenko | db0a97f | 2017-06-27 19:09:13 +0300 | [diff] [blame] | 338 | inputfile = utils.render_template(file_name, options) | 
| dis | 2b2d863 | 2016-12-08 17:56:57 +0200 | [diff] [blame] | 339 | 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 Panchenko | db0a97f | 2017-06-27 19:09:13 +0300 | [diff] [blame] | 379 | f = utils.render_template(config_file, options) | 
| dis | 2b2d863 | 2016-12-08 17:56:57 +0200 | [diff] [blame] | 380 | return yaml.load(f, TemplateLoader) |