#!/bin/sh
#
# Set up diskless/stateless workstations for Debian Edu, using LTSP 
#
# Author: Finn-Arne Johansen
# Date:   2006-01-25

# Make sure we terminate on the first error instead of making a mess
set -e

cat <<EOF

info: See http://wiki.debian.org/DebianEdu/HowTo/LtspDisklessWorkstation for
info: instructions on how to set up and configure a diskless workstation
info: using this script.

EOF

info() {
    echo "info: $@"
}

warning() {
    echo "warning: $@"
}

error() {
    echo "error: $@"
}

target=

while [ $# -gt 0 ] ; do 
  case "$1" in 
    --target) target="$2" ; shift ;;
  esac
  shift
done

# Make the installation a bit more quiet when started from the command line
# The locale is not setup in the thin client chroot (yet?)
OLD_LC_ALL=$LC_ALL
LC_ALL=C
export LC_ALL

#avoid running deamons in the chroot
LTSP_HANDLE_DAEMONS=false
export LTSP_HANDLE_DAEMONS

if [ 'amd64' = $(/usr/bin/dpkg --print-architecture) ] ; then
    arch=i386
else
    arch=$(dpkg --print-architecture)
fi

test "$target" || target=/opt/ltsp/$arch

if [ ! -f $target/etc/ltsp_chroot ] ; then
    error "$target/etc/ltsp_chroot do not exist."
    error "You need to run debian-edu-ltsp first."
    exit 1
fi

optsize=$(LANG=C df -mP $target | grep -v Filesystem|awk '{print $2}')
sizeneeded=5073
if [ "$sizeneeded" -gt "$optsize" ] ; then
    warning "The diskless workstation setup need at least $sizeneeded MiB disk space in"
    warning "total in /opt/ltsp/$arch/.  Only $optsize MiB was detected."

    # Stupid LVM do not understand the new device paths, map to the device path LVM understand
    optdev=$(LANG=C df -mP $target | grep -v Filesystem|awk '{print $1}')
    optdev=$(LANG=C ls -ld /dev/*/* 2>/dev/null | grep -- "-> $optdev" | rev | awk '{print $3}' | rev)

    warning "Trying to extend $optdev to $sizeneeded MiB."
    sizeneeded=$(( $sizeneeded + 128 )) # Add 128 for the LVM and fs overhead
    if lvresize -L${sizeneeded}m $optdev && resize2fs $optdev ; then
        :
    else
        error "Resize of $optdev failed."
        exit 1
    fi
fi

in_target() {
    chroot $target "$@"
}

# Create a new resolv.conf based on the one on the host if the current
# one do not work.
# FixME, this will not work if server have no net during setup, and is
# not a main-server
fix_target_dns() {
    testname=www.skolelinux.org
    resolvconf=$target/etc/resolv.conf
    if getent hosts $testname ; then
	if [ ! -L $resolvconf ] && in_target getent hosts $testname ; then
	    info "DNS lookup is working in $target and is not a symlink"
	else
	    info "Replacing $resolvconf to fix DNS lookup on clients"
	    rm -rf $resolvconf
	    if echo "$PROFILE" | egrep -q 'Main-Server' ; then
	        # Must be combined Main-Server + Thin-Client-Server
	        dnsserver=192.168.0.254
	    else
	        dnsserver=10.0.2.2
	    fi
	    sed -e "s:127.0.0.1:$dnsserver:g" /etc/resolv.conf | \
		grep -v "^#" > $resolvconf || true
	    chmod 0644 $resolvconf
	fi
    else
	# DNS is not working in the host environment, nothing to do
	warning "DNS is not working in LTSP host environment."
	cat /etc/resolv.conf | sed 's/^/info: host resolv.conf: /'
	# Replace resolvconf symink with something that might work
	rm $resolvconf
	cat > $resolvconf <<EOF
search intern
nameserver=10.0.2.2
EOF
    fi
}

# Insert commands to be executed when the script is terminated
exit_handler() {
    # Unmount if anything is mounted
    for dir in $umounts ; do
	info "Unmounting $dir"
	umount $dir || true
    done

    if [ -f $target/etc/apt/apt.conf.d/ltsp-make-client ] ; then
	rm $target/etc/apt/apt.conf.d/ltsp-make-client
    fi

    # Clean up changes done to the tasksel behaviour earlier and go
    # back to the default behavior.
    for test in desktop new-install ; do
	if [ -f $target/usr/lib/tasksel/tests/$test.edu ] ; then
	    rm $target/usr/lib/tasksel/tests/$test
	    chroot $target dpkg-divert --package debian-edu-install --rename --quiet \
		--remove /usr/lib/tasksel/tests/$test
	    rm $target/usr/lib/tasksel/tests/$test.edu
	fi
    done

    if [ true = "$run_successfull" ] ; then
	info "conversion of LTSP to diskless workstation ended successfully"
    else
	error "conversion of LTSP to diskless workstation ended abnormally"
    fi
}
trap exit_handler EXIT INT

echo 'APT::Install-Recommends "no";' > \
    $target/etc/apt/apt.conf.d/90ltsp-make-client

fix_target_dns
in_target apt-get update || true

# Check if server uses mirror or cd to install
if grep -v '^#' $target/etc/apt/sources.list | grep -q file:///cdrom ||
   in_target apt-cache policy ltsp-server | grep -q "cdrom://" ; then 
  mirror=file:///cdrom

  if false ; then
  error "Unable to use CD/DVD as source for converting the ltsp chroot"
  error "to a diskless workstation."
  error "The reason is that the CD/DVD is not a signed APT repository."
  error "Editing $target/etc/apt/sources.list to fix this."
  sed 's%^\(deb file:///cdrom .*\)$%# \1%' \
    < $target/etc/apt/sources.list > $target/etc/apt/sources.list.new && \
    mv $target/etc/apt/sources.list.new $target/etc/apt/sources.list
  fi
  # Our CDs and DVDs are unsigned, so we can not check authentication
  # when using them.  These options should work with apt and aptitude.
  (
      echo 'APT::Get::AllowUnauthenticated "true";'
      echo 'APT::Authentication::TrustCDROM "true";'
      echo 'Aptitude::Cmdline::ignore-trust-violations "true";'
  ) >> $target/etc/apt/apt.conf.d/90ltsp-make-client
else
  mirror=http://ftp.debian.org/debian
fi

# Mount the CD ROM if needed
umounts=""
cdromdir=""
case $mirror in
    file:///cdrom)
        if mount -t iso9660 /dev/cdrom /cdrom ; then
            umounts="/cdrom"
	fi
	cdromdir="/cdrom"
	;;
    file:///media/cdrom)
        if mount -t iso9660 /dev/cdrom /media/cdrom ; then
            umounts="/media/cdrom"
	fi
	cdromdir="/media/cdrom"
	;;
    *)
        ;;
esac


if [ -e "$cdromdir/.disk/info" ] ; then 
  umounts="$target/cdrom $umounts"
  mount --bind $cdromdir  $target/cdrom
fi

umounts="$target/proc $umounts"
mount -t proc proc  $target/proc

# Bind-mount /var/cache/apt/archives/ on $target/var/cache/apt/archives/,
# to avoid having to download packages twice.
umounts="$target/var/cache/apt/archives $umounts"
mount --bind /var/cache/apt/archives $target/var/cache/apt/archives

# And make sure our archive is allowed
info "Installing debian-edu-archive-keyring"
in_target apt-get install -y -q debian-edu-archive-keyring
info "Updating package list from APT sources (apt-get update)"
in_target apt-get update

# Add Edu config with language and profile.
mkdir -p $target/etc/debian-edu
sed 's/^PROFILE=\".*/PROFILE=\"Workstation\"/' \
    < /etc/debian-edu/config > $target/etc/debian-edu/config

# Add preseeding values, without the popularity-contest settings to
# avoid several hosts reporting with the same popcon ID.
(
    cat /usr/lib/debian-edu-install/defaults.common \
        /usr/lib/debian-edu-install/defaults.networked \
        /usr/lib/debian-edu-install/defaults.workstation
    debconf-get-selections | egrep "^locales|^popularity-contest"
) | in_target debconf-set-selections

if [ -f /etc/locale.gen -a ! -f $target/locale.gen ] ; then 
  cp /etc/locale.gen $target/etc/locale.gen 
fi

if [ -f /etc/environment -a ! -f $target/etc/environment ] ; then 
  cp /etc/environment $target/etc/environment
fi

# set the timezone
cp -d /etc/localtime $target/etc 
cp -d /etc/timezone $target/etc 

# Make the install Silent
DEBIAN_FRONTEND=noninteractive
export DEBIAN_FRONTEND

# Instruct LTSP policy-rc.d to tell invoke-rc.d to not start any
# daemons
LTSP_HANDLE_DAEMONS=false
export LTSP_HANDLE_DAEMONS

# install some packages needed by tasksel
in_target apt-get install -y -q \
    education-tasks

# Install all edu-dependencies, using the task used by d-i.

# Set up tasksel to select the debian-edu task packages to install the
# same way d-i select which packages to install.

# Modify tasksel desktop test so the normal Debian desktop profiles
# are not installed. Use dpkg-divert to to switch to our version while
# tasksel runs.
file=/usr/lib/tasksel/tests/desktop
sed -e 's/if desktop_hardware/# Do not install a desktop - added by debian-edu-install\nunmark\n\n&/' \
  <$target$file >$target$file.edu
in_target dpkg-divert --package debian-edu-install --rename --quiet --add $file
ln -s ./desktop.edu $target$file

# And for the standard system task too
file=/usr/lib/tasksel/tests/new-install
sed -e 's/^case/# Do not install standard system task - added by debian-edu-install\nexit 3\n\n&/' \
  <$target$file >$target$file.edu
in_target dpkg-divert --package debian-edu-install --rename --quiet --add $file
ln -s ./new-install.edu $target$file

# Pass on the current desktop setting and the profile we want to install
(
    debconf-get-selections | grep tasksel/desktop
    echo debian-edu-install debian-edu-install/profile multiselect Workstation
) | in_target debconf-set-selections

info "installing packages using tasksel"
LC_ALL=$OLD_LC_ALL in_target tasksel --new-install

ADDPKGS=""

# FIXME Add Samba when we have working thin client smb.conf
#ADDPKGS="$ADDPKGS samba"

# Why are these added to the installation?  For SMB
# mounted home directories? [pere 2007-07-11]
# Disabled for now [pere 2010-01-09]
#ADDPKGS="$ADDPKGS libpam-mount smbfs"

# Make it possible to localize the installation
ADDPKGS="$ADDPKGS localization-config"

# All Packages may not exist on the CD, so we
# install one at a time, to get as many as possible
for PKG in $ADDPKGS ; do
    if in_target apt-get install -y -q $PKG ; then
	:
    else
	warning "Failed to install $PKG"
    fi
done

# Get rid of lvm2, as it causes the shutdown to hang.
chroot $target apt-get -y purge lvm2

# Configure locale specific settings.
if [ "$LANG" ] ; then
    info "Running update-locale-config $LANG"
    in_target update-locale-config $LANG
else
    warning "LANG not set, not changing locale setting in chroot."
fi

# Kill processes that are currently running on $target
if [ "$CHROOTPART" = "$target" ] ; then 
  fuser -mkv $target/ || /bin/true
fi

only_run_on_rclevel() {
  runlevel=$1 # Use level N to disable on all levels
  shift
  for service in $@ ; do
      info "Making sure init.d/$service only run in runlevel $runlevel"
      for link in $(cd $target/etc; ls rc[S2345].d/[SK]*$service || true); do
	  # link now look like 'rcS.d/S99foo'
	  set `echo $link|sed "s%rc\(.\).d\/\([SK]\)\(..\)$service%\1 \2 \3%"`
	  lvl=$1
	  action=$2
	  seq=$3
	  if [ "K" = "$action" ] && [ "$runlevel" = "$lvl" ]; then
	      mv $target/etc/$link $target/etc/rc$lvl.d/S$seq$service
	  fi
	  if [ "S" = "$action" ] && [ "$runlevel" != "$lvl" ]; then
	      mv $target/etc/$link $target/etc/rc$lvl.d/K$seq$service
	  fi
      done
  done	
}

enable_on_rclevel() {
    service=$1
    shift
    if [ ! -f $target/etc/init.d/$service ] ; then
	error "Unable to enable service $service, do not exist"
	return
    fi
    info "Making sure init.d/$service run in runlevel $@"
    for runlevel in $@ ; do
	rcd=$target/etc/rc$runlevel.d
	if [ ! -f $rcd/S??$service ] ; then
	    seq=""
	    for link in $(cd $target/etc; ls rc$runlevel.d/K??$service 2>/dev/null || true); do
		set `echo $link|sed "s%rc\(.\).d\/\([SK]\)\(..\)$service%\1 \2 \3%"`
		seq=$3
		mv $rcd/K??$service $rcd/S$seq$service
	    done
	    if [ -z "$seq" ] ; then # No stop link found, create start link
		# Assume insserv will pick a better sequence number
		ln -s ../init.d/$service $rcd/S01$service
	    fi
	fi
    done
}

# Needed to get the loopback interface
only_run_on_rclevel S ifupdown networking

# Needed to get utmp (and w/who working)
only_run_on_rclevel S bootmisc.sh

# a lot of services should only be started in runlevel 3, eg, when
# running in workstation mode.  Portmap should not start in rcS.d, to
# avoid it on thin clients.
enable_on_rclevel portmap 3
enable_on_rclevel nfs-common 3
enable_on_rclevel dbus 3
enable_on_rclevel hal 3
only_run_on_rclevel 3 kdm xfs nscd cupsys autofs pulseaudio \
    portmap nfs-common nslcd rwhod saned cups timidity jackd

# a lot of services should only be started in runlevel 4, eg. when running
# in thin client mode
only_run_on_rclevel 4 ltsp-client samba ltsp-client-core

# Some services should not be started on the terminals
only_run_on_rclevel N munin-node cron report-reboot open-backdoor enable-nat \
    cfengine2 start-wlan xdebian-edu-firstboot \
    readahead readahead-desktop stop-readahead\
    resize_lvm hdparm xfs rsync hddtemp fam resolvconf fetch-ldap-cert

# check which network the install is on, and start either as diskless
# workstation or thin client.  Make sure to insert these after using
# only_run_on_rclevel, as the latter do not invoke update-rc.d to
# reorder scripts based on dependencies.
ln -s /usr/share/debian-edu-config/ltsp_set_runlevel \
    $target/etc/init.d/ltsp_set_runlevel
in_target update-rc.d ltsp_set_runlevel start 99 S .

# make cdrom and other static local devices work for diskless
# workstation.
ln -s /usr/share/debian-edu-config/ltsp_local_mount \
    $target/etc/init.d/ltsp_local_mount
in_target update-rc.d ltsp_local_mount start 01 3 .

append_if_missing() {
    file="$1"
    pattern="$2"
    if [ -f "$file" ] && grep -q "$pattern" "$file" ; then
	:
    else
	(
	    if [ -f "$file" ] ; then cat "$file" ; fi
	    cat -
	) > "$file.new" && mv "$file.new" "$file"
    fi
}

# Make sure our extra configuration settings are used
setupfile=$target/etc/default/ltsp-client-setup
cat <<'EOF' | append_if_missing $setupfile "/usr/share/debian-edu-config/default-ltsp-client-setup"
# Add some configuration needed by diskless workstations
. /usr/share/debian-edu-config/default-ltsp-client-setup
EOF

# Make sure ifupdown knows about loopback
cat <<'EOF' | append_if_missing $target/etc/network/interfaces \
    "iface lo inet loopback"
# The loopback network interface
auto lo
iface lo inet loopback
    dns-search intern
EOF

## set up samba for thinclients
# Fixme - provide a working smb.conf
#ln -sf smb-thinclient-debian-edu.conf $target/etc/samba/smb.conf

# Create /skole, since the chroot will be read-only
mkdir -p $target/skole

# Make sure self signed SSL certificate for internal web site also
# work on diskless workstations.  Must do this before running
# cfengine, to makesure snakeoil-on-ice do not complain
OVERRIDE_FILE=/etc/iceweasel/profile/cert_override.txt
if [ -f $OVERRIDE_FILE ] ; then
    mkdir -p $(dirname $target$OVERRIDE_FILE)
    if cp $OVERRIDE_FILE $target$OVERRIDE_FILE ; then
	chmod a+r $target$OVERRIDE_FILE
    else
	echo -e "error: Can't copy the new Iceweasel override settings to LTSP."\
	"\n	Iceweasel profiles created on a thin client will not accept"\
	"\n	the new certificate" 1>&2
    fi
else
    error "Fail to find Iceweasel certificate override file."
fi
ls -l $target$OVERRIDE_FILE |sed "s%^%info: $0: snakeoil-on-ice: %"

# Make the ldap users availible in the chroot
in_target cfengine-debian-edu -Dinstallation

# Fetch the LDAP SSL cert while the file system is writable.
# Make sure pam-ldapd SSL certificate is available on the clients
if [ ! -f $target/etc/ldap/ssl/ldap-server-pubkey.pem ] ; then
    if [ -f /etc/ldap/ssl/slapd.pem ] ; then
        # Use the current one of the server if present.  Used for
        # combined main-server+thin-client-server installations.
	awk '/^-----BEGIN CERTIFICATE-----$/ { yes=1 }
             yes { print }
             /^-----END CERTIFICATE-----$/ { yes=0 }' \
		 < /etc/ldap/ssl/slapd.pem \
		 > $target/etc/ldap/ssl/ldap-server-pubkey.pem
	info "Copied LDAP SSL certificate from host."
    elif [ -x $target/etc/init.d/fetch-ldap-cert ] && \
	in_target /etc/init.d/fetch-ldap-cert start; then
        # Used for thin-client-server when the main-server is
        # available during installation.
	info "Fetched LDAP SSL certificate from main-server."
    else
        # Used for thin-client-server when the main-server is not
        # available during installation.
	# This make it easier to hijack the LDAP connections from
	# clients.
	warning "LDAP SSL certificate not found, clients will download dynamically at boot every time."

	cat <<'EOF' | append_if_missing $setupfile "/etc/ldap/ssl"
# Make sure init.d/fetch-ldap-cert can work at boot if needed
if [ ! -f /etc/ldap/ssl/ldap-server-pubkey.pem ] ; then
    copy_dirs="$copy_dirs /etc/ldap/ssl"
fi
EOF
	# Enable boot script too
	only_run_on_rclevel S fetch-ldap-cert
    fi
fi

fix_target_dns

# it is nice to have a running ssh for the admin
rm -f $target/etc/ssh/sshd_not_to_be_run

if [ -f $target/etc/kde3/kdm/kdmrc ] ; then
    # Set up randomdevice for kdm if it is installed
    update-ini-file $target/etc/kde3/kdm/kdmrc 'General' RandomDevice \
	/dev/urandom

    #Include pammount config in /etc/pam.d/kdm
    KDM_PAM=$target/etc/pam.d/kdm
    KDM_ADD=$(grep -n ^auth $KDM_PAM | head -1 | cut -f1 -d:)

    # Disabled, as it is not clear why this is needed, and kdm login
    # fail when /etc/pam.d/common-pammount is missing. [pere 2009-12-26]
    if false ; then
        cat << EOF | patch -Np0 $KDM_PAM
--- $KDM_PAM
+++ $KDM_PAM.new
@@ -$KDM_ADD,0 +$KDM_ADD,2 @@
+@include common-pammount
+
EOF
    fi
fi

# Need to set up some boot info in dhcpd.conf
# FIXME I guess this also will fail if done on a thin-client-server
# without proper connection
# But then again - the boot information should then be located on the
# main server
NETIF=eth0
DHCP_OLD=/etc/dhcp3/dhcpd-debian-edu.conf
DHCP_NEW=/etc/dhcp3/dhcpd-stateless-debian-edu.conf
DHCP_ADD=
IP_ETH=$(/sbin/ifconfig $NETIF | sed -ne 's/ *inet addr:\([0-9.]*\) .*/\1/p')
NET_ETH=$(/sbin/route -n | awk "/$NETIF/ { print \$1 }"| head -1 )
if [ -z "$NET_ETH" ] ; then 
  NET_ETH="10.0.2.0"
fi
MASK_ETH=$(/sbin/route -n | awk "/$NETIF/ { print \$3 }"| head -1 )
if [ -z "$MASK_ETH" ] ; then 
  MASK_ETH="255.255.254.0"
fi
INTERNAL_NET=$(grep -n "shared-network INTERNAL" $DHCP_OLD | cut -f1 -d:)
THIN_NET=$(grep -n "shared-network THINCLIENTS"  $DHCP_OLD | cut -f1 -d:)

if [ "$INTERNAL_NET" ] ; then # Not LDAP based DHCP configuration
  OPTION_END=$(head -$THIN_NET $DHCP_OLD | tail -n +$INTERNAL_NET  | \
               grep -ne "^ *option" | tail -1 | cut -f1 -d:)
  PXEFILE="$(tail -n +$THIN_NET $DHCP_OLD | grep -e '^ *filename.*pxe' | head -1)"

  head -$THIN_NET $DHCP_OLD | tail -n +$INTERNAL_NET | \
    grep -qe "^ *filename" || DHCP_ADD="$DHCP_ADD$PXEFILE\n"
  head -$THIN_NET $DHCP_OLD | tail -n +$INTERNAL_NET | \
    grep -qe "^ *option root-path" || \
    DHCP_ADD="$DHCP_ADD  option root-path \"$IP_ETH:$target\";\n"

  if [ "$DHCP_ADD" ] ; then
    DHCP_SPLIT=$(expr $INTERNAL_NET + $OPTION_END)
    head -$DHCP_SPLIT $DHCP_OLD > $DHCP_NEW
    echo "$DHCP_ADD" >> $DHCP_NEW
    tail -n +$DHCP_SPLIT $DHCP_OLD >> $DHCP_NEW
    test -h /etc/dhcp3/dhcpd.conf && \
      ln -sf dhcpd-stateless-debian-edu.conf /etc/dhcp3/dhcpd.conf
    /usr/sbin/invoke-rc.d dhcp3-server restart || true # Is this a fatal error?
  fi
fi

# Export filesystem to the stateless machines if it is not exported already
if grep -q "^$target " /etc/exports ; then
    :
else
    echo "$target $NET_ETH/$MASK_ETH(ro,async,no_root_squash,subtree_check)" >> /etc/exports
    /usr/sbin/invoke-rc.d nfs-kernel-server restart
fi

run_successfull=true # report success to exit_handler()
