#!/bin/bash --norc
#
# mkinitrd compability wrapper for SUSE.
#
# Copyright (c) 2013 SUSE Linux Products GmbH. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

boot_dir="/boot"
quiet=0
logfile=/var/log/YaST2/mkinitrd.log
dracut_cmd=dracut

error() { echo "$@" >&2; }

usage () {
    [[ $1 = '-n' ]] && cmd=echo || cmd=error

    $cmd "usage: ${0##*/} [options]"
    $cmd ""
    $cmd "	Create initial ramdisk images that contain all kernel modules needed"
    $cmd "	in the early boot process, before the root file system becomes"
    $cmd "	available."
    $cmd "	This usually includes SCSI and/or RAID modules, a file system module"
    $cmd "	for the root file system, or a network interface driver module for dhcp."
    $cmd ""
    $cmd "	options:"
    $cmd "	-f \"feature list\"	Features to be enabled when generating initrd."
    $cmd "				Available features are:"
    $cmd "					iscsi, md, multipath, lvm, lvm2,"
    $cmd "					ifup, fcoe, dcbd"
    $cmd "	-k \"kernel list\"	List of kernel images for which initrd files are"
    $cmd "				created. Defaults to all kernels found in /boot."
    $cmd "	-i \"initrd list\"	List of file names for the initrd; position have"
    $cmd "				match to \"kernel list\". Defaults to all kernels"
    $cmd "				found in /boot."
    $cmd "	-b boot_dir		Boot directory. Defaults to /boot."
    $cmd "	-t tmp_dir		Temporary directory. Defaults to /var/tmp."
    $cmd "	-M map			System.map file to use."
    $cmd "	-A			Create a so called \"monster initrd\" which"
    $cmd "				includes all features and modules possible."
    $cmd "	-B			Do not update bootloader configuration."
    $cmd "	-v			Verbose mode."
    $cmd "	-L			Disable logging."
    $cmd "	-h			This help screen."
    $cmd "	-m \"module list\"	Modules to include in initrd. Defaults to the"
    $cmd "				INITRD_MODULES variable in /etc/sysconfig/kernel"
    $cmd "	-u \"DomU module list\"	Modules to include in initrd. Defaults to the"
    $cmd "				DOMU_INITRD_MODULES variable in"
    $cmd "				/etc/sysconfig/kernel."
    $cmd "	-d root_device		Root device. Defaults to the device from"
    $cmd "				which / is mounted. Overrides the rootdev"
    $cmd "				enviroment variable if set."
    $cmd "	-j device		Journal device"
    $cmd "	-D interface		Run dhcp on the specified interface."
    $cmd "	-I interface		Configure the specified interface statically."
    $cmd "	-a acpi_dsdt		Obsolete, do not use."
    $cmd "	-s size			Add splash animation and bootscreen to initrd."

    [[ $1 = '-n' ]] && exit 0
    exit 1
}

# Little helper function for reading args from the commandline.
# it automatically handles -a b and -a=b variants, and returns 1 if
# we need to shift $3.
read_arg() {
    # $1 = arg name
    # $2 = arg value
    # $3 = arg parameter
    param="$1"
    local rematch='^[^=]*=(.*)$' result
    if [[ $2 =~ $rematch ]]; then
        read "$param" <<< "${BASH_REMATCH[1]}"
    else
	for ((i=3; $i <= $#; i++)); do
            # Only read next arg if it not an arg itself.
            if [[ ${@:$i:1} = -* ]];then
		break
            fi
            result="$result ${@:$i:1}"
            # There is no way to shift our callers args, so
            # return "no of args" to indicate they should do it instead.
	done
	read "$1" <<< "$result"
        return $(($i - 3))
    fi
}

# Helper functions to calculate ipconfig command line
calc_netmask() {
    local prefix=$1

    [ -z "$prefix" ] && return
    mask=$(( 0xffffffff << (32 - $prefix) ))
    byte1=$(( mask >> 24 ))
    byte2=$(( mask >> 16 ))
    byte3=$(( mask >> 8 ))
    byte4=$(( mask & 0xff ))
    netmask=$(printf "%d.%d.%d.%d" $(( byte1 & 0xff )) $(( byte2 & 0xff )) $(( byte3 & 0xff )) $byte4);

    echo $netmask
}

ipconfig() {
    local interface=$1
    local iplink macaddr broadcast gateway ipaddr prefix netmask

    iplink=$(ip addr show dev $interface | sed -n 's/ *inet \(.*\) brd.*/\1/p')
    macaddr=$(ip addr show dev $interface | sed -n 's/.*ether \(.*\) brd.*/\1/p')
    broadcast=$(ip addr show dev $interface | sed -n 's/.*brd \(.*\) scope.*/\1/p')
    gateway=$(ip route show dev $interface | sed -n 's/default via \([0-9\.]*\).*/\1/p')

    ipaddr=${iplink%%/*}
    prefix=${iplink##*/}
    netmask=$(calc_netmask $prefix)

    echo "${ipaddr}:${serveraddr}:${gateway}:${netmask}:${hostname}:${interface}:none::${macaddr}"
}

is_xen_kernel() {
    local kversion=$1
    local root_dir=$2
    local cfg

    for cfg in ${root_dir}/boot/config-$kversion $root_dir/lib/modules/$kversion/build/.config
    do
        test -r $cfg || continue
        grep -q "^CONFIG_XEN=y\$" $cfg
        return
    done
    test $kversion != "${kversion%-xen*}"
    return
}

# kernel_image_gz_from_image() and kernel_version_from_image() are helpers
# for arm* kernels which produce zImage files which cannot be read from
# get_kernel_version -> get rid of this workaround if possible
kernel_image_gz_from_image() {
    local arch=$(uname -i)
    local r=${1}.gz

    # uImage kernels can't be extracted directly. Use the vmlinux.gz instead
    r=${r//uImage/vmlinux}

    # on ARM a zImage can't be extracted directly. Other platforms define it
    # as a gzipped vmlinux file, but not ARM. So only on ARM, use vmlinux.gz.
    if [[ $arch =~ arm ]] || [[ $arch =~ aarch ]]; then
        r=${r//zImage/vmlinux}
    fi

    echo $r
}

kernel_version_from_image() {
    local kernel_image="$1" kernel_image_gz=$(kernel_image_gz_from_image "$1")

    if get_kernel_version "$kernel_image" 2>/dev/null; then
        return
    fi
    get_kernel_version "$kernel_image_gz" 2>/dev/null
}

# Taken over from SUSE mkinitrd
default_kernel_images() {
    local regex kernel_image kernel_version version_version initrd_image
    local qf='%{NAME}-%{VERSION}-%{RELEASE}\n'

    case "$(uname -m)" in
        s390|s390x)
            regex='image'
            ;;
        ppc|ppc64)
            regex='vmlinux'
            ;;
        i386|x86_64)
            regex='vmlinuz'
            ;;
        arm*)
            regex='[uz]Image'
            ;;
        aarch64)
            regex='Image'
            ;;
        *)  regex='vmlinu.'
            ;;
    esac

    kernel_images=""
    initrd_images=""
    for kernel_image in $(ls $boot_dir \
            | sed -ne "\|^$regex\(-[0-9.]\+-[0-9]\+-[a-z0-9]\+$\)\?|p" \
            | grep -v kdump$ ) ; do

        # Note that we cannot check the RPM database here -- this
        # script is itself called from within the binary kernel
        # packages, and rpm does not allow recursive calls.

        [ -L "$boot_dir/$kernel_image" ] && continue
        [ "${kernel_image%%.gz}" != "$kernel_image" ] && continue

        kernel_version=$(kernel_version_from_image \
                         $boot_dir/$kernel_image 2> /dev/null)
        initrd_image=$(echo $kernel_image | sed -e "s|${regex}|initrd|")
        if [ "$kernel_image" != "$initrd_image" -a \
             -n "$kernel_version" -a \
             -d "/lib/modules/$kernel_version" ]; then
                kernel_images="$kernel_images $boot_dir/$kernel_image"
                initrd_images="$initrd_images $boot_dir/$initrd_image"
        fi
    done
    for kernel_image in $kernel_images;do
	kernels="$kernels ${kernel_image#*-}"
    done
    for initrd_image in $initrd_images;do
	targets="$targets $initrd_image"
    done
}

while (($# > 0)); do
    case ${1%%=*} in
	-f) read_arg feature_list "$@" || shift $?
	    # Could be several features
	    ;;
	-k) # Would be nice to get a list of images here
	    read_arg kernel_images "$@" || shift $?
	    for kernel_image in $kernel_images;do
		[ -L "/boot/$kernel_image" ] && kernel_image="$(readlink "/boot/$kernel_image")"
		kernels="$kernels ${kernel_image#*-}"
	    done
	    ;;
	-i) read_arg initrd_images "$@" || shift $?
	    for initrd_image in $initrd_images;do
		[ -L "/boot/$initrd_image" ] && initrd_image="$(readlink "/boot/$initrd_image")"
		# Check if the initrd_image contains a path.
		# if not, then add the default boot_dir
		dname=`dirname $initrd_image`
		if [ "$dname" == "." ]; then
                    targets="$targets $boot_dir/$initrd_image";
		else
                    targets="$targets $initrd_image";
		fi
	    done
	    ;;
	-b) read_arg boot_dir "$@" || shift $?
	    if [ ! -d $boot_dir ];then
		error "Boot directory $boot_dir does not exist"
		exit 1
	    fi
	    ;;
	-t) read_arg tmp_dir "$@" || shift $?
	    dracut_args="${dracut_args} --tmpdir $tmp_dir"
	    ;;
	-M) read_arg map_file "$@" || shift $?
	    ;;
	-A) dracut_args="${dracut_args} --no-host-only";;
	-B) skip_update_bootloader=1;;
        -v|--verbose) dracut_args="${dracut_args} -v";;
	-L) logfile=;;
        -h|--help) usage -n;;
	-m) read_arg module_list "$@" || shift $? ;;
	-u) read_arg domu_module_list "$@" || shift $?
	    echo "mkinitrd: DomU modules not yet supported" ;;
        -d) read_arg rootfs "$@" || shift $?
            dracut_args="${dracut_args} --filesystems $rootfs" ;;
	-D) read_arg dhcp_if "$@" || shift $?
	    dracut_cmdline="${dracut_cmdline} ip=${dhcp_if}:dhcp"
	    ;;
	-I) read_arg static_if "$@" || shift $?
	    dracut_cmdline="${dracut_cmdline} ip=$(ipconfig $static_if)":
	    ;;
	-a) read_arg acpi_dsdt "$@" || shift $?
	    echo "Obsolete -a param, use acpi_table_dir= and acpi_override= variables in /etc/dracut.conf.d/"
	    exit 1
	    ;;
	-s) read_arg boot_splash "$@" || shift $?
	    echo "mkinitrd: boot splash not yet supported"
	    exit 1
	    ;;
	-V) echo "mkinitrd: vendor scipts are no longer supported"
	    exit 1;;
	--dracut)
	    read_arg dracut_cmd "$@" || shift $? ;;
        --version|-R)
            echo "mkinitrd: dracut compatibility wrapper"
            exit 0;;
	--quiet|-q) quiet=1;;
        *)  if [[ ! $targets ]]; then
            targets=$1
            elif [[ ! $kernels ]]; then
            kernels=$1
            else
            usage
            fi;;
    esac
    shift
done

[[ $targets && $kernels ]] || default_kernel_images
if [[ ! $targets || ! $kernels ]];then
    error "No kernel found in $boot_dir or bad modules dir in /lib/modules"
    exit 1
fi

# We can have several targets/kernels, transform the list to an array
targets=( $targets )
[[ $kernels ]] && kernels=( $kernels )

[[ $logfile ]]        && dracut_args="${dracut_args} --logfile $logfile"
dracut_args="${dracut_args} --force"

[[ $dracut_cmdline ]] && dracut_args="${dracut_args} --kernel-cmdline ${dracut_cmdline}"
[ -z "$(type -p update-bootloader)" ] && skip_update_bootloader=1

# Update defaults from /etc/sysconfig/kernel
if [ -f /etc/sysconfig/kernel ] ; then
    . /etc/sysconfig/kernel
fi
[[ $module_list ]] || module_list="${INITRD_MODULES}"
[[ $domu_module_list ]] || domu_module_list="${DOMU_INITRD_MODULES}"
shopt -s extglob

failed=""

for ((i=0 ; $i<${#targets[@]} ; i++)); do

    if [[ $img_vers ]];then
	target="${targets[$i]}-${kernels[$i]}"
    else
	target="${targets[$i]}"
    fi
    kernel="${kernels[$i]}"

    if is_xen_kernel $kernel $rootfs ; then
	modules_all="${module_list} ${domu_module_list}"
    else
        modules_all="${module_list}"
    fi

    # Remove leading and trailing spaces needs (set above): shopt -s extglob
    modules_all=${modules_all%%+([[:space:]])}
    modules_all=${modules_all##+([[:space:]])}

    echo "Creating initrd: $target"

    # Duplicate code: No way found how to redirect output based on $quiet
    if [[ $quiet == 1 ]];then
        # Duplicate code: --force-drivers must not be called with empty string
        # -> dracut bug workarounded ugly, because of complex whitespace
        # expansion magics
        if [ -n "${modules_all}" ];then
            $dracut_cmd $dracut_args --force-drivers "${modules_all}" "$target" "$kernel" &>/dev/null
            [ $? -ne 0 ] && failed="$failed $target"
        else
            $dracut_cmd $dracut_args "$target" "$kernel" &>/dev/null
            [ $? -ne 0 ] && failed="$failed $target"
        fi
    else
        if [ -n "${modules_all}" ];then
            $dracut_cmd $dracut_args --force-drivers "${modules_all}" "$target" "$kernel"
            [ $? -ne 0 ] && failed="$failed $target"
        else
            $dracut_cmd $dracut_args "$target" "$kernel"
            [ $? -ne 0 ] && failed="$failed $target"
        fi
    fi
done

if [ "$skip_update_bootloader" ] ; then
    echo 2>&1 "Did not refresh the bootloader. You might need to refresh it manually."
else
    update-bootloader --refresh
    [ $? -ne 0 ] && echo "Updating bootloader failed" && exit 1
fi

if [ "$failed" != "" ]; then
    echo "Generating $failed targets failed"
    exit 1
fi

exit 0
