installer/isoInstaller.py
f4d17450
 #! /usr/bin/python2
 #
 #    Copyright (C) 2015 vmware inc.
 #
 #    Author: Mahmoud Bassiouny <mbassiouny@vmware.com>
 
0f3948ba
 from argparse import ArgumentParser
5d05cfcc
 import os.path
f4d17450
 import curses
4967065a
 import subprocess
 import re
 import json
 import time
3c4cf0da
 import crypt
 import string
 import random
8e65c5ee
 import urllib2
130228a5
 import requests
 import cracklib
022959f9
 import modules.commons
72832a72
 from partitionISO import PartitionISO
f4d17450
 from packageselector import PackageSelector
 from installer import Installer
4d53ba03
 from installercontainer import InstallerContainer
f4d17450
 from windowstringreader import WindowStringReader
 from jsonwrapper import JsonWrapper
 from selectdisk import SelectDisk
bc3b375f
 from license import License
564d9533
 from linuxselector import LinuxSelector
f4d17450
 
 class IsoInstaller(object):
564d9533
 
bffb7f74
     def get_config(self, path):
         if path.startswith("http://"):
fda9bae9
             # Do 5 trials to get the kick start
4967065a
             # TODO: make sure the installer run after network is up
1b11aa2d
             ks_file_error = "Failed to get the kickstart file at {0}".format(path)
fda9bae9
             wait = 1
130228a5
             for x in range(0, 5):
4967065a
                 err_msg = ""
                 try:
                     response = requests.get(path, timeout=3)
                     if response.ok:
                         return json.loads(response.text)
                     err_msg = response.text
                 except Exception as e:
                     err_msg = e
1b11aa2d
 
                 modules.commons.log(modules.commons.LOG_ERROR,
                     ks_file_error)
                 modules.commons.log(modules.commons.LOG_ERROR,
                     "error msg: {0}".format(err_msg))
                 print(ks_file_error)
                 print("retry in a second")
fda9bae9
                 time.sleep(wait)
                 wait = wait * 2
4967065a
 
             # Something went wrong
1b11aa2d
             print(ks_file_error)
             print("exiting the installer, check the logs for more details")
4967065a
             raise Exception(err_msg)
         else:
             if path.startswith("cdrom:/"):
bffb7f74
                 self.mount_RPMS_cd()
                 path = os.path.join(self.cd_path, path.replace("cdrom:/", "", 1))
130228a5
             return (JsonWrapper(path)).read()
4967065a
 
     def mount_RPMS_cd(self):
bffb7f74
         # check if the cd is already mounted
         if self.cd_path:
             return
 
4967065a
         # Mount the cd to get the RPMS
         process = subprocess.Popen(['mkdir', '-p', '/mnt/cdrom'])
         retval = process.wait()
 
         # Retry mount the CD
130228a5
         for i in range(0, 3):
4967065a
             process = subprocess.Popen(['mount', '/dev/cdrom', '/mnt/cdrom'])
             retval = process.wait()
             if retval == 0:
bffb7f74
                 self.cd_path = "/mnt/cdrom"
                 return
1b11aa2d
             print("Failed to mount the cd, retry in a second")
4967065a
             time.sleep(1)
1b11aa2d
         print("Failed to mount the cd, exiting the installer")
         print("check the logs for more details")
4967065a
         raise Exception("Can not mount the cd")
3c4cf0da
 
     def validate_hostname(self, hostname):
4c700ec8
         error_empty = "Empty hostname or domain is not allowed"
         error_dash = "Hostname or domain should not start or end with '-'"
         error_hostname = "Hostname should start with alpha char and <= 64 chars"
 
130228a5
         if hostname is None or len(hostname) == 0:
4c700ec8
             return False, error_empty
 
         fields = hostname.split('.')
         for field in fields:
             if len(field) == 0:
                 return False, error_empty
             if field[0] == '-' or field[-1] == '-':
                 return False, error_dash
 
         machinename = fields[0]
         return (len(machinename) <= 64) and (ord(machinename[0]) in self.alpha_chars), error_hostname
3c4cf0da
 
1b11aa2d
     @staticmethod
     def validate_password(text):
3c4cf0da
         try:
             p = cracklib.VeryFascistCheck(text)
         except ValueError, message:
             p = str(message)
         return p == text, "Error: " + p
 
1b11aa2d
     @staticmethod
     def generate_password_hash(password):
3c4cf0da
         shadow_password = crypt.crypt(password, "$6$" + "".join([random.choice(string.ascii_letters + string.digits) for _ in range(16)]))
         return shadow_password
 
58f4c279
     def validate_http_response(self, url, checks, exception_text, error_text):
         try:
             if url.startswith("https"):
130228a5
                 response = urllib2.urlopen(url, cafile="/usr/lib/python2.7/site-packages/requests/cacert.pem")
58f4c279
             else:
                 response = urllib2.urlopen(url)
 
         except:
             return exception_text
         else:
             if response.getcode() != 200:
                 return error_text
130228a5
 
58f4c279
         html = response.read()
130228a5
 
58f4c279
         for pattern, count, failed_check_text in checks:
             match = re.findall(pattern, html)
             if len(match) != count:
                 return failed_check_text
 
         return ""
130228a5
     def ui_install(self, options_file, rpm_path):
         # This represents the installer screen, the bool indicated if I can go back to this window or not
         items = []
         random_id = '%12x' % random.randrange(16**12)
         random_hostname = "photon-" + random_id.strip()
         install_config = {'iso_system': False}
1b11aa2d
         install_config['ui_install'] = True
130228a5
         license_agreement = License(self.maxy, self.maxx)
         select_disk = SelectDisk(self.maxy, self.maxx, install_config)
         select_partition = PartitionISO(self.maxy, self.maxx, install_config)
         package_selector = PackageSelector(self.maxy, self.maxx, install_config, options_file)
         self.alpha_chars = range(65, 91)
         self.alpha_chars.extend(range(97, 123))
         hostname_accepted_chars = list(self.alpha_chars)
         # Adding the numeric chars
         hostname_accepted_chars.extend(range(48, 58))
         # Adding the . and -
         hostname_accepted_chars.extend([ord('.'), ord('-')])
 
         hostname_reader = WindowStringReader(
             self.maxy, self.maxx, 10, 70,
             'hostname',
             None, # confirmation error msg if it's a confirmation text
             None, # echo char
             hostname_accepted_chars, # set of accepted chars
             self.validate_hostname, # validation function of the input
             None, # post processing of the input field
             'Choose the hostname for your system', 'Hostname:', 2, install_config,
             random_hostname,
             True)
         root_password_reader = WindowStringReader(
             self.maxy, self.maxx, 10, 70,
             'password',
             None, # confirmation error msg if it's a confirmation text
             '*', # echo char
             None, # set of accepted chars
1b11aa2d
             IsoInstaller.validate_password, # validation function of the input
130228a5
             None,  # post processing of the input field
             'Set up root password', 'Root password:', 2, install_config)
         confirm_password_reader = WindowStringReader(
             self.maxy, self.maxx, 10, 70,
             'password',
             "Passwords don't match, please try again.", # confirmation error msg if it's a confirmation text
             '*', # echo char
             None, # set of accepted chars
             None, # validation function of the input
1b11aa2d
             IsoInstaller.generate_password_hash, # post processing of the input field
130228a5
             'Confirm root password', 'Confirm Root password:', 2, install_config)
 
         items.append((license_agreement.display, False))
         items.append((select_disk.display, True))
         items.append((select_partition.display, False))
         items.append((select_disk.guided_partitions, False))
         items.append((package_selector.display, True))
         select_linux_index = -1
         if self.is_vmware_virtualization():
             linux_selector = LinuxSelector(self.maxy, self.maxx, install_config)
             items.append((linux_selector.display, True))
             select_linux_index = items.index((linux_selector.display, True))
         items.append((hostname_reader.get_user_string, True))
         items.append((root_password_reader.get_user_string, True))
         items.append((confirm_password_reader.get_user_string, False))
         installer = InstallerContainer(
             install_config,
             self.maxy,
             self.maxx,
             True,
             rpm_path=rpm_path,
             log_path="/var/log")
 
         items = items + [(installer.install, False)]
 
         index = 0
         params = None
         while True:
             result = items[index][0](params)
             if result.success:
                 index += 1
                 params = result.result
                 if index == len(items) - 1:
                     self.screen.clear()
                 if index == len(items):
                     break
                 #Skip linux select screen for ostree installation.
                 if index == select_linux_index:
                     if install_config['type'] == 'ostree_server':
                         index += 1
             else:
                 index -= 1
                 while index >= 0 and items[index][1] is False:
                     index -= 1
                 if index < 0:
                     index = 0
                 #Skip linux select screen for ostree installation.
                 if index == select_linux_index:
                     if install_config['type'] == 'ostree_server':
                         index -= 1
 
1b11aa2d
     def ks_install(self, options_file, rpm_path, ks_config):
         install_config = ks_config
         install_config['iso_system'] = False
         if self.is_vmware_virtualization() and 'install_linux_esx' not in install_config:
             install_config['install_linux_esx'] = True
 
         json_wrapper_option_list = JsonWrapper("build_install_options_all.json")
         option_list_json = json_wrapper_option_list.read()
         options_sorted = option_list_json.items()
 
         base_path = os.path.dirname("build_install_options_all.json")
         package_list = []
 
         package_list = PackageSelector.get_packages_to_install(options_sorted, install_config['type'], base_path)
         if 'additional_packages' in install_config:
             package_list.extend(install_config['additional_packages'])
         install_config['packages'] = package_list
 
         if 'partitions' in install_config:
             partitions = install_config['partitions']
         else:
             partitions = modules.commons.default_partitions
 
         install_config['disk'] = modules.commons.partition_disk(install_config['disk'], partitions)
 
         if "hostname" in install_config:
             evalhostname = os.popen('printf ' + install_config["hostname"].strip(" ")).readlines()
             install_config['hostname'] = evalhostname[0]
         if "hostname" not in install_config or install_config['hostname'] == "":
             random_id = '%12x' % random.randrange(16**12)
             install_config['hostname'] = "photon-" + random_id.strip()
 
         # crypt the password if needed
         if install_config['password']['crypted']:
             install_config['password'] = install_config['password']['text']
         else:
             install_config['password'] = crypt.crypt(install_config['password']['text'],
                 "$6$" + "".join([random.choice(string.ascii_letters + string.digits) for _ in range(16)]))
 
         installer = InstallerContainer(
             install_config,
             self.maxy, self.maxx,
             True,
             rpm_path=rpm_path,
             log_path="/var/log")
 
         installer.install(None)
58f4c279
 
564d9533
     def is_vmware_virtualization(self):
         process = subprocess.Popen(['systemd-detect-virt'], stdout=subprocess.PIPE)
130228a5
         out, err = process.communicate()
         if err is not None and err != 0:
564d9533
             return False
         else:
             return out == 'vmware\n'
 
5d05cfcc
     def __init__(self, stdscreen, options_file):
f4d17450
         self.screen = stdscreen
 
         # Init the colors
         curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
         curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
         curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_GREEN)
         curses.init_pair(4, curses.COLOR_RED, curses.COLOR_WHITE)
 
         self.screen.bkgd(' ', curses.color_pair(1))
 
130228a5
         self.maxy, self.maxx = self.screen.getmaxyx()
5f854778
         self.screen.addstr(self.maxy - 1, 0, '  Arrow keys make selections; <Enter> activates.')
f4d17450
         curses.curs_set(0)
 
130228a5
         self.cd_path = None
 
4967065a
         kernel_params = subprocess.check_output(['cat', '/proc/cmdline'])
bffb7f74
 
         # check the kickstart param
         ks_config = None
4967065a
         m = re.match(r".*ks=(\S+)\s*.*\s*", kernel_params)
         if m != None:
bffb7f74
             ks_config = self.get_config(m.group(1))
 
         # check for the repo param
         m = re.match(r".*repo=(\S+)\s*.*\s*", kernel_params)
         if m != None:
             rpm_path = m.group(1)
         else:
             # the rpms should be in the cd
             self.mount_RPMS_cd()
130228a5
             rpm_path = os.path.join(self.cd_path, "RPMS")
f4d17450
 
1b11aa2d
         if ks_config:
             self.ks_install(options_file, rpm_path, ks_config)
109493d5
         else:
1b11aa2d
             self.ui_install(options_file, rpm_path)
f4d17450
 
 if __name__ == '__main__':
5d05cfcc
     usage = "Usage: %prog [options]"
0f3948ba
     parser = ArgumentParser(usage)
130228a5
     parser.add_argument("-j", "--json-file", dest="options_file", default="input.json")
5d05cfcc
 
0f3948ba
     options = parser.parse_args()
5d05cfcc
     curses.wrapper(IsoInstaller, options.options_file)