#!/bin/sh

#######################################################################
# Copyright (C) 2008 by NetApp.  All Rights Reserved.
#
#######################################################################

#
# Command usage function
#
usage()
{
	if [ $print_usage -eq 1 ]; then
		msg "Usage: INSTALL [-check] [-1] update|copy|install image1|image2 dir|tarball [dev]"
		msg "  -check - will perform everything up to the point of writing to the device"
		msg "  -1 - will cause install to install only on the specified image"
		msg "  update|copy|install - operation to perform"
		msg "  image1|image2 - the currently running image"
		msg "  dir|tarball - the location of the files to install or a gzipped tarball"
		msg "  dev - optional device containing the boot device used for the"
		msg "        install operation"
	fi
}


# For managing firmware environment variables!
. /etc/netapp_common.subr

#
# Set a few things
#
files='kernel platform.ko rootfs.img platfs.img VERSION COMPAT.TXT INSTALL CHECKSUM cap.xml'
print_usage=1

FDISK=fdisk
DISKINFO=diskinfo
# Define a mount point for the boot device
if [ -n "${CFCARD_MNTPT}" ]; then
	CF_MNT=${CFCARD_MNTPT}
else
	# CFCARD_MNTPT might not be defined if coming from older (eg: tricky)
	# systems, so provide a default of /cfcard
	CF_MNT="/cfcard"
	SetCFcardMntPt "${CF_MNT}"
fi

#
# Convert to uppercase
#
toupper()
{
	echo $1 | sed y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIGKLMNOPQRSTUVWXYZ/
}

#
# Trim leading and trailing white space
#
trim()
{
	echo $1 | sed 's/^[ \t]*//' | sed 's/[ \t]*$//'
}

#
# Make a directory
# NOTE: this now assumes it is passed an absolute path (which has
# always been true).  See burt 295028 for why this was changed.
#
create_dir()
{
	if [ ! -d $1 ]; then
		# Don't use mkdir -p on our /cfcard partitions due to burt 295028
		cd /
		for dir in `echo $1 | tr "/" " "`
		do
			mkdir $dir > /dev/null 2>&1
			cd $dir > /dev/null 2>&1
		done
		cd /
		if [ ! -d $1 ]; then
			msg "Unable to create the directory: $1"
			return 1
		fi
		msg "Directory $1 created"
	fi
	return 0
}

#
# Copy files( src, dest, file_list )
#
copy_files()
{
	local srcdir=$1
	shift
	local dstdir=$1
	shift

	# Give the new partition table a chance to get to the device 
	# and let any pending I/O settle down.
	msg "Syncing device..."
	sync ; sleep 3

	if [ $tarball ]; then
		msg "Extracting to $dstdir..."
		tar -xvzf $tarball -C $dstdir
		if [ $? -ne 0 ]; then
			msg "Unable to extract $tarball to $dstdir - this is VERY bad"
			return 1
		fi
	else
		for file in $@; do
			msg "Copying $file from $srcdir to $dstdir..."
			cp $srcdir/$file $dstdir
			if [ $? -ne 0 ]; then
				msg "Unable to copy the file $srcdir/$file to $dstdir - this is VERY bad"
				return 1
			fi
		done
	fi
	touch $dstdir/_installtime
	return 0
}

# If netbooted image doesn't have the new fdisk changes, then a different config
# will be used.
write_fdisk_config()
{

	echo "p 1 6 $cylstart $psize" > $TMP_MNT/dskfmt
	echo "a 1" >> $TMP_MNT/dskfmt

	# run fdisk in 'test' mode and check for errors.
	$FDISK -t -i -f $TMP_MNT/dskfmt $cfdev 2>&1 | grep "adjusting start offset of partition"
	if [ $? -eq 0 ]; then
		# If fdisk was adjusting the start of the partition, then it does
		# not have our fix.  In that case, use the older layout.
		cylstart=`$DISKINFO $cfdev` || err_exit "Unable to read diskinfo from device $cfdev"
		cylstart=`echo $cylstart | awk '{print $7}'`
		psize=`expr $msize - $cylstart`
		echo "p 1 6 $cylstart $psize" > $TMP_MNT/dskfmt
		echo "a 1" >> $TMP_MNT/dskfmt
	fi

}

#
# Get a kernel environment variable
#
get_kenv()
{
	kenv $1 2> /dev/null
	return $?
}

#
# Clean up some state, then exit.
# This allows install to be called again (specifically during 
# a netboot), if it happens to fail the first time.
#
clean_exit()
{
	# if $TMP_MNT has been used, try to unmmount it
	# depending on when this is called, it could be a 
	# ramdisk, or a boot device, or not even mounted at all.
	if [ -n "$TMP_MNT" ]; then
		umount $TMP_MNT 2> /dev/null
	fi

	if [ "${op}" = "install" ]; then
		umount $CF_MNT 2> /dev/null
	fi
	sync
	exit $1
}

#
# Error and exit function
#
err_exit()
{
	if [ $tmp ]; then
		rm -rf $tmp
	fi
	echo "ERROR: $1." >&2
	usage
	clean_exit 1
}

#
# Output a message
#
msg()
{
	echo "$1"
}

#
# Compute a numeric representation of the major and minor
# portions of the version string.
#
compute_version()
{
    local version=0
    local tmp=`echo $1 | sed 's/[a-zA-Z].*__//'`
    tmp=`echo $tmp | sed 's/[a-zA-Z].*//'`
    local major=`echo $tmp | awk -F. '{printf("%d", $1)}'`
    local minor=`echo $tmp | awk -F. '{printf("%d", $2)}'`

    if [ -z $major ]; then
            major=0
    fi

    # Data ONTAP 8(BR) is greater than 10(Tricky), so 8 becomes 80.
    # If there is a version 11 or greater, it must also be greater than
    # version 8, so 11  becomes 110.
    if [ $major -gt 7 -a \
         $major -lt 10 -o \
         $major -ge 11 ]; then
        major=`expr $major \* 10`
    fi
    version=`expr $version \* 100 + $major`


    if [ -z $minor ]; then
            minor=0
    fi
    version=`expr $version \* 100 + $minor`

    return $version
}

# Notify the dblade if a new version of Data ONTAP has been installed
# This information is used for NDU data mining.
notify_dblade()
{
	# Compare the new BUILD and VERSION files with what ever is in the
	# root directory of the currently running build.  Any differences
	# should trigger the "ndu_takeover" flag to be set within the dblade.

	diff -q /BUILD $otherdir/BUILD > /dev/null 2>&1
	rc_diff_build=$?
	diff -q /VERSION $otherdir/VERSION > /dev/null 2>&1
	rc_diff_vers=$?
	if [ $rc_diff_build -ne 0 -o \
	     $rc_diff_vers -ne 0 ]; then
		# OK to fail.  zsmcli might not exist (Tricky), or 
		# dblade might not be loaded (install from boot menu).
		# ignore any errors.
		zsmcli "system-set-new-ontap-version-downloaded" > /dev/null 2>&1 ||
		true
	fi
	return 0
}

#
# Brand Check is used to verify the software brand matches the
# branding of the underlying hardware.
#
# Input: BUILD file will be used to check for the software branding name
#
# The logic works as folows:
# 1) The BUILD file will be used to check for the software branding
# 2) The hardware branding is obtainied from the SYS_MODEL environment variable
# 3) If a mismatch is detected, failed the installation.
#
brand_check()
{
	buildfile=$1

	# The SwBrand, software branding, is obtained from the TYPE field in the BUILD file.
	# The TYPE field can be one of the following:
	# TYPE:
	# TYPE: IBM
	# TYPE: SIM
	# TYPE: REGULAR
	# TYPE: DEBUG
	# TYPE: DEBUG IBM
	# TYPE: DEBUG SIM
	# TYPE: DEBUG REGULAR
	# We only care about "IBM" brand. Every thing else is treated as null string.
	if grep 'TYPE:.\+IBM' ${buildfile} >/dev/null ; then
		SwBrand="IBM"
		SwType="IBM ONTAP"
	else
		SwType="NetApp ONTAP"
	fi

	# The HwBrand,  hardware branding, is obtained from the SYS_MODEL environment variable.
	# The SYS_MODEL can be one of the following:
	# IBM:
	#	IBM-XXXX
	# NetApp:
	#	FASXXXX
	# VSIM:
	#	SIMBOX
	# VSA:
	#	DOvXXX
	#	FCvXXX
	# We only care about "IBM" brand. Every thing else is treated as null string.
	if get_kenv SYS_MODEL | grep "IBM"  >/dev/null ; then
		HwBrand="IBM"
		HwType="product of IBM"
	else
		HwType="product of NetApp"
	fi

	# branding check
	if [ "${SwBrand}" != "${HwBrand}" ]; then
		err_exit "Cannot install ${SwType} on ${HwType}"
	fi
}
# This Function is used to 
# 1. Calculate free space along with directory(second arg) with first argument "size"
# 2. Delete directory(second arg) contents along with some stale images of 7G when first arg "delete"
delete_old_files()
{
        local option=$1
        local tmpdir=$2
        if [ "$option" = "size" ]; then
                local available=`df -k | grep cfcard | awk '{print $4}'`
                for file in ${tmpdir}/*; do
                        if [ -e $file ]; then
                                temp=`du -k $file | awk '{print $1}'`
                                available=`expr $available + $temp`
                        fi
                done
                for file in ${olddir}/*; do
                        if [ -e $file ]; then
                                temp=`du -k $file | awk '{print $1}'`
                                available=`expr $available + $temp`
                        fi
                done
                echo $available
        elif [  "$option" = "delete" ]; then
                rm -rf ${tmpdir}/* 2> /dev/null
                if [ -f ${currentdir}/kernel ]; then
                        rm -rf ${olddir}/* 2> /dev/null
                fi
        fi

}

#
# Validate the machine type
#
machine=`uname -m`
case $machine in
	amd64)	machine=x86_64
		def_cfdev=/dev/ad0
	;;
	i386)	machine=x86
		def_cfdev=/dev/ad4
	;;
	*)	err_exit "Unrecognized machine type: $machine"
	;;
esac

# If this is not a LOADER based system, then this will be empty
LOADER_VERSION=`get_kenv LOADER_VERSION`

#
# Validate the number of paramters
#
if [ `toupper $1`null = "-CHECK"null ]; then
	checkonly=1
	shift
	msg "INSTALL running in check only mode"
fi

if [ $1null = "-1"null ]; then
	oneonly=1
	shift
fi

if [ $# -lt 3 -o $# -gt 4 ]; then
	err_exit "Invalid number of parameters specified"
fi

op=`toupper $1`
current=$2
location=$3
cfdev=$4

#
# Only accept copy, update and install as operations
#
if [ $op != 'COPY' -a $op != 'UPDATE'  -a $op != 'INSTALL' ]; then
	err_exit "Invalid operation specified"
fi
msg "Mode of operation is $op"

#
# Only accept image1 and image2 as the location of the currently running image
#
if [ `toupper $current` = 'IMAGE1' ]; then
	current=image1
	other=image2
elif [ `toupper $current` = 'IMAGE2' ]; then
	current=image2
	other=image1
else
	err_exit "Invalid current image specified"
fi

#
# Ensure the that currently running image is really the currently running image.
#
imagestr=`get_kenv bootarg.init.rootimage`
if [ $? -ne 0 ]; then
	imagestr=`get_kenv kernelname`
fi
if [ $imagestr ]; then
	echo $imagestr | grep -q $other 2> /dev/null
	if [ $? -eq 0 ]; then
		err_exit "Cannot write to a currently running image"
	fi
fi

currentdir=/cfcard/$machine/freebsd/$current
otherdir=/cfcard/$machine/freebsd/$other
olddir=/cfcard/backup/$machine/kernel

msg "Current image is $current"
msg "Alternate image is $other"

#
# Is it a directory or a tarball?
#
if [ ! -d $location ]; then
	tar -tzf $location > /dev/null
	if [ $? -ne 0 ]; then
		err_exit "Invalid location specified - must be a directory or a gzipped tarball"
	fi
	tarball=$location
	tmp=`mktemp -d /tmp/install.tmp.XXXXX`
	tar -tvzf $tarball > $tmp/filelist
else
	newdir=$location
fi

#
# If it's a tarball then untar a few things to temp
#

#
# If this in an INSTALL operation then check for a valid optional device
#
if [ $op = "INSTALL" ]; then
	if [ $cfdev ]; then
		if [ ! -c $cfdev ]; then
			err_exit "Device $cfdev does not exist"
		fi
		cfpart=$cfdev"s1"
	else
		cfdev=`get_kenv ntap.init.cfdevice`
		if [ $cfdev ]; then
			cfpart=$cfdev
			cfdev=`echo $cfdev | sed 's/s.*//'`
		else
			cfdev=$def_cfdev
			cfpart=$cfdev"s1"
		fi
	fi

	msg "Using device $cfdev for install"
fi
print_usage=0

#
# Ensure the files for the new image exist
#
if [ $tarball ]; then
	for file in $files; do
		cat $tmp/filelist | grep -q $file
		if [ $? -ne 0 ]; then
			err_exit "'$file' does not exist in $tarball"
		fi
	done
else
	for file in $files; do
		if [ ! -e $newdir/$file ]; then
			err_exit "'$file' does not exist in $newdir"
		fi
	done
fi

#
# If this is not an INSTALL operation, compute the available disk space
# available = the free space on the device + the space of the files to be replaced.
#
if [ $op != 'INSTALL' ]; then
	if [ ! -d /cfcard ]; then
		err_exit "No /cfcard directory found"
	fi

	df -k | grep -q cfcard
	if [ $? != 0 ]; then
		err_exit "Boot device not mounted"
	fi

	available=$(delete_old_files size $otherdir)
	availableM=`expr $available / 1024`
	msg "Available space on boot device is $availableM MB"

	#
	# Compute the required disk space - the space of the new files.
	#
	required=0
	for file in $files; do
		if [ $tarball ]; then
			temp=`grep $file $tmp/filelist | awk '{print $5}'`
			temp=`expr $temp / 1024`
		else
			temp=`du -k $newdir/$file | awk '{print $1}'`
		fi
		required=`expr $required + $temp`
	done

	#
	# Ensure that the device will not be more than 95% full
	#
	slack=`df -k | grep cfcard | awk '{print $2}'`
	slack=`expr $slack / 20`
	required=`expr $required + $slack`
	# convert to MB
	requiredM=`expr $required / 1024`

	msg "Required  space on boot device is $requiredM MB"
	if [ $required -gt $available ]; then
		err_exit "Insufficient space available on device"
	fi

#
# Otherwise check that the media size is at least 1GB
#
else
	msize=`$DISKINFO $cfdev` || err_exit "Unable to read diskinfo from device $cfdev"
	msize=`echo $msize | awk '{print $3}'`
	if [ $msize -lt 1000000000 ]; then
	msizeM=`expr $msize / 1024 / 1024`
		msg "The version of Data ONTAP you are attempting to install is not"
                msg "supported on a boot device of $msizeM MB capacity."
		err_exit "Size of boot device is less than 1GB"
	fi
fi

#
# If it's a tarball then extract the BUILD, VERSION, COMPAT.TXT and CHECKSUM files to tmp
#
if [ $tarball ]; then
	tar -xzf $tarball --include='[B|V|C]*' -C $tmp
	newdir=$tmp
	# The file program is busted w.r.t. reading stdin as a pipe.  It wants
	# to read at most 64K and expects to receive it indivisibly in one
	# read.  We use dd to place the first 64K from the extracted kernel
	# into a temporary file and query that instead.
	filetype=`k=$tmp/kernel; tar -xzf $tarball 2>/dev/null -O kernel | dd ibs=1 obs=64K count=64K 2>/dev/null >$k; file $k; rm -f $k`
else
	filetype=`file $newdir/kernel`
fi

#
# Ensure that kernel matches the machine type
#
case $machine in
	x86_64)	echo $filetype | grep -q 'x86-64'
		if [ $? -eq 1 ]; then
			err_exit "Attempt to install an invalid kernel binary on target hardware: $filetype"
		fi
	;;
	x86)	echo $filetype | grep -q 'Intel 80386'
		if [ $? -eq 1 ]; then
			err_exit "Attempt to install an invalid kernel binary on target hardware: $filetype"
		fi
	;;
	*)	err_exit "Invalid machine type: $machine"
	;;
esac
msg "Kernel binary matches install machine type"

#
# Ensure the software brand matches the branding of the underlying hardware
#
brand_check "${newdir}/BUILD"

#
# Ensure that the file checksums match
#
# parameters are:
#	-package
#		verify checksums in the package
#	-dir path
#		verify the checksums in the specified directory path
#
verify_checksums()
{
	if [ "$1" = "-package" ]; then
		check_package="1"
		source="Package"
		checksum_dir="$newdir"
	elif [ "$1" = "-dir" ]; then
		check_package="0"
		source="Installed"
		checksum_dir="$2"
		if [ ! -d "$checksum_dir" ]; then
			err_exit "MD5 checksum failure: $checksum_dir is not a directory"
		fi
	else
		err_exit "Invalid parameters to verify_checksums"
	fi

	clist=`cut -d' ' -f2  ${checksum_dir}/CHECKSUM` || 
			err_exit "${checksum_dir}/CHECKSUM file not found"
	clist=`echo $clist | sed 's/[()]//g'`
	files=$clist
	clist=`echo $clist | sed 's/CHECKSUM //'`
	for i in $clist; do
		if [ "$check_package" = "1" -a $tarball ]; then
			md5sum=`tar -xvzf $tarball -O $i  2>/dev/null | md5`
		else
			md5sum=`md5 -q ${checksum_dir}/$i`
		fi
		grep -q $md5sum ${checksum_dir}/CHECKSUM 2> /dev/null
		if [ $? -eq 1 ]; then
			err_exit "MD5 checksum failure on ${source} file: $i"
		fi
	done
	msg "${source} MD5 checksums pass"
}

verify_checksums -package

#
# Special case: versions are identical -> this becomes a copy operation
# Again, only do this if the operation is not an INSTALL.
#
if [ $op != 'INSTALL' ]; then
	diff $currentdir/VERSION $newdir/VERSION > /dev/null 2>&1
	if [ $? -eq 0 ]; then
		op='COPY'
	fi
fi

#
# For the UPDATE operation, check that the current VERSION file exists
#
if [ $op = 'UPDATE' ]; then

	# VERSION file must exist
	if [ ! -e $currentdir/VERSION ]; then
		err_exit "$current VERSION file does not exist"
	fi        
        #Check basic compatibility
        grep -qEf $newdir/COMPAT.TXT  $currentdir/VERSION
        if [ $? -eq 1 ]; then
            err_exit "Versions are incompatible"
        fi        
        msg "Versions are compatible"    
#
# For the INSTALL operation check for the existance of the device and
# ensure that it is not currently mounted.
#
elif [ $op = 'INSTALL' ]; then
	if [ ! -c $cfdev ]; then
		err_exit "Cannot find boot device file $cfdev"
	fi

	mount | grep -q $cfdev
	if [ $? = 0 ]; then
		err_exit "Cannot initialize a mounted filesystem"
	fi
	msg "Install device found"

	if [ $oneonly ]; then
		msg "Install command will only setup $current"
	fi

#
# For the COPY operations, don't need to do much at all.
#
#else
fi

#
# If running as check only then stop here.
#
rm -rf $tmp
if [ $checkonly ]; then
	clean_exit 0
fi
msg "Getting ready to install image"

#
# If this is an INSTALL operation then format the device
#
if [ $op = 'INSTALL' ]; then

	# compute the start sector and size of the first partition
	msize=`$DISKINFO $cfdev` || err_exit "Unable to read diskinfo from device $cfdev" 
	msize=`echo $msize | awk '{print $4}'`
	# To satisfy Data ONTAP 7G, cylstart must be a multiple of 8.  7G uses
	# 288, so we will hard code that here as well.  Anything too much larger
	# (like 63*8=504) leads to intermittent failures during INSTALL
	# NOTE: FreeBSD' fdisk has been modified to allow this non-head boundary
	# start of a partition.
	cylstart=288
	psize=`expr $msize - $cylstart`

	# whipup a ramdisk to place a file specifing partitioning info for fdisk
	mddev=`mdconfig -a -tmalloc -s1024`
	if [ $? != 0 ]; then
		err_exit "Unable to allocate a small ramdisk"
	fi

	newfs /dev/$mddev
	if [ $? != 0 ]; then
		mdconfig -d -u$mddev
		err_exit "Unable to newfs ramdisk"
	fi
	sync

	TMP_MNT=`mktemp -d /tmp/mnt.XXXXX`
	mount /dev/$mddev $TMP_MNT
	if [ $? != 0 ]; then
		mdconfig -d -u$mddev
		err_exit "Unable to mount ramdisk"
	fi

	write_fdisk_config

	# partition the disk
	$FDISK -i -f $TMP_MNT/dskfmt $cfdev
	if [ $? != 0 ]; then
		umount $TMP_MNT
		mdconfig -d -u$mddev
		err_exit "Unable to initialize the boot device partition table"
	fi
	sync ; sleep 3;
	msg "Partitioning of boot device complete"

	# toss out the ramdisk
	umount $TMP_MNT
	mdconfig -d -u$mddev
	sync

	# create a new filesystem on partition 1
	newfs_msdos -F16 -O"ONTAP" -m0xf8 $cfpart
	if [ $? != 0 ]; then
		err_exit "Unable to create a FAT filesystem on the boot device"
	fi
	msg "New FAT filesystem created"
	sync

	# mount the new filesystem for copying over files
	mount -o longnames -t msdosfs $cfpart ${CF_MNT}
	if [ $? != 0 ]; then
		err_exit "Unable to mount FAT filesystem on the boot device"
	fi
	sync
	msg "New filesystem mounted"

        if [ "$LOADER_VERSION" != "" ]; then
		# Restore loader's environment to the boot device since we blew
		# it away when the boot device was formatted
		SaveLoaderEnv ||
			msg "Warning: Could not preserve Loader environment."
		sync
	fi

	currentdir=${CF_MNT}/$machine/freebsd/$current
	otherdir=${CF_MNT}/$machine/freebsd/$other
	olddir=${CF_MNT}/backup/$machine/kernel
fi

#
# If destination directory does not exist then create it.
#
create_dir $otherdir
if [ $? != 0 ]; then
	clean_exit 1
fi
if [ $op = 'INSTALL' ]; then
	create_dir $currentdir
	if [ $? != 0 ]; then
		clean_exit 1
	fi
fi

#
# Copy the files
#
if [ $op = 'INSTALL' -a $oneonly ]; then
	delete_old_files delete $currentdir
	copy_files $newdir $currentdir $files
	if [ $? != 0 ]; then
		clean_exit 1
	fi
	verify_checksums -dir $currentdir

	# Swap for diags install
	otherdir=$currentdir
else
	if [ $op = 'INSTALL' ]; then
		delete_old_files delete $currentdir
		copy_files $newdir $currentdir $files
		if [ $? != 0 ]; then
			clean_exit 1
		fi
		verify_checksums -dir $currentdir
		# we don't need two copies of diags.tgz
		rm -f ${currentdir}/diags.tgz
	fi

	delete_old_files delete $otherdir
	copy_files $newdir $otherdir $files
	if [ $? != 0 ]; then
		clean_exit 1
	fi
	verify_checksums -dir $otherdir
fi

#
# See if there were diags in this package and install them
#
if [ -f $otherdir/diags.tgz ]; then
	# See if we have enough space to install this.
	# Assume we need twice the size of the tarball
	diags_needed=`ls -l $otherdir/diags.tgz | awk '{print $5}'`
	diags_needed=`expr $diags_needed \* 2 / 1024`
	avail=`df -k $otherdir | grep -v Avail | awk '{print $4}'`
	avail=`expr $avail \* 90 / 100` # 10% head room

	if [ $diags_needed -gt $avail ]; then
		msg "Not enough space to install diagnostics and firmware files"
	else
		# This the directory hierarchy built into the tar file
		msg "Installing diagnostics and firmware files"
		tar -xzf $otherdir/diags.tgz -C ${CF_MNT}
		if [ $? != 0 ]; then
			msg "Failed to install diagnostics and firmware files"
			# ignore!
		else
			# Successfully extracted, no need to keep the package
			rm -f ${otherdir}/diags.tgz
		fi
	fi
fi

# After the install, and before we unmount and exit,
# tell the dblade to set a flag.
notify_dblade

#
# If this was an install operation then unmount the boot device.
#
if [ $op = "INSTALL" ]; then
	umount ${CF_MNT}
	if [ $? != 0 ]; then
		err_exit "Unable to unmount the boot device"
	fi
	sync
fi

clean_exit 0
