installer/installer.py
f4d17450
 #    Copyright (C) 2015 vmware inc.
 #
 #    Author: Mahmoud Bassiouny <mbassiouny@vmware.com>
 
 import subprocess
 import curses
 import os
 import crypt
 import re
 import random
 import string
 import shutil
 import fnmatch
 import signal
 import sys
4967065a
 import glob
 import modules.commons
f4d17450
 from jsonwrapper import JsonWrapper
 from progressbar import ProgressBar
 from window import Window
 from actionresult import ActionResult
57342c05
 from __builtin__ import isinstance
f4d17450
 
 class Installer(object):
85904234
     def __init__(self, install_config, maxy = 0, maxx = 0, iso_installer = False,
1b11aa2d
                  rpm_path = "../stage/RPMS", log_path = "../stage/LOGS"):
f4d17450
         self.install_config = install_config
         self.iso_installer = iso_installer
         self.rpm_path = rpm_path
         self.log_path = log_path
         self.mount_command = "./mk-mount-disk.sh"
         self.prepare_command = "./mk-prepare-system.sh"
         self.finalize_command = "./mk-finalize-system.sh"
         self.chroot_command = "./mk-run-chroot.sh"
         self.setup_grub_command = "./mk-setup-grub.sh"
         self.unmount_disk_command = "./mk-unmount-disk.sh"
 
cad80a3b
         if 'working_directory' in self.install_config:
b84da80d
             self.working_directory = self.install_config['working_directory']
f4d17450
         else:
b84da80d
             self.working_directory = "/mnt/photon-root"
         self.photon_root = self.working_directory + "/photon-chroot";
f4d17450
 
         self.restart_command = "shutdown"
 
         if self.iso_installer:
             self.output = open(os.devnull, 'w')
         else:
             self.output = None
 
         if self.iso_installer:
             #initializing windows
             self.maxy = maxy
             self.maxx = maxx
             self.height = 10
             self.width = 75
             self.progress_padding = 5
 
             self.progress_width = self.width - self.progress_padding
             self.starty = (self.maxy - self.height) / 2
             self.startx = (self.maxx - self.width) / 2
72832a72
             self.window = Window(self.height, self.width, self.maxy, self.maxx, 'Installing Photon', False, items =[])
f4d17450
             self.progress_bar = ProgressBar(self.starty + 3, self.startx + self.progress_padding / 2, self.progress_width)
 
         signal.signal(signal.SIGINT, self.exit_gracefully)
 
     # This will be called if the installer interrupted by Ctrl+C or exception
     def exit_gracefully(self, signal, frame):
         if self.iso_installer:
             self.progress_bar.hide()
1b11aa2d
             self.window.addstr(0, 0, 'Oops, Installer got interrupted.\n\nPress any key to get to the bash...')
f4d17450
             self.window.content_window().getch()
022959f9
 
cad80a3b
         modules.commons.dump(modules.commons.LOG_FILE_NAME)
f4d17450
         sys.exit(1)
 
     def install(self, params):
         try:
             return self.unsafe_install(params)
34dfc7d7
         except Exception as inst:
f4d17450
             if self.iso_installer:
34dfc7d7
                 modules.commons.log(modules.commons.LOG_ERROR, repr(inst))
f4d17450
                 self.exit_gracefully(None, None)
             else:
                 raise
 
     def unsafe_install(self, params):
 
         if self.iso_installer:
             self.window.show_window()
b84da80d
             self.progress_bar.initialize('Initializing installation...')
f4d17450
             self.progress_bar.show()
6860f77c
             #self.rpm_path = "https://dl.bintray.com/vmware/photon_release_1.0_TP2_x86_64"
             if self.rpm_path.startswith("https://") or self.rpm_path.startswith("http://"):
                 cmdoption = 's/baseurl.*/baseurl={}/g'.format(self.rpm_path.replace('/','\/'))
                 process = subprocess.Popen(['sed', '-i', cmdoption,'/etc/yum.repos.d/photon-iso.repo']) 
                 retval = process.wait()
                 if retval != 0:
                     modules.commons.log(modules.commons.LOG_INFO, "Failed to reset repo")
                     self.exit_gracefully(None, None)
adde0296
 
             cmdoption = 's/cachedir=\/var/cachedir={}/g'.format(self.photon_root.replace('/','\/'))
             process = subprocess.Popen(['sed', '-i', cmdoption,'/etc/tdnf/tdnf.conf']) 
             retval = process.wait()
             if retval != 0:
                 modules.commons.log(modules.commons.LOG_INFO, "Failed to reset tdnf cachedir")
                 self.exit_gracefully(None, None)
4967065a
         self.execute_modules(modules.commons.PRE_INSTALL)
 
b84da80d
         self.initialize_system()
f4d17450
 
6860f77c
         if self.iso_installer:
564d9533
             self.adjust_packages_for_vmware_virt()
6860f77c
             selected_packages = self.install_config['packages']
9d42f28e
             state = 0
             packages_to_install = {}
             total_size = 0
             with open(modules.commons.TDNF_CMDLINE_FILE_NAME, "w") as tdnf_cmdline_file:
                 tdnf_cmdline_file.write("tdnf install --installroot {0} --nogpgcheck {1}".format(self.photon_root, " ".join(selected_packages)))
             with open(modules.commons.TDNF_LOG_FILE_NAME, "w") as tdnf_errlog:
                 process = subprocess.Popen(
                     ['tdnf', 'install'] + selected_packages + ['--installroot', self.photon_root, '--nogpgcheck', '--assumeyes'], stdout=subprocess.PIPE, stderr=tdnf_errlog)
                 while True:
                     output = process.stdout.readline()
                     if output == '':
                         retval = process.poll()
                         if retval is not None:
                             break
                     if state == 0:
                         if output == 'Installing:\n':
                             state = 1
                     elif state == 1: #N A EVR Size(readable) Size(in bytes)
                         if output == '\n':
                             state = 2
                             self.progress_bar.update_num_items(total_size)
                         else:
                             info = output.split()
                             package = '{0}-{1}.{2}'.format(info[0], info[2], info[1])
                             packages_to_install[package] = int(info[5])
                             total_size += int(info[5])
                     elif state == 2:
                         if output == 'Downloading:\n':
                             self.progress_bar.update_message('Preparing ...')
                             state = 3
                     elif state == 3:
f5cac196
                         self.progress_bar.update_message(output)
9d42f28e
                         if output == 'Running transaction\n':
                             state = 4
                     else:
f5cac196
                         modules.commons.log(modules.commons.LOG_INFO, "[tdnf] {0}".format(output))
9d42f28e
                         prefix = 'Installing/Updating: '
                         if output.startswith(prefix):
                             package = output[len(prefix):].rstrip('\n')
                             self.progress_bar.increment(packages_to_install[package])
                         self.progress_bar.update_message(output)
6860f77c
                 # 0 : succeed; 137 : package already installed; 65 : package not found in repo.
                 if retval != 0 and retval != 137:
9d42f28e
                     modules.commons.log(modules.commons.LOG_ERROR, "Failed to install some packages, refer to {0}".format(modules.commons.TDNF_LOG_FILE_NAME))
6860f77c
                     self.exit_gracefully(None, None)
         else:
f4d17450
         #install packages
f5cac196
             rpms = []
6860f77c
             for rpm in self.rpms_tobeinstalled:
                 # We already installed the filesystem in the preparation
                 if rpm['package'] == 'filesystem':
                     continue
f5cac196
                 rpms.append(rpm['filename'])
             return_value = self.install_package(rpms)
             if return_value != 0:
                 self.exit_gracefully(None, None)
6860f77c
 
f4d17450
 
         if self.iso_installer:
             self.progress_bar.show_loading('Finalizing installation')
b84da80d
 
efdf7f57
         shutil.copy("/etc/resolv.conf", self.photon_root + '/etc/.')
f4d17450
         self.finalize_system()
 
b84da80d
         if not self.install_config['iso_system']:
4967065a
             # Execute post installation modules
             self.execute_modules(modules.commons.POST_INSTALL)
 
a6e91563
             if self.iso_installer and os.path.isdir("/sys/firmware/efi"):
                 self.install_config['boot'] = 'efi'
f4d17450
             # install grub
85904234
             if 'boot_partition_number' not in self.install_config['disk']:
                 self.install_config['disk']['boot_partition_number'] = 1
 
7fd875e7
             try:
                 if self.install_config['boot'] == 'bios':
85904234
                     process = subprocess.Popen(
                                   [self.setup_grub_command, '-w', self.photon_root,
                                   "bios", self.install_config['disk']['disk'],
                                   self.install_config['disk']['root'],
                                   self.install_config['disk']['boot'],
                                   self.install_config['disk']['bootdirectory'],
                                   str(self.install_config['disk']['boot_partition_number'])],
                                   stdout=self.output)
7fd875e7
                 elif self.install_config['boot'] == 'efi':
85904234
                     process = subprocess.Popen(
                                   [self.setup_grub_command, '-w', self.photon_root,
                                   "efi", self.install_config['disk']['disk'],
                                   self.install_config['disk']['root'],
                                   self.install_config['disk']['boot'],
                                   self.install_config['disk']['bootdirectory'],
                                   str(self.install_config['disk']['boot_partition_number'])],
                                   stdout=self.output)
7fd875e7
             except:
                 #install bios if variable is not set.
85904234
                 process = subprocess.Popen(
                               [self.setup_grub_command, '-w', self.photon_root,
                               "bios", self.install_config['disk']['disk'],
                               self.install_config['disk']['root'],
                               self.install_config['disk']['boot'],
                               self.install_config['disk']['bootdirectory'],
                               str(self.install_config['disk']['boot_partition_number'])],
                               stdout=self.output)
f4d17450
             retval = process.wait()
 
c3771c35
             self.update_fstab()
 
20741901
         if os.path.exists(self.photon_root + '/etc/resolv.conf'):
             os.remove(self.photon_root + '/etc/resolv.conf')
 
c3771c35
         command = [self.unmount_disk_command, '-w', self.photon_root]
         if not self.install_config['iso_system']:
             command.extend(self.generate_partitions_param(reverse = True))
         process = subprocess.Popen(command, stdout=self.output)
f4d17450
         retval = process.wait()
 
         if self.iso_installer:
             self.progress_bar.hide()
             self.window.addstr(0, 0, 'Congratulations, Photon has been installed in {0} secs.\n\nPress any key to continue to boot...'.format(self.progress_bar.time_elapsed))
e63361bf
             eject_cdrom = True
1b11aa2d
             if 'ui_install' in self.install_config:
4967065a
                 self.window.content_window().getch()
e63361bf
             if 'eject_cdrom' in self.install_config and not self.install_config['eject_cdrom']:
                 eject_cdrom = False
             if eject_cdrom:
                 process = subprocess.Popen(['eject', '-r'], stdout=self.output)
                 process.wait()
f4d17450
         return ActionResult(True, None)
bffb7f74
         
b84da80d
     def copy_rpms(self):
         # prepare the RPMs list
b944098f
         json_pkg_to_rpm_map = JsonWrapper(self.install_config["pkg_to_rpm_map_file"])
         pkg_to_rpm_map = json_pkg_to_rpm_map.read()
f4d17450
 
         self.rpms_tobeinstalled = []
         selected_packages = self.install_config['packages']
b944098f
 
         for pkg in selected_packages: 
             if pkg in pkg_to_rpm_map:
                 if not pkg_to_rpm_map[pkg]['rpm'] is None:
                     name = pkg_to_rpm_map[pkg]['rpm']
                     basename = os.path.basename(name)
                     self.rpms_tobeinstalled.append({'filename': basename, 'path': name, 'package' : pkg})
 
b84da80d
         # Copy the rpms
f4d17450
         for rpm in self.rpms_tobeinstalled:
b84da80d
             shutil.copy(rpm['path'], self.photon_root + '/RPMS/')
f4d17450
 
b84da80d
     def copy_files(self):
         # Make the photon_root directory if not exits
         process = subprocess.Popen(['mkdir', '-p', self.photon_root], stdout=self.output)
         retval = process.wait()
 
         # Copy the installer files
         process = subprocess.Popen(['cp', '-r', "../installer", self.photon_root], stdout=self.output)
         retval = process.wait()
 
bffb7f74
         # Create the rpms directory
         process = subprocess.Popen(['mkdir', '-p', self.photon_root + '/RPMS'], stdout=self.output)
         retval = process.wait()
6860f77c
         self.copy_rpms()
bffb7f74
 
6860f77c
     def bind_installer(self):
         # Make the photon_root/installer directory if not exits
         process = subprocess.Popen(['mkdir', '-p', os.path.join(self.photon_root, "installer")], stdout=self.output)
         retval = process.wait()
         # The function finalize_system will access the file /installer/mk-finalize-system.sh after chroot to photon_root. 
         # Bind the /installer folder to self.photon_root/installer, so that after chroot to photon_root,
         # the file can still be accessed as /installer/mk-finalize-system.sh.
         process = subprocess.Popen(['mount', '--bind', '/installer', os.path.join(self.photon_root, "installer")], stdout=self.output)
         retval = process.wait()
b84da80d
 
9d42f28e
     def bind_repo_dir(self):
         rpm_cache_dir = self.photon_root + '/cache/tdnf/photon-iso/rpms'
f5cac196
         if self.rpm_path.startswith("https://") or self.rpm_path.startswith("http://"):
             return
9d42f28e
         if subprocess.call(['mkdir', '-p', rpm_cache_dir]) != 0 or subprocess.call(['mount', '--bind', self.rpm_path, rpm_cache_dir]) != 0:
             modules.commons.log(modules.commons.LOG_ERROR, "Fail to bind cache rpms")
             self.exit_gracefully(None, None)
 
c3771c35
     def update_fstab(self):
         fstab_file = open(os.path.join(self.photon_root, "etc/fstab"), "w")
         fstab_file.write("#system\tmnt-pt\ttype\toptions\tdump\tfsck\n")
 
         for partition in self.install_config['disk']['partitions']:
             options = 'defaults'
             dump = 1
             fsck = 2
 
             if 'mountpoint' in partition and partition['mountpoint'] == '/':
72832a72
                 options = options + ',barrier,noatime,noacl,data=ordered'
c3771c35
                 fsck = 1
             
             if partition['filesystem'] == 'swap':
                 mountpoint = 'swap'
                 dump = 0
                 fsck = 0
             else:
                 mountpoint = partition['mountpoint']
 
             fstab_file.write("{}\t{}\t{}\t{}\t{}\t{}\n".format(
                 partition['path'],
                 mountpoint,
                 partition['filesystem'],
                 options,
                 dump,
                 fsck
                 ))
28b4d571
         # Add the cdrom entry
         fstab_file.write("/dev/cdrom\t/mnt/cdrom\tiso9660\tro,noauto\t0\t0\n")
c3771c35
 
         fstab_file.close()
 
     def generate_partitions_param(self, reverse = False):
         if reverse:
             step = -1
         else:
             step = 1
         params = []
         for partition in self.install_config['disk']['partitions'][::step]:
             if partition["filesystem"] == "swap":
                 continue
 
             params.extend(['--partitionmountpoint', partition["path"], partition["mountpoint"]])
         return params
 
b84da80d
     def initialize_system(self):
f4d17450
         #Setup the disk
b84da80d
         if (not self.install_config['iso_system']):
c3771c35
             command = [self.mount_command, '-w', self.photon_root]
             command.extend(self.generate_partitions_param())
             process = subprocess.Popen(command, stdout=self.output)
f4d17450
             retval = process.wait()
b84da80d
         
6860f77c
         if self.iso_installer:
             self.bind_installer()
9d42f28e
             self.bind_repo_dir()
6860f77c
             process = subprocess.Popen([self.prepare_command, '-w', self.photon_root, 'install'], stdout=self.output)
             retval = process.wait()
         else:
             self.copy_files()
             #Setup the filesystem basics
             process = subprocess.Popen([self.prepare_command, '-w', self.photon_root], stdout=self.output)
             retval = process.wait()
cad80a3b
 
f4d17450
     def finalize_system(self):
         #Setup the disk
b84da80d
         process = subprocess.Popen([self.chroot_command, '-w', self.photon_root, self.finalize_command, '-w', self.photon_root], stdout=self.output)
f4d17450
         retval = process.wait()
         if self.iso_installer:
562cb584
 
             modules.commons.dump(modules.commons.LOG_FILE_NAME)
             shutil.copy(modules.commons.LOG_FILE_NAME, self.photon_root + '/var/log/')
9d42f28e
             shutil.copy(modules.commons.TDNF_LOG_FILE_NAME, self.photon_root + '/var/log/')
562cb584
 
6860f77c
             # unmount the installer directory
             process = subprocess.Popen(['umount', os.path.join(self.photon_root, "installer")], stdout=self.output)
             retval = process.wait()
e9ff2627
             # remove the installer directory
             process = subprocess.Popen(['rm', '-rf', os.path.join(self.photon_root, "installer")], stdout=self.output)
             retval = process.wait()
adde0296
             # Disable the swap file
             process = subprocess.Popen(['swapoff', '-a'], stdout=self.output)
f4d17450
             retval = process.wait()
adde0296
             # remove the tdnf cache directory and the swapfile.
             process = subprocess.Popen(['rm', '-rf', os.path.join(self.photon_root, "cache")], stdout=self.output)
f5cac196
             retval = process.wait()
f4d17450
 
f5cac196
     def install_package(self,  rpm_file_names):
85b82155
 
f5cac196
         rpms = set(rpm_file_names)
         rpm_paths = []
         for root, dirs, files in os.walk(self.rpm_path):
             for f in files:
                 if f in rpms:
                     rpm_paths.append(os.path.join(root, f))
85b82155
 
f5cac196
         rpm_params = ['--root', self.photon_root, '--dbpath', '/var/lib/rpm']
f4d17450
 
f5cac196
         if ('type' in self.install_config and (self.install_config['type'] in ['micro', 'minimal'])) or self.install_config['iso_system']:
             rpm_params.append('--excludedocs')
85b82155
 
f5cac196
         modules.commons.log(modules.commons.LOG_INFO, "installing packages {0}, with params {1}".format(rpm_paths, rpm_params))
         process = subprocess.Popen(['rpm', '-Uvh'] + rpm_params + rpm_paths, stderr=subprocess.STDOUT)
f4d17450
         return process.wait()
 
4967065a
     def execute_modules(self, phase):
022959f9
         modules_paths = glob.glob('modules/m_*.py')
         for mod_path in modules_paths:
4967065a
             module = mod_path.replace('/', '.', 1)
             module = os.path.splitext(module)[0]
             try:
                 __import__(module)
                 mod = sys.modules[module]
             except ImportError:
022959f9
                 modules.commons.log(modules.commons.LOG_ERROR, 'Error importing module {}'.format(module))
4967065a
                 continue
             
             # the module default is disabled
             if not hasattr(mod, 'enabled') or mod.enabled == False:
022959f9
                 modules.commons.log(modules.commons.LOG_INFO, "module {} is not enabled".format(module))
4967065a
                 continue
             # check for the install phase
             if not hasattr(mod, 'install_phase'):
022959f9
                 modules.commons.log(modules.commons.LOG_ERROR, "Error: can not defind module {} phase".format(module))
4967065a
                 continue
             if mod.install_phase != phase:
022959f9
                 modules.commons.log(modules.commons.LOG_INFO, "Skipping module {0} for phase {1}".format(module, phase))
4967065a
                 continue
             if not hasattr(mod, 'execute'):
022959f9
                 modules.commons.log(modules.commons.LOG_ERROR, "Error: not able to execute module {}".format(module))
4967065a
                 continue
1b11aa2d
             mod.execute(module, self.install_config, self.photon_root)
4d53ba03
 
564d9533
     def adjust_packages_for_vmware_virt(self):
         try:
36a312f8
             if self.install_config['install_linux_esx']:
564d9533
                 selected_packages = self.install_config['packages']
                 try:
                     selected_packages.remove('linux')
                 except ValueError:
                     pass
                 try:
                     selected_packages.remove('initramfs')
                 except ValueError:
                     pass
                 selected_packages.append('linux-esx')
         except KeyError:
             pass
6860f77c
 
4d53ba03
     def run(self, command, comment = None):
         if comment != None:
022959f9
             modules.commons.log(modules.commons.LOG_INFO, "Installer: {} ".format(comment))
8a85a8a1
             self.progress_bar.update_loading_message(comment)
4d53ba03
 
022959f9
         modules.commons.log(modules.commons.LOG_INFO, "Installer: {} ".format(command))
ea62503b
         process = subprocess.Popen([command], shell=True, stdout=subprocess.PIPE)
         out,err = process.communicate()
         if err != None and err != 0 and "systemd-tmpfiles" not in command:
             modules.commons.log(modules.commons.LOG_ERROR, "Installer: failed in {} with error code {}".format(command, err))
             modules.commons.log(modules.commons.LOG_ERROR, out)
             self.exit_gracefully(None, None)
 
         return err