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