Add new DUT Manage page to make it easier to add/remove/label DUT's

BUG=chromium:653727
TEST=local

Change-Id: Id554af22b672ecd1bb9f8eeb89bf9b0a4206a7a0
Reviewed-on: https://chromium-review.googlesource.com/396463
Commit-Ready: Keith Haddow <haddowk@chromium.org>
Tested-by: Keith Haddow <haddowk@chromium.org>
Reviewed-by: Keith Haddow <haddowk@chromium.org>
Reviewed-by: Michael Tang <ntang@chromium.org>
(cherry picked from commit 63cc4470e13cf7dab9edb7e18c0c39e9cd65ba47)
Reviewed-on: https://chromium-review.googlesource.com/399138
Commit-Queue: Keith Haddow <haddowk@chromium.org>
diff --git a/frontend/afe/moblab_rpc_interface.py b/frontend/afe/moblab_rpc_interface.py
index 22e6fef..f0ec8e3 100644
--- a/frontend/afe/moblab_rpc_interface.py
+++ b/frontend/afe/moblab_rpc_interface.py
@@ -18,17 +18,19 @@
 import ConfigParser
 import logging
 import os
+import re
 import shutil
 import socket
-import re
-
+import subprocess
 import common
 
 from autotest_lib.client.common_lib import error
 from autotest_lib.client.common_lib import global_config
+from autotest_lib.frontend.afe import models, model_logic, model_attributes
 from autotest_lib.frontend.afe import rpc_utils
 from autotest_lib.server.hosts import moblab_host
 
+
 _CONFIG = global_config.global_config
 MOBLAB_BOTO_LOCATION = '/home/moblab/.boto'
 
@@ -45,6 +47,11 @@
 _RESULT_STORAGE_SERVER = 'results_storage_server'
 _USE_EXISTING_BOTO_FILE = 'use_existing_boto_file'
 
+# Location where dhcp leases are stored.
+_DHCPD_LEASES = '/var/lib/dhcp/dhcpd.leases'
+
+# File where information about the current device is stored.
+_ETC_LSB_RELEASE = '/etc/lsb-release'
 
 @rpc_utils.moblab_only
 def get_config_values():
@@ -456,10 +463,106 @@
 @rpc_utils.moblab_only
 def get_version_info():
     """ RPC handler to get informaiton about the version of the moblab.
+
     @return: A serialized JSON RPC object.
     """
-    lines = open('/etc/lsb-release').readlines()
-    lines.remove('')
-    version_response = {x.split('=')[0]: x.split('=')[1] for x in lines}
+    lines = open(_ETC_LSB_RELEASE).readlines()
+    version_response = {
+        x.split('=')[0]: x.split('=')[1] for x in lines if '=' in x}
     return rpc_utils.prepare_for_serialization(version_response)
 
+
+@rpc_utils.moblab_only
+def get_connected_dut_info():
+    """ RPC handler to get informaiton about the DUTs connected to the moblab.
+
+    @return: A serialized JSON RPC object.
+    """
+    # Make a list of the connected DUT's
+    leases = _get_dhcp_dut_leases()
+
+    # Get a list of the AFE configured DUT's
+    hosts = list(rpc_utils.get_host_query((), False, False, True, {}))
+    models.Host.objects.populate_relationships(hosts, models.Label,
+                                               'label_list')
+    configured_duts = {}
+    for host in hosts:
+        labels = [label.name for label in host.label_list]
+        labels.sort()
+        configured_duts[host.hostname] = ', '.join(labels)
+
+    return rpc_utils.prepare_for_serialization(
+            {'configured_duts': configured_duts,
+             'connected_duts': leases})
+
+
+def _get_dhcp_dut_leases():
+     """ Extract information about connected duts from the dhcp server.
+
+     @return: A dict of ipaddress to mac address for each device connected.
+     """
+     lease_info = open(_DHCPD_LEASES).read()
+
+     leases = {}
+     for lease in lease_info.split('lease'):
+         if lease.find('binding state active;') != -1:
+             ipaddress = lease.split('\n')[0].strip(' {')
+             last_octet = int(ipaddress.split('.')[-1].strip())
+             if last_octet > 150:
+                 continue
+             mac_address_search = re.search('hardware ethernet (.*);', lease)
+             if mac_address_search:
+                 leases[ipaddress] = mac_address_search.group(1)
+     return leases
+
+
+@rpc_utils.moblab_only
+def add_moblab_dut(ipaddress):
+    """ RPC handler to add a connected DUT to autotest.
+
+    @return: A string giving information about the status.
+    """
+    cmd = '/usr/local/autotest/cli/atest host create %s &' % ipaddress
+    subprocess.call(cmd, shell=True)
+    return (True, 'DUT %s added to Autotest' % ipaddress)
+
+
+@rpc_utils.moblab_only
+def remove_moblab_dut(ipaddress):
+    """ RPC handler to remove DUT entry from autotest.
+
+    @return: True if the command succeeds without an exception
+    """
+    models.Host.smart_get(ipaddress).delete()
+    return (True, 'DUT %s deleted from Autotest' % ipaddress)
+
+
+@rpc_utils.moblab_only
+def add_moblab_label(ipaddress, label_name):
+    """ RPC handler to add a label in autotest to a DUT entry.
+
+    @return: A string giving information about the status.
+    """
+    # Try to create the label in case it does not already exist.
+    label = None
+    try:
+        label = models.Label.add_object(name=label_name)
+    except:
+        label = models.Label.smart_get(label_name)
+    host_obj = models.Host.smart_get(ipaddress)
+    if label:
+        label.host_set.add(host_obj)
+        return (True, 'Added label %s to DUT %s' % (label_name, ipaddress))
+    return (False, 'Failed to add label %s to DUT %s' % (label_name, ipaddress))
+
+
+@rpc_utils.moblab_only
+def remove_moblab_label(ipaddress, label_name):
+    """ RPC handler to remove a label in autotest from a DUT entry.
+
+    @return: A string giving information about the status.
+    """
+    host_obj = models.Host.smart_get(ipaddress)
+    models.Label.smart_get(label_name).host_set.remove(host_obj)
+    return (True, 'Removed label %s from DUT %s' % (label_name, ipaddress))
+
diff --git a/frontend/client/src/autotest/MoblabSetupClient.gwt.xml b/frontend/client/src/autotest/MoblabSetupClient.gwt.xml
index 97b2859..2face8e 100644
--- a/frontend/client/src/autotest/MoblabSetupClient.gwt.xml
+++ b/frontend/client/src/autotest/MoblabSetupClient.gwt.xml
@@ -2,6 +2,7 @@
     <inherits name='com.google.gwt.user.User'/>
     <inherits name='com.google.gwt.json.JSON'/>
     <inherits name='com.google.gwt.http.HTTP'/>
+    <inherits name="com.google.gwt.user.theme.chrome.Chrome"/>
 
     <source path="moblab"/>
     <source path="common"/>
@@ -10,4 +11,5 @@
     <stylesheet src='common.css'/>
     <stylesheet src='afeclient.css'/>
     <stylesheet src='standard.css'/>
+    <stylesheet src='moblabsetup.css'/>
 </module>
diff --git a/frontend/client/src/autotest/moblab/DutManagementView.java b/frontend/client/src/autotest/moblab/DutManagementView.java
new file mode 100644
index 0000000..4e8ddad
--- /dev/null
+++ b/frontend/client/src/autotest/moblab/DutManagementView.java
@@ -0,0 +1,295 @@
+package autotest.moblab;
+
+import autotest.common.ui.TabView;
+import autotest.moblab.rpc.ConnectedDutInfo;
+import autotest.moblab.rpc.MoblabRpcCallbacks;
+import autotest.moblab.rpc.MoblabRpcHelper;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.HasVerticalAlignment;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.TextArea;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.VerticalPanel;
+
+/**
+ * Implement a tab that makes it easier to add/remove/maintain duts.
+ */
+public class DutManagementView extends TabView {
+
+  private FlexTable dutInfoTable;
+  private VerticalPanel dutSetupPanel;
+  private ListBox options;
+  private TextArea informationArea;
+  private Button actionButton;
+  private CheckBox poolCheckBox;
+  private TextBox poolLabelName;
+  private Label poolLabel;
+
+  private static final int DHCP_IP_COLUMN = 0;
+  private static final int DHCP_MAC_COLUMN = 1;
+  private static final int SELECTION_COLUMN = 2;
+  private static final int LABELS_COLUMN = 3;
+
+  @Override
+  public String getElementId() {
+    return "dut_manage";
+  }
+
+  @Override
+  public void refresh() {
+    super.refresh();
+    dutInfoTable.removeAllRows();
+    poolCheckBox.setValue(false);
+    poolLabelName.setText("");
+    loadData();
+  }
+
+  @Override
+  public void initialize() {
+    super.initialize();
+    // Main table of connected DUT information.
+    dutInfoTable = new FlexTable();
+    
+    // The row of controls underneath the main data table.
+    dutSetupPanel = new VerticalPanel();
+
+    // List of actions to be applied to connected DUT's.
+    options = new ListBox();
+    options.addItem("Add Selected DUT's");
+    options.addItem("Remove Selected DUT's");
+    options.addItem("Add Label Selected DUT's");
+    options.addItem("Remove Label Selected DUT's");
+    options.setStyleName("dut_manage_action_row");
+    options.addChangeHandler(new ChangeHandler() {
+      @Override
+      public void onChange(ChangeEvent event) {
+        if (options.getSelectedIndex() == 2 || options.getSelectedIndex() == 3) {
+          poolCheckBox.setEnabled(true);
+          poolCheckBox.setValue(false);
+          poolLabelName.setEnabled(true);
+          poolLabelName.setText("");
+        } else {
+          poolCheckBox.setEnabled(false);
+          poolLabelName.setEnabled(false);
+        }
+      }
+    });
+
+    // Logging area at the end of the screen that gives status messages about bulk
+    // actions requested.
+    informationArea = new TextArea();
+    informationArea.setVisibleLines(10);
+    informationArea.setCharacterWidth(80);
+    informationArea.setReadOnly(true);
+
+    // Apply button, each action needs to be applied after selecting the devices and
+    // the action to be performed.
+    actionButton = new Button("Apply", new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        ((Button)event.getSource()).setEnabled(false);
+        int action = options.getSelectedIndex();
+        try {
+          for (int i = 1; i < dutInfoTable.getRowCount(); i++) {
+            if (((CheckBox)dutInfoTable.getWidget(i, SELECTION_COLUMN)).getValue()) {
+              if (action == 0) {
+                  addDut(i);
+              } else if (action == 1) {
+                removeDut(i);
+              } else if (action == 2) {
+                addLabel(i, getLabelString());
+              } else if (action == 3) {
+                removeLabel(i, getLabelString());
+              }
+            }
+          }
+        } finally {
+          ((Button)event.getSource()).setEnabled(true);
+        }
+      }});
+
+
+    // For adding and removing labels a text input of the label is required.
+    poolCheckBox = new CheckBox();
+
+    // Pools are just special labels, this is just a helper to so users get
+    // it correct more of the time.
+    poolCheckBox.setText("Is pool label ?");
+    poolCheckBox.setStyleName("dut_manage_action_row_item");
+
+    // The text label explaining the text box is for entering the label.
+    poolLabel = new Label();
+    poolLabel.setText("Label name:");
+    poolLabel.setStyleName("dut_manage_action_row_item");
+
+    // The text entry of the label to add or remove.
+    poolLabelName = new TextBox();
+    poolLabelName.setStyleName("dut_manage_action_row_item");
+    poolCheckBox.setEnabled(false);
+    poolLabelName.setEnabled(false);
+  }
+
+  private String getLabelString() {
+    StringBuilder builder = new StringBuilder(poolLabelName.getText());
+    if (poolCheckBox.getValue()) {
+      builder.insert(0, "pool:");
+    }
+    return builder.toString();
+  }
+
+  private void loadData() {
+
+    MoblabRpcHelper.fetchDutInformation(new MoblabRpcCallbacks.FetchConnectedDutInfoCallback() {
+      @Override
+      public void onFetchConnectedDutInfoSubmitted(ConnectedDutInfo info) {
+        addTableHeader();
+        // The header is row 0
+        int row = 1;
+        for (final String dutIpAddress : info.getConnectedIpsToMacAddress().keySet()) {
+          addRowStyles(row);
+          String labelString;
+          if (info.getConfiguredIpsToLabels().keySet().contains(dutIpAddress)) {
+            labelString = info.getConfiguredIpsToLabels().get(dutIpAddress);
+          } else {
+            labelString = "DUT Not Configured in Autotest";
+          }
+          addRow(row, dutIpAddress, info.getConnectedIpsToMacAddress().get(dutIpAddress), labelString);
+          row++;
+        }
+        for (final String dutIpAddress : info.getConfiguredIpsToLabels().keySet()) {
+          if (!info.getConnectedIpsToMacAddress().keySet().contains(dutIpAddress)) {
+            // Device is in AFE but not detected in the DHCP table.
+            addRowStyles(row);
+            addRow(row, dutIpAddress, "",
+                "DUT Configured in Autotest but does not appear to be attached.");
+            row++;
+          }
+        }
+      }
+    });
+
+    // Assemble the display panels in the correct order.
+    dutSetupPanel.add(dutInfoTable);
+    HorizontalPanel actionRow = new HorizontalPanel();
+    actionRow.setStyleName("dut_manage_action_row");
+    actionRow.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
+    actionRow.add(options);
+    actionRow.add(poolCheckBox);
+    actionRow.add(poolLabel);
+    actionRow.add(poolLabelName);
+    actionRow.add(actionButton);
+    dutSetupPanel.add(actionRow);
+    dutSetupPanel.add(informationArea);
+    addWidget(dutSetupPanel, "view_dut_manage");
+  }
+
+  /**
+   * Add the correct css styles for each data row.
+   * @param row index of the row to apply the styles for, first data row is 1.
+   */
+  private void addRowStyles(int row) {
+    dutInfoTable.getCellFormatter().addStyleName(row, DHCP_IP_COLUMN,"ip_cell");
+    dutInfoTable.getCellFormatter().addStyleName(row,DHCP_MAC_COLUMN,"mac_cell");
+    dutInfoTable.getCellFormatter().addStyleName(row,SELECTION_COLUMN,"selection_cell");
+    dutInfoTable.getCellFormatter().addStyleName(row,LABELS_COLUMN,"labels_cell");
+  }
+
+  /**
+   * Insert or update the data in the table for a given row.
+   * @param row  The row index to update, first data row is 1.
+   * @param ipColumn String to be added into the first column.
+   * @param macColumn String to be added to the second column.
+   * @param labelsColumn String to be added to the fourth column.
+   */
+  private void addRow(int row, String ipColumn, String macColumn, String labelsColumn) {
+    dutInfoTable.setWidget(row, DHCP_IP_COLUMN, new Label(ipColumn));
+    dutInfoTable.setWidget(row, DHCP_MAC_COLUMN, new Label(macColumn));
+    dutInfoTable.setWidget(row, SELECTION_COLUMN, new CheckBox());
+    dutInfoTable.setWidget(row, LABELS_COLUMN, new Label(labelsColumn));
+  }
+
+  /**
+   * Add the column headers with the correct css styling into the data table.
+   */
+  private void addTableHeader() {
+    dutInfoTable.addStyleName("dut_info_table");
+    dutInfoTable.getCellFormatter().addStyleName(0, DHCP_IP_COLUMN,
+        "dut_manage_column_label_c");
+    dutInfoTable.getCellFormatter().addStyleName(0, DHCP_MAC_COLUMN,
+        "dut_manage_column_label_c");
+    dutInfoTable.getCellFormatter().addStyleName(0, SELECTION_COLUMN,
+        "dut_manage_column_label_c");
+    dutInfoTable.getCellFormatter().addStyleName(0, LABELS_COLUMN,
+        "dut_manage_column_label_c");
+    dutInfoTable.setWidget(0, DHCP_IP_COLUMN, new Label("DCHP Lease Address"));
+    dutInfoTable.setWidget(0, DHCP_MAC_COLUMN, new Label("DCHP MAC Address"));
+    dutInfoTable.setWidget(0, LABELS_COLUMN, new Label("DUT Labels"));
+  }
+
+  /**
+   * Make an RPC call to the autotest system to enroll the DUT listed at the given row number.
+   * @param row_number the row number in the table that has details of the device to enroll.
+   */
+  private void addDut(int row_number) {
+    String ipAddress = ((Label)dutInfoTable.getWidget(row_number, DHCP_IP_COLUMN)).getText();
+    MoblabRpcHelper.addMoblabDut(ipAddress, new LogAction(informationArea));
+  }
+
+  /**
+   * Make an RPC to to the autotest system to delete information about the DUT listed at the given
+   * row.
+   * @param row_number the row number in the table that has details of the device to remove.
+   */
+  private void removeDut(int row_number) {
+    String ipAddress = ((Label)dutInfoTable.getWidget(row_number, DHCP_IP_COLUMN)).getText();
+    MoblabRpcHelper.removeMoblabDut(ipAddress, new LogAction(informationArea));
+  }
+
+  /**
+   * Make an RPC to to the autotest system to add a label to a DUT whoes details are in the given
+   * row.
+   * @param row_number row in the data table that has the information about the DUT
+   * @param labelName the label string to be added.
+   */
+  private void addLabel(int row_number, String labelName) {
+    String ipAddress = ((Label)dutInfoTable.getWidget(row_number, DHCP_IP_COLUMN)).getText();
+    MoblabRpcHelper.addMoblabLabel(ipAddress, labelName, new LogAction(informationArea));
+  }
+
+  /**
+   * Make an RPC to to the autotest system to remove a label to a DUT whoes details are in the
+   * given row.
+   * @param row_number row in the data table that has the information about the DUT
+   * @param labelName the label string to be removed.
+   */
+  private void removeLabel(int row_number, String labelName) {
+    String ipAddress = ((Label)dutInfoTable.getWidget(row_number, DHCP_IP_COLUMN)).getText();
+    MoblabRpcHelper.removeMoblabLabel(ipAddress, labelName, new LogAction(informationArea));
+  }
+
+  /**
+   * Call back that inserts a string from an completed RPC into the UI.
+   */
+  private static class LogAction implements MoblabRpcCallbacks.LogActionCompleteCallback {
+    private TextArea informationArea;
+
+    LogAction(TextArea informationArea){
+      this.informationArea = informationArea;
+    }
+    @Override
+    public void onLogActionComplete(boolean status, String information) {
+      String currentText = informationArea.getText();
+      informationArea
+          .setText(new StringBuilder().append(information).append(
+              "\n").append(currentText).toString());
+    }
+  }
+}
diff --git a/frontend/client/src/autotest/moblab/MoblabSetupClient.java b/frontend/client/src/autotest/moblab/MoblabSetupClient.java
index e2a4fc9..a0138d3 100644
--- a/frontend/client/src/autotest/moblab/MoblabSetupClient.java
+++ b/frontend/client/src/autotest/moblab/MoblabSetupClient.java
@@ -13,6 +13,7 @@
   private ConfigSettingsView configSettingsView;
   private BotoKeyView botoKeyView;
   private LaunchControlKeyView launchControlKeyView;
+  private DutManagementView dutManagementView;
 
   public CustomTabPanel mainTabPanel = new CustomTabPanel();
 
@@ -28,10 +29,12 @@
     configSettingsView = new ConfigSettingsView();
     botoKeyView = new BotoKeyView();
     launchControlKeyView = new LaunchControlKeyView();
+    dutManagementView = new DutManagementView();
     mainTabPanel.addTabView(configWizardView);
     mainTabPanel.addTabView(configSettingsView);
     mainTabPanel.addTabView(botoKeyView);
     mainTabPanel.addTabView(launchControlKeyView);
+    mainTabPanel.addTabView(dutManagementView);
 
     final RootPanel rootPanel = RootPanel.get("tabs");
     rootPanel.add(mainTabPanel);
diff --git a/frontend/client/src/autotest/moblab/rpc/ConnectedDutInfo.java b/frontend/client/src/autotest/moblab/rpc/ConnectedDutInfo.java
new file mode 100644
index 0000000..fc670f2
--- /dev/null
+++ b/frontend/client/src/autotest/moblab/rpc/ConnectedDutInfo.java
@@ -0,0 +1,44 @@
+package autotest.moblab.rpc;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * The connected DUT information RPC entity.
+ */
+public class ConnectedDutInfo extends JsonRpcEntity {
+
+  private Map<String, String> connectedIpsToMacAddresses;
+  private Map<String, String> configuredIpsToLabels;
+
+  public ConnectedDutInfo() {
+    connectedIpsToMacAddresses = new TreeMap<String, String>();
+    configuredIpsToLabels = new TreeMap<String, String>();
+  }
+
+  public Map<String, String> getConnectedIpsToMacAddress() { return connectedIpsToMacAddresses; }
+  public Map<String, String> getConfiguredIpsToLabels() { return configuredIpsToLabels; }
+
+  @Override
+  public void fromJson(JSONObject object) {
+    JSONObject leases = object.get("connected_duts").isObject();
+    for (String lease : leases.keySet()) {
+      connectedIpsToMacAddresses.put(lease, leases.get(lease).isString().stringValue());
+    }
+    JSONObject configuredDuts = object.get("configured_duts").isObject();
+    for (String ipAddress : configuredDuts.keySet()) {
+      configuredIpsToLabels.put(ipAddress, configuredDuts.get(ipAddress).isString().stringValue());
+    }
+  }
+
+  @Override
+  public JSONObject toJson() {
+    // This is a read only RPC call so nothing to be submitted back to the
+    // server from the UI.
+    return null;
+  }
+}
diff --git a/frontend/client/src/autotest/moblab/rpc/MoblabRpcCallbacks.java b/frontend/client/src/autotest/moblab/rpc/MoblabRpcCallbacks.java
index 8429b6f..b439b9d 100644
--- a/frontend/client/src/autotest/moblab/rpc/MoblabRpcCallbacks.java
+++ b/frontend/client/src/autotest/moblab/rpc/MoblabRpcCallbacks.java
@@ -41,4 +41,18 @@
     public void onVersionInfoFetched(VersionInfo info);
   }
 
+  /**
+   * Callback for to get information about the connected DUT's.
+   */
+  public interface FetchConnectedDutInfoCallback {
+    public void onFetchConnectedDutInfoSubmitted(ConnectedDutInfo info);
+  }
+
+  /**
+   * Generic callback to return a status and information string from a RPC call.
+   */
+  public interface LogActionCompleteCallback {
+    public void onLogActionComplete(boolean didSucceed, String information);
+  }
+
 }
diff --git a/frontend/client/src/autotest/moblab/rpc/MoblabRpcHelper.java b/frontend/client/src/autotest/moblab/rpc/MoblabRpcHelper.java
index 26d50c2..12795a6 100644
--- a/frontend/client/src/autotest/moblab/rpc/MoblabRpcHelper.java
+++ b/frontend/client/src/autotest/moblab/rpc/MoblabRpcHelper.java
@@ -1,12 +1,15 @@
 package autotest.moblab.rpc;
 
+import com.google.gwt.json.client.JSONNumber;
 import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
 import com.google.gwt.json.client.JSONValue;
 
 import autotest.common.JsonRpcCallback;
 import autotest.common.JsonRpcProxy;
 import autotest.common.SimpleCallback;
 
+import com.google.gwt.user.client.Window;
 import java.util.Map;
 
 /**
@@ -143,11 +146,111 @@
       final MoblabRpcCallbacks.FetchVersionInfoCallback callback) {
     JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
     rpcProxy.rpcCall("get_version_info", null, new JsonRpcCallback() {
+          @Override
+          public void onSuccess(JSONValue result) {
+            VersionInfo info = new VersionInfo();
+            info.fromJson(result.isObject());
+            callback.onVersionInfoFetched(info);
+          }
+        });
+  }
+
+   /**
+   * Get information about the DUT's connected to the moblab.
+   */
+  public static void fetchDutInformation(
+      final MoblabRpcCallbacks.FetchConnectedDutInfoCallback callback) {
+    JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+    rpcProxy.rpcCall("get_connected_dut_info", null, new JsonRpcCallback() {
       @Override
       public void onSuccess(JSONValue result) {
-        VersionInfo info = new VersionInfo();
+        ConnectedDutInfo info = new ConnectedDutInfo();
         info.fromJson(result.isObject());
-        callback.onVersionInfoFetched(info);
+        callback.onFetchConnectedDutInfoSubmitted(info);
+      }
+    });
+  }
+
+  /**
+   * Enroll a device into the autotest syste.
+   * @param dutIpAddress ipAddress of the new DUT.
+   * @param callback Callback execute when the RPC is complete.
+   */
+  public static void addMoblabDut(String dutIpAddress,
+      final MoblabRpcCallbacks.LogActionCompleteCallback callback) {
+    JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+    JSONObject params = new JSONObject();
+    params.put("ipaddress", new JSONString(dutIpAddress));
+    rpcProxy.rpcCall("add_moblab_dut", params, new JsonRpcCallback() {
+      @Override
+      public void onSuccess(JSONValue result) {
+        boolean didSucceed = result.isArray().get(0).isBoolean().booleanValue();
+        String information = result.isArray().get(1).isString().stringValue();
+        callback.onLogActionComplete(didSucceed, information);
+      }
+    });
+  }
+
+  /**
+   * Remove a device from the autotest system.
+   * @param dutIpAddress ipAddress of the DUT to remove.
+   * @param callback Callback to execute when the RPC is complete.
+   */
+  public static void removeMoblabDut(String dutIpAddress,
+      final MoblabRpcCallbacks.LogActionCompleteCallback callback) {
+    JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+    JSONObject params = new JSONObject();
+    params.put("ipaddress", new JSONString(dutIpAddress));
+    rpcProxy.rpcCall("remove_moblab_dut", params, new JsonRpcCallback() {
+      @Override
+      public void onSuccess(JSONValue result) {
+        boolean didSucceed = result.isArray().get(0).isBoolean().booleanValue();
+        String information = result.isArray().get(1).isString().stringValue();
+        callback.onLogActionComplete(didSucceed, information);
+      }
+    });
+  }
+
+  /**
+   * Add a label to a specific DUT.
+   * @param dutIpAddress  ipAddress of the device to have the new label applied.
+   * @param labelName the label to apply
+   * @param callback callback to execute when the RPC is complete.
+   */
+  public static void addMoblabLabel(String dutIpAddress, String labelName,
+      final MoblabRpcCallbacks.LogActionCompleteCallback callback) {
+    JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+    JSONObject params = new JSONObject();
+    params.put("ipaddress", new JSONString(dutIpAddress));
+    params.put("label_name", new JSONString(labelName));
+    rpcProxy.rpcCall("add_moblab_label", params, new JsonRpcCallback() {
+      @Override
+      public void onSuccess(JSONValue result) {
+        boolean didSucceed = result.isArray().get(0).isBoolean().booleanValue();
+        String information = result.isArray().get(1).isString().stringValue();
+        callback.onLogActionComplete(didSucceed, information);
+      }
+    });
+  }
+
+  /**
+   * Remove a label from a specific DUT.
+   * @param dutIpAddress ipAddress of the device to have the label removed.
+   * @param labelName the label to remove
+   * @param callback callback to execute when the RPC is complete.
+   */
+  public static void removeMoblabLabel(String dutIpAddress, String labelName,
+      final MoblabRpcCallbacks.LogActionCompleteCallback callback) {
+    JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+    JSONObject params = new JSONObject();
+    params.put("ipaddress", new JSONString(dutIpAddress));
+    params.put("label_name", new JSONString(labelName));
+    rpcProxy.rpcCall("remove_moblab_label", params, new JsonRpcCallback() {
+      @Override
+      public void onSuccess(JSONValue result) {
+        boolean didSucceed = result.isArray().get(0).isBoolean().booleanValue();
+        String information = result.isArray().get(1).isString().stringValue();
+        callback.onLogActionComplete(didSucceed, information);
       }
     });
   }
diff --git a/frontend/client/src/autotest/public/MoblabSetupClient.html b/frontend/client/src/autotest/public/MoblabSetupClient.html
index 48bab03..14a8c8b 100644
--- a/frontend/client/src/autotest/public/MoblabSetupClient.html
+++ b/frontend/client/src/autotest/public/MoblabSetupClient.html
@@ -34,6 +34,10 @@
         <span id="view_submit_launch_control_key"></span>
       </div>
 
+      <div id="dut_manage" title="Manage DUTs">
+        <span id="view_dut_manage"></span><br>
+      </div>
+
     </div>
 
     <br>
diff --git a/frontend/client/src/autotest/public/moblabsetup.css b/frontend/client/src/autotest/public/moblabsetup.css
new file mode 100644
index 0000000..ed80b74
--- /dev/null
+++ b/frontend/client/src/autotest/public/moblabsetup.css
@@ -0,0 +1,58 @@
+
+.dut_info_table {
+  padding: 5px;
+  background-color:  white;
+  border-spacing: 0;
+  border-collapse: collapse;
+  border-bottom: thin solid black;
+}
+
+.dut_manage_column_label_c {
+  color: black;
+  border-bottom: thin solid #111111;
+  text-align: center;
+  padding-left: 15px;
+  padding-right: 15px;
+  padding-bottom: 10px;
+}
+
+.dut_manage_column_label_l {
+  color: black;
+  border-bottom: thin solid #111111;
+  text-align: left;
+  padding-left: 5px;
+}
+
+.ip_cell,
+.mac_cell,
+.selection_cell,
+.labels_cell {
+  border-width: 0px 0px 0px 0px;
+  border-style: solid;
+  border-color: black;
+}
+
+.ip_cell,
+.mac_cell,
+.selection_cell {
+  text-align:center;
+}
+
+.labels_cell {
+  text-align:left;
+}
+
+
+.dut_manage_action_row {
+  padding: 5px;
+  text-align: center;
+}
+
+.dut_manage_action_row {
+  padding: 5px;
+  text-align: center;
+}
+
+.dut_manage_action_row_item {
+  padding-left: 20px;
+}