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