Initial implementation of shared test cell warden.

Change-Id: Ia973d514fe1dd11ffe4cdb7c902cc43a9c2eb626
diff --git a/tools/dev/bash_profile b/tools/dev/bash_profile
index 81252ce..a7aca28 100644
--- a/tools/dev/bash_profile
+++ b/tools/dev/bash_profile
@@ -103,10 +103,41 @@
     echo $OCI
 }
 
+# ON.Lab shared test cell warden address
+export CELL_WARDEN="10.254.1.19"
+
 # Applies the settings in the specified cell file or lists current cell definition
 # if no cell file is given.
 function cell {
-    if [ -n "$1" ]; then
+    cell=$1
+    case "$cell" in
+    "borrow")
+        aux="/tmp/cell-$$"
+        curl -sS -X POST "http://$CELL_WARDEN:4321/?user=$(id -un)&duration=${2:-60}" \
+            -d "$(cat ~/.ssh/id_rsa.pub)" > $aux
+        . $aux
+        rm -f $aux
+        export ONOS_INSTANCES=$(env | grep 'OC[0-9]*=' | sort | cut -d= -f2)
+        setPrimaryInstance 1 >/dev/null
+        cell
+        ;;
+    "return")
+        curl -sS -X DELETE "http://$CELL_WARDEN:4321/?user=$(id -un)"
+        ;;
+
+    "status")
+        curl -sS "http://$CELL_WARDEN:4321/"
+        ;;
+
+    "")
+        env | egrep "ONOS_CELL"
+        env | egrep "OCI"
+        env | egrep "OC[0-9]+" | sort
+        env | egrep "OC[NT]"
+        env | egrep "ONOS_" | egrep -v 'ONOS_ROOT|ONOS_CELL|ONOS_INSTANCES' | sort
+        ;;
+
+    *)
         [ ! -f $ONOS_ROOT/tools/test/cells/$1 ] && \
             echo "No such cell: $1" >&2 && return 1
         unset ONOS_CELL ONOS_NIC ONOS_IP ONOS_APPS ONOS_BOOT_FEATURES
@@ -121,16 +152,10 @@
         export ONOS_INSTANCES=$(env | grep 'OC[0-9]*=' | sort | cut -d= -f2)
         setPrimaryInstance 1 >/dev/null
         cell
-    else
-        env | egrep "ONOS_CELL"
-        env | egrep "OCI"
-        env | egrep "OC[0-9]+" | sort
-        env | egrep "OC[NT]"
-        env | egrep "ONOS_" | egrep -v 'ONOS_ROOT|ONOS_CELL|ONOS_INSTANCES' | sort
-    fi
+    esac
 }
 
-cell $ONOS_CELL > /dev/null
+[ -n "$ONOS_CELL" ] && cell $ONOS_CELL > /dev/null
 
 # Lists available cells
 function cells {
diff --git a/tools/test/bin/ogroup-opts b/tools/test/bin/ogroup-opts
index 428cc15..dd92944 100644
--- a/tools/test/bin/ogroup-opts
+++ b/tools/test/bin/ogroup-opts
@@ -40,7 +40,7 @@
 function _cell-opts () {
   local cur=${COMP_WORDS[COMP_CWORD]}
   if [ $COMP_CWORD -eq 1 ]; then
-    COMPREPLY=( $( compgen -W "$(cd $ONOS_ROOT/tools/test/cells && ls -1)" -- $cur ) )
+    COMPREPLY=( $( compgen -W "$(cd $ONOS_ROOT/tools/test/cells && ls -1) borrow return status" -- $cur ) )
   fi
 }
 
diff --git a/utils/warden/pom.xml b/utils/warden/pom.xml
new file mode 100644
index 0000000..bf6c8d4
--- /dev/null
+++ b/utils/warden/pom.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015-present Open Networking Laboratory
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onosproject</groupId>
+        <artifactId>onlab-utils</artifactId>
+        <version>1.6.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>onlab-warden</artifactId>
+    <packaging>jar</packaging>
+
+    <description>System Test Cell Warden</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+            <version>8.1.18.v20150929</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlet</artifactId>
+            <version>8.1.18.v20150929</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>2.4.3</version>
+                <configuration>
+                    <transformers>
+                        <transformer
+                                implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                            <mainClass>org.onlab.warden.Main</mainClass>
+                        </transformer>
+                    </transformers>
+                    <filters>
+                        <filter>
+                            <artifact>*:*</artifact>
+                            <excludes>
+                                <exclude>META-INF/*.SF</exclude>
+                                <exclude>META-INF/*.DSA</exclude>
+                                <exclude>META-INF/*.RSA</exclude>
+                            </excludes>
+                        </filter>
+                    </filters>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/utils/warden/src/main/java/org/onlab/warden/Main.java b/utils/warden/src/main/java/org/onlab/warden/Main.java
new file mode 100644
index 0000000..e9bd71a
--- /dev/null
+++ b/utils/warden/src/main/java/org/onlab/warden/Main.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onlab.warden;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Main program for executing scenario test warden.
+ */
+public final class Main {
+
+    // Public construction forbidden
+    private Main(String[] args) {
+    }
+
+    /**
+     * Main entry point for the cell warden.
+     *
+     * @param args command-line arguments
+     */
+    public static void main(String[] args) {
+        Main main = new Main(args);
+        main.run();
+    }
+
+    // Runs the warden processing
+    private void run() {
+        startWebServer();
+    }
+
+    // Initiates a web-server.
+    private static void startWebServer() {
+        WardenServlet.warden = new Warden();
+        org.eclipse.jetty.util.log.Log.setLog(new NullLogger());
+        Server server = new Server(4321);
+        ServletHandler handler = new ServletHandler();
+        server.setHandler(handler);
+        handler.addServletWithMapping(WardenServlet.class, "/*");
+        try {
+            server.start();
+        } catch (Exception e) {
+            print("Warden already active...");
+        }
+    }
+
+    private static void print(String s) {
+        System.out.println(s);
+    }
+
+    // Logger to quiet Jetty down
+    private static class NullLogger implements Logger {
+        @Override
+        public String getName() {
+            return "quiet";
+        }
+
+        @Override
+        public void warn(String msg, Object... args) {
+        }
+
+        @Override
+        public void warn(Throwable thrown) {
+        }
+
+        @Override
+        public void warn(String msg, Throwable thrown) {
+        }
+
+        @Override
+        public void info(String msg, Object... args) {
+        }
+
+        @Override
+        public void info(Throwable thrown) {
+        }
+
+        @Override
+        public void info(String msg, Throwable thrown) {
+        }
+
+        @Override
+        public boolean isDebugEnabled() {
+            return false;
+        }
+
+        @Override
+        public void setDebugEnabled(boolean enabled) {
+        }
+
+        @Override
+        public void debug(String msg, Object... args) {
+        }
+
+        @Override
+        public void debug(Throwable thrown) {
+        }
+
+        @Override
+        public void debug(String msg, Throwable thrown) {
+        }
+
+        @Override
+        public Logger getLogger(String name) {
+            return this;
+        }
+
+        @Override
+        public void ignore(Throwable ignored) {
+        }
+    }
+
+}
diff --git a/utils/warden/src/main/java/org/onlab/warden/Reservation.java b/utils/warden/src/main/java/org/onlab/warden/Reservation.java
new file mode 100644
index 0000000..59ffe3a
--- /dev/null
+++ b/utils/warden/src/main/java/org/onlab/warden/Reservation.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onlab.warden;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Cell reservation record.
+ */
+final class Reservation {
+
+    final String cellName;
+    final String userName;
+    final long time;
+    final int duration;
+
+    // Creates a new reservation record
+    Reservation(String cellName, String userName, long time, int duration) {
+        this.cellName = cellName;
+        this.userName = userName;
+        this.time = time;
+        this.duration = duration;
+    }
+
+    /**
+     * Decodes reservation record from the specified line.
+     *
+     * @param line string line
+     */
+    Reservation(String line) {
+        String[] fields = line.trim().split("\t");
+        checkState(fields.length == 4, "Incorrect reservation encoding");
+        this.cellName = fields[0];
+        this.userName = fields[1];
+        this.time = Long.parseLong(fields[2]);
+        this.duration = Integer.parseInt(fields[3]);
+    }
+
+    /**
+     * Encodes reservation record into a string line.
+     *
+     * @return encoded string
+     */
+    String encode() {
+        return String.format("%s\t%s\t%s\t%s\n", cellName, userName, time, duration);
+    }
+
+}
diff --git a/utils/warden/src/main/java/org/onlab/warden/Warden.java b/utils/warden/src/main/java/org/onlab/warden/Warden.java
new file mode 100644
index 0000000..3f2caba
--- /dev/null
+++ b/utils/warden/src/main/java/org/onlab/warden/Warden.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onlab.warden;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.ByteStreams;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.google.common.base.Preconditions.*;
+
+/**
+ * Warden for tracking use of shared test cells.
+ */
+class Warden {
+
+    private static final String CELL_NOT_NULL = "Cell name cannot be null";
+    private static final String USER_NOT_NULL = "User name cannot be null";
+    private static final String KEY_NOT_NULL = "User key cannot be null";
+    private static final String UTF_8 = "UTF-8";
+    private static final long TIMEOUT = 3;
+
+    private static final String AUTHORIZED_KEYS = "authorized_keys";
+
+    private static final int MAX_MINUTES = 240; // 4 hours max
+    private static final int MINUTE = 60_000; // 1 minute
+
+    private final File log = new File("warden.log");
+
+    private final File cells = new File("cells");
+    private final File supported = new File(cells, "supported");
+    private final File reserved = new File(cells, "reserved");
+
+    private final Random random = new Random();
+
+    private final Timer timer = new Timer("cell-pruner", true);
+
+    /**
+     * Creates a new cell warden.
+     */
+    Warden() {
+        random.setSeed(System.currentTimeMillis());
+        timer.schedule(new Reposessor(), MINUTE / 4, MINUTE);
+    }
+
+    /**
+     * Returns list of names of supported cells.
+     *
+     * @return list of cell names
+     */
+    Set<String> getCells() {
+        String[] list = supported.list();
+        return list != null ? ImmutableSet.copyOf(list) : ImmutableSet.of();
+    }
+
+    /**
+     * Returns list of names of available cells.
+     *
+     * @return list of cell names
+     */
+    Set<String> getAvailableCells() {
+        Set<String> available = new HashSet<>(getCells());
+        available.removeAll(getReservedCells());
+        return ImmutableSet.copyOf(available);
+    }
+
+    /**
+     * Returns list of names of reserved cells.
+     *
+     * @return list of cell names
+     */
+    Set<String> getReservedCells() {
+        String[] list = reserved.list();
+        return list != null ? ImmutableSet.copyOf(list) : ImmutableSet.of();
+    }
+
+
+    /**
+     * Returns reservation for the specified user.
+     *
+     * @param userName user name
+     * @return cell reservation record or null if user does not have one
+     */
+    Reservation currentUserReservation(String userName) {
+        checkNotNull(userName, USER_NOT_NULL);
+        for (String cellName : getReservedCells()) {
+            Reservation reservation = currentCellReservation(cellName);
+            if (reservation != null && userName.equals(reservation.userName)) {
+                return reservation;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the name of the user who reserved the given cell.
+     *
+     * @param cellName cell name
+     * @return cell reservation record or null if cell is not reserved
+     */
+    Reservation currentCellReservation(String cellName) {
+        checkNotNull(cellName, CELL_NOT_NULL);
+        File cellFile = new File(reserved, cellName);
+        if (!cellFile.exists()) {
+            return null;
+        }
+        try (InputStream stream = new FileInputStream(cellFile)) {
+            return new Reservation(new String(ByteStreams.toByteArray(stream), "UTF-8"));
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to get current user for cell " + cellName, e);
+        }
+    }
+
+    /**
+     * Reserves a cell for the specified user and their public access key.
+     *
+     * @param userName user name
+     * @param sshKey   user ssh public key
+     * @param minutes  number of minutes for reservation
+     * @return reserved cell definition
+     */
+    synchronized String borrowCell(String userName, String sshKey, int minutes) {
+        checkNotNull(userName, USER_NOT_NULL);
+        checkNotNull(sshKey, KEY_NOT_NULL);
+        checkArgument(minutes > 0, "Number of minutes must be positive");
+        checkArgument(minutes < MAX_MINUTES, "Number of minutes must be less than %d", MAX_MINUTES);
+        long now = System.currentTimeMillis();
+        Reservation reservation = currentUserReservation(userName);
+        if (reservation == null) {
+            Set<String> cells = getAvailableCells();
+            checkState(!cells.isEmpty(), "No cells are presently available");
+            String cellName = ImmutableList.copyOf(cells).get(random.nextInt(cells.size()));
+            reservation = new Reservation(cellName, userName, now, minutes);
+        } else {
+            reservation = new Reservation(reservation.cellName, userName, now, minutes);
+        }
+
+        reserveCell(reservation.cellName, reservation);
+        installUserKeys(reservation.cellName, userName, sshKey);
+        log(userName, reservation.cellName, "borrowed for " + minutes + " minutes");
+        return getCellDefinition(reservation.cellName);
+    }
+
+    /**
+     * Reserves the specified cell for the user the source file and writes the
+     * specified content to the target file.
+     *
+     * @param cellName    cell name
+     * @param reservation cell reservation record
+     */
+    private void reserveCell(String cellName, Reservation reservation) {
+        try (FileOutputStream stream = new FileOutputStream(new File(reserved, cellName))) {
+            stream.write(reservation.encode().getBytes(UTF_8));
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to reserve cell " + cellName, e);
+        }
+    }
+
+    /**
+     * Returns the specified cell for the specified user and their public access key.
+     *
+     * @param userName user name
+     */
+    synchronized void returnCell(String userName) {
+        checkNotNull(userName, USER_NOT_NULL);
+        Reservation reservation = currentUserReservation(userName);
+        checkState(reservation != null, "User %s has no cell reservations", userName);
+        checkState(new File(reserved, reservation.cellName).delete(),
+                   "Unable to return cell %s", reservation.cellName);
+        uninstallUserKeys(reservation.cellName);
+        log(userName, reservation.cellName, "returned");
+    }
+
+    /**
+     * Reads the definition of the specified cell.
+     *
+     * @param cellName cell name
+     * @return cell definition
+     */
+    String getCellDefinition(String cellName) {
+        File cellFile = new File(supported, cellName);
+        try (InputStream stream = new FileInputStream(cellFile)) {
+            return new String(ByteStreams.toByteArray(stream), UTF_8);
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to definition for cell " + cellName, e);
+        }
+    }
+
+    // Returns list of cell hosts, i.e. OC#, OCN
+    private List<String> cellHosts(String cellName) {
+        ImmutableList.Builder<String> builder = ImmutableList.builder();
+        Pattern pattern = Pattern.compile("export OC[0-9N]=(.*)");
+        for (String line : getCellDefinition(cellName).split("\n")) {
+            Matcher matcher = pattern.matcher(line);
+            if (matcher.matches()) {
+                builder.add(matcher.group(1).replaceAll("[\"']", ""));
+            }
+        }
+        return builder.build();
+    }
+
+    // Installs the specified user's key on all hosts of the given cell.
+    private void installUserKeys(String cellName, String userName, String sshKey) {
+        File authKeysFile = authKeys(sshKey);
+        for (String host : cellHosts(cellName)) {
+            installAuthorizedKey(host, authKeysFile.getPath());
+        }
+        checkState(authKeysFile.delete(), "Unable to install user keys");
+    }
+
+    // Uninstalls the user keys on the specified cell
+    private void uninstallUserKeys(String cellName) {
+        for (String host : cellHosts(cellName)) {
+            installAuthorizedKey(host, AUTHORIZED_KEYS);
+        }
+    }
+
+    // Installs the authorized keys on the specified host.
+    private void installAuthorizedKey(String host, String authorizedKeysFile) {
+        String cmd = "scp " + authorizedKeysFile + " sdn@" + host + ":.ssh/authorized_keys";
+        try {
+            Process process = Runtime.getRuntime().exec(cmd);
+            process.waitFor(TIMEOUT, TimeUnit.SECONDS);
+        } catch (Exception e) {
+            throw new IllegalStateException("Unable to set authorized keys for host " + host);
+        }
+    }
+
+    // Returns the file containing authorized keys that incudes the specified key.
+    private File authKeys(String sshKey) {
+        File keysFile = new File(AUTHORIZED_KEYS);
+        try {
+            File tmp = File.createTempFile("warden-", ".auth");
+            tmp.deleteOnExit();
+            try (InputStream stream = new FileInputStream(keysFile);
+                 PrintWriter output = new PrintWriter(tmp)) {
+                String baseKeys = new String(ByteStreams.toByteArray(stream), UTF_8);
+                output.println(baseKeys);
+                output.println(sshKey);
+                return tmp;
+            } catch (IOException e) {
+                throw new IllegalStateException("Unable to generate authorized keys", e);
+            }
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to generate authorized keys", e);
+        }
+    }
+
+    // Creates an audit log entry.
+    void log(String userName, String cellName, String action) {
+        try (FileOutputStream fos = new FileOutputStream(log, true);
+             PrintWriter pw = new PrintWriter(fos)) {
+            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            pw.println(String.format("%s\t%s\t%s\t%s", format.format(new Date()),
+                                     userName, cellName, action));
+            pw.flush();
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to log reservation action", e);
+        }
+    }
+
+    // Task for re-possessing overdue cells
+    private class Reposessor extends TimerTask {
+        @Override
+        public void run() {
+            long now = System.currentTimeMillis();
+            for (String cellName : getReservedCells()) {
+                Reservation reservation = currentCellReservation(cellName);
+                if (reservation != null &&
+                        (reservation.time + reservation.duration * MINUTE) < now) {
+                    try {
+                        returnCell(reservation.userName);
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/utils/warden/src/main/java/org/onlab/warden/WardenServlet.java b/utils/warden/src/main/java/org/onlab/warden/WardenServlet.java
new file mode 100644
index 0000000..9f6ad33
--- /dev/null
+++ b/utils/warden/src/main/java/org/onlab/warden/WardenServlet.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onlab.warden;
+
+import com.google.common.io.ByteStreams;
+import org.eclipse.jetty.server.Response;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+/**
+ * Web socket servlet capable of creating web sockets for the STC monitor.
+ */
+public class WardenServlet extends HttpServlet {
+
+    static Warden warden;
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+            throws ServletException, IOException {
+        resp.setContentType("text/plain; charset=UTF-8");
+        SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+        try (PrintWriter out = resp.getWriter()) {
+            for (String cellName : warden.getCells()) {
+                Reservation reservation = warden.currentCellReservation(cellName);
+                if (reservation != null) {
+                    long expiration = reservation.time + reservation.duration * 60_000;
+                    out.println(String.format("%-10s\t%-10s\t%s\t%s\t%s minutes", cellName,
+                                              reservation.userName,
+                                              fmt.format(new Date(reservation.time)),
+                                              fmt.format(new Date(expiration)),
+                                              reservation.duration));
+                } else {
+                    out.println(String.format("%-10s\t%-10s", cellName, "available"));
+                }
+            }
+        } catch (Exception e) {
+            resp.setStatus(Response.SC_INTERNAL_SERVER_ERROR);
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+            throws ServletException, IOException {
+        try (PrintWriter out = resp.getWriter()) {
+            String sshKey = new String(ByteStreams.toByteArray(req.getInputStream()), "UTF-8");
+            String userName = req.getParameter("user");
+            String sd = req.getParameter("duration");
+            int duration = isNullOrEmpty(sd) ? 60 : Integer.parseInt(sd);
+            String cellDefinition = warden.borrowCell(userName, sshKey, duration);
+            out.println(cellDefinition);
+        } catch (Exception e) {
+            resp.setStatus(Response.SC_INTERNAL_SERVER_ERROR);
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
+            throws ServletException, IOException {
+        try (PrintWriter out = resp.getWriter()) {
+            String userName = req.getParameter("user");
+            warden.returnCell(userName);
+        } catch (Exception e) {
+            resp.setStatus(Response.SC_INTERNAL_SERVER_ERROR);
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/utils/warden/src/main/java/org/onlab/warden/package-info.java b/utils/warden/src/main/java/org/onlab/warden/package-info.java
new file mode 100644
index 0000000..d2e3d73
--- /dev/null
+++ b/utils/warden/src/main/java/org/onlab/warden/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Cell warden to coordinate borrowing and returning test cells.
+ */
+package org.onlab.warden;
\ No newline at end of file