Repo tidy-up + licence scrubing
[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
22 from common import (
23     N,
24     E,
25     R,
26     ArgParser,
27     exec_cmd,
28     parse,
29     err,
30     log,
31     delete,
32     commafy,
33 )
34
35 DEA_1 = '''
36 title: Deployment Environment Adapter (DEA)
37 # DEA API version supported
38 version: 1.1
39 created: {date}
40 comment: {comment}
41 '''
42
43 DHA_1 = '''
44 title: Deployment Hardware Adapter (DHA)
45 # DHA API version supported
46 version: 1.1
47 created: {date}
48 comment: {comment}
49
50 # Adapter to use for this definition
51 # adapter: [ipmi|libvirt]
52 adapter:
53
54 # Node list.
55 # Mandatory properties are id and role.
56 # All other properties are adapter specific.
57 # For Non-Fuel nodes controlled by:
58 #   - ipmi adapter you need to provide:
59 #       pxeMac
60 #       ipmiIp
61 #       ipmiUser
62 #       ipmiPass
63 #     and you *MAY* provide (optional, not added by reap.py):
64 #       ipmiPort
65 #   - libvirt adapter you need to provide:
66 #       libvirtName: <whatever>
67 #       libvirtTemplate: [libvirt/vms/controller.xml | libvirt/vms/compute.xml]
68 #
69 # For the Fuel Node you need to provide:
70 #       libvirtName: <whatever>
71 #       libvirtTemplate: libvirt/vms/fuel.xml
72 #       isFuel: yes
73 #       username: root
74 #       password: r00tme
75 '''
76
77 DHA_2 = '''
78 # Adding the Fuel node as node id {node_id}
79 # which may not be correct - please adjust as needed.
80 '''
81
82 TEMPLATER = 'templater.py'
83
84 DISKS = {'fuel': '100G',
85          'controller': '100G',
86          'compute': '100G'}
87
88
89 class Reap(object):
90
91     def __init__(self, dea_file, dha_file, comment, base_dea, template):
92         self.dea_file = dea_file
93         self.dha_file = dha_file
94         self.comment = comment
95         self.base_dea = base_dea
96         self.template = template
97         self.temp_dir = None
98         self.env = None
99         self.env_id = None
100         self.last_node = None
101
102     def get_env(self):
103         env_list = parse(exec_cmd('fuel env'))
104         if len(env_list) == 0:
105             err('No environment deployed')
106         elif len(env_list) > 1:
107             err('More than 1 environment deployed')
108         self.env = env_list[0]
109         self.env_id = self.env[E['id']]
110
111     def download_config(self, config_type):
112         log('Download %s config for environment %s'
113             % (config_type, self.env_id))
114         exec_cmd('fuel %s --env %s --download --dir %s'
115                  % (config_type, self.env_id, self.temp_dir))
116
117     def download_node_config(self, nodeid):
118         log('Download node %s config for environment %s to %s'
119             % (nodeid, self.env_id,self.temp_dir))
120         exec_cmd('fuel deployment --node-id %s --env %s  --default --dir %s'
121                  % (nodeid, self.env_id, self.temp_dir))
122
123     def write(self, file, text, newline=True):
124         mode = 'a' if os.path.isfile(file) else 'w'
125         with open(file, mode) as f:
126             f.write('%s%s' % (text, ('\n' if newline else '')))
127
128     def write_yaml(self, file, data, newline=True):
129         self.write(file, yaml.dump(data, default_flow_style=False).strip(),
130                    newline)
131
132     def get_node_by_id(self, node_list, node_id):
133         for node in node_list:
134             if node[N['id']] == node_id:
135                 return node
136
137     def reap_interface(self, node_id, interfaces):
138         interface, mac = self.get_interface(node_id)
139         if_name = None
140         if interfaces:
141             if_name = self.check_dict_exists(interfaces, interface)
142         if not if_name:
143             if_name = 'interfaces_%s' % str(len(interfaces) + 1)
144             interfaces[if_name] = interface
145         return if_name, mac
146
147     def reap_transformation(self, node_id, roles, transformations):
148         main_role = 'controller' if 'controller' in roles else 'compute'
149         node_file = glob.glob('%s/deployment_%s/%s.yaml'
150                               % (self.temp_dir, self.env_id, node_id))
151         tr_name = None
152         with open(node_file[0]) as f:
153             node_config = yaml.load(f)
154         transformation = {'transformations':
155                               node_config['network_scheme']['transformations']}
156         if transformations:
157             tr_name = self.check_dict_exists(transformations, transformation)
158         if not tr_name:
159             tr_name = 'transformations_%s' % str(len(transformations) + 1)
160             transformations[tr_name] = transformation
161         return tr_name
162
163     def check_dict_exists(self, main_dict, dict):
164         for key, val in main_dict.iteritems():
165             if cmp(dict, val) == 0:
166                 return key
167
168     def reap_nodes_interfaces_transformations(self):
169         node_list = parse(exec_cmd('fuel node'))
170         real_node_ids = [node[N['id']] for node in node_list]
171         real_node_ids.sort()
172         min_node = real_node_ids[0]
173         interfaces = {}
174         transformations = {}
175         dea_nodes = []
176         dha_nodes = []
177
178         for real_node_id in real_node_ids:
179             node_id = int(real_node_id) - int(min_node) + 1
180             self.last_node = node_id
181             node = self.get_node_by_id(node_list, 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         exec_cmd('python %s %s %s %s'
357                  % (TEMPLATER, self.dea_file, self.template, self.base_dea))
358
359     def finale(self):
360         log('DEA file is available at %s' % self.dea_file)
361         log('DHA file is available at %s (this is just a template)'
362             % self.dha_file)
363         if self.base_dea:
364             log('DEA base file is available at %s' % self.base_dea)
365         shutil.rmtree(self.temp_dir)
366
367     def reap(self):
368         self.intro()
369         self.reap_environment_info()
370         self.reap_nodes_interfaces_transformations()
371         self.reap_fuel_settings()
372         self.reap_network_settings()
373         self.reap_settings()
374         if self.base_dea:
375             self.create_base_dea()
376         self.finale()
377
378
379 def parse_arguments():
380     parser = ArgParser(prog='python %s' % __file__)
381     parser.add_argument('dea_file', nargs='?', action='store',
382                         default='dea.yaml',
383                         help='Deployment Environment Adapter: dea.yaml')
384     parser.add_argument('dha_file', nargs='?', action='store',
385                         default='dha.yaml',
386                         help='Deployment Hardware Adapter: dha.yaml')
387     parser.add_argument('comment', nargs='?', action='store', help='Comment')
388     parser.add_argument('-base_dea',
389                         dest='base_dea',
390                         help='Create specified base DEA file from "dea_file"')
391     parser.add_argument('-template',
392                         dest='template',
393                         nargs='?',
394                         default='base_dea_template.yaml',
395                         help='Base DEA is generated from this template')
396     args = parser.parse_args()
397     return (args.dea_file,
398             args.dha_file,
399             args.comment,
400             args.base_dea,
401             args.template)
402
403
404 def main():
405     dea_file, dha_file, comment, base_dea, template = parse_arguments()
406
407     r = Reap(dea_file, dha_file, comment, base_dea, template)
408     r.reap()
409
410
411 if __name__ == '__main__':
412     main()