Merge "Properly clean up after SIGTERM (kill) and Ctrl-c" into stable/brahmaputra
[fuel.git] / deploy / deploy.py
1 #!/usr/bin/python
2 ###############################################################################
3 # Copyright (c) 2015 Ericsson AB and others.
4 # szilard.cserey@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 import os
13 import io
14 import re
15 import sys
16 import yaml
17 import errno
18 import signal
19 import netaddr
20
21 from dea import DeploymentEnvironmentAdapter
22 from dha import DeploymentHardwareAdapter
23 from install_fuel_master import InstallFuelMaster
24 from deploy_env import CloudDeploy
25 from execution_environment import ExecutionEnvironment
26
27 from common import (
28     log,
29     exec_cmd,
30     err,
31     warn,
32     check_file_exists,
33     create_dir_if_not_exists,
34     delete,
35     check_if_root,
36     ArgParser,
37 )
38
39 FUEL_VM = 'fuel'
40 PATCH_DIR = 'fuel_patch'
41 WORK_DIR = '~/deploy'
42 CWD = os.getcwd()
43 MOUNT_STATE_VAR = 'AUTODEPLOY_ISO_MOUNTED'
44
45
46 class cd:
47
48     def __init__(self, new_path):
49         self.new_path = os.path.expanduser(new_path)
50
51     def __enter__(self):
52         self.saved_path = CWD
53         os.chdir(self.new_path)
54
55     def __exit__(self, etype, value, traceback):
56         os.chdir(self.saved_path)
57
58
59 class AutoDeploy(object):
60
61     def __init__(self, no_fuel, fuel_only, no_health_check, cleanup_only,
62                  cleanup, storage_dir, pxe_bridge, iso_file, dea_file,
63                  dha_file, fuel_plugins_dir, fuel_plugins_conf_dir,
64                  no_plugins):
65         self.no_fuel = no_fuel
66         self.fuel_only = fuel_only
67         self.no_health_check = no_health_check
68         self.cleanup_only = cleanup_only
69         self.cleanup = cleanup
70         self.storage_dir = storage_dir
71         self.pxe_bridge = pxe_bridge
72         self.iso_file = iso_file
73         self.dea_file = dea_file
74         self.dha_file = dha_file
75         self.fuel_plugins_dir = fuel_plugins_dir
76         self.fuel_plugins_conf_dir = fuel_plugins_conf_dir
77         self.no_plugins = no_plugins
78         self.dea = (DeploymentEnvironmentAdapter(dea_file)
79                     if not cleanup_only else None)
80         self.dha = DeploymentHardwareAdapter(dha_file)
81         self.fuel_conf = {}
82         self.fuel_node_id = self.dha.get_fuel_node_id()
83         self.fuel_username, self.fuel_password = self.dha.get_fuel_access()
84         self.tmp_dir = None
85
86     def modify_ip(self, ip_addr, index, val):
87         ip_str = str(netaddr.IPAddress(ip_addr))
88         decimal_list = map(int, ip_str.split('.'))
89         decimal_list[index] = val
90         return '.'.join(map(str, decimal_list))
91
92     def collect_fuel_info(self):
93         self.fuel_conf['ip'] = self.dea.get_fuel_ip()
94         self.fuel_conf['gw'] = self.dea.get_fuel_gateway()
95         self.fuel_conf['dns1'] = self.dea.get_fuel_dns()
96         self.fuel_conf['netmask'] = self.dea.get_fuel_netmask()
97         self.fuel_conf['hostname'] = self.dea.get_fuel_hostname()
98         self.fuel_conf['showmenu'] = 'yes'
99
100     def install_fuel_master(self):
101         log('Install Fuel Master')
102         new_iso = ('%s/deploy-%s'
103                    % (self.tmp_dir, os.path.basename(self.iso_file)))
104         self.patch_iso(new_iso)
105         self.iso_file = new_iso
106         self.install_iso()
107
108     def install_iso(self):
109         fuel = InstallFuelMaster(self.dea_file, self.dha_file,
110                                  self.fuel_conf['ip'], self.fuel_username,
111                                  self.fuel_password, self.fuel_node_id,
112                                  self.iso_file, WORK_DIR,
113                                  self.fuel_plugins_dir, self.no_plugins)
114         fuel.install()
115
116     def patch_iso(self, new_iso):
117         tmp_orig_dir = '%s/origiso' % self.tmp_dir
118         tmp_new_dir = '%s/newiso' % self.tmp_dir
119         try:
120             self.copy(tmp_orig_dir, tmp_new_dir)
121             self.patch(tmp_new_dir, new_iso)
122         except Exception as e:
123             exec_cmd('fusermount -u %s' % tmp_orig_dir, False)
124             os.environ.pop(MOUNT_STATE_VAR, None)
125             delete(self.tmp_dir)
126             err(e)
127
128     def copy(self, tmp_orig_dir, tmp_new_dir):
129         log('Copying...')
130         os.makedirs(tmp_orig_dir)
131         os.makedirs(tmp_new_dir)
132         exec_cmd('fuseiso %s %s' % (self.iso_file, tmp_orig_dir))
133         os.environ[MOUNT_STATE_VAR] = tmp_orig_dir
134         with cd(tmp_orig_dir):
135             exec_cmd('find . | cpio -pd %s' % tmp_new_dir)
136         exec_cmd('fusermount -u %s' % tmp_orig_dir)
137         os.environ.pop(MOUNT_STATE_VAR, None)
138         delete(tmp_orig_dir)
139         exec_cmd('chmod -R 755 %s' % tmp_new_dir)
140
141     def patch(self, tmp_new_dir, new_iso):
142         log('Patching...')
143         patch_dir = '%s/%s' % (CWD, PATCH_DIR)
144         ks_path = '%s/ks.cfg.patch' % patch_dir
145
146         with cd(tmp_new_dir):
147             exec_cmd('cat %s | patch -p0' % ks_path)
148             delete('.rr_moved')
149             isolinux = 'isolinux/isolinux.cfg'
150             log('isolinux.cfg before: %s'
151                 % exec_cmd('grep netmask %s' % isolinux))
152             self.update_fuel_isolinux(isolinux)
153             log('isolinux.cfg after: %s'
154                 % exec_cmd('grep netmask %s' % isolinux))
155
156             iso_label = self.parse_iso_volume_label(self.iso_file)
157             log('Volume label: %s' % iso_label)
158
159             iso_linux_bin = 'isolinux/isolinux.bin'
160             exec_cmd('mkisofs -quiet -r -J -R -b %s '
161                      '-no-emul-boot -boot-load-size 4 '
162                      '-boot-info-table -hide-rr-moved '
163                      '-x "lost+found:" -V %s -o %s .'
164                      % (iso_linux_bin, iso_label, new_iso))
165
166     def update_fuel_isolinux(self, file):
167         with io.open(file) as f:
168             data = f.read()
169         for key, val in self.fuel_conf.iteritems():
170             # skip replacing these keys, as the format is custom
171             if key in ['ip', 'gw', 'netmask', 'hostname']:
172                 continue
173
174             pattern = r'%s=[^ ]\S+' % key
175             replace = '%s=%s' % (key, val)
176             data = re.sub(pattern, replace, data)
177
178         # process networking parameters
179         ip = ':'.join([self.fuel_conf['ip'],
180                       '',
181                       self.fuel_conf['gw'],
182                       self.fuel_conf['netmask'],
183                       self.fuel_conf['hostname'],
184                       'eth0:off:::'])
185
186         data = re.sub(r'ip=[^ ]\S+', 'ip=%s' % ip, data)
187
188         netmask = self.fuel_conf['netmask']
189         data = self.append_kernel_param(data, 'netmask=%s' % netmask)
190
191         with io.open(file, 'w') as f:
192             f.write(data)
193
194     def append_kernel_param(self, data, kernel_param):
195         """Append the specified kernel parameter to a list of kernel
196         parameters. Do it only if it isn't already there.
197         """
198         data_final = ''
199         key = re.match(r'(.+?=)', kernel_param).group()
200
201         for line in data.splitlines():
202             data_final += line
203             if (re.search(r'append ', line) and
204                 not re.search(key, line)):
205                 data_final += ' ' + kernel_param
206             data_final += '\n'
207
208         return data_final
209
210     def parse_iso_volume_label(self, iso_filename):
211         label_line = exec_cmd('isoinfo -d -i %s | grep -i "Volume id: "' % iso_filename)
212         # cut leading text: 'Volume id: '
213         return label_line[11:]
214
215     def deploy_env(self):
216         dep = CloudDeploy(self.dea, self.dha, self.fuel_conf['ip'],
217                           self.fuel_username, self.fuel_password,
218                           self.dea_file, self.fuel_plugins_conf_dir,
219                           WORK_DIR, self.no_health_check)
220         return dep.deploy()
221
222     def setup_execution_environment(self):
223         exec_env = ExecutionEnvironment(self.storage_dir, self.pxe_bridge,
224                                         self.dha_file, self.dea)
225         exec_env.setup_environment()
226
227     def cleanup_execution_environment(self):
228         exec_env = ExecutionEnvironment(self.storage_dir, self.pxe_bridge,
229                                         self.dha_file, self.dea)
230         exec_env.cleanup_environment()
231
232     def create_tmp_dir(self):
233         self.tmp_dir = '%s/fueltmp' % CWD
234         delete(self.tmp_dir)
235         create_dir_if_not_exists(self.tmp_dir)
236
237     def deploy(self):
238         self.collect_fuel_info()
239         if not self.no_fuel:
240             self.setup_execution_environment()
241             self.create_tmp_dir()
242             self.install_fuel_master()
243         if not self.fuel_only:
244             return self.deploy_env()
245         return True
246
247     def run(self):
248         check_if_root()
249         if self.cleanup_only:
250             self.cleanup_execution_environment()
251         else:
252             deploy_success = self.deploy()
253             if self.cleanup:
254                 self.cleanup_execution_environment()
255             return deploy_success
256         return True
257
258 def check_bridge(pxe_bridge, dha_path):
259     with io.open(dha_path) as yaml_file:
260         dha_struct = yaml.load(yaml_file)
261     if dha_struct['adapter'] != 'libvirt':
262         log('Using Linux Bridge %s for booting up the Fuel Master VM'
263             % pxe_bridge)
264         r = exec_cmd('ip link show %s' % pxe_bridge)
265         if pxe_bridge in r and 'state DOWN' in r:
266             err('Linux Bridge {0} is not Active, bring'
267                 ' it UP first: [ip link set dev {0} up]'.format(pxe_bridge))
268
269
270 def check_fuel_plugins_dir(dir):
271     msg = None
272     if not dir:
273         msg = 'Fuel Plugins Directory not specified!'
274     elif not os.path.isdir(dir):
275         msg = 'Fuel Plugins Directory does not exist!'
276     elif not os.listdir(dir):
277         msg = 'Fuel Plugins Directory is empty!'
278     if msg:
279         warn('%s No external plugins will be installed!' % msg)
280
281
282 def parse_arguments():
283     parser = ArgParser(prog='python %s' % __file__)
284     parser.add_argument('-nf', dest='no_fuel', action='store_true',
285                         default=False,
286                         help='Do not install Fuel Master (and Node VMs when '
287                              'using libvirt)')
288     parser.add_argument('-nh', dest='no_health_check', action='store_true',
289                         default=False,
290                         help='Don\'t run health check after deployment')
291     parser.add_argument('-fo', dest='fuel_only', action='store_true',
292                         default=False,
293                         help='Install Fuel Master only (and Node VMs when '
294                              'using libvirt)')
295     parser.add_argument('-co', dest='cleanup_only', action='store_true',
296                         default=False,
297                         help='Cleanup VMs and Virtual Networks according to '
298                              'what is defined in DHA')
299     parser.add_argument('-c', dest='cleanup', action='store_true',
300                         default=False,
301                         help='Cleanup after deploy')
302     if {'-iso', '-dea', '-dha', '-h'}.intersection(sys.argv):
303         parser.add_argument('-iso', dest='iso_file', action='store', nargs='?',
304                             default='%s/OPNFV.iso' % CWD,
305                             help='ISO File [default: OPNFV.iso]')
306         parser.add_argument('-dea', dest='dea_file', action='store', nargs='?',
307                             default='%s/dea.yaml' % CWD,
308                             help='Deployment Environment Adapter: dea.yaml')
309         parser.add_argument('-dha', dest='dha_file', action='store', nargs='?',
310                             default='%s/dha.yaml' % CWD,
311                             help='Deployment Hardware Adapter: dha.yaml')
312     else:
313         parser.add_argument('iso_file', action='store', nargs='?',
314                             default='%s/OPNFV.iso' % CWD,
315                             help='ISO File [default: OPNFV.iso]')
316         parser.add_argument('dea_file', action='store', nargs='?',
317                             default='%s/dea.yaml' % CWD,
318                             help='Deployment Environment Adapter: dea.yaml')
319         parser.add_argument('dha_file', action='store', nargs='?',
320                             default='%s/dha.yaml' % CWD,
321                             help='Deployment Hardware Adapter: dha.yaml')
322     parser.add_argument('-s', dest='storage_dir', action='store',
323                         default='%s/images' % CWD,
324                         help='Storage Directory [default: images]')
325     parser.add_argument('-b', dest='pxe_bridge', action='store',
326                         default='pxebr',
327                         help='Linux Bridge for booting up the Fuel Master VM '
328                              '[default: pxebr]')
329     parser.add_argument('-p', dest='fuel_plugins_dir', action='store',
330                         help='Fuel Plugins directory')
331     parser.add_argument('-pc', dest='fuel_plugins_conf_dir', action='store',
332                         help='Fuel Plugins Configuration directory')
333     parser.add_argument('-np', dest='no_plugins', action='store_true',
334                         default=False, help='Do not install Fuel Plugins')
335
336     args = parser.parse_args()
337     log(args)
338
339     check_file_exists(args.dha_file)
340
341     if not args.cleanup_only:
342         check_file_exists(args.dea_file)
343         check_fuel_plugins_dir(args.fuel_plugins_dir)
344
345     if not args.no_fuel and not args.cleanup_only:
346         log('Using OPNFV ISO file: %s' % args.iso_file)
347         check_file_exists(args.iso_file)
348         log('Using image directory: %s' % args.storage_dir)
349         create_dir_if_not_exists(args.storage_dir)
350         check_bridge(args.pxe_bridge, args.dha_file)
351
352     kwargs = {'no_fuel': args.no_fuel, 'fuel_only': args.fuel_only,
353               'no_health_check': args.no_health_check,
354               'cleanup_only': args.cleanup_only, 'cleanup': args.cleanup,
355               'storage_dir': args.storage_dir, 'pxe_bridge': args.pxe_bridge,
356               'iso_file': args.iso_file, 'dea_file': args.dea_file,
357               'dha_file': args.dha_file,
358               'fuel_plugins_dir': args.fuel_plugins_dir,
359               'fuel_plugins_conf_dir': args.fuel_plugins_conf_dir,
360               'no_plugins': args.no_plugins}
361     return kwargs
362
363
364 def handle_signals(signal_num, frame):
365     signal.signal(signal.SIGINT, signal.SIG_IGN)
366     signal.signal(signal.SIGTERM, signal.SIG_IGN)
367
368     log('Caught signal %s, cleaning up and exiting.' % signal_num)
369
370     mount_point = os.environ.get(MOUNT_STATE_VAR)
371     if mount_point:
372         log('Unmounting ISO from "%s"' % mount_point)
373         # Prevent 'Device or resource busy' errors when unmounting
374         os.chdir('/')
375         exec_cmd('fusermount -u %s' % mount_point, True)
376         # Be nice and remove our environment variable, even though the OS would
377         # would clean it up anyway
378         os.environ.pop(MOUNT_STATE_VAR)
379
380     sys.exit(1)
381
382
383 def main():
384     signal.signal(signal.SIGINT, handle_signals)
385     signal.signal(signal.SIGTERM, handle_signals)
386     kwargs = parse_arguments()
387     d = AutoDeploy(**kwargs)
388     sys.exit(d.run())
389
390 if __name__ == '__main__':
391     main()