#! /bin/bash

#*********************************************************************
#
# fai-mirror -- create and manage a partial mirror for FAI
#
# This script is part of FAI (Fully Automatic Installation)
# (c) 2004-2016, Thomas Lange, lange@informatik.uni-koeln.de
#
#*********************************************************************
# 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; see the file COPYING. If not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
# MA 02111-1307, USA.
#*********************************************************************

# variables: NFSROOT, FAI_CONFIGDIR, FAI_ETC_DIR

export FAI_ROOT=/ # do not execute in chroot, needed for install_packages call
export PATH=$PATH:/usr/sbin

trap "umount_dirs" EXIT ERR
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
usage() {

    echo "fai-mirror -- create and manage a partial mirror for FAI."
    echo "Please read the manual page fai-mirror(1)."
    exit
}
# - - - - - - - - - - - - - - - - - - - - - - - - - -
die() {

    local e=$1   # first parameter is the exit code
    shift

    echo "$@" >&2 # print error message
    exit $e
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
excludeclass() {

    # removes/excludes all classes in $* from $classes
    local insert eclasses newclass c e

    eclasses="$*"
    eclasses=${eclasses//,/ }

    for c in $classes; do
        insert=1
        for e in $eclasses; do
          [ $c = $e ] && insert=0
        done
        [ $insert = 1 ] && newclass="$newclass $c"
    done
    classes="$newclass"
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
umount_dirs() {

    [ "$FAI_DEBMIRROR" ] && umount $MNTPOINT 2>/dev/null 1>&2 || true
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cleandirs() {

    return # currently nothing to do
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
initialize() {

    # TODO: root is only needed when FAI_DEBMIRROR is defined. Then we
    # must mount a directory

    aptcache=$mirrordir/aptcache   # holds the package cache data
    archivedir=$aptcache/var/cache/apt/archives
    statefile=$aptcache/statefile

    # also used in install_packages.conf
    export aptoptions=" \
      -o Aptitude::Log=/dev/null \
      -o Aptitude::CmdLine::Ignore-Trust-Violations=yes\
      -o APT::Get::AllowUnauthenticated=true \
      -o DPkg::force-conflicts::=yes \
      -o Dir::State=$aptcache/var/lib/apt \
      -o Dir::Log=$aptcache/var/log/apt \
      -o Dir::State::extended_states=$aptcache/var/lib/apt/lists/extended_states \
      -o Dir::State::status=$statefile \
      -o Dir::Cache=$aptcache/var/cache/apt \
      -o Dir::State=$aptcache/var/cache/apt \
      -o Dir::Cache::Archives=$archivedir \
      -o Dir::Etc=$aptcache/etc/apt/ \
      -o Dir::State::Lists=$aptcache/var/lib/apt/lists/"

    # used by install_packages
    export FAI_ARCHIVE_DIR=$archivedir

    if [ -n "$arch" ]; then
        aptoptions="$aptoptions -o APT::Architecture=$arch" # add architecture
    fi

    # we only need some empty dirs
    set -e
    mkdir -p $archivedir/partial $aptcache/etc/apt/preferences.d $aptcache/var/lib/apt/lists/partial $aptcache/var/log/apt
    > $statefile
    set +e
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
delete_base_packages() {

    # now delete all packages that are already included in base.tar.xz
    local p all arch

    if [ ! -f $NFSROOT/var/tmp/base-pkgs.lis ]; then
        echo "$NFSROOT/var/tmp/base-pkgs.lis not available."
        echo "Can't remove wasteful packages that are already in base.tar.xz."
        return
    fi
    echo "Removing packages that are already included in base.tar.xz"
    for p in $(< $NFSROOT/var/tmp/base-pkgs.lis); do

        all=(${p//:/ })
        p=${all[0]}
        arch=${all[1]}

        if [ -f $archivedir/${p}_*$arch.deb ]; then
            [ $verbose -eq 1 ] && echo "deleting package $p $arch"
            rm $archivedir/${p}_*$arch.deb
        fi
    done
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
add_base_packages() {

    local plist
    # add packages from base.tar.xz and additional packages in nfsroot

    # arch dependent packages defined in fai-make-nfsroot
    echo "Adding packages of $cfdir/NFSROOT."
    if [ -f $NFSROOT/var/tmp/packages.nfsroot ]; then
        plist=$(< $NFSROOT/var/tmp/packages.nfsroot)
        apt-get $qflag -d $aptoptions -y --fix-missing install $plist
    else
        echo "WARNING: $NFSROOT/var/tmp/packages.nfsroot does not exists." >&2
        echo "Can't add those packages. Maybe the nfsroot is not yet created." >&2
    fi

    mv $aptcache/etc/apt/sources.list $aptcache   # save sources.list
    # now use different sources.list for debootstrap packages
    echo "$FAI_DEBOOTSTRAP" | awk '{printf "deb %s %s main\n",$2,$1}' | perl -p -e 's/file:/copy:/' > $aptcache/etc/apt/sources.list
    echo "Adding packages from $NFSROOT/var/tmp/base-pkgs.lis"
    if [ -f $NFSROOT/var/tmp/base-pkgs.lis ]; then
        plist=$(< $NFSROOT/var/tmp/base-pkgs.lis)
        apt-get $qflag $aptoptions update >/dev/null
        apt-get $qflag -d $aptoptions -y --fix-missing install $plist
    else
        echo "WARNING: $NFSROOT/var/tmp/base-pkgs.lis does not exists." >&2
        echo "Can't add those packages. Maybe the nfsroot is not yet created." >&2
    fi
    mv $aptcache/sources.list $aptcache/etc/apt/sources.list   # restore sources.list
    apt-get $qflag $aptoptions update >/dev/null
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
set-classes() {

    # if -c is given, ignore -x
    if [ -n "$cclasses" ]; then
        export classes=${cclasses//,/ }
        return
    fi

    set +e
    # all available file names are classes
    classes=$(cd $FAI_CONFIGDIR/package_config; ls -1 | egrep -i "^[a-zA-Z0-9_-]+$")
    addclasses=$(grep -h PACKAGES $FAI_CONFIGDIR/package_config/* | sed -e 's/#.*//' | awk '{printf $3"\n"$4"\n"$5"\n"$6"\n"}')
    export classes=$(echo -e "$classes\n$addclasses\n" | sort | uniq)
    [ -n "$exclasses" ] && excludeclass $exclasses
    set -e
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

preserve=0
verbose=0
add=1
qflag=-qq
bcount=0
while getopts "a:bBvhx:pc:C:m:P:" opt ; do
    case "$opt" in
        a) arch=$OPTARG ;;
        B) add=0 ; ((bcount++)) ;;
        b) add=2 ; ((bcount++)) ;;
        C) cfdir=$OPTARG ;;
        h) usage ;;
        x) exclasses="$OPTARG";;
        c) cclasses="$OPTARG";;
        m) MAXPACKAGES="$OPTARG";;
        p) preserve=1;;
        v) verbose=1; vflag=-v; qflag='';;
        P) aptpref="$OPTARG";;
        ?) die 1 "Unknown option";;
    esac
done
shift $(($OPTIND - 1))

# use FAI_ETC_DIR from environment variable
[ -n "$FAI_ETC_DIR" -a -z "$cfdir" ] && echo "Using environment variable \$FAI_ETC_DIR."
# use -C option if present otherwise use $FAI_ETC_DIR or default to /etc/fai
[ -z "$cfdir" ] && cfdir=${FAI_ETC_DIR:=/etc/fai}
cfdir=$(readlink -f $cfdir) # canonicalize path
[ ! -d "$cfdir" ] && die 6 "$cfdir is not a directory"
[ "$verbose" -eq 1 ] && echo "Using configuration files from $cfdir"
[ $bcount -gt 1 ] && die 7 "You can't use -b and -B simultaneously."
. $cfdir/fai.conf
. $cfdir/nfsroot.conf
: ${MNTPOINT:=/media/mirror}  # default value

export NFSROOT

[ -x "$(which reprepro)" ] || die 8 "reprepro not found. Please install the package."
[ -n "$exclasses" -a -n "$cclasses" ] && die 3 "Options -x and -c not allowed at the same time."

# use first argument if given, use variable mirrordir if not argument was given
[ -n "$1" ] && mirrordir=$1
[ -z "$mirrordir" ] && die 2 "Please give the absolute path to the mirror."
{ echo $mirrordir | egrep -q '^/'; } || die 4 "Mirrordir must start with a slash /."

[ -d $FAI_CONFIGDIR/package_config ] || die 6 "Can't find package config files in $FAI_CONFIGDIR."

# set default if undefined
: ${MAXPACKAGES:=1}
export MAXPACKAGES

initialize

# if we are using nfs mounts for Debian mirror, this may fail here, since inside a chroot environment different dir are used
# if sources.list includes file AND FAI_DEBMIRROR is defined we have to mount
# otherwise mounting is not needed. call task_mirror
if [ "$FAI_DEBMIRROR" ]; then
    mkdir -p $MNTPOINT
    mount -r $FAI_DEBMIRROR $MNTPOINT || exit 9
fi

# TODO: use -p to preserve sources.list
sed -e 's/file:/copy:/' $cfdir/apt/sources.list > $aptcache/etc/apt/sources.list
if [ "$(ls -A $cfdir/apt/sources.list.d/ 2>/dev/null)" ]; then
    sed -e 's/file:/copy:/' $cfdir/apt/sources.list.d/* >> $aptcache/etc/apt/sources.list
fi

if [ -f "$aptpref" ]; then
    cp "$aptpref" $aptcache/etc/apt/preferences
fi
echo "Getting package information"
apt-get $qflag $aptoptions update >/dev/null

set-classes
[ $add -eq 1 ] && add_base_packages
echo "Downloading packages for classes:" $classes
FAI=$FAI_CONFIGDIR install_packages -N -d $vflag
umount_dirs
trap "" EXIT ERR
[ $add -eq 0 ] && delete_base_packages

# create mirror directory structure
echo "Calling reprepro"
mkdir $mirrordir/conf
cat > $mirrordir/conf/distributions <<EOF   # generate config file for reprepro
Codename: cskoeln
Architectures: i386 amd64
Components: main non-free contrib
DebIndices: Packages Release . .xz
Description: FAI packages from author's repository
Label: fai-project.org
Origin: Original FAI archive
Suite: jessie
EOF
# '

# maybe using reprepro pulls it's possible to move instead of copy the packages
reprepro -b $mirrordir includedeb cskoeln $mirrordir/aptcache/var/cache/apt/archives/*.deb
rm -r $mirrordir/aptcache/var/cache/apt/archives/*.deb

echo "$0 finished."
echo -n "Mirror size and location: ";du -sh --exclude aptcache --exclude db $mirrordir
cleandirs
