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