#!/bin/sh
# Example of how to use opengl with lxc and ubuntu 16.04, 18.04, or 19.04
#
# lxc 3.6 and up make this really easy (modulo a few odd nvidia settings).
#
# It's much simpler than this example makes it look, but
# when you're just starting out, sometimes
# having a full working example is more helpful than
# simple fragments.

set -e

# Which version this machine is running
case $(cat /etc/issue) in
*16.04*) _os=16;;
*18.04*) _os=18;;
*19.04*) _os=19;;
*) echo "Sorry, this demo has only been tested on Ubuntu 16.04, 18.04, and 19.04."; exit 1;;
esac

PATH=/snap/bin:$PATH

# Work around unreliable 'lxc stop', which hangs frequently
# https://discuss.linuxcontainers.org/t/lxc-stop-hangs-and-works-after-second-try/3598
lxc_stop() {
  lxc exec $1 -- shutdown -h now
}

usage() {
    cat << _EOF_
Demonstrate opengl on multiple flavors of Ubuntu in lxc.

Wipes your lxc/lxd configuration, so only run this on machines that do
not currently use lxc/lxd.

You'll need to do 'xhost +' locally on all machines first, since this
demo hasn't figured out X authentication yet.

Multimachine example:
to prepare ubuntu 16.04/18.04/19.04 images locally, then
set up lxc / run glxgears in lxc / tear down lxc
on two remote machines (running any of those three ubuntus):
  sh lxc-ubuntu-opengl-demo.sh serve 16 18 19
  sh lxc-ubuntu-opengl-demo.sh multi-setup host1 host2
  sh lxc-ubuntu-opengl-demo.sh multi-run host1 host2
  sh lxc-ubuntu-opengl-demo.sh multi-teardown host1 host2

Single-machine example: to prepare image and run glxinfo locally, run these steps, in order:
  sh lxc-ubuntu-opengl-demo.sh install
  sh lxc-ubuntu-opengl-demo.sh init
  sh lxc-ubuntu-opengl-demo.sh image
  sh lxc-ubuntu-opengl-demo.sh localize
  sh lxc-ubuntu-opengl-demo.sh run
  sh lxc-ubuntu-opengl-demo.sh purge
  sh lxc-ubuntu-opengl-demo.sh uninstall
_EOF_
}

# Uninstall lxd
do_uninstall() {
  if test -d /snap/lxd
  then
    sudo snap remove lxd
  fi
}

# Purge all traces of lxd's data from the universe
do_purge() {
  rm -f empty-lxd.sh
  wget https://raw.githubusercontent.com/lxc/lxd/master/scripts/empty-lxd.sh
  if ! test -x /usr/bin/jq
  then
    sudo apt install -y jq
  fi
  sh empty-lxd.sh
}

# Install the right version of lxd
# Idempotent.  Runs quickly and does nothing second time.
do_install() {
  # Can't use lxc from apt, it's waaay too old on some systems (ubuntu 16.04)
  if test -x /usr/bin/lxc
  then
    sudo apt remove --purge lxd lxd-client || true
  fi

  if ! snap --version
  then
    # what, you don't have snap installed already?
    sudo apt install snapd
  fi

  # Install lxc 3.0 from snap
  if ! test -d /snap/lxd
  then
    sudo snap install lxd
    echo "Note: you may need to log out and log back in now to get access to lxd from commandline."
  fi
  which lxc
  lxc --version
}

do_uninstall() {
  sudo snap remove lxd
}

# Initialize lxd, if not already initialized
# Idempotent.  Runs quickly and does nothing second time.
# You may need to add yourself to the lxd group before doing this.
do_init() {
  # fixme: handle more types of storage?
  if df -T /var/lib/lxd | grep btrfs
  then
    driver=btrfs
  else
    driver=dir
  fi

  if lxc storage ls | grep default
  then
    echo "lxd is already initialized"
  else
    lxd init --preseed <<_EOF_
config: {}
networks:
- config:
    ipv4.address: auto
    ipv6.address: none
  description: ""
  managed: false
  name: lxdbr0
  type: ""
storage_pools:
- config:
    source: /var/lib/lxd/storage-pools/default
  description: ""
  name: default
  driver: $driver
profiles:
- config: {}
  description: ""
  devices:
    eth0:
      name: eth0
      nictype: bridged
      parent: lxdbr0
      type: nic
    root:
      path: /
      pool: default
      type: disk
  name: default
cluster: null
_EOF_
  fi
}

# Create Ubuntu images ready to run an opengl application
# and publish them with alias gpu-${OS}
# Usage: do_image [releaseyear...]
# Default is same release as on local system.
do_image() {
  OSES="$*"
  # Default to local OS
  case "$OSES" in
  "") OSES=$_os;; 
  esac

  for OS in $OSES
  do
    # Prepare a base image gpu-${OS} with updates and a few packages, for fast startup
    lxc_stop preloaded-${OS} || true
    # fixme: make deletion optional; otherwise just return if image already there
    lxc delete preloaded-${OS} || true
    lxc launch --profile default ubuntu:${OS}.04 preloaded-${OS}
    # Wait for network to come up
    while ! lxc exec preloaded-${OS} -- host archive.ubuntu.com
    do
      sleep 1
    done
    while ! lxc exec preloaded-${OS} -- apt update
    do
      sleep 1
    done
    lxc exec preloaded-${OS} -- apt dist-upgrade -y
    lxc exec preloaded-${OS} -- apt install -y x11-apps mesa-utils libgl1-mesa-glx
    lxc_stop preloaded-${OS}
    sleep 5   # let it finish stopping
    lxc publish --public preloaded-${OS} --alias gpu-${OS}
    lxc delete preloaded-${OS}
  done
}

# Download a preloaded container and customize it for this machine.
do_localize() {
  case "$1" in
  "") LXDHOST=local;;
  *) LXDHOST=$1;;
  esac

  if ! lxc remote list | grep "$LXDHOST"
  then
    echo "Getting access to remote lxd repository $LXDHOST"
    lxc remote add "$LXDHOST" "$LXDHOST"
  fi

  HOST=$(hostname)
  UID=$(id -u)
  GID=$(id -g)

  case "$DISPLAY" in
  "") echo "Please set DISPLAY first."; exit 1;;
  esac
  XNUM=${DISPLAY#:}

  XSOCK=/tmp/.X11-unix/X$XNUM
  if ! test -S "$XSOCK"
  then
    echo "Please start X first... or something.  Can't open $XSOCK"; exit 1
  fi

  is_nvidia=false
  if lspci | grep VGA | grep -i nvidia
  then
    is_nvidia=true
  fi

  # Create the profile opengl-demo suitable for accessing the GPU
  lxc profile create opengl-demo 2> /dev/null || true
  cat > opengl-demo-profile.tmp <<_EOF_
description: GPU LXD profile for host $HOST
devices:
  gpu:
    type: gpu
  X$XNUM:
    path: $XSOCK
    source: $XSOCK
    type: disk
config:
  environment.DISPLAY: :$XNUM
  raw.idmap: "uid $UID 1000\ngid $GID 1000"
  nvidia.runtime: $is_nvidia
  nvidia.driver.capabilities: "graphics"
_EOF_
  lxc profile edit opengl-demo < opengl-demo-profile.tmp

  lxc_stop opengl-demo-$_os || true
  lxc delete opengl-demo-$_os || true
  lxc launch --profile default --profile opengl-demo "${LXDHOST}":gpu-$_os opengl-demo-$_os

  # If this is an nvidia system, verify generic nvidia driver is working
  if $is_nvidia
  then
    lxc exec opengl-demo-$_os -- "nvidia-smi"
  fi
  # Verify opengl app can run
  lxc exec opengl-demo-$_os -- glxinfo | grep str
  lxc_stop opengl-demo-$_os
}

# Run a command in the localized container.
# By default, just runs glxinfo.
# If 1st arg is -e, container is ephemeral, i.e. any changes are discarded.
do_run() {
  # Clean up after any previous ephemeral run that was interrupted
  lxc delete --force quick 2> /dev/null || true

  local name=quick
  case "$1" in
  --ephemeral|-e) shift; time lxc copy opengl-demo-$_os quick --ephemeral;;
  *) name=opengl-demo-$_os; lxc_stop $name || true; sleep 1;;
  esac

  lxc start $name
  cmd="$*"
  if test "$cmd" = ""
  then
    cmd="glxinfo | grep str"
  fi
  status=0
  # Running xset here is a bit lame, but it's hard to tell if the demo works if screen is blanked
  if ! lxc exec $name -- su ubuntu -c "xset dpms force on && xset -dpms && xset s off && $cmd"
  then
    status=$?
  fi
  lxc_stop $name || true
  return $status
}

#-------------------------------------------------------------------

# Multimachine demo part 1
# Arguments are operating system years (16, 18, 19) to make images for
# Uses the above building blocks to:
# - create an image locally for each supported flavor of ubuntu
# - publish them
do_serve() {
  OSES="$*"
  if test "$OSES" = ""
  then
    echo "Which Ubuntu x.04 do you want to serve?  Give space separate list with 16, 18, and/or 19."
    exit 1
  fi
  # Set up the lxdhost that all machines will pull images from
  do_install
  do_init
  # Tell it to listen on the network (please edit!)
  lxc config set core.https_address [::]:8443
  lxc config set core.trust_password SOME-PASSWORD
  # Populate it with images for the remote machines' operating systems
  do_image $OSES
}

# Multimachine demo part 2
# Run install, init, and localize steps on the given remote hosts.
# FIXME: assumes DISPLAY should be :0 on remote machine...
# and probably assumes you've run xhost + on all the remote machines.
do_multi_setup() {
  for host
  do
    echo "======== Setting up ${host}... ========"
    scp lxc-opengl-demo.sh "${host}":
    ssh -t "${host}" "sh lxc-opengl-demo.sh install && sh lxc-opengl-demo.sh init && DISPLAY=:0 sh lxc-opengl-demo.sh localize $(hostname)"
  done
}

# Multimachine demo part 3
# Run a command in the appropriate image on each given machine concurrently
# Edit to taste (especially cmd).
do_multi_run() {
  local cmd=glxgears
  for host
  do
    echo "======== Running $cmd on ${host}... ========"
    # You might need to do xhost + on the target systems first.
    scp lxc-opengl-demo.sh "${host}":
    ssh -t "${host}" sh -x lxc-opengl-demo.sh run "$cmd" &
  done
  wait
}

# Multimachine demo part 4
# Purge and uninstall lxc/lxd on the given machines
# Optional and traumatic.
do_multi_teardown() {
  for host
  do
    echo "======== Purging ${host}... ========"
    scp lxc-opengl-demo.sh "${host}":
    ssh -t "${host}" 'sh -x lxc-opengl-demo.sh purge && sh -x lxc-opengl-demo.sh uninstall'
  done
}

verb=$1
case "$verb" in
""|help)   usage; exit 0;;
localise)  verb=localize;;
esac

shift
case $verb in
# Simple verbs:
install)    do_install;;
init)       do_init;;
image)      do_image "$@";;
localize)   do_localize "$@";;
run)        do_run "$@";;
uninstall)  do_uninstall;;
purge)      do_purge;;
# Multi-machine demo:
serve)          do_serve "$@";;
multi-setup)    do_multi_setup "$@";;
multi-run)      do_multi_run "$@";;
multi-teardown) do_multi_teardown "$@";;
*)          usage; exit 1;;
esac
