#! /bin/ksh

#
# 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) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
#


export PATH=/usr/bin:/usr/sbin

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

function get_zone_state {
	typeset -n z=$1
	typeset fields

	zoneadm -z ${z.name} list -p | IFS=: read -A fields
	if (( ${#fields[@]} < 6 )); then
		error "%s: unknown data returned from zoneadm list -p" \
		    "${zone.name}"
		return 1
	fi
	z.state=${fields[2]}
	z.brand=${fields[5]}

	return 0
}

#
# run cmd undo rollback
#
# Runs the command given in the cmd array.  If all goes well, it adds the
# command in the undo array to the rollback array (of arrays) and returns 0.
# If the command does not return 0, the commands that exist in the rollback
# array are executed in the reverse of the order that they were added.
#
# In all cases, the exit value from the command in cmd is returned.
#
function run {
	typeset -n cmd=$1
	typeset -n undo=$2
	typeset -n rollback=$3

	if (( dryrun )); then
		print " " ${cmd[@]}
		return 0
	fi
	(( verbose )) && log "Running <%s>" "${cmd[*]}"
	( eval "${cmd[@]}" )
	typeset -i rv=$?
	if (( rv == 0 )); then
		# Push the undo array onto the rollback stack
		typeset -i rbsize=${#rollback[@]}
		typeset -a rollback[rbsize]
		set -A rollback[$rbsize] -- "${undo[@]}"
		return 0
	fi

	error "Command <%s> failed with exit status %d.  Rolling back." \
	    "${cmd[*]}" $rv

	typeset -i i
	for (( i = ${#rollback[@]} - 1; i >= 0; i-- )); do
		log "Executing rollback command <%s>" "${rollback[i][*]}"
		( eval "${rollback[i][@]}" )
	done
	return $rv
}

#
# get_active_be zone
#
# Overrides shared get_active_be.  Is used instead of brand-specific
# get_active_be because they know about the new layout, not the old.
#
function get_active_be {
	typeset -n zone=$1
	typeset -A uuid2gzbe
	typeset -i needs_selection=0

	#
	# solaris10 only supports a single for those that need to be converted.
	#
	if [[ ${zone.brand} == "solaris10" ]]; then
		zone.active_ds="${zone.path.ds}/ROOT/zbe-0"
		return 0
	fi

	#
	# The rest is solaris brand or something that uses the solaris brand
	#

	#
	# Load an associative array of global zone BEs.  Store current uuid
	# of GZBE in $active_gzbe.
	#
	# Initially the gzbes associative array is one dimensional.  The
	# following loop (beginning with zfs list) will add a second
	# dimension.  The fully populated uiid2gzbe structure will look like:
	#
	# uuid2gzbe[<gzbe uuid>]=<gzbe name>
	# uuid2gzbe[<gzbe uuid>][ngzbe name]=<ngz active>
	#
	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

	#
	# Look for the best match of zone BEs
	#
	typeset name parent active tab=$(printf "\t")
	typeset prop_parent="org.opensolaris.libbe:parentbe"
	typeset prop_active="org.opensolaris.libbe:active"

	typeset -A ngzbe
	zfs list -H -r -d 1 -t filesystem -o name,$prop_parent,$prop_active \
	    "${zone.path.ds}/ROOT" | while IFS=$tab read name parent active ; do

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

	    	if [[ -z ${uuid2gzbe[$parent]} ]]; then
	    		uuid2gzbe[$parent]='Missing Global Zone Boot Env'
		fi
		ngzbe[$(basename $name)].parent=$parent
		ngzbe[$(basename $name)].active=$active

		[[ $parent == "$active_gzbe" ]] || continue
		[[ $active == on ]] || continue
		vlog "Found active dataset %s" "$name"
		if [[ -n "$active_ds" ]]; then
			error "$f_multiple_ds"
			error "%s: Multiple active datasets" "${zone.name}"
			(( needs_selection=1 ))
			continue
		fi
		active_ds=$name
	done
	if [[ -n $active_ds ]]; then
		zone.active_ds=$active_ds
		return 0
	fi

	#
	# Active dataset was not found by property.  See if something is mounted
	# on the zone root.
	#
	active_ds=$(zfs list -H -o name ${zone.root})
	if [[ $active_ds == "${zone.path.ds}"/ROOT/* ]]; then
		zone.active_ds=$active_ds
		return 0
	fi

	#
	# If no active boot environment was found, offer a table of available
	# boot environments
	#
	error "%s: No active dataset" "${zone.name}"

	print "\nThe following unconverted boot environments exist this zone.\n"
	print -- "Boot Environment  NGZ Active  Global Zone Boot Environment" \
	    "GZ Active"
	print -- "----------------- ----------- ----------------------------" \
	    "---------"
	typeset be gzbe gzactive
	for be in ${!ngzbe[@]} ; do
		gzbe="${uuid2gzbe[${ngzbe[$be].parent}]}"
		if [[ $gzbe == "$active_gzbe" ]]; then
			gzactive=yes
		else
			gzactive=no
		fi
		printf "%-17s %-11s %-28s %s\n" "$be" "${ngzbe[$be].active}" \
		    "$gzbe" "$gzactive"
	done


	print "\nUse:\n"
	print "\tdsconvert -b <bootenv> ...\n"
       	print "to specify the boot environment to activate during conversion.\n"

	return 1
}

function usage {
	print "Usage: dsconvert [-hnv] [-b <BE>] zone\n\n" \
	    "    -h\t\tShow this help message\n" \
	    "    -n\t\tDry run - make no changes\n" \
	    "    -v\t\tVerbose\n" \
	    "    -b <BE>\tActivate the specified zone boot environment\n"
}

#
# main
#


typeset -i dryrun=0
bootenv=
m_conversion_complete="%s: Conversion complete"
while getopts :b:hnv opt ; do
	case $opt in
		b)	bootenv=$OPTARG
			;;
		h)	usage
			exit 0
			;;
		n)	(( dryrun = 1 ))
			m_conversion_complete="%s: Dryrun complete"
			function nopfn { echo " " "$*"; }
			nop=nopfn
			;;
		v)	(( verbose = 1 )) ;;	# used in this script
		?)	usage 1>&2
			exit 1
			;;
	esac
done
shift $(( OPTIND - 1 ))
if (( $# != 1 )); then
	usage 1>&2
	exit 1
fi
zonename=$1

zoneadm -z "$zonename" list >/dev/null 2>&1 || {
	error "%s: %s: No such zone configured" "$(basename "$0")" "$zonename"
	exit 1
}

init_zone zone "$zonename"
typeset -a cmd undo rollback
typeset -i has_bes=0
typeset active_ds=

init_dataset old_ROOT_ds "${zone.path.ds}/ROOT" >/dev/null 2>&1 || {
	error "%s: zone has no old style ROOT dataset." "$zonename"
	exit 1
}

get_zone_state zone || exit 1

#
# This check is used for the brief window where people may be trying to use
# an ON build with a build of the solaris brand that is too old.  Since the
# sysboot script was introduced with the dataset layout change, it makes for
# an easy check.
#
if [[ ${zone.brand} != solaris10 && ! -f /usr/lib/brand/solaris/sysboot ]]
then
	error "%s: %s must be updated before converting\n\t%s branded zones" \
	    "${zone.name}" pkg:/system/zones/brand/solaris "${zone.brand}"
	exit 1
fi

case "${zone.state}" in
	installed) : ;;
	configured|unavailable)
		# Be sure that there's a dataset under the old ROOT
		typeset be_datasets
		set -A be_datasets $(zfs list -H -o name -d 1 -r \
		    "${zone.path.ds}/ROOT")
		if (( ${#be_datasets[@]} < 2 )); then
			log "%s: zone has no boot environments" "$zonename"
			(( hasbes=0 ))
		else
			get_ds_from_path "${zone.root}" active_ds || {
				log "%s: configured but no BE mounted at %s" \
				    "$zonename" "${zone.root}"
				(( hasbes=0 ))
			}
		fi
		;;
	*)	error "%s: Unable to convert zone in state %s" "${zonename}" \
		    "${zone.state}"
		exit 1
		;;
esac

if [[ -n $bootenv ]]; then
	if [[ ${zone.brand} == "solaris10" ]]; then
		error "%s: The -b option is not supported with the %s brand" \
		    "$zonename" "${zone.brand}"
		exit 1
	fi
	zone.active_ds="${zone.path.ds}/ROOT/$bootenv"
	if ! zfs list "${zone.active_ds}" >/dev/null 2>&1 ; then
		error "%s: Zone has no boot environment named %s" \
		    "$zonename" "$bootenv"
		exit 1
	fi
else
	get_active_be zone || exit 1
fi

#
# Look at zonecfg fs and dataset resources to see if anything under /export
# is added.  The message that comes out of zonecfg_has_export is not helpful
# in this particular case, so we ignore it.
#
typeset -i skip_export=0
zonecfg_has_export zone >/dev/null 2>&1 && (( skip_export = 1 ))

log "%s: Creating zone rpool dataset" "$zonename"
set -A cmd zfs create -o mountpoint=/rpool -o zoned=on "${zone.rpool_ds}"
set -A undo zfs destroy "${zone.rpool_ds}"
run cmd undo rollback || exit 1

if (( skip_export == 0 )); then
	log "%s: Creating zone export dataset" "$zonename"
	set -A cmd zfs create -o mountpoint=/export "${zone.rpool_ds}/export"
	set -A undo zfs destroy "${zone.rpool_ds}/export"
	run cmd undo rollback || exit 1

	log "%s: Creating zone export/home dataset" "$zonename"
	set -A cmd zfs create "${zone.rpool_ds}/export/home"
	set -A undo zfs destroy "${zone.rpool_ds}/export/home"
	run cmd undo rollback || exit 1
fi

set -A props -- -o mountpoint=legacy -o canmount=noauto
if [[ ${zone.brand} != solaris10 ]]; then
	a_push props -o "com.oracle.libbe:nbe_handle=on"
fi
log "%s: Creating zone ROOT dataset" "$zonename"
set -A cmd zfs create "${props[@]}" "${zone.ROOT_ds}"
set -A undo zfs destroy "${zone.ROOT_ds}"
run cmd undo rollback || exit 1

#
# Moving the dataset hierarchy is problematic when clones are involved.
# The easiest way to avoid the problem with clones is to use more of them.
#
log "%s: Cloning datasets from ROOT to rpool/ROOT" "$zonename"
set -A cmd zfs set -r canmount=off "${zone.path.ds}/ROOT"
set -A undo zfs set -r canmount=${old_ROOT_ds.props[canmount].value} \
    "${zone.path.ds}/ROOT"
run cmd undo rollback || exit 1

#
# Unconventional quoting of date format string is to silence SCCS keyword
# warnings.
#
snapname=$(basename $0)-$(TZ=UTC date "+%Y""%m%dT%H""%M""%SZ")
set -A cmd zfs snapshot -r "${zone.path.ds}/ROOT@$snapname"
set -A undo zfs destroy -rd "${zone.path.ds}/ROOT@$snapname"
run cmd undo rollback || exit 1

#
# Get a list of all the old datasets and their properties.  Once we
# have that, use that information to create clones.
#
typeset -a dslist
get_datasets -p ${zone.path.ds}/ROOT dslist || {
	error "%s: Unable to get list of datasets under %s" \
	    "${zone.name}" "${zone.path.ds}/ROOT"
	exit 1
}

#
# Go through the list and be sure they are all unmounted and won't become
# mounted.  This is needed because the clones that get created will need to use
# the mount points currently used by the originals.  Ignore the first item in
# the array because that is the ROOT dataset.
#
typeset -i i
for (( i = ${#dslist[@]} - 1; i > 1; i-- )); do

	# Before this conversion multiple datasets in the BE were broken.
	# Therefore, we don't deal with them here either.
	if [[ ${dslist[i].name} == ${zone.path.ds}/ROOT/*/* ]]; then
		# Force a rollback and exit
		set -A cmd error \
		    "%s: Unsupported non-root dataset %s in boot environment" \
		    "${zone.name}" "${dslist[i].name}" \; false
		set -A undo echo "this command will never run"
		run cmd undo rollback
		exit 1
	fi

	if [[ ${dslist[i].props[canmount].value} != no ]]; then
		set -A cmd zfs set canmount=off "${dslist[i].name}"
		set -A undo zfs set canmount=${dslist[i].props[canmount].value} \
	           "${dslist[i].name}"
		run cmd undo rollback || exit 1
	fi
	if [[ ${dslist[i].props[mounted].value} == yes ]]; then
		set -A cmd unmount "${dslist[i].name}"
		if [[ ${dslist[i].props[mountpoint].value} == legacy ]]; then
			# Create undo using existing mountpoint
			set -A undo mount -F zfs "${dslist[i].name}" \
			    "$(nawk -F '\t' -v "dev=${dslist[i].name}" \
			    '$1 == dev {print $2; exit}' /etc/mnttab)"
		else
			# This conversion process assumes no one uses temporary
			# mount points for zones before this conversion is
			# necessary.
			set -A undo zfs mount "${dslist[i].name}"
		fi
		run cmd undo rollback || exit 1
	fi
done

#
# The sources are all unmounted and along the way we verified that it is only
# root datasets.  Create clones.
#
for (( i = 1; i < ${#dslist[@]}; i++ )); do
       	set -A props -- -o mountpoint=/ -o canmount=noauto

	# preserve user properties
	for prop in "${!dslist[i].props[@]}" ; do
		# Skip non-user properties and inherited user properties
		[[ $prop == *:* ]] || continue
		[[ ${dslist[i].props[$prop].source} == @(local|received) ]] ||
		    continue

		a_push props -o "$prop=${dslist[i].props[$prop].value}"
	done

	src=${dslist[i].name}@$snapname
	new=${dslist[i].name/${zone.path.ds}/${zone.rpool_ds}}

	set -A cmd zfs clone "${props[@]}" "$src" "$new"
	set -a undo destroy_zone_dataset "$new"
	run cmd undo rollback || exit 1

	(( has_bes=1 ))
done

if (( has_bes )); then
	#
	# The layout is now correct, except for datasets that need to be
	# deleted.  Load the brand-specific common functions.  Note that
	# all of the non-solaris10 brands that this script support use
	# the solaris common.ksh.
	#
	if [[ ${zone.brand} == "solaris10" ]]; then
		. /usr/lib/brand/solaris10/common.ksh
	else
		. /usr/lib/brand/solaris/common.ksh
	fi

	# Clean up boot environment properties as needed
	(( dryrun )) || set_active_be zone "$(basename "${zone.active_ds}")"

	#
	# If we started this exercise with a zone that was attached, just run
	# the rough equivalent of the sysboot hook to mount it.  If it wasn't
	# attached, then we need to remount the active dataset at the zone
	# root.
	#
	log "%s: Remounting zone at %s" "$zonename" "${zone.root}"
	if [[ ${zone.state} == installed ]]; then
		set -A cmd mount_active_be -c zone
		set -A undo umount "${zone.root}"
		run cmd undo rollback || exit 1

		if [[ ${zone.brand} != solaris10 ]]; then
			log "\n%8s: zone %s needs to be updated with attach -u" \
			    NOTICE "$zonename"

			log "%8s: \"zoneadm -z %s detach\" to detach, then" \
			    Run "$zonename"
			log "%8s: \"zoneadm -z %s attach -u\" to attach.\n" \
			    Run "$zonename"
		fi
	elif [[ ${zone.state} == unavailable ]]; then
		set -A cmd mount_active_be -c zone
		set -A undo umount "${zone.root}"
		run cmd undo rollback || exit 1

		if [[ ${zone.brand} == solaris10 ]]; then
			log "\n%8s: zone %s needs to be attached." NOTICE \
			    "$zonename"
			log "%8s: \"zoneadm -z %s attach\" to attach.\n" \
			    Run "$zonename"
		else
			log "\n%8s: zone %s needs to be updated with attach -u" \
			    NOTICE "$zonename"
			log "%8s: \"zoneadm -z %s attach -u\" to attach.\n" \
			    Run "$zonename"
		fi

	else	# detached

		#
		# Before the dataset revision, each BE only supported a single
		# (root) dataset.  As such, we can use a rather simplistic
		# approach to mounting the active BE.
		#
		set -A cmd zfs set canmount=off "${zone.active_ds}"
		set -A undo zfs set canmount="$(/usr/sbin/zfs get -H \
		    -o value canmount "${zone.active_ds}")" "${zone.active_ds}"
		run cmd undo rollback || exit 1

		set -A cmd zfs set zoned=off "${zone.active_ds}"
		set -A undo zfs inherit zoned "${zone.active_ds}"
		run cmd undo rollback || exit 1

		set -A cmd zfs set mountpoint="${zone.root}" "${zone.active_ds}"
		set -A undo zfs set mountpoint=$(/usr/sbin/zfs get -H \
		    -o value mountpoint "${zone.active_ds}") \
		    "${zone.active_ds}"
		run cmd undo rollback || exit 1

		set -A cmd zfs set canmount=on "${zone.active_ds}"
		set -A undo zfs set canmount=off "${zone.active_ds}"
		run cmd undo rollback || exit 1

		set -A cmd zfs mount "${zone.active_ds}"
		set -A undo zfs unmount "${zone.active_ds}"
		run cmd undo rollback || exit 1

		log "\n%8s: zone %s converted, but is still detached" \
		    NOTICE "$zonename"
		if [[ ${zone.brand} == solaris10 ]]; then
			log "%8s: \"zoneadm -z %s attach\" to attach.\n" \
			    Run "$zonename"
		else
			log "%8s: \"zoneadm -z %s attach -u\" to attach.\n" \
			    Run "$zonename"
		fi
	fi
fi

#
# Clean up old datasets
#
log "%s: Removing obsolete datasets" "${zone.name}"
for (( i = ${#dslist[@]} - 1; i >= 0; i-- )); do
	(( verbose )) && log "%s: Destroying dataset %s" "${zone.name}" \
	    "${dslist[i].name}"
	(( dryrun )) || destroy_zone_dataset "${dslist[i].name}"
done

if (( skip_export == 0 )) && [[ -d ${zone.root}/export ]]; then
	log "%s: Migrating /export from the active boot environent" \
	    "${zone.name}"
	(( dryrun )) || migrate_export zone
	(( dryrun )) || migrate_rpool zone
fi

#
# Convert for dataset aliases
#
# If there are no delegated datasets, this does nothing.  If there are
# delegated datasets that don't have aliases defined (none should have
# aliases yet), the default aliases get populated into the zone configuration.
#
typeset -i errors=0
log "%s: Converting dataset aliases\n" "$zonename"
zonecfg -z "$zonename" "$(zonecfg -z $zonename info dataset | \
    nawk '$1 == "name:" { printf "select dataset name=%s; end;", $2 }')"
if (( $? != 0 )); then
	(( errors++ ))
	cat <<-NOMORE

	The automated conversion process is complete.

	Before the zone will boot zone configuration problems need to be
	corrected.  Run 'zonecfg -z $zonename' then use the verify subcommand
	within zonecfg to get a list of configuration problems that must be
	corrected within zonecfg.

	NOMORE
fi

log "%s: Verifying zone with zoneadm\n" "$zonename"
zoneadm -z "$zonename" verify
if (( $? != 0 )); then
	if (( errors == 0 )); then
		print "\nThe automated conversion process is complete."
	fi
	(( errors++ ))
	cat <<-NOMORE

	As displayed above, system configuration problems exist that must
	be corrected befor the zone will boot.  Before booting $zonename you
	will need to manually correct those problems.

	You may run 'zoneadm -z $zonename verify' at any time to re-verify the
	system configuration related to this zone.

	NOMORE
fi

#
# Check for potential /dev/zvol conflicts.  While this may emit warnings, it
# it should not cause the exit code to change from 0.
#
log "%s: Checking for potential configuration conflicts\n" "$zonename"
zonecfg -z "$zonename" verify -v
if (( $? != 0 )); then
	cat <<-NOMORE

	With Zone Dataset Aliasing, ZFS datasets that are delegated to
	$zonename appear within the zone as virtual ZFS pools.  One or more
	of the device resources that appears in the configuration for
	$zonename may hide the existence of similarly named ZFS volumes
	within these virtual pools.  See zonecfg(1M) and dev(7FS) for
	details.

	You may run 'zonecfg -z $zonename verify -v' at any time to perform
	this check again.

	NOMORE
fi

if (( errors != 0 )); then
	exit 1
fi

log "$m_conversion_complete" "$zonename"
exit 0
