#!/bin/bash

command -v unpack_img >/dev/null || . /lib/img-lib.sh
command -v getarg >/dev/null || . /lib/dracut-lib.sh
command -v fetch_url >/dev/null || . /lib/url-lib.sh

# show critical error messages more visible to user
warn_critical() {
    local msg="$1"
    if ! [ -d /run/anaconda ]; then
        mkdir -p /run/anaconda
    fi
    echo "$msg" >> /run/anaconda/initrd_errors.txt
    warn "$msg"
}

# config_get SECTION KEY < FILE
# read an .ini-style config file, find the KEY in the given SECTION, and return
# the value provided for that key.
# ex: product=$(config_get Main Product < /.buildstamp)
config_get() {
    local section="$1" key="$2" cursec="" k="" v=""
    while read -r line; do
        case "$line" in
            \#*) continue ;;
            \[*\]*) cursec="${line#[}"; cursec="${cursec%%]*}" ;;
            *=*) k="${line%%=*}"; v="${line#*=}" ;;
        esac
        if [ "$cursec" = "$section" ] && [ "$k" == "$key" ]; then
            echo "$v"
            break
        fi
    done
}

find_iso() {
    local f="" p="" iso="" isodir="$1" tmpmnt=""
    tmpmnt=$(mkuniqdir /run/install tmpmnt)
    for f in "$isodir"/*.iso; do
        [ -e "$f" ] || continue
        mount -o loop,ro "$f" "$tmpmnt" || continue
        # Valid ISOs either have stage2 in one of the supported paths
        # or have a .treeinfo that might tell use where to find the stage2 image.
        # If it does not have any of those, it is not valid and will not be used.
        for p in $tmpmnt/LiveOS/squashfs.img $tmpmnt/images/install.img $tmpmnt/.treeinfo; do
            if [ -e "$p" ]; then iso=$f; break; fi
        done
        umount "$tmpmnt"
        if [ "$iso" ]; then echo "$iso"; return 0; fi
    done
    return 1
}

find_runtime() {
    [ -f "$1" ] && [ "${1%.iso}" == "$1" ] && echo "$1" && return
    local ti_img="" dir="$1"
    [ -e "$dir"/.treeinfo ] && \
        ti_img=$(config_get stage2 mainimage < "$dir/.treeinfo")
    for f in $ti_img images/install.img LiveOS/squashfs.img; do
        [ -e "$dir/$f" ] && echo "$dir/$f" && return
    done
}

find_tty() {
    # find the real tty for /dev/console
    local tty="console"
    while [ -f /sys/class/tty/$tty/active ]; do
        tty=$(< /sys/class/tty/$tty/active)
        tty=${tty##* } # last item in the list
    done
    echo "$tty"
}


repodir="/run/install/repo"
isodir="/run/install/isodir"
rulesfile="/etc/udev/rules.d/90-anaconda.rules"

# try to find a usable runtime image from the repo mounted at $mnt.
# if successful, move the mount(s) to $repodir/$isodir.
anaconda_live_root_dir() {
    local img="" iso="" mnt="$1" path="$2"
    img=$(find_runtime "$mnt/$path")
    if [ -n "$img" ]; then
        info "anaconda: found $img"
        [ "$mnt" = "$repodir" ] || { mount --make-rprivate /; mount --move "$mnt" $isodir; }
        anaconda_auto_updates "$repodir/$path/images"
    else
        if [ "${path%.iso}" != "$path" ]; then
            iso=$path
            path=${path%/*.iso}
        else
            iso=$(find_iso "$mnt/$path")
        fi
        [ -n "$iso" ] || { warn "no suitable images"; return 1; }
        info "anaconda: found $iso"
        mount --make-rprivate /
        mount --move "$mnt" $isodir
        iso=${isodir}/${iso#"$mnt"}
        mount -o loop,ro "$iso" $repodir
        img=$(find_runtime $repodir) || { warn "$iso has no suitable runtime"; }
        anaconda_auto_updates $repodir/images
    fi
    anaconda_mount_sysroot "$img"
}

anaconda_net_root() {
    local repo="$1"
    info "anaconda: fetching stage2 from $repo"

    # Try to get the local path to stage2 from treeinfo.
    treeinfo=$(fetch_url "$repo/.treeinfo" 2> /tmp/treeinfo_err) && \
        stage2=$(config_get stage2 mainimage < "$treeinfo")

    # No treeinfo available.
    [ -z "$treeinfo" ] && debug_msg "$(cat /tmp/treeinfo_err)"

    # Use the default local path to stage2.
    if [ -z "$treeinfo" ] || [ -z "$stage2" ]; then
        warn "can't find installer main image path in .treeinfo"
        stage2="images/install.img"
    fi

    # Fetch the stage2.
    if runtime=$(fetch_url "$repo/$stage2") \
        || runtime=$(fetch_url "$repo/LiveOS/squashfs.img"); then

        info "anaconda: successfully fetched stage2 from $repo"

        # NOTE: Should be the same as anaconda_auto_updates()
        updates=$(fetch_url "$repo/images/updates.img" 2> /tmp/updates_err)
        [ -z "$updates" ] && debug_msg "$(cat /tmp/updates_err)"
        [ -n "$updates" ] && unpack_updates_img "$updates" /updates

        product=$(fetch_url "$repo/images/product.img" 2> /tmp/product_err)
        [ -z "$product" ] && debug_msg "$(cat /tmp/product_err)"
        [ -n "$product" ] && unpack_updates_img "$product" /updates

        anaconda_mount_sysroot "$runtime"
        return 0
    fi

    warn_critical "anaconda: failed to fetch stage2 from $repo"
    return 1
}

anaconda_mount_sysroot() {
    local img="$1"
    if [ -e "$img" ]; then
        /sbin/dmsquash-live-root "$img"
        if [ -d /run/rootfsbase ]; then
            # /run/rootfsbase has been created
            # Which means that the Squash filesystem is plain
            # and does not contain the embedded EXT4 inside.
            # Also known as flattened SquashFS or directly compressed SquashFS.
            printf "mount -t overlay LiveOS_rootfs \
                   -o lowerdir=/run/rootfsbase,upperdir=/run/overlayfs,workdir=/run/ovlwork \
                   %s" "${NEWROOT}" > "${hookdir}/mount/01-$$-anaconda.sh"
        else
            # Otherwise, assumption is that /dev/mapper/live-rw should have been created.
            # dracut & systemd only mount things with root=live: so we have to do this ourselves
            # See https://bugzilla.redhat.com/show_bug.cgi?id=1232411
            printf 'mount /dev/mapper/live-rw %s\n' "$NEWROOT" > "$hookdir/mount/01-$$-anaconda.sh"
        fi
    fi
}

# find updates.img/product.img/RHUpdates and unpack/copy them so they'll
# end up in the location(s) that anaconda expects them
anaconda_auto_updates() {
    local dir="$1"
    if [ -d "$dir/RHupdates" ]; then
        copytree "$dir/RHupdates" /updates
    fi
    if [ -e "$dir/updates.img" ]; then
        unpack_updates_img "$dir/updates.img" /updates
    fi
    if [ -e "$dir/product.img" ]; then
        unpack_updates_img "$dir/product.img" /updates
    fi
}

# Unpack an image into the given dir.
unpack_updates_img() {
    local img="$1" tmpdir="/tmp/${1##*/}.$$" outdir="${2:-/updates}"
    # NOTE: unpack_img $img $outdir can clobber existing subdirs in $outdir,
    # which is why we use a tmpdir and copytree (which doesn't clobber)
    unpack_img "$img" "$tmpdir"
    copytree "$tmpdir" "$outdir"
    rm -rf "$tmpdir"
}

# These could probably be in dracut-lib or similar

copytree() {
    local src="$1" dest="$2"
    mkdir -p "$dest"; dest=$(readlink -f -q "$dest")
    ( cd "$src" || return 1; cp -a . -t "$dest" )
}

disk_to_dev_path() {
    case "$1" in
        CDLABEL=*|LABEL=*) echo "/dev/disk/by-label/${1#*LABEL=}" ;;
        UUID=*) echo "/dev/disk/by-uuid/${1#UUID=}" ;;
        /dev/*) echo "$1" ;;
        *) echo "/dev/$1" ;;
    esac
}

find_mount() {
    local dev mnt etc wanted_dev
    wanted_dev="$(readlink -e -q "$1")"
    # shellcheck disable=SC2034  # etc eats the rest of line
    while read -r dev mnt etc; do
        [ "$dev" = "$wanted_dev" ] && echo "$mnt" && return 0
    done < /proc/mounts
    return 1
}

when_diskdev_appears() {
    local dev="${1#/dev/}" cmd=""; shift
    cmd="/sbin/initqueue --settled --onetime --name $1 $*"
    {
        printf 'SUBSYSTEM=="block", KERNEL=="%s", RUN+="%s"\n' "$dev" "$cmd"
        printf 'SUBSYSTEM=="block", SYMLINK=="%s", RUN+="%s"\n' "$dev" "$cmd"
    } >> $rulesfile
}

when_any_cdrom_appears() {
    local cmd="/sbin/initqueue --settled --onetime --name autocd $*"
    printf 'SUBSYSTEM=="block", ENV{ID_CDROM_MEDIA}=="1", RUN+="%s"\n' "$cmd" \
      >> $rulesfile
}

when_any_hmcdrv_appears() {
    local dev="hmcdrv"
    local cmd="/sbin/initqueue --settled --onetime --name $dev $*"
    printf 'KERNEL=="%s", RUN+="%s"\n' "$dev" "$cmd" \
      >> $rulesfile
}

plymouth_running() {
    type plymouth >/dev/null 2>&1 && plymouth --ping 2>/dev/null
}

# print something to the display (and put it in the log so we know what's up)
tell_user() {
    if plymouth_running; then
        # NOTE: if we're doing graphical splash but we don't have all the
        # font-rendering libraries, no message will appear.
        plymouth display-message --text="$*"
        echo "$*"     # this goes to journal only
    else
        echo "$*" >&2 # this goes to journal+console
    fi
}

# print something only in if debug/inst.debug/rd.debug
debug_msg() {
   if getargbool 0 rd.debug || getargbool 0 debug || getargbool 0 inst.debug; then
     echo "$*" >&2
  fi
}

dev_is_cdrom() {
    udevadm info --query=property --name="$1" | grep -q 'ID_CDROM=1'
}

dev_is_on_disk_with_iso9660() {
    # Get the name of the device.
    local dev_name="${1}"

    # Get the path of the device.
    local dev_path
    dev_path="$(udevadm info -q path --name "${dev_name}")"

    # Is the device a partition?
    udevadm info -q property --path "${dev_path}" | grep -q 'DEVTYPE=partition' || return 1

    # Get the path of the parent.
    local disk_path="${dev_path%/*}"

    # Is the parent a disk?
    udevadm info -q property --path "${disk_path}" | grep -q 'DEVTYPE=disk' || return 1

    # Does the parent has the iso9660 filesystem?
    udevadm info -q property --path "${disk_path}" | grep -q 'ID_FS_TYPE=iso9660' || return 1

    return 0
}

# dracut doesn't bring up the network unless:
#   a) $netroot is set (i.e. you have a network root device), or
#   b) /tmp/net.ifaces exists.
# So for non-root things that need the network (like kickstart) we need to
# make sure /tmp/net.ifaces exists.
# For details see 40network/net-genrules.sh (and the rest of 40network).
set_neednet() {
    # if there's no netroot, make sure /tmp/net.ifaces exists
    [ -z "$netroot" ] && true >> /tmp/net.ifaces
}

parse_kickstart() {
    PYTHONHASHSEED=42 /sbin/parse-kickstart "$1" > /etc/cmdline.d/80-kickstart.conf
    unset CMDLINE  # re-read the commandline
    . /tmp/ks.info # save the parsed kickstart
    [ -e "$parsed_kickstart" ] && cp "$parsed_kickstart" /run/install/ks.cfg
}

# print a list of net devices that dracut says are set up.
online_netdevs() {
    local netif=""
    for netif in /tmp/net.*.did-setup; do
        netif=${netif#*.}; netif=${netif%.*}
        [ -d "/sys/class/net/$netif" ] && echo "$netif"
    done
}

# Filter locations that are http, https or ftp urls.
get_urls() {
    local locations="${1}"
    local location

    # Filter locations.
    for location in $locations; do
        case "${location%%:*}" in
            http|https|ftp)
                echo "$location"
            ;;
            *)
                warn "anaconda: this location will be ignored: $location"
            ;;
        esac
    done
}

# This is where we actually run the kickstart. Whee!
# We can't just add udev rules (we'll miss devices that are already active),
# and we can't just run the scripts manually (we'll miss devices that aren't
# yet active - think driver disks!).
#
# So: we have to write out the rules and then retrigger them.
#
# Really what we want to do here is just start over from the "cmdline"
# phase, but since we can't do that, we'll kind of fake it.
#
# XXX THIS IS KIND OF A GROSS HACK AND WE NEED A BETTER WAY TO DO IT
run_kickstart() {
    local do_disk="" do_net=""

    # kickstart's done - time to find a real root device
    [ "$root" = "anaconda-kickstart" ] && root=""

    # don't look for the kickstart again
    # shellcheck disable=SC2034  # used by other anaconda-related dracut stuff
    kickstart=""

    # re-parse new cmdline stuff from the kickstart
    . "$hookdir"/cmdline/*parse-anaconda-repo.sh
    . "$hookdir"/cmdline/*parse-livenet.sh
    . "$hookdir"/cmdline/*parse-anaconda-dd.sh

    # Kickstart network configuration (which might even be empty) should be
    # applied to get installer image or driver disks only if the tasks haven't
    # already been performed using network configuration by boot options. This
    # is ensured by the .done files checking.

    case "$root" in
        anaconda-net:*)   [ ! -f /tmp/anaconda_netroot.done ] && do_net=1 ;;
        anaconda-hmc)     do_disk=1 ;;
        anaconda-disk:*)  do_disk=1 ;;
        anaconda-auto-cd) do_disk=1 ;;
    esac
    [ -f /tmp/dd_net ] && [ ! -f /tmp/dd_net.done ] && do_net=1
    [ -f /tmp/dd_disk ] && do_disk=1

    # disk: replay udev events to trigger actions
    if [ "$do_disk" ]; then
        # set up new rules
        . "$hookdir"/pre-trigger/*repo-genrules.sh
        . "$hookdir"/pre-trigger/*driver-updates-genrules.sh
        udevadm control --reload
        # trigger the rules for all the block devices we see
        udevadm trigger --action=change --subsystem-match=block
    fi

    # net: re-run online hooks
    if [ "$do_net" ]; then
        # If NetworkManager is used in initramfs
        if ls -U "$hookdir"/cmdline/*-nm-config.sh >/dev/null 2>&1 ; then
            # First try to re-run online hooks on any online device.
            # We don't want to reconfigure the network by applying kickstart config
            # so use existing network connections if there are any.
            # Based on nm-run.sh
            for _i in /sys/class/net/*/
            do
                state=/run/NetworkManager/devices/$(cat "$_i/ifindex")
                grep -q connection-uuid= "$state" 2>/dev/null || continue
                nm_connected_device_found="yes"
                ifname=$(basename "$_i")
                source_hook initqueue/online "$ifname"
            done

            if [ "${nm_connected_device_found}" != "yes" ]; then
                # Configure NM based on the cmdline now updated with kickstart.
                # The configuration will be applied by the next run of NM
                # via settled hook in *-nm-run.sh script which also calls the
                # online hooks.
                . "$hookdir"/cmdline/*-nm-config.sh
            fi
        else
            # make dracut create the net udev rules (based on the new cmdline)
            . "$hookdir"/pre-udev/*-net-genrules.sh
            udevadm control --reload
            udevadm trigger --action=add --subsystem-match=net
            for netif in $(online_netdevs); do
                source_hook initqueue/online "$netif"
            done
        fi
    fi

    # and that's it - we're back to the mainloop.
    true > /tmp/ks.cfg.done # let wait_for_kickstart know that we're done.
}

wait_for_kickstart() {
    echo "[ -e /tmp/ks.cfg.done ]" > "$hookdir/initqueue/finished/kickstart.sh"
}

wait_for_updates() {
    echo "[ -e /tmp/liveupdates.done ]" > "$hookdir/initqueue/finished/updates.sh"
}

wait_for_dd() {
    echo "[ -e /tmp/dd.done ]" > "$hookdir/initqueue/finished/dd.sh"
}
