This last week I needed to install Google Chrome on a headless Ubuntu 14.04 server for use with rendering web assets in via Selenium’s Chrome WebDriver. The configuration took a bit of testing and work to get it all together, so I wanted to share it here. I’ll first go over the general method of how everything is installed, then I’ll share the actual Ansible scripts used to do it.

Overview

Xvfb

Xvfb is a headless display. It allows us to run Chrome (or any application with a UI) on a device that doesn’t have an actual physical display.

Fortunately, it is easily installed via apt-get install xvfb.

However, I also wanted xvfb to run on boot but it doesn’t come with a daemon so I had to create one. I just copied the xvfb-init.d-template from below into /etc/init.d/xvfb, set it to default runlevels via update-rc.d xvfb defaults, then started it with service xvfb start.

Chrome

Because Chrome contains some closed-source components, Ubuntu doesn’t include it in any of its default distributed repositories. This means you can’t just download it via apt-get install google-chrome-stable. One option is to add Google’s repository to your apt sources, but I thought it would be easier to just download and install the .deb package that Google provides.

However, dpkg can’t resolve and install dependencies when installing via .deb packages, so you have to follow up the install with apt-get install --fix-broken.

wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
dpkg -i google-chrome-stable_current_amd64.deb
sudo apt-get install --fix-broken

Chrome WebDriver for Selenium

For the Chrome WebDriver, I needed to download the archive, unzip it, and make sure it was owned by the right user so it could be executable when my application ran.

In retrospect, I should have downloaded the Chrome WebDriver in the same way as xvfb and google-chrome-stable: via ansible. But at the time I thought that the driver needed to exist in the same directory as the Java service, which location didn’t exist when Ansible was run. What I did instead was write a rather tedious bash script that was the equivalent of a few lines of ansible. I’ve included it in the Source section below. It may be of some use to you.

Source

Here are the actual Ansible and bash scripts that I ended up using. You may need to tweak them for your own use.

Xvfb Ansible Scripts

- name: Check if xvfb is installed
  command: dpkg -s 
  register: xvfb_check_deb
  failed_when: xvfb_check_deb.rc > 1
  changed_when: xvfb_check_deb == 1
  tags:
    - xvfb

- name: Install xvfb package
  apt: name=xvfb state=present
  sudo: true
  when: xvfb_check_deb.rc == 1
  tags:
    - xvfb

- name: Install xvfb init.d daemon script
  action: template src=xvfb-init.d-template dest=/etc/init.d/xvfb mode=0755
  tags:
    - xvfb

- name: Set xvfb to run on startup
  shell: update-rc.d xvfb defaults
  sudo: true
  when: xvfb_check_deb.rc == 1
  tags:
    - xvfb

- name: Start xvfb service
  action: service name=xvfb state=started
  tags:
    - xvfb

Xvfb Init Script

# xvfb-init.d-template
### BEGIN INIT INFO
# Provides: Xvfb
# Required-Start: $local_fs $remote_fs
# Required-Stop:
# X-Start-Before:
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Loads X Virtual Frame Buffer
### END INIT INFO

XVFB=/usr/bin/Xvfb
XVFBARGS=":1 -screen 0 1920x1080x24 -ac +extension GLX +render -noreset"
PIDFILE=/var/run/xvfb.pid
case "$1" in
    status)
        echo "Checking xvfb status"
        if [ -f $PIDFILE ]; then
            PID=`cat $PIDFILE`
            if [ -z "`ps axf | grep ${PID} | grep -v grep`" ]; then
                printf "%s\n" "Process dead but pidfile exists"
                exit 1
            else
                echo "Running"
            fi
        else
            printf "%s\n" "Service not running"
            exit 3
        fi
        ;;
    start)
        echo -n "Starting virtual X frame buffer: Xvfb"
        start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile --background --startas /bin/bash -- -c "exec $XVFB $XVFBARGS > /var/log/xvfb.log 2>&1"
        echo "."
        ;;
    stop)
        echo -n "Stopping virtual X frame buffer: Xvfb"
        start-stop-daemon --stop --quiet --pidfile $PIDFILE
        echo "."
        ;;
    restart)
        $0 stop
        $0 start
        ;;
    *)
        echo "Usage: /etc/init.d/xvfb {start|stop|restart}"
        exit 1
esac

exit 0

Google Chrome Ansible Scripts

- name: Check if google-chrome-stable is installed
  command: dpkg -s 
  register: google_chrome_check_deb
  failed_when: google_chrome_check_deb.rc > 1
  changed_when: google_chrome_check_deb == 1
  tags:
    - chrome

- name: Download google-chrome-stable
  get_url:
    url="/"
    dest="/home//"
  when: google_chrome_check_deb.rc == 1
  tags:
    - chrome

- name: Install google-chrome-stable
  apt: deb="/home//"
  sudo: true
  when: google_chrome_check_deb.rc == 1
  tags:
    - chrome

- name: Fix any missing google-chrome-stable dependencies
  shell: apt-get install -f
  sudo: true
  when: google_chrome_check_deb.rc == 1
  tags:
    - chrome

Ansible Properties

Some of the xvfb and chrome specific properties I supplied in a vars file:

# Google chrome package url and name
google_chrome_package_filename: 'google-chrome-stable_current_amd64.deb'
google_chrome_base_url: 'https://dl.google.com/linux/direct'
google_chrome_package_name: 'google-chrome-stable'

# Xvfb package url and name
xvfb_package_name: 'xvfb'

Chrome WebDriver Install Bash Script

#!/bin/bash

echo "Begin Chrome Driver install script."

if [ -z $SERVICE_USER -a -z $SERVICE_GROUP ]; then
    # On maestro deploy, user should be specified with RUN_USER
    # On domo vm deploy, user should be specified by SERVICE_USER
    SERVICE_USER=$RUN_USER
    SERVICE_GROUP=$RUN_USER
fi

CHROME_ZIP="chromedriver_linux64.zip"
if [[ "$OSTYPE" == "linux-gnu" ]]; then
    echo "Detected Linux operating system"
    CHROME_ZIP="chromedriver_linux64.zip"
elif [[ "$OSTYPE" == "darwin"* ]]; then
    echo "Detected OS X operating system"
    CHROME_ZIP="chromedriver_mac32.zip"
else
    echo "ERROR: Unsupported operating system for DaVinci's Chrome Driver. Exiting now."
    exit 1
fi

CHROME_DRIVER="chromedriver"
CHROME_DRIVER_VERSION="2.19"
CHROME_DRIVER_URL="http://chromedriver.storage.googleapis.com/${CHROME_DRIVER_VERSION}/${CHROME_ZIP}"

echo "Checking for Selenium Chrome driver at ./${CHROME_DRIVER}"
if [ -f $CHROME_DRIVER ] ; then
    echo "Chrome driver already installed."
else
    TRIES=0
    while [ $TRIES -lt 5 -a ! -f $CHROME_DRIVER ]; do
        echo "Attempt $TRIES at downloading Selenium Chrome driver from ${CHROME_DRIVER_URL}"
        if curl -O $CHROME_DRIVER_URL ; then
            if file $CHROME_ZIP | grep "Zip archive data"; then
                echo "Download successful."
                echo "Unzipping $CHROME_ZIP"
                if unzip $CHROME_ZIP ; then
                    echo "$CHROME_ZIP unzipped successfully"
                    if [ -f $CHROME_DRIVER ] ; then
                        echo "Chrome driver unzipped and $CHROME_DRIVER located."
                        echo "Setting $CHROME_DRIVER ownership to ${SERVICE_USER}:${SERVICE_GROUP}"
                        chown ${SERVICE_USER}:${SERVICE_GROUP} $CHROME_DRIVER
                    else
                        echo "ERROR: Chrome driver unzipped successfully but unable to locate ${CHROME_DRIVER}."
                    fi
                else
                    echo "ERROR: Unzip of Chrome driver failed."
                fi
            else
                echo "Download unsuccessful. $CHROME_ZIP is not a zip archive."
            fi
        else
            echo "ERROR: Download of Selenium Chrome driver failed. Failing prematurely."
            exit 1
        fi
        if [ ! -f $CHROME_DRIVER ] ; then
            ((TRIES++))
            SLEEP=$(($TRIES * 30))
            echo "Failed to install Chrome driver. Sleeping for $SLEEP seconds then trying again."
            sleep $SLEEP
        fi
    done
    if [ -f $CHROME_ZIP ] ; then
        echo "Cleaning up $CHROME_ZIP"
        rm $CHROME_ZIP
    fi
    if [ -f $CHROME_DRIVER ] ; then
        echo "Chrome driver installed successfully!"
    else
        echo "ERROR: Unable to install Chrome driver, service has failed to start. Exiting now."
        exit 1
    fi
fi