installer/isoInstaller.py
f4d17450
 #! /usr/bin/python2
 #
 #    Copyright (C) 2015 vmware inc.
 #
 #    Author: Mahmoud Bassiouny <mbassiouny@vmware.com>
 
5d05cfcc
 from optparse import OptionParser
 import os.path
f4d17450
 import curses
 import sys
4967065a
 import subprocess
 import re
 import requests
 import json
 import time
 import os
3c4cf0da
 import cracklib
 import crypt
 import string
 import random
386e9359
 import urllib
8e65c5ee
 import urllib2
022959f9
 import modules.commons
f4d17450
 from diskpartitioner import DiskPartitioner
 from packageselector import PackageSelector
 from custompackageselector import CustomPackageSelector
 from installer import Installer
4d53ba03
 from installercontainer import InstallerContainer
 from ostreeinstaller import OstreeInstaller
f4d17450
 from windowstringreader import WindowStringReader
3c4cf0da
 from ostreewindowstringreader import OSTreeWindowStringReader
f4d17450
 from jsonwrapper import JsonWrapper
 from selectdisk import SelectDisk
bc3b375f
 from license import License
3c4cf0da
 from ostreeserverselector import OSTreeServerSelector
f4d17450
 
 class IsoInstaller(object):
58f4c279
     
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
fda9bae9
             wait = 1
             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
022959f9
                 modules.commons.log(modules.commons.LOG_ERROR, "Failed to get the kickstart file at {0}, error msg: {1}".format(path, err_msg))
4967065a
                 print "Failed to get the kickstart file at {0}, retry in a second".format(path)
fda9bae9
                 time.sleep(wait)
                 wait = wait * 2
4967065a
 
 
             # Something went wrong
             print "Failed to get the kickstart file at {0}, exiting the installer, check the logs for more details".format(path)
             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))
4967065a
             return (JsonWrapper(path)).read();
 
     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
         for i in range(0,3):
             process = subprocess.Popen(['mount', '/dev/cdrom', '/mnt/cdrom'])
             retval = process.wait()
             if retval == 0:
bffb7f74
                 self.cd_path = "/mnt/cdrom"
                 return
4967065a
             print "Failed to mount the cd, retry in a second"
             time.sleep(1)
         print "Failed to mount the cd, exiting the installer, check the logs for more details"
         raise Exception("Can not mount the cd")
3c4cf0da
 
     def validate_hostname(self, hostname):
042e4ee4
         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"
 
3c4cf0da
         if (hostname == None or len(hostname) == 0):
042e4ee4
             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
 
58f4c279
     def validate_ostree_url_input(self, ostree_repo_url):
         if not ostree_repo_url:
1d241d9e
             return False, "Error: Invalid input"
386e9359
 
58f4c279
         exception_text = "Error: Invalid or unreachable URL"
         error_text = "Error: Repo URL not accessible"
         ret = self.validate_http_response(ostree_repo_url, [], exception_text, error_text)
         if ret != "":
             return False, ret
8e65c5ee
 
58f4c279
         exception_text = "Error: Invalid repo - missing config"
         ret = self.validate_http_response(
             ostree_repo_url + "/config",
             [ [".*\[core\]\s*", 1, "Error: Invalid config - 'core' group expected" ],
               ["\s*mode[ \t]*=[ \t]*archive-z2[^ \t]", 1, "Error: can't pull from repo in 'bare' mode, 'archive-z2' mode required" ] ],
             exception_text, exception_text)
         if ret != "":
             return False, ret
     
         exception_text = "Error: Invalid repo - missing refs"
         ret = self.validate_http_response(ostree_repo_url + "/refs/heads", [], exception_text, exception_text)
         if ret != "":
             return False, ret
 
         exception_text = "Error: Invalid repo - missing objects"
         ret = self.validate_http_response(ostree_repo_url + "/objects", [], exception_text, exception_text)
         if ret != "":
             return False, ret
 
         self.ostree_repo_url = ostree_repo_url
386e9359
 
         return True, None
 
58f4c279
     def validate_ostree_refs_input(self, ostree_repo_ref):
         if not ostree_repo_ref:
             return False, "Error: Invalid input"
 
         ret = self.validate_http_response(
                 self.ostree_repo_url  + '/refs/heads/' + ostree_repo_ref,
                 [ ["^\s*[0-9A-Fa-f]{64}\s*$", 1, "Error: Incomplete Refspec path, or unexpected Refspec format"] ],
                 "Error: Invalid Refspec path",
                 "Error: Refspec not accessible")
         if ret != "":
             return False, ret
 
         return True, None
386e9359
 
3c4cf0da
     def validate_password(self, text):
         try:
             p = cracklib.VeryFascistCheck(text)
         except ValueError, message:
             p = str(message)
         return p == text, "Error: " + p
 
     def generate_password_hash(self,  password):
         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"):
                 response = urllib2.urlopen(url,cafile="/usr/lib/python2.7/site-packages/requests/cacert.pem")
             else:
                 response = urllib2.urlopen(url)
 
         except:
             return exception_text
         else:
             if response.getcode() != 200:
                 return error_text
         
         html = response.read()
         
         for pattern, count, failed_check_text in checks:
             match = re.findall(pattern, html)
             if len(match) != count:
                 return failed_check_text
 
         return ""
 
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))
 
         self.maxy,  self.maxx = self.screen.getmaxyx()
         self.screen.addstr(self.maxy - 1, 0, '<Tab> moves; <Space> selects; <Enter> forward')
 
         curses.curs_set(0)
 
bffb7f74
         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()
             rpm_path=os.path.join(self.cd_path, "RPMS")
f4d17450
 
         # This represents the installer screen, the bool indicated if I can go back to this window or not
4967065a
         items = []
         if not ks_config:
a9158ca2
             random_id = '%12x' % random.randrange(16**12)
             random_hostname = "photon-" + random_id.strip()
109493d5
             install_config = {'iso_system': False}
bffb7f74
             license_agreement = License(self.maxy, self.maxx)
109493d5
             select_disk = SelectDisk(self.maxy, self.maxx, install_config)
             package_selector = PackageSelector(self.maxy, self.maxx, install_config, options_file)
3c4cf0da
 
             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)
             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
                     self.validate_password, # validation function of the input
                     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
                     self.generate_password_hash, # post processing of the input field
                     'Confirm root password', 'Confirm Root password:', 2, install_config)
             ostree_server_selector = OSTreeServerSelector(self.maxy, self.maxx, install_config)
             ostree_url_reader = OSTreeWindowStringReader(
                     self.maxy, self.maxx, 10, 80, 
                     'ostree_repo_url', 
                     None, # confirmation error msg if it's a confirmation text
                     None, # echo char
                     None, # set of accepted chars
386e9359
                     self.validate_ostree_url_input, # validation function of the input
3c4cf0da
                     None, # post processing of the input field
                     'Please provide the URL of OSTree repo', 'OSTree Repo URL:', 2, install_config,
1d241d9e
                     "http://")
3c4cf0da
             ostree_ref_reader = OSTreeWindowStringReader(
                     self.maxy, self.maxx, 10, 70, 
                     'ostree_repo_ref', 
                     None, # confirmation error msg if it's a confirmation text
                     None, # echo char
                     None, # set of accepted chars
386e9359
                     self.validate_ostree_refs_input, # validation function of the input
3c4cf0da
                     None, # post processing of the input field
58f4c279
                     'Please provide the Refspec in OSTree repo', 'OSTree Repo Refspec:', 2, install_config,
                     "photon/1.0/x86_64/minimal")
bffb7f74
             
4967065a
             items = items + [
b84da80d
                     (license_agreement.display, False),
                     (select_disk.display, True),
f4d17450
                     (package_selector.display, True),
                     (hostname_reader.get_user_string, True),
                     (root_password_reader.get_user_string, True),
1ec3a643
                     (confirm_password_reader.get_user_string, False),
3c4cf0da
                     (ostree_server_selector.display, True),
                     (ostree_url_reader.get_user_string, True),
                     (ostree_ref_reader.get_user_string, True),
4967065a
                  ]
109493d5
         else:
             install_config = ks_config
             install_config['iso_system'] = False
 
         installer = InstallerContainer(install_config, self.maxy, self.maxx, True, rpm_path=rpm_path, log_path="/var/log", ks_config=ks_config)
4d53ba03
 
4967065a
         items = items + [(installer.install, False)]
f4d17450
 
         index = 0
         params = None
         while True:
             result = items[index][0](params)
             if result.success:
                 index += 1
                 params = result.result
                 if index == len(items):
                     break
             else:
                 index -= 1
                 while index >= 0 and items[index][1] == False:
                     index -= 1
                 if index < 0:
                     index = 0
 
 if __name__ == '__main__':
5d05cfcc
     usage = "Usage: %prog [options]"
     parser = OptionParser(usage)
     parser.add_option("-j",  "--json-file", dest="options_file",  default="input.json")
 
     (options,  args) = parser.parse_args()
     curses.wrapper(IsoInstaller, options.options_file)