Merge "virtual_fuel: initial support for remote libvirt"
[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         delete(tmp_new_dir)
169
170     def update_fuel_isolinux(self, file):
171         with io.open(file) as f:
172             data = f.read()
173         for key, val in self.fuel_conf.iteritems():
174             # skip replacing these keys, as the format is different
175             if key in ['ip', 'gw', 'netmask', 'hostname']:
176                 continue
177
178             pattern = r'%s=[^ ]\S+' % key
179             replace = '%s=%s' % (key, val)
180             data = re.sub(pattern, replace, data)
181
182         # process networking parameters
183         ip = ':'.join([self.fuel_conf['ip'],
184                       '',
185                       self.fuel_conf['gw'],
186                       self.fuel_conf['netmask'],
187                       self.fuel_conf['hostname'],
188                       'eth0:off:::'])
189
190         data = re.sub(r'ip=[^ ]\S+', 'ip=%s' % ip, data)
191
192         with io.open(file, 'w') as f:
193             f.write(data)
194
195     def parse_iso_volume_label(self, iso_filename):
196         label_line = exec_cmd('isoinfo -d -i %s | grep -i "Volume id: "' % iso_filename)
197         # cut leading text: 'Volume id: '
198         return label_line[11:]
199
200     def deploy_env(self):
201         dep = CloudDeploy(self.dea, self.dha, self.fuel_conf['ip'],
202                           self.fuel_username, self.fuel_password,
203                           self.dea_file, self.fuel_plugins_conf_dir,
204                           WORK_DIR, self.no_health_check, self.deploy_timeout,
205                           self.no_deploy_environment)
206         return dep.deploy()
207
208     def setup_execution_environment(self):
209         exec_env = ExecutionEnvironment(self.storage_dir, self.pxe_bridge,
210                                         self.dha_file, self.dea)
211         exec_env.setup_environment()
212
213     def cleanup_execution_environment(self):
214         exec_env = ExecutionEnvironment(self.storage_dir, self.pxe_bridge,
215                                         self.dha_file, self.dea)
216         exec_env.cleanup_environment()
217
218     def create_tmp_dir(self):
219         self.tmp_dir = '%s/fueltmp' % CWD
220         delete(self.tmp_dir)
221         create_dir_if_not_exists(self.tmp_dir)
222
223     def deploy(self):
224         self.collect_fuel_info()
225         if not self.no_fuel:
226             self.setup_execution_environment()
227             self.create_tmp_dir()
228             self.install_fuel_master()
229         if not self.fuel_only:
230             return self.deploy_env()
231         # Exit status
232         return 0
233
234     def run(self):
235         check_if_root()
236         if self.cleanup_only:
237             self.cleanup_execution_environment()
238         else:
239             deploy_success = self.deploy()
240             if self.cleanup:
241                 self.cleanup_execution_environment()
242             return deploy_success
243         # Exit status
244         return 0
245
246
247 def check_bridge(pxe_bridge, dha_path):
248     # Assume that bridges on remote nodes exists, we could ssh but
249     # the remote user might not have a login shell.
250     if os.environ.get('LIBVIRT_DEFAULT_URI'):
251         return
252
253     with io.open(dha_path) as yaml_file:
254         dha_struct = yaml.load(yaml_file)
255     if dha_struct['adapter'] != 'libvirt':
256         log('Using Linux Bridge %s for booting up the Fuel Master VM'
257             % pxe_bridge)
258         r = exec_cmd('ip link show %s' % pxe_bridge)
259         if pxe_bridge in r and 'state DOWN' in r:
260             err('Linux Bridge {0} is not Active, bring'
261                 ' it UP first: [ip link set dev {0} up]'.format(pxe_bridge))
262
263
264 def check_fuel_plugins_dir(dir):
265     msg = None
266     if not dir:
267         msg = 'Fuel Plugins Directory not specified!'
268     elif not os.path.isdir(dir):
269         msg = 'Fuel Plugins Directory does not exist!'
270     elif not os.listdir(dir):
271         msg = 'Fuel Plugins Directory is empty!'
272     if msg:
273         warn('%s No external plugins will be installed!' % msg)
274
275
276 def parse_arguments():
277     parser = ArgParser(prog='python %s' % __file__)
278     parser.add_argument('-nf', dest='no_fuel', action='store_true',
279                         default=False,
280                         help='Do not install Fuel Master (and Node VMs when '
281                              'using libvirt)')
282     parser.add_argument('-nh', dest='no_health_check', action='store_true',
283                         default=False,
284                         help='Don\'t run health check after deployment')
285     parser.add_argument('-fo', dest='fuel_only', action='store_true',
286                         default=False,
287                         help='Install Fuel Master only (and Node VMs when '
288                              'using libvirt)')
289     parser.add_argument('-co', dest='cleanup_only', action='store_true',
290                         default=False,
291                         help='Cleanup VMs and Virtual Networks according to '
292                              'what is defined in DHA')
293     parser.add_argument('-c', dest='cleanup', action='store_true',
294                         default=False,
295                         help='Cleanup after deploy')
296     if {'-iso', '-dea', '-dha', '-h'}.intersection(sys.argv):
297         parser.add_argument('-iso', dest='iso_file', action='store', nargs='?',
298                             default='%s/OPNFV.iso' % CWD,
299                             help='ISO File [default: OPNFV.iso]')
300         parser.add_argument('-dea', dest='dea_file', action='store', nargs='?',
301                             default='%s/dea.yaml' % CWD,
302                             help='Deployment Environment Adapter: dea.yaml')
303         parser.add_argument('-dha', dest='dha_file', action='store', nargs='?',
304                             default='%s/dha.yaml' % CWD,
305                             help='Deployment Hardware Adapter: dha.yaml')
306     else:
307         parser.add_argument('iso_file', action='store', nargs='?',
308                             default='%s/OPNFV.iso' % CWD,
309                             help='ISO File [default: OPNFV.iso]')
310         parser.add_argument('dea_file', action='store', nargs='?',
311                             default='%s/dea.yaml' % CWD,
312                             help='Deployment Environment Adapter: dea.yaml')
313         parser.add_argument('dha_file', action='store', nargs='?',
314                             default='%s/dha.yaml' % CWD,
315                             help='Deployment Hardware Adapter: dha.yaml')
316     parser.add_argument('-s', dest='storage_dir', action='store',
317                         default='%s/images' % CWD,
318                         help='Storage Directory [default: images]')
319     parser.add_argument('-b', dest='pxe_bridge', action='store',
320                         default='pxebr',
321                         help='Linux Bridge for booting up the Fuel Master VM '
322                              '[default: pxebr]')
323     parser.add_argument('-p', dest='fuel_plugins_dir', action='store',
324                         help='Fuel Plugins directory')
325     parser.add_argument('-pc', dest='fuel_plugins_conf_dir', action='store',
326                         help='Fuel Plugins Configuration directory')
327     parser.add_argument('-np', dest='no_plugins', action='store_true',
328                         default=False, help='Do not install Fuel Plugins')
329     parser.add_argument('-dt', dest='deploy_timeout', action='store',
330                         default=240, help='Deployment timeout (in minutes) '
331                         '[default: 240]')
332     parser.add_argument('-nde', dest='no_deploy_environment',
333                         action='store_true', default=False,
334                         help=('Do not launch environment deployment'))
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     iso_abs_path = os.path.abspath(args.iso_file)
346     if not args.no_fuel and not args.cleanup_only:
347         log('Using OPNFV ISO file: %s' % iso_abs_path)
348         check_file_exists(iso_abs_path)
349         log('Using image directory: %s' % args.storage_dir)
350         create_dir_if_not_exists(args.storage_dir)
351         check_bridge(args.pxe_bridge, args.dha_file)
352
353     kwargs = {'no_fuel': args.no_fuel, 'fuel_only': args.fuel_only,
354               'no_health_check': args.no_health_check,
355               'cleanup_only': args.cleanup_only, 'cleanup': args.cleanup,
356               'storage_dir': args.storage_dir, 'pxe_bridge': args.pxe_bridge,
357               'iso_file': iso_abs_path, 'dea_file': args.dea_file,
358               'dha_file': args.dha_file,
359               'fuel_plugins_dir': args.fuel_plugins_dir,
360               'fuel_plugins_conf_dir': args.fuel_plugins_conf_dir,
361               'no_plugins': args.no_plugins,
362               'deploy_timeout': args.deploy_timeout,
363               'no_deploy_environment': args.no_deploy_environment}
364     return kwargs
365
366
367 def handle_signals(signal_num, frame):
368     signal.signal(signal.SIGINT, signal.SIG_IGN)
369     signal.signal(signal.SIGTERM, signal.SIG_IGN)
370
371     log('Caught signal %s, cleaning up and exiting.' % signal_num)
372
373     mount_point = os.environ.get(MOUNT_STATE_VAR)
374     if mount_point:
375         log('Unmounting ISO from "%s"' % mount_point)
376         # Prevent 'Device or resource busy' errors when unmounting
377         os.chdir('/')
378         exec_cmd('fusermount -u %s' % mount_point, True)
379         # Be nice and remove our environment variable, even though the OS would
380         # would clean it up anyway
381         os.environ.pop(MOUNT_STATE_VAR)
382
383     sys.exit(1)
384
385
386 def main():
387     signal.signal(signal.SIGINT, handle_signals)
388     signal.signal(signal.SIGTERM, handle_signals)
389     kwargs = parse_arguments()
390     d = AutoDeploy(**kwargs)
391     sys.exit(d.run())
392
393 if __name__ == '__main__':
394     main()