Merge "Tell debconf not to be interactive" 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 ip= %s' % isolinux))
152             self.update_fuel_isolinux(isolinux)
153             log('isolinux.cfg after: %s'
154                 % exec_cmd('grep ip= %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 different
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         with io.open(file, 'w') as f:
189             f.write(data)
190
191     def parse_iso_volume_label(self, iso_filename):
192         label_line = exec_cmd('isoinfo -d -i %s | grep -i "Volume id: "' % iso_filename)
193         # cut leading text: 'Volume id: '
194         return label_line[11:]
195
196     def deploy_env(self):
197         dep = CloudDeploy(self.dea, self.dha, self.fuel_conf['ip'],
198                           self.fuel_username, self.fuel_password,
199                           self.dea_file, self.fuel_plugins_conf_dir,
200                           WORK_DIR, self.no_health_check)
201         return dep.deploy()
202
203     def setup_execution_environment(self):
204         exec_env = ExecutionEnvironment(self.storage_dir, self.pxe_bridge,
205                                         self.dha_file, self.dea)
206         exec_env.setup_environment()
207
208     def cleanup_execution_environment(self):
209         exec_env = ExecutionEnvironment(self.storage_dir, self.pxe_bridge,
210                                         self.dha_file, self.dea)
211         exec_env.cleanup_environment()
212
213     def create_tmp_dir(self):
214         self.tmp_dir = '%s/fueltmp' % CWD
215         delete(self.tmp_dir)
216         create_dir_if_not_exists(self.tmp_dir)
217
218     def deploy(self):
219         self.collect_fuel_info()
220         if not self.no_fuel:
221             self.setup_execution_environment()
222             self.create_tmp_dir()
223             self.install_fuel_master()
224         if not self.fuel_only:
225             return self.deploy_env()
226         # Exit status
227         return 0
228
229     def run(self):
230         check_if_root()
231         if self.cleanup_only:
232             self.cleanup_execution_environment()
233         else:
234             deploy_success = self.deploy()
235             if self.cleanup:
236                 self.cleanup_execution_environment()
237             return deploy_success
238         # Exit status
239         return 0
240
241 def check_bridge(pxe_bridge, dha_path):
242     with io.open(dha_path) as yaml_file:
243         dha_struct = yaml.load(yaml_file)
244     if dha_struct['adapter'] != 'libvirt':
245         log('Using Linux Bridge %s for booting up the Fuel Master VM'
246             % pxe_bridge)
247         r = exec_cmd('ip link show %s' % pxe_bridge)
248         if pxe_bridge in r and 'state DOWN' in r:
249             err('Linux Bridge {0} is not Active, bring'
250                 ' it UP first: [ip link set dev {0} up]'.format(pxe_bridge))
251
252
253 def check_fuel_plugins_dir(dir):
254     msg = None
255     if not dir:
256         msg = 'Fuel Plugins Directory not specified!'
257     elif not os.path.isdir(dir):
258         msg = 'Fuel Plugins Directory does not exist!'
259     elif not os.listdir(dir):
260         msg = 'Fuel Plugins Directory is empty!'
261     if msg:
262         warn('%s No external plugins will be installed!' % msg)
263
264
265 def parse_arguments():
266     parser = ArgParser(prog='python %s' % __file__)
267     parser.add_argument('-nf', dest='no_fuel', action='store_true',
268                         default=False,
269                         help='Do not install Fuel Master (and Node VMs when '
270                              'using libvirt)')
271     parser.add_argument('-nh', dest='no_health_check', action='store_true',
272                         default=False,
273                         help='Don\'t run health check after deployment')
274     parser.add_argument('-fo', dest='fuel_only', action='store_true',
275                         default=False,
276                         help='Install Fuel Master only (and Node VMs when '
277                              'using libvirt)')
278     parser.add_argument('-co', dest='cleanup_only', action='store_true',
279                         default=False,
280                         help='Cleanup VMs and Virtual Networks according to '
281                              'what is defined in DHA')
282     parser.add_argument('-c', dest='cleanup', action='store_true',
283                         default=False,
284                         help='Cleanup after deploy')
285     if {'-iso', '-dea', '-dha', '-h'}.intersection(sys.argv):
286         parser.add_argument('-iso', dest='iso_file', action='store', nargs='?',
287                             default='%s/OPNFV.iso' % CWD,
288                             help='ISO File [default: OPNFV.iso]')
289         parser.add_argument('-dea', dest='dea_file', action='store', nargs='?',
290                             default='%s/dea.yaml' % CWD,
291                             help='Deployment Environment Adapter: dea.yaml')
292         parser.add_argument('-dha', dest='dha_file', action='store', nargs='?',
293                             default='%s/dha.yaml' % CWD,
294                             help='Deployment Hardware Adapter: dha.yaml')
295     else:
296         parser.add_argument('iso_file', action='store', nargs='?',
297                             default='%s/OPNFV.iso' % CWD,
298                             help='ISO File [default: OPNFV.iso]')
299         parser.add_argument('dea_file', action='store', nargs='?',
300                             default='%s/dea.yaml' % CWD,
301                             help='Deployment Environment Adapter: dea.yaml')
302         parser.add_argument('dha_file', action='store', nargs='?',
303                             default='%s/dha.yaml' % CWD,
304                             help='Deployment Hardware Adapter: dha.yaml')
305     parser.add_argument('-s', dest='storage_dir', action='store',
306                         default='%s/images' % CWD,
307                         help='Storage Directory [default: images]')
308     parser.add_argument('-b', dest='pxe_bridge', action='store',
309                         default='pxebr',
310                         help='Linux Bridge for booting up the Fuel Master VM '
311                              '[default: pxebr]')
312     parser.add_argument('-p', dest='fuel_plugins_dir', action='store',
313                         help='Fuel Plugins directory')
314     parser.add_argument('-pc', dest='fuel_plugins_conf_dir', action='store',
315                         help='Fuel Plugins Configuration directory')
316     parser.add_argument('-np', dest='no_plugins', action='store_true',
317                         default=False, help='Do not install Fuel Plugins')
318
319     args = parser.parse_args()
320     log(args)
321
322     check_file_exists(args.dha_file)
323
324     if not args.cleanup_only:
325         check_file_exists(args.dea_file)
326         check_fuel_plugins_dir(args.fuel_plugins_dir)
327
328     if not args.no_fuel and not args.cleanup_only:
329         log('Using OPNFV ISO file: %s' % args.iso_file)
330         check_file_exists(args.iso_file)
331         log('Using image directory: %s' % args.storage_dir)
332         create_dir_if_not_exists(args.storage_dir)
333         check_bridge(args.pxe_bridge, args.dha_file)
334
335     kwargs = {'no_fuel': args.no_fuel, 'fuel_only': args.fuel_only,
336               'no_health_check': args.no_health_check,
337               'cleanup_only': args.cleanup_only, 'cleanup': args.cleanup,
338               'storage_dir': args.storage_dir, 'pxe_bridge': args.pxe_bridge,
339               'iso_file': args.iso_file, 'dea_file': args.dea_file,
340               'dha_file': args.dha_file,
341               'fuel_plugins_dir': args.fuel_plugins_dir,
342               'fuel_plugins_conf_dir': args.fuel_plugins_conf_dir,
343               'no_plugins': args.no_plugins}
344     return kwargs
345
346
347 def handle_signals(signal_num, frame):
348     signal.signal(signal.SIGINT, signal.SIG_IGN)
349     signal.signal(signal.SIGTERM, signal.SIG_IGN)
350
351     log('Caught signal %s, cleaning up and exiting.' % signal_num)
352
353     mount_point = os.environ.get(MOUNT_STATE_VAR)
354     if mount_point:
355         log('Unmounting ISO from "%s"' % mount_point)
356         # Prevent 'Device or resource busy' errors when unmounting
357         os.chdir('/')
358         exec_cmd('fusermount -u %s' % mount_point, True)
359         # Be nice and remove our environment variable, even though the OS would
360         # would clean it up anyway
361         os.environ.pop(MOUNT_STATE_VAR)
362
363     sys.exit(1)
364
365
366 def main():
367     signal.signal(signal.SIGINT, handle_signals)
368     signal.signal(signal.SIGTERM, handle_signals)
369     kwargs = parse_arguments()
370     d = AutoDeploy(**kwargs)
371     sys.exit(d.run())
372
373 if __name__ == '__main__':
374     main()