2019-02-22 11:40:08 +03:00
#
# Common code for unattended installations
#
# Copyright 2019 Red Hat, Inc.
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.
2019-06-08 23:12:39 +03:00
import getpass
import locale
import os
import pwd
2019-09-27 16:25:23 +03:00
import re
2019-06-07 13:03:20 +03:00
import tempfile
2019-02-22 11:40:16 +03:00
2019-03-05 20:20:29 +03:00
from gi . repository import Libosinfo
2019-07-30 19:04:13 +03:00
from . import urlfetcher
from . . import progress
2019-06-17 04:34:47 +03:00
from . . logger import log
2019-06-17 04:12:39 +03:00
2019-03-05 20:20:29 +03:00
2020-07-18 01:27:14 +03:00
def _is_user_login_safe ( login ) :
return login != " root "
def _login_from_hostuser ( ) :
hostuser = getpass . getuser ( )
realname = pwd . getpwnam ( hostuser ) . pw_gecos
if not _is_user_login_safe ( hostuser ) :
return None , None # pragma: no cover
return hostuser , realname # pragma: no cover
2019-03-07 15:52:50 +03:00
def _make_installconfig ( script , osobj , unattended_data , arch , hostname , url ) :
2019-03-05 22:27:37 +03:00
"""
Build a Libosinfo . InstallConfig instance
"""
def get_timezone ( ) :
TZ_FILE = " /etc/localtime "
2019-06-08 23:12:39 +03:00
linkpath = os . path . realpath ( TZ_FILE )
tokens = linkpath . split ( " zoneinfo/ " )
2019-06-11 21:37:28 +03:00
if len ( tokens ) > 1 :
return tokens [ 1 ]
2019-03-05 22:27:37 +03:00
def get_language ( ) :
2019-06-08 23:12:39 +03:00
return locale . getlocale ( ) [ 0 ]
2019-03-05 22:27:37 +03:00
config = Libosinfo . InstallConfig ( )
2019-10-17 19:01:25 +03:00
# Set user login and name
# In case it's specified via command-line, use the specified one as login
# and realname. Otherwise, fallback fto the one from the system
2019-11-25 20:02:27 +03:00
login = unattended_data . user_login
realname = unattended_data . user_login
if not login :
2020-07-18 01:27:14 +03:00
login , realname = _login_from_hostuser ( )
2019-11-25 20:02:27 +03:00
if login :
login = login . lower ( )
2020-07-18 01:27:14 +03:00
if not _is_user_login_safe ( login ) :
2019-11-25 20:02:27 +03:00
raise RuntimeError (
2020-07-12 00:31:40 +03:00
_ ( " %(osname)s cannot use ' %(loginname)s ' as user-login. " ) %
{ " osname " : osobj . name , " loginname " : login } )
2019-10-17 19:08:28 +03:00
2019-11-25 20:02:27 +03:00
config . set_user_login ( login )
config . set_user_realname ( realname )
2019-03-05 22:27:37 +03:00
# Set user-password.
# In case it's required and not passed, just raise a RuntimeError.
2019-07-03 17:01:28 +03:00
if ( script . requires_user_password ( ) and
not unattended_data . get_user_password ( ) ) :
2019-03-05 22:27:37 +03:00
raise RuntimeError (
_ ( " %s requires the user-password to be set. " ) %
osobj . name )
2019-07-03 17:01:28 +03:00
config . set_user_password ( unattended_data . get_user_password ( ) or " " )
2019-03-05 22:27:37 +03:00
# Set the admin-password:
# In case it's required and not passed, just raise a RuntimeError.
2019-07-03 17:01:28 +03:00
if ( script . requires_admin_password ( ) and
not unattended_data . get_admin_password ( ) ) :
2019-03-05 22:27:37 +03:00
raise RuntimeError (
_ ( " %s requires the admin-password to be set. " ) %
osobj . name )
2019-07-03 17:01:28 +03:00
config . set_admin_password ( unattended_data . get_admin_password ( ) or " " )
2019-03-05 22:27:37 +03:00
# Set the target disk.
# virtiodisk is the preferred way, in case it's supported, otherwise
# just fallback to scsi.
#
# Note: this is linux specific and will require some changes whenever
# support for Windows will be added.
tgt = " /dev/vda " if osobj . supports_virtiodisk ( ) else " /dev/sda "
2019-03-29 00:44:44 +03:00
if osobj . is_windows ( ) :
tgt = " C "
2019-03-05 22:27:37 +03:00
config . set_target_disk ( tgt )
# Set hardware architecture and hostname
config . set_hardware_arch ( arch )
2019-09-27 16:25:23 +03:00
# Some installations will bail if the Computer's name contains one of the
# following characters: "[{|}~[\\]^':; <=>?@!\"#$%`()+/.,*&]".
# In order to take a safer path, let's ensure that we never set those,
# replacing them by "-".
hostname = re . sub ( " [ { |}~[ \\ ]^ ' :; <=>?@! \" #$ % `()+/.,*&] " , " - " , hostname )
2019-03-05 22:27:37 +03:00
config . set_hostname ( hostname )
# Try to guess the timezone from '/etc/localtime', in case it's not
# possible 'America/New_York' will be used.
timezone = get_timezone ( )
if timezone :
config . set_l10n_timezone ( timezone )
# Try to guess to language and keyboard layout from the system's
# language.
#
2019-06-11 21:37:28 +03:00
# This method has flaws as it's quite common to have language and
2019-03-05 22:27:37 +03:00
# keyboard layout not matching. Otherwise, there's no easy way to guess
# the keyboard layout without relying on a set of APIs of an specific
# Desktop Environment.
language = get_language ( )
if language :
config . set_l10n_language ( language )
config . set_l10n_keyboard ( language )
2019-03-07 15:52:50 +03:00
if url :
2019-03-09 00:57:40 +03:00
config . set_installation_url ( url ) # pylint: disable=no-member
2019-03-07 15:52:50 +03:00
2020-01-24 12:22:36 +03:00
if unattended_data . reg_login :
config . set_reg_login ( unattended_data . reg_login )
2019-03-29 00:44:44 +03:00
if unattended_data . product_key :
config . set_reg_product_key ( unattended_data . product_key )
2019-06-17 04:12:39 +03:00
log . debug ( " InstallScriptConfig created with the following params: " )
log . debug ( " username: %s " , config . get_user_login ( ) )
log . debug ( " realname: %s " , config . get_user_realname ( ) )
log . debug ( " target disk: %s " , config . get_target_disk ( ) )
log . debug ( " hardware arch: %s " , config . get_hardware_arch ( ) )
log . debug ( " hostname: %s " , config . get_hostname ( ) )
log . debug ( " timezone: %s " , config . get_l10n_timezone ( ) )
log . debug ( " language: %s " , config . get_l10n_language ( ) )
log . debug ( " keyboard: %s " , config . get_l10n_keyboard ( ) )
2019-08-01 00:43:49 +03:00
if hasattr ( config , " get_installation_url " ) :
log . debug ( " url: %s " ,
config . get_installation_url ( ) ) # pylint: disable=no-member
2020-01-24 12:22:36 +03:00
log . debug ( " reg-login %s " , config . get_reg_login ( ) )
2019-06-17 04:12:39 +03:00
log . debug ( " product-key: %s " , config . get_reg_product_key ( ) )
2019-03-05 22:27:37 +03:00
return config
2019-03-05 20:20:29 +03:00
class OSInstallScript :
"""
Wrapper for Libosinfo . InstallScript interactions
"""
2019-03-05 22:13:50 +03:00
@staticmethod
def have_new_libosinfo ( ) :
2019-06-17 04:34:47 +03:00
from . . osdict import OSDB
2019-06-10 03:18:13 +03:00
win7 = OSDB . lookup_os ( " win7 " )
for script in win7 . get_install_script_list ( ) :
if ( Libosinfo . InstallScriptInjectionMethod . CDROM &
script . get_injection_methods ( ) ) :
return True
2019-06-11 21:37:28 +03:00
return False # pragma: no cover
2019-03-05 22:13:50 +03:00
2019-08-01 00:43:49 +03:00
@staticmethod
def have_libosinfo_installation_url ( ) :
return hasattr ( Libosinfo . InstallConfig , " set_installation_url " )
2019-07-16 18:14:31 +03:00
def __init__ ( self , script , osobj , osinfomediaobj , osinfotreeobj ) :
2019-03-05 20:20:29 +03:00
self . _script = script
self . _osobj = osobj
2019-07-12 16:02:21 +03:00
self . _osinfomediaobj = osinfomediaobj
2019-07-16 18:14:31 +03:00
self . _osinfotreeobj = osinfotreeobj
2019-03-05 22:27:37 +03:00
self . _config = None
2019-03-05 20:20:29 +03:00
2019-06-11 21:37:28 +03:00
if not OSInstallScript . have_new_libosinfo ( ) : # pragma: no cover
2019-06-10 03:18:13 +03:00
raise RuntimeError ( _ ( " libosinfo or osinfo-db is too old to "
" support unattended installs. " ) )
2019-03-05 22:13:50 +03:00
2019-03-05 20:20:29 +03:00
def get_expected_filename ( self ) :
return self . _script . get_expected_filename ( )
2019-06-11 21:37:28 +03:00
def set_preferred_injection_method ( self , namestr ) :
# If we ever make this user configurable, this will need to be smarter
names = {
" cdrom " : Libosinfo . InstallScriptInjectionMethod . CDROM ,
" initrd " : Libosinfo . InstallScriptInjectionMethod . INITRD ,
}
2019-03-05 20:20:29 +03:00
2019-06-17 04:12:39 +03:00
log . debug ( " Using ' %s ' injection method " , namestr )
2019-06-11 21:37:28 +03:00
injection_method = names [ namestr ]
2019-03-05 20:20:29 +03:00
supported_injection_methods = self . _script . get_injection_methods ( )
if ( injection_method & supported_injection_methods == 0 ) :
raise RuntimeError (
2020-07-12 00:31:40 +03:00
_ ( " OS ' %(osname)s ' does not support required "
" injection method ' %(methodname)s ' " ) %
{ " osname " : self . _osobj . name , " methodname " : namestr } )
2019-03-05 20:20:29 +03:00
self . _script . set_preferred_injection_method ( injection_method )
2019-06-11 21:37:28 +03:00
def set_installation_source ( self , namestr ) :
# If we ever make this user configurable, this will need to be smarter
names = {
" media " : Libosinfo . InstallScriptInstallationSource . MEDIA ,
" network " : Libosinfo . InstallScriptInstallationSource . NETWORK ,
}
2019-03-05 20:20:29 +03:00
2019-06-17 04:12:39 +03:00
log . debug ( " Using ' %s ' installation source " , namestr )
2019-06-11 21:37:28 +03:00
self . _script . set_installation_source ( names [ namestr ] )
2019-03-05 20:20:29 +03:00
2019-03-05 22:27:37 +03:00
def _requires_param ( self , config_param ) :
param = self . _script . get_config_param ( config_param )
return bool ( param and not param . is_optional ( ) )
2019-03-05 20:20:29 +03:00
2019-03-05 22:27:37 +03:00
def requires_user_password ( self ) :
return self . _requires_param (
Libosinfo . INSTALL_CONFIG_PROP_USER_PASSWORD )
def requires_admin_password ( self ) :
return self . _requires_param (
Libosinfo . INSTALL_CONFIG_PROP_ADMIN_PASSWORD )
def set_config ( self , config ) :
self . _config = config
2019-06-08 21:16:52 +03:00
def generate ( self ) :
2019-07-12 16:02:22 +03:00
if self . _osinfomediaobj :
return self . _script . generate_for_media (
self . _osinfomediaobj , self . _config )
2019-07-16 18:14:31 +03:00
if hasattr ( self . _script , " generate_for_tree " ) and self . _osinfotreeobj :
# osinfo_install_script_generate_for_tree() is part of
# libosinfo 1.6.0
return self . _script . generate_for_tree (
self . _osinfotreeobj , self . _config )
2019-07-12 16:02:22 +03:00
2019-06-08 21:16:52 +03:00
return self . _script . generate ( self . _osobj . get_handle ( ) , self . _config )
2019-03-05 20:20:29 +03:00
2019-03-05 22:27:37 +03:00
def generate_cmdline ( self ) :
2019-07-12 16:02:22 +03:00
if self . _osinfomediaobj :
return self . _script . generate_command_line_for_media (
self . _osinfomediaobj , self . _config )
2019-07-16 18:14:31 +03:00
if ( hasattr ( self . _script , " generate_command_line_for_tree " ) and
self . _osinfotreeobj ) :
# osinfo_install_script_generate_command_line_for_tree() is part of
# libosinfo 1.6.0
return self . _script . generate_command_line_for_tree (
self . _osinfotreeobj , self . _config )
2019-03-05 20:20:29 +03:00
return self . _script . generate_command_line (
2019-03-05 22:27:37 +03:00
self . _osobj . get_handle ( ) , self . _config )
2019-02-22 11:40:16 +03:00
2019-07-03 17:01:29 +03:00
def _generate_debug ( self ) :
2019-07-12 16:02:19 +03:00
original_user_password = self . _config . get_user_password ( )
original_admin_password = self . _config . get_admin_password ( )
self . _config . set_user_password ( " [SCRUBBLED] " )
self . _config . set_admin_password ( " [SCRUBBLED] " )
debug_content = self . generate ( )
self . _config . set_user_password ( original_user_password )
self . _config . set_admin_password ( original_admin_password )
return debug_content
2019-07-03 17:01:29 +03:00
2019-06-14 04:50:38 +03:00
def write ( self ) :
2019-06-10 02:10:19 +03:00
fileobj = tempfile . NamedTemporaryFile (
2019-06-14 04:50:38 +03:00
prefix = " virtinst-unattended-script " , delete = False )
2019-06-10 02:10:19 +03:00
scriptpath = fileobj . name
content = self . generate ( )
open ( scriptpath , " w " ) . write ( content )
2019-07-03 17:01:29 +03:00
debug_content = self . _generate_debug ( )
2019-06-17 04:12:39 +03:00
log . debug ( " Generated unattended script: %s " , scriptpath )
2019-07-03 17:01:29 +03:00
log . debug ( " Generated script contents: \n %s " , debug_content )
2019-06-10 02:10:19 +03:00
return scriptpath
2019-02-22 11:40:08 +03:00
class UnattendedData ( ) :
profile = None
2019-07-03 17:01:28 +03:00
admin_password_file = None
2019-10-17 19:01:25 +03:00
user_login = None
2019-07-03 17:01:28 +03:00
user_password_file = None
2019-03-29 00:44:43 +03:00
product_key = None
2020-01-24 12:22:36 +03:00
reg_login = None
2019-02-22 11:40:16 +03:00
2019-07-03 17:01:28 +03:00
def _get_password ( self , pwdfile ) :
with open ( pwdfile , " r " ) as fobj :
return fobj . readline ( ) . rstrip ( " \n \r " )
def get_user_password ( self ) :
if self . user_password_file :
return self . _get_password ( self . user_password_file )
def get_admin_password ( self ) :
if self . admin_password_file :
return self . _get_password ( self . admin_password_file )
2019-02-22 11:40:16 +03:00
2019-06-11 22:18:47 +03:00
def _make_scriptmap ( script_list ) :
"""
Generate a mapping of profile name - > [ list , of , rawscripts ]
"""
script_map = { }
for script in script_list :
profile = script . get_profile ( )
if profile not in script_map :
script_map [ profile ] = [ ]
script_map [ profile ] . append ( script )
return script_map
def _find_default_profile ( profile_names ) :
profile_prefs = [ " desktop " ]
found = None
for p in profile_prefs :
if p in profile_names :
found = p
break
return found or profile_names [ 0 ]
2019-07-30 18:23:35 +03:00
def _lookup_rawscripts ( osinfo , profile , os_media ) :
2019-06-09 18:26:28 +03:00
script_list = [ ]
if os_media :
if not os_media . supports_installer_script ( ) :
# This is a specific annotation for media like livecds that
# don't support unattended installs
raise RuntimeError (
_ ( " OS ' %s ' media does not support unattended "
2019-06-13 02:11:21 +03:00
" installation " ) % ( osinfo . name ) )
2019-06-09 18:26:28 +03:00
# In case we're dealing with a media installation, let's try to get
# the installer scripts from the media, in case any is set.
script_list = os_media . get_install_script_list ( )
if not script_list :
2019-06-13 02:11:21 +03:00
script_list = osinfo . get_install_script_list ( )
2019-06-09 18:26:28 +03:00
if not script_list :
raise RuntimeError (
_ ( " OS ' %s ' does not support unattended installation. " ) %
2019-06-13 02:11:21 +03:00
osinfo . name )
2019-06-09 18:26:28 +03:00
2019-06-11 22:18:47 +03:00
script_map = _make_scriptmap ( script_list )
profile_names = list ( sorted ( script_map . keys ( ) ) )
if profile :
rawscripts = script_map . get ( profile , [ ] )
if not rawscripts :
raise RuntimeError (
2020-07-12 00:31:40 +03:00
_ ( " OS ' %(osname)s ' does not support unattended "
" installation for the ' %(profilename)s ' profile. "
" Available profiles: %(profiles)s " ) %
{ " osname " : osinfo . name , " profilename " : profile ,
" profiles " : " , " . join ( profile_names ) } )
2019-06-11 22:18:47 +03:00
else :
profile = _find_default_profile ( profile_names )
2019-06-17 04:12:39 +03:00
log . warning ( _ ( " Using unattended profile ' %s ' " ) , profile )
2019-06-11 22:18:47 +03:00
rawscripts = script_map [ profile ]
2019-06-09 18:26:28 +03:00
# Some OSes (as Windows) have more than one installer script,
# depending on the OS version and profile chosen, to be used to
2019-07-30 18:23:35 +03:00
# perform the unattended installation.
ids = [ ]
for rawscript in rawscripts :
ids . append ( rawscript . get_id ( ) )
2019-06-09 18:26:28 +03:00
2019-07-30 18:23:35 +03:00
log . debug ( " Install scripts found for profile ' %s ' : %s " ,
profile , " , " . join ( ids ) )
return rawscripts
2019-06-09 18:26:28 +03:00
2019-07-30 18:23:35 +03:00
def prepare_install_scripts ( guest , unattended_data ,
2019-07-16 18:14:30 +03:00
url , os_media , os_tree , injection_method ) :
2019-03-22 18:23:38 +03:00
def _get_installation_source ( os_media ) :
2019-03-22 18:23:39 +03:00
if not os_media :
2019-03-07 15:52:54 +03:00
return " network "
2019-06-09 18:26:28 +03:00
return " media "
2019-03-07 15:52:54 +03:00
2019-07-30 18:23:35 +03:00
scripts = [ ]
rawscripts = _lookup_rawscripts ( guest . osinfo ,
2019-06-13 02:11:21 +03:00
unattended_data . profile , os_media )
2019-07-12 16:02:21 +03:00
osinfomediaobj = os_media . get_osinfo_media ( ) if os_media else None
2019-07-16 18:14:31 +03:00
osinfotreeobj = os_tree . get_osinfo_tree ( ) if os_tree else None
2019-02-22 11:40:16 +03:00
2019-07-30 18:23:35 +03:00
for rawscript in rawscripts :
script = OSInstallScript (
rawscript , guest . osinfo , osinfomediaobj , osinfotreeobj )
script . set_preferred_injection_method ( injection_method )
2019-03-07 15:52:54 +03:00
2019-07-30 18:23:35 +03:00
installationsource = _get_installation_source ( os_media )
script . set_installation_source ( installationsource )
2019-02-22 11:40:16 +03:00
2019-07-30 18:23:35 +03:00
config = _make_installconfig ( script , guest . osinfo , unattended_data ,
guest . os . arch , guest . name , url )
script . set_config ( config )
scripts . append ( script )
return scripts
2019-07-30 19:04:13 +03:00
def download_drivers ( locations , scratchdir , meter ) :
meter = progress . ensure_meter ( meter )
fetcher = urlfetcher . DirectFetcher ( None , scratchdir , meter )
fetcher . meter = meter
drivers = [ ]
2019-10-02 18:27:19 +03:00
try :
for location in locations :
filename = location . rsplit ( ' / ' , 1 ) [ 1 ]
driver = fetcher . acquireFile ( location )
drivers . append ( ( driver , filename ) )
except Exception : # pragma: no cover
for driverpair in drivers :
os . unlink ( driverpair [ 0 ] )
raise
2019-07-30 19:04:13 +03:00
return drivers