You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@aurora.apache.org by jc...@apache.org on 2016/05/06 20:46:49 UTC

aurora git commit: Add client and scheduler support for launching tasks using the Mesos unified containerizer

Repository: aurora
Updated Branches:
  refs/heads/master 3687c6a1a -> 1eac7a675


Add client and scheduler support for launching tasks using the Mesos unified containerizer

A few notes:

1. It's not possible to configure Mesos 0.27.x to launch docker tasks due to a bug in parsing the
docker_store_dir flag. Fixed here: https://reviews.apache.org/r/43451/ but has not been backported
to Mesos 0.27. This means we can only launch tasks that use AppC images until we upgrade our Mesos
dependency to 0.28.x. The good news is I've confirmed that launching tasks with Docker images *does*
work by using Aurora linked against 0.27.x but running Mesos 0.28.x in Vagrant.

2. The Mesos unified containerizer does not automatically create mount points in the filesystem from
the image. It expects the full path to the mount to exist in the image. For /etc/passwd and
/etc/groups this is not a problem, but for the announcer acls file it was. I ended up moving the
announcer acl file into its own directory and mount that instead. In conjunction with this I also
had to modify our http_example Dockerfile to explicitly create that mount point. A case could be
made for sticking with the current path and just creating an empty file in the image, I felt that
creating an empty directory was slightly less gross. This is tracked by
https://issues.apache.org/jira/browse/MESOS-5229.

Reviewed at https://reviews.apache.org/r/46835/


Project: http://git-wip-us.apache.org/repos/asf/aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/1eac7a67
Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/1eac7a67
Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/1eac7a67

Branch: refs/heads/master
Commit: 1eac7a6758957040e160cd0e3a50839c04254c03
Parents: 3687c6a
Author: Joshua Cohen <jc...@apache.org>
Authored: Fri May 6 15:13:56 2016 -0500
Committer: Joshua Cohen <jc...@apache.org>
Committed: Fri May 6 15:13:56 2016 -0500

----------------------------------------------------------------------
 3rdparty/python/requirements.txt                |  2 +-
 RELEASE-NOTES.md                                | 13 +++
 Vagrantfile                                     |  2 +-
 .../thrift/org/apache/aurora/gen/api.thrift     | 51 +++++-----
 build-support/packer/build.sh                   | 41 ++++++++
 docs/features/containers.md                     | 19 +++-
 docs/reference/configuration.md                 | 47 ++++++++--
 docs/reference/scheduler-configuration.md       |  2 -
 examples/vagrant/announcer-auth.json            | 28 ------
 examples/vagrant/config/announcer-auth.json     | 28 ++++++
 .../mesos_config/etc_mesos-slave/appc_store_dir |  1 +
 .../etc_mesos-slave/image_providers             |  1 +
 .../etc_mesos-slave/image_provisioner_backend   |  1 +
 .../mesos_config/etc_mesos-slave/isolation      |  1 +
 examples/vagrant/upstart/aurora-scheduler.conf  |  4 +-
 .../configuration/ConfigurationManager.java     |  8 --
 .../configuration/executor/ExecutorModule.java  | 13 +--
 .../scheduler/mesos/MesosTaskFactory.java       | 56 +++++++++--
 .../scheduler/mesos/TestExecutorSettings.java   |  6 +-
 .../scheduler/storage/db/TaskConfigManager.java |  6 +-
 .../scheduler/storage/db/views/DbContainer.java | 11 ++-
 .../storage/db/views/DbTaskConfig.java          |  2 -
 .../python/apache/aurora/config/schema/base.py  | 22 ++++-
 src/main/python/apache/aurora/config/thrift.py  | 68 ++++++++++++--
 .../apache/aurora/executor/common/sandbox.py    | 22 +++--
 .../scheduler/storage/db/TaskConfigMapper.xml   | 22 ++---
 .../configuration/ConfigurationManagerTest.java | 27 ------
 .../mesos/MesosTaskFactoryImplTest.java         | 98 +++++++++++++++++---
 .../storage/AbstractCronJobStoreTest.java       |  3 +-
 .../storage/AbstractTaskStoreTest.java          | 11 +--
 .../storage/db/DbJobUpdateStoreTest.java        |  7 +-
 .../python/apache/aurora/config/test_thrift.py  | 25 +++++
 .../aurora/executor/common/test_sandbox.py      | 10 +-
 src/test/sh/org/apache/aurora/e2e/Dockerfile    |  5 +-
 .../apache/aurora/e2e/http/http_example.aurora  |  8 +-
 .../http/http_example_bad_healthcheck.aurora    |  6 ++
 .../aurora/e2e/http/http_example_updated.aurora |  6 ++
 .../sh/org/apache/aurora/e2e/test_end_to_end.sh | 58 +++++++++---
 38 files changed, 535 insertions(+), 206 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/3rdparty/python/requirements.txt
----------------------------------------------------------------------
diff --git a/3rdparty/python/requirements.txt b/3rdparty/python/requirements.txt
index 666c4ae..eab0acc 100644
--- a/3rdparty/python/requirements.txt
+++ b/3rdparty/python/requirements.txt
@@ -21,7 +21,7 @@ mox==0.5.3
 pex==1.1.2
 protobuf==2.6.1
 psutil==3.2.2
-pystachio==0.8.0
+pystachio==0.8.1
 requests==2.7.0
 requests-kerberos==0.7.0
 subprocess32==3.2.7

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/RELEASE-NOTES.md
----------------------------------------------------------------------
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 8d5cbed..4cbf92e 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -14,6 +14,14 @@
   ones in your cluster. Uses the Mesos default of 5s.
 - New scheduler command line option `-framework_name`  to change the name used for registering
   the Aurora framework with Mesos. The current default value is 'TwitterScheduler'.
+- Added experimental support for launching tasks using filesystem images and the Mesos [unified
+  containerizer](https://github.com/apache/mesos/blob/master/docs/container-image.md). See that
+  linked documentation for details on configuring Mesos to use the unified containerizer. Note that
+  earlier versions of Mesos do not fully support the unified containerizer. Mesos 0.28.x or later is
+  recommended for anyone adopting task images via the Mesos containerizer.
+- Upgraded to pystachio 0.8.1 to pick up support for the new [Choice type](https://github.com/wickman/pystachio/blob/v0.8.1/README.md#choices).
+- The `container` property of a `Job` is now a Choice of either a `Container` holder, or a direct
+  reference to either a `Docker` or `Mesos` container.
 
 ### Deprecations and removals:
 
@@ -22,6 +30,11 @@
 - Deprecated `-framework_name` default argument 'TwitterScheduler'. In a future release this
   will change to 'aurora'. Please be aware that depending on your usage of Mesos, this will
   be a backward incompatible change. For details, see MESOS-703.
+- The `-thermos_observer_root` command line arg has been removed from the scheduler. This was a
+  relic from the time when executor checkpoints were written globally, rather than into a task's
+  sandbox.
+- Setting the `container` property of a `Job` to a `Container` holder is deprecated in favor of
+  setting it directly to the appropriate (i.e. `Docker` or `Mesos`) container type.
 
 0.13.0
 ------

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/Vagrantfile
----------------------------------------------------------------------
diff --git a/Vagrantfile b/Vagrantfile
index 3f126ee..b2b8f00 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -25,7 +25,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
   config.vm.hostname = "aurora.local"
   # See build-support/packer/README.md for instructions on updating this box.
   config.vm.box = "apache-aurora/dev-environment"
-  config.vm.box_version = "0.0.5"
+  config.vm.box_version = "0.0.6"
 
   config.vm.define "devcluster" do |dev|
     dev.vm.network :private_network, ip: "192.168.33.7"

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/api/src/main/thrift/org/apache/aurora/gen/api.thrift
----------------------------------------------------------------------
diff --git a/api/src/main/thrift/org/apache/aurora/gen/api.thrift b/api/src/main/thrift/org/apache/aurora/gen/api.thrift
index 3847095..a99889c 100644
--- a/api/src/main/thrift/org/apache/aurora/gen/api.thrift
+++ b/api/src/main/thrift/org/apache/aurora/gen/api.thrift
@@ -167,8 +167,32 @@ struct Volume {
   3: Mode mode
 }
 
+/** Describes an image for use with the Mesos unified containerizer in the Docker format */
+struct DockerImage {
+  /** The name of the image to run */
+  1: string name
+  /** The Docker tag identifying the image */
+  2: string tag
+}
+
+/** Describes an image for use with the Mesos unified containerizer in the AppC format */
+struct AppcImage {
+  /** The name of the image to run */
+  1: string name
+  /** The appc image id identifying the image */
+  2: string imageId
+}
+
+/** Describes an image to be used with the Mesos unified containerizer */
+union Image {
+  1: DockerImage docker
+  2: AppcImage appc
+}
+
 /** Describes a mesos container, this is the default */
 struct MesosContainer {
+  /** the optional filesystem image to use when launching this task. */
+  1: optional Image image
 }
 
 /** Describes a parameter passed to docker cli */
@@ -193,28 +217,6 @@ union Container {
   2: DockerContainer docker
 }
 
-/** Describes an image for use with the Mesos unified containerizer in the Docker format */
-struct DockerImage {
-  /** The name of the image to run */
-  1: string name
-  /** The Docker tag identifying the image */
-  2: string tag
-}
-
-/** Describes an image for use with the Mesos unified containerizer in the AppC format */
-struct AppcImage {
-  /** The name of the image to run */
-  1: string name
-  /** The appc image id identifying the image */
-  2: string imageId
-}
-
-/** Describes an image to be used with the Mesos unified containerizer */
-union Image {
-  1: DockerImage docker
-  2: AppcImage appc
-}
-
 /** Describes resource value required to run a task. */
 union Resource {
   1: double numCpus
@@ -240,11 +242,6 @@ struct TaskConfig {
  18: optional bool production
  /** Task tier type. */
  30: optional string tier
- /**
-  * If using the Mesos unified containerizer, the image to run (N.B. mutually exlusive with
-  * specifying a container)
-  */
- 31: optional Image image
  /** All resources required to run a task. */
  32: set<Resource> resources
 

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/build-support/packer/build.sh
----------------------------------------------------------------------
diff --git a/build-support/packer/build.sh b/build-support/packer/build.sh
index 76197c3..2a01d77 100644
--- a/build-support/packer/build.sh
+++ b/build-support/packer/build.sh
@@ -59,6 +59,46 @@ function install_docker {
     docker-engine
 }
 
+function install_docker2aci {
+  DOCKER2ACI_VERSION="0.9.3"
+  GOLANG_VERSION="1.6.2"
+
+  TEMP_PATH=$(mktemp -d)
+  pushd "$TEMP_PATH"
+
+  echo "Downloading go..."
+  curl -sL "https://storage.googleapis.com/golang/go${GOLANG_VERSION}.linux-amd64.tar.gz" | tar -xz
+
+  export GOROOT="$PWD/go"
+  export PATH="$PATH:$GOROOT/bin"
+
+  echo "Downloading docker2aci source..."
+  curl -sL "https://github.com/appc/docker2aci/archive/v${DOCKER2ACI_VERSION}.tar.gz" | tar -xz
+  pushd "docker2aci-${DOCKER2ACI_VERSION}"
+
+  # This is a version of https://github.com/appc/docker2aci/blob/v0.9.3/build.sh that has been
+  # modified to work without the need to be in a git repo.
+  ORG_PATH="github.com/appc"
+  REPO_PATH="${ORG_PATH}/docker2aci"
+  GLDFLAGS="-X github.com/appc/docker2aci/lib.Version=${DOCKER2ACI_VERSION}"
+
+  if [ ! -h "gopath/src/${REPO_PATH}" ]; then
+    mkdir -p "gopath/src/${ORG_PATH}"
+    ln -s ../../../.. "gopath/src/${REPO_PATH}" || exit 255
+  fi
+  export GOBIN="${PWD}/bin"
+  export GOPATH="${PWD}/gopath:${PWD}/Godeps/_workspace"
+  eval "$(go env)"
+  echo "Building docker2aci..."
+  go build -o "$GOBIN/docker2aci" -ldflags "${GLDFLAGS}" "${REPO_PATH}/"
+  mv "$GOBIN/docker2aci" /usr/local/bin/docker2aci
+
+  popd
+  popd
+
+  rm -rf "$TEMP_PATH"
+}
+
 function install_mesos {
   apt-key adv --keyserver keyserver.ubuntu.com --recv E56151BF
   DISTRO=$(lsb_release -is | tr '[:upper:]' '[:lower:]')
@@ -114,6 +154,7 @@ function compact_box {
 remove_unused
 install_base_packages
 install_docker
+install_docker2aci
 install_mesos
 install_thrift
 warm_artifact_cache

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/docs/features/containers.md
----------------------------------------------------------------------
diff --git a/docs/features/containers.md b/docs/features/containers.md
index 0d4791f..f774f18 100644
--- a/docs/features/containers.md
+++ b/docs/features/containers.md
@@ -1,7 +1,6 @@
 Containers
 ==========
 
-
 Docker
 ------
 
@@ -41,3 +40,21 @@ Example (available in the [Vagrant environment](../getting-started/vagrant.md)):
 In order to correctly execute processes inside a job, the docker container must have Python 2.7
 installed. Further details of how to use Docker can be found in the
 [Reference Documentation](../reference/configuration.md#docker-object).
+
+Mesos
+-----
+
+*Note: In order to use filesystem images with Aurora, you must be running at least Mesos 0.28.x*
+
+Aurora supports specifying a task filesystem image to use with the [Mesos containerizer](http://mesos.apache.org/documentation/latest/container-image/).
+This is done by setting the ```container``` property of the Job to a ```Mesos``` container object
+that includes the image to use. Both [AppC](https://github.com/appc/spec/blob/master/SPEC.md) and 
+[Docker](https://github.com/docker/docker/blob/master/image/spec/v1.md) images are supported.
+
+```
+job = Job(
+   ...
+   container = Mesos(image=DockerImage(name='my-image', tag='my-tag'))
+   ...
+)
+```
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/docs/reference/configuration.md
----------------------------------------------------------------------
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index 9fcfdfc..eb0af3e 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -328,16 +328,20 @@ Job Schema
 
 ### Job Objects
 
+*Note: Specifying a ```Container``` object as the value of the ```container``` property is
+  deprecated in favor of setting its value directly to the appropriate ```Docker``` or ```Mesos```
+  container type*
+
    name | type | description
    ------ | :-------: | -------
   ```task``` | Task | The Task object to bind to this job. Required.
   ```name``` | String | Job name. (Default: inherited from the task attribute's name)
   ```role``` | String | Job role account. Required.
   ```cluster``` | String | Cluster in which this job is scheduled. Required.
-   ```environment``` | String | Job environment, default ```devel```. Must be one of ```prod```, ```devel```, ```test``` or ```staging<number>```.
+  ```environment``` | String | Job environment, default ```devel```. Must be one of ```prod```, ```devel```, ```test``` or ```staging<number>```.
   ```contact``` | String | Best email address to reach the owner of the job. For production jobs, this is usually a team mailing list.
   ```instances```| Integer | Number of instances (sometimes referred to as replicas or shards) of the task to create. (Default: 1)
-   ```cron_schedule``` | String | Cron schedule in cron format. May only be used with non-service jobs. See [Cron Jobs](../features/cron-jobs.md) for more information. Default: None (not a cron job.)
+  ```cron_schedule``` | String | Cron schedule in cron format. May only be used with non-service jobs. See [Cron Jobs](../features/cron-jobs.md) for more information. Default: None (not a cron job.)
   ```cron_collision_policy``` | String | Policy to use when a cron job is triggered while a previous run is still active. KILL_EXISTING Kill the previous run, and schedule the new run CANCEL_NEW Let the previous run continue, and cancel the new run. (Default: KILL_EXISTING)
   ```update_config``` | ```UpdateConfig``` object | Parameters for controlling the rate and policy of rolling updates.
   ```constraints``` | dict | Scheduling constraints for the tasks. See the section on the [constraint specification language](#specifying-scheduling-constraints)
@@ -346,7 +350,7 @@ Job Schema
   ```priority``` | Integer | Preemption priority to give the task (Default 0). Tasks with higher priorities may preempt tasks at lower priorities.
   ```production``` | Boolean |  Whether or not this is a production task that may [preempt](../features/multitenancy.md#preemption) other tasks (Default: False). Production job role must have the appropriate [quota](../features/multitenancy.md#preemption).
   ```health_check_config``` | ```HealthCheckConfig``` object | Parameters for controlling a task's health checks. HTTP health check is only used if a  health port was assigned with a command line wildcard.
-  ```container``` | ```Container``` object | An optional container to run all processes inside of.
+  ```container``` | Choice of ```Container```, ```Docker``` or ```Mesos``` object | An optional container to run all processes inside of.
   ```lifecycle``` | ```LifecycleConfig``` object | An optional task lifecycle configuration that dictates commands to be executed on startup/teardown.  HTTP lifecycle is enabled by default if the "health" port is requested.  See [LifecycleConfig Objects](#lifecycleconfig-objects) for more information.
   ```tier``` | String | Task tier type. The default scheduler tier configuration allows for 3 tiers: `revocable`, `preemptible`, and `preferred`. The `revocable` tier requires the task to run with Mesos revocable resources. Setting the task's tier to `preemptible` allows for the possibility of that task being preempted by other tasks when cluster is running low on resources. The `preferred` tier prevents the task from using revocable resources and from being preempted. Since it is possible that a cluster is configured with a custom tier configuration, users should consult their cluster administrator to be informed of the tiers supported by the cluster. Attempts to schedule jobs with an unsupported tier will be rejected by the scheduler.
 
@@ -367,8 +371,6 @@ Parameters for controlling the rate and policy of rolling updates.
 
 ### HealthCheckConfig Objects
 
-*Note: ```endpoint```, ```expected_response``` and ```expected_response_code``` are deprecated from ```HealthCheckConfig``` and must be definied in ```HttpHealthChecker```.*
-
 Parameters for controlling a task's health checks via HTTP or a shell command.
 
 | param                          | type      | description
@@ -448,16 +450,18 @@ guarantees should they be needed.
 
 ### Container Objects
 
-*Note: The only container type currently supported is "docker".  Docker support is currently EXPERIMENTAL.*
+*Note: Both Docker and Mesos unified-container support are currently EXPERIMENTAL.*
 *Note: In order to correctly execute processes inside a job, the Docker container must have python 2.7 installed.*
 
 *Note: For private docker registry, mesos mandates the docker credential file to be named as `.dockercfg`, even though docker may create a credential file with a different name on various platforms. Also, the `.dockercfg` file needs to be copied into the sandbox using the `-thermos_executor_resources` flag, specified while starting Aurora.*
 
-Describes the container the job's processes will run inside.
+Describes the container the job's processes will run inside. If not using Docker or the Mesos
+unified-container, the container can be omitted from your job config.
 
   param          | type           | description
   -----          | :----:         | -----------
   ```docker```   | Docker         | A docker container to use.
+  ```mesos```    | Mesos          | A mesos container to use.
 
 ### Docker Object
 
@@ -476,6 +480,34 @@ See [Docker Command Line Reference](https://docs.docker.com/reference/commandlin
   ```name```       | String          | The name of the docker parameter. E.g. volume
   ```value```      | String          | The value of the parameter. E.g. /usr/local/bin:/usr/bin:rw
 
+### Mesos Object
+
+  param            | type                           | description
+  -----            | :----:                         | -----------
+  ```image```      | Choice(AppcImage, DockerImage) | An optional filesystem image to use within this container.
+
+### AppcImage
+
+*Note: In order to correctly execute processes inside a job, the filesystem image must include python 2.7.*
+
+Describes an AppC filesystem image.
+
+  param          | type   | description
+  -----          | :----: | -----------
+  ```name```     | String | The name of the appc image.
+  ```image_id``` | String | The [image id](https://github.com/appc/spec/blob/master/spec/aci.md#image-id) of the appc image.
+
+### DockerImage
+
+*Note: In order to correctly execute processes inside a job, the filesystem image must include python 2.7.*
+
+Describes a Docker filesystem image.
+
+  param      | type   | description
+  -----      | :----: | -----------
+  ```name``` | String | The name of the docker image.
+  ```tag```  | String | The tag that identifies the docker image.
+
 ### LifecycleConfig Objects
 
 *Note: The only lifecycle configuration supported is the HTTP lifecycle via the HttpLifecycleConfig.*
@@ -570,4 +602,3 @@ For example, if '{{`thermos.ports[http]`}}' is specified in a `Process`
 configuration, it is automatically extracted and auto-populated by
 Aurora, but must be specified with, for example, `thermos -P http:12345`
 to map `http` to port 12345 when running via the CLI.
-

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/docs/reference/scheduler-configuration.md
----------------------------------------------------------------------
diff --git a/docs/reference/scheduler-configuration.md b/docs/reference/scheduler-configuration.md
index 5f898a8..f7d676d 100644
--- a/docs/reference/scheduler-configuration.md
+++ b/docs/reference/scheduler-configuration.md
@@ -212,8 +212,6 @@ Optional flags:
 	A comma separated list of additional resources to copy into the sandbox.Note: if thermos_executor_path is not the thermos_executor.pex file itself, this must include it.
 -thermos_home_in_sandbox (default false)
 	If true, changes HOME to the sandbox before running the executor. This primarily has the effect of causing the executor and runner to extract themselves into the sandbox.
--thermos_observer_root (default /var/run/thermos)
-	Path to the thermos observer root (by default /var/run/thermos.)
 -transient_task_state_timeout (default (5, mins))
 	The amount of time after which to treat a task stuck in a transient state as LOST.
 -use_beta_db_task_store (default false)

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/examples/vagrant/announcer-auth.json
----------------------------------------------------------------------
diff --git a/examples/vagrant/announcer-auth.json b/examples/vagrant/announcer-auth.json
deleted file mode 100644
index 01a24ac..0000000
--- a/examples/vagrant/announcer-auth.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
-  "auth": [
-    {
-      "scheme": "digest",
-      "credential": "user:pass"
-    }
-  ],
-  "acl": [
-    {
-      "scheme": "digest",
-      "credential": "user:pass",
-      "permissions": {
-        "read": true,
-        "create": true,
-        "delete": true,
-        "write": true
-      }
-    },
-    {
-      "scheme": "world",
-      "credential": "anyone",
-      "permissions": {
-        "read": true,
-        "delete": true
-      }
-    }
-  ]
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/examples/vagrant/config/announcer-auth.json
----------------------------------------------------------------------
diff --git a/examples/vagrant/config/announcer-auth.json b/examples/vagrant/config/announcer-auth.json
new file mode 100644
index 0000000..01a24ac
--- /dev/null
+++ b/examples/vagrant/config/announcer-auth.json
@@ -0,0 +1,28 @@
+{
+  "auth": [
+    {
+      "scheme": "digest",
+      "credential": "user:pass"
+    }
+  ],
+  "acl": [
+    {
+      "scheme": "digest",
+      "credential": "user:pass",
+      "permissions": {
+        "read": true,
+        "create": true,
+        "delete": true,
+        "write": true
+      }
+    },
+    {
+      "scheme": "world",
+      "credential": "anyone",
+      "permissions": {
+        "read": true,
+        "delete": true
+      }
+    }
+  ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/examples/vagrant/mesos_config/etc_mesos-slave/appc_store_dir
----------------------------------------------------------------------
diff --git a/examples/vagrant/mesos_config/etc_mesos-slave/appc_store_dir b/examples/vagrant/mesos_config/etc_mesos-slave/appc_store_dir
new file mode 100644
index 0000000..d1e45ab
--- /dev/null
+++ b/examples/vagrant/mesos_config/etc_mesos-slave/appc_store_dir
@@ -0,0 +1 @@
+/tmp/mesos/images/appc

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/examples/vagrant/mesos_config/etc_mesos-slave/image_providers
----------------------------------------------------------------------
diff --git a/examples/vagrant/mesos_config/etc_mesos-slave/image_providers b/examples/vagrant/mesos_config/etc_mesos-slave/image_providers
new file mode 100644
index 0000000..7168c0c
--- /dev/null
+++ b/examples/vagrant/mesos_config/etc_mesos-slave/image_providers
@@ -0,0 +1 @@
+appc

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/examples/vagrant/mesos_config/etc_mesos-slave/image_provisioner_backend
----------------------------------------------------------------------
diff --git a/examples/vagrant/mesos_config/etc_mesos-slave/image_provisioner_backend b/examples/vagrant/mesos_config/etc_mesos-slave/image_provisioner_backend
new file mode 100644
index 0000000..00ae42f
--- /dev/null
+++ b/examples/vagrant/mesos_config/etc_mesos-slave/image_provisioner_backend
@@ -0,0 +1 @@
+copy

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/examples/vagrant/mesos_config/etc_mesos-slave/isolation
----------------------------------------------------------------------
diff --git a/examples/vagrant/mesos_config/etc_mesos-slave/isolation b/examples/vagrant/mesos_config/etc_mesos-slave/isolation
new file mode 100644
index 0000000..c362230
--- /dev/null
+++ b/examples/vagrant/mesos_config/etc_mesos-slave/isolation
@@ -0,0 +1 @@
+filesystem/linux

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/examples/vagrant/upstart/aurora-scheduler.conf
----------------------------------------------------------------------
diff --git a/examples/vagrant/upstart/aurora-scheduler.conf b/examples/vagrant/upstart/aurora-scheduler.conf
index 084016a..3d9e706 100644
--- a/examples/vagrant/upstart/aurora-scheduler.conf
+++ b/examples/vagrant/upstart/aurora-scheduler.conf
@@ -41,8 +41,8 @@ exec bin/aurora-scheduler \
   -native_log_file_path=/var/db/aurora \
   -backup_dir=/var/lib/aurora/backups \
   -thermos_executor_path=$DIST_DIR/thermos_executor.pex \
-  -global_container_mounts=/home/vagrant/aurora/examples/vagrant/announcer-auth.json:/home/vagrant/aurora/examples/vagrant/announcer-auth.json:ro \
-  -thermos_executor_flags="--announcer-ensemble localhost:2181 --announcer-zookeeper-auth-config /home/vagrant/aurora/examples/vagrant/announcer-auth.json" \
+  -global_container_mounts=/home/vagrant/aurora/examples/vagrant/config:/home/vagrant/aurora/examples/vagrant/config:ro,/etc/passwd:/etc/passwd:ro,/etc/group:/etc/group:ro \
+  -thermos_executor_flags="--announcer-ensemble localhost:2181 --announcer-zookeeper-auth-config /home/vagrant/aurora/examples/vagrant/config/announcer-auth.json" \
   -allowed_container_types=MESOS,DOCKER \
   -http_authentication_mechanism=BASIC \
   -use_beta_db_task_store=true \

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java b/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
index e1ce638..4ef202c 100644
--- a/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
+++ b/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
@@ -207,10 +207,6 @@ public class ConfigurationManager {
   static final String EXECUTOR_REQUIRED_WITH_DOCKER =
       "This scheduler is configured to require an executor for Docker-based tasks.";
 
-  @VisibleForTesting
-  static final String CONTAINER_AND_IMAGE_ARE_MUTUALLY_EXCLUSIVE =
-      "A task may not have both a Docker container and an image.";
-
   /**
    * Check validity of and populates defaults in a task configuration.  This will return a deep copy
    * of the provided task configuration with default configuration values applied, and configuration
@@ -311,10 +307,6 @@ public class ConfigurationManager {
               + containerType.get().toString());
     }
 
-    if (containerType.get() != Container._Fields.MESOS && config.isSetImage()) {
-      throw new TaskDescriptionException(CONTAINER_AND_IMAGE_ARE_MUTUALLY_EXCLUSIVE);
-    }
-
     ThriftBackfill.backfillTask(builder);
 
     String types = config.getResources().stream()

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/java/org/apache/aurora/scheduler/configuration/executor/ExecutorModule.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/configuration/executor/ExecutorModule.java b/src/main/java/org/apache/aurora/scheduler/configuration/executor/ExecutorModule.java
index 32f2fa9..8b7f8dc 100644
--- a/src/main/java/org/apache/aurora/scheduler/configuration/executor/ExecutorModule.java
+++ b/src/main/java/org/apache/aurora/scheduler/configuration/executor/ExecutorModule.java
@@ -73,14 +73,10 @@ public class ExecutorModule extends AbstractModule {
       help = "Extra arguments to be passed to the thermos executor")
   private static final Arg<String> THERMOS_EXECUTOR_FLAGS = Arg.create(null);
 
-  @CmdLine(name = "thermos_observer_root",
-      help = "Path to the thermos observer root (by default /var/run/thermos.)")
-  private static final Arg<String> THERMOS_OBSERVER_ROOT = Arg.create("/var/run/thermos");
-
   @CmdLine(name = "thermos_home_in_sandbox",
       help = "If true, changes HOME to the sandbox before running the executor. "
-             + "This primarily has the effect of causing the executor and runner "
-             + "to extract themselves into the sandbox.")
+          + "This primarily has the effect of causing the executor and runner "
+          + "to extract themselves into the sandbox.")
   private static final Arg<Boolean> THERMOS_HOME_IN_SANDBOX = Arg.create(false);
 
   /**
@@ -140,11 +136,6 @@ public class ExecutorModule extends AbstractModule {
   private static ExecutorSettings makeThermosExecutorSettings()  {
     List<Protos.Volume> volumeMounts =
         ImmutableList.<Protos.Volume>builder()
-            .add(Protos.Volume.newBuilder()
-                .setHostPath(THERMOS_OBSERVER_ROOT.get())
-                .setContainerPath(THERMOS_OBSERVER_ROOT.get())
-                .setMode(Protos.Volume.Mode.RW)
-                .build())
             .addAll(Iterables.transform(
                 GLOBAL_CONTAINER_MOUNTS.get(),
                 v -> Protos.Volume.newBuilder()

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/java/org/apache/aurora/scheduler/mesos/MesosTaskFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/mesos/MesosTaskFactory.java b/src/main/java/org/apache/aurora/scheduler/mesos/MesosTaskFactory.java
index b325106..eb516b3 100644
--- a/src/main/java/org/apache/aurora/scheduler/mesos/MesosTaskFactory.java
+++ b/src/main/java/org/apache/aurora/scheduler/mesos/MesosTaskFactory.java
@@ -20,6 +20,7 @@ import java.util.Set;
 import javax.inject.Inject;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.protobuf.ByteString;
@@ -35,9 +36,13 @@ import org.apache.aurora.scheduler.configuration.executor.ExecutorSettings;
 import org.apache.aurora.scheduler.resources.AcceptedOffer;
 import org.apache.aurora.scheduler.resources.ResourceSlot;
 import org.apache.aurora.scheduler.resources.Resources;
+import org.apache.aurora.scheduler.storage.entities.IAppcImage;
 import org.apache.aurora.scheduler.storage.entities.IAssignedTask;
 import org.apache.aurora.scheduler.storage.entities.IDockerContainer;
+import org.apache.aurora.scheduler.storage.entities.IDockerImage;
+import org.apache.aurora.scheduler.storage.entities.IImage;
 import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.apache.aurora.scheduler.storage.entities.IMesosContainer;
 import org.apache.aurora.scheduler.storage.entities.IMetadata;
 import org.apache.aurora.scheduler.storage.entities.IServerInfo;
 import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
@@ -169,7 +174,15 @@ public interface MesosTaskFactory {
       }
 
       if (config.getContainer().isSetMesos()) {
-        configureTaskForNoContainer(task, taskBuilder, acceptedOffer);
+        ExecutorInfo.Builder executorInfoBuilder = configureTaskForExecutor(task, acceptedOffer);
+
+        Optional<ContainerInfo.Builder> containerInfoBuilder = configureTaskForImage(
+            task.getTask().getContainer().getMesos());
+        if (containerInfoBuilder.isPresent()) {
+          executorInfoBuilder.setContainer(containerInfoBuilder.get());
+        }
+
+        taskBuilder.setExecutor(executorInfoBuilder.build());
       } else if (config.getContainer().isSetDocker()) {
         IDockerContainer dockerContainer = config.getContainer().getDocker();
         if (config.isSetExecutorConfig()) {
@@ -193,12 +206,43 @@ public interface MesosTaskFactory {
       }
     }
 
-    private void configureTaskForNoContainer(
-        IAssignedTask task,
-        TaskInfo.Builder taskBuilder,
-        AcceptedOffer acceptedOffer) {
+    private Optional<ContainerInfo.Builder> configureTaskForImage(IMesosContainer mesosContainer) {
+      requireNonNull(mesosContainer);
+
+      if (mesosContainer.isSetImage()) {
+        IImage image = mesosContainer.getImage();
+
+        Protos.Image.Builder imageBuilder = Protos.Image.newBuilder();
+
+        if (image.isSetAppc()) {
+          IAppcImage appcImage = image.getAppc();
+
+          imageBuilder.setType(Protos.Image.Type.APPC);
+          imageBuilder.setAppc(Protos.Image.Appc.newBuilder()
+              .setName(appcImage.getName())
+              .setId(appcImage.getImageId()));
+        } else if (image.isSetDocker()) {
+          IDockerImage dockerImage = image.getDocker();
+
+          imageBuilder.setType(Protos.Image.Type.DOCKER);
+          imageBuilder.setDocker(Protos.Image.Docker.newBuilder()
+              .setName(dockerImage.getName() + ":" + dockerImage.getTag()));
+        } else {
+          throw new SchedulerException("Task had no supported image set.");
+        }
+
+        ContainerInfo.MesosInfo.Builder mesosContainerBuilder =
+            ContainerInfo.MesosInfo.newBuilder();
+
+        mesosContainerBuilder.setImage(imageBuilder);
+
+        return Optional.of(ContainerInfo.newBuilder()
+            .setType(ContainerInfo.Type.MESOS)
+            .setMesos(mesosContainerBuilder)
+            .addAllVolumes(executorSettings.getExecutorConfig().getVolumeMounts()));
+      }
 
-      taskBuilder.setExecutor(configureTaskForExecutor(task, acceptedOffer).build());
+      return Optional.absent();
     }
 
     private ContainerInfo getDockerContainerInfo(IDockerContainer config) {

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/java/org/apache/aurora/scheduler/mesos/TestExecutorSettings.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/mesos/TestExecutorSettings.java b/src/main/java/org/apache/aurora/scheduler/mesos/TestExecutorSettings.java
index 9aadceb..46973b8 100644
--- a/src/main/java/org/apache/aurora/scheduler/mesos/TestExecutorSettings.java
+++ b/src/main/java/org/apache/aurora/scheduler/mesos/TestExecutorSettings.java
@@ -67,13 +67,15 @@ public final class TestExecutorSettings {
       new ExecutorConfig(THERMOS_EXECUTOR_INFO, ImmutableList.of());
 
   public static final ExecutorSettings THERMOS_EXECUTOR = new ExecutorSettings(
-      THERMOS_CONFIG, false);
+      THERMOS_CONFIG,
+      false /* populate discovery info */);
 
   public static ExecutorSettings thermosOnlyWithOverhead(ResourceSlot overhead) {
     ExecutorConfig config = THERMOS_EXECUTOR.getExecutorConfig();
     ExecutorInfo.Builder executor = config.getExecutor().toBuilder();
     executor.clearResources().addAllResources(overhead.toResourceList(TaskTestUtil.DEV_TIER));
     return new ExecutorSettings(
-        new ExecutorConfig(executor.build(), config.getVolumeMounts()), false);
+        new ExecutorConfig(executor.build(), config.getVolumeMounts()),
+        false /* populate discovery info */);
   }
 }

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/java/org/apache/aurora/scheduler/storage/db/TaskConfigManager.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/storage/db/TaskConfigManager.java b/src/main/java/org/apache/aurora/scheduler/storage/db/TaskConfigManager.java
index 9eadf70..c761642 100644
--- a/src/main/java/org/apache/aurora/scheduler/storage/db/TaskConfigManager.java
+++ b/src/main/java/org/apache/aurora/scheduler/storage/db/TaskConfigManager.java
@@ -121,10 +121,10 @@ class TaskConfigManager {
       if (!container.getParameters().isEmpty()) {
         configMapper.insertDockerParameters(containerInsert.getId(), container.getParameters());
       }
-    }
+    } else if (config.getContainer().isSetMesos()
+        && config.getContainer().getMesos().isSetImage()) {
 
-    if (config.isSetImage()) {
-      IImage image = config.getImage();
+      IImage image = config.getContainer().getMesos().getImage();
 
       switch (image.getSetField()) {
         case DOCKER:

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/java/org/apache/aurora/scheduler/storage/db/views/DbContainer.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/storage/db/views/DbContainer.java b/src/main/java/org/apache/aurora/scheduler/storage/db/views/DbContainer.java
index ae97638..8d4d7ec 100644
--- a/src/main/java/org/apache/aurora/scheduler/storage/db/views/DbContainer.java
+++ b/src/main/java/org/apache/aurora/scheduler/storage/db/views/DbContainer.java
@@ -19,15 +19,20 @@ import org.apache.aurora.gen.MesosContainer;
 
 public final class DbContainer {
   private DockerContainer docker;
+  private DbImage image;
 
   private DbContainer() {
   }
 
   Container toThrift() {
-    if (docker == null) {
-      return Container.mesos(new MesosContainer());
-    } else {
+    if (docker != null) {
       return Container.docker(docker);
     }
+
+    if (image != null) {
+      return Container.mesos(new MesosContainer().setImage(image.toThrift()));
+    }
+
+    return Container.mesos(new MesosContainer());
   }
 }

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/java/org/apache/aurora/scheduler/storage/db/views/DbTaskConfig.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/storage/db/views/DbTaskConfig.java b/src/main/java/org/apache/aurora/scheduler/storage/db/views/DbTaskConfig.java
index a7523c4..a90cb00 100644
--- a/src/main/java/org/apache/aurora/scheduler/storage/db/views/DbTaskConfig.java
+++ b/src/main/java/org/apache/aurora/scheduler/storage/db/views/DbTaskConfig.java
@@ -48,7 +48,6 @@ public final class DbTaskConfig {
   private List<Metadata> metadata;
   private DbContainer container;
   private String tier;
-  private DbImage image;
   private List<DBResource> resources;
 
   private DbTaskConfig() {
@@ -70,7 +69,6 @@ public final class DbTaskConfig {
         .setMaxTaskFailures(maxTaskFailures)
         .setProduction(production)
         .setTier(tier)
-        .setImage(image == null ? null : image.toThrift())
         .setConstraints(constraints.stream()
             .map(DbConstraint::toThrift)
             .collect(toImmutableSet()))

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/python/apache/aurora/config/schema/base.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/config/schema/base.py b/src/main/python/apache/aurora/config/schema/base.py
index 00be874..8451630 100644
--- a/src/main/python/apache/aurora/config/schema/base.py
+++ b/src/main/python/apache/aurora/config/schema/base.py
@@ -15,6 +15,8 @@
 # Disable checkstyle for this entire file as it is a pystachio schema.
 # checkstyle: noqa
 
+from pystachio import Choice
+
 from apache.thermos.config.schema import *
 
 
@@ -109,15 +111,31 @@ class MesosTaskInstance(Struct):
   health_check_config = Default(HealthCheckConfig, HealthCheckConfig())
   lifecycle           = LifecycleConfig
 
+
 class Parameter(Struct):
   name = Required(String)
   value = Required(String)
 
+
 class Docker(Struct):
   image = Required(String)
   parameters = Default(List(Parameter), [])
 
 
+class AppcImage(Struct):
+  name = Required(String)
+  image_id = Required(String)
+
+
+class DockerImage(Struct):
+  name = Required(String)
+  tag = Required(String)
+
+
+class Mesos(Struct):
+  image = Choice([AppcImage, DockerImage])
+
+
 class Container(Struct):
   docker = Docker
 
@@ -150,7 +168,9 @@ class MesosJob(Struct):
 
   enable_hooks = Default(Boolean, False)  # enable client API hooks; from env python-list 'hooks'
 
-  container = Container
+  # Specifying a `Container` with a `docker` property for Docker jobs is deprecated, instead just
+  # specify the value of the container property to be a `Docker` container directly.
+  container = Choice([Container, Docker, Mesos])
 
 
 Job = MesosJob

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/python/apache/aurora/config/thrift.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/config/thrift.py b/src/main/python/apache/aurora/config/thrift.py
index 928ca93..81a5055 100644
--- a/src/main/python/apache/aurora/config/thrift.py
+++ b/src/main/python/apache/aurora/config/thrift.py
@@ -18,18 +18,30 @@ import re
 from pystachio import Empty, Ref
 from twitter.common.lang import Compatibility
 
-from apache.aurora.config.schema.base import HealthCheckConfig, MesosContext, MesosTaskInstance
+from apache.aurora.config.schema.base import AppcImage as PystachioAppcImage
+from apache.aurora.config.schema.base import Container as PystachioContainer
+from apache.aurora.config.schema.base import DockerImage as PystachioDockerImage
+from apache.aurora.config.schema.base import (
+    Docker,
+    HealthCheckConfig,
+    Mesos,
+    MesosContext,
+    MesosTaskInstance
+)
 from apache.thermos.config.loader import ThermosTaskValidator
 
 from gen.apache.aurora.api.constants import AURORA_EXECUTOR_NAME, GOOD_IDENTIFIER_PATTERN_PYTHON
 from gen.apache.aurora.api.ttypes import (
+    AppcImage,
     Constraint,
     Container,
     CronCollisionPolicy,
     DockerContainer,
+    DockerImage,
     DockerParameter,
     ExecutorConfig,
     Identity,
+    Image,
     JobConfiguration,
     JobKey,
     LimitConstraint,
@@ -127,17 +139,55 @@ def select_service_bit(job):
   return fully_interpolated(job.service(), bool)
 
 
+def create_docker_container(container):
+  params = list()
+  if container.parameters() is not Empty:
+    for p in fully_interpolated(container.parameters()):
+      params.append(DockerParameter(p['name'], p['value']))
+  return DockerContainer(fully_interpolated(container.image()), params)
+
+
 def create_container_config(container):
   if container is Empty:
     return Container(MesosContainer(), None)
-  elif container.docker() is not Empty:
-    params = list()
-    if container.docker().parameters() is not Empty:
-      for p in fully_interpolated(container.docker().parameters()):
-        params.append(DockerParameter(p['name'], p['value']))
-    return Container(None, DockerContainer(fully_interpolated(container.docker().image()), params))
-  else:
-    raise InvalidConfig('If a container is specified it must set one type.')
+
+  def is_docker_container(c):
+    return isinstance(c, PystachioContainer) and c.docker() is not None
+
+  unwrapped = container.unwrap()
+  if isinstance(unwrapped, Docker) or is_docker_container(unwrapped):
+    return Container(
+        None,
+        (create_docker_container(unwrapped.docker()
+         if is_docker_container(unwrapped)
+         else unwrapped)))
+
+  if isinstance(unwrapped, Mesos):
+    return Container(MesosContainer(image_to_thrift(unwrapped.image())), None)
+
+  raise InvalidConfig('If a container is specified it must set one type.')
+
+
+def image_to_thrift(image):
+  if image is Empty:
+    return None
+
+  unwrapped = image.unwrap()
+  if isinstance(unwrapped, PystachioAppcImage):
+    return Image(
+        docker=None,
+        appc=AppcImage(
+            fully_interpolated(unwrapped.name()),
+            fully_interpolated(unwrapped.image_id())))
+
+  if isinstance(unwrapped, PystachioDockerImage):
+    return Image(
+        docker=DockerImage(
+            fully_interpolated(unwrapped.name()),
+            fully_interpolated(unwrapped.tag())),
+        appc=None)
+
+  raise InvalidConfig('Invalid image configuration: unexpected image type found.')
 
 
 # TODO(wickman): We should revert to using the MesosTaskInstance.

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/python/apache/aurora/executor/common/sandbox.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/executor/common/sandbox.py b/src/main/python/apache/aurora/executor/common/sandbox.py
index 36f1eab..be1deba 100644
--- a/src/main/python/apache/aurora/executor/common/sandbox.py
+++ b/src/main/python/apache/aurora/executor/common/sandbox.py
@@ -62,8 +62,9 @@ class DefaultSandboxProvider(SandboxProvider):
   SANDBOX_NAME = 'sandbox'
 
   def from_assigned_task(self, assigned_task):
-    if assigned_task.task.container.docker:
-      return DockerDirectorySandbox(self.SANDBOX_NAME)
+    container = assigned_task.task.container
+    if container.docker or container.mesos and container.mesos.image:
+      return FileSystemImageSandbox(self.SANDBOX_NAME, self._get_sandbox_user(assigned_task))
     else:
       return DirectorySandbox(
         os.path.abspath(self.SANDBOX_NAME),
@@ -119,24 +120,27 @@ class DirectorySandbox(SandboxInterface):
       raise self.DeletionError('Failed to destroy sandbox: %s' % e)
 
 
-class DockerDirectorySandbox(DirectorySandbox):
-  """ A sandbox implementation that configures the sandbox correctly for docker. """
+class FileSystemImageSandbox(DirectorySandbox):
+  """
+  A sandbox implementation that configures the sandbox correctly for tasks provisioned from a
+  filesystem image.
+  """
 
   MESOS_DIRECTORY_ENV_VARIABLE = 'MESOS_DIRECTORY'
   MESOS_SANDBOX_ENV_VARIABLE = 'MESOS_SANDBOX'
 
-  def __init__(self, sandbox_name):
+  def __init__(self, sandbox_name, user=None):
     self._mesos_host_sandbox = os.environ[self.MESOS_DIRECTORY_ENV_VARIABLE]
     self._root = os.path.join(self._mesos_host_sandbox, sandbox_name)
-    super(DockerDirectorySandbox, self).__init__(self._root, user=None)
+    super(FileSystemImageSandbox, self).__init__(self._root, user=user)
 
   def _create_symlinks(self):
-    # This sets up the docker container to have a similar directory structure to the host.
+    # This sets up the container to have a similar directory structure to the host.
     # It takes self._mesos_host_sandbox (e.g. "[exec-root]/runs/RUN1/") and:
     #   - Sets mesos_host_sandbox_root = "[exec-root]/runs/" (one level up from mesos_host_sandbox)
     #   - Creates mesos_host_sandbox_root (recursively)
     #   - Symlinks _mesos_host_sandbox -> $MESOS_SANDBOX (typically /mnt/mesos/sandbox)
-    # $MESOS_SANDBOX is provided in the environment by the Mesos docker containerizer.
+    # $MESOS_SANDBOX is provided in the environment by the Mesos containerizer.
 
     mesos_host_sandbox_root = os.path.dirname(self._mesos_host_sandbox)
     try:
@@ -147,4 +151,4 @@ class DockerDirectorySandbox(DirectorySandbox):
 
   def create(self):
     self._create_symlinks()
-    super(DockerDirectorySandbox, self).create()
+    super(FileSystemImageSandbox, self).create()

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/main/resources/org/apache/aurora/scheduler/storage/db/TaskConfigMapper.xml
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/storage/db/TaskConfigMapper.xml b/src/main/resources/org/apache/aurora/scheduler/storage/db/TaskConfigMapper.xml
index cd181bb..2c8af8b 100644
--- a/src/main/resources/org/apache/aurora/scheduler/storage/db/TaskConfigMapper.xml
+++ b/src/main/resources/org/apache/aurora/scheduler/storage/db/TaskConfigMapper.xml
@@ -96,7 +96,7 @@
 
   <resultMap id="dockerContainerMap" type="org.apache.aurora.gen.DockerContainer">
     <id column="id"/>
-    <result column="image" property="image"/>
+    <result column="docker_image" property="image"/>
     <collection
         property="parameters"
         select="selectDockerParameters"
@@ -104,10 +104,6 @@
         foreignColumn="container_id"/>
   </resultMap>
 
-  <resultMap id="containerMap" type="org.apache.aurora.scheduler.storage.db.views.DbContainer">
-    <association property="docker" resultMap="dockerContainerMap"/>
-  </resultMap>
-
   <resultMap id="dockerImageMap" type="org.apache.aurora.gen.DockerImage">
     <id column="id" />
   </resultMap>
@@ -121,6 +117,11 @@
     <association property="docker" columnPrefix="docker_" resultMap="dockerImageMap" />
   </resultMap>
 
+  <resultMap id="containerMap" type="org.apache.aurora.scheduler.storage.db.views.DbContainer">
+    <association property="docker" resultMap="dockerContainerMap" />
+    <association property="image" columnPrefix="image_" resultMap="imageMap" />
+  </resultMap>
+
   <resultMap id="metadataMap" type="org.apache.aurora.gen.Metadata">
     <id column="id" />
   </resultMap>
@@ -142,7 +143,6 @@
         resultMap="org.apache.aurora.scheduler.storage.db.JobKeyMapper.jobKeyMap"
         columnPrefix="j_"/>
     <association property="container" resultMap="containerMap" columnPrefix="c_"/>
-    <association property="image" resultMap="imageMap" columnPrefix="image_" />
     <collection
         property="constraints"
         columnPrefix="constraint_"
@@ -184,14 +184,14 @@
       j.name AS j_name,
       p.port_name AS p_port_name,
       d.id AS c_id,
-      d.image AS c_image,
+      d.image AS c_docker_image,
       m.id AS m_id,
       m.key AS m_key,
       m.value AS m_value,
-      di.name as image_docker_name,
-      di.tag as image_docker_tag,
-      ai.name as image_appc_name,
-      ai.image_id as image_appc_image_id,
+      di.name as c_image_docker_name,
+      di.tag as c_image_docker_tag,
+      ai.name as c_image_appc_name,
+      ai.image_id as c_image_appc_image_id,
       tc.id AS constraint_id,
       tc.name AS constraint_name,
       tlc.id AS constraint_l_id,

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/java/org/apache/aurora/scheduler/configuration/ConfigurationManagerTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/configuration/ConfigurationManagerTest.java b/src/test/java/org/apache/aurora/scheduler/configuration/ConfigurationManagerTest.java
index 98fe860..ddf8143 100644
--- a/src/test/java/org/apache/aurora/scheduler/configuration/ConfigurationManagerTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/configuration/ConfigurationManagerTest.java
@@ -23,11 +23,9 @@ import com.google.common.collect.ImmutableSet;
 import org.apache.aurora.gen.Constraint;
 import org.apache.aurora.gen.Container;
 import org.apache.aurora.gen.CronCollisionPolicy;
-import org.apache.aurora.gen.DockerImage;
 import org.apache.aurora.gen.DockerParameter;
 import org.apache.aurora.gen.ExecutorConfig;
 import org.apache.aurora.gen.Identity;
-import org.apache.aurora.gen.Image;
 import org.apache.aurora.gen.JobConfiguration;
 import org.apache.aurora.gen.JobKey;
 import org.apache.aurora.gen.LimitConstraint;
@@ -245,31 +243,6 @@ public class ConfigurationManagerTest {
   }
 
   @Test
-  public void testImageAndDockerContainerConfigurationAreMutuallyExclusive() throws Exception {
-    TaskConfig builder = CONFIG_WITH_CONTAINER.newBuilder();
-    builder.getContainer().getDocker().unsetParameters();
-
-    Image image = new Image();
-    image.setDocker(new DockerImage().setName("my-container").setTag("tag"));
-    builder.setImage(image);
-
-    expectTaskDescriptionException(ConfigurationManager.CONTAINER_AND_IMAGE_ARE_MUTUALLY_EXCLUSIVE);
-    CONFIGURATION_MANAGER.validateAndPopulate(ITaskConfig.build(builder));
-  }
-
-  @Test
-  public void testImageWithoutContainerIsAllowed() throws Exception {
-    TaskConfig builder = UNSANITIZED_JOB_CONFIGURATION.deepCopy().getTaskConfig();
-    builder.unsetConstraints();
-
-    Image image = new Image();
-    image.setDocker(new DockerImage().setName("my-container").setTag("tag"));
-    builder.setImage(image);
-
-    CONFIGURATION_MANAGER.validateAndPopulate(ITaskConfig.build(builder));
-  }
-
-  @Test
   public void testTaskResourceBackfill() throws Exception {
     TaskConfig builder = CONFIG_WITH_CONTAINER.newBuilder();
     builder.unsetResources();

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/java/org/apache/aurora/scheduler/mesos/MesosTaskFactoryImplTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/mesos/MesosTaskFactoryImplTest.java b/src/test/java/org/apache/aurora/scheduler/mesos/MesosTaskFactoryImplTest.java
index ad397c6..fea95f1 100644
--- a/src/test/java/org/apache/aurora/scheduler/mesos/MesosTaskFactoryImplTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/mesos/MesosTaskFactoryImplTest.java
@@ -22,10 +22,13 @@ import com.google.common.collect.ImmutableSet;
 
 import org.apache.aurora.GuavaUtils;
 import org.apache.aurora.common.testing.easymock.EasyMockTest;
+import org.apache.aurora.gen.AppcImage;
 import org.apache.aurora.gen.AssignedTask;
 import org.apache.aurora.gen.Container;
 import org.apache.aurora.gen.DockerContainer;
+import org.apache.aurora.gen.DockerImage;
 import org.apache.aurora.gen.DockerParameter;
+import org.apache.aurora.gen.Image;
 import org.apache.aurora.gen.MesosContainer;
 import org.apache.aurora.gen.ServerInfo;
 import org.apache.aurora.gen.TaskConfig;
@@ -44,6 +47,7 @@ import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
 import org.apache.mesos.Protos;
 import org.apache.mesos.Protos.ContainerInfo;
 import org.apache.mesos.Protos.ContainerInfo.DockerInfo;
+import org.apache.mesos.Protos.ContainerInfo.MesosInfo;
 import org.apache.mesos.Protos.ContainerInfo.Type;
 import org.apache.mesos.Protos.ExecutorInfo;
 import org.apache.mesos.Protos.Offer;
@@ -73,7 +77,6 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 public class MesosTaskFactoryImplTest extends EasyMockTest {
-
   private static final ITaskConfig TASK_CONFIG = ITaskConfig.build(
       TaskTestUtil.makeConfig(TaskTestUtil.JOB)
           .newBuilder()
@@ -95,6 +98,17 @@ public class MesosTaskFactoryImplTest extends EasyMockTest {
                   new DockerContainer("testimage").setParameters(
                       ImmutableList.of(new DockerParameter("label", "testparameter")))))));
 
+  private static final ExecutorSettings EXECUTOR_SETTINGS_WITH_VOLUMES = new ExecutorSettings(
+      new ExecutorConfig(
+          TestExecutorSettings.THERMOS_EXECUTOR_INFO,
+          ImmutableList.of(
+              Volume.newBuilder()
+                  .setHostPath("/host")
+                  .setContainerPath("/container")
+                  .setMode(Mode.RO)
+                  .build())),
+      false /* populate discovery info */);
+
   private static final SlaveID SLAVE = SlaveID.newBuilder().setValue("slave-id").build();
   private static final Offer OFFER_THERMOS_EXECUTOR = Protos.Offer.newBuilder()
       .setId(Protos.OfferID.newBuilder().setValue("offer-id"))
@@ -288,16 +302,7 @@ public class MesosTaskFactoryImplTest extends EasyMockTest {
 
   @Test
   public void testGlobalMounts() {
-    config = new ExecutorSettings(
-        new ExecutorConfig(
-            TestExecutorSettings.THERMOS_EXECUTOR_INFO,
-            ImmutableList.of(
-                Volume.newBuilder()
-                    .setHostPath("/host")
-                    .setContainerPath("/container")
-                    .setMode(Mode.RO)
-                    .build())),
-        false);
+    config = EXECUTOR_SETTINGS_WITH_VOLUMES;
 
     expect(tierManager.getTier(TASK_WITH_DOCKER.getTask())).andReturn(DEV_TIER);
     taskFactory = new MesosTaskFactoryImpl(config, tierManager, SERVER_INFO);
@@ -353,7 +358,7 @@ public class MesosTaskFactoryImplTest extends EasyMockTest {
 
   @Test
   public void testPopulateDiscoveryInfoNoPort() {
-    config = new ExecutorSettings(THERMOS_CONFIG, true);
+    config = new ExecutorSettings(THERMOS_CONFIG, true /* populate discovery info */);
     AssignedTask builder = TASK.newBuilder();
     builder.unsetAssignedPorts();
     builder.setTask(
@@ -371,7 +376,7 @@ public class MesosTaskFactoryImplTest extends EasyMockTest {
 
   @Test
   public void testPopulateDiscoveryInfo() {
-    config = new ExecutorSettings(THERMOS_CONFIG, true);
+    config = new ExecutorSettings(THERMOS_CONFIG, true /* populate discovery info */);
     expect(tierManager.getTier(TASK_CONFIG)).andReturn(DEV_TIER);
     taskFactory = new MesosTaskFactoryImpl(config, tierManager, SERVER_INFO);
 
@@ -381,6 +386,73 @@ public class MesosTaskFactoryImplTest extends EasyMockTest {
     checkDiscoveryInfo(task, ImmutableMap.of("http", 80), TASK.getTask().getJob());
   }
 
+  @Test
+  public void testDockerImageWithMesosContainer() throws Exception {
+    String imageName = "some-image-name";
+    String imageTag = "some-image-tag";
+    IAssignedTask taskWithDockerImage = IAssignedTask.build(TASK.newBuilder()
+        .setTask(
+            new TaskConfig(TASK.getTask().newBuilder()
+                .setContainer(Container.mesos(
+                    new MesosContainer().setImage(
+                        Image.docker(new DockerImage(imageName, imageTag))))))));
+
+    expect(tierManager.getTier(taskWithDockerImage.getTask())).andReturn(DEV_TIER);
+    control.replay();
+
+    taskFactory = new MesosTaskFactoryImpl(
+        EXECUTOR_SETTINGS_WITH_VOLUMES,
+        tierManager,
+        SERVER_INFO);
+
+    TaskInfo task = taskFactory.createFrom(taskWithDockerImage, OFFER_THERMOS_EXECUTOR);
+    assertEquals(
+        ContainerInfo.newBuilder()
+            .setType(Type.MESOS)
+            .setMesos(MesosInfo.newBuilder().setImage(
+                Protos.Image.newBuilder()
+                    .setType(Protos.Image.Type.DOCKER)
+                    .setDocker(Protos.Image.Docker.newBuilder()
+                        .setName(imageName + ":" + imageTag))))
+            .addAllVolumes(EXECUTOR_SETTINGS_WITH_VOLUMES.getExecutorConfig().getVolumeMounts())
+            .build(),
+        task.getExecutor().getContainer());
+  }
+
+  @Test
+  public void testAppcImageWithMesosContainer() throws Exception {
+    String imageName = "some-image-name";
+    String imageId = "some-image-id";
+    IAssignedTask taskWithAppcImage = IAssignedTask.build(TASK.newBuilder()
+        .setTask(
+            new TaskConfig(TASK.getTask().newBuilder()
+                .setContainer(Container.mesos(
+                    new MesosContainer().setImage(
+                        Image.appc(new AppcImage(imageName, imageId))))))));
+
+    expect(tierManager.getTier(taskWithAppcImage.getTask())).andReturn(DEV_TIER);
+    control.replay();
+
+    taskFactory = new MesosTaskFactoryImpl(
+        EXECUTOR_SETTINGS_WITH_VOLUMES,
+        tierManager,
+        SERVER_INFO);
+
+    TaskInfo task = taskFactory.createFrom(taskWithAppcImage, OFFER_THERMOS_EXECUTOR);
+    assertEquals(
+        ContainerInfo.newBuilder()
+            .setType(Type.MESOS)
+            .setMesos(MesosInfo.newBuilder().setImage(
+                Protos.Image.newBuilder()
+                    .setType(Protos.Image.Type.APPC)
+                    .setAppc(Protos.Image.Appc.newBuilder()
+                        .setName(imageName)
+                        .setId(imageId))))
+            .addAllVolumes(EXECUTOR_SETTINGS_WITH_VOLUMES.getExecutorConfig().getVolumeMounts())
+            .build(),
+        task.getExecutor().getContainer());
+  }
+
   private static ResourceSlot getTotalTaskResources(TaskInfo task) {
     Resources taskResources = fromResourceList(task.getResourcesList());
     Resources executorResources = fromResourceList(task.getExecutor().getResourcesList());

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/java/org/apache/aurora/scheduler/storage/AbstractCronJobStoreTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/storage/AbstractCronJobStoreTest.java b/src/test/java/org/apache/aurora/scheduler/storage/AbstractCronJobStoreTest.java
index 2343394..c316e49 100644
--- a/src/test/java/org/apache/aurora/scheduler/storage/AbstractCronJobStoreTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/storage/AbstractCronJobStoreTest.java
@@ -142,8 +142,7 @@ public abstract class AbstractCronJobStoreTest {
                 .setCronSchedule("schedule")
                 .setCronCollisionPolicy(CronCollisionPolicy.CANCEL_NEW)
                 .setTaskConfig(config.newBuilder())
-                .setInstanceCount(5)),
-        StorageEntityUtil.getField(ITaskConfig.class, "image"));
+                .setInstanceCount(5)));
   }
 
   private Set<IJobConfiguration> fetchJobs() {

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/java/org/apache/aurora/scheduler/storage/AbstractTaskStoreTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/storage/AbstractTaskStoreTest.java b/src/test/java/org/apache/aurora/scheduler/storage/AbstractTaskStoreTest.java
index af56115..b1593f6 100644
--- a/src/test/java/org/apache/aurora/scheduler/storage/AbstractTaskStoreTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/storage/AbstractTaskStoreTest.java
@@ -51,7 +51,6 @@ import org.apache.aurora.gen.MaintenanceMode;
 import org.apache.aurora.gen.MesosContainer;
 import org.apache.aurora.gen.Metadata;
 import org.apache.aurora.gen.ScheduledTask;
-import org.apache.aurora.gen.TaskConfig;
 import org.apache.aurora.gen.TaskQuery;
 import org.apache.aurora.scheduler.base.JobKeys;
 import org.apache.aurora.scheduler.base.Query;
@@ -153,9 +152,7 @@ public abstract class AbstractTaskStoreTest extends TearDownTestCase {
   @Test
   public void testSave() {
     IScheduledTask aWithHost = setHost(TASK_A, HOST_A);
-    StorageEntityUtil.assertFullyPopulated(
-        aWithHost.newBuilder(),
-        StorageEntityUtil.getField(TaskConfig.class, "image"));
+    StorageEntityUtil.assertFullyPopulated(aWithHost.newBuilder());
 
     saveTasks(aWithHost, TASK_B);
     assertStoreContents(aWithHost, TASK_B);
@@ -187,7 +184,8 @@ public abstract class AbstractTaskStoreTest extends TearDownTestCase {
 
     Image image = new Image();
     image.setDocker(new DockerImage().setName("some-name").setTag("some-tag"));
-    builder.getAssignedTask().getTask().setImage(image);
+    builder.getAssignedTask().getTask().setContainer(
+        Container.mesos(new MesosContainer().setImage(image)));
 
     IScheduledTask task = IScheduledTask.build(builder);
     saveTasks(task);
@@ -200,7 +198,8 @@ public abstract class AbstractTaskStoreTest extends TearDownTestCase {
 
     Image image = new Image();
     image.setAppc(new AppcImage().setName("some-name").setImageId("some-tag"));
-    builder.getAssignedTask().getTask().setImage(image);
+    builder.getAssignedTask().getTask().setContainer(
+        Container.mesos(new MesosContainer().setImage(image)));
 
     IScheduledTask task = IScheduledTask.build(builder);
     saveTasks(task);

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/java/org/apache/aurora/scheduler/storage/db/DbJobUpdateStoreTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/storage/db/DbJobUpdateStoreTest.java b/src/test/java/org/apache/aurora/scheduler/storage/db/DbJobUpdateStoreTest.java
index e43ec6c..0853039 100644
--- a/src/test/java/org/apache/aurora/scheduler/storage/db/DbJobUpdateStoreTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/storage/db/DbJobUpdateStoreTest.java
@@ -60,7 +60,6 @@ import org.apache.aurora.scheduler.storage.entities.IJobUpdateKey;
 import org.apache.aurora.scheduler.storage.entities.IJobUpdateQuery;
 import org.apache.aurora.scheduler.storage.entities.IJobUpdateSummary;
 import org.apache.aurora.scheduler.storage.entities.ILock;
-import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
 import org.apache.aurora.scheduler.storage.testing.StorageEntityUtil;
 import org.apache.aurora.scheduler.testing.FakeStatsProvider;
 import org.junit.After;
@@ -147,8 +146,7 @@ public class DbJobUpdateStoreTest {
         StorageEntityUtil.getField(JobUpdateSummary.class, "state"),
         StorageEntityUtil.getField(IJobUpdateSummary.class, "state"),
         StorageEntityUtil.getField(Range.class, "first"),
-        StorageEntityUtil.getField(Range.class, "last"),
-        StorageEntityUtil.getField(ITaskConfig.class, "image"));
+        StorageEntityUtil.getField(Range.class, "last"));
     saveUpdate(update1, Optional.of("lock1"));
     assertUpdate(update1);
 
@@ -184,8 +182,7 @@ public class DbJobUpdateStoreTest {
         StorageEntityUtil.getField(JobUpdateSummary.class, "state"),
         StorageEntityUtil.getField(IJobUpdateSummary.class, "state"),
         StorageEntityUtil.getField(Range.class, "first"),
-        StorageEntityUtil.getField(Range.class, "last"),
-        StorageEntityUtil.getField(ITaskConfig.class, "image"));
+        StorageEntityUtil.getField(Range.class, "last"));
     saveUpdate(update, Optional.of("lock1"));
     assertUpdate(update);
   }

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/python/apache/aurora/config/test_thrift.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/config/test_thrift.py b/src/test/python/apache/aurora/config/test_thrift.py
index 7a076f0..8769db3 100644
--- a/src/test/python/apache/aurora/config/test_thrift.py
+++ b/src/test/python/apache/aurora/config/test_thrift.py
@@ -19,10 +19,13 @@ import pytest
 
 from apache.aurora.config import AuroraConfig
 from apache.aurora.config.schema.base import (
+    AppcImage,
     Container,
     Docker,
+    DockerImage,
     HealthCheckConfig,
     Job,
+    Mesos,
     Parameter,
     SimpleTask
 )
@@ -82,6 +85,28 @@ def test_config_with_tier():
   assert job.taskConfig.tier == 'devel'
 
 
+def test_config_with_docker_image():
+  image_name = 'some-image'
+  image_tag = 'some-tag'
+  job = convert_pystachio_to_thrift(
+      HELLO_WORLD(container=Mesos(image=DockerImage(name=image_name, tag=image_tag))))
+
+  assert job.taskConfig.container.mesos.image.appc is None
+  assert job.taskConfig.container.mesos.image.docker.name == image_name
+  assert job.taskConfig.container.mesos.image.docker.tag == image_tag
+
+
+def test_config_with_appc_image():
+  image_name = 'some-image'
+  image_id = 'some-image-id'
+  job = convert_pystachio_to_thrift(
+          HELLO_WORLD(container=Mesos(image=AppcImage(name=image_name, image_id=image_id))))
+
+  assert job.taskConfig.container.mesos.image.docker is None
+  assert job.taskConfig.container.mesos.image.appc.name == image_name
+  assert job.taskConfig.container.mesos.image.appc.imageId == image_id
+
+
 def test_docker_with_parameters():
   helloworld = HELLO_WORLD(
     container=Container(

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/python/apache/aurora/executor/common/test_sandbox.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/executor/common/test_sandbox.py b/src/test/python/apache/aurora/executor/common/test_sandbox.py
index bd402fc..e47d9b8 100644
--- a/src/test/python/apache/aurora/executor/common/test_sandbox.py
+++ b/src/test/python/apache/aurora/executor/common/test_sandbox.py
@@ -18,7 +18,7 @@ import mock
 import pytest
 from twitter.common.contextutil import temporary_dir
 
-from apache.aurora.executor.common.sandbox import DirectorySandbox, DockerDirectorySandbox
+from apache.aurora.executor.common.sandbox import DirectorySandbox, FileSystemImageSandbox
 
 
 def test_directory_sandbox():
@@ -82,16 +82,16 @@ def test_create_ioerror(chown):
 
 
 @mock.patch('os.makedirs')
-def test_docker_sandbox_create_ioerror(makedirs):
+def test_filesystem_image_sandbox_create_ioerror(makedirs):
   makedirs.side_effect = IOError('Disk is borked')
 
   with mock.patch.dict('os.environ', {
-      DockerDirectorySandbox.MESOS_DIRECTORY_ENV_VARIABLE: 'some-directory',
-      DockerDirectorySandbox.MESOS_SANDBOX_ENV_VARIABLE: 'some-sandbox'
+    FileSystemImageSandbox.MESOS_DIRECTORY_ENV_VARIABLE: 'some-directory',
+    FileSystemImageSandbox.MESOS_SANDBOX_ENV_VARIABLE: 'some-sandbox'
   }):
     with temporary_dir() as d:
       real_path = os.path.join(d, 'sandbox')
-      ds = DockerDirectorySandbox(real_path)
+      ds = FileSystemImageSandbox(real_path)
       with pytest.raises(DirectorySandbox.CreationError):
         ds.create()
 

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/sh/org/apache/aurora/e2e/Dockerfile
----------------------------------------------------------------------
diff --git a/src/test/sh/org/apache/aurora/e2e/Dockerfile b/src/test/sh/org/apache/aurora/e2e/Dockerfile
index b2557b5..6fdea3d 100644
--- a/src/test/sh/org/apache/aurora/e2e/Dockerfile
+++ b/src/test/sh/org/apache/aurora/e2e/Dockerfile
@@ -15,6 +15,9 @@
 FROM python:2.7
 
 # mesos.native requires libcurl-nss to initialize MesosExecutorDriver
-RUN apt-get update && apt-get -y install libcurl4-nss-dev
+# The mesos containerizer does not auto-create mount points, so we must initialize them manually.
+# TODO(jcohen): Remove this mkdir when https://issues.apache.org/jira/browse/MESOS-5229 is resolved.
+RUN apt-get update && apt-get -y install libcurl4-nss-dev \
+    && mkdir -p /home/vagrant/aurora/examples/vagrant/config
 
 COPY http_example.py /tmp/

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/sh/org/apache/aurora/e2e/http/http_example.aurora
----------------------------------------------------------------------
diff --git a/src/test/sh/org/apache/aurora/e2e/http/http_example.aurora b/src/test/sh/org/apache/aurora/e2e/http/http_example.aurora
index 2813b6c..219c40f 100644
--- a/src/test/sh/org/apache/aurora/e2e/http/http_example.aurora
+++ b/src/test/sh/org/apache/aurora/e2e/http/http_example.aurora
@@ -41,7 +41,7 @@ health_check_config = HealthCheckConfig(initial_interval_secs=5, interval_secs=1
 
 job = Service(
   cluster = 'devcluster',
-  instances = 2,
+  instances = 1,
   update_config = update_config,
   health_check_config = health_check_config,
   task = test_task,
@@ -70,5 +70,11 @@ jobs = [
     container = Container(docker=Docker(image = 'http_example'))
   ).bind(
     cmd = 'cp /tmp/http_example.py .'
+  ),
+  job(
+    name = 'http_example_appc',
+    container = Mesos(image=AppcImage(name='http_example', image_id='{{appc_image_id}}'))
+  ).bind(
+    cmd = 'cp /tmp/http_example.py .'
   )
 ]

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/sh/org/apache/aurora/e2e/http/http_example_bad_healthcheck.aurora
----------------------------------------------------------------------
diff --git a/src/test/sh/org/apache/aurora/e2e/http/http_example_bad_healthcheck.aurora b/src/test/sh/org/apache/aurora/e2e/http/http_example_bad_healthcheck.aurora
index 0534c9e..08553e4 100644
--- a/src/test/sh/org/apache/aurora/e2e/http/http_example_bad_healthcheck.aurora
+++ b/src/test/sh/org/apache/aurora/e2e/http/http_example_bad_healthcheck.aurora
@@ -76,5 +76,11 @@ jobs = [
     container = Container(docker=Docker(image = 'http_example'))
   ).bind(
     cmd = 'cp /tmp/http_example.py .'
+  ),
+  job(
+    name = 'http_example_appc',
+    container = Mesos(image=AppcImage(name='http_example', image_id='{{appc_image_id}}'))
+  ).bind(
+    cmd = 'cp /tmp/http_example.py .'
   )
 ]

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/sh/org/apache/aurora/e2e/http/http_example_updated.aurora
----------------------------------------------------------------------
diff --git a/src/test/sh/org/apache/aurora/e2e/http/http_example_updated.aurora b/src/test/sh/org/apache/aurora/e2e/http/http_example_updated.aurora
index b33e8f5..8b3a50b 100644
--- a/src/test/sh/org/apache/aurora/e2e/http/http_example_updated.aurora
+++ b/src/test/sh/org/apache/aurora/e2e/http/http_example_updated.aurora
@@ -62,5 +62,11 @@ jobs = [
     container = Container(docker=Docker(image = 'http_example'))
   ).bind(
     cmd = 'cp /tmp/http_example.py .'
+  ),
+  job(
+    name = 'http_example_appc',
+    container = Mesos(image=AppcImage(name='http_example', image_id='{{appc_image_id}}'))
+  ).bind(
+    cmd = 'cp /tmp/http_example.py .'
   )
 ]

http://git-wip-us.apache.org/repos/asf/aurora/blob/1eac7a67/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh
----------------------------------------------------------------------
diff --git a/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh b/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh
index eee6b4c..abe0ca7 100755
--- a/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh
+++ b/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh
@@ -19,7 +19,7 @@
 # from within the environment.
 if [[ "$USER" != "vagrant" ]]; then
   vagrant up
-  vagrant ssh -c /vagrant/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh "$@"
+  time vagrant ssh -c /vagrant/src/test/sh/org/apache/aurora/e2e/test_end_to_end.sh "$@"
   exit $?
 fi
 
@@ -90,13 +90,15 @@ test_config() {
 
 test_inspect() {
   local _jobkey=$1 _config=$2
+  shift 2
+  local _extra_args="${@}"
 
-  aurora job inspect $_jobkey $_config
+  aurora job inspect $_jobkey $_config $_extra_args
 }
 
 test_create() {
   local _jobkey=$1 _config=$2
-  shift; shift
+  shift 2
   local _extra_args="${@}"
 
   aurora job create $_jobkey $_config $_extra_args
@@ -165,8 +167,10 @@ assert_update_state() {
 
 test_update() {
   local _jobkey=$1 _config=$2 _cluster=$3
+  shift 3
+  local _extra_args="${@}"
 
-  aurora update start $_jobkey $_config
+  aurora update start $_jobkey $_config $_extra_args
   assert_update_state $_jobkey 'ROLLING_FORWARD'
   local _update_id=$(aurora update list $_jobkey --status ROLLING_FORWARD \
       | tail -n +2 | awk '{print $2}')
@@ -189,8 +193,11 @@ test_update() {
 
 test_update_fail() {
   local _jobkey=$1 _config=$2 _cluster=$3  _bad_healthcheck_config=$4
+  shift 4
+  local _extra_args="${@}"
+
   # Make sure our updates works.
-  aurora update start $_jobkey $_config
+  aurora update start $_jobkey $_config $_extra_args
   assert_update_state $_jobkey 'ROLLING_FORWARD'
   local _update_id=$(aurora update list $_jobkey --status ROLLING_FORWARD \
       | tail -n +2 | awk '{print $2}')
@@ -198,7 +205,7 @@ test_update_fail() {
   aurora update wait $_jobkey $_update_id
 
   # Starting update with a health check that is meant to fail. Expected behavior is roll back.
-  aurora update start $_jobkey $_bad_healthcheck_config
+  aurora update start $_jobkey $_bad_healthcheck_config $_extra_args
   local _update_id=$(aurora update list $_jobkey --status active \
       | tail -n +2 | awk '{print $2}')
   # || is so that we don't return an EXIT so that `trap collect_result` doesn't get triggered.
@@ -312,22 +319,24 @@ test_http_example() {
   local _base_config=$4 _updated_config=$5
   local _bad_healthcheck_config=$6
   local _job=$7
+  local _bind_parameters=${8:-""}
+
   local _jobkey="$_cluster/$_role/$_env/$_job"
   local _task_id_prefix="${_role}-${_env}-${_job}-0"
   local _discovery_name="${_job}.${_env}.${_role}"
 
   test_config $_base_config $_jobkey
-  test_inspect $_jobkey $_base_config
-  test_create $_jobkey $_base_config
+  test_inspect $_jobkey $_base_config $_bind_parameters
+  test_create $_jobkey $_base_config $_bind_parameters
   test_job_status $_cluster $_role $_env $_job
   test_scheduler_ui $_role $_env $_job
   test_observer_ui $_cluster $_role $_job
   test_discovery_info $_task_id_prefix $_discovery_name
   test_restart $_jobkey
-  test_update $_jobkey $_updated_config $_cluster
-  test_update_fail $_jobkey $_base_config  $_cluster $_bad_healthcheck_config
+  test_update $_jobkey $_updated_config $_cluster $_bind_parameters
+  test_update_fail $_jobkey $_base_config  $_cluster $_bad_healthcheck_config $_bind_parameters
   # Running test_update second time to change state to success.
-  test_update $_jobkey $_updated_config $_cluster
+  test_update $_jobkey $_updated_config $_cluster $_bind_parameters
   test_announce $_role $_env $_job
   test_run $_jobkey
   test_kill $_jobkey
@@ -387,6 +396,29 @@ test_basic_auth_unauthenticated() {
   restore_netrc
 }
 
+test_appc() {
+  TEMP_PATH=$(mktemp -d)
+  pushd "$TEMP_PATH"
+
+  # build the appc image from the docker image
+  docker save -o http_example-latest.tar http_example
+  docker2aci http_example-latest.tar
+
+  APPC_IMAGE_ID="sha512-$(sha512sum http_example-latest.aci | awk '{print $1}')"
+  APPC_IMAGE_DIRECTORY="/tmp/mesos/images/appc/images/$APPC_IMAGE_ID"
+
+  sudo mkdir -p "$APPC_IMAGE_DIRECTORY"
+  sudo tar -xf http_example-latest.aci -C "$APPC_IMAGE_DIRECTORY"
+  # This restart is necessary for mesos to pick up the image from the local store.
+  sudo restart mesos-slave
+
+  popd
+  rm -rf "$TEMP_PATH"
+
+  TEST_JOB_APPC_ARGS=("${BASE_ARGS[@]}" "$TEST_JOB_APPC" "--bind appc_image_id=$APPC_IMAGE_ID")
+  test_http_example "${TEST_JOB_APPC_ARGS[@]}"
+}
+
 RETCODE=1
 # Set up shorthands for test
 export TEST_ROOT=/vagrant/src/test/sh/org/apache/aurora/e2e
@@ -398,6 +430,7 @@ TEST_ENV=test
 TEST_JOB=http_example
 TEST_JOB_REVOCABLE=http_example_revocable
 TEST_JOB_DOCKER=http_example_docker
+TEST_JOB_APPC=http_example_appc
 TEST_CONFIG_FILE=$EXAMPLE_DIR/http_example.aurora
 TEST_CONFIG_UPDATED_FILE=$EXAMPLE_DIR/http_example_updated.aurora
 TEST_BAD_HEALTHCHECK_CONFIG_UPDATED_FILE=$EXAMPLE_DIR/http_example_bad_healthcheck.aurora
@@ -442,6 +475,9 @@ test_http_revocable_example "${TEST_JOB_REVOCABLE_ARGS[@]}"
 sudo docker build -t http_example ${TEST_ROOT}
 test_http_example "${TEST_JOB_DOCKER_ARGS[@]}"
 
+# This test relies on the docker image having been built above.
+test_appc
+
 test_admin "${TEST_ADMIN_ARGS[@]}"
 test_basic_auth_unauthenticated  "${TEST_JOB_ARGS[@]}"