#
# 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) 2009, 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
#
# backtrace [message]
#
# Used to generate a backtrace (newest on top) of functions up to the caller.
# Intended for use when internal errors are encountered.
#
function backtrace {
typeset msg="$*"
typeset -a args
#
# Use "set -u" followed by an unset variable reference to force
# a backtrace.
#
set -A args -- $(exec 2>&1; set -u; unset bt; $bt)
if [[ -n "$msg" ]]; then
print -u2 -- "${msg}:"
else
print -u2 "Backtrace:"
fi
#
# The ksh backtrace format is unlike that seen in common debuggers and
# other languages, so the logic below transforms it. That is, we go
# from a message like the following (but all on one line). do_{a,b,c}
# are functions that led to the backtrace and ./foo is the script name.
#
# ./foo[17]: do_a[6]: do_b[10]: do_c[14]: backtrace: line 47: bt:
# parameter not set
#
# To:
# do_c[14]:
# do_b[10]:
# do_a[6]:
# ./foo[17]:
#
typeset -i i
#
# Skip errors about this function as we are reporting on the path
# that led to this function, not the function itself. From the example
# above, we remove the arguments that make up "backtrace: line 47: bt:
# parameter not set"
#
for (( i = ${#args[@]} - 1; i >= 0; i-- )); do
[[ "${args[i]}" == "${.sh.fun}:" ]] && break
done
# Print a backtrace, newest on top
for (( i-- ; i >= 0; i-- )); do
print -u2 "\t${args[i]}"
done
}
#
# Send the error message to the screen and to the logfile.
#
function error {
typeset fmt="$1"
shift
[[ -z "$fmt" ]] && fail_internal "format argument undefined"
printf -- "${MSG_PREFIX}ERROR: ${fmt}\n" "$@"
[[ -n $LOGFILE ]] && printf "[$(date)] ERROR: ${fmt}\n" "$@" >&2
}
function fatal {
typeset fmt="$1"
shift
[[ -z $EXIT_CODE ]] \
&& fail_internal 'fatal (%s) called with undefined $EXIT_CODE' \
"$(printf -- "$fmt" "$@")"
error "$fmt" "$@"
exit $EXIT_CODE
}
function fail_fatal {
typeset fmt="$1"
shift
[[ -n "$fmt" ]] && error "$fmt" "$@"
EXIT_CODE=$ZONE_SUBPROC_FATAL
exit $ZONE_SUBPROC_FATAL
}
function fail_tryagain {
typeset fmt="$1"
[[ -z "$fmt" ]] && fail_internal "format argument undefined"
printf "ERROR: " 1>&2
printf -- "$@" 1>&2
printf "\n" 1>&2
EXIT_CODE=$ZONE_SUBPROC_TRYAGAIN
exit $ZONE_SUBPROC_TRYAGAIN
}
function fail_usage {
#
# format is optional argument to fail_usage: do not fail_internal if
# none given
#
typeset fmt=$1
shift
[[ -n $fmt ]] && log "$fmt\n" "$@"
[[ -n $m_brnd_usage ]] && log "$m_brnd_usage"
[[ -z $m_usage ]] && fail_internal "m_usage undefined"
log "$m_usage\n"
EXIT_CODE=$ZONE_SUBPROC_USAGE
exit $ZONE_SUBPROC_USAGE
}
function fail_unavailable {
typeset fmt="$1"
shift
[[ -n "$fmt" ]] && error "$fmt" "$@"
EXIT_CODE=$ZONE_SUBPROC_UNAVAILABLE
exit $ZONE_SUBPROC_UNAVAILABLE
}
#
# fail_internal is intended to be used in places where we are checking for
# logic errors, much as assert is used in C.
#
function fail_internal {
typeset fmt=$1
shift
backtrace "Unrecoverable internal error"
error "$fmt" "$@"
EXIT_CODE=$ZONE_SUBPROC_FATAL
exit $ZONE_SUBPROC_FATAL
}
#
# Send the provided printf()-style arguments to the screen and to the logfile.
#
function log {
typeset fmt="$1"
shift
[[ -z "$fmt" ]] && fail_internal "format argument undefined"
printf -- "${MSG_PREFIX}${fmt}\n" "$@"
[[ -n $LOGFILE ]] && printf "[$(date)] ${MSG_PREFIX}${fmt}\n" "$@" >&2
}
#
# Print provided text to the screen if the shell variable "OPT_V" is set.
# The text is always sent to the logfile.
#
function vlog {
typeset fmt="$1"
shift
[[ -z "$fmt" ]] && fail_internal "format argument undefined"
[[ -n $OPT_V ]] && printf -- "${MSG_PREFIX}${fmt}\n" "$@"
[[ -n $LOGFILE ]] && printf "[$(date)] ${MSG_PREFIX}${fmt}\n" "$@" >&2
}
# Check that zone is not in the ROOT dataset.
function fail_zonepath_in_rootds {
typeset gzrootds=$(get_ds_from_path /)
if [[ -z $gzrootds ]]; then
#
# This is an internal error because we shouldn't have made it
# this far if the system wasn't ZFS root.
#
fail_internal "Unable to find dataset mounted at /"
fi
[[ $1 == "$gzrootds"/* ]] && fail_fatal "$f_zfs_in_root"
}
# Return success if system is labeled (aka Trusted Extensions).
function is_system_labeled {
[[ -x /bin/plabel ]] || return 1
/bin/plabel >/dev/null 2>&1 && return 0
return 1
}
#
# Validate that the directory is safe.
#
# It is possible for a malicious zone root user to modify a zone's filesystem
# so that modifications made to the zone's filesystem by administrators in the
# global zone modify the global zone's filesystem. We can prevent this by
# ensuring that all components of paths accessed by scripts are real (i.e.,
# non-symlink) directories.
#
# NOTE: The specified path should be an absolute path as would be seen from
# within the zone. Also, this function does not check parent directories.
# If, for example, you need to ensure that every component of the path
# '/foo/bar/baz' is a directory and not a symlink, then do the following:
#
# safe_dir /foo
# safe_dir /foo/bar
# safe_dir /foo/bar/baz
#
function safe_dir {
typeset dir="$1"
if [[ -h $ZONEROOT/$dir || ! -d $ZONEROOT/$dir ]]; then
fatal "$e_baddir" "$dir"
fi
}
# Like safe_dir except the dir doesn't have to exist.
function safe_opt_dir {
typeset dir="$1"
[[ ! -e $ZONEROOT/$dir ]] && return
if [[ -h $ZONEROOT/$dir || ! -d $ZONEROOT/$dir ]]; then
fatal "$e_baddir" "$dir"
fi
}
# Only make a copy if we haven't already done so.
function safe_backup {
typeset src="$1"
typeset dst="$2"
if [[ ! -h $src && ! -h $dst && ! -d $dst && ! -f $dst ]]; then
/usr/bin/cp -p $src $dst || fatal "$e_badfile" "$src"
fi
}
# Make a copy even if the destination already exists.
function safe_copy {
typeset src="$1"
typeset dst="$2"
if [[ ! -h $src && ! -h $dst && ! -d $dst ]]; then
/usr/bin/cp -p $src $dst || fatal "$e_badfile" "$src"
fi
}
# Make a recursive copy
function safe_copy_rec {
typeset src="$1"
typeset dst="$2"
if [[ ! -h $src && ! -h $dst && ! -d $dst ]]; then
/usr/bin/cp -pr $src $dst || fatal "$e_badfile" "$src"
fi
}
# Move a file
function safe_move {
typeset src="$1"
typeset dst="$2"
if [[ ! -h $src && ! -h $dst && ! -d $dst ]]; then
/usr/bin/mv $src $dst || fatal "$e_badfile" "$src"
fi
}
function safe_rm {
if [[ ! -h $ZONEROOT/$1 && -f $ZONEROOT/$1 ]]; then
rm -f "$ZONEROOT/$1"
fi
}
#
# Replace the file with a wrapper pointing to the native brand code.
# However, we only do the replacement if the file hasn't already been
# replaced with our wrapper. This function expects the cwd to be the
# location of the file we're replacing.
#
# Some of the files we're replacing are hardlinks to isaexec so we need to 'rm'
# the file before we setup the wrapper while others are hardlinks to rc scripts
# that we need to maintain.
#
function safe_replace {
typeset filename="$1"
typeset runname="$2"
typeset mode="$3"
typeset own="$4"
typeset rem="$5"
typeset nativedir="$6"
if [ -h $filename -o ! -f $filename ]; then
return
fi
egrep -s "Solaris Brand Replacement" $filename
if [ $? -eq 0 ]; then
return
fi
safe_backup $filename $filename.pre_p2v
if [ $rem = "remove" ]; then
rm -f $filename
fi
cat <<-END >$filename || exit 1
#!/bin/sh -p
#
# Solaris Brand Replacement
#
# Attention. This file has been replaced with a new version for
# use in a virtualized environment. Modification of this script is not
# supported and all changes will be lost upon reboot. The
# {name}.pre_p2v version of this file is a backup copy of the
# original and should not be deleted.
#
END
echo "__S10_BRAND_NATIVE_PATH=$nativedir;" >>$filename || exit 1
echo ". $runname \"\$@\"" >>$filename || exit 1
chmod $mode $filename
chown $own $filename
}
function safe_wrap {
typeset filename="$1"
typeset runname="$2"
typeset mode="$3"
typeset own="$4"
if [ -f $filename ]; then
log "$e_cannot_wrap" "$filename"
exit 1
fi
cat <<-END >$filename || exit 1
#!/bin/sh
#
# Solaris Brand Wrapper
#
# Attention. This file has been created for use in a
# virtualized environment. Modification of this script
# is not supported and all changes will be lost upon reboot.
#
END
echo ". $runname \"\$@\"" >>$filename || exit 1
chmod $mode $filename
chown $own $filename
}
#
# Read zonecfg fs entries and save the relevant data, one entry per
# line.
# This assumes the properties from the zonecfg output, e.g.:
# fs:
# dir: /opt
# special: /opt
# raw not specified
# type: lofs
# options: [noexec,ro,noatime]
#
# and it assumes the order of the fs properties as above.
#
function 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.
#
function 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("command failed: %s\n", cmd);
exit 1;
}
}' >>$LOGFILE
}
#
# Unmount zonecfg fs entries from the zonepath.
#
function umnt_fs {
if [[ -z $fstmpfile || ! -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("command failed: %s\n", cmd);
}
}' >>$LOGFILE
}
#
# get_dataset path [outvar]
#
# Find the dataset mounted at a given path. The implementation is tolerant
# of the path not being an exact match of the entry in /etc/mnttab (e.g. an
# extra / somewhere) but the supplied path must be a mount point of a ZFS
# dataset. If a second argument is provided, it must be the name of a variable
# into which the result will be stored.
#
# On success, returns 0. In the one argument form, the dataset is printed
# to stdout. In the two argument form, the dataset name is stored in the
# variable by the name of the second argument.
# If no match is found, returns 1.
#
function get_ds_from_path {
typeset path=$1
typeset dsn mountpoint
/usr/sbin/zfs list -H -o name,mountpoint "$path" 2>/dev/null | \
IFS=$'\t' read dsn mountpoint
[[ -z $dsn || -z $mountpoint ]] && return 1
# If mountpoint=legacy, consult /etc/mnttab.
if [[ $mountpoint == legacy ]]; then
mountpoint=$(nawk -F$'\t' -v "dsn=$dsn" \
'$1 == dsn { print $2; exit }' /etc/mnttab)
[[ -z $mountpoint ]] && return 1
fi
[[ $mountpoint -ef $path ]] || return 1
if [[ -n "$2" ]]; then
typeset -n res=$2
res=$dsn
else
print -- "$dsn"
fi
return 0
}
#
# create_zone_rpool [-e] [-r] zone
#
# Establishes the standard dataset hierarchy under <zonepath>/rpool
#
# Arguments:
# -e Do not create rpool/export hierarchy
# -r Do not create rpool/ROOT or rpool/export hierarchy
# zone a zone structure created with init_zone
#
# Globals:
#
# EXIT_CODE Set to ZONE_SUBPROC_FATAL if any dataset that could collide
# with a future install or attach operation is created.
#
function create_zone_rpool {
typeset opt
typeset -i skipexport=0
typeset -i skipROOT=0
while getopts :er opt; do
case $opt in
e) skipexport=1 ;;
r) skipROOT=1 ;;
?) fail_internal "$f_int_bad_opt" "$OPTARG" ;;
esac
done
shift $(( OPTIND - 1 ))
case $# in
0) fail_internal "$f_int_missing_arg" zonepath_dataset ;;
1) typeset -n zone=$1 ;;
*) shift;
fail_internal "$f_int_bad_arg" "$*"
;;
esac
#
# rpool
#
typeset ds
init_dataset ds "${zone.rpool_ds}" >/dev/null 2>&1
if (( $? != 0 )); then
zfs create -o zoned=on -o mountpoint=/rpool \
"${zone.rpool_ds}" || {
log "$f_zfs_create" "${zone.ROOT_ds}"
return 1
}
else
zfs_set zoned=on ds || return 1
zfs_set canmount=on ds || return 1
zfs_set mountpoint=/rpool ds || return 1
# Dealing with existing rpool. Perhaps someone intentionally
# got rid of export dataset. We shouldn't make it come back.
(( skipexport = 1 ))
fi
if (( $skipROOT != 0 )); then
return 0
fi
#
# If the zone configuration already has export in it, don't create it.
# Message that is logged in the event that it exists is inappropriate
# for this function, so send it to the bit bucket.
#
zonecfg_has_export zone >/dev/null 2>&1 && (( skipexport = 1 ))
#
# rpool/ROOT
#
init_dataset ds "${zone.ROOT_ds}" >/dev/null 2>&1
if (( $? != 0 )); then
zfs create -o canmount=noauto -o mountpoint=legacy \
"${zone.ROOT_ds}" || {
log "$f_zfs_create" "${zone.ROOT_ds}"
return 1
}
else
zfs inherit zoned "${zone.ROOT_ds}"
typeset prop
for prop in canmount=noauto mountpoint=legacy; do
zfs_set "$prop" ds || return 1
done
fi
# zfs_set doesn't do -r so use zfs command directly.
zfs set -r canmount=noauto "${zone.ROOT_ds}" || return 1
#
# rpool/export
#
if (( skipexport == 0 )); then
zfs create -o mountpoint=/export "${zone.rpool_ds}/export" || {
log "$f_zfs_create" "${zone.rpool_ds}/export"
return 1
}
EXIT_CODE=$ZONE_SUBPROC_FATAL
zfs create "${zone.rpool_ds}/export/home" || {
log "$f_zfs_create" "${zone.rpool_ds}/export/home"
return 1
}
fi
return 0
}
#
# create_active_ds [-s snapshot] [-r] zone
#
# Set up ZFS dataset hierarchy for the zone root dataset and the datasets
# listed in zone.new_be_datasets. If an active dataset is being created
# as a clone of another BE (that is, -s is used), the caller is responsible for
# cloning any child datasets and zone.new_be_datasets is ignored.
#
# Arguments and options:
#
# -s snapshot If specified, the active dataset is cloned from this snapshot.
# With -s, the caller is responsible for cloning any child
# datasets. That is, /var is not created automatically.
# -r If specified, only create rpool, not ROOT, export or zbe
# zone zone structure created with init_zone.
#
# Globals:
#
# EXIT_CODE Set to ZONE_SUBPROC_FATAL if any ZBE or rpool/export is
# created.
#
function create_active_ds {
typeset snapshot opt
typeset -i skipROOT=0
while getopts :s:r opt; do
case $opt in
s) snapshot="$OPTARG" ;;
r) skipROOT=1 ;;
?) fail_internal "$f_int_bad_opt" "$OPTARG" ;;
esac
done
shift $(( OPTIND - 1 ))
case $# in
0) fail_internal "$f_int_missing_arg" zone ;;
1) : ;;
*) fail_internal "$f_int_bad_arg" "$*" ;;
esac
typeset -n zone=$1
#
# Find the zone's current dataset. This should have been created by
# zoneadm.
#
[[ -z "${zone.path.ds}" ]] && fail_fatal "$f_no_ds"
# Check that zone is not in the ROOT dataset.
fail_zonepath_in_rootds "${zone.path.ds}"
#
# Create the zone's rpool, rpool/ROOT, rpool/export, etc. If creating
# from a snapshot (part of cloning process) assume the caller has
# already created it.
#
if [[ -z "$snapshot" ]]; then
if (( $skipROOT == 1 )); then
# Does not set EXIT_CODE.
create_zone_rpool -r zone || return 1
return 0
fi
# Sets EXIT_CODE.
create_zone_rpool zone || return 1
fi
#
# Create the new active dataset with "zfs create" or "zfs clone",
# depending on whether a snapshot was passed. If the create or clone
# operation fails 100 times, it's likely it will never succeed.
#
typeset bename dsname
typeset -a zfs_prop_options
#
# mountpoint=/ is safe because create_zone_rpool verifies zoned=on
# for parent.
#
typeset -a be_props
set -A be_props -- -o canmount=noauto -o mountpoint=/
typeset -a sl_opt
if is_system_labeled; then
# On TX, reset the mlslabel upon cloning
set -A sl_opt -- -o mlslabel=none
fi
typeset -i i
typeset be_prefix
if [[ ${zone.brand} == "solaris10" ]]; then
be_prefix=zbe
else
be_prefix=solaris
fi
for (( i = 0 ; i < 100 ; i++ )); do
bename=$(printf "%s-%d" "$be_prefix" $i)
dsname="${zone.ROOT_ds}/$bename"
if [[ -n "$snapshot" ]]; then
vlog "Cloning active_ds $dsname from $snapshot"
zfs clone "${be_props[@]}" "${sl_opt[@]}" "$snapshot" \
"$dsname" >/dev/null 2>&1 && break
else
vlog "Creating active_ds $dsname"
zfs create "${be_props[@]}" "$dsname" \
>/dev/null 2>&1 && break
fi
bename=
dsname=
done
[[ -z $bename ]] && return 1
EXIT_CODE=$ZONE_SUBPROC_FATAL
# If clone wasn't used, create the child datasets, if any.
if [[ -z $snapshot ]]; then
typeset child
for child in ${zone.new_be_datasets[@]}; do
vlog "Creating child dataset: %s" "$child"
zfs create -o mountpoint="/$child" -o canmount=noauto \
"$dsname/$child" || return 1
done
fi
# Activate the BE.
set_active_be zone "$bename" || return 1
}
#
# Perform validation and cleanup in the zoneroot after unpacking the archive.
#
function post_unpack {
#
# Check if the image was created with a valid libc.so.1.
#
hwcap=$(moe -v -32 $ZONEROOT/lib/libc.so.1 2>&1)
if (( $? != 0 )); then
vlog "$f_hwcap_info" "$hwcap"
fail_fatal "$f_sanity_hwcap"
fi
( cd "$ZONEROOT" && \
find . \( -type b -o -type c \) -exec rm -f "{}" \; )
# Create directories that, when missing, befuddle zoneadm mount
# and various SMF services running in the zone.
typeset -A dirs
set -A dirs[tmp] 1777 root sys
set -A dirs[dev] 755 root sys
set -A dirs[etc/zones] 755 root sys
typeset dir
for dir in ${!dirs[@]}; do
safe_opt_dir /$dir
[[ -d $ZONEROOT/$dir ]] && continue
typeset mode=${dirs[$dir][0]}
typeset user=${dirs[$dir][1]}
typeset group=${dirs[$dir][2]}
mkdir -m "$mode" "$ZONEROOT/$dir" ||
fatal "$f_mkdir" "$ZONEROOT/$dir"
chown "$user:$group" "$ZONEROOT/$dir" ||
fatal "$f_chown" "$ZONEROOT/$dir"
done
}
#
# Determine flar compression style from identification file.
#
function get_compression {
typeset ident=$1
typeset line=$(grep "^files_compressed_method=" $ident)
print ${line##*=}
}
#
# Determine flar archive style from identification file.
#
function get_archiver {
typeset ident=$1
typeset line=$(grep "^files_archived_method=" $ident)
print ${line##*=}
}
#
# Get the archive base.
#
# We must unpack the archive in the right place within the zonepath so
# that files are installed into the various mounted filesystems that are set
# up in the zone's configuration. These are already mounted for us by the
# mntfs function.
#
# Archives can be made of either a physical host's root file system or a
# zone's zonepath. For a physical system, if the archive is made using an
# absolute path (/...) we can't use it. For a zone the admin can make the
# archive from a variety of locations;
#
# a) zonepath itself: This will be a single dir, probably named with the
# zone name, it will contain a root dir and under the root we'll see all
# the top level dirs; etc, var, usr... We must be above the ZONEPATH
# when we unpack the archive but this will only work if the the archive's
# top-level dir name matches the ZONEPATH base-level dir name. If not,
# this is an error.
#
# b) inside the zonepath: We'll see root and it will contain all the top
# level dirs; etc, var, usr.... We must be in the ZONEPATH when we unpack
# the archive.
#
# c) inside the zonepath root: We'll see all the top level dirs, ./etc,
# ./var, ./usr.... This is also the case we see when we get an archive
# of a physical sytem. We must be in ZONEROOT when we unpack the archive.
#
# Note that there can be a directory named "root" under the ZONEPATH/root
# directory.
#
# This function handles the above possibilities so that we reject absolute
# path archives and figure out where in the file system we need to be to
# properly unpack the archive into the zone. It sets the ARCHIVE_BASE
# variable to the location where the achive should be unpacked.
#
function get_archive_base {
stage1=$1
archive=$2
stage2=$3
vlog "$m_analyse_archive"
base=$($stage1 $archive | $stage2 2>/dev/null | nawk -F/ '{
# Check for an absolute path archive
if (substr($0, 1, 1) == "/")
exit 1
if ($1 != ".")
dirs[$1] = 1
else
dirs[$2] = 1
}
END {
for (d in dirs) {
cnt++
if (d == "bin") sawbin = 1
if (d == "etc") sawetc = 1
if (d == "root") sawroot = 1
if (d == "var") sawvar = 1
}
if (cnt == 1) {
# If only one top-level dir named root, we are in the
# zonepath, otherwise this must be an archive *of*
# the zonepath so print the top-level dir name.
if (sawroot)
print "*zonepath*"
else
for (d in dirs) print d
} else {
# We are either in the zonepath or in the zonepath/root
# (or at the top level of a full system archive which
# looks like the zonepath/root case). Figure out which
# one.
if (sawroot && !sawbin && !sawetc && !sawvar)
print "*zonepath*"
else
print "*zoneroot*"
}
}')
if (( $? != 0 )); then
umnt_fs
fatal "$e_absolute_archive"
fi
if [[ "$base" == "*zoneroot*" ]]; then
ARCHIVE_BASE=$ZONEROOT
elif [[ "$base" == "*zonepath*" ]]; then
ARCHIVE_BASE=$ZONEPATH
else
# We need to be in the dir above the ZONEPATH but we need to
# validate that $base matches the final component of ZONEPATH.
bname=$(basename $ZONEPATH)
if [[ "$bname" != "$base" ]]; then
umnt_fs
fatal "$e_mismatch_archive" "$base" "$bname"
fi
ARCHIVE_BASE=$(dirname $ZONEPATH)
fi
}
#
# Unpack cpio archive into zoneroot.
#
function install_cpio {
stage1=$1
archive=$2
get_archive_base "$stage1" "$archive" "cpio -it"
cpioopts="-idmP@/fE $fscpiofile"
vlog "cd \"$ARCHIVE_BASE\" && $stage1 \"$archive\" | cpio $cpioopts"
# Ignore errors from cpio since we expect some errors depending on
# how the archive was made.
( cd "$ARCHIVE_BASE" && $stage1 "$archive" | cpio $cpioopts )
post_unpack
return 0
}
#
# Unpack pax archive into zoneroot.
#
function install_pax {
typeset archive=$1
typeset filtopt
get_archive_base "cat" "$archive" "pax"
if [[ -n $fspaxfile && -s $fspaxfile ]]; then
filtopt="-c $(/usr/bin/cat $fspaxfile)"
fi
vlog "cd \"$ARCHIVE_BASE\" && pax -r@/ -p e -f \"$archive\" $filtopt"
# Ignore errors from pax since we expect some errors depending on
# how the archive was made.
( cd "$ARCHIVE_BASE" && pax -r@/ -p e -f "$archive" $filtopt )
post_unpack
return 0
}
#
# Unpack UFS dump into zoneroot.
#
function install_ufsdump {
archive=$1
vlog "cd \"$ZONEROOT\" && ufsrestore rf \"$archive\""
#
# ufsrestore goes interactive if you ^C it. To prevent that,
# we make sure its stdin is not a terminal.
#
( cd "$ZONEROOT" && ufsrestore rf "$archive" < /dev/null )
result=$?
post_unpack
return $result
}
#
# Copy directory hierarchy into zoneroot.
#
function install_dir {
source_dir=$1
cpioopts="-pPdm@/"
first=1
filt=$(for i in $(cat $fspaxfile)
do
echo $i | egrep -s "/" && continue
if [[ $first == 1 ]]; then
printf "^%s" $i
first=0
else
printf "|^%s" $i
fi
done)
list=$(cd "$source_dir" && ls -d * | egrep -v "$filt")
flist=$(for i in $list
do
printf "%s " "$i"
done)
findopts="-xdev ( -type d -o -type f -o -type l ) -print"
vlog "cd \"$source_dir\" && find $flist $findopts | "
vlog "cpio $cpioopts \"$ZONEROOT\""
# Ignore errors from cpio since we expect some errors depending on
# how the archive was made.
( cd "$source_dir" && find $flist $findopts | \
cpio $cpioopts "$ZONEROOT" )
post_unpack
return 0
}
#
# This is a common function for laying down a zone image from a variety of
# different sources. This can be used to either install a fresh zone or as
# part of zone migration during attach.
#
# The first argument specifies the type of image: archive, directory or stdin.
# The second argument specifies the image itself. In the case of stdin, the
# second argument specifies the format of the stream (cpio, flar, etc.).
# Any validation or post-processing on the image is done elsewhere.
#
# This function calls a 'sanity_check' function which must be provided by
# the script which includes this code.
#
# Returns:
# Returns 0 on success. Always exits on failure.
#
# Globals:
# EXIT_CODE Set to ZONE_SUBPROC_UNAVAILABLE on successful return.
#
function install_image {
typeset -n zone=$1
typeset intype=$2
typeset insrc=$3
if [[ -z ${zone.name} || -z $intype || -z $insrc ]]; then
fail_internal "Missing argument to install_image. Got: '%s'" \
"$*"
fi
typeset filetype="unknown"
typeset filetypename="unknown"
typeset filetypeprefx=
typeset stage1="cat"
if [[ "$intype" == "directory" ]]; then
if [[ "$insrc" == "-" ]]; then
# Indicates that the existing zonepath is prepopulated.
filetype="existing"
filetypename="existing"
else
if [[ "$(echo $insrc | cut -c 1)" != "/" ]]; then
fatal "$e_path_abs" "$insrc"
fi
if [[ ! -e "$insrc" ]]; then
log "$e_not_found" "$insrc"
fatal "$e_install_abort"
fi
if [[ ! -r "$insrc" ]]; then
log "$e_not_readable" "$insrc"
fatal "$e_install_abort"
fi
if [[ ! -d "$insrc" ]]; then
log "$e_not_dir"
fatal "$e_install_abort"
fi
sanity_check $insrc
filetype="directory"
filetypename="directory"
fi
else
# Common code for both archive and stdin stream.
if [[ "$intype" == "archive" ]]; then
if [[ $insrc != /* ]]; then
log "$e_path_abs" "$insrc"
fatal "$e_install_abort"
elif [[ ! -f "$insrc" ]]; then
log "$e_not_found" "$insrc"
fatal "$e_install_abort"
fi
ftype="$(LC_ALL=C file "$insrc" | cut -d: -f 2)"
#
# If it is a compressed stream, extract the first
# megabyte into a temporary file to figure out what
# kind of data is in the file.
#
case "$ftype" in
*bzip2*)
stage1=bzcat
filetypeprefx="bzipped "
;;
*gzip*) stage1=gzcat
filetypeprefix="gzipped "
;;
esac
if [[ $stage1 != cat ]]; then
typeset tastefile=$(mktemp)
[[ -n $tastefile ]] || fatal "$e_tmpfile"
"$stage1" "$insrc" | dd of=$tastefile \
bs=1024k count=1 2>/dev/null
ftype="$(LC_ALL=C file "$tastefile" \
| cut -d: -f 2)"
rm -f "$tastefile"
fi
elif [[ $intype == stdin ]]; then
# For intype == stdin, the insrc parameter specifies
# the stream format coming on stdin.
ftype="$insrc"
insrc="-"
else
fail_internal "intype '%s' is invalid" "$intype"
fi
# Setup vars for the archive type we have.
case "$ftype" in
*cpio*) filetype="cpio"
filetypename="cpio archive"
;;
*ufsdump*)
if [[ ${zone.brand} != solaris10 ]]; then
log "$e_unsupported_archive" "$ftype" \
"${zone.brand}"
fatal "$e_install_abort"
fi
filetype="ufsdump"
filetypename="ufsdump archive"
;;
flar|flash|*Flash\ Archive*)
if [[ ${zone.brand} != solaris10 ]]; then
log "$e_unsupported_archive" "$ftype" \
"${zone.brand}"
fatal "$e_install_abort"
fi
filetype="flar"
filetypename="flash archive"
;;
tar|*USTAR\ tar\ archive)
filetype="tar"
filetypename="tar archive"
;;
pax|*USTAR\ tar\ archive\ extended\ format*)
filetype="xustar"
filetypename="pax (xustar) archive"
;;
zfs|*ZFS\ snapshot\ stream*)
filetype="zfs"
filetypename="ZFS send stream"
;;
*) log "$e_unsupported_archive" "$ftype" "${zone.brand}"
fatal "$e_install_abort"
;;
esac
fi
# compressed archives only supported for cpio and zfs
if [[ $stage1 != cat ]]; then
filetypename="${filetypeprefx}$filetypename"
if [[ $filetype != cpio && $filetype != zfs ]]; then
log "$e_unsupported_archive" "$filetypename" \
"${zone.brand}"
fatal "$e_install_abort"
fi
fi
vlog "$filetypename"
if [[ $filetype != @(existing|zfs|flar) ]]; then
#
# Since we're not using a pre-existing ZFS dataset layout, or
# an archive containing a dataset layout, create the zone
# datasets and mount them.
#
# Sets EXIT_CODE.
create_active_ds zone || fatal "$f_no_active_ds"
mount_active_be -c zone || fatal "$f_mount_active_be"
# If the brand supports candidate zbes, tag this as a candidate.
if [[ -n $PROP_CANDIDATE ]]; then
zfs set "$PROP_CANDIDATE=on" "${zone.active_ds}" ||
fatal "$e_zfs_set" "$PROP_CANDIDATE=on" \
"${zone.active_ds}"
fi
fi
fstmpfile=$(/usr/bin/mktemp -t -p /var/tmp)
if [[ -z "$fstmpfile" ]]; then
fatal "$e_tmpfile"
fi
# Make sure we always have the files holding the directories to filter
# out when extracting from a CPIO or PAX archive. We'll add the fs
# entries to these files in get_fs_info()
fscpiofile=$(/usr/bin/mktemp -t -p /var/tmp fs.cpio.XXXXXX)
if [[ -z "$fscpiofile" ]]; then
rm -f $fstmpfile
fatal "$e_tmpfile"
fi
# Filter out these directories.
cat >>$fscpiofile <<-EOF
dev/*
devices/*
devices
proc/*
tmp/*
var/run/*
system/contract/*
system/object/*
system/volatile/*
rpool/boot/*
rpool/boot
rpool/etc/*
rpool/etc
EOF
fspaxfile=$(/usr/bin/mktemp -t -p /var/tmp fs.pax.XXXXXX)
if [[ -z "$fspaxfile" ]]; then
rm -f $fstmpfile $fscpiofile
fatal "$e_tmpfile"
fi
print -n "dev devices proc tmp var/run system/contract system/object" \
"system/volatile rpool/boot rpool/etc" >>$fspaxfile
# Set up any fs mounts so the archive will install into the correct locations.
if [[ $filetype != @(existing|zfs|flar) ]]; then
get_fs_info
mnt_fs
if (( $? != 0 )); then
umnt_fs >/dev/null 2>&1
rm -f $fstmpfile $fscpiofile $fspaxfile
fatal "$mount_failed"
fi
fi
if [[ $filetype == existing ]]; then
if [[ -z ${zone.zbe_cloned_from} ]]; then
log "$no_installing"
else
log "$from_clone" "${zone.zbe_cloned_from}"
fi
else
log "$installing"
fi
#
# Install the image into the zonepath.
#
unpack_result=0
if [[ "$filetype" == "cpio" ]]; then
install_cpio "$stage1" "$insrc"
unpack_result=$?
elif [[ "$filetype" == "flar" ]]; then
# Sets EXIT_CODE.
$stage1 $insrc | install_flar zone
unpack_result=$?
elif [[ "$filetype" == "xustar" ]]; then
install_pax "$insrc"
unpack_result=$?
elif [[ "$filetype" == "tar" ]]; then
vlog "cd \"${zone.root}\" && tar -xf \"$insrc\""
# Ignore errors from tar since we expect some errors depending
# on how the archive was made.
( cd "${zone.root}" && tar -xf "$insrc" )
unpack_result=0
post_unpack
elif [[ "$filetype" == "ufsdump" ]]; then
install_ufsdump "$insrc"
unpack_result=$?
elif [[ "$filetype" == "directory" ]]; then
install_dir "$insrc"
unpack_result=$?
elif [[ "$filetype" == "zfs" ]]; then
# Sets EXIT_CODE.
extract_zfs zone "$stage1" "$insrc"
unpack_result=$?
fi
# Clean up any fs mounts used during unpacking.
umnt_fs
rm -f $fstmpfile $fscpiofile $fspaxfile
chmod 700 "${zone.path}"
(( unpack_result != 0 )) && fatal "$f_unpack_failed"
#
# We are now far enough along that the admin may be able to fix up an
# extracted/copied image in the event that an attach fails. Instead of
# deleting the new datasets, mark them as pinned so the error path
# doesn't delete them. Spit a message if pinning fails, but don't
# abort the operation.
#
if [[ $filetype != existing ]]; then
pin_datasets "${zone.path.ds}" || error "$f_pin"
fi
EXIT_CODE=$ZONE_SUBPROC_UNAVAILABLE
# Verify this is a valid image.
mount_active_be -C zone
sanity_check "${zone.root}"
return 0
}
#
# extract_zfs zone filter file
#
# Receive the zfs stream from the specified file. The stream is passed through
# the specified filter, such as gzcat or bzcat. If no filter is needed,
# use "cat" as the filter. zone should have been initialized by init_zone.
# File can be a regular file or /dev/stdin.
#
# On successful creation, the active ZBE is mounted on the zone root.
#
# Globals:
#
# EXIT_CODE Set to ZONE_SUBPROC_FATAL while temporary extraction dataset
# exists. Set to ZONE_SUBPROC_UNAVAILABLE on success.
#
# Returns the return value from "zfs receive". May exit with fatal errors.
#
function extract_zfs {
typeset -n zone=$1
typeset stage1=$2
typeset insrc=$3
#
# Receive the stream into a temporary dataset then move the datasets
# into place. Be careful while doing this that the recieved datasets
# don't get mounted.
#
zfs create -o zoned=on "${zone.path.ds}/installtmp" || fatal "$f_no_ds"
#
# Be sure that an uninstall is forced if we are interrupted before
# the install or attach completes.
#
EXIT_CODE=$ZONE_SUBPROC_FATAL
typeset -a cmd
set -A cmd zfs receive -F -u -x zoned "${zone.path.ds}/installtmp/ds"
vlog "$stage1 $insrc | ${cmd[*]}"
# $stage1 intentionally not quoted to support commands with arguments.
$stage1 "$insrc" | "${cmd[@]}"
typeset unpack_result=$?
if ! rationalize_datasets zone "${zone.path.ds}/installtmp/ds"; then
zfs destroy "${zone.path.ds}/installtmp" || \
error "$e_zfs_destroy" "${zone.path.ds}/installtmp"
#
# If rationalize_datasets returned early because it could not
# find an active ZBE, it will have set EXIT_CODE to
# ZONE_SUBPROC_TRYAGAIN In such a case, pin the rationalized
# datasets so that cleanup functions don't remove them. Also,
# avoid the call to fatal(), as that would give an improper
# error message. Set the state to unavailable so that another
# extraction can't be tried until the existing one is cleaned
# up via uninstall.
#
if [[ $EXIT_CODE == $ZONE_SUBPROC_TRYAGAIN ]]; then
EXIT_CODE=$ZONE_SUBPROC_UNAVAILABLE
pin_datasets ${zone.rpool_ds} && exit $EXIT_CODE
#
# Could not pin datasets, so they will probably
# get destroyed by the exit handler.
#
fail_fatal "$f_pin"
fi
fatal "$f_invalid_data"
fi
#
# If the destroy fails, this will trigger a failure in
# zoneadm verify.
#
zfs destroy "${zone.path.ds}/installtmp" || \
fatal "$e_zfs_destroy" "${zone.path.ds}/installtmp"
[[ $unpack_result == 0 ]] && EXIT_CODE=$ZONE_SUBPROC_UNAVAILABLE
return $unpack_result
}
#
# rationalize_datasets zone dsname
#
# dsname is the top-level dataset that should contain at least a BE and maybe
# other zone datasets in an unknown hierarchy. rationalize_datasets looks
# through the dataset and data hierarchy found there to find a way to
# shoehorn it into the proper dataset layout.
#
# Upon successful conversion:
#
# - $dsname will no longer exist
# - zoned, mountpoint, canmount will have been set properly
# - the active BE will have been determined and mounted.
# - 0 is returned
#
# If the rationalization fails, 1 is returned and the zone and $dsname are in
# a undetermined state.
#
# EXIT_CODE May be set as described by discover_active_be(), which is
# brand-specific.
#
function rationalize_datasets {
typeset -n zone=$1
typeset topdsn=$2
typeset dsn
typeset rpooldsn ROOTdsn
#
# This forms the logic of guessing where things are at.
#
# Build an associative array of datasets in the source area.
typeset -A dsa
get_datasets -A "$topdsn" dsa || return 1
#
# Look for any of the following dataset layouts. The layouts are
# describe from the perspective of the source system. In the table,
# <rootpool> refers to the the root pool, typically "rpool", on the
# source system. <zpds> refers to the zone path dataset - the dataset
# mounted at the zonepath.
#
# Source Layout Dataset provided as arg to zfs send Check# Notes
# ------ ----------- ----------------------------------- ------ -----
# GZ global $rootpool 2 1
# GZ global $rootpool/ROOT 3 1
# GZ global $rootpool/ROOT/$be 4 2
# NGZ s10 $zpds 5 2
# NGZ s11x2010.11 $zpds 1 1
# NGZ s11 $zpds/rpool 2 1
# NGZ any !s10 $zpds/rpool/ROOT 3 1
# $zpds/ROOT 1
# NGZ any $zpds/rpool/ROOT/$be 4 2
# $zpds/ROOT/$be 2
#
# The Layout column refers to the following dataset hierarchies:
#
# GZ global: Same across s10 and s11, expcept s11 adds
# rpool/export/home
# rpool
# rpool/ROOT
# rpool/ROOT/$be
# rpool/export
#
# NGZ s10:
# $zpds (A single BE exists in the "root" subdirectory.)
#
# NGZ s11express: Applies to Solaris 11 Express 2010.11.
# $zpds
# $zpds/ROOT
# $zpds/ROOT/$be
#
# NGZ s11:
# $zpds
# $zpds/rpool
# $zpds/rpool/ROOT
# $zpds/rpool/ROOT/$be
# $zpds/rpool/export
# $zpds/rpool/export/home
#
# any:
# Any of the above layouts are supported.
#
# any !s10:
# Any of the above layouts except NGZ s10 are supported.
#
# Notes:
#
# 1. The archive must be created with "zfs send -R", "zfs send -r",
# or "zfs send -rc". Note that "zfs send -r" first appears in
# Solaris 11.
# 2. The archive may be created with any of the options specified
# in Note 1 (assuming support in that Solaris release) or
# without the -R or -r[c] options to zfs send.
#
# Check 1
if [[ -n ${dsa[$topdsn/rpool]} && \
-n ${dsa[$topdsn/rpool/ROOT]} ]]; then
rpooldsn=$topdsn/rpool
ROOTdsn=$topdsn/rpool/ROOT
# Check 2
elif [[ -n ${dsa[$topdsn/ROOT]} ]]; then
rpooldsn=$topdsn
ROOTdsn=$topdsn/ROOT
# Check for 3, 4, and 5 - We need to mount it to figure it out.
else
typeset dir=$(mktemp -d)
# We know that it is zoned from the way it was received.
zfs set canmount=noauto "$topdsn" && \
zfs set mountpoint=/ $topdsn ||
vlog "Unable to set properties for mounting %s" "$topdsn"
if zfs_tmpmount "$topdsn" "$dir" >/dev/null 2>&1; then
if [[ -d $dir/usr && -d $dir/var && -d $dir/etc ]]
then
# Looks like the inside of a BE (Check 4)
rpooldsn=
ROOTdsn=$(dirname "$topdsn")
elif [[ ${zone.brand} == "solaris10" && \
-d $dir/root/usr && -d $dir/root/var && \
-d $dir/root/etc ]]; then
# Looks like an S10 zonepath dataset (Check 5)
rpooldsn=
ROOTdsn=$(dirname "$topdsn")
# Fix it to look like Check 4, above.
convert_s10_zonepath_to_be "$dir" || {
umount -f "$dir" >/dev/null 2>&1
return 1
}
else
# Must be a ROOT at $topdsn (Check 3)
rpooldsn=
ROOTdsn=$topdsn
fi
umount -f "$dir" >/dev/null 2>&1
else
# Must be a ROOT at $topdsn (Check 3)
rpooldsn=
ROOTdsn=$topdsn
fi
rmdir "$dir" >/dev/null 2>&1
fi
# Create rpool and rpool/ROOT if it doesen't already exist.
create_zone_rpool -e zone || return 1
if [[ -n $rpooldsn ]]; then
#
# Now look for datasets that collide
#
typeset -a collide_ds
typeset -a move_ds
typeset -A seen_ds
/usr/sbin/zfs list -H -o name -t filesystem,volume -r \
"$rpooldsn" | while read dsn; do
[[ $dsn == "$rpooldsn" ]] && continue
[[ $dsn == "$rpooldsn"/ROOT ]] && continue
# dataset name relative to rpooldsn
typeset rdsn=${dsn#$rpooldsn/}
if /usr/sbin/zfs list "${zone.rpool_ds}/$rdsn" \
>/dev/null 2>&1; then
#
# keep track of collisions that can be deleted
# for possible removal in reverse order
#
if ds_empty "${zone.rpool_ds}/$rdsn"; then
a_push collide_ds "$rdsn"
continue
fi
log "$e_ds_conflict" "$rdsn" \
"${zone.rpool_ds}/$rdsn"
(( collide++ ))
continue
fi
#
# ZBEs will be handled below, as they need to be
# tagged all at once after any colliding ZBEs are
# renamed.
#
[[ $dsn == "$rpooldsn"/ROOT/* ]] && continue
#
# If the parent of this dataset has already been added
# to the move list (or not added because its parent was
# added), it will be moved with its parent. Don't try
# to move it after it is already gone.
#
seen_ds[$rdsn]=1
[[ -n ${seen_ds[$(dirname "$rdsn")]} ]] && continue
a_push move_ds "$rdsn"
done
if (( collide != 0 )); then
return 1
fi
for dsn in "${collide_ds[@]}"; do
vlog "Removing empty dataset '%s' due to collision" \
"$dsn"
zfs destroy "$dsn"
done
for dsn in "${move_ds[@]}"; do
vlog "Dataset '%s' received from archive" \
"${zone.rpool_ds}/$dsn"
zfs rename "$rpooldsn/$dsn" "${zone.rpool_ds}/$dsn" ||
return 1
done
fi
#
# The zone's rpool dataset (if any) has been migrated, except for the
# be(s) found in $ROOTdsn. Merge them in with any existing zbes.
#
typeset -a bes
typeset be newbe
typeset -A zone.allowed_bes # used by discover_active_be
typeset -A allowed_bes
typeset -A bemap
typeset -i i
typeset be_prefix
if [[ ${zone.brand} == "solaris10" ]]; then
be_prefix=zbe
else
be_prefix=solaris
fi
tag_candidate_zbes "$ROOTdsn" bes allowed_bes || return 1
for be in "${bes[@]}"; do
# Use the next available BE name
for (( i = 0 ; i < 100 ; i++ )); do
newbe=$be_prefix-$i
#
# Try to claim this BE. If it fails, it probably means
# that there is already a BE by that name and we should
# try to claim the next one.
#
if /usr/sbin/zfs rename "$ROOTdsn/$be" \
"${zone.ROOT_ds}/$newbe" >/dev/null 2>&1; then
break
fi
newbe=
done
if [[ -z $newbe ]]; then
error "$e_be_move_failed" "$be"
return 1
fi
[[ -n ${allowed_bes[$be]} ]] && zone.allowed_bes[$newbe]=1
done
#
# The only datasets that may still exist under $topdsn are $rpooldsn
# and $ROOTdsn. Verify that. If it all checks out, destroy $topdsn
# and all of its children and call it a day.
#
typeset -i errors=0 delete_topdsn=0
/usr/sbin/zfs list -H -o name -t filesystem,volume "$topdsn" \
2>/dev/null | while read dsn; do
# Assuming there are no errors, $topdsn should be deleted.
(( delete_topdsn=1 ))
[[ $dsn == "$topdsn" ]] && continue
[[ $dsn == "$rpooldsn" ]] && continue
[[ $dsn == "$ROOTdsn" ]] && continue
log "$e_unexpected_ds" "${dsn#$topdsn/}"
(( errors++ ))
done
(( errors == 0 )) || return 1
(( delete_topdsn )) && zfs destroy -r "$topdsn"
fix_zone_rpool_props zone || return 1
#
# If the zone doesn't have /export, set up the /export dataset
# hierarchy. Since this isn't strictly necessary for the zone
# to work, do not fail the attach if creation fails.
#
if ! zonecfg_has_export zone && ! /usr/sbin/zfs list -H -o name \
"${zone.rpool_ds/export}" >/dev/null 2>&1; then
vlog "Creating /export"
if ! zfs create -o mountpoint=/export "${zone.rpool_ds}/export"
then
log "$f_zfs_create" "${zone.rpool_ds}/export"
else
zfs create "${zone.rpool_ds}/export/home" ||
log "$f_zfs_create" "${zone.rpool_ds}/export/home"
fi
fi
# May set EXIT_CODE.
discover_active_be zone || return 1
mount_active_be -C zone || return 1
return 0
}
#
# ds_empty <datasetname>
#
# Returns 0 if the dataset has no snapshots and there are no files or
# directories in the dataset. Assumes the dataset is a filesystem.
#
function ds_empty {
typeset dsn="$1"
# If any snapshots or descendant datasets exist, it's not empty.
typeset -i children
children=$(/usr/sbin/zfs list -Hr -d 1 -t snapshot -o name "$dsn" | \
awk 'END {print NR}')
if (( children > 1 )); then
vlog "Dataset %s has %d snapshots" "$dsn" $(( children - 1 ))
return 1
fi
#
# If it's already mounted, look inside it. Be careful not to descend
# into datasets mounted on it to avoid false positives. Note that we
# ignore mount points for already mounted datasets. This is important
# for the case where an empty BE that contains a separate /var is
# mounted.
#
if [[ "$(zfs list -H -o mounted "$dsn")" == yes ]]; then
typeset mntpt
mntpt=$(zfs list -H -o mountpoint "$dsn") || return 1
# Only look at the first line or two to see if it is empty.
# If it contains only directories, those are likely
# mountpoints or ancestors of mountpoints.
find "$mntpt" -mount -type d -o -print | awk 'NR > 1 { exit 1 }
END { if (NR == 0) { exit 0 } else { exit 1}}'
if (( $? != 0 )); then
vlog "Dataset %s mounted at %s is not empty" "$dsn" \
"$mntpt"
return 1
fi
return 0
fi
# Mount it to see if there are any files in it
typeset dir=$(mktemp -d)
zfs_tmpmount "$dsn" "$dir" || {
vlog "Unable to mount dataset %s on %s" "$dsn" "$dir"
rmdir "$dir" || \
vlog "Unable to clean up temporary directory at %s" "$dir"
return 1
}
typeset contents
contents=$(ls -A "$dir")
umount "$dir" && rmdir $dir || \
vlog "Unable to clean up temporary mount of %s at %s" "$dsn" "$dir"
[[ -n "$contents" ]] && return 1
return 0
}
#
# fix_zone_rpool_props zone
#
# Troll through the zone's rpool dataset and fix the properties on datasets
# such that the BE's are likely to mount.
#
function fix_zone_rpool_props {
typeset -n zone=$1
vlog "Fixing properties on zone datasets"
zfs set zoned=on "${zone.rpool_ds}"
typeset dsn
zfs list -H -o name -d 1 -t filesystem,volume "${zone.rpool_ds}" | \
while read dsn; do
[[ $dsn == "${zone.rpool_ds}" ]] && continue
zfs inherit -r zoned $dsn || return 1
done
typeset be_dsn=
zfs list -H -o name -t filesystem "${zone.ROOT_ds}" | \
while read dsn; do
[[ $dsn == "${zone.ROOT_ds}" ]] && continue
if [[ $dsn != "${zone.ROOT_ds}"/*/* ]]; then
be_dsn=$dsn
else
# Fix mountpoint .../zbe-0/var -> /var
zfs set mountpoint=${dsn#$be_dsn} "$dsn" || return 1
fi
zfs set canmount=noauto "$dsn" || return 1
done
}
#
# attach_datasets -m install_media -t install_type zone
#
# Attaches datasets then performs any required installation tasks.
#
# Options and arguments
#
# -m install_media If install_media is '-', attempt to find a ZBE to
# attach. The selection is performed by the brand's
# discover_active_be() function.
# -t install_type Can be any value accepted by install_image.
# zone zone structure initialized by init_zone.
#
# Globals
# EXIT_CODE Depending on the level of success, may be set to
# ZONE_SUBPROC_UNAVAILABLE (returns 0),
# ZONE_SUBPROC_FATAL (returns 1, datasets partially
# extracted)
#
# Return values
#
# 0 on success, else exits.
# Exits with failure if:
# - zonepath is in the global zone's ROOT dataset.
# - active BE could not be found
# - the ZFS properties on the active BE could not be set
# - the active BE could not be mounted on the zoneroot
#
function attach_datasets {
typeset opt
typeset install_media= inst_type=
while getopts :m:t: opt; do
case $opt in
m) install_media=$OPTARG ;;
t) inst_type=$OPTARG ;;
?) fail_internal "$f_int_bad_opt" "$OPTARG" ;;
esac
done
[[ -z "$install_media" ]] && fail_internal "$f_int_missing_opt" m
[[ -z "$inst_type" ]] && fail_internal "$f_int_missing_opt" t
shift $(( OPTIND - 1 ))
case $# in
0) fail_internal "$f_int_missing_arg" "zone" ;;
1) : ;;
*) fail_internal "$f_int_bad_arg" "$*" ;;
esac
typeset -n zone=$1
# Validate that the zonepath is not in the root dataset.
fail_zonepath_in_rootds "${zone.path.ds}"
#
# Fix mountpoint and other properties for ZBEs detached using the
# old scheme.
#
if ! convert_old_detached_zbes zone; then
# So long as any failed conversions didn't leave anything
# mounted on the zone root, allow the attach to continue.
get_ds_from_path "${zone.root}" && fatal "$f_detach_convert"
fi
if [[ "$install_media" == "-" ]]; then
discover_active_be zone || return 1
elif [[ $inst_type == zbe ]]; then
claim_zbe zone "$install_media" || return 1
inst_type=directory
install_media=-
fi
#
# The zone's datasets are now in place.
#
# Sets EXIT_CODE.
install_image zone "$inst_type" "$install_media"
return 0
}
#
# claim_zbe zone
#
# This function exists here only to give a clear error message in the event
# that attach_datasets() calls brand-specific functionality not appropriate
# to this brand. Brands that support claim_zbe() must define it.
#
# claim_zbe will be called if zoneadm is invoked as
#
# zoneadm -z <zone> attach -z <zbe>
#
# As such, any brand that doesn't support -z should have bailed before calling
# attach_datasets.
#
function claim_zbe {
# If we make it to here, it is programmer error.
fail_internel "%s not defined from this brand" "$0"
}
#
# convert_s10_zonepath_to_zbe dir
#
# This function exists here only to give a clear error message in the event
# that attach_datasets() calls brand-specific functionality not appropriate
# to this brand.
#
function convert_s10_zonepath_to_be {
# Anyone that has called this from common code should have already
# checked the brand.
fail_internal "$s10_zbe_not_supported"
}
#
# tag_candidate_zbes ROOTdsn [be_array_name [curgz_assoc_array_name]]
#
# This generic function only returns the list of zbes found in the specified
# dataset. A brand-specific function may exist for brands that have more
# sophisticated zbe management needs.
#
# 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 Only used by some brands, not implemented in this
# implementation. Intended to return the list zbes
# associated with the current global zone in an
# associative array.
#
# Returns 0 if all went well and at least one zbe exists, else 1.
#
function tag_candidate_zbes {
(( $# < 2 )) && return 0
typeset ROOTdsn=$1
typeset -n bes=$2
typeset dsn
/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")"
done
if (( ${#bes[@]} == 0 )); then
error "$e_no_active_be"
fi
return 0
}
#
# convert_old_detached_zbes zone
#
# Earlier releases left detached datasets mounted on the zone root. This
# function cleans those up, if needed.
#
# Arguments:
#
# zone zone structure initialized by init_zone.
#
# Return:
#
# 0 Nothing unexpected happened. There is no longer anything mounted on
# the zone root
# 1 One or more ZBEs could not be converted.
#
function convert_old_detached_zbes {
typeset -n zone=$1
typeset retval=0
typeset first=true # Is this the first ZBE converted?
#
# Look at each ZBE. Ignore zfs list result, as it is OK to call this
# on a zone that has no ZBEs
#
/usr/sbin/zfs list -H -o name,mountpoint -r -d 1 "${zone.ROOT_ds}" \
2>/dev/null | while IFS=$'\t' read dsn zbe_mntpt; do
# Skip the ROOT dataset
[[ $dsn == "${zone.ROOT_ds}" ]] && continue;
#
# If the ZBE's mount point is already set to /, this doesn't
# look like a detached zbe. Because the currently configured
# zone root may be different than the zone root on some other
# host where this storage may have previously been presented,
# all the references to the zone root are based on the mount
# point of the BE's top level dataset rather than the currently
# configured zone root.
#
[[ $zbe_mntpt == / ]] && continue
log "$m_convert_detached" "$(basename "$dsn")"
#
# Before doing anything that causes unmounts, get a list of
# datasets that exist under the ZBE's top dataset, as well as
# their properties. This will be used when fixing up
# properties later.
#
typeset -a dsslist # indexed array of datasets in dsn
typeset -A dsnbydir # associative array indexed by mntpt
get_datasets -p "$dsn" dsslist || fatal "$f_no_active_ds"
typeset -i i errors=0
for (( i = 0; i < ${#dsslist[@]}; i++ )); do
typeset -n dss=dsslist[$i] # ref to current item
# Ignore things that don't get mounted
[[ ${dss.props[type].value} == filesystem ]] || \
continue
# figure out where it is mounted
mountpt=${dss.props[mountpoint].value}
# Legacy mountpoints do not need to be fixed.
[[ $mountpt == legacy ]] && continue
# Make mountpoint relative to BE root
if [[ $mountpt == "$zbe_mntpt" ]]; then
mountpt=/
elif [[ $mountpt == ${zbe_mntpt}/* ]]; then
mountpt=${mountpt#${zbe_mntpt}}
fi
if [[ -n ${dsnbydir[$mountpt]} ]]; then
error "$e_ds_mnt_multiply_defined" "$mountpt"
(( errors++ ))
mountpt=
fi
if [[ -n $mountpt ]]; then
dsnbydir[$mountpt]=$i
fi
done
#
# Allow progression through all ZBEs, converting those that
# can be converted.
#
if (( errors != 0 )); then
retval=1
continue
fi
if $first; then
first=false
# Set up proper attributes on the ROOT dataset.
typeset rootds rpoolds
init_dataset rpoolds "${zone.rpool_ds}"
init_dataset rootds "${zone.ROOT_ds}"
#
# Unmount the BE so that we can fix up mounts. Note
# that if file systems are mounted with temporary mount
# points, the persistent mountpoint property is hidden.
#
unmount_be zone || return 1
if ! zfs_set zoned=on rpoolds ||
! zfs_set canmount=noauto rootds ||
! zfs_set mountpoint=legacy rootds; then
# If datasets above ZBEs can't be fixed,
# return immediately. zfs_set has already
# given an error message.
return 1
fi
fi
#
# Walk through any remaining datasets and fix mount points
#
typeset -a mntlist
get_sorted_subscripts dsnbydir mntlist
for dir in "${mntlist[@]}"; do
typeset -n ds=dsslist[${dsnbydir[$dir]}]
refresh_dataset ds
if ! fix_ds_mountpoint ds "$dir"; then
retval=1
fi
done
done
return $retval
}
#
# get_datasets [-A] [-t type] [-p] dataset array_name
#
# Updates indexed array (or associative array with -A) named array_name with
# the names of datasets found under the given dataset, including the given
# dataset. Use of an array generated with this function is preferable to "for
# ds in $(zfs list -r ...)" because this is tolerant of dataset names that
# contain spaces.
#
# Example:
#
# typeset -a array
# get_datasets [options] $dataset array
# for ds in "${array[@]}"; do
# ...
# done
#
# Returns 0 on success or 1 if dataset was not found.
# Note: No error messages are printed if no dataset is found.
#
function get_datasets {
#
# Option and argument processing
#
typeset opt var dstype=filesystem assoc
typeset -i getprops=0
while getopts :Apt: opt; do
case $opt in
A) assoc=1 ;;
p) getprops=1 ;;
t) dstype=$OPTARG ;;
?) fail_internal "$f_int_bad_opt" "$OPTARG" ;;
esac
done
shift $(( $OPTIND - 1 ))
[[ -z "$1" ]] && fail_internal "$f_int_missing_arg" dataset
[[ -z "$2" ]] && fail_internal "$f_int_missing_arg" array
typeset dataset="$1"
typeset -n array="$2"
unset array
[[ -n $assoc ]] && typeset -A array
#
# Build the list of datasets
#
typeset ds
typeset -i index=0
/usr/sbin/zfs list -H -o name -t $dstype -r "$dataset" 2>/dev/null \
| while read ds; do
if (( getprops )); then
if [[ -n $assoc ]]; then
array[$ds]=
init_dataset "array[$ds]" "$ds"
else
array[$index]=
init_dataset "array[$index]" "$ds"
fi
else
if [[ -n $assoc ]]; then
array[$ds]=$ds
else
array[$index]="$ds"
fi
fi
(( index++ ))
done
if (( index == 0 )); then
return 1
fi
return 0
}
#
# snapshot_zone_rpool zone snapformat snapname
#
# Creates a recursive snapshot of the specified zone.
#
# Arguments
#
# zone A zone, initialized with init_zone.
# snapformat A printf-friendly string that includes %d in it.
# snapname Upon return, this variable will contain the name of the
# snapshot. This should be the name of the variable, without
# a $.
#
# Globals:
# PATH Must contain /sbin or /usr/sbin.
#
# Return
# 0 Success, $snapname can be trusted
# 1 Fail, $snapname may have garbage
# exit If an internal error occurs
#
function snapshot_zone_rpool {
#
# Option/Argument processing
#
[[ -z "$1" ]] && fail_internal "$f_int_missing_arg" zone
[[ -z "$2" ]] && fail_internal "$f_int_missing_arg" snapformat
[[ -z "$3" ]] && fail_internal "$f_int_missing_arg" snapname
typeset -n zone="$1"
typeset snap_fmt="$2"
typeset -n snapname=$3
#
# Find a name that works for the snapshot
#
typeset rpool_ds=${zone.rpool_ds}
typeset -i i
for (( i=0; i < 100; i++ )); do
snapname=$(printf -- "$snap_fmt" $i)
zfs snapshot -r "$rpool_ds@$snapname" >/dev/null 2>&1 \
&& return 0
done
# No name found, fail
return 1
}
#
# clone_zone_rpool srczone dstzone snapname
#
# Clones the active BE dataset and other non-BE datasets from one zone to
# another. If srczone and dstzone are the same zone, the only effect is that
# a new boot environment is created.
#
# Upon successful return, the specified snapshot will have been marked for
# deffered destruction with 'zfs destroy -d'
#
# Options and Arguments
#
# srczone A zone structure, initialized with init_zone.
# dstzone A zone structure, initialized with init_zone.
# snapname The name of the snapshot (part after @) from
# snapshot_zone_rpool.
#
# Globals:
#
# EXIT_CODE Set to $ZONE_SUBPROC_FATAL if one or more datasets have been
# created but not cleaned up.
# Set to $ZONE_SUBPROC_TRYAGAIN if the operation failed and
# cleanup was successful.
#
# Return
# 0 Success
# 1 Fail
#
function clone_zone_rpool {
typeset -n s="$1" d="$2"
typeset snapname="$3"
typeset -a dslist props
typeset -i propcnt=0
typeset dsname newdsname snap
typeset dss newdss # dataset structure
typeset -i clone_made clone_reqd
typeset -a sl_opt
#
# When cloning a BE within a zone, s and d will refer to the same
# zone. create_active_ds will adjust d.active_ds, which is the
# same as s.active_ds. To be sure that cloning of the source BE's
# child datasets happen, we need to remember what the initial active
# BE was.
#
typeset src_active_ds=${s.active_ds}
#
# In order to see the persistent value of mountpoints, datasets
# must be unmounted.
#
# Note that this causes problems for cloning from snapshots, which is
# still awaiting implementation for non-native brands.
#
unmount_be s || return 1
get_datasets -t filesystem,volume "${s.rpool_ds}" dslist || return 1
if is_system_labeled; then
# On TX, reset the mlslabel upon cloning
set -A sl_opt -- -o mlslabel=none
fi
#
# If the source and destination are in different pools, the datasets
# will be copied with "zfs send | zfs recv". However, this is tricky.
#
# - If the destination zone already exists (i.e. in a different global
# zone BE), there may be ZBE collisions.
# - The source zone may itself be a clone of yet another zone. This
# implies the need for zfs send -[r]c.
# - The datasets within the source zone may have clones (e.g.
# rpool/export/a may have been cloned to rpool/export/b). These
# "internal clones" should be preserved in the destination zone.
#
# To deal with this, the following approach is used. This only applies
# to the cross-pool clone scenario.
#
# - <zpds>: (zonepath dataset) should have already been created by
# zoneadm.
# - <zpds>/rpool/ROOT/*: Datasets that are not part of the active
# ZBE have the snapshot that was specified by the snapname argument
# to this function deleted.
# - <zpds>/rpool: After the snapshot removal described above, the
# source zone's rpool dataset is copied to the destination pool
# at <zpds>/clonetmp.
#
# After the datasets are copied to the destination pool,
# rataionalize_datasets() is used to put them into place.
#
typeset -i xpool=0
[[ ${s.path.ds%%/*} != ${d.path.ds%%/*} ]] && xpool=1
for dsname in "${dslist[@]}"; do
init_dataset dss "$dsname"
newdsname=${dss.name/${s.path.ds}/${d.path.ds}}
snap="${dss.name}@$snapname"
clone_made=0
clone_reqd=0
# zvols are not supported inside of a boot environment
if [[ ${dss.name} == "${s.ROOT_ds}/"* ]]; then
typeset dstype
#
# The following zfs call should only fail if some
# is removing or renaming datasets while this is
# running. If someone is doing that, abort the clone
# operation because it's likely that something will
# break.
#
dstype=$(zfs get -H -o value type "${dss.name}") || \
return 1
if [[ $dstype == volume ]]; then
error "$e_volume_in_bootenv" "${dss.name}"
return 1
fi
fi
#
# Filter through the datasets to throw away snapshots that
# will not be cloned. Set other flags that will be needed
# for post-processing.
#
case "${dss.name}" in
$src_active_ds) # Clone the active boot env
(( xpool )) && continue;
# The BE name need not be the same in src and dst.
# Find the first available BE name by cloning.
# Sets EXIT_CODE.
create_active_ds -s "$snap" d || return 1
(( clone_made=1 ))
newdsname=${dss.name/${src_active_ds}/${d.active_ds}}
;;
$src_active_ds/*) # Clone the active boot env nested ds
(( xpool )) && continue;
# Rejigger the name to match the BE name picked above
newdsname=${dss.name/${src_active_ds}/${d.active_ds}}
(( clone_reqd=1 ))
;;
${s.ROOT_ds}/*) # Do not clone inactive BE
# If we are just creating a new BE in an existing zone,
# don't worry about this dataset.
[[ ${s.name} == ${d.name} ]] && continue
vlog "Not cloning %s: not part of source active BE" \
"$snap"
if (( xpool )); then
#
# Normally we allow deferred destroy of
# snapshots, just in case something (e.g. a
# backup) has a hold on a snapshot. We need
# to be a bit more stringent here, as this
# snapshot must not exist when the 'zfs send'
# starts so as to prevent copying inactive
# ZBEs to the clone zone.
#
zfs destroy $snap || return 1
fi
continue
;;
*) # Clone everything else, if needed.
(( xpool )) && continue;
# If we are just creating a new BE in an existing zone,
# don't worry about this dataset.
[[ ${s.name} == ${d.name} ]] && continue
#
# It is possible that the destination zonepath already
# exists and is at least partially populated due to the
# same zone in some other boot environment. If non-BE
# datasets already exist, reuse them.
#
if /usr/sbin/zfs list "$newdsname" >/dev/null 2>&1; then
vlog "Not cloning %s: dataset already exists" \
"$newdsname"
continue
fi
;;
esac
if (( clone_made == 0 )); then
zfs list "$newdsname" >/dev/null 2>&1
if (( $? == 0 && clone_reqd )); then
error "$e_dataset_exists" "$newdsname"
return 1
fi
vlog "Cloning $snap to $newdsname"
zfs clone "${sl_opt[@]}" "$snap" "$newdsname" ||
return 1
EXIT_CODE=$ZONE_SUBPROC_FATAL
(( clone_made=1 ))
fi
#
# Force the zone's rpool to be zoned and everything else
# to inherit the zoned property.
#
init_dataset newdss "$newdsname"
if [[ $newdsname == "${d.rpool_ds}" ]]; then
zfs_set zoned=on newdss || return 1
else
zfs inherit zoned "$newdsname" || return 1
fi
#
# Locally set properties to match those found on the source.
#
typeset prop
zoned_src=${newdss.props[zoned].source}
for prop in mountpoint canmount; do
if [[ "${dss.props[$prop].source}" == \
@(local|received) ]]; then
zfs_set $prop="${dss.props[$prop].value}" \
newdss || return 1
fi
done
done
if (( xpool )); then
zfs create "${d.path.ds}/clonetmp" || return 1
/usr/sbin/zfs send -rc "${s.rpool_ds}@$snapname" |
/usr/sbin/zfs recv -Fu "${d.path.ds}/clonetmp/rpool"
if (( $? != 0 )); then
error "$e_ds_copy_failed" "${s.rpool_ds}@$snapname" \
"${d.path.ds}/clonetmp/rpool"
zfs destroy -rd "${s.rpool_ds}@$snapname" ||
log "$m_manual_snapshot_cleanup" \
"${s.rpool_ds}@$snapname"
zfs destroy -r "${d.path.ds}/clonetmp" &&
EXIT_CODE=$ZONE_SUBPROC_TRYAGAIN
return 1
fi
rationalize_datasets d "${d.path.ds}/clonetmp/rpool"
if (( $? != 0 )); then
zfs destroy -rd "${s.rpool_ds}@$snapname" ||
log "$m_manual_snapshot_cleanup" \
"${s.rpool_ds}@$snapname"
zfs destroy -r "${d.path.ds}/clonetmp" &&
EXIT_CODE=$ZONE_SUBPROC_TRYAGAIN
return 1
fi
zfs destroy "${d.path.ds}/clonetmp" || \
fatal "Failed to destroy temporary dataset."
# Clean up snapshots on the destination.
zfs destroy -rd "${d.rpool_ds}@$snapname"
fi
# Remount the source zone. Just complain if it can't be remounted
# as the next boot, clone, etc. will succeed even if it's not mounted.
mount_active_be -c s || log "$e_mount1_failed" "${s.name}"
# Mount the new zone
[[ -d ${d.root} ]] || mkdir -m 755 "${d.root}"
mount_active_be -c d || return 1
# Perform a deferred destruction of snapshots. Any snapshot that
# wasn't cloned will be immediately destroyed.
zfs destroy -rd "${s.rpool_ds}@$snapname" || return 1
return 0
}
#
# initializes a new dataset structure
#
# Example:
# typeset dss
# init_dataset dss rpool
# print "${dss.name} mounted at ${dss.props[mountpoint].value}"
#
# After calling init_dataset, dss looks like
#
# dss.name=rpool
# dss.props[mountpoint].value=/rpool
# dss.props[mountpoint].source=local
# ...
#
# Returns 0 if one or more properties were found on the dataset, else 1.
#
function init_dataset {
typeset -n dss="$1"
dss="$2"
dss.name="$2"
dss.props=
typeset -A dss.props
refresh_dataset dss
return $?
}
function refresh_dataset {
typeset -n dss="$1"
typeset -r tab="$(printf "\t")"
typeset prop src val
typeset -i rv=1
/usr/sbin/zfs get -Hp -o property,source,value all "${dss.name}" \
| while IFS=$tab read prop src val; do
dss.props[$prop].value="$val"
dss.props[$prop].source="$src"
(( rv=0 ))
done
(( rv == 0 )) || error "refresh of ${dss.name} failed"
return $rv
}
#
# init_zfs_fs varname [path]
#
# Allocate a new zfs_fs structure
#
function init_zfs_fs {
typeset -n ref=$1
ref=
ref.ds=
# When this variable is set to a value, cache the dataset
function ref.set {
get_ds_from_path "${.sh.value}" ${.sh.name}.ds
}
[[ -n "$2" ]] && ref="$2"
}
#
# init_zone zone zonename [zonepath]
#
# Initialize a zone structure with the following useful members.
#
# brand The zone's brand.
# path The zonepath. See -p option below.
# path.ds The zonepath dataset name. Automatically updated when zonepath
# is updated if a dataset is mounted on the zonepath. This
# member should not be updated directly.
# root Read-only. The zoneroot. Automatically derived from zonepath.
# rpool_ds Read-only. The name of the dataset that contains the zone
# rpool. Automatically derived from path.ds
# ROOT_ds Read-only. The name of the dataset that contains boot
# environments. Automatically derived from path.ds
# new_be_datasets List of datasets that will be created when a new empty
# boot environment is created. For example, if each BE should
# get a separate /var, this list will contain one element: var.
#
# Other members are commonly initialized as needed by other functions. For
# example,
#
# active_ds The name of the dataset that should be mounted on the zone
# root. This is updated by brand-specific get_active_be() and
# set_active_be() functions.
# allowed_bes During attach, this associative array may be initialized
# to signal set_active_be() that it can only choose from these
# boot environments when deciding on which one to make active.
#
# Options and arguments:
#
# zone The name of the variable that will contain the structure.
# zonename The name of the zone.
# zonepath The zonepath. If this option is not provided, the value for
# zonepath will be looked up in the zone configuration, if it
# exists.
#
function init_zone {
#
# Argument and option processing
#
typeset opt
[[ -z "$1" ]] && fail_internal "$f_int_missing_arg" zone
[[ -z "$2" ]] && fail_internal "$f_int_missing_arg" zonename
typeset -n ref=$1
ref=$2
ref.name=$2
shift 2
ref.path=
init_zfs_fs ref.path
# Called after init_zfs_fs to make use of discipline function.
[[ -n $1 ]] && ref.path=$1
#
# Set up remaining members
#
if [[ -z "${ref.path}" ]]; then
set -- $(zonecfg -z "$ref" info zonepath 2>/dev/null)
ref.path=$2
fi
set -- $(zonecfg -z "$ref" info brand 2>/dev/null)
ref.brand=$2
# root is always zonepath/root
typeset -r ref.root=
function ref.root.get {
typeset -n pathref=${.sh.name%.root}.path
.sh.value="$pathref/root"
}
# rpool dataset is always zonepath_ds/rpool
typeset -r ref.rpool_ds=
function ref.rpool_ds.get {
typeset -n pathdsref=${.sh.name%.rpool_ds}.path.ds
if [[ -z "$pathdsref" ]]; then
.sh.value=
else
.sh.value="$pathdsref/rpool"
fi
}
# ROOT dataset is always zonepath_ds/rpool/ROOT
typeset -r ref.ROOT_ds=
function ref.ROOT_ds.get {
typeset -n pathdsref=${.sh.name%.ROOT_ds}.path.ds
if [[ -z "$pathdsref" ]]; then
.sh.value=
else
.sh.value="$pathdsref/rpool/ROOT"
fi
}
# If a new empty BE is created, which datasets should be in it?
# This list may be overridden.
set -A ref.new_be_datasets var
}
#
# bind_legacy_zone_globals zone
#
# Generates the commands to bind legacy globals to a specific zone's members.
# Output should be passed to eval.
#
# Example:
#
# typeset zone=
# init_zone zone z1
# eval $(bind_legacy_zone_globals zone)
#
function bind_legacy_zone_globals {
[[ -z "$1" ]] && fail_internal "$f_int_missing_arg" zone
cat <<-EOF
typeset -n ZONENAME="$1.name";
typeset -n ZONEPATH="$1.path";
typeset -n ZONEPATH_DS="$1.path.ds";
typeset -n ZONEROOT="$1.root";
typeset -n ACTIVE_DS="$1.active_ds";
EOF
}
#
# a_push array_name item ...
#
# Push item(s) onto an index array
#
function a_push {
typeset -n array=$1
typeset -i len=${#array[@]}
shift;
typeset item
for item in "$@"; do
array[len++]="$item"
done
}
#
# get_sorted_subscripts associative_array_name indexed_array_name
#
# The specification for ksh93 is silent about the order of ${!array[@]}.
# This function provides a guaranteed way to get the subscripts of an
# associative array in order.
#
# Example:
# typeset -A a_array
# typeset -a i_array
# a_array[foo/bar]=stuff
# a_array[foo]=otherstuff
# get_sorted_subscripts a_array i_array
# for subscript in "${i_array[@]}"; do
# print "a_array[$subscript] = ${a_array[$subscript]}"
# done
#
function get_sorted_subscripts {
typeset -n a="$1" i="$2"
set -s -- "${!a[@]}"
set -A i "$@"
}
#
# zfs_set property=value dss
#
# Sets the property, or generates a clear message that it can't.
#
# Arguments:
# property=value Passed directly to "zfs set". dss.props[property].* is
# updated.
# dss The name of a dataset structure, initialized with
# init_dataset.
#
# Example:
# typeset dss
# init_dataset dss "zones/z1/rpool"
# zfs_set zoned=on dss
#
# Returns 0 on succes, else 1
#
function zfs_set {
[[ -z "$1" ]] && fail_internal "$f_int_missing_arg" "prop=value"
[[ -z "$2" ]] && fail_internal "$f_int_missing_arg" "dataset"
typeset propval="$1"
typeset -n dss="$2" # dataset structure
[[ -z "${dss.name}" ]] && fail_internal "uninitialized ds"
vlog " setting ZFS property %s on %s" "$propval" "${dss.name}"
/usr/sbin/zfs set "$propval" "${dss.name}" || {
error "$e_zfs_set" "$propval" "${dss.name}"
return 1
}
#
# Update the property on the dataset. Note that setting some
# properties (e.g. zoned) may cause others to change (e.g. mounted), so
# this is imperfect. It is best to use update_dataset when you really
# care about getting an accurate snapshot of the properties.
# update_dataset is not called here to avoid a lot of overhead when
# the caller has many properties to set.
#
dss.props[${propval%%=}].value="${propval#=}"
dss.props[${propval%%=}].source=local
return 0
}
#
# zfs [zfs(1M) args]
#
# On its own, zfs(1M) only tells you that something failed, it doesn't tell
# you anything about the options and arguments that were passed to it. This
# serves as a wrapper around zfs(1M) to be more verbose about what zfs(1M)
# failed to do.
#
# To avoid unnecessary error messages for the times that zfs failures are
# expected (e.g. part of a test condition for existence of a dataset), use
# "/usr/sbin/zfs" instead of "zfs".
#
function zfs {
/usr/sbin/zfs "$@"
typeset -i rv=$?
(( rv == 0 )) || error "$e_cmd_failed" "zfs $*" $rv
return $rv
}
#
# fix_ds_mountpoint dataset mountpoint
#
# Updates the dataset's mountpoint, zoned, and canmount properties so that the
# dataset is mountable in a zone. If the values in the dataset structure
# indicate that no changes are needed, no changes are made. If changes are
# made the dataset structure is refreshed to match the current state of the
# dataset according to zfs(1M).
#
# The dataset must not be mounted when this function is called.
#
# Arguments:
# dataset The name of a dataset structure, initilized with init_dataset.
# mountpoint The new value for the mountpoint property. For zoned datasets
# this should be relative to the zone root.
#
# Returns 0 on succes, else 1.
#
function fix_ds_mountpoint {
case $# in
0|1) fail_internal "$f_int_missing_arg" "dataset or dir" ;;
2) : ;;
*) fail_internal "$f_int_bad_arg" "$*" ;;
esac
typeset -n dss="$1"
typeset mountpoint="$2"
typeset -i dirty=0
#
# If nothing needs to be fixed, don't fix it.
#
if [[ "${dss.props[mountpoint].value}" == "$mountpoint" && \
"${dss.props[zoned].value}" == on && \
"${dss.props[zoned].source}" == inherited* && \
"${dss.props[canmount].value}" == noauto ]]; then
#
# Currently we can only verify mountpoints if a dataset is not
# mounted. The lack of ability to get the persistent value of
# mountpoint from zfs(1M) is a bit of a problem:
#
# - If it is mounted with source of "temporary" we can't get
# to the persistent value to be sure that it will be mounted
# at the right place next time.
# - If the mounted property has a source of "-" and it is
# zoned, that means one of two things:
#
# i) The zone virtual platform must be up and it is mounted
# relative to the zone root. The mountpoint property
# that is seen from the global zone includes the
# zone root. This function doesn't expect to be called
# to fix a mountpoint in a running zone.
# ii) It is not mounted and the mountpoint can be trusted.
#
[[ "${dss.props[mounted].value}" == no ]] && return 0
#
# It is mounted, making it impossible or unwise to muck with
# the mount point.
#
error "$e_no_mntpt_change_for_mounted" "${dss.name}"
return 1
fi
#
# We can't fix the mountpoint on a mounted dataset without causing
# an unmount.
#
if [[ "${dss.props[mountpoint].value}" == mounted ]]; then
error "$e_no_mntpt_change_for_mounted" "${dss.name}"
return 1
fi
vlog "$m_fix_ds_mountpoint" "${dss.name}" \
"${dss.props[mountpoint].value}" "$mountpoint"
# Fix the zoned property if it is not inherited.
if [[ "${dss.props[zoned].source}" != inherited* ]]; then
if [[ ${dss.props[zoned].value} != on ]]; then
vlog "Inheriting property zoned on ${dss.name}"
zfs inherit zoned "${dss.name}" || return 1
fi
(( dirty=1 ))
fi
#
# Verify that the value is now zoned. If the parent dataset wasn't
# zoned then this dataset is not zoned and a basic assumption of the
# zone dataset structure is broken. Note that we aren't using the
# cached value in $dss because the zoned property may have changed
# above.
#
typeset zonedval
zonedval=$(zfs get -H -o value zoned "${dss.name}") || return 1
if [[ $zonedval != on ]]; then
error "$e_parent_not_zoned" "${dss.name}"
return 1
fi
# All BE datasets should have canmount=noauto
typeset cm="${dss.props[canmount].value}"
if [[ "$cm" != noauto ]]; then
zfs_set canmount=noauto dss || return 1
(( dirty=1 ))
fi
#
# Now that we are sure that mucking with the mountpoint won't cause
# mounts in the global zone, update the mountpoint property.
#
if [[ "${dss.props[mountpoint].value}" != "$mountpoint" ]]; then
zfs_set mountpoint="$mountpoint" dss || return 1
(( dirty=1 ))
fi
if (( dirty )); then
refresh_dataset dss || return 1
fi
return 0
}
#
# zfs_tmpmount dataset mountpoint
#
# Mount the specified dataset using a ZFS temporary mount. The mountpoint
# is created if necessary. The zfs mountpoint property must not be "legacy"
# or "none" and the canmount property must not be "no" for this to succeed.
#
# Special protection against devices files is needed for datasets mounted by
# the global zone that are delegated to non-global zones. The temporary
# mount option "nodevices" overrides the "devices" zfs property. This
# provides protection that wouldn't be afforded by simply setting the zfs
# "devices" property to "off". This is not a concern for datasets that are
# mounted from within the zone because the zone=<zonename> property implies
# that device special files are disallowed.
#
# Arguments:
# dataset The name of a dataset. This may be a string or a dataset
# structure initialized with init_dataset.
# mountpoint The place where it gets mounted.
#
# Returns 0 on success, else 1.
#
function zfs_tmpmount {
typeset dsname="$1"
typeset dir="$2"
vlog "Mounting $dsname at $dir with ZFS temporary mount"
[[ -d "$dir" ]] || mkdir -m 755 -p "$dir"
zfs mount -o nodevices,mountpoint="$dir" "$dsname" || {
error "$e_temp_mount_failed" "$dsname" "$dir"
return 1
}
return 0
}
#
# mount_be_ds -r root [-m mountpoint] dataset_structure_name
#
# Uses the ZFS Temporary Mount feature to mount the specified dataset.
#
# -m mountpoint The place where the dataset will be mounted,
# relative root option. If this value and
# the mountpoint property in the dataset are
# in conflict, the dataset will be modified to
# have its mountpoint set to this value.
# -r root The root of the the zone. This plus mountpoint
# (or mountpoint property on dataset) determines
# where the mount will occur. Required.
# dataset_structure_name The name of a structure initialized with
# init_dataset. Before any action is taken,
# the properties on this dataset will be
# refreshed to ensure they match the current
# state of the system.
#
function mount_be_ds {
typeset root= mntpt= opt=
#
# Argument processing
#
while getopts :m:r: opt; do
case "$opt" in
m) mntpt=$OPTARG ;;
r) root=$OPTARG ;;
?) fail_internal "$f_int_bad_opt" "$OPTARG" ;;
esac
done
shift $(( OPTIND - 1 ))
[[ -z "$root" ]] && fail_internal "$f_int_missing_opt" r
[[ -z "$1" ]] && fail_internal "$f_int_missing_arg" dataset
typeset -n dss=$1
shift
(( $# == 0 )) || fail_internal "$f_int_bad_arg" "$*"
vlog "Preparing to mount %s at %s%s" "${dss.name}" "${root}" "${mntpt}"
#
# Real work
#
# Verify that all the properties are OK prior to mounting
refresh_dataset dss || return 1
#
# Temporary mounts hide the persistent value of the mountpoint
# property. As such, assume that if it is mounted somewhere under
# $root, all is well.
#
if [[ "${dss.props[mountpoint].source}" == temporary ]]; then
if [[ -z "$mntpt" \
&& "${dss.props.[mountpoint].value}" == "$root"/* ]]; then
return 0
fi
# Ask zfs for an exact match
[[ "$(get_ds_from_path "${root}${mntpt}")" \
== "${dss.name}" ]] && return 0
fi
#
# Fix up the mountpoint, zoned, and canmount properties
#
if [[ -z "$mntpt" ]]; then
mntpt="${dss.props[mountpoint].value}"
fi
fix_ds_mountpoint dss "$mntpt" || {
error "$e_mount1_failed" "${dss.name}"
return 1
}
# Use zfs(1M) to mount it.
zfs_tmpmount "${dss.name}" "${root}${mntpt}" || return 1
return 0
}
#
# mount_be -c root_dataset mountpoint
#
# Mounts the specified boot environment at the specified mountpoint.
# In addition to mounting the root dataset, child datasets that have
# have canmount=noauto and a path as a mountpoint are mounted.
#
function mount_be {
typeset -A dsa
typeset -A mnt
typeset line
typeset -i dscnt=0
typeset dir
typeset -i mount_children=0
typeset zfslist_r # -r for zfs list, if needed
typeset extra_vlog=
while getopts :c opt; do
case $opt in
c) (( mount_children=1 ))
zfslist_r=-r
extra_vlog=" (including child datasets)"
;;
?) fail_internal "$f_int_bad_opt" "$OPTARG"
;;
esac
done
shift $(( OPTIND - 1 ))
typeset rootds="$1" root="$2"
vlog "Mounting boot environment in $rootds at ${root}${extra_vlog}"
#
# Find all of the datasets under the root dataset, store them in the dsa
# associative array. stderr and return value from zfs list command are
# ignored. Instead, there is a check to be sure that the root dataset
# was added in the body of the while loop.
#
zfs list -H -o name -t filesystem $zfslist_r "$rootds" 2>/dev/null \
| while read line; do
dsa["$line"].dss=
init_dataset "dsa[$line].dss" "$line"
# We know where rootds needs to be mounted, so skip checks.
[[ $line == "$rootds" ]] && continue
#
# Be sure mountpoint and canmount are OK. Informational
# messages are given rather than errors to align with
# behavior in beadm.
#
typeset dir="${dsa[$line].dss.props[mountpoint].value}"
typeset cm="${dsa[$line].dss.props[canmount].value}"
if [[ "$dir" == legacy || "$dir" == none ]]; then
log "$m_not_mounting_mountpoint" "$d" "$dir"
unset dsa[$line]
continue
fi
# If canmount=on it will be set to noauto when it is mounted.
if [[ $cm == off ]]; then
log "$m_not_mounting_canmount" "$d" "$cm"
unset dsa[$line]
continue
fi
done
#
# Be sure the root dataset was found
#
if [[ ${dsa[$rootds].dss.name} != "$rootds" ]]; then
error "$e_no_such_dataset" "$rootds"
return 1
fi
#
# In most circumstances, only the root gets mounted. However, if the
# zone is intended to be left in the installed state, such as with
# sysboot or halt, the entire BE is mounted.
#
if (( mount_children == 0 )); then
if [[ ${dsa[$rootds].dss.props[mountpoint].value} != "$root" \
|| ${dsa[$rootds].dss.props[mounted].value} != yes ]]; then
mount_be_ds -r "$root" -m / dsa[$rootds].dss || return 1
else
vlog "${.sh.fun} $rootds already on $root"
fi
return 0
fi
#
# Mount the file systems.
#
typeset -a umount_on_error
typeset -i errors=0
get_sorted_subscripts dsa subs
for dir in "${subs[@]}"; do
mount_be_ds -r "$root" "dsa[$dir].dss" || {
(( errors++ ))
break
}
a_push umount_on_error "$dir"
done
# If no errors, we are done.
if (( errors == 0 )); then
return 0
fi
# The mount process was not error-free. Unmount whatever was mounted.
for (( i = ${#umount_on_error[@]} - 1 ; i >= 0 ; i-- )); do
zfs unmount "${umount_on_error[i]}" \
|| error "e_unmount_failed" "${umount_on_error[i]}"
done
return 1
}
#
# mount_active_be [-b bootenv] [-c] zoneref
#
# Mounts the active boot environment at the zoneroot.
#
# Arguments and Options:
#
# -b bootenv Set this boot environment as the active boot environment
# before mounting.
# -c Mount the complete dataset, including children of the root
# dataset. If the wrong BE is currently mounted, it will be
# unmounted first.
# -C Similar to -c, but the BE that is already partially or fully
# mounted is assumed to be the correct one. No attempt will
# be made to unmount the mounted zone root. This should be used
# when we know that the right BE was already partially mounted
# and we just need the child datasets to be mounted too.
# zoneref A zone structure initialized by init_zone
#
# Returns 0 on success, else 1
#
function mount_active_be {
typeset mount_children= unmount_children= be= opt
while getopts :b:cC opt; do
case $opt in
b) be=$OPTARG ;;
c) mount_children=-c ;;
C) mount_children=-c
unmount_children=-C
;;
?) fail_internal "$f_int_bad_opt" "$OPTARG" ;;
esac
done
shift $(( OPTIND - 1 ))
typeset -n zone="$1"
if [[ -n $be ]]; then
set_active_be zone "$be" || return 1
elif [[ -z "${zone.active_ds}" ]]; then
get_active_be zone || return 1
fi
#
# The unmount is required until such a time as mount_be is able to
# get the persistent mountpoint property out of zfs(1M).
#
unmount_be $unmount_children zone || return 1
mount_be $mount_children "${zone.active_ds}" \
"${zone.root}" || return 1
return 0
}
#
# unmount_be zoneref
#
# Unmounts the zone mounted at zoneref.root. This is expected to be called
# at a time when the zone has no active virtual platform. As such, it should
# only have local mounts. In the case of a zone being halted, this function
# should have no work to do. During sysboot, attach, and clone, this function
# is likely to unmount all datasets in a BE.
#
# Options and Arguments:
#
# -C Only unmount ZFS datasets that are children of the dataset
# mounted on the zone root.
# zoneref A zone structure initialized by init_zone
#
# Returns 0 if everything under the zoneroot was unmounted, else 1
#
function unmount_be {
typeset -a mounts
typeset tab=$(printf "\t")
typeset dev dir fstype junk opt
typeset -i zfs_children_only=0
while getopts :C opt; do
case $opt in
C) (( zfs_children_only=1 )) ;;
?) fail_internal "$f_int_bad_opt" $opt ;;
esac
done
shift $(( OPTIND - 1 ))
typeset -n zone=$1
typeset root=${zone.root}
[[ -z "$root" ]] && fail_internal "zoneroot is null"
if (( zfs_children_only )) && [[ -z "${zone.active_ds}" ]]; then
get_active_be zone || return 1
fi
# Read /etc/mnttab
while IFS=$tab read dev dir fstype junk; do
set -- $line
if (( zfs_children_only )); then
[[ $fstype != zfs ]] && continue
[[ $dir == "$root" ]] && continue
# Do not umount added fs resources
[[ $dev == ${zone.active_ds}/* ]] || continue
fi
if [[ "$dir" == "$root" ]]; then
a_push mounts "$dir"
continue
fi
if [[ "$dir" == "$root"/* ]]; then
a_push mounts "$dir"
continue
fi
done < /etc/mnttab
(( ${#mounts[@]} == 0 )) && return 0
# Sort
set -s -- "${mounts[@]}"
set -A mounts "$@"
# Unmount in reverse sorted order
typeset -i i rv=0
for (( i = ${#mounts[@]} - 1; i >= 0; i-- )); do
vlog "Unmounting ${mounts[i]}"
#
# If a graceful umount fails, it may be an indication that some
# global zone process is still active on the file system's
# contents. We should allow the umount to fail so that we
# don't pull the rug out from a process that may be doing
# delicate operations in the zone's BE.
#
umount "${mounts[i]}" || {
rv=1
error "$e_unmount_failed" "${mounts[i]}"
}
done
return $rv
}
#
# detach_zone zone
#
# Unmount the ZBE then copy the zone configuration to SUNWdetached.xml.
#
# Arguments:
#
# zone A zone structure initialized with init_zone
#
# Returns:
# 0 on success, exits with $ZONE_SUBPROC_TRYAGAIN on error
#
function detach_zone {
typeset -n zone=$1
unmount_be zone || fail_tryagain "$f_unmount_be"
cp /etc/zones/${zone.name}.xml ${zone.path}/SUNWdetached.xml
return 0
}
#
# Determines if any part of the zone's /export hierarchy comes from
# fs or dataset resources in the zone configuration.
#
# Returns 0 (true) if export is provided by the zone configuration.
#
function zonecfg_has_export {
typeset -n zone=$1
typeset dir
for dir in $(zonecfg -z "${zone.name}" info fs | \
nawk '$1 == "dir:" { print $2 }' | LC_ALL=C sort); do
if [[ $dir == /export || $dir == /export/* ]]; then
log "$m_manual_export_migrate" "${zone.root}/export" \
"zonecfg fs $dir"
return 0
fi
done
typeset dsname line
zonecfg -z "${zone.name}" info dataset | \
nawk '$1 == "name:" { print $2}' | \
while read line; do
zfs list -H -o name,mountpoint "$line" 2>/dev/null
done | while read dsname dir; do
if [[ $dir == /export || $dir == /export/* ]]; then
log "$m_manual_export_migrate" "${zone.root}/export" \
"zonecfg dataset $dsname"
return 0
fi
done
return 1
}
#
# umount_destroy_rmdir dirname dsname
#
# Cleans up the specified mount, destroys the dataset, and removes the mount
# point. Calls error() with a useful message on first failure and returns 1.
# Returns 0 on success.
#
function umount_destroy_rmdir {
typeset dir=$1
typeset dsname=$2
umount "$dir" || {
error "$e_unmount_failed" "$dir"
return 1
}
zfs destroy "$dsname" || {
error "$e_zfs_destroy" "$dsname"
return 1
}
rmdir "$dir" || {
error "$f_rmdir" "$dir"
return 1
}
return 0
}
#
# During p2v and v2v, move any contents from the BE's /export to the non-BE
# /export. Same goes for /export/home.
#
# If existing contents are found in .../rpool/export or .../rpool/export/home,
# a message is displayed indicating manual migration is required and a the
# return value is 0. If migration is attempted but unsuccessful, the return
# value is 1. If migration is successful, the return value is 0.
#
# In the event that the zone's existing /export and/or /export/home exists but is
# not a directory (e.g. is a symlink) the corresponding dataset(s) are destroyed
# so as to not be in the way for any migration. If /export exists and is not
# a directory, both the export and export/home datasets are destroyed and no
# migration takes place.
#
function migrate_export {
typeset -n zone=$1
typeset dir
typeset -i destroy_export=0 destroy_exporthome=0
# If /export doesn't exist or is empty there is no work to do.
[[ -d ${zone.root}/export ]] || return 0
[[ -z "$(ls -A ${zone.root}/export)" ]] && return 0
#
# If zonecfg fs or dataset resources specify a file system to mount
# anywhere under /export, assume that they don't want /export migrated.
#
zonecfg_has_export zone && return 0
#
# Mount /export and /export home under a temporary directory.
# Note that it the zone's export dataset is mounted at $dir/export
# not at $dir to make it so that mv(1) can be used for a very simple
# migration process.
#
dir=$(mktemp -d)
zfs_tmpmount "${zone.rpool_ds}/export" "$dir/export" || {
rmdir $dir
error "$e_export_migration_failed"
return 1
}
zfs_tmpmount "${zone.rpool_ds}/export/home" "$dir/export/home" || {
umount "$dir/export"
rmdir "$dir/export" "$dir"
error "$e_export_migration_failed"
return 1
}
#
# Check to see if the existing .../rpool/export dataset hierarchy
# contains anything. If so, don't clobber it.
#
(cd "$dir" && find export) | \
nawk '$0 !~ /^(export|export\/home)$/ {exit 1}' || {
umount "$dir/export/home"
umount "$dir/export"
rmdir "$dir/export" 2>/dev/null
rmdir "$dir"
log "$m_manual_export_migrate" "${zone.root}/export" \
"${zone.rpool_ds}/export"
return 0
}
#
# It is possible that /export and/or /export/home exsists but is not a
# directory. If so, the corresponding dataset should be deleted so
# that migration (if any) doesn't choke trying to put a directory on
# top of a symlink or other non-directory.
#
if [[ -h "${zone.root}/export/home" ]]; then
(( destroy_exporthome = 1 ))
elif [[ -e "${zone.root}/export/home" && \
! -d "${zone.root}/export/home" ]]; then
(( destroy_exporthome = 1 ))
fi
if [[ -h "${zone.root}/export" ]]; then
(( destroy_export = 1 ))
(( destroy_exporthome = 1 ))
elif [[ -e "${zone.root}/export" && ! -d "${zone.root}/export" ]]
then
(( destroy_export = 1 ))
(( destroy_exporthome = 1 ))
fi
if (( destroy_exporthome )); then
umount_destroy_rmdir "$dir/export/home" \
"${zone.rpool_ds}/export/home" || {
error "$e_export_migration_failed"
return 1
}
fi
if (( destroy_export )); then
umount_destroy_rmdir "$dir/export" \
"${zone.rpool_ds}/export" || {
error "$e_export_migration_failed"
return 1
}
# Nothing left to migrate to. Finish cleanup and return.
rmdir $dir
return 0
fi
# Odd quoting below to prevent SCCS keyword expansion & warnings.
typeset bkup
bkup=${zone.root}/export.backup.$(TZ=UTC date +%Y""%m""%dT""%H""%M""%SZ)
if [[ -e "$bkup" ]]; then
#
# There's no legitimate reason that we should have a
# collision - this is likely an attack by the provider
# of the archive.
#
umount "$dir/export/home"
umount "$dir/export"
rmdir "$dir/export" 2>/dev/null
rmdir "$dir"
fatal "$f_backup_dir_exists" "$bkup"
fi
log "$m_migrating_data" "$(zfs list -H -o name "${zone.root}/export")" \
"$(zfs list -H -o name "$dir/export")"
#
# cpio insists on printing the number of blocks transferred on stderr.
# This output only serves to confuse so prevent it from being displayed
# if cpio is otherwise successful.
#
typeset cpioout rv
cpioout=$( cd "${zone.root}" && find export | \
LC_ALL=C cpio -pdumP@/ "$dir" 2>&1)
rv=$?
if [[ $cpioout != [0-9]*" blocks" || $rv != 0 ]]; then
print -- "$cpioout"
(( destroy_exporthome )) || umount "$dir/export/home"
umount "$dir/export"
rmdir "$dir/export" 2>/dev/null
rmdir "$dir"
error "$e_export_migration_failed"
return 1
fi
mv ${zone.root}/export "$bkup"
log "$m_backup_saved" /export "/$(basename "$bkup")"
# Migration was successful. Even if a umount fails, still return 0.
(( destroy_exporthome )) || umount "$dir/export/home"
umount "$dir/export"
rmdir "$dir/export" 2>/dev/null
rmdir "$dir"
return 0
}
#
# migrate_rpool zone
#
# Migrates the contents of the /rpool directory out of the ZBE into the rpool
# dataset.
#
# Arguments
#
# zone A zone data structure initialized with init_zone.
#
# Returns
#
# 0 on success, 1 on failure.
#
function migrate_rpool {
typeset -n zone=$1
typeset dir
[[ -d ${zone.root}/rpool ]] || return 0
[[ -z "$(ls -A ${zone.root}/rpool)" ]] && return 0
dir=$(mktemp -d)
zfs_tmpmount "${zone.rpool_ds}" "$dir/rpool" || {
rmdir $dir/rpool >/dev/null 2>&1
rmdir $dir
error "$e_rpool_migration_failed"
return 1
}
typeset bkup
bkup=${zone.root}/rpool.backup.$(TZ=UTC date +%Y""%m""%dT""%H""%M""%SZ)
if [[ -e $bkup ]]; then
#
# There's no legitimate reason that we should have a
# collision - this is likely an attack by the provider
# of the archive.
#
umount $dir/rpool
rmdir $dir/rpool
rmdir $dir
fatal "$f_backup_dir_exists" "$bkup"
fi
log "$m_migrating_data" "$(zfs list -H -o name "${zone.root}/rpool")" \
"$(zfs list -H -o name "$dir/rpool")"
#
# cpio insists on printing the number of blocks transferred on stderr.
# This output only serves to confuse so prevent it from being displayed
# if cpio is otherwise successful.
#
typeset cpioout rv
cpioout=$( cd "${zone.root}" && find rpool | \
LC_ALL=C cpio -pdumP@/ "$dir" 2>&1)
rv=$?
if [[ $cpioout != [0-9]*" blocks" || $rv != 0 ]]; then
print -- "$cpioout"
umount "$dir/rpool"
rmdir "$dir/rpool"
rmdir "$dir"
error "$e_rpool_migration_failed"
return 1
fi
mv ${zone.root}/rpool "$bkup"
log "$m_backup_saved" /rpool "/$(basename "$bkup")"
# Migration was successful. Even if a umount fails, still return 0.
umount "$dir/rpool"
rmdir "$dir/rpool"
rmdir "$dir"
return 0
}
CLUSTER_HOOK="/usr/cluster/lib/sc/zc_handler"
function call_cluster_hook {
if [[ -f $CLUSTER_HOOK ]]; then
$CLUSTER_HOOK "$@"
return $?
else
return $ZONE_SUBPROC_OK
fi
}
#
# start_log zone subcommand [command line]
#
# Sets up the environment for logging functions to log to a log file. The log
# is created as /var/log/zones/zoneadm.<timestamp>.<zone>.<subcommand>.
# However, see the effect of the ZONEADM_LOGFILE environment variable.
#
# Example:
#
# init_zone zone "$1" "$2"
# start_log zone attach "$0" "$@"
#
# Arguments
#
# zone A zone structure initialized with init_zone.
# subcommand The subcommand of zoneadm that is running.
# command line The command line of the script calling this function. This
# array will be logged to indicate what is running
#
# Globals
#
# LOGFILE Set to the name of the log file.
# ZONEADM_LOGFILE If this environment variable is set to a writable file,
# logging is done to that file. If this environment
# variable did not already refer to a writable file,
# it is set to the value of LOGFILE. Thus, in situations
# where one brand script calls another (perhaps via
# another invocation of zoneadm), there is a single
# log file created.
# LOGGING_COMMAND A string that represents the command line. Used by
# finish_log().
# start_log().
# FINISH_LOG Set to true or false. If ZONEADM_LOGFILE is set
# to a writable file when start_log() is called,
# FINISH_LOG is set to false. This affects behavior
# of finish_log().
# stderr (fds 2 & 3) stderr is copied to file descriptor 3, then stderr
# is redirected for append to $LOGFILE.
#
function start_log {
typeset subcommand zonename
(( $# < 2 )) && fail_internal "Too few arguments to start_log"
typeset -n zone=$1
typeset subcommand=$2
typeset zonename=${zone.name}
shift 2
LOGGING_COMMAND="$*"
[[ -z $zonename ]] && fail_internal "zone structure not initialized"
if [[ -n $ZONEADM_LOGFILE && -f $ZONEADM_LOGFILE &&
-w $ZONEADM_LOGFILE ]]; then
#
# Some other script that called this one already set things
# up. Continue to use existing $ZONEADM_LOGFILE as $LOGFILE.
#
FINISH_LOG=false
LOGFILE=$ZONEADM_LOGFILE
else
if [[ ! -d /var/log/zones ]]; then
mkdir -m 755 /var/log/zones ||
fatal "$f_mkdir" /var/log/zones
fi
FINISH_LOG=true
#
# Use a subshell to set noclobber, then try to create
# a unique log file without the ugly file name generated
# by mktemp.
#
typeset name timestamp
timestamp=$(TZ=GMT date +"%Y""%m""%dT""%H""%M""%"SZ)
name=/var/log/zones/zoneadm.$timestamp.$zonename.$subcommand
LOGFILE=$(set -o noclobber
try=$name
i=0
while (( i++ < 100 )); do
exec 2>$try
if (( $? == 0 )); then
print "$try"
break
fi
try=$name.$try
done)
[[ -z $LOGFILE || ! -f $LOGFILE ]] &&
fail_internal "Cannot create unique log file"
ZONEADM_LOGFILE=$LOGFILE
fi
#
# Before redirecting stderr to $LOGFILE, save a copy of it to fd 3
# so that it can be restored by finish_log().
#
exec 3>&2
exec 2>>$LOGFILE
vlog "==== Starting: %s ====" "$*"
[[ $FINISH_LOG == true ]] && log "$m_log_progress_to" "$LOGFILE"
}
#
# The following export is performed in the global scope to force the
# environment variable to exist. If it is exported from within the function,
# it just puts it into the global scope and not into the environment.
#
export ZONEADM_LOGFILE
#
# finish_log zone [logfilevar]
#
# Finish logging started by start_log().
#
# Arguments
#
# zone A zone structure initialized with init_zone.
# logfilevar The name of a variable to contain the name of the
# resulting log file.
#
# Globals
#
# LOGFILE The name of the log file. Unset before return.
# ZONEADM_LOGFILE If FINISH_LOG is true, this environment variable
# is unset before return.
# LOGGING_COMMAND The command that is being logged. Set by start_log().
# FINISH_LOG If set to true and ${zone.root}/var/log exists as a
# directory, $LOGFILE is copied to the same path in the
# zone. If necessary, /var/log/zones is created in the
# zone.
#
function finish_log {
typeset -n zone=$1
typeset newlog
typeset logfile=$LOGFILE
[[ -z $LOGFILE ]] && return
vlog "==== Completed: %s ====" "$LOGGING_COMMAND"
# Stop logging to $LOGFILE and restore stderr.
exec 2<&3
exec 3<&-
unset LOGFILE
[[ $FINISH_LOG == true ]] || return
#
# If the operation ended such that there is no zone mounted
# (e.g. uninstall, failed install, etc.) do not attempt to copy
# it into the zone.
#
[[ -z ${zone.root} ]] && return
[[ -d ${zone.root}/var/log ]] || return
safe_dir /var
safe_dir /var/log
if [[ ! -d ${zone.root}/var/log/zones ]]; then
# If the log file can't be safely copied into the zone,
# give up on copying it there.
if [[ -e ${zone.root}/var/log/zones ||
-h ${zone.root}/var/log/zones ]]; then
error "$e_baddir" /var/log/zones
return
fi
mkdir -m 755 ${zone.root}/var/log/zones ||
fatal "$f_mkdir" ${zone.root}/var/log/zones
fi
safe_copy "$logfile" "${zone.root}${logfile}"
if [[ -n $2 ]]; then
typeset -n out=$2
out=$logfile
fi
log "$m_log_copied_to" "${zone.root}$logfile"
}
#
# pin_datasets topds
#
# Keeps track of which decendants of topds exist at a point in time by
# tracking the guid property of each dataset. Note that as datasets
# are renamed, the guid stays the same and as such datasets stay pinned
# across renames. See also delete_unpinned_datasets() and unpin_datasets().
#
# Arguments
#
# topds The name of the top dataset to pin. Most likely this is
# a zonepath dataset.
#
# Globals
#
# DATASET_PINS An associative array mapping the guid property
# to the name of pinned dataset. The name is
# not actually important - it is an arbitrary
# value assigned to the array element.
#
# Return
# 0 Success - at least one dataset is pinned.
# 1 Failure
#
unset DATASET_PINS
typeset -A DATASET_PINS
function pin_datasets {
typeset topdsn=$1
typeset guid dsn
typeset retval=1
vlog "Pinning datasets under %s" "$topdsn"
/usr/sbin/zfs get -Hrp -o value,name guid "$topdsn" 2>/dev/null |
while IFS=$'\t' read guid dsn; do
vlog "Pinning %s" "$dsn"
DATASET_PINS[$guid]="$dsn"
retval=0
done
return $retval
}
#
# unpin_datasets topds
#
# Undoes the work of pin_datasets() for all datasets that are descendants
# of topds.
#
# Arguments
#
# topds The name of the top dataset to pin. Most likely this is
# a zonepath dataset.
#
# Globals
#
# DATASET_PINS An associative array mapping the guid property
# to the name of pinned dataset.
#
# Return
# 0 Success
# 1 Failure - nothing was unpinned
#
function unpin_datasets {
typeset topdsn=$1
typeset retval=1
vlog "Unpinning datasets under %s" "$topdsn"
/usr/sbin/zfs get -Hrp -o value,name guid "$topdsn" 2>/dev/null |
while IFS=$'\t' read guid dsn; do
[[ -z ${DATASET_PINS[$guid]} ]] && continue
vlog "Unpinning %s" "$dsn"
unset DATASET_PINS[$guid]
retval=0
done
return $retval
}
#
# delete_unpinned_datasets topds
#
# Deletes each dataset under topds that is not pinned by pin_datasets(). As a
# safety measure, if topds is not pinned, nothing will be deleted and 1 will be
# returned.
#
# Note: Before destroying any datasets, the current working directory is
# changed to / to avoid EBUSY on dataset unmount.
#
# Arguments
#
# topds The name of the top dataset to pin. Most likely this is
# a zonepath dataset.
#
# Globals
#
# DATASET_PINS An associative array mapping the guid property
# to the name of pinned dataset.
#
# Return
# 0 Success
# 1 Failure or partial failure.
#
function delete_unpinned_datasets {
typeset topdsn=$1
typeset -i ispinned=0
vlog "Destroying datasets under %s that are not pinned" "$topdsn"
# Avoid EBUSY during the umount(2) performed by 'zfs destroy'.
cd /
typeset name guid
typeset -A todestroy
/usr/sbin/zfs get -Hrp -o value,name guid "$topdsn" 2>/dev/null |
while IFS=$'\t' read guid name; do
# Be sure it is pinned before allowing anything to be deleted.
if [[ $name == $topdsn ]]; then
if [[ -z ${DATASET_PINS[$guid]} ]]; then
error "$e_not_pinned" "$topdsn"
return 1
fi
(( ispinned=1 ))
fi
# Do not destroy pinned datasets.
[[ -n ${DATASET_PINS[$guid]} ]] && continue
# To minimize the chance of snapshot collisions during clone
# promotion, remove all snapshots that we can ASAP.
if [[ $name == *@* ]]; then
/usr/sbin/zfs destroy "$name" >/dev/null 2>&1
if (( $? == 0 )); then
vlog "Destroyed unpinned snapshot %s" "$name"
continue
fi
else
# No need to add snapshots to the todestroy list.
# They will get cleaned up when the clones are
# destroyed.
todestroy["$name"]=$guid
fi
done
# If no work to be done, return immediately.
(( ${#todestroy[@]} == 0 )) && return 0
#
# Be sure that if there is anything to do that it is pinned. If
# we detect that it is not pinned at this point, it means there is
# a logic error.
#
(( ispinned == 0 )) && fail_internal "$e_not_pinned" "$topdsn"
# If the uninstall functions aren't already loaded, load them
typeset -f destroy_zone_dataset >/dev/null 2>&1 ||
. /usr/lib/brand/shared/uninstall.ksh
#
# Destroy the datasets in reverse order. Because of clones that
# exist within the received datasets, there may be some failures.
# Don't worry about that so long as each iteration makes progress.
#
while (( ${#todestroy[@]} != 0 )); do
typeset progress=false
typeset -a names
typeset -i i
get_sorted_subscripts todestroy names
for (( i=$((${#names[@]} - 1)); i >= 0; i-- )); do
name=${names[$i]}
guid=${todestroy[$name]}
#
# This is icky: destroy_zone_dataset exits the process
# if it is not successful. If it succeeds, it does not
# necessarily return 0 and as such any return is
# considered a successful return. To avoid this
# pitfall a subshell is used. First, any existing exit
# handler is disabled for the subshell. If
# destroy_zone_dataset fails, the return code from the
# subshell will be the exit value specified within
# uninstall.ksh. If it succeeds, the return code from
# the subshell will be 0.
#
(
trap - EXIT
destroy_zone_dataset "$name" >/dev/null
exit 0
) || continue
vlog "Destroyed unpinned dataset %s" "$name"
progress=true
unset todestroy[$name]
unset DATASET_PINS[$guid]
done
if [[ $progress != true ]]; then
for name in "${names[@]}"; do
log "$e_destroy_unpinned" "$name"
done
return 1
fi
done
return 0
}
#
# Zoneadmd writes a one-line index file into the zone when the zone boots,
# so any information about installed zones from the original system will
# be lost at that time. Here we'll warn the sysadmin about any pre-existing
# zones that they might want to clean up by hand, but we'll leave the zonepaths
# in place in case they're on shared storage and will be migrated to
# a new host.
#
function warn_zones {
typeset zoneconfig=$ZONEROOT/etc/zones
[[ ! -d $zoneconfig ]] && return
if [[ -h $zoneconfig/index || ! -f $zoneconfig/index ]]; then
error "$e_badfile" "/etc/zones/index"
return
fi
# Read the zone's /etc/zones/index and remember important parts.
typeset -A zones
typeset name state path uuid
cat $zoneconfig/index | while IFS=: read name state path uuid; do
[[ $name == global ]] && continue
[[ $state == installed ]] || continue
zones[$name]=( path=$path state=$state )
done
# Return if there are no installed zones to warn about.
(( ${#zones[@]} == 0 )) && return
#
# Special case for having only one zone: Per above, the zone is
# installed and the zone name is not global. If the zonepath is /,
# this image is a part of a V2V operation.
#
if (( ${#zones[@]} == 1 )); then
name=${!zones[*]}
[[ ${zones[$name].path} == / ]] && return
fi
# Log messages about nested zones and zonepaths.
log "$v_rmzones" "${!zones[*]}"
log "$v_rmzonepaths"
for name in "${!zones[@]}"; do
log " %s" "${zones[$name].path}"
done
}
# Setup i18n output
TEXTDOMAIN="SUNW_OST_OSCMD"
export TEXTDOMAIN
e_cannot_wrap=$(gettext "%s: error: wrapper file already exists")
e_baddir=$(gettext "Invalid '%s' directory within the zone")
e_badfile=$(gettext "Invalid '%s' file within the zone")
e_path_abs=$(gettext "Pathname specified to -a '%s' must be absolute.")
e_not_found=$(gettext "%s: error: file or directory not found.")
e_install_abort=$(gettext "Installation aborted.")
e_not_readable=$(gettext "Cannot read directory '%s'")
e_not_dir=$(gettext "Error: must be a directory")
e_unsupported_archive=$(gettext "Archive format '%s' not supported by this brand. See %s(5) for supported archive types.")
e_absolute_archive=$(gettext "Error: archive contains absolute paths instead of relative paths.")
e_mismatch_archive=$(gettext "Error: the archive top-level directory (%s) does not match the zonepath (%s).")
e_tmpfile=$(gettext "Unable to create temporary file")
e_tmpdir=$(gettext "Unable to create temporary directory %s")
e_rmdir=$(gettext "Unable to remove directory %s")
e_rm=$(gettext "Unable to remove %s")
e_mv=$(gettext "Unable to rename '%s' to '%s'")
e_root_full=$(gettext "Zonepath root %s exists and contains data; remove or move aside prior to install.")
e_temp_mount_failed=$(gettext "ZFS temporary mount of %s on %s failed.")
e_no_such_dataset=$(gettext "Error: %s: No such dataset.")
e_ds_mnt_multiply_defined=$(gettext "Error: multiple datasets list %s as mountpoint.")
e_unmount_failed=$(gettext "unable to unmount %s.")
e_mount1_failed=$(gettext "Error: could not mount %s.")
e_parent_not_zoned=$(gettext "Error: parent dataset of %s is not zoned.")
e_export_migration_failed=$(gettext "Error: migration of /export from active boot environment to the zone's\nrpool/export dataset failed. Manual cleanup required.")
e_rpool_migration_failed=$(gettext "Error: migration of data in /rpool from active boot environment to the zone's\nrpool dataset failed. Manual cleanup required.")
e_zfs_destroy=$(gettext "Error: cannot destroy dataset %s")
e_file_conflict=$(gettext "Received file %s collides in datasets %s.")
e_ds_conflict=$(gettext "Received dataset %s collides with existing dataset %s.")
e_unexpected_ds=$(gettext "Unexpected dataset %s found in receive stream.")
e_ds_copy_failed=$(gettext "Failed to copy datasets from %s to %s")
e_be_move_failed=$(gettext "Failed to move be dataset %s.")
f_no_ds=$(gettext "The zonepath must be a ZFS dataset.\nThe parent directory of the zonepath must be a ZFS dataset so that the\nzonepath ZFS dataset can be created properly.")
e_no_active_be=$(gettext "Error: No active boot environment found.")
e_no_mntpt_change_for_mounted=$(gettext "Error: Cannot change mountpoint because %s is mounted")
e_zfs_set=$(gettext "Error: Cannot set zfs property %s on %s")
e_zfs_inherit=$(gettext "Error: Cannot inherit zfs property %s on %s")
e_cmd_failed=$(gettext "Error: Command <%s> exited with status %d")
e_not_pinned=$(gettext "Dataset %s is not pinned")
e_destroy_unpinned=$(gettext "Unable to destroy unpinned dataset '%s'.")
e_dataset_exists=$(gettext "Dataset '%s' already exists")
s10_zbe_not_supported=$(gettext "Solaris 10 style boot environments not supported by this brand.")
f_mkdir=$(gettext "Unable to create directory %s.")
f_chmod=$(gettext "Unable to chmod directory %s.")
f_chown=$(gettext "Unable to chown directory %s.")
f_rmdir=$(gettext "Unable to remove directory %s.")
f_hwcap_info=$(gettext "HWCAP: %s\n")
f_sanity_hwcap=$(gettext \
"The image was created with an incompatible libc.so.1 hwcap lofs mount.\n"\
" The zone will not boot on this platform. See the zone's\n"\
" documentation for the recommended way to create the archive.")
f_int_bad_opt=$(gettext "Internal error: bad option -%s")
f_int_missing_opt=$(gettext "Internal error: missing option -%s")
f_int_missing_arg=$(gettext "Internal error: missing argument %s")
f_int_bad_opt_combo=$(gettext "Internal error: incompatible options -%s and %-s")
f_int_bad_arg=$(gettext "Internal error: extra argument %s")
f_mount=$(gettext "Error: error mounting zone root dataset.")
f_ds_config=$(gettext "Failed to configure dataset %s: could not set %s.")
f_backup_dir_exists=$(gettext "Backup directory %s already exists.")
f_zfs_snapshot=$(gettext "Failed to snapshot source zone.")
f_zone_clone=$(gettext "Failed to clone zone.")
f_zfs_create=$(gettext "Failed to create dataset %s.")
f_zfs_snapshot_of=$(gettext "Failed to create snapshot of %s.")
f_detach_convert=$(gettext "Conversion of detached datasets failed.")
f_mount_active_be=$(gettext "Unable to mount zone root dataset.")
f_unmount_be=$(gettext "Unable to unmount boot environment.")
f_pin=$(gettext "Failed to mark existing datasets for preservation.")
f_unpin=$(gettext "Failed to remove preservation mark from pre-existing datasets.")
f_invalid_data=$(gettext "Invalid data received")
m_interrupt=$(gettext "Cleaning up due to interrupt. Please be patient.")
m_brnd_usage=$(gettext "brand-specific usage: ")
m_analyse_archive=$(gettext "Analysing the archive")
m_fix_ds_mountpoint=$(gettext "Changing mountpoint of dataset %s from %s to %s.")
m_not_mounting_mountpoint=$(gettext "Not mounting %s because mountpoint is '%s'.")
m_not_mounting_canmount=$(gettext "Not mounting %s because canmount is '%s'.")
m_manual_export_migrate=$(gettext "Manual migration of export required. Potential conflicts in\n%s and %s.")
m_backup_saved=$(gettext "A backup copy of %s is stored at %s.\nIt can be deleted after verifying it was migrated correctly.")
m_migrating_data=$(gettext "Migrating data\n\tfrom: %s\n\t to: %s")
m_convert_detached=$(gettext "Converting detached zone boot environment '%s'.")
m_log_progress_to=$(gettext "Progress being logged to %s")
m_log_copied_to=$(gettext "Log saved in non-global zone as %s")
m_manual_snapshot_cleanup=$(gettext "Manual cleanup of snapshot %s required")
not_readable=$(gettext "Cannot read file '%s'")
not_flar=$(gettext "Input is not a flash archive")
bad_flar=$(gettext "Flash archive is a corrupt")
bad_zfs_flar=$(gettext "Flash archive contains a ZFS send stream.\n\tRecreate the flar using the -L option with cpio or pax.")
f_unpack_failed=$(gettext "Unpacking the archive failed")
unknown_archiver=$(gettext "Archiver %s is not supported")
cmd_not_exec=$(gettext "Required command '%s' not executable!")
installing=$(gettext " Installing: This may take several minutes...")
no_installing=$(gettext " Installing: Using existing zone boot environment")
from_clone=$(gettext " Installing: Using clone of zone boot environment '%s'")
v_rmzones=$(gettext "The following zones in this image will be unusable: %s")
v_rmzonepaths=$(gettext "These zonepaths will be extracted but will not be used:")
#
# Exit values used by the script, as #defined in <sys/zone.h>
#
ZONE_SUBPROC_OK=0
ZONE_SUBPROC_UNAVAILABLE=252
ZONE_SUBPROC_USAGE=253
ZONE_SUBPROC_TRYAGAIN=254
ZONE_SUBPROC_FATAL=255