#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#

#
# Copyright (c) 2008, 2012, Oracle and/or its affiliates. All rights reserved.
#

#
# Only change PATH if you give full consideration to GNU or other variants
# of common commands having different arguments and output.
#
export PATH=/usr/bin:/usr/sbin
unset LD_LIBRARY_PATH

. /usr/lib/brand/shared/common.ksh

PROP_PARENT="org.opensolaris.libbe:parentbe"
PROP_ACTIVE="org.opensolaris.libbe:active"
PROP_BE_HANDLE="com.oracle.libbe:nbe_handle"
PROP_CANDIDATE="com.oracle.zoneadm:candidate_zbe"

f_incompat_options=$(gettext "cannot specify both %s and %s options")
f_sanity_detail=$(gettext  "Missing %s at %s")
sanity_ok=$(gettext     "  Sanity Check: Passed.  Looks like a Solaris system.")
sanity_fail=$(gettext   "  Sanity Check: FAILED (see log for details).")
sanity_fail_vers=$(gettext  "  Sanity Check: the Solaris image (release %s) is not an OpenSolaris image and cannot be installed in this type of branded zone.")
p2ving=$(gettext        "      Updating: Converting image to non-global.")
p2v_done=$(gettext      "        Result: Conversion complete.")
p2v_fail=$(gettext      "        Result: Conversion failed")
install_fail=$(gettext  "        Result: *** Installation FAILED ***")
f_zfs_in_root=$(gettext "Installing a zone inside of the root pool's 'ROOT' dataset is unsupported.")
f_no_gzbe=$(gettext "unable to determine global zone boot environment.")
f_multiple_ds=$(gettext "multiple active datasets.")
f_no_active_ds=$(gettext "no active dataset.")
f_zfs_unmount=$(gettext "Unable to unmount the zone's root ZFS dataset (%s).\nIs there a global zone process inside the zone root?\nThe current zone boot environment will remain mounted.\n")
f_zfs_mount=$(gettext "Unable to mount the zone's ZFS dataset.")

f_sysrepo_fail=$(gettext "Unable to enable svc:/application/pkg/system-repository, please enable the service manually.")
f_zones_proxyd_fail=$(gettext "Unable to enable svc:/application/pkg/zones-proxyd, please enable the service manually.")
f_set_sysrepo_prop_fail=$(gettext "Unable to set the use-system-repo property.")
# Used by install and attach during argument processing
f_arg_not_dir=$(gettext "Argument %s is not a directory")
f_arg_not_file=$(gettext "Argument %s is not a regular file")
f_arg_not_file_or_dir=$(gettext "Argument %s is not a file or directory")
f_scxml=$(gettext "Configuration profile %s must have an .xml suffix")
f_gz_image=$(gettext "Cannot attach global zone image")
f_image_arch_mismatch=$(gettext "Current architecture (%s) doesn't match zone image architecture (%s).")

m_brnd_usage=$(gettext "brand-specific usage: ")

v_reconfig=$(gettext "Performing zone system configuration")
e_reconfig=$(gettext "System configuration failed")
v_mounting=$(gettext "Mounting the zone")
e_badmount=$(gettext "Zone mount failed")
v_unmount=$(gettext "Unmounting zone")
e_badunmount=$(gettext "Zone unmount failed")
e_exitfail=$(gettext "Postprocessing failed.")
v_update_format=$(gettext "Updating image format")
e_update_format=$(gettext "Updating image format failed")

m_complete_seconds=$(gettext    "        Done: Installation completed in %s seconds.")
m_postnote=$(gettext    "  Next Steps: Boot the zone, then log into the zone console (zlogin -C)")
m_postnote2=$(gettext "              to complete the configuration process.")

m_zbe_discover_failed=$(gettext "Unable to determine which boot environment to activate.  Candidates are:\n")
m_zbe_discover_header=$(gettext "Zone Boot Environment Active Global Zone Boot Environment\n--------------------- ------ ------------------------------------")
missing_gzbe=$(gettext "Missing Global Zone Boot Environment")
m_again_with_dash_z=$(gettext "Use the following command to attach a specific zone boot environment:\n%s")
e_no_be_0=$(gettext "Zone has no boot environments")
e_no_be_1=$(gettext "Zone has no boot environment with any of these names: %s")
f_sanity_variant=$(gettext "  Sanity Check: FAILED, couldn't determine %s from image.")
m_cache=$(gettext        "                     Cache: Using %s.")
f_update_required=$(gettext "Attach failed. This zone must be attached with the -u or -U option.")
f_uname_p=$(gettext "Unable to determine current architecture.")
f_list=$(gettext "Could not list packages in image: %s")
f_osnet=$(gettext "Could not find the osnet-incorporation package installed in image: %s")
f_update=$(gettext "Could not update attaching zone")
f_system_volatile=$(gettext "Could not create /system/volatile in the zone.")
m_active_zbe=$(gettext   "      Zone BE root dataset: %s")
m_prep_image=$(gettext   "  Updating non-global zone: Preparing packages.")
m_image_link=$(gettext   "  Updating non-global zone: Linking to image %s.")
m_image_audit=$(gettext  "  Updating non-global zone: Auditing packages.")
m_image_sync=$(gettext   "  Updating non-global zone: Syncing packages.")
m_image_sync1=$(gettext  "  Updating non-global zone: Syncing packages (pass 1 of 2).")
m_image_sync2=$(gettext  "  Updating non-global zone: Syncing packages (pass 2 of 2).")
m_image_update=$(gettext "  Updating non-global zone: Updating packages.")
m_sync_done=$(gettext    "  Updating non-global zone: Zone updated.")
m_complete=$(gettext     "                    Result: Attach Succeeded.")

is_brand_labeled() {
	if [[ -z $ALTROOT ]]; then
		AR_OPTIONS=""
	else
		AR_OPTIONS="-R $ALTROOT"
	fi
	brand=$(/usr/sbin/zoneadm $AR_OPTIONS -z $ZONENAME \
		list -p | awk -F: '{print $6}')
	[[ $brand == "labeled" ]] && return 0
	return 1
}

function sanity_check {
	typeset dir="$1"
	shift
	res=0

	#
	# Check for some required directories.
	#
	checks="etc etc/svc var var/svc"
	for x in $checks; do
		if [[ ! -e $dir/$x ]]; then
			log "$f_sanity_detail" "$x" "$dir"
			res=1
		fi
	done
	if (( $res != 0 )); then
		log "$sanity_fail"
		fatal "$install_fail" "$ZONENAME"
	fi

	# Check for existence of pkg command.
	if [[ ! -x $dir/usr/bin/pkg ]]; then
		log "$f_sanity_detail" "usr/bin/pkg" "$dir"
		log "$sanity_fail"
		fatal "$install_fail" "$ZONENAME"
	fi

	#
	# XXX There should be a better way to do this.
	# Check image release.  We only work on the same minor release as the
	# system is running.  The INST_RELEASE file doesn't exist with IPS on
	# OpenSolaris, so its presence means we have an earlier Solaris
	# (i.e. non-OpenSolaris) image.
	#
	if [[ -f "$dir/var/sadm/system/admin/INST_RELEASE" ]]; then
		image_vers=$(nawk -F= '{if ($1 == "VERSION") print $2}' \
		    $dir/var/sadm/system/admin/INST_RELEASE)
		vlog "$sanity_fail_vers" "$image_vers"
		fatal "$install_fail" "$ZONENAME"
	fi

	vlog "$sanity_ok"
}

function get_current_gzbe {
	#
	# If there is no alternate root (normal case) then set the
	# global zone boot environment by finding the boot environment
	# that is active now.
	# If a zone exists in a boot environment mounted on an alternate root,
	# then find the boot environment where the alternate root is mounted.
	#
	CURRENT_GZBE=$(beadm list -H | nawk -v alt=$ALTROOT -F\; '{
		if (length(alt) == 0) {
		    # Field 3 is the BE status.  "N" is the active BE.
		    if ($3 !~ "N")
			next
		} else {
		    # Field 4 is the BE mountpoint.
		    if ($4 != alt)
		next
		}
		# Field 2 is the BE UUID
		print $2
	    }')
	[[ -z "$CURRENT_GZBE" ]] && fatal "$f_no_gzbe"
	return 0
}

#
# get_active_be zone
#
# Finds the active boot environment for the given zone.
#
# Arguments:
#
#  zone		zone structure initialized with init_zone
#
# Globals:
#
#  CURRENT_GZBE	Current global zone boot environment.  If not already set,
#		it will be set.
#
# Returns:
#
#  0 on success, else 1.
#
function get_active_be {
	typeset -n zone=$1
	typeset active_ds=
	typeset tab=$(printf "\t")

	if [[ -z "$CURRENT_GZBE" ]]; then
		get_current_gzbe
	fi

	typeset name parent active
	zfs list -H -r -d 1 -t filesystem -o name,$PROP_PARENT,$PROP_ACTIVE \
	    "${zone.ROOT_ds}" | while IFS=$'\t' read name parent active ; do
		[[ $parent == "$CURRENT_GZBE" ]] || continue
		[[ $active == on ]] || continue
		vlog "Found active dataset %s" "$name"
		if [[ -n "$active_ds" ]]; then
			error "$f_multiple_ds"
			return 1
		fi
		active_ds=$name
	done
	if [[ -z $active_ds ]]; then
		error "$f_no_active_ds"
		return 1
	fi

	zone.active_ds=$active_ds
}

#
# claim_zbe zone zbe
#
# If the zbe belongs to the existing gzbe or the zbe was extracted from an
# archive and has not yet been attached, set it as the active zbe.  Otherwise,
# clone it and set the clone to the active zbe.
#
# Globals:
#
#  EXIT_CODE	On success, set as described in clone_zbe.
#
# Returns 0 on success, !0 on failure.
#
function claim_zbe {
	typeset -n zone=$1
	typeset zbe=$2
	typeset dss

	init_dataset dss "${zone.ROOT_ds}/$zbe" || return 1

	if [[ -z "$CURRENT_GZBE" ]]; then
		get_current_gzbe
	fi

	if [[ ${dss.props[$PROP_CANDIDATE].value} == "$CURRENT_GZBE" ||
	    ${dss.props[$PROP_PARENT].value} == "$CURRENT_GZBE" ]]; then
		mount_active_be -b "$zbe" zone
		return $?
	fi

	# Sets EXIT_CODE.
	clone_zbe zone "$zbe"
	return $?
}

#
# clone_zbe [-u] zone zbe
#
# Clones a zbe within a zone and sets the new ZBE as the active BE for this
# zone.
#
# Options and arguments:
#
#   zone 	zone structure initialized with init_zone
#   zbe		Name of the zone boot environment to clone.
#
# Globals:
#
#   EXIT_CODE	On success, set to ZONE_SUBPROC_UNAVAILABLE.
#		On failure, set as described in clone_zone_rpool.
#
# Returns:
#
#   0 Success, and members of the zone structure have been updated.
#		zone.active_ds		Updated with the dataset that is
#					the root of the zbe.
#		zone.zbe_cloned_from	Set to the name of the zbe passed in
#   1 Failure.  Error message has been logged.
#
function clone_zbe {
	typeset -n zone=$1
	typeset zbe=$2
	typeset -i i
	typeset snapname
	typeset dsn=${zone.ROOT_ds}/$zbe

	[[ -z $zbe ]] && fail_internal "zbe not specified"
	/usr/sbin/zfs list -o name "$dsn" >/dev/null 2>&1 || \
	    fail_internal "Dataset '%s' does not exist" "$dsn"

	typeset now=$(date +%Y-%m-%d-%H:%M:%S)
	snapname=$now
	for (( i=0; i < 100; i++ )); do
		/usr/sbin/zfs snapshot -r "$dsn@$snapname" >/dev/null 2>&1 && \
		    break
		snapname=$(printf "%s-%02d" "$now" $i)
	done
	if (( i == 100 )); then
		error "$f_zfs_snapshot_of" "$dsn"
		return 1
	fi

	# Clone, activate, and mount ZBE
	zone.active_ds=${zone.ROOT_ds}/$zbe
	# Sets EXIT_CODE.  "zone" appears twice as it is the source and target.
	clone_zone_rpool zone zone "$snapname" || return 1
	zone.zbe_cloned_from=$zbe
	EXIT_CODE=$ZONE_SUBPROC_UNAVAILABLE
	return 0
}

#
# discover_active_be zone
#
# Looks for the ZBE that is best suited to be the active ZBE.
#
# The caller may optionally constrain the list of ZBEs that is considered for
# activation by populating the zone.allowed_bes associative array.  In such a
# case,  If zone.allowed_bes is a non-empty associative array, only BEs in that
# array are considered.
#
# After selecting which ZBE is the best candidate for activation, care will
# be taken not to "steal" the ZBE from another global zone.  If the chosen
# zbe has $PROP_PARENT matching the UUID of an extant global zone BE, it is
# the chosen ZBE is cloned and this new clone is the ZBE that is selected for
# activation.
#
# Note, however, that a ZBE that appears in zone.allowed_bes is
# never cloned.  It is assumed that zone.allowed_bes contains a set of ZBEs
# that was received from an archive and any existing values in $PROP_PARENT
# on these ZBEs are stale.
#
# Arguments
#
#  zone		A zone structure initialized with init_zone.
#
# Globals
#
#  EXIT_CODE	ZONE_SUBPROC_UNAVAILABLE	ZBE cloning was attempted
#						and succeeded.
#		ZONE_SUBPROC_FATAL		ZBE cloning was attempted,
#						failed, and cleanup failed.
#		ZONE_SUBPROC_TRYAGAIN		ZBE selection required.  A
#						list of available ZBEs was
#						displayed.
#
# Returns:
#
#   0	Success.  The discovered active_be has been activated (see
#	set_active_be() for details) and has been mounted on the zone root.
#   1	Active dataset could not be found and an error message has been
#   	printed.
#
function discover_active_be {
	typeset -n zone=$1
	shift
	typeset -A uuid2gzbe
	typeset -i needs_selection=0

	#
	# Load an associative array of global zone BEs.  Store current uuid
	# of GZBE in $active_gzbe.
	#
	# uuid2gzbe[<gzbe uuid>]=<gzbe name>
	#
	typeset be uuid active junk
	typeset active_gzbe
	beadm list -H | while IFS=\; read be uuid active junk ; do
		uuid2gzbe[$uuid]=$be
		[[ $active == *N* ]] && active_gzbe=$uuid
	done
	if [[ -z $active_gzbe ]]; then
		error "%s: unable to get global zone BE UUID" "${zone.name}"
		return 1
	fi

	#
	# Load an associative array of non-global zone BEs and arrays of
	# likely candidates.
	#
	# ngzbe[<ngzbe name>].parent=<gzbe uuid>
	# ngzbe[<ngzbe name>].active=<on|off|->
	# ngzbe[<ngzbe name>].mountpoint=</|zoneroot>
	#
	typeset name mountpoint parent active candidate
	typeset -A ngzbe
	typeset -a activezbe	# NGZ BEs that are active
	typeset -a this_gz	# NGZ BEs that match this GZ BE
	typeset -a this_gz_active	# match GZ BE and is active
	zfs list -H -r -d 1 -t filesystem -o \
	    name,mountpoint,$PROP_PARENT,$PROP_ACTIVE,$PROP_CANDIDATE \
	    "${zone.ROOT_ds}" | \
	    while IFS=$'\t' read name mountpoint parent active candidate
	    do

		# skip the non-BE top-level dataset
		[[ $name == "${zone.ROOT_ds}" ]] && continue

		typeset curbe=$(basename "$name")

		# skip BEs that are not in the allowed_bes list
		if (( ${#zone.allowed_bes[@]} != 0 )) &&
		    [[ -z ${zone.allowed_bes[$curbe]} ]]; then
			vlog "Ignoring dataset %s: %s not in allowed_bes" \
			    "$name" "$(basename "$name")"
			continue
		fi

	    	if [[ -z ${uuid2gzbe[$parent]} ]]; then
	    		uuid2gzbe[$parent]=$missing_gzbe
		fi
		ngzbe[$curbe].parent=$parent
		ngzbe[$curbe].active=$active
		ngzbe[$curbe].mountpoint=$mountpoint
		ngzbe[$curbe].candidate=$candidate

		# Update some arrays used in decision process.
		if [[ $parent == "$active_gzbe" ]]; then
		       	a_push this_gz "$curbe"
			[[ $active == on ]] && a_push this_gz_active "$curbe"
		fi
		[[ $active == on ]] && a_push activezbe "$curbe"
	done
	#
	# If there are no BEs, return an error
	#
	if (( ${#ngzbe[@]} == 0 )); then
		if (( ${#zone.allowed_bes[@]} == 0 )); then
			error "$e_no_be_0"
		else
			error "$e_no_be_1" "${!zone.allowed_bes[*]}"
		fi
		return 1
	fi

	# If there was only one ZBE active for this GZ, activate it.
	if (( ${#this_gz_active[@]} == 1 )); then
		mount_active_be -c -b "${this_gz_active[0]}" zone
		return $?
	fi

	# If there was only one ZBE associated with this GZ, activate it.
	if (( ${#this_gz[@]} == 1 )); then
		mount_active_be -c -b "${this_gz[0]}" zone
		return $?
	fi

	# If there was only one ZBE that was active, clone and/or activate it.
	if (( ${#activezbe[@]} == 1 )); then
		typeset zbe="${activezbe[0]}"
		if [[ ${#zone.allowed_bes[@]} != 0 ]]; then
			mount_active_be -c -b "$zbe" zone
			return $?
		fi
		# If the zbe is not associated with any gzbe, do not clone it.
		if [[ ${uuid2gzbe[${ngzbe[$zbe].parent}]} == "$missing_gzbe" ]]
		then
			mount_active_be -c -b "$zbe" zone
			return $?
		fi
		clone_zbe zone "$zbe"
		return $?
	fi

	# If there was only one ZBE, clone and/or activate it
	if (( ${#ngzbe[@]} == 1 )); then
		#
		# We really want the name of index 0, but a subscript of 0
		# is not supported.  Since we know that there is only one
		# item in the associative array, the name of all the items
		# is equivalent to the name of the first item.
		#
		typeset zbe="${!ngzbe[@]}"
		if [[ ${#zone.allowed_bes[@]} ]]; then
			mount_active_be -c -b "$zbe" zone
			return $?
		fi
		# Sets EXIT_CODE.
		clone_zbe zone "$zbe"
		return $?
	fi

	log "$m_zbe_discover_failed"
	log "$m_zbe_discover_header"
	typeset zbe
	for zbe in "${!ngzbe[@]}" ; do
		typeset uuid=${ngzbe[$zbe].parent}
		typeset bename=${uuid2gzbe[$uuid]}
		if [[ $bename == $missing_gzbe ]]; then
			typeset cuuid=${ngzbe[$zbe].candidate}
			if [[ $cuuid != $missing_gzbe && \
			    -n ${uuid2gzbe[$cuuid]} ]]; then
				bename="Extracted for ${uuid2gzbe[$cuuid]}"
			else
		       		bename=$uuid
			fi
		fi
		log "%-21s %-6s %s" "$zbe" "${ngzbe[$zbe].active}" "$bename"
	done

	log "%s" ""

	#
	# Install and attach need different messages.  m_usage_dash_z should
	# be defined in the brand's install and attach scripts.
	#
	if (( ${#ATTACH_Z_COMMAND[@]} != 0 )); then
		EXIT_CODE=$ZONE_SUBPROC_TRYAGAIN
	       	log "$m_again_with_dash_z" "${ATTACH_Z_COMMAND[*]}"
	fi

	return 1
}

#
# set_active_be zone bootenv
#
# Sets the active boot environment for the zone.  This includes updating the
# zone structure and setting the required properties ($PROP_PARENT,
# $PROP_ACTIVE) on the top-level BE datasets.
#
function set_active_be {
	typeset -n zone="$1"
	typeset be=$2
	typeset name canmount parent active candidate

	[[ -z $be ]] && fail_internal "zbe not specified"

	if [[ -z "$CURRENT_GZBE" ]]; then
	       	get_current_gzbe
	fi

	#
	# Turn off the active property on BE's with the same GZBE and ensure
	# that there aren't any BE datasets that will mount automatically.
	#
	zfs list -H -r -d 1 -t filesystem -o \
	    name,canmount,$PROP_PARENT,$PROP_ACTIVE,$PROP_CANDIDATE \
	    ${zone.ROOT_ds} | \
	    while IFS=$'\t' read name canmount parent active candidate ; do
		# skip the ROOT dataset
		[[ $name ==  "${zone.ROOT_ds}" ]] && continue
		# The root of each BE should only be mounted explicitly.
		if [[ $canmount != noauto ]]; then
			zfs set canmount=noauto "$name" || \
			    fail_internal "$e_zfs_set" canmount "$name"
		fi
		#
		# If this was extracted from an archive within this GZ,
		# finish the association process.  In the unlikely event
		# that these property updates fail, manual cleanup may
		# be required, but it should not prevent the attach.
		#
		if [[ $candidate == "$CURRENT_GZBE" ]]; then
			zfs inherit "$PROP_CANDIDATE" "$name" ||
			    log "$e_zfs_inherit" "$PROP_CANDIDATE" "$name"
			#
			# Setting the parent to this gzbe makes it possible
			# for beadm to clean up the zbes within the zone
			# once one of the candidate zbe's is attached.
			#
			if [[ $parent != "$CURRENT_GZBE" ]]; then
				zfs set "$PROP_PARENT=$CURRENT_GZBE" "$name" ||
				    log "$e_zfs_set" "$PROP_PARENT" "$name"
				parent=$CURRENT_GZBE
			fi
		fi

		# Deactivate BEs for this GZ that are not being set to active.
		[[ $parent == "$CURRENT_GZBE" ]] || continue
		[[ $active == on ]] || continue
		[[ $name ==  "${zone.ROOT_ds}/$be" ]] && continue
		vlog "Deactivating active dataset %s" "$name"
		zfs set $PROP_ACTIVE=off "$name" || return 1
	done

	zone.active_ds="${zone.ROOT_ds}/$be"
	zfs set "$PROP_PARENT=$CURRENT_GZBE" ${zone.active_ds} \
	    || return 1
	zfs set "$PROP_ACTIVE=on" ${zone.active_ds} || return 1
	zfs set "$PROP_BE_HANDLE=on" "${zone.rpool_ds}" || return 1

	typeset origin
	zfs list -H -o name,origin "${zone.active_ds}" | while read name origin
	do
		if [[ $origin == "${zone.ROOT_ds}"/* ]]; then
			vlog "Promoting active dataset '%s'" "${zone.active_ds}"
			zfs promote "${zone.active_ds}"
		fi
	done

	return 0
}

#
# Run system configuration inside a zone.
#
function reconfigure_zone {
	typeset sc_config=$1
	vlog "$v_reconfig"

	vlog "$v_mounting"
	ZONE_IS_MOUNTED=1
	zoneadm -z $ZONENAME mount -f || fatal "$e_badmount"

	if [[ -n $sc_config ]]; then
		sc_config_base=$(basename "$sc_config")
		# Remove in case $sc_config_base is a directory
		safe_dir "/system"
		safe_dir "/system/volatile"
		rm -rf "$ZONEPATH/lu/system/volatile/$sc_config_base"
		safe_copy_rec $sc_config \
		    "$ZONEPATH/lu/system/volatile/$sc_config_base"
		zlogin -S $ZONENAME "_UNCONFIG_ALT_ROOT=/a \
		    /usr/sbin/sysconfig configure -g system \
		    -c /system/volatile/$sc_config_base --destructive" \
		    </dev/null >/dev/null 2>&1
	else
		zlogin -S $ZONENAME "_UNCONFIG_ALT_ROOT=/a \
		    /usr/sbin/sysconfig configure -g system --destructive" \
		    </dev/null >/dev/null 2>&1
	fi

	if (( $? != 0 )); then
		error "$e_reconfig"
		failed=1
	fi

	vlog "$v_unmount"
	zoneadm -z $ZONENAME unmount || fatal "$e_badunmount"
	ZONE_IS_MOUNTED=0

	[[ -n $failed ]] && fatal "$e_exitfail"
}

#
# Emits to stdout the fmri for the supplied package,
# stripped of publisher name and other junk.
#
function get_pkg_fmri {
	typeset pname=$1
	typeset pkg_fmri=
	typeset info_out=

	info_out=$(LC_ALL=C ${PKG:-pkg} info pkg:/$pname 2>/dev/null)
	if (( $? != 0 )); then
		return 1
	fi
	pkg_fmri=$(echo $info_out | grep FMRI | cut -d'@' -f 2)
	echo "$pname@$pkg_fmri"
	return 0
}

#
# Emits to stdout the entire incorporation for this image,
# stripped of publisher name and other junk.
#
function get_entire_incorp {
	get_pkg_fmri entire
	return $?
}

function get_osnet_incorp {
	get_pkg_fmri consolidation/osnet/osnet-incorporation
	return $?
}

#
# Handle pkg exit code.  Exit 0 means Command succeeded, exit 4 means
# No changes were made - nothing to do.  Any other exit code is an error.
#
# Arguments:
#
#   msg		Error message passed as argument to error function
#   errfn	Function to call if an error is detected.  If not specified,
#		fail_fatal is used.
#
function pkg_err_check {
	typeset res=$?
	typeset msg=$1
	typeset errfn=${2:-fail_fatal}
	(( $res != 0 && $res != 4 )) && "$errfn" "$msg"
}

#
# Enable the services needed to perform packaging operations inside a zone.
#
function enable_zones_services {
	services_required="$1"

	/usr/sbin/svcadm enable -st svc:/application/pkg/system-repository
	if (( $? != 0 )) && [[ -n "$services_required" ]]; then
		error "$f_sysrepo_fail"
		return 1
	fi
	/usr/sbin/svcadm enable -st svc:/application/pkg/zones-proxyd
	if (( $? != 0 )) && [[ -n "$services_required" ]]; then
		error "$f_zones_proxyd_fail"
		return 1
	fi
	return 0
}

#
# tag_candidate_zbes ROOTdsn [be_array_name [curgz_assoc_array_name]]
#
# Tags each dataset that is a child of ROOTdsn with
# $PROP_CANDIDATE=$CURRENT_GZBE.
#
# Arguments:
#   ROOTdsn		The name of a dataset that contains zbes.
#   be_array_name	If specified, this variable will contain an array
#			of candidate zbes on return.
#   curgz_assoc_array_name If specified and any zbes have $PROP_PARENT that
#			matches $CURRENT_GZBE, curgz_assoc_array_name will
#			contain that list.  Otherwise, curgz_assoc_array_name
#			will be updated to reflect all of the zbes found.  Note
#			that curgz_assoc_array_name is an associative (not
#			indexed) array with keys that match the zbe name.  The
#			value assigned to each key is not significant.
#
# Returns 0 if there is at least one zbe found
# Returns 1 if there are no zbes or there is a failure updating properties.
#
function tag_candidate_zbes {
	typeset ROOTdsn=$1
	if [[ -n $2 ]]; then
		typeset -n bes=$2
	fi
	typeset -a bes
	if [[ -n $3 ]]; then
		typeset -n curgzbes=$3
	fi
	typeset -A curgzbes
	typeset dsn parent

	if [[ -z "$CURRENT_GZBE" ]]; then
		get_current_gzbe
	fi

	/usr/sbin/zfs list -H -o name -r -d 1 -t filesystem "$ROOTdsn" \
	    2>/dev/null | while read dsn ; do
		[[ $dsn == "$ROOTdsn" ]] && continue
		a_push bes "$(basename "$dsn")"

		zfs set "$PROP_CANDIDATE=$CURRENT_GZBE" "$dsn" || return 1

		# See if the zbe is already associated with the GZBE
		parent=$(zfs get "$PROP_PARENT" "$dsn")
		if [[ $parent == "$CURRENT_GZBE" ]]; then
			curgzbes[$(basename "$dsn")]=1
		fi
	done
	if (( ${#bes[@]} == 0 )); then
		error "$e_no_active_be"
		return 1
	fi

	#
	# If there were no zbes that already had a parent of $CURRENT_GZBE,
	# mark all of the found zbes as being allowed.
	#
	if (( ${#curgzbes[@]} == 0 )); then
		typeset be
		for be in "${bes[@]}"; do
			curgzbes[$be]=1
		done
	fi

	return 0
}

#
# attach_image zone allow_update
#
# Arguments:
#
#   zone		A zone reference, initialized by init_zone.  A BE
#			must be fully mounted at ${zone.root}.  The BE may
#			contain a GZ or NGZ pkg(5) image.
#   allow_update	One of "none", "min", or "all".
#			none:	No updates to the zone image are allowed.
#			min:	The mininal updates required to attach the
#				zone are allowed.  This includes updating
#				the pkg(5) image format and satisfying
#				parent dependencies.  Corresponds to the
#				default action of p2v (install <-a|-d>)
#				and the historical -u option to attach.
#			all:	Like min, but updates all packages to the
#				latest version that are compatible with the
#				global zone image.
#
# Side effects on global variables:
#
# PKG		If not set on entry, set to pkg
# GZ_IMAGE	If not set on entry, set to /
# PKG_CACHEROOT If not set on entry and a pkg(5) cache exists at
#		/var/pkg/publisher, it is set to PKG_CACHEROOT and exported
#		to the environment.
# OPT_V		If set, pkg(1) commands will be verbose.
# EXIT_CODE	Not changed, but must be set.
#
# Returns:
#  If it returns, all went well - the zone is attached.  Otherwise, it fails.
#
function attach_image {
	typeset -n zone=$1
	typeset allow_update=$2
	typeset variantname=variant.opensolaris.zone
	typeset -a variant
	typeset savestate
	typeset variant_changed=false
	typeset verbose
	PKG=${PKG:-pkg}
	GZ_IMAGE=${GZ_IMAGE:-/}

	[[ -n $OPT_V ]] && verbose=-v

	# Sanity check arguments
	(( $# == 2 )) ||
	    fail_internal "expected 2 args, got %d (%s)" $# "$*"
	[[ ! -d ${zone.root} ]] &&
	    fail_internal "zone root '%s' does not exist" "${zone.root}"
	[[ $allow_update != @(none|min|all) ]] &&
	    fail_internal "invalid value '%s' for allow_update" "$allow_update"

	# EXIT_CODE must be set for proper error handling
	[[ -z $EXIT_CODE ]] && fail_internal "EXIT_CODE is not set"

	# get the current architecture.
	typeset arch
	arch=$(uname -p) || fail "$f_uname_p"

	# If there is a cache, use it.
	if [[ -f /var/pkg/pkg5.image && -d /var/pkg/publisher ]]; then
		# respect PKG_CACHEROOT if the caller has it set.
		[[ -z "$PKG_CACHEROOT" ]] &&
		    export PKG_CACHEROOT=/var/pkg/publisher
		log "$m_cache" "$PKG_CACHEROOT"
	fi

	#
	# pkg update-format doesn't allow a dry run or provide any other way to
	# see if an update is needed.
	#
	if [[ $allow_update != none ]]; then
		log "$v_update_format"
		$PKG -R "${zone.root}" update-format ||
		    pkg_err_check "$e_update_format" fatal
	fi

	# Set the use-system-repo property.
	$PKG -R "${zone.root}" set-property use-system-repo true ||
	    fatal "\n$f_set_sysrepo_prop_fail"

	#
	# Update that catalogs once, subsequent packaging operations will use
	# --no-refresh to avoid unnecessary catalog checks and updates.  If
	# image updates are not allowed, there's no need to refresh the
	# catalogs.
	#
	if [[ $allow_update != none ]]; then
		$PKG -R "${zone.root}" refresh --full ||
		    pkg_err_check "$e_update_format"
	fi

	# prevent attach of a image with a different architecture
	typeset ngz_arch
	ngz_arch=$($PKG -R "${zone.root}" variant -H variant.arch) ||
	(( $? == 0 )) || fatal "$f_sanity_variant" variant.arch
	ngz_arch=$(set -- $ngz_arch; echo $2)
	[[ "$ngz_arch" != "$arch" ]] &&
		fatal "$f_image_arch_mismatch" "$arch" "$ngz_arch"

	#
	# Attach the zone to the global zone as a linked image.  This
	# writes linked image metadata into the non-global zone which
	# will constrain subsequent packaging operations (but only after
	# the zone variant is set to nonglobal.) The attach operation is
	# performed on the global zone image (and the pkg command will
	# subsequently recurse into and modify the zone image).
	#
	typeset -a pkg_attach_args cmd
	set -A pkg_attach_args -- \
	    attach-linked --no-refresh --linked-md-only --allow-relink \
	    -f $verbose
	log "$m_image_link" "$GZ_IMAGE"
	set -A cmd $PKG -R "${GZ_IMAGE}" "${pkg_attach_args[@]}" \
	    -c "zone:${zone.name}" "${zone.root}"
	vlog "Running '%s'" "${cmd[*]}"
	"${cmd[@]}" || pkg_err_check "$f_update" fatal

	# get a list of fmris installed in the ngz image.
	typeset tmpfile1 tmpfile2
	tmpfile1=$(mktemp -t zoneadm_image_attach.$$.XXXXXX) || \
	    fatal "$e_tmpfile"
	tmpfile2=$(mktemp -t zoneadm_image_attach.$$.XXXXXX) || \
	    fatal "$e_tmpfile"
	set -A cmd $PKG -R "${zone.root}" list --no-refresh -Hv
	vlog "Running '%s'" "${cmd[*]}"
	"${cmd[@]}" > $tmpfile2 || fatal "$f_list" "${zone.root}"
	nawk '{print $1}' $tmpfile2 > $tmpfile1 || fatal "$f_list"
	rm -f "$tmpfile2"

	#
	# Lookup the 'entire' incorporation in the global zone.  We
	# check for this because if the user has removed it then we'll
	# want to remove it from the zone during attach.  The reason is
	# that we're unlikely to be able to attach a highly constrained
	# zone (one that has entire installed) to a loosely constrained
	# global zone (one that doesn't have entire installed).
	#
	typeset gz_entire_pkgver=$(PKG_IMAGE=$GZ_IMAGE; get_entire_incorp)

	#
	# Lookup the 'osnet-incorporation' package in the global zone.
	# We need this to do a manual sync check (and possible manual
	# sync) of the zone.  (and by manual we mean a sync that does
	# not utilize the pkg linked image framework.)
	#
	typeset gz_osnet_pkgver=$(PKG_IMAGE=$GZ_IMAGE; get_osnet_incorp)
	[[ -z "$gz_osnet_pkgver" ]] && fatal "$f_osnet" "$GZ_IMAGE"

	# Lookup for the 'entire' incorporation in the non-global zone.
	# Get the full package name and version.
	typeset ngz_entire_fmri=$(egrep '^pkg://[^]]*/entire@' $tmpfile1)
	typeset ngz_entire_pkgver=${ngz_entire_fmri#pkg://*/}

	# Lookup for the 'osnet-incorporation' package in the non-global zone.
	# Get the full package name and version.
	typeset ngz_osnet_fmri=$(egrep \
	    '^pkg://[^]]*/consolidation/osnet/osnet-incorporation@' $tmpfile1)
	typeset ngz_osnet_pkgver=${ngz_osnet_fmri#pkg://*/}
	[[ -z "$gz_osnet_pkgver" ]] && fatal "$f_osnet" "${zone.root}"

	# get a list of incorporation fmris installed in the ngz image.
	typeset ngz_incorporations=$(
	    egrep '^pkg://.*/consolidation/.*/.*-incorporation@' "$tmpfile1"
	)

	# we're done with the ngz list of packages
	rm -f "$tmpfile1"

	# check if the 'osnet-incorporation' package is in sync between
	# the gz and ngz.
	typeset manual_sync_required=1
	[[ "$gz_osnet_pkgver" == "$ngz_osnet_pkgver" ]] &&
		manual_sync_required=0

	if [[ $manual_sync_required != 0 && $allow_update == none ]]; then
		fail_tryagain "\n$f_update_required"
	elif [[ $manual_sync_required != 0 ]]; then
		#
		# before we do a manual sync, we need to make sure that
		# system/volatile exists.  old (s11 express / snv_151a)
		# images may not have this directory, which in turn will
		# cause rem_drv and update_drv to fail (since they want
		# to create lock files in that directory).
		#
		ZONEROOT=${zone.root} safe_opt_dir system
		ZONEROOT=${zone.root} safe_opt_dir system/volatile
		if [[ ! -d "${zone.root}/system/volatile" ]]; then
			mkdir -m 755 -p "${zone.root}/system/volatile" || \
			    fatal "$f_system_volatile"
		fi

		# prepare to try and manually sync the zone image
		typeset -a pkg_install_args
		set -A pkg_install_args -- \
		    install -I $verbose --no-refresh --accept

		#
		# if the gz doesn't have entire installed we need to
		# remove it from the ngz during out sync.
		#
		[[ -z "$gz_entire_pkgver" ]] &&
			a_push pkg_install_args --reject pkg:///entire

		#
		# to manually sync the image we must relax all the image
		# install holds.  we do this by specifying the names of
		# all the installed incorporations (excluding publisher
		# and version numbers).
		#
		for fmri in $ngz_incorporations; do
			# strip version and publisher from the fmri
			pub_pkg=${fmri%@*}
			pkg=${pub_pkg#pkg://*/}

			#
			# if entire is not installed then we're going to
			# try to sync the image by specifying a version
			# of the osnet-incorporation, but older versions
			# of the pkg client don't allow you to specify
			# overlapping pkg patterns on the cli, so don't
			# try to relax the osnet-incorporation if we're
			# going to explicitly sync it.
			#
			[[ -z "$gz_entire_pkgver" && $pkg == \
			    consolidation/osnet/osnet-incorporation ]] &&
				continue

			#
			# due to bugs in s11 fcs, it's possible that we
			# may have the ldoms-incorporation installed on
			# non-sparc machines (or nvidia-incorporation
			# on non-i386 machines).  if so we don't want to
			# specify these packages on the command line.
			# (since that would require them to be installed
			# and as part of this image update the solver
			# may want to remove them.)
			#
			[[ "$arch" != sparc && $pkg == \
			    consolidation/ldoms/ldoms-incorporation ]] &&
				continue
			[[ "$arch" != i386 && $pkg == \
			    consolidation/nvidia/nvidia-incorporation ]] &&
				continue

			# relax this incorporation
			a_push pkg_install_args pkg:///$pkg
		done

		if [[ -n "$gz_entire_pkgver" && -n "$ngz_entire_pkgver" ]]; then
			# entire is installed in the gz and ngz, so sync that.
			a_push pkg_install_args $gz_entire_pkgver
		else
			# we're in one of the following three cases:
			# 1) entire is installed in the gz but not in
			#    the ngz.
			# 2) entire is not installed in the gz but is in
			#    the ngz (it will be rejected from the ngz).
			# 3) entire is not installed in the gz or ngz
			# in all these cases we must sync osnet-incorporation
			a_push pkg_install_args $gz_osnet_pkgver
		fi

		# try to manually sync the image.
		set -A cmd $PKG -R "${zone.root}" "${pkg_install_args[@]}"
		if [[ "$allow_update" == all ]]; then
			log "$m_image_sync"
		elif [[ "$allow_update" == min ]]; then
			log "$m_image_sync1"
		else
			fail_internal "allow_update is '$allow_update'"
		fi
		vlog "Running '%s'" "${cmd[*]}"
		"${cmd[@]}" || pkg_err_check "$f_update" fatal
	fi

	#
	# If the image is a global zone and updates are allowed, do p2v.
	#
	set -A variant \
	    $(LC_ALL=C $PKG -R "${zone.root}" variant -H $variantname)
	(( $? == 0 )) || fatal "$f_sanity_variant" "$variantname"
	[[ ${variant[0]} != "${variantname}" ]] &&
	    fatal "$f_sanity_variant" "$variantname"
	case ${variant[1]} in
	global)
		# We can't change the variant if updates aren't allowed.
		[[ $allow_update == @(min|all) ]] || fatal "$f_gz_image"

		log "$p2ving"
		typeset -a p2vopts
		set -A p2vopts $verbose_mode "${zone.name}" "${zone.path}"
		vlog "running: p2v ${p2vopts[@]}"
		/usr/lib/brand/solaris/p2v "${p2vopts[@]}"
		p2v_exit=$?
		if (( $p2v_exit != 0 )); then
			# Pass the p2v exit code up to zoneadm
			EXIT_CODE=$p2v_exit
			log "$p2v_fail"
			fatal "\n$install_fail"
		fi
		vlog "$p2v_done"
		;;
	nonglobal)
		:
		;;
	*)	fail_internal "$variantname is '${variant[1]}'"
		;;
	esac

	# Assemble the arguments to sync the zone image
	typeset -a pkg_sync_args
	typeset log_msg
	typeset fail_msg=$f_update
	case $allow_update in
	none)
		#
		# $f_update_required advises the use of -u or -U.  It should
		# only be used with attach, as -u means something different
		# with install.  install will only have "min" or "all" as 
		# values for allow_update.
		#
		fail_msg=$f_update_required
		log_msg=$m_image_audit
		set -A pkg_sync_args -- sync-linked --no-pkg-updates
		;;
	min)
		if [[ $manual_sync_required != 0 ]]; then
			log_msg=$m_image_sync2
		else
			log_msg=$m_image_sync
		fi
		set -A pkg_sync_args -- sync-linked
		;;
	all)
		log_msg=$m_image_update
		set -A pkg_sync_args -- update -f
		;;
	*)
		fail_internal "Invalid allow_update value: $allow_update"
	esac
	a_push pkg_sync_args -I $verbose --no-refresh --accept
	[[ $allow_update != none && -z $gz_entire_pkgver ]] &&
	    a_push pkg_sync_args --reject pkg:///entire

	# Sync the zone image.
	log "$log_msg"
	set -A cmd $PKG -R "${zone.root}" "${pkg_sync_args[@]}"
	vlog "Running '%s'" "${cmd[*]}"
	"${cmd[@]}" || pkg_err_check "$fail_msg" fatal

	log "\n$m_sync_done"
	log "$m_complete"
}
