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