virtual_fuel: make vm_template an attribute
[fuel.git] / deploy / deploy-config.py
1 #!/usr/bin/python
2 ###############################################################################
3 # Copyright (c) 2015 Ericsson AB and others.
4 # jonas.bjurel@ericsson.com
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
9 ###############################################################################
10
11 ###############################################################################
12 # Description
13 # This script constructs the final deployment dea.yaml and dha.yaml files
14 # The dea.yaml get's constructed from (in reverse priority):
15 # 1) dea-base
16 # 2) dea-pod-override
17 # 3) deployment-scenario dea-override-config section
18 #
19 # The dha.yaml get's constructed from (in reverse priority):
20 # 1) pod dha
21 # 2) deployment-scenario dha-override-config section
22 ###############################################################################
23
24 import os
25 import yaml
26 import sys
27 import urllib2
28 import calendar
29 import time
30 import collections
31 import hashlib
32
33 from functools import reduce
34 from operator import or_
35 from common import (
36     log,
37     exec_cmd,
38     err,
39     warn,
40     check_file_exists,
41     create_dir_if_not_exists,
42     delete,
43     check_if_root,
44     ArgParser,
45 )
46
47 def parse_arguments():
48     parser = ArgParser(prog='python %s' % __file__)
49     parser.add_argument('-dha', dest='dha_uri', action='store',
50                         default=False, help='dha configuration file FQDN URI', required=True)
51     parser.add_argument('-deab', dest='dea_base_uri', action='store',
52                         default=False, help='dea base configuration FQDN URI', required=True)
53     parser.add_argument('-deao', dest='dea_pod_override_uri', action='store',
54                         default=False, help='dea POD override configuration FQDN URI',
55                         required=True)
56     parser.add_argument('-scenario-base-uri', dest='scenario_base_uri', action='store',
57                         default=False, help='Deploymen scenario base directory URI',
58                         required=True)
59     parser.add_argument('-scenario', dest='scenario', action='store',
60                         default=False, help='Deploymen scenario short-name (priority), or base file name (in the absense of a shortname defenition)',
61                         required=True)
62
63     parser.add_argument('-plugins', dest='plugins_uri', action='store',
64                         default=False, help='Plugin configurations directory URI',
65                         required=True)
66     parser.add_argument('-output', dest='output_path', action='store',
67                         default=False,
68                         help='Local path for resulting output configuration files',
69                         required=True)
70     args = parser.parse_args()
71     log(args)
72     kwargs = {'dha_uri': args.dha_uri,
73               'dea_base_uri': args.dea_base_uri,
74               'dea_pod_override_uri': args.dea_pod_override_uri,
75               'scenario_base_uri': args.scenario_base_uri,
76               'scenario': args.scenario,
77               'plugins_uri': args.plugins_uri,
78               'output_path': args.output_path}
79     return kwargs
80
81 def warning(msg):
82     red = '\033[0;31m'
83     NC = '\033[0m'
84     print('%(red)s WARNING: %(msg)s %(NC)s' % {'red': red,
85                                                'msg': msg,
86                                                'NC': NC})
87
88 def setup_yaml():
89     represent_dict_order = lambda self, data:  self.represent_mapping('tag:yaml.org,2002:map', data.items())
90     yaml.add_representer(collections.OrderedDict, represent_dict_order)
91
92 def sha_uri(uri):
93     response = urllib2.urlopen(uri)
94     data = response.read()
95     sha1 = hashlib.sha1()
96     sha1.update(data)
97     return sha1.hexdigest()
98
99 def merge_fuel_plugin_version_list(list1, list2):
100     final_list = []
101     # When the plugin version in not there in list1 it will
102     # not be copied
103     for e_l1 in list1:
104         plugin_version = e_l1.get('metadata',
105                                   {'plugin_version', None}).get('plugin_version')
106         plugin_version_found = False
107         for e_l2 in list2:
108             if plugin_version == e_l2.get('metadata',
109                                           {'plugin_version',
110                                            None}).get('plugin_version'):
111                 final_list.append(dict(mergedicts(e_l1, e_l2)))
112                 plugin_version_found = True
113         if not plugin_version_found:
114             final_list.append(e_l1)
115     return final_list
116
117 def merge_lists(list1, list2):
118     if list1 and list2:
119         if isinstance(list1[0], dict):
120             if 'plugin_version' in list1[0].get('metadata', {}):
121                 return merge_fuel_plugin_version_list(list1, list2)
122             else:
123                 warning("Lists with dictionary inside are not merge able! "
124                                 "List2 will overwrite List1. "
125                                 "List1: %s; List2: %s"
126                                 % (list1, list2))
127                 return list2
128         else:
129             return list2
130     elif list1:
131         return list1
132     else:
133         return list2
134
135 def mergedicts(dict1, dict2):
136     for k in set(dict1.keys()).union(dict2.keys()):
137         if k in dict1 and k in dict2:
138             if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
139                 yield (k, dict(mergedicts(dict1[k], dict2[k])))
140             elif isinstance(dict1[k], list) and isinstance(dict2[k], list):
141                 yield (k, merge_lists(dict1[k], dict2[k]))
142             else:
143                 # If one of the values is not a dict nor a list,
144                 # you can't continue merging it.
145                 # Value from second dict overrides one in first and we move on.
146                 yield (k, dict2[k])
147         elif k in dict1:
148             yield (k, dict1[k])
149         else:
150             yield (k, dict2[k])
151
152 setup_yaml()
153 kwargs = parse_arguments()
154
155 # Generate final dea.yaml by merging following config files/fragments in revers priority order:
156 # "dea-base", "dea-pod-override", "deplyment-scenario/module-config-override"
157 # and "deployment-scenario/dea-override"
158 print 'Generating final dea.yaml configuration....'
159
160 # Fetch dea-base, extract and purge meta-data
161 print 'Parsing dea-base from: ' + kwargs["dea_base_uri"] + "...."
162 response = urllib2.urlopen(kwargs["dea_base_uri"])
163 dea_base_conf = yaml.load(response.read())
164 dea_base_title = dea_base_conf['dea-base-config-metadata']['title']
165 dea_base_version = dea_base_conf['dea-base-config-metadata']['version']
166 dea_base_creation = dea_base_conf['dea-base-config-metadata']['created']
167 dea_base_sha = sha_uri(kwargs["dea_base_uri"])
168 dea_base_comment = dea_base_conf['dea-base-config-metadata']['comment']
169 dea_base_conf.pop('dea-base-config-metadata')
170 final_dea_conf = dea_base_conf
171
172 # Fetch dea-pod-override, extract and purge meta-data, merge with previous dea data structure
173 print 'Parsing the dea-pod-override from: ' + kwargs["dea_pod_override_uri"] + "...."
174 response = urllib2.urlopen(kwargs["dea_pod_override_uri"])
175 dea_pod_override_conf = yaml.load(response.read())
176 if dea_pod_override_conf:
177     dea_pod_title = dea_pod_override_conf['dea-pod-override-config-metadata']['title']
178     dea_pod_version = dea_pod_override_conf['dea-pod-override-config-metadata']['version']
179     dea_pod_creation = dea_pod_override_conf['dea-pod-override-config-metadata']['created']
180     dea_pod_sha = sha_uri(kwargs["dea_pod_override_uri"])
181     dea_pod_comment = dea_pod_override_conf['dea-pod-override-config-metadata']['comment']
182     print 'Merging dea-base and dea-pod-override configuration ....'
183     dea_pod_override_conf.pop('dea-pod-override-config-metadata')
184     if dea_pod_override_conf:
185         final_dea_conf = dict(mergedicts(final_dea_conf, dea_pod_override_conf))
186
187 # Fetch deployment-scenario, extract and purge meta-data, merge deployment-scenario/
188 # dea-override-configith previous dea data structure
189 print 'Parsing deployment-scenario from: ' + kwargs["scenario"] + "...."
190
191 response = urllib2.urlopen(kwargs["scenario_base_uri"] + "/scenario.yaml")
192 scenario_short_translation_conf = yaml.load(response.read())
193 if kwargs["scenario"] in scenario_short_translation_conf:
194     scenario_uri = kwargs["scenario_base_uri"] + "/" + scenario_short_translation_conf[kwargs["scenario"]]['configfile']
195 else:
196     scenario_uri = kwargs["scenario_base_uri"] + "/" + kwargs["scenario"]
197 response = urllib2.urlopen(scenario_uri)
198 deploy_scenario_conf = yaml.load(response.read())
199
200 if deploy_scenario_conf:
201     deploy_scenario_title = deploy_scenario_conf['deployment-scenario-metadata']['title']
202     deploy_scenario_version = deploy_scenario_conf['deployment-scenario-metadata']['version']
203     deploy_scenario_creation = deploy_scenario_conf['deployment-scenario-metadata']['created']
204     deploy_scenario_sha = sha_uri(scenario_uri)
205     deploy_scenario_comment = deploy_scenario_conf['deployment-scenario-metadata']['comment']
206     deploy_scenario_conf.pop('deployment-scenario-metadata')
207 else:
208     print "Deployment scenario file not found or is empty"
209     print "Cannot continue, exiting ...."
210     sys.exit(1)
211
212 dea_scenario_override_conf = deploy_scenario_conf["dea-override-config"]
213 if dea_scenario_override_conf:
214     print 'Merging dea-base-, dea-pod-override- and deployment-scenario configuration into final dea.yaml configuration....'
215     final_dea_conf = dict(mergedicts(final_dea_conf, dea_scenario_override_conf))
216
217 # Fetch plugin-configuration configuration files, extract and purge meta-data,
218 # merge/append with previous dea data structure, override plugin-configuration with
219 # deploy-scenario/module-config-override
220 modules = []
221 module_uris = []
222 module_titles = []
223 module_versions = []
224 module_creations = []
225 module_shas = []
226 module_comments = []
227 if deploy_scenario_conf["stack-extensions"]:
228     for module in deploy_scenario_conf["stack-extensions"]:
229         print 'Loading configuration for module: ' + module["module"] + ' and merging it to final dea.yaml configuration....'
230         response = urllib2.urlopen(kwargs["plugins_uri"] + '/' + module["module-config-name"] + '_' + module["module-config-version"] + '.yaml')
231         module_conf = yaml.load(response.read())
232         modules.append(module["module"])
233         module_uris.append(kwargs["plugins_uri"] + '/' + module["module-config-name"] + '_' + module["module-config-version"] + '.yaml')
234         module_titles.append(str(module_conf['plugin-config-metadata']['title']))
235         module_versions.append(str(module_conf['plugin-config-metadata']['version']))
236         module_creations.append(str(module_conf['plugin-config-metadata']['created']))
237         module_shas.append(sha_uri(kwargs["plugins_uri"] + '/' + module["module-config-name"] + '_' + module["module-config-version"] + '.yaml'))
238         module_comments.append(str(module_conf['plugin-config-metadata']['comment']))
239         module_conf.pop('plugin-config-metadata')
240         final_dea_conf['settings']['editable'].update(module_conf)
241         scenario_module_override_conf = module.get('module-config-override')
242         if scenario_module_override_conf:
243             dea_scenario_module_override_conf = {}
244             dea_scenario_module_override_conf['settings'] = {}
245             dea_scenario_module_override_conf['settings']['editable'] = {}
246             dea_scenario_module_override_conf['settings']['editable'][module["module"]] = scenario_module_override_conf
247             final_dea_conf = dict(mergedicts(final_dea_conf, dea_scenario_module_override_conf))
248
249 # Dump final dea.yaml including configuration management meta-data to argument provided
250 # directory
251 if not os.path.exists(kwargs["output_path"]):
252     os.makedirs(kwargs["output_path"])
253 print 'Dumping final dea.yaml to ' + kwargs["output_path"] + '/dea.yaml....'
254 with open(kwargs["output_path"] + '/dea.yaml', "w") as f:
255     f.write("title: DEA.yaml file automatically generated from the configuration files stated in the \"configuration-files\" fragment below\n")
256     f.write("version: " + str(calendar.timegm(time.gmtime()))  + "\n")
257     f.write("created: " + str(time.strftime("%d/%m/%Y")) + " " + str(time.strftime("%H:%M:%S"))  + "\n")
258     f.write("comment: none\n")
259
260     f.write("configuration-files:\n")
261     f.write("  dea-base:\n")
262     f.write("    uri: " +  kwargs["dea_base_uri"] + "\n")
263     f.write("    title: " +  str(dea_base_title) + "\n")
264     f.write("    version: " +  str(dea_base_version) + "\n")
265     f.write("    created: " +  str(dea_base_creation) + "\n")
266     f.write("    sha1: " +  str(dea_base_sha) + "\n")
267     f.write("    comment: " +  str(dea_base_comment) + "\n")
268
269     f.write("  pod-override:\n")
270     f.write("    uri: " +  kwargs["dea_pod_override_uri"] + "\n")
271     f.write("    title: " +  str(dea_pod_title) + "\n")
272     f.write("    version: " +  str(dea_pod_version) + "\n")
273     f.write("    created: " +  str(dea_pod_creation) + "\n")
274     f.write("    sha1: " +  str(dea_pod_sha) + "\n")
275     f.write("    comment: " +  str(dea_pod_comment) + "\n")
276
277     f.write("  deployment-scenario:\n")
278     f.write("    uri: " +  str(scenario_uri) + "\n")
279     f.write("    title: " +  str(deploy_scenario_title) + "\n")
280     f.write("    version: " +  str(deploy_scenario_version) + "\n")
281     f.write("    created: " +  str(deploy_scenario_creation) + "\n")
282     f.write("    sha1: " +  str(deploy_scenario_sha) + "\n")
283     f.write("    comment: " +  str(deploy_scenario_comment) + "\n")
284
285     f.write("  plugin-modules:\n")
286     for k in range(0,len(modules)):
287         f.write("  - module: " + modules[k] + "\n")
288         f.write("    uri: " + module_uris[k] + "\n")
289         f.write("    title: " + module_titles[k] + "\n")
290         f.write("    version: " + module_versions[k] + "\n")
291         f.write("    created: " + module_creations[k] + "\n")
292         f.write("    sha-1: " + module_shas[k] + "\n")
293         f.write("    comment: " + module_comments[k] + "\n")
294
295     yaml.dump(final_dea_conf, f, default_flow_style=False)
296
297 # Load POD dha and override it with "deployment-scenario/dha-override-config" section
298 print 'Generating final dha.yaml configuration....'
299 print 'Parsing dha-pod yaml configuration....'
300 response = urllib2.urlopen(kwargs["dha_uri"])
301 dha_pod_conf = yaml.load(response.read())
302 dha_pod_title = dha_pod_conf['dha-pod-config-metadata']['title']
303 dha_pod_version = dha_pod_conf['dha-pod-config-metadata']['version']
304 dha_pod_creation = dha_pod_conf['dha-pod-config-metadata']['created']
305 dha_pod_sha = sha_uri(kwargs["dha_uri"])
306 dha_pod_comment = dha_pod_conf['dha-pod-config-metadata']['comment']
307 dha_pod_conf.pop('dha-pod-config-metadata')
308 final_dha_conf = dha_pod_conf
309
310 dha_scenario_override_conf = deploy_scenario_conf["dha-override-config"]
311 # Only virtual deploy scenarios can override dha.yaml since there
312 # is no way to programatically override a physical environment:
313 # wireing, IPMI set-up, etc.
314 # For Physical environments, dha.yaml overrides will be silently ignored
315 if dha_scenario_override_conf and (final_dha_conf['adapter'] == 'libvirt' or final_dha_conf['adapter'] == 'esxi' or final_dha_conf['adapter'] == 'vbox'):
316     print 'Merging dha-pod and deployment-scenario override information to final dha.yaml configuration....'
317     final_dha_conf = dict(mergedicts(final_dha_conf, dha_scenario_override_conf))
318
319 # Dump final dha.yaml to argument provided directory
320 print 'Dumping final dha.yaml to ' + kwargs["output_path"] + '/dha.yaml....'
321 with open(kwargs["output_path"] + '/dha.yaml', "w") as f:
322     f.write("title: DHA.yaml file automatically generated from the configuration files stated in the \"configuration-files\" fragment below\n")
323     f.write("version: " + str(calendar.timegm(time.gmtime()))  + "\n")
324     f.write("created: " + str(time.strftime("%d/%m/%Y")) + " " + str(time.strftime("%H:%M:%S"))  + "\n")
325     f.write("comment: none\n")
326
327     f.write("configuration-files:\n")
328
329     f.write("  dha-pod-configuration:\n")
330     f.write("    uri: " +  kwargs["dha_uri"] + "\n")
331     f.write("    title: " +  str(dha_pod_title) + "\n")
332     f.write("    version: " +  str(dha_pod_version) + "\n")
333     f.write("    created: " +  str(dha_pod_creation) + "\n")
334     f.write("    sha-1: " +  str(dha_pod_sha) + "\n")
335     f.write("    comment: " +  str(dha_pod_comment) + "\n")
336
337     f.write("  deployment-scenario:\n")
338     f.write("    uri: " +  str(scenario_uri) + "\n")
339     f.write("    title: " +  str(deploy_scenario_title) + "\n")
340     f.write("    version: " +  str(deploy_scenario_version) + "\n")
341     f.write("    created: " +  str(deploy_scenario_creation) + "\n")
342     f.write("    sha-1: " +  str(deploy_scenario_sha) + "\n")
343     f.write("    comment: " +  str(deploy_scenario_comment) + "\n")
344     yaml.dump(final_dha_conf, f, default_flow_style=False)