#!/bin/sh
# minc : Pure shell script mini container command
#
# Copyright (C) 2014,2015 Masami Hiramatsu <masami.hiramatsu@gmail.com>
# This program is released under the MIT License, see LICENSE.
#
# This requires util-linux newer than 2.24 (unshare "-f"
# option and mount correctly support /proc/mounts)

LIBEXEC=/usr/libexec/mincs
MINCEXEC=$LIBEXEC/minc-exec
MINCCOAT=$LIBEXEC/minc-coat
MINCFARM=$LIBEXEC/minc-farm

# Exit if any errors
set -e
set -u

usage() { # [error messages]
  test $# -ne 0 && echo "$*"
  echo "$0 - Run given command in a temporary namespace"
  echo "Usage: $0 [options] <command> [argument...]"
  echo " options:"
  echo "    -h or --help	Show this help"
  echo "    -k or --keep	Keep the temporary directory"
  echo "    -t or --tempdir DIR|UUID"
  echo "			Set DIR for temporary directory (imply -k),"
  echo "       			or reuse UUID named container"
  echo "    -D or --direct	Don't use temporary directory"
  echo "    -r or --rootdir DIR|UUID"
  echo "        		Set DIR for original root directory,"
  echo "        		or use UUID named container image"
  echo "    -X or --X11		Export local X11 unix socket"
  echo "    -n or --net	[MODE]	Use network namespace and specify mode"
  echo "                        MODE: raw[,interface] dens"
  echo "    -c or --cpu MASK	Set CPU mask"
  echo "    -b or --bind HOSTPATH[:PATH]"
  echo "        		Bind HOSTPATH to PATH in container"
  echo "    -p or --port HOSTPORT:PORT[:udp]"
  echo "        		Bind HOSTPORT to PORT in container"
  echo "                        This implies using dens mode"
  echo "    -B or --background  Run container in background mode"
  echo "    --name NAME		Set <NAME> as container's name (hostname)"
  echo "    --user UID[:GID]	Specify uid and gid to run command"
  echo "    --usedev		Use devtmpfs for /dev (for loopback etc.)"
  echo "    --nocaps [CAPS]	Drop given capabilities (same as capsh)"
  echo "    --pivot             Use pivot_root instead of capsh"
  echo "    --mem-limit SIZE	memory limitation by cgroups"
  echo "    --mem-swap SIZE	memory+swap limitation by cgroups"
  echo "    --mem-kmem SIZE	kernel memory limitation by cgroups"
  echo "    --cpu-shares SHARE	cpu shares setting by cgroups (default: 1024)"
  echo "    --cpu-quota QUOTA	cpu quota in usec by cgroups (default: 100000)"
  echo "    --pid-max MAX	PID limitation by cgroups"
  echo "    --cross ARCH	Make a cross-arch container by using qemu-user-mode"
  echo "    --arch ARCH		Alias of --cross"
  echo "    --nopriv DIR	Run MINC without root privilege on given <DIR>"
  echo "    --qemu		Run MINC in qemu-kvm instead of chroot"
  echo "    --um		Run MINC in user-mode-linux instead of chroot"
  echo "    --ftrace SCRIPT     Set ftrace pid filter and run SCRIPT to setup"
  echo "    --debug		Debug mode"
  exit $#
}

# normalize to qemu arch
qemuarch() { # arch
  case "$1" in
  amd64|x86_64) echo x86_64 ;;
  arm|armv7l|armel) echo arm ;;
  arm64|aarch64) echo aarch64 ;;
  ppc64|ppc64le|ppc64el) echo ppc64le ;;
  esac
}

get_qemu_bin() { # arch
  grep interpreter /proc/sys/fs/binfmt_misc/qemu-$1 | cut -f 2 -d " "
}

abspath() { # dir
  (cd $1; pwd)
}

abspath2() { # host_dir [container_path]
  if [ $# -eq 2 ]; then
    echo `abspath $1`:$2
  else
    echo `abspath $1`:`abspath $1`
  fi
}

KEEPDIR=0
USE_FARM=
MINC_TMPDIR=
export MINC_DEBUG_PREFIX=
export MINC_RWBIND=
export MINC_OPT_PTY=
export MINC_CPUMASK=
export MINC_NETNS=
export MINC_DEBUG=
export MINC_BASEDIR=/
export MINC_USE_DEV=
export MINC_UTSNAME=
export MINC_DIRECT=
export MINC_DROPCAPS=
export MINC_MEM_LIMIT=
export MINC_MEM_SWAP=
export MINC_MEM_KMEM=
export MINC_CPU_SHARES=
export MINC_CPU_QUOTA=
export MINC_PID_MAX=
export MINC_CROSS_QEMU=
export MINC_NOPRIV=
export MINC_QEMU=
export MINC_PORT_MAP=
export MINC_ARCH=`uname -m`
export MINC_BACKGROUND=
export MINC_PIVOT=0
export MINC_SSHFS_HOST=

parse_sshfs() { # sshfs-path
  :;: "Parse sshfs://[USER@]HOST[:PORT]/[PATH]" ;:
  MINC_SSHFS_DATA=${1#sshfs://}
  MINC_SSHFS_HOST=${MINC_SSHFS_DATA%%/*}
  if [ "$MINC_SSHFS_HOST" != "$MINC_SSHFS_DATA" ]; then
    MINC_SSHFS_PATH=${MINC_SSHFS_DATA#$MINC_SSHFS_HOST}
  else
    MINC_SSHFS_PATH=/
  fi
  MINC_SSHFS_PORT=${MINC_SSHFS_HOST##*:}
  if [ "$MINC_SSHFS_PORT" = "$MINC_SSHFS_HOST" ]; then
    MINC_SSHFS_PORT=22
  else
    MINC_SSHFS_HOST=${MINC_SSHFS_HOST%:$MINC_SSHFS_PORT}
  fi
  return 0
}

minc_set_rootfs() {
    export MINC_BASEDIR=$1
    case $MINC_BASEDIR in
      sshfs://*) :;: 'Use sshfs' ;:
        which sshfs || usage "Error: No sshfs command"
        parse_sshfs $1 || usage "Error: $1 is not sshfs"
        return 0
      ;;
    esac
    if [ ! -d "$MINC_BASEDIR" ]; then
      # ensure the given id image exists
      $MINCFARM pull $MINC_BASEDIR > /dev/null || \
        usage "Error: no such image: $MINC_BASEDIR"
      USE_FARM=image
    else
      MINC_BASEDIR=`abspath $MINC_BASEDIR`
    fi
}

# Parse options
while [ "$#" -ne 0 ]; do
cmd=$1
case "$cmd" in [!-]*) # User given command
    break ;;
esac

shift 1
case "$cmd" in
  --keep|-k) # Keep the temporary directory
    KEEPDIR=1
    ;;
  --tempdir|-t) # Give a temporary directory (imply -k)
    export MINC_TMPDIR=$1
    KEEPDIR=1
    if [ ! -d "$MINC_TMPDIR" ]; then
      USE_FARM=container
    else
      MINC_TMPDIR=`abspath $MINC_TMPDIR`
    fi
    shift 1
    ;;
  --rootdir|-r) # Give a rootdir or image instead of /
    minc_set_rootfs $1
    shift 1
    ;;
  --direct|-D)
    export MINC_DIRECT=1
    ;;
  --X11|-X) # Export X11 connection
    [ -z "$DISPLAY" ] && usage "Error: \$DISPLAY is empty"
    export MINC_RWBIND="$MINC_RWBIND /tmp/.X11-unix:/tmp/.X11-unix"
    if [ "$XAUTHORITY" ] ;then
      export MINC_RWBIND="$MINC_RWBIND $XAUTHORITY:$XAUTHORITY"
    fi
    ;;
  --bind|-b)
    orig_path=`echo $1 | sed "s/:/ /"`
    export MINC_RWBIND="$MINC_RWBIND "`abspath2 $orig_path`
    shift 1
    ;;
  --background|-B)
    MINC_BACKGROUND=":;: 'Exit.';:"
    ;;
  --net|-n) # Use NetNS
    export MINC_NETNS="minc$$"
    if [ $# -ne 0 ]; then
      case "$1" in
        -*) ;;
        raw*|dens)
          export MINC_NETMODE=$1
          shift 1;;
      esac
    fi
    ;;
  --cpu|-c) # Use CPU mask
    MINC_CPUMASK=$1
    shift 1
    ;;
  --mem-limit) # memory limitation by cgroups
    MINC_MEM_LIMIT=$1
    shift 1
    ;;
  --mem-swap) # memory+swap limitation by cgroups
    MINC_MEM_SWAP=$1
    shift 1
    ;;
  --mem-kmem) # kernel memory limitation by cgroups
    MINC_MEM_KMEM=$1
    shift 1
    ;;
  --cpu-shares) # cpu shares setting by cgroups (default: 1024)
    MINC_CPU_SHARES=$1
    shift 1
    ;;
  --cpu-quota) # cpu quota setting in usec by cgroups (default: 100000)
    MINC_CPU_QUOTA=$1
    shift 1
    ;;
  --pid-max) # pid limitation by cgroups
    MINC_PID_MAX=$1
    shift 1
    ;;
  -p|--port)
    export MINC_PORT_MAP="$1 $MINC_PORT_MAP"
    export MINC_NETNS="minc$$"
    export MINC_NETMODE="dens"
    shift 1
    ;;
  --name)
    export MINC_UTSNAME=$1
    shift 1
    ;;
  --user)
    export MINC_USERSPEC=$1
    shift 1
    ;;
  --usedev)
    export MINC_USE_DEV=1
    ;;
  --nocaps)
    export MINC_DROPCAPS="$1"
    shift 1
    ;;
  --pivot)
    export MINC_PIVOT=1
    ;;
  --cross|--arch)
    export MINC_ARCH=`qemuarch $1`
    [ -z "$MINC_ARCH" ] && usage "\"$1\" is not supported cross arch."
    export MINC_CROSS_QEMU=`get_qemu_bin $MINC_ARCH`
    shift 1
    ;;
  --nopriv)
    export MINC_NOPRIV=1
    export MINC_DIRECT=1
    export MINC_OPT_PTY=1
    minc_set_rootfs $1
    shift 1
    ;;
  --qemu)
    export MINC_QEMU=1
    ;;
  --um)
    export MINC_QEMU=1
    export MINC_ARCH=um
    ;;
  --ftrace)
    [ ! -f "$1" ] && usage "\"$1\" is not a file."
    export MINC_FTRACE=$1
    shift 1
    ;;
  --help|-h) # Help Message
    usage
    ;;
  --debug) # Debug mode
    set -x
    export MINC_DEBUG=1
    ;;
  *)
    usage "Parse error: $cmd is not supported."
    ;;
esac
done

TRAPCMD=
if [ -z "$USE_FARM" ]; then
  :;: 'Setup temporary working directory for this container';:
  if [ -z "$MINC_TMPDIR" ]; then
    export MINC_TMPDIR=`mktemp -d /tmp/minc$$-XXXXXX`
    TRAPCMD="echo To reuse this, run: $0 -r $MINC_BASEDIR -t $MINC_TMPDIR"
  fi
  :;: 'Trap the program exit and remove the working directory';:
  if [ $KEEPDIR -eq 0 ]; then
    TRAPCMD="rm -rf $MINC_TMPDIR"
  fi
  trap "$TRAPCMD" EXIT
  if [ "$MINC_SSHFS_HOST" ]; then
    mkdir -p $MINC_TMPDIR/sshfs
    export MINC_BASEDIR=$MINC_TMPDIR/sshfs
    sshfs -p $MINC_SSHFS_PORT $MINC_SSHFS_HOST:$MINC_SSHFS_PATH $MINC_BASEDIR
    TRAPCMD="umount $MINC_BASEDIR && $TRAPCMD"
    trap "$TRAPCMD" EXIT
  fi
elif [ "$USE_FARM" = "image" ]; then
  :;: 'Use minc-farm to setup new container from image';:
  UUID=`$MINCFARM fork $MINC_BASEDIR`
  export MINC_BASEDIR=`$MINCFARM imagestack $MINC_BASEDIR`
  export MINC_TMPDIR=`$MINCFARM dir $UUID`
  echo $UUID
  KEEPDIR=1
  TRAPCMD="echo To reuse this, run: $0 -t "`echo $UUID | cut -b 1-12`
  trap "$TRAPCMD" EXIT
else
  :;: '(Re)use existing minc-farm container';:
  UUID=`$MINCFARM baseid $MINC_TMPDIR` ||
    usage "Error No such container. $MINC_TMPDIR"
  export MINC_BASEDIR=`$MINCFARM imagestack $UUID`
  export MINC_TMPDIR=`$MINCFARM dir $MINC_TMPDIR`
  KEEPDIR=1
fi

if [ "$MINC_BACKGROUND" ]; then
  if [ $KEEPDIR -eq 0 ]; then
    MINC_BACKGROUND=$TRAPCMD
  fi
  trap "echo Run container in background. See $MINC_TMPDIR/log for output." EXIT
  setsid $MINCEXEC "$@" 1> $MINC_TMPDIR/log 2> $MINC_TMPDIR/debug < /dev/null &
else
  trap '' INT
  trap '' QUIT
  $MINCEXEC "$@"
fi
