installer/installer.py
f4d17450
 #!/usr/bin/python2
 #
 #    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):
b422b988
     def __init__(self, install_config, maxy = 0, maxx = 0, iso_installer = False, rpm_path = "../stage/RPMS", log_path = "../stage/LOGS", ks_config = None):
f4d17450
         self.install_config = install_config
4967065a
         self.ks_config = ks_config
f4d17450
         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.install_package_command = "./mk-install-package.sh"
         self.chroot_command = "./mk-run-chroot.sh"
         self.setup_grub_command = "./mk-setup-grub.sh"
         self.unmount_disk_command = "./mk-unmount-disk.sh"
 
         if self.iso_installer:
b84da80d
             self.working_directory = "/mnt/photon-root"
f4d17450
         elif '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
             self.window = Window(self.height, self.width, self.maxy, self.maxx, 'Installing Photon', False)
             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()
3dd32127
             self.window.addstr(0, 0, 'Opps, Installer got interrupted.\n\nPress any key to get to the bash...')
f4d17450
             self.window.content_window().getch()
022959f9
 
34dfc7d7
         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:
             self.get_size_of_packages()
             selected_packages = self.install_config['packages']
             for package in selected_packages:
                 self.progress_bar.update_message('Installing {0}...'.format(package))
                 process = subprocess.Popen(['tdnf', 'install', package, '--installroot', self.photon_root, '--nogpgcheck', '--assumeyes'], stdout=self.output, stderr=subprocess.STDOUT)
                 retval = process.wait()
                 # 0 : succeed; 137 : package already installed; 65 : package not found in repo.
                 if retval != 0 and retval != 137:
                     modules.commons.log(modules.commons.LOG_ERROR, "Failed install: {} with error code {}".format(package, retval))
                     self.exit_gracefully(None, None)
                 self.progress_bar.increment(self.size_of_packages[package])
         else:
f4d17450
         #install packages
6860f77c
             for rpm in self.rpms_tobeinstalled:
                 # We already installed the filesystem in the preparation
                 if rpm['package'] == 'filesystem':
                     continue
                 return_value = self.install_package(rpm['filename'])
                 if return_value != 0:
                     self.exit_gracefully(None, None)
 
f4d17450
 
         if self.iso_installer:
             self.progress_bar.show_loading('Finalizing installation')
b84da80d
 
f4d17450
         self.finalize_system()
 
b84da80d
         if not self.install_config['iso_system']:
4967065a
             # Execute post installation modules
             self.execute_modules(modules.commons.POST_INSTALL)
 
f4d17450
             # install grub
7fd875e7
             try:
                 if self.install_config['boot'] == 'bios':
c3771c35
                     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']], stdout=self.output)
7fd875e7
                 elif self.install_config['boot'] == 'efi':
c3771c35
                     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']], stdout=self.output)
7fd875e7
             except:
                 #install bios if variable is not set.
c3771c35
                 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']], stdout=self.output)
7fd875e7
 
f4d17450
             retval = process.wait()
 
c3771c35
             self.update_fstab()
 
         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))
4967065a
             if self.ks_config == None:
                 self.window.content_window().getch()
f4d17450
 
         return ActionResult(True, None)
bffb7f74
         
b84da80d
     def copy_rpms(self):
         # prepare the RPMs list
f7ba3634
         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']
f7ba3634
 
         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
 
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'] == '/':
                 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
                 ))
863df6e9
         # 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()
             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()
     
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/')
 
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)
f4d17450
 
     def install_package(self,  package_name):
         rpm_params = ''
85b82155
 
5d61f62a
         os.environ["RPMROOT"] = self.rpm_path
85b82155
         rpm_params = rpm_params + ' --force '
         rpm_params = rpm_params + ' --root ' + self.photon_root
89f1079a
         rpm_params = rpm_params + ' --dbpath /var/lib/rpm '
85b82155
 
b84da80d
         if ('type' in self.install_config and (self.install_config['type'] in ['micro', 'minimal'])) or self.install_config['iso_system']:
f4d17450
             rpm_params = rpm_params + ' --excludedocs '
 
85b82155
         process = subprocess.Popen([self.install_package_command, '-w', self.photon_root, package_name, rpm_params],  stdout=self.output)
 
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
             mod.execute(module, self.ks_config, self.install_config, self.photon_root)
4d53ba03
 
6860f77c
     def get_install_size_of_a_package(self, name_size_pairs, package):
         modules.commons.log(modules.commons.LOG_INFO, "Find the install size of: {} ".format(package))
         for index, name in enumerate(name_size_pairs, start=0):
             if name[name.find(":") + 1:].strip() == package.strip():  
                 item = name_size_pairs[index + 1] 
                 size = item[item.find("(") + 1:item.find(")")]
                 return int(size)
34dfc7d7
         raise LookupError("Cannot find package {} in the repo.".format(package))
6860f77c
     def get_size_of_packages(self):
         #call tdnf info to get the install size of all the packages.
         process = subprocess.Popen(['tdnf', 'info', '--installroot', self.photon_root], stdout=subprocess.PIPE)
         out,err = process.communicate()
         if err != None and err != 0:
             modules.commons.log(modules.commons.LOG_ERROR, "Failed to get infomation from : {} with error code {}".format(package, err))
 
         name_size_pairs = re.findall("(?:^Name.*$)|(?:^.*Install Size.*$)", out, re.M)
         selected_packages = self.install_config['packages']
         self.size_of_packages = {}
         progressbar_num_items = 0
         for package in selected_packages:
             size = self.get_install_size_of_a_package(name_size_pairs, package)
             progressbar_num_items += size;
             self.size_of_packages[package] = size;
         self.progress_bar.update_num_items(progressbar_num_items)    
 
 
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))
d99e1c67
         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