#!/bin/bash
# lxc: linux Container library

# Authors:
# Daniel Lezcano <daniel.lezcano@free.fr>
# Jean-Marc Lacroix <jeanmarc.lacroix@free.fr> (2011)

# This library is free software; you can redistribute it and/or modify
# it under  the  terms of the   GNU Lesser  General  Public License as
# published by the Free Software Foundation; either version 2.1 of the
# License, or (at your option) any later version.

# This library 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
# Lesser General Public License for more details.

# You should  have received a  copy of the   GNU Lesser General Public
# License along with this library; if not, write  to the Free Software
# Foundation, Inc., 59 Temple Place,  Suite 330, Boston, MA 02111-1307
# USA


#define minimal lxc list package
minimal_lst_packages="
ifupdown,\
locales,\
libui-dialog-perl,\
dialog,\
dhcp3-client,\
netbase,\
net-tools,\
iproute,\
openssh-server"

configure_debian()
{
    rootfs=$1
    hostname=$2

    # configure the inittab
    cat <<EOF > $rootfs/etc/inittab
id:3:initdefault:
si::sysinit:/etc/init.d/rcS
l0:0:wait:/etc/init.d/rc 0
l1:1:wait:/etc/init.d/rc 1
l2:2:wait:/etc/init.d/rc 2
l3:3:wait:/etc/init.d/rc 3
l4:4:wait:/etc/init.d/rc 4
l5:5:wait:/etc/init.d/rc 5
l6:6:wait:/etc/init.d/rc 6
# Normally not reached, but fallthrough in case of emergency.
z6:6:respawn:/sbin/sulogin
1:2345:respawn:/sbin/getty 38400 console
c1:12345:respawn:/sbin/getty 38400 tty1 linux
c2:12345:respawn:/sbin/getty 38400 tty2 linux
c3:12345:respawn:/sbin/getty 38400 tty3 linux
c4:12345:respawn:/sbin/getty 38400 tty4 linux

EOF

    # as there are    4  /dev/ttyxx device  in  inittab,   then create
    # associated mknod device  in rootfs, otherwise error in container
    # :'..respawning too    fast  : disabled    ...'   but  on Debian,
    # according  release  version ,  there   is or not  already  mknod
    # definition ...

    if [ "$opt_distrib" != "lenny" ]; then 
		mknod --mode=600 $rootfs/dev/tty1 c 4 1
		mknod --mode=600 $rootfs/dev/tty2 c 4 2
		mknod --mode=600 $rootfs/dev/tty3 c 4 3
		mknod --mode=600 $rootfs/dev/tty4 c 4 4
		chown root.tty  $rootfs/dev/tty*
	fi
    # disable selinux in debian
    mkdir -p $rootfs/selinux
    echo 0 > $rootfs/selinux/enforce

    # configure the network using the dhcp
    cat <<EOF > $rootfs/etc/network/interfaces
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
EOF

    # set the hostname
    cat <<EOF > $rootfs/etc/hostname
$hostname
EOF

    # reconfigure some services
    if [ -z "$LANG" ]; then
	  chroot $rootfs locale-gen en_US.UTF-8
	  chroot $rootfs update-locale LANG=en_US.UTF-8
    else
	  chroot $rootfs locale-gen $LANG
	  chroot $rootfs update-locale LANG=$LANG
    fi

    # remove pointless services in a container
    chroot $rootfs /usr/sbin/update-rc.d -f umountfs remove
    chroot $rootfs /usr/sbin/update-rc.d -f hwclock.sh remove
    chroot $rootfs /usr/sbin/update-rc.d -f hwclockfirst.sh remove

    echo "root:root42" | chroot $rootfs chpasswd
    echo "$0: Warning, Root password is 'root42' in your new $opt_path VM, please change it !"
    echo "$0: installation OK, please launch : sudo lxc-start --name $opt_name --rcfile $opt_path/config"
    return 0
}

download_debian()
{

    cache=$1
    arch=$2
    distrib=$3

    # check if the mini debian was not already downloaded
    dir_cache=$cache/partial-$arch-$distrib
    mkdir -p $dir_cache
    if [ $? -ne 0 ]; then
	  echo "$0: error: Failed to create '$dir_cache' directory"
	  return 1
    fi

	if [ "$opt_packages" == "" ]; then
		echo "$0: info: option --packages not used, using now debootstrap with minbase variant option"
		opt_variant="--variant=minbase"
	else
		echo "$0: info: option --packages used, use debootstrap with no variant option"
		opt_variant=""
	fi

    # download a mini debian into a cache
    echo "Downloading $arch Debian minimal in '$dir_cache' in progress..."
    debootstrap \
	$opt_variant \
	--verbose \
	--arch=$arch \
	--include="$minimal_lst_packages,$opt_packages" \
	$distrib \
	$dir_cache \
	http://ftp.debian.org/debian
    if [ $? -ne 0 ]; then
	  echo "$0: error: Failed to download the rootfs with debootstrap, aborting job"
	  return 1
    fi

    mv "$dir_cache" "$1/rootfs-$arch-$distrib"
    echo "$0: info : Download complete, '$1/rootfs-$arch-$distrib' ready for installation..."

    return 0
}

copy_debian()
{
    cache=$1
    arch=$2
    rootfs=$3
    distrib=$4

    # make a local copy of the minidebian
    echo  "Copying rootfs to $rootfs..."
    cp -a $cache/rootfs-$arch-$distrib/* $rootfs || return 1
    return 0
}

install_debian()
{
    rootfs=$1
    mkdir -p /var/lock/subsys/
    (
	flock -n -x 200
	if [ $? -ne 0 ]; then
	    echo "$0: error: Cache repository is busy."
	    return 1
	fi


	# test if user has forced architecture ....
	if [ "$opt_force_arch" == "false" ] ; then
		# read current architecture on running OS, and according result,
		# force correct value 
		arch_os=$(arch)
		if [ "$arch_os" == "x86_64" ]; then
			arch_deb=amd64
		else
			if [ "$arch_os" == "i686" ]; then
				arch_deb=i386
			fi
		fi
	else
		# of course, we assume in this case that running architecture 
		# is ALWAYS on Intel/Amd ...		
		arch_deb=$opt_force_arch
	fi
	
	echo "Checking cache previously download in $cache/rootfs-$arch_deb-$opt_distrib ... "
	if [ ! -e "$cache/rootfs-$arch_deb-$opt_distrib" ]; then
	    download_debian $cache $arch_deb $opt_distrib
	    if [ $? -ne 0 ]; then
			echo "$0: error: Failed to download 'Debian base'"
			return 1
	    fi
	else
		echo "Using now previously download in $cache/rootfs-$arch_deb-$opt_distrib"
	fi

	copy_debian $cache $arch_deb $rootfs $opt_distrib
	if [ $? -ne 0 ]; then
	    echo "$0: error: Failed to copy rootfs"
	    return 1
	fi

	return 0

	) 200>/var/lock/subsys/lxc

    return $?
}

copy_configuration()
{
    path=$1
    rootfs=$2
    name=$3

	# remove potential file if debugging multiple time the same config
	rm -f $path/config
    cat <<EOF >> $path/config
lxc.tty = 4
lxc.pts = 1024
lxc.rootfs = $rootfs
lxc.cgroup.devices.deny = a
# /dev/null and zero
lxc.cgroup.devices.allow = c 1:3 rwm
lxc.cgroup.devices.allow = c 1:5 rwm
# consoles
lxc.cgroup.devices.allow = c 5:1 rwm
lxc.cgroup.devices.allow = c 5:0 rwm
lxc.cgroup.devices.allow = c 4:0 rwm
lxc.cgroup.devices.allow = c 4:1 rwm
# /dev/{,u}random
lxc.cgroup.devices.allow = c 1:9 rwm
lxc.cgroup.devices.allow = c 1:8 rwm
lxc.cgroup.devices.allow = c 136:* rwm
lxc.cgroup.devices.allow = c 5:2 rwm
# char device for autofs package, otherwise /etc/init.d.autofs does not run
lxc.cgroup.devices.allow = c 10:235 rwm
# rtc
lxc.cgroup.devices.allow = c 254:0 rwm
# mounts point
lxc.mount.entry=proc   $rootfs/proc proc nodev,noexec,nosuid 0 0
lxc.mount.entry=devpts $rootfs/dev/pts devpts defaults 0 0
lxc.mount.entry=sysfs  $rootfs/sys sysfs defaults  0 0

# container name
lxc.utsname = $name

# first Ethernet device
lxc.network.type  = veth
# first Ethernet device name in container, please, use the same name in /etc/network/interface
lxc.network.name  = eth0
# status of device when LXC container start up
lxc.network.flags = up

# please uncomment according your choices
# second Ethernet device
## lxc.network.type  = veth
# second Ethernet device name in container, please, use the same name in /etc/network/interface
## lxc.network.name  = eth0-admin
# status of device when LXC container start up
##lxc.network.flags = up
# internal connexion of eth0-admin is made to internal bridge br0-admin,
# please create it before with 'sudo brctl addbr br0-admin && sudo brctl addif br0-admin eth0-admin'
##lxc.network.link = br0-admin
# use a static IPV4 addr, remember, must be coherent with /etc/network/interface
##lxc.network.ipv4 = 192.168.1.1/24
# if you would like force a static MAC addr, then uncomment this line
##lxc.network.hwaddr = 00:FF:12:34:56:78

EOF

    if [ $? -ne 0 ]; then
	  echo "$0: error: Failed to add configuration"
	  return 1
    fi

    return 0
}

make_clean()
{

    if [ ! -e $cache ]; then
	  exit 0
    fi

    # lock, so we won't purge while someone is creating a repository
    (
	flock -n -x 200
	if [ $? != 0 ]; then
	    echo "$0: error: Cache repository is busy."
	    exit 1
	fi

	echo -n "Purging the download cache $cache ..."
	rm --preserve-root \
	    --one-file-system \
	    -rf $cache && echo "Done." || exit 1
	exit 0

    ) 200>/var/lock/subsys/lxc
}

make_usage()
{
    cat <<EOF
NAME: lxc-debian

SYNOPSIS
 lxc-debian [-h|--help] or ...
  [-c|--clean]  or ...
  [-p|--path=<path> -n|--name=<hostname> -D|--distrib=[lenny|squeeze|wheezy]]
     [-A|--architecture=[adm64|i386]]
     [-d|--debug]

 -h|--help     (optionnal) this help documentation

 -c|--clean    (optionnal) in order to clean local cache 


 -p|--path    (mandatory)  the  directory   for  LXC   virtual machine
 installation

 -n|--name     (mandatory) the hostname for the LXC virtual machine

 -D|--distrib  (mandatory) the Debian distribution to install

 -d|--debug    (optionnal) in order to debug (use internaly set -x) 

 -P|--packages (optionnal) in order to  add Debian packages to initial
 list

 -A|--architecture  (optionnal) in order   to force initial user space
 architecture (amd64 or i386)

DESCRIPTION Lxc-debian installs one debootstrap Debian distribution in
a dedicated path specified by <--path> option in order to create later
one Linux LXC virtual machine (VM) with lxc-start command.

The   first  step is   to   download one  minimal Debian  distribution
specified by  <--distrib>  option  into  a local  cache   installed in
$cache.  
If distribution is already downloaded, then this step is skipped.

Second step consist  to extract  packages from   local cache  to  path
specified by <--path> option.

Last step consist to create a dedicated init sequence in the VM with a
minimal  mount /etc/fstab file.  The hostname   of the VM  is set with
<--name> option  and  one LXC  configuration is  automatically created
with   default dhcp boot configuration    (please adjust it).  The LXC
configuration file is set in <--path>/config file.

The  initial   Debian  distribution is  download   from  web   site  :
http://ftp.debian.org/debian.

The minimal list of  high level packages  used in the new  LXC VM is :
$minimal_lst_packages

If user want to  add  packages to initial  debootstrap list,  then use
<--packages> option with packages separated by comma.(look at EXAMPLES
section)

Of course, it  is user responsability  to set correct package coherent
with the Debian distribution used.

As LXC is a virtualized container technology used in user space, it is
possible  to   have on  the  same  host different   Debian release LXC
containers.  This is  mainly  due to the  fact  that Posix is a   very
stable API interface  so  that it is   possible  at one time  to  have
different   release.  This is why <--distrib>   must be specified as a
mandatory parameter. Remember always that with  LXC, you have only one
Linux  kernel and as  much Glibc as container.   So as all users space
software components are always dependant from Glibc, it is possible to
have different Debian release distributions.  There is however a limit
to  this capability, this  is when one   user space application make a
direct access to kernel without calling Posix service via Glibc.

About  <--architecture> option.   This   option can be  used  only  on
Intel/Amd  X86 architecture.  With new  x86  64 bits extensions, it is
possible  to have  on the same   plateform one LXC container  with one
Debian/X86/ia32   architecture,   and  one   another   container  with
Debian/X86/amd64  architecture, providing the  Linux kernel is running
in  amd64  mode.  This tools  does  not NOT VERIFY  if current running
kernel is in 64 bits or not.  It is user responsability to manage this
option according  his platform (!). If  this option is not  used, then
internal architecture is set with the  output value provided by 'arch'
tool.

As a conclusion, with <--architecture>  and <--distrib> options, it is
possible to  build a rich environment mixing  stable, legacy and futur
Debian plateform.  This is very important  if for example  you want to
host on a network of several architectures simultaneously.

This     allows for example     system administrators  to  migrate old
applications to new release one without interrupted service.

 EXAMPLES

  sudo lxc-debian --path=/tmp/test_lxc \\
	  --distrib=squeeze --name=vm_squeeze

  sudo lxc-debian --path=/tmp/test_lxc \\
	  --distrib=lenny --name=vm_lenny \\
	  --packages='iputils-ping,tcpdump'

  sudo lxc-debian --path=/tmp/test_lxc \\
	  --distrib=wheezy --name=vm_wheezy \\
	  --architecture=i386

AUTHORS
  daniel.lezcano@free.fr (initial) & jeanmarc.lacroix@free.fr (2011)

EOF

    return 0
}

set -e
# Main code ....
cache="/var/cache/lxc/debian"
opt_clean="false"
options=$(getopt -o hcdp:n:D:P:A: -l help,path:,packages:,name:,distrib:,clean,debug,architecture: -- "$@")
opt_packages=""
opt_force_arch="false"

if [ $? -ne 0 ]; then
    make_usage $(basename $0)
	exit 1
fi
eval set -- "$options"

while true
do
    # to debug ....echo $* $1
	case "$1" in
		-h|--help)       make_usage $0 && exit 0;;
        -p|--path)       opt_path=$2; shift 2;;
		-n|--name)       opt_name=$2; shift 2;;
		-c|--clean)      opt_clean="true"; shift 2;;
		-D|--distrib)    opt_distrib=$2; shift 2;;
 		-P|--packages)   opt_packages=$2; shift 2;;
		-d|--debug)      set -x; shift 1;;
		-A|--architecture) opt_force_arch=$2; shift 2;;
        --)              shift 1; break ;;
        *)               break ;;
    esac
done
if [ $opt_clean == "true" ]; then
    make_clean || exit 1
    exit 0
fi

if [ -z "$opt_path" ]; then
    echo "$0: error: 'opt_path' parameter is required, please consult help (-h option)"
    exit 1
fi

if [ "$opt_force_arch" != "false" ] ; then
	if [ "$opt_force_arch" != "i386" ] \
		&& [ "$opt_force_arch" != "amd64" ] ; then
		echo "$0: error: '--architecture=$opt_force_arch' parameter invalid, please consult help (-h option)"
		exit 1
	fi
fi

if [ -z "$opt_name" ]; then
    echo "$0: error: 'name' parameter is required, please consult help (-h option)"
    exit 1
fi

if [ "$(id -u)" != "0" ]; then
    echo "$0: error: This script should be run as 'root' uid"
    exit 1
fi

type debootstrap 2>&1>/dev/null
if [ $? -ne 0 ]; then
    echo "$0: error: 'debootstrap' command is missing, please install Debian debootstrap package"
    exit 1
fi

rootfs=$opt_path/rootfs

mkdir -p $rootfs || exit 1

install_debian $rootfs
if [ $? -ne 0 ]; then
    echo "$0: error: failed to install debian"
    exit 1
fi

configure_debian $rootfs $opt_name
if [ $? -ne 0 ]; then
    echo "$0: error: failed to configure debian for a container"
    exit 1
fi

copy_configuration $opt_path $rootfs $opt_name
if [ $? -ne 0 ]; then
    echo "$0: error: failed write configuration file"
    exit 1
fi
