#!/bin/ksh -p
#
# Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#
# ident	"@(#)s9_install.ksh	1.2	08/07/10 SMI"
#

PRODUCT_NAME="Solaris 9 Containers"
PRODUCT_REV="1.0"
PRODUCT_VERSION="$PRODUCT_NAME $PRODUCT_REV"

INSTALLER_NAME="solaris9 brand installer"
INSTALLER_VERSION="solaris9 brand installer 1.21"


# Restrict executables to /bin, /usr/bin and /usr/sfw/bin
PATH=/bin:/usr/bin:/usr/sbin:/usr/sfw/bin
export PATH

# Setup i18n output
TEXTDOMAIN="SUNW_OST_OSCMD"
export TEXTDOMAIN

# Log passed arguments to file descriptor 2
log()
{
	[[ -n $logfile ]] && echo "[`date`]" "$@" >&2
}

#
# Send the provided printf()-style arguments to the screen and to the
# logfile.
#
screenlog()
{
	typeset fmt="$1"
	shift

	printf "$fmt\n" "$@"
	[[ -n $logfile ]] && printf "[`date`] $fmt\n" "$@" >&2
}

# Print and log provided text if the shell variable "verbose_mode" is set
vscreenlog()
{
	typeset fmt="$1"
	shift

	[[ -n $verbose_mode ]] && printf "$fmt\n" "$@"
	[[ -n $logfile ]] && printf "[`date`] $fmt\n" "$@" >&2
}

cmd_not_found=$(gettext "Required command '%s' cannot be found!")
cmd_not_exec=$(gettext "Required command '%s' not executable!")
zone_initfail=$(gettext "Attempt to initialize zone '%s' FAILED.")
path_abs=$(gettext "Pathname specified to -a '%s' must be absolute.")

cmd_h=$(gettext "%s -z <zone name> %s -h")
cmd_V=$(gettext "%s -z <zone name> %s -V")
cmd_full=$(gettext "%s -z <zone name> %s -u | -p [-v | -s] -a archive | -d directory")

both_modes=$(gettext "%s: error: cannot select both silent and verbose modes")

both_choices=$(gettext "%s: error: cannot select both preserve and unconfigure options")

both_kinds=$(gettext "%s: error: cannot specify both archive and directory")

not_found=$(gettext "%s: error: file or directory not found.")

wrong_dir_type=$(gettext "error: must be a directory")

not_readable=$(gettext "Cannot read file '%s'")

no_install=$(gettext "Could not create install directory '%s'")
no_log=$(gettext "Could not create log directory '%s'")

media_taste=$(gettext   "    Media Type: %s")
bad_archive=$(gettext "ERROR: must be a flash archive, a cpio archive (can also
be gzipped or bzipped), a pax XUSTAR archive, or a level 0 ufsdump archive.")

product_vers=$(gettext  "       Product: %s")
install_vers=$(gettext  "     Installer: %s")
install_zone=$(gettext  "          Zone: %s")
install_path=$(gettext  "          Path: %s")
install_from=$(gettext  "        Source: %s")
installing=$(gettext    "    Installing: This may take several minutes...")
install_prog=$(gettext  "    Installing: %s")

install_fail=$(gettext  "        Result: *** Installation FAILED ***")
install_log=$(gettext   "      Log File: %s")

install_abort=$(gettext "        Result: Installation aborted.")
install_good=$(gettext  "        Result: Installation completed successfully.")

not_s9_image=$(gettext  "  Sanity Check: %s doesn't look like a Solaris 9 image.")
sanity_ok=$(gettext     "  Sanity Check: Passed.  Looks like a Solaris 9 system.")
sanity_fail_detail=$(gettext  "  Sanity Check: Missing %s at %s")
sanity_fail=$(gettext   "  Sanity Check: FAILED (see log for details).")


p2ving=$(gettext        "Postprocessing: This may take several minutes...")
p2v_prog=$(gettext      "   Postprocess: ")
p2v_done=$(gettext      "        Result: Postprocessing complete.")
p2v_fail=$(gettext      "        Result: Postprocessing failed.")
servicetag_ok=$(gettext "   Service Tag: Added successfully.")
servicetag_fail=$(gettext "   Service Tag: FAILED (see log for details)")

tag_prog=$(gettext      "   Service Tag: ")

media_missing=\
$(gettext "%s: error: you must specify an installation source using '-a' or '-d'.")

cfgchoice_missing=\
$(gettext "%s: error: you must specify -u (sys-unconfig) or -p (preserve identity).")

mount_failed=$(gettext "ERROR: zonecfg(1M) 'fs' mount failed")

not_flar=$(gettext "Input is not a flash archive")
bad_flar=$(gettext "Flash archive is a corrupt")
unknown_archiver=$(gettext "Archiver %s is not supported")

no_rtu=$(gettext "ERROR: The $PRODUCT_NAME right-to-use package has not been installed")

# Clean up on interrupt
trap_cleanup()
{
	msg=$(gettext "Installation cancelled due to interrupt.")

	screenlog "$msg"
	exit $int_code
}

#
# Output the usage message.
#
# This is done this way due to limitations in the way gettext strings are
# extracted from shell scripts and processed.  Use of this somewhat awkward
# syntax allows us to produce longer lines of text than otherwise would be
# possible without wrapping lines across more than one line of code.
#
usage()
{
	int_code=$ZONE_SUBPROC_USAGE

	echo $(gettext "Usage:")
	printf "  $cmd_h\n" "zoneadm" "install"
	printf "  $cmd_full\n" "zoneadm" "install"
	printf "  $cmd_V\n" "zoneadm" "install"

	echo
	echo $(gettext "Examples")
	echo "========"

	echo $(gettext "Example 1: Install from a flash archive of") \
	    $(gettext "an existing S9 installation.  sys-unconfig(1m) the") \
	    $(gettext "zone, and provide verbose output regarding the") \
	    $(gettext "progress of the installation:") | fmt -80

	echo
	echo "    # zoneadm -z myzone install -v -u -a /tmp/oldsystem.flar"
	echo

	echo $(gettext "Example 2: Install from an unpacked directory of") \
	    $(gettext "an existing S9 installation.  Preserve system ") \
	    $(gettext "identity.  Note that a copy of") \
	    $(gettext "this directory will be made during installation.") \
	        | fmt -80

	echo
	echo "    # zoneadm -z myzone install -p -d /export/oldsystem/"
	echo

	exit $int_code
}

#
# There's a sanity check, but there ain't no sanity clause.
#
sanity_check()
{
	typeset dir="$1"
	shift
	ret=0

	checks="etc etc/init usr usr/sbin usr/lib sbin lib lib/ld.so.1"
	for x in $checks; do
		if [[ ! -e $dir/$x ]]; then
			vscreenlog "$sanity_fail_detail" "$x" "$dir"
			ret=1
		fi
	done

	return $ret
}

#
# Read zonecfg fs entries and save the relevant data, one entry per line.
# This assumes the order of the fields from the zonecfg output, e.g.:
#	fs:
#		dir: /opt
#		special: /opt
#		raw not specified
#		type: lofs
#		options: [noexec,ro,noatime]
#
get_fs_info()
{
	zonecfg -z $zonename info fs | nawk '{
		if ($1 == "options:") {
			# Remove brackets.
			options=substr($2, 2, length($2) - 2);
			printf("%s %s %s %s\n", dir, type, special, options);
		} else if ($1 == "dir:") {
			dir=$2;
		} else if ($1 == "special:") {
			special=$2;
		} else if ($1 == "type:") {
			type=$2
		}
	}' > $fstmpfile
}

#
# Mount zonecfg fs entries into the zonepath.
#
mnt_fs()
{
	if [ ! -s $fstmpfile ]; then
		return;
	fi

	# Sort the fs entries so we can handle nested mounts.
	sort $fstmpfile | nawk -v zonepath=$zonepath '{
		if (NF == 4)
			options="-o " $4;
		else
			options=""

		# Create the mount point.  Ignore errors since we might have
		# a nested mount with a pre-existing mount point.
		cmd="/usr/bin/mkdir -p " zonepath "/root" $1 " >/dev/null 2>&1"
		system(cmd);

		cmd="/usr/sbin/mount -F " $2 " " options " " $3 " " \
		    zonepath "/root" $1;
		if (system(cmd) != 0) {
			printf("%s\n", cmd);
			exit 1;
		}
	}'
}

#
# Unmount zonecfg fs entries from the zonepath.
#
umnt_fs()
{
	if [ ! -s $fstmpfile ]; then
		return;
	fi

	# Reverse sort the fs entries so we can handle nested unmounts.
	sort -r $fstmpfile | nawk -v zonepath=$zonepath '{
		cmd="/usr/sbin/umount " zonepath "/root" $1
		if (system(cmd) != 0) {
			printf("%s\n", cmd);
		}
	}'
}

#
# Determine flar compression style from identification file.
#
get_compression()
{
	typeset ident=$1
	typeset line=$(grep "files_compressed_method" $ident)

	print ${line##*=}
}

#
# Determine flar archive style from identification file.
#
get_archiver()
{
        typeset ident=$1
        typeset line=$(grep "files_archived_method" $ident)

        print ${line##*=}
}

#
# Unpack flar into current directory (which should be zoneroot).  The flash
# archive is standard input.  See flash_archive(4) man page.
# 
# We can't use "flar split" since it will only unpack into a directory called
# "archive".  We need to unpack in place in order to properly handle nested
# fs mounts within the zone root.  This function does the unpacking into the
# current directory.
#
# This code is derived from the gen_split() function in /usr/sbin/flar so
# we keep the same style as the original.
#
do_flar()
{
	typeset result
        typeset archiver_command
        typeset archiver_arguments

	# Read cookie
	read -r input_line
	if [[ $? -ne 0 ]]; then
		screenlog "$not_readable" "$install_media"
		return 1
	fi
	# The cookie has format FlAsH-aRcHiVe-m.n where m and n are integers.
	if [[ ${input_line%%-[0-9]*.[0-9]*} != "FlAsH-aRcHiVe" ]]; then
		screenlog "$not_flar"
		return 1
	fi

	while [ true ]
	do
		# We should always be at the start of a section here
		read -r input_line
		if [[ ${input_line%%=*} != "section_begin" ]]; then
			screenlog "$bad_flar"
			return 1
		fi
		section_name=${input_line##*=}

		# If we're at the archive, we're done skipping sections.
		if [[ "$section_name" = "archive" ]]; then
			break
		fi
		
		#
		# Save identification section to a file so we can determine
		# how to unpack the archive.
		#
		if [[ "$section_name" = "identification" ]]; then
			/usr/bin/rm -f identification
			while read -r input_line
			do
				if [[ ${input_line%%=*} = \
				    "section_begin" ]]; then
					/usr/bin/rm -f identification
					screenlog "$bad_flar"
					return 1
				fi

				if [[ $input_line = \
				    "section_end=$section_name" ]]; then
					break;
				fi
				echo $input_line >> identification
			done

			continue
		fi

		#
		# Otherwise skip past this section; read lines until detecting
		# section_end.  According to flash_archive(4) we can have
		# an arbitrary number of sections but the archive section
		# must be last.
		#
		success=0
		while read -r input_line
		do
			if [[ $input_line = "section_end=$section_name" ]]; then
				success=1
				break
			fi
			# Fail if we miss the end of the section
			if [[ ${input_line%%=*} = "section_begin" ]]; then
				/usr/bin/rm -f identification
				screenlog "$bad_flar"
				return 1
			fi
		done
		if [[ $success = 0 ]]; then
			#
			# If we get here we read to the end of the file before
			# seeing the end of the section we were reading.
			#
			/usr/bin/rm -f identification
			screenlog "$bad_flar"
			return 1
		fi
	done

	# Get the information needed to unpack the archive.
	archiver=$(get_archiver identification)
	if [[ $archiver = "pax" ]]; then
		# pax archiver specified
		archiver_command="/usr/bin/pax"
		archiver_arguments="-r -p e"
	elif [[ $archiver = "cpio" || -z $archiver ]]; then
		# cpio archived specified OR no archiver specified - use default
		archiver_command="/usr/bin/cpio"
		archiver_arguments="-icdum"
	else
		# unknown archiver specified
		screenlog "$unknown_archiver" $archiver
		return 1
	fi

	if [[ ! -x $archiver_command ]]; then
		/usr/bin/rm -f identification
		screenlog "$cmd_not_exec" $archiver_command
		return 1
	fi 

	compression=$(get_compression identification)

	# We're done with the identification file
	/usr/bin/rm -f identification

	# Extract archive
	if [[ $compression = "compress" ]]; then
		/usr/bin/zcat | \
		    $archiver_command $archiver_arguments 2>/dev/null
	else
		$archiver_command $archiver_arguments 2>/dev/null
	fi
	result=$?

	[[ $result -ne 0 ]] && return 1

	return 0 
}

#
# The main body of the script starts here.
#
# This script should never be called directly by a user but rather should
# only be called by zoneadm to install a BrandZ solaris9 zone.
#

#
# Exit values used by the script, as #defined in <sys/zone.h>
#
#	ZONE_SUBPROC_OK
#	===============
#	Installation was successful
#
#	ZONE_SUBPROC_USAGE
#	==================
#	Improper arguments were passed, so print a usage message before exiting
#
#	ZONE_SUBPROC_NOTCOMPLETE
#	========================
#	Installation did not complete, but another installation attempt can be
#	made without an uninstall
#
#	ZONE_SUBPROC_FATAL
#	==================
#	Installation failed and an uninstall will be required before another
#	install can be attempted
#
ZONE_SUBPROC_OK=0
ZONE_SUBPROC_USAGE=253
ZONE_SUBPROC_NOTCOMPLETE=254
ZONE_SUBPROC_FATAL=255

#
# Exit code to return if install is interrupted or exit code is otherwise
# unspecified.
#
int_code=$ZONE_SUBPROC_USAGE

trap trap_cleanup INT

# If we weren't passed at least two arguments, exit now.
[[ $# -lt 2 ]] && usage

#
# This script is always started with a full path so we can extract the
# brand directory name here.
#
branddir=$(dirname "$0")
zonename="$1"
zonepath="$2"

zoneroot="$zonepath/root"
logdir="$zoneroot/var/log"

shift; shift	# remove zonename and zonepath from arguments array

unset install_archive
unset install_dir
unset msg
unset silent_mode
unset verbose_mode

#
# It is worth noting here that we require the end user to pick one of
# -u (sys-unconfig) or -p (preserve config).  This is because we can't
# really know in advance which option makes a better default.  Forcing
# the user to pick one or the other means that they will consider their
# choice and hopefully not be surprised or disappointed with the result.
#
unset unconfig_zone
unset preserve_zone

while getopts "a:d:DhpsuvV" opt
do
	case "$opt" in
		h) 	usage;;
		s)	silent_mode=1;;
		u)	unconfig_zone="-u";;
		p)	preserve_zone="-p";;
		v)	verbose_mode="-v";;
		V)	echo $PRODUCT_VERSION; echo $INSTALLER_VERSION; exit 0;;
		D)	set -o xtrace ;;
		a) 	install_archive="$OPTARG" ; install_media="$OPTARG";;
		d) 	install_dir="$OPTARG" ; install_media="$OPTARG";;
		*)	usage;;
	esac
done
shift OPTIND-1

# Providing more than one passed argument generates a usage message
if [[ $# -gt 1 ]]; then
	msg=$(gettext "ERROR: Too many arguments provided:")

	screenlog "$msg"
	screenlog "  \"%s\"" "$@"
	screenlog ""
	usage
fi

# The install can't be both verbose AND silent...
if [[ -n $silent_mode && -n $verbose_mode ]]; then
	screenlog "$both_modes" "zoneadm install"
	exit $int_code
fi

if [[ -z $install_media ]]; then
	screenlog "$media_missing" "zoneadm install"
	exit $int_code
fi

if [[ -n $install_archive && -n $install_dir ]]; then
	screenlog "$both_kinds" "zoneadm install"
fi

# The install can't both preserve and unconfigure
if [[ -n $unconfig_zone && -n $preserve_zone ]]; then
	screenlog "$both_choices" "zoneadm install"
	exit $int_code
fi

# Must pick one or the other.
if [[ -z $unconfig_zone && -z $preserve_zone ]]; then
	screenlog "$cfgchoice_missing" "zoneadm install"
	exit $int_code
fi

#
# Verify the RTU has been installed.
#
if [[ ! -f /usr/lib/brand/solaris9/files/patches/order ]]; then
	screenlog "$no_rtu"
	exit $int_code
fi

#
# Validate $install_media (things common to archive/dir)
#
if [[ "`echo $install_media | cut -c 1`" != "/" ]]; then
	screenlog "$path_abs" "$install_media"
	exit $int_code
fi

if [[ ! -a "$install_media" ]]; then
	screenlog "$not_found" "$install_media"
	screenlog "$install_abort" "$zonename"
	exit $int_code
fi

if [[ ! -r "$install_media" ]]; then
	screenlog "$not_readable" "$install_media"
	screenlog "$install_abort" "$zonename"
	exit $int_code
fi

if [[ -n $install_archive ]]; then
	if [[ ! -f "$install_archive" ]]; then
		screenlog "$media_taste" "$bad_archive"
		screenlog "$install_abort" "$zonename"
		exit $int_code
	fi
fi

if [[ -n $install_dir ]]; then
	if [[ ! -d "$install_dir" ]]; then
		screenlog "$media_taste" "$wrong_dir_type"
		screenlog "$install_abort" "$zonename"
		exit $int_code
	fi
fi

logfile="/var/tmp/$zonename.install.$$.log"
zone_logfile="${logdir}/$zonename.install.$$.log"
exec 2>"$logfile"
screenlog "$install_log" "$logfile"

fstmpfile=/tmp/fsinfo.$zonename.$$

vscreenlog "$product_vers" "$PRODUCT_VERSION"
vscreenlog "$install_vers" "$INSTALLER_VERSION"
vscreenlog "$install_zone" "$zonename"
vscreenlog "$install_path" "$zonepath"

log "Starting pre-installation tasks."

if [[ -z $install_archive && -n $install_dir ]]; then
	#
	# Minimal check to make sure that the user is passing
	# us something that at least seems to be S9.
	#
	sanity_check $install_dir
	if [[ $? -ne 0 ]]; then
		screenlog "$not_s9_image" "$install_dir"
		exit $int_code
	fi

	filetype="directory"
	filetypename="directory"
else
	ftype="`LC_ALL=C file $install_archive | cut -d: -f 2`"
	case "$ftype" in
	*cpio*)		filetype="cpio"
			filetypename="cpio archive"
		;;
	*bzip2*)	filetype="bzip2"
			filetypename="bzipped cpio archive"
		;;
	*gzip*)		filetype="gzip"
			filetypename="gzipped cpio archive"
		;;
	*ufsdump*)	filetype="ufsdump"
			filetypename="ufsdump archive"
		;;
	*Flash\ Archive*)	filetype="flar"
			filetypename="flash archive"
		;;
	*USTAR\ tar\ archive\ extended\ format*)	filetype="xustar"
			filetypename="pax (xustar) archive"
		;;
	*)		screenlog "$media_taste" "$bad_archive"
			screenlog "$install_abort" "$zonename"
			exit $int_code
		;;
	esac
fi

#
# From here on out, an unspecified exit or interrupt should exit with
# ZONE_SUBPROC_NOTCOMPLETE, meaning a user will need to do an uninstall before
# attempting another install, as we've modified the directories we were going
# to install to in some way.
#
int_code=$ZONE_SUBPROC_NOTCOMPLETE

if [[ ! -d "$zoneroot" ]]
then
	if ! mkdir -p "$zoneroot" 2>/dev/null; then
		screenlog "$no_install" "$zoneroot"
		exit $int_code
	fi
fi

log "Installation started for zone \"$zonename\""

screenlog "$install_from" "$install_media"
vscreenlog "$media_taste" "$filetypename"

# Set up any fs mounts so the archive will install into the correct locations.
get_fs_info
mnt_fs
if [ $? -ne 0 ]; then
	screenlog "$mount_failed"
	umnt_fs >/dev/null 2>&1
	rm -f $fstmpfile
	exit $int_code 
fi

screenlog "$installing"

unpack_result=0
stage1="cat"
if [[ "$filetype" = "gzip" ]]; then
	stage1="gzcat"
	filetype="cpio"
fi

if [[ "$filetype" = "bzip2" ]]; then
	stage1="bzcat"
	filetype="cpio"
fi

if [[ "$filetype" = "cpio" ]]; then

	cpioopts="-idm"
	log "cd \"$zoneroot\" && $stage1 \"$install_archive\" | cpio $cpioopts"

	( cd "$zoneroot" && $stage1 "$install_archive" | cpio $cpioopts )

	unpack_result=$?

elif [[ "$filetype" = "flar" ]]; then
	log "cd $zoneroot &&"
	log "do_flar < \"$install_archive\""

	( cd "$zoneroot" && do_flar < "$install_archive" )
	unpack_result=$?

elif [[ "$filetype" = "xustar" ]]; then
	log "cd \"$zoneroot\" && pax -r -f \"$install_archive\""

	( cd "$zoneroot" && pax -r -f "$install_archive" )

	unpack_result=$?

elif [[ "$filetype" = "ufsdump" ]]; then
	log "cd \"$zoneroot\" && ufsrestore rf \"$install_archive\""
	#
	# ufsrestore goes interactive if you ^C it.  To prevent that,
	# we make sure its stdin is not a terminal.
	#
	( cd "$zoneroot" && ufsrestore rf "$install_archive" < /dev/null )
	unpack_result=$?

elif [[ "$filetype" = "directory" ]]; then

	cpioopts="-pdm"
	list=`cd "$install_dir" && echo !(proc|devices|tmp|dev)`
	
	findopts="-xdev ( -type d -o -type f -o -type l ) -print"
	log "cd \"$install_dir\" && find $list $findopts | cpio $cpioopts \"$zoneroot\""

	( cd "$install_dir" && find $list $findopts | cpio $cpioopts "$zoneroot" )
	unpack_result=$?
fi

# Clean up any fs mounts used during unpacking.
umnt_fs
rm -f $fstmpfile

#
# Do a sanity check to see if various things we think should be present
# are present.  If not, the user might have supplied a cpio archive which was
# not created properly.
#
if [[ $unpack_result -eq 0 ]]; then
	sanity_check $zoneroot
	if [[ $? -ne 0 ]]; then
		screenlog "$sanity_fail"
		screenlog ""
		screenlog "$install_fail" "$zonename"
		screenlog "$install_log" "$logfile"

		exit $int_code
	else
		vscreenlog "$sanity_ok"
	fi
fi
	
chmod 700 $zonepath

screenlog "$p2ving"
log "running: p2v $verbose_mode $unconfig_zone $zonename"

#
# Run p2v.
#
# Getting the output to the right places is a little tricky because what
# we want is for p2v to output in the same way the installer does: verbose
# messages to the log file always, and verbose messages printed to the
# user if the user passes -v.  This rules out simple redirection.  And
# we can't use tee or other tricks because they cause us to lose the
# return value from the p2v script due to the way shell pipelines work.
#
# The simplest way to do this seems to be to hand off the management of
# the log file to the p2v script.  So we run p2v with -l to tell it where
# to find the log file and then reopen the log (O_APPEND) when p2v is done.
#
/usr/lib/brand/solaris9/s9_p2v -l "$logfile" -m "$p2v_prog" \
     $verbose_mode $unconfig_zone $zonename
p2v_result=$?
exec 2>>$logfile

if [[ $p2v_result -eq 0 ]]; then
	vscreenlog "$p2v_done"
else
	screenlog "$p2v_fail"
	screenlog ""
	screenlog "$install_fail"
	screenlog "$install_log" "$logfile"
	exit $ZONE_SUBPROC_FATAL
fi

int_code=$ZONE_SUBPROC_OK

#
# Play the same redirection trick with s9_servicetag as we
# did with p2v.  See above.
#
/usr/lib/brand/solaris9/s9_servicetag $verbose_mode -m "$tag_prog" \
    -l "$logfile" -a $zonename

exec 2>>$logfile

screenlog ""
screenlog "$install_good" "$zonename"

#
# Just in case the log directory isn't present...
#
if [[ ! -d "$logdir" ]]; then
	if ! mkdir -p "$logdir" 2>/dev/null; then
		screenlog "$no_log" "$logdir"
	fi
fi
cp $logfile $zone_logfile
screenlog "$install_log" "$zone_logfile"

exit 0
