mcp/deploy/scripts: Move to git submodule
[fuel.git] / deploy / reap.py
1 #!/usr/bin/python
2 ###############################################################################
3 # Copyright (c) 2015, 2016 Ericsson AB and others.
4 # szilard.cserey@ericsson.com
5 # peter.barabas@ericsson.com
6 # All rights reserved. This program and the accompanying materials
7 # are made available under the terms of the Apache License, Version 2.0
8 # which accompanies this distribution, and is available at
9 # http://www.apache.org/licenses/LICENSE-2.0
10 ###############################################################################
11
12
13 import time
14 import os
15 import yaml
16 import glob
17 import shutil
18 import tempfile
19 import re
20 import netaddr
21 import templater
22
23 from common import (
24     N,
25     E,
26     R,
27     ArgParser,
28     exec_cmd,
29     parse,
30     err,
31     log,
32     delete,
33     commafy,
34 )
35
36 DEA_1 = '''
37 title: Deployment Environment Adapter (DEA)
38 # DEA API version supported
39 version: 1.1
40 created: {date}
41 comment: {comment}
42 '''
43
44 DHA_1 = '''
45 title: Deployment Hardware Adapter (DHA)
46 # DHA API version supported
47 version: 1.1
48 created: {date}
49 comment: {comment}
50
51 # Adapter to use for this definition
52 # adapter: [ipmi|libvirt]
53 adapter:
54
55 # Node list.
56 # Mandatory properties are id and role.
57 # All other properties are adapter specific.
58 # For Non-Fuel nodes controlled by:
59 #   - ipmi adapter you need to provide:
60 #       pxeMac
61 #       ipmiIp
62 #       ipmiUser
63 #       ipmiPass
64 #     and you *MAY* provide (optional, not added by reap.py):
65 #       ipmiPort
66 #   - libvirt adapter you need to provide:
67 #       libvirtName: <whatever>
68 #       libvirtTemplate: [libvirt/vms/controller.xml | libvirt/vms/compute.xml]
69 #
70 # For the Fuel Node you need to provide:
71 #       libvirtName: <whatever>
72 #       libvirtTemplate: libvirt/vms/fuel.xml
73 #       isFuel: yes
74 #       username: root
75 #       password: r00tme
76 '''
77
78 DHA_2 = '''
79 # Adding the Fuel node as node id {node_id}
80 # which may not be correct - please adjust as needed.
81 '''
82
83 DISKS = {'fuel': '100G',
84          'controller': '100G',
85          'compute': '100G'}
86
87
88 class Reap(object):
89
90     def __init__(self, dea_file, dha_file, comment, base_dea, template):
91         self.dea_file = dea_file
92         self.dha_file = dha_file
93         self.comment = comment
94         self.base_dea = base_dea
95         self.template = template
96         self.temp_dir = None
97         self.env = None
98         self.env_id = None
99         self.last_node = None
100
101     def get_env(self):
102         env_list = parse(exec_cmd('fuel env'))
103         if len(env_list) == 0:
104             err('No environment deployed')
105         elif len(env_list) > 1:
106             err('More than 1 environment deployed')
107         self.env = env_list[0]
108         self.env_id = self.env[E['id']]
109
110     def download_config(self, config_type):
111         log('Download %s config for environment %s'
112             % (config_type, self.env_id))
113         exec_cmd('fuel %s --env %s --download --dir %s'
114                  % (config_type, self.env_id, self.temp_dir))
115
116     def download_node_config(self, nodeid):
117         log('Download node %s config for environment %s to %s'
118             % (nodeid, self.env_id,self.temp_dir))
119         exec_cmd('fuel deployment --node-id %s --env %s  --default --dir %s'
120                  % (nodeid, self.env_id, self.temp_dir))
121
122     def write(self, file, text, newline=True):
123         mode = 'a' if os.path.isfile(file) else 'w'
124         with open(file, mode) as f:
125             f.write('%s%s' % (text, ('\n' if newline else '')))
126
127     def write_yaml(self, file, data, newline=True):
128         self.write(file, yaml.dump(data, default_flow_style=False).strip(),
129                    newline)
130
131     def get_node_by_id(self, node_list, node_id):
132         for node in node_list:
133             if node[N['id']] == node_id:
134                 return node
135
136     def reap_interface(self, node_id, interfaces):
137         interface, mac = self.get_interface(node_id)
138         if_name = None
139         if interfaces:
140             if_name = self.check_dict_exists(interfaces, interface)
141         if not if_name:
142             if_name = 'interfaces_%s' % str(len(interfaces) + 1)
143             interfaces[if_name] = interface
144         return if_name, mac
145
146     def reap_transformation(self, node_id, roles, transformations):
147         main_role = 'controller' if 'controller' in roles else 'compute'
148         node_file = glob.glob('%s/deployment_%s/%s.yaml'
149                               % (self.temp_dir, self.env_id, node_id))
150         tr_name = None
151         with open(node_file[0]) as f:
152             node_config = yaml.load(f)
153         transformation = {'transformations':
154                               node_config['network_scheme']['transformations']}
155         if transformations:
156             tr_name = self.check_dict_exists(transformations, transformation)
157         if not tr_name:
158             tr_name = 'transformations_%s' % str(len(transformations) + 1)
159             transformations[tr_name] = transformation
160         return tr_name
161
162     def check_dict_exists(self, main_dict, dict):
163         for key, val in main_dict.iteritems():
164             if cmp(dict, val) == 0:
165                 return key
166
167     def reap_nodes_interfaces_transformations(self):
168         node_list = parse(exec_cmd('fuel node'))
169         real_node_ids = [node[N['id']] for node in node_list]
170         real_node_ids = map(int, real_node_ids)
171         real_node_ids.sort()
172         min_node = min(real_node_ids)
173         interfaces = {}
174         transformations = {}
175         dea_nodes = []
176         dha_nodes = []
177
178         for real_node_id in real_node_ids:
179             node_id = real_node_id - min_node + 1
180             self.last_node = node_id
181             node = self.get_node_by_id(node_list, str(real_node_id))
182             roles = commafy(node[N['roles']])
183             if not roles:
184                 err('Fuel Node %s has no role' % real_node_id)
185             dea_node = {'id': node_id,
186                         'role': roles}
187             dha_node = {'id': node_id}
188             if_name, mac = self.reap_interface(real_node_id, interfaces)
189             log('reap transformation for node %s' % real_node_id)
190             tr_name = self.reap_transformation(real_node_id, roles,
191                                                transformations)
192             dea_node.update(
193                 {'interfaces': if_name,
194                  'transformations': tr_name})
195
196             dha_node.update(
197                 {'pxeMac': mac if mac else None,
198                  'ipmiIp': None,
199                  'ipmiUser': None,
200                  'ipmiPass': None,
201                  'libvirtName': None,
202                  'libvirtTemplate': None})
203
204             dea_nodes.append(dea_node)
205             dha_nodes.append(dha_node)
206
207         self.write_yaml(self.dha_file, {'nodes': dha_nodes}, False)
208         self.write_yaml(self.dea_file, {'nodes': dea_nodes})
209         self.write_yaml(self.dea_file, interfaces)
210         self.write_yaml(self.dea_file, transformations)
211         self.reap_fuel_node_info()
212         self.write_yaml(self.dha_file, {'disks': DISKS})
213
214     def reap_fuel_node_info(self):
215         dha_nodes = []
216         dha_node = {
217             'id': self.last_node + 1,
218             'libvirtName': None,
219             'libvirtTemplate': None,
220             'isFuel': True,
221             'username': 'root',
222             'password': 'r00tme'}
223
224         dha_nodes.append(dha_node)
225
226         self.write(self.dha_file, DHA_2.format(node_id=dha_node['id']), False)
227         self.write_yaml(self.dha_file, dha_nodes)
228
229     def reap_environment_info(self):
230         network_file = ('%s/network_%s.yaml'
231                         % (self.temp_dir, self.env_id))
232         network = self.read_yaml(network_file)
233
234         env = {'environment':
235                    {'name': self.env[E['name']],
236                     'net_segment_type':
237                         network['networking_parameters']['segmentation_type']}}
238         self.write_yaml(self.dea_file, env)
239         wanted_release = None
240         rel_list = parse(exec_cmd('fuel release'))
241         for rel in rel_list:
242             if rel[R['id']] == self.env[E['release_id']]:
243                 wanted_release = rel[R['name']]
244         self.write_yaml(self.dea_file, {'wanted_release': wanted_release})
245
246     def reap_fuel_settings(self):
247         data = self.read_yaml('/etc/fuel/astute.yaml')
248         fuel = {}
249         del data['ADMIN_NETWORK']['mac']
250         del data['ADMIN_NETWORK']['interface']
251         for key in ['ADMIN_NETWORK', 'HOSTNAME', 'DNS_DOMAIN', 'DNS_SEARCH',
252                     'DNS_UPSTREAM', 'NTP1', 'NTP2', 'NTP3', 'FUEL_ACCESS']:
253             fuel[key] = data[key]
254         for key in fuel['ADMIN_NETWORK'].keys():
255             if key not in ['ipaddress', 'netmask',
256                            'dhcp_pool_start', 'dhcp_pool_end', 'ssh_network']:
257                 del fuel['ADMIN_NETWORK'][key]
258
259         ## FIXME(armband): Factor in support for adding public/other interfaces.
260         ## TODO: Following block expects interface name(s) to be lowercase only
261         interfaces_list = exec_cmd('ip -o -4 a | grep -e "e[nt][hopsx].*"')
262         for interface in re.split('\n', interfaces_list):
263             # Sample output line from above cmd:
264             # 3: eth1 inet 10.0.2.10/24 scope global eth1 valid_lft forever ...
265             ifcfg = re.split(r'\s+', interface)
266             ifcfg_name = ifcfg[1]
267             ifcfg_ipaddr = ifcfg[3]
268
269             # Filter out admin interface (device name is not known, match IP)
270             current_network = netaddr.IPNetwork(ifcfg_ipaddr)
271             if str(current_network.ip) == fuel['ADMIN_NETWORK']['ipaddress']:
272                 continue
273
274             # Read ifcfg-* network interface config file, write IFCFG_<IFNAME>
275             ifcfg_sec = 'IFCFG_%s' % ifcfg_name.upper()
276             fuel[ifcfg_sec] = {}
277             ifcfg_data = {}
278             ifcfg_f = ('/etc/sysconfig/network-scripts/ifcfg-%s' % ifcfg_name)
279             with open(ifcfg_f) as f:
280                 for line in f:
281                     if line.startswith('#'):
282                         continue
283                     (key, val) = line.split('=')
284                     ifcfg_data[key.lower()] = val.rstrip()
285
286             # Keep only needed info (e.g. filter-out type=Ethernet).
287             fuel[ifcfg_sec]['ipaddress'] = ifcfg_data['ipaddr']
288             fuel[ifcfg_sec]['device'] = ifcfg_data['device']
289             fuel[ifcfg_sec]['netmask'] = str(current_network.netmask)
290             fuel[ifcfg_sec]['gateway'] = ifcfg_data['gateway']
291
292         self.write_yaml(self.dea_file, {'fuel': fuel})
293
294     def reap_network_settings(self):
295         network_file = ('%s/network_%s.yaml'
296                         % (self.temp_dir, self.env_id))
297         data = self.read_yaml(network_file)
298         network = {}
299         network['networking_parameters'] = data['networking_parameters']
300         network['networks'] = data['networks']
301         for net in network['networks']:
302             del net['id']
303             del net['group_id']
304         self.write_yaml(self.dea_file, {'network': network})
305
306     def reap_settings(self):
307         settings_file = '%s/settings_%s.yaml' % (self.temp_dir, self.env_id)
308         settings = self.read_yaml(settings_file)
309         self.write_yaml(self.dea_file, {'settings': settings})
310
311     def get_interface(self, real_node_id):
312         exec_cmd('fuel node --node-id %s --network --download --dir %s'
313                  % (real_node_id, self.temp_dir))
314         interface_file = ('%s/node_%s/interfaces.yaml'
315                           % (self.temp_dir, real_node_id))
316         interfaces = self.read_yaml(interface_file)
317         interface_config = {}
318         pxe_mac = None
319         for interface in interfaces:
320             networks = []
321             for network in interface['assigned_networks']:
322                 networks.append(network['name'])
323                 if network['name'] == 'fuelweb_admin':
324                     pxe_mac = interface['mac']
325             if networks:
326                 interface_config[interface['name']] = networks
327         return interface_config, pxe_mac
328
329     def read_yaml(self, yaml_file):
330         with open(yaml_file) as f:
331             data = yaml.load(f)
332             return data
333
334     def intro(self):
335         delete(self.dea_file)
336         delete(self.dha_file)
337
338         self.temp_dir = tempfile.mkdtemp()
339         date = time.strftime('%c')
340         self.write(self.dea_file,
341                    DEA_1.format(date=date, comment=self.comment), False)
342         self.write(self.dha_file,
343                    DHA_1.format(date=date, comment=self.comment))
344         self.get_env()
345
346         # Need to download deployment with explicit node ids
347         node_list = parse(exec_cmd('fuel node'))
348         real_node_ids = [node[N['id']] for node in node_list]
349         real_node_ids.sort()
350         self.download_node_config(','.join(real_node_ids))
351
352         self.download_config('settings')
353         self.download_config('network')
354
355     def create_base_dea(self):
356         templater = templater.Templater(self.dea_file,
357                                         self.template,
358                                         self.base_dea)
359         templater.run()
360
361     def finale(self):
362         log('DEA file is available at %s' % self.dea_file)
363         log('DHA file is available at %s (this is just a template)'
364             % self.dha_file)
365         if self.base_dea:
366             log('DEA base file is available at %s' % self.base_dea)
367         shutil.rmtree(self.temp_dir)
368
369     def reap(self):
370         self.intro()
371         self.reap_environment_info()
372         self.reap_nodes_interfaces_transformations()
373         self.reap_fuel_settings()
374         self.reap_network_settings()
375         self.reap_settings()
376         if self.base_dea:
377             self.create_base_dea()
378         self.finale()
379
380
381 def parse_arguments():
382     parser = ArgParser(prog='python %s' % __file__)
383     parser.add_argument('dea_file', nargs='?', action='store',
384                         default='dea.yaml',
385                         help='Deployment Environment Adapter: dea.yaml')
386     parser.add_argument('dha_file', nargs='?', action='store',
387                         default='dha.yaml',
388                         help='Deployment Hardware Adapter: dha.yaml')
389     parser.add_argument('comment', nargs='?', action='store', help='Comment')
390     parser.add_argument('-base_dea',
391                         dest='base_dea',
392                         help='Create specified base DEA file from "dea_file"')
393     parser.add_argument('-template',
394                         dest='template',
395                         nargs='?',
396                         default='base_dea_template.yaml',
397                         help='Base DEA is generated from this template')
398     args = parser.parse_args()
399     return (args.dea_file,
400             args.dha_file,
401             args.comment,
402             args.base_dea,
403             args.template)
404
405
406 def main():
407     dea_file, dha_file, comment, base_dea, template = parse_arguments()
408
409     r = Reap(dea_file, dha_file, comment, base_dea, template)
410     r.reap()
411
412
413 if __name__ == '__main__':
414     main()