""" Photon installer """ # Copyright (C) 2015 vmware inc. # # Author: Mahmoud Bassiouny import subprocess import os import shutil import signal import sys import glob import modules.commons from jsonwrapper import JsonWrapper from progressbar import ProgressBar from window import Window from actionresult import ActionResult class Installer(object): """ Photon installer """ mount_command = "./mk-mount-disk.sh" prepare_command = "./mk-prepare-system.sh" finalize_command = "./mk-finalize-system.sh" chroot_command = "./mk-run-chroot.sh" setup_grub_command = "./mk-setup-grub.sh" unmount_disk_command = "./mk-unmount-disk.sh" def __init__(self, install_config, maxy=0, maxx=0, iso_installer=False, rpm_path="../stage/RPMS", log_path="../stage/LOGS"): self.install_config = install_config self.install_config['iso_installer'] = iso_installer self.rpm_path = rpm_path self.log_path = log_path if 'working_directory' in self.install_config: self.working_directory = self.install_config['working_directory'] else: self.working_directory = "/mnt/photon-root" self.photon_root = self.working_directory + "/photon-chroot" self.rpms_tobeinstalled = None if self.install_config['iso_installer']: self.output = open(os.devnull, 'w') #initializing windows height = 10 width = 75 progress_padding = 5 progress_width = width - progress_padding starty = (maxy - height) // 2 startx = (maxx - width) // 2 self.window = Window(height, width, maxy, maxx, 'Installing Photon', False) self.progress_bar = ProgressBar(starty + 3, startx + progress_padding // 2, progress_width) else: self.output = None signal.signal(signal.SIGINT, self.exit_gracefully) def install(self, params): """ Install photon system and handle exception """ del params try: return self._unsafe_install() except Exception as inst: if self.install_config['iso_installer']: modules.commons.log(modules.commons.LOG_ERROR, repr(inst)) self.exit_gracefully(None, None) else: raise def _unsafe_install(self): """ Install photon system """ self._setup_install_repo() self._initialize_system() self._install_packages() self._enable_network_in_chroot() self._finalize_system() self._disable_network_in_chroot() self._cleanup_and_exit() return ActionResult(True, None) def exit_gracefully(self, signal1, frame1): """ This will be called if the installer interrupted by Ctrl+C, exception or other failures """ del signal1 del frame1 if self.install_config['iso_installer']: self.progress_bar.hide() self.window.addstr(0, 0, 'Oops, Installer got interrupted.\n\n' + 'Press any key to get to the bash...') self.window.content_window().getch() modules.commons.dump(modules.commons.LOG_FILE_NAME) sys.exit(1) def _cleanup_and_exit(self): """ Unmount the disk, eject cd and exit """ command = [Installer.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) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_ERROR, "Failed to unmount disks") if self.install_config['iso_installer']: self.progress_bar.hide() self.window.addstr(0, 0, 'Congratulations, Photon has been installed in {0} secs.\n\n' 'Press any key to continue to boot...' .format(self.progress_bar.time_elapsed)) self._eject_cdrom() def _copy_rpms(self): """ Prepare RPM list and copy rpms """ # prepare the RPMs list json_pkg_to_rpm_map = JsonWrapper(self.install_config["pkg_to_rpm_map_file"]) pkg_to_rpm_map = json_pkg_to_rpm_map.read() self.rpms_tobeinstalled = [] selected_packages = self.install_config['packages'] for pkg in selected_packages: if pkg in pkg_to_rpm_map: if pkg_to_rpm_map[pkg]['rpm'] is not None: name = pkg_to_rpm_map[pkg]['rpm'] basename = os.path.basename(name) self.rpms_tobeinstalled.append({'filename': basename, 'path': name, 'package' : pkg}) # Copy the rpms for rpm in self.rpms_tobeinstalled: shutil.copy(rpm['path'], self.photon_root + '/RPMS/') def _copy_files(self): """ Copy the rpm files and instal scripts. """ # Make the photon_root directory if not exits process = subprocess.Popen(['mkdir', '-p', self.photon_root], stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_ERROR, "Fail to create the root directory") self.exit_gracefully(None, None) # Copy the installer files process = subprocess.Popen(['cp', '-r', "../installer", self.photon_root], stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_ERROR, "Fail to copy install scripts") self.exit_gracefully(None, None) # Create the rpms directory process = subprocess.Popen(['mkdir', '-p', self.photon_root + '/RPMS'], stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_ERROR, "Fail to create the rpms directory") self.exit_gracefully(None, None) self._copy_rpms() def _bind_installer(self): """ Make the photon_root/installer directory if not exits 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. """ # Make the photon_root/installer directory if not exits if(subprocess.call(['mkdir', '-p', os.path.join(self.photon_root, "installer")]) != 0 or subprocess.call(['mount', '--bind', '/installer', os.path.join(self.photon_root, "installer")]) != 0): modules.commons.log(modules.commons.LOG_ERROR, "Fail to bind installer") self.exit_gracefully(None, None) def _unbind_installer(self): # unmount the installer directory process = subprocess.Popen(['umount', os.path.join(self.photon_root, "installer")], stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_ERROR, "Fail to unbind the installer directory") # remove the installer directory process = subprocess.Popen(['rm', '-rf', os.path.join(self.photon_root, "installer")], stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_ERROR, "Fail to remove the installer directory") def _bind_repo_dir(self): """ Bind repo dir for tdnf installation """ rpm_cache_dir = self.photon_root + '/cache/tdnf/photon-iso/rpms' if self.rpm_path.startswith("https://") or self.rpm_path.startswith("http://"): return 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) def _unbind_repo_dir(self): """ Unbind repo dir after installation """ rpm_cache_dir = self.photon_root + '/cache/tdnf/photon-iso/rpms' if self.rpm_path.startswith("https://") or self.rpm_path.startswith("http://"): return if (subprocess.call(['umount', rpm_cache_dir]) != 0 or subprocess.call(['rm', '-rf', rpm_cache_dir]) != 0): modules.commons.log(modules.commons.LOG_ERROR, "Fail to unbind cache rpms") self.exit_gracefully(None, None) def _update_fstab(self): """ update fstab """ with open(os.path.join(self.photon_root, "etc/fstab"), "w") as fstab_file: 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'] == '/': options = options + ',barrier,noatime,noacl,data=ordered' 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 )) # Add the cdrom entry fstab_file.write("/dev/cdrom\t/mnt/cdrom\tiso9660\tro,noauto\t0\t0\n") def _generate_partitions_param(self, reverse=False): """ Generate partition param for mount command """ 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 def _initialize_system(self): """ Prepare the system to install photon """ #Setup the disk if not self.install_config['iso_system']: command = [Installer.mount_command, '-w', self.photon_root] command.extend(self._generate_partitions_param()) process = subprocess.Popen(command, stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_INFO, "Failed to setup the disk for installation") self.exit_gracefully(None, None) if self.install_config['iso_installer']: self._bind_installer() self._bind_repo_dir() process = subprocess.Popen([Installer.prepare_command, '-w', self.photon_root, 'install'], stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_INFO, "Failed to bind the installer and repo needed by tdnf") self.exit_gracefully(None, None) else: self._copy_files() #Setup the filesystem basics process = subprocess.Popen([Installer.prepare_command, '-w', self.photon_root], stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_INFO, "Failed to setup the file systems basics") self.exit_gracefully(None, None) def _finalize_system(self): """ Finalize the system after the installation """ #Setup the disk process = subprocess.Popen([Installer.chroot_command, '-w', self.photon_root, Installer.finalize_command, '-w', self.photon_root], stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_ERROR, "Fail to setup th target system after the installation") if self.install_config['iso_installer']: modules.commons.dump(modules.commons.LOG_FILE_NAME) shutil.copy(modules.commons.LOG_FILE_NAME, self.photon_root + '/var/log/') shutil.copy(modules.commons.TDNF_LOG_FILE_NAME, self.photon_root + '/var/log/') self._unbind_installer() self._unbind_repo_dir() # Disable the swap file process = subprocess.Popen(['swapoff', '-a'], stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_ERROR, "Fail to swapoff") # remove the tdnf cache directory and the swapfile. process = subprocess.Popen(['rm', '-rf', os.path.join(self.photon_root, "cache")], stdout=self.output) retval = process.wait() if retval != 0: modules.commons.log(modules.commons.LOG_ERROR, "Fail to remove the cache") if not self.install_config['iso_system']: # Execute post installation modules self._execute_modules(modules.commons.POST_INSTALL) if os.path.exists(modules.commons.KS_POST_INSTALL_LOG_FILE_NAME): shutil.copy(modules.commons.KS_POST_INSTALL_LOG_FILE_NAME, self.photon_root + '/var/log/') if self.install_config['iso_installer'] and os.path.isdir("/sys/firmware/efi"): self.install_config['boot'] = 'efi' # install grub if 'boot_partition_number' not in self.install_config['disk']: self.install_config['disk']['boot_partition_number'] = 1 try: if self.install_config['boot'] == 'bios': process = subprocess.Popen( [Installer.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) elif self.install_config['boot'] == 'efi': process = subprocess.Popen( [Installer.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) except: #install bios if variable is not set. process = subprocess.Popen( [Installer.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) retval = process.wait() self._update_fstab() def _execute_modules(self, phase): """ Execute the scripts in the modules folder """ sys.path.append("./modules") modules_paths = glob.glob('modules/m_*.py') for mod_path in modules_paths: module = mod_path.replace('/', '.', 1) module = os.path.splitext(module)[0] try: __import__(module) mod = sys.modules[module] except ImportError: modules.commons.log(modules.commons.LOG_ERROR, 'Error importing module {}'.format(module)) continue # the module default is disabled if not hasattr(mod, 'enabled') or mod.enabled is False: modules.commons.log(modules.commons.LOG_INFO, "module {} is not enabled".format(module)) continue # check for the install phase if not hasattr(mod, 'install_phase'): modules.commons.log(modules.commons.LOG_ERROR, "Error: can not defind module {} phase".format(module)) continue if mod.install_phase != phase: modules.commons.log(modules.commons.LOG_INFO, "Skipping module {0} for phase {1}".format(module, phase)) continue if not hasattr(mod, 'execute'): modules.commons.log(modules.commons.LOG_ERROR, "Error: not able to execute module {}".format(module)) continue mod.execute(self.install_config, self.photon_root) def _adjust_packages_for_vmware_virt(self): """ Install linux_esx on Vmware virtual machine if requested """ try: if self.install_config['install_linux_esx']: 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 def _setup_install_repo(self): """ Setup the tdnf repo for installation """ if self.install_config['iso_installer']: self.window.show_window() self.progress_bar.initialize('Initializing installation...') self.progress_bar.show() #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('/', r'\/')) 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) cmdoption = (r's/cachedir=\/var/cachedir={}/g' .format(self.photon_root.replace('/', r'\/'))) 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) def _install_packages(self): """ Install packages using tdnf or rpm command """ if self.install_config['iso_installer']: self._tdnf_install_packages() else: self._rpm_install_packages() def _tdnf_install_packages(self): """ Install packages using tdnf command """ self._adjust_packages_for_vmware_virt() selected_packages = self.install_config['packages'] 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().decode() 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: self.progress_bar.update_message(output) if output == 'Running transaction\n': state = 4 else: modules.commons.log(modules.commons.LOG_INFO, "[tdnf] {0}".format(output)) 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) # 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 to install some packages, refer to {0}" .format(modules.commons.TDNF_LOG_FILE_NAME)) self.exit_gracefully(None, None) self.progress_bar.show_loading('Finalizing installation') def _rpm_install_packages(self): """ Install packages using rpm command """ rpms = [] for rpm in self.rpms_tobeinstalled: # We already installed the filesystem in the preparation if rpm['package'] == 'filesystem': continue rpms.append(rpm['filename']) rpms = set(rpms) rpm_paths = [] for root, _, files in os.walk(self.rpm_path): for file in files: if file in rpms: rpm_paths.append(os.path.join(root, file)) # --nodeps is for hosts which do not support rich dependencies rpm_params = ['--nodeps', '--root', self.photon_root, '--dbpath', '/var/lib/rpm'] if (('type' in self.install_config and (self.install_config['type'] in ['micro', 'minimal'])) or self.install_config['iso_system']): rpm_params.append('--excludedocs') 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) return_value = process.wait() if return_value != 0: self.exit_gracefully(None, None) def _eject_cdrom(self): """ Eject the cdrom on request """ eject_cdrom = True if 'ui_install' in self.install_config: self.window.content_window().getch() 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() def _enable_network_in_chroot(self): """ Enable network in chroot """ if os.path.exists("/etc/resolv.conf"): shutil.copy("/etc/resolv.conf", self.photon_root + '/etc/.') def _disable_network_in_chroot(self): """ disable network in chroot """ if os.path.exists(self.photon_root + '/etc/resolv.conf'): os.remove(self.photon_root + '/etc/resolv.conf')