[MobLab] Moblab Setup page.

* Adds a new page located at /moblab_setup
* 2 are on this page: 1 to upload a Boto Key and 1 to edit the config values.
* Link to /moblab_setup is visible only on a moblab system.
* RPC's that execute moblab_setup's actions are gated by a decorator that
  limits them to only run on a moblab_system.
* Unittests for new RPC's.
* Editting the config values, writes the full config to shadow_config.ini and
  reboots the system so changes takes effort.
* Resetting the config values, makes shadow_config.ini an empty file and
  reboots the system so it is restored.
* Uploading the boto key uses shutil.copyfile to write in the new boto file's
  contents to the boto file location.

BUG=chromium:396694
TEST=unittests, Uploaded a boto key and successfully ran a suite, editted
config and ensured changes were written into shadow_config, & reset config
and ensured that the default settings were restored.
DEPLOY=afe,apache
CQ-DEPEND=CL:212322
CQ-DEPEND=CL:212323
CQ-DEPEND=CL:212295

Change-Id: Ie354a2df310393045f3116e93004f58ea671de36
Reviewed-on: https://chromium-review.googlesource.com/209685
Reviewed-by: Simran Basi <sbasi@chromium.org>
Commit-Queue: Simran Basi <sbasi@chromium.org>
Tested-by: Simran Basi <sbasi@chromium.org>
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index a8dcd6a..cfd861d 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -38,6 +38,7 @@
 from autotest_lib.frontend.afe import control_file, rpc_utils
 from autotest_lib.frontend.afe import site_rpc_interface
 from autotest_lib.frontend.tko import rpc_interface as tko_rpc_interface
+from autotest_lib.server import utils
 from autotest_lib.server.cros.dynamic_suite import tools
 
 def get_parameterized_autoupdate_image_url(job):
@@ -1019,6 +1020,7 @@
                                    "Resetting": "Resetting hosts"}
 
     result['wmatrix_url'] = rpc_utils.get_wmatrix_url()
+    result['is_moblab'] = bool(utils.is_moblab())
 
     return result
 
diff --git a/frontend/afe/site_rpc_interface.py b/frontend/afe/site_rpc_interface.py
index 1c90778..cb6a4f7 100644
--- a/frontend/afe/site_rpc_interface.py
+++ b/frontend/afe/site_rpc_interface.py
@@ -9,8 +9,12 @@
 import common
 import datetime
 import logging
+import os
+import shutil
+import utils
 
 from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib import global_config
 from autotest_lib.client.common_lib import priorities
 from autotest_lib.client.common_lib.cros import dev_server
 from autotest_lib.server import utils
@@ -18,6 +22,11 @@
 from autotest_lib.server.cros.dynamic_suite import control_file_getter
 from autotest_lib.server.cros.dynamic_suite import job_status
 from autotest_lib.server.cros.dynamic_suite import tools
+from autotest_lib.server.hosts import moblab_host
+
+
+_CONFIG = global_config.global_config
+MOBLAB_BOTO_LOCATION = '/home/moblab/.boto'
 
 
 # Relevant CrosDynamicSuiteExceptions are defined in client/common_lib/error.py.
@@ -191,3 +200,66 @@
                                           control_file=control_file,
                                           hostless=True,
                                           keyvals=timings)
+
+
+# TODO: hide the following rpcs under is_moblab
+def moblab_only(func):
+    """Ensure moblab specific functions only run on Moblab devices."""
+    def verify(*args, **kwargs):
+        if not utils.is_moblab():
+            raise error.RPCException('RPC: %s can only run on Moblab Systems!',
+                                     func.__name__)
+        return func(*args, **kwargs)
+    return verify
+
+
+@moblab_only
+def get_config_values():
+    """Returns all config values parsed from global and shadow configs.
+
+    Config values are grouped by sections, and each section is composed of
+    a list of name value pairs.
+    """
+    sections =_CONFIG.get_sections()
+    config_values = {}
+    for section in sections:
+        config_values[section] = _CONFIG.config.items(section)
+    return _rpc_utils().prepare_for_serialization(config_values)
+
+
+@moblab_only
+def update_config_handler(config_values):
+    """
+    Update config values and override shadow config.
+
+    @param config_values: See get_moblab_settings().
+    """
+    for section, config_value_list in config_values.iteritems():
+        for key, value in config_value_list:
+            _CONFIG.override_config_value(section, key, value)
+    if not _CONFIG.shadow_file or not os.path.exists(_CONFIG.shadow_file):
+        raise error.RPCException('Shadow config file does not exist.')
+
+    with open(_CONFIG.shadow_file, 'w') as config_file:
+        _CONFIG.config.write(config_file)
+    # TODO (sbasi) crbug.com/403916 - Remove the reboot command and
+    # instead restart the services that rely on the config values.
+    os.system('sudo reboot')
+
+
+@moblab_only
+def reset_config_settings():
+    with open(_CONFIG.shadow_file, 'w') as config_file:
+      pass
+    os.system('sudo reboot')
+
+
+@moblab_only
+def set_boto_key(boto_key):
+    """Update the boto_key file.
+
+    @param boto_key: File name of boto_key uploaded through handle_file_upload.
+    """
+    if not os.path.exists(boto_key):
+        raise error.RPCException('Boto key: %s does not exist!' % boto_key)
+    shutil.copyfile(boto_key, moblab_host.MOBLAB_BOTO_LOCATION)
diff --git a/frontend/afe/site_rpc_interface_unittest.py b/frontend/afe/site_rpc_interface_unittest.py
index 085ae7f..73f56ef 100644
--- a/frontend/afe/site_rpc_interface_unittest.py
+++ b/frontend/afe/site_rpc_interface_unittest.py
@@ -7,15 +7,20 @@
 """Unit tests for frontend/afe/site_rpc_interface.py."""
 
 
+import __builtin__
+import ConfigParser
 import mox
+import StringIO
 import unittest
 
 import common
 
 from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib import global_config
 from autotest_lib.client.common_lib import priorities
 from autotest_lib.client.common_lib.cros import dev_server
 from autotest_lib.frontend.afe import site_rpc_interface
+from autotest_lib.server import utils
 from autotest_lib.server.cros.dynamic_suite import control_file_getter
 from autotest_lib.server.cros.dynamic_suite import constants
 
@@ -267,6 +272,113 @@
             job_id)
 
 
+    def setIsMoblab(self, is_moblab):
+        """Set utils.is_moblab result.
+
+        @param is_moblab: Value to have utils.is_moblab to return.
+        """
+        self.mox.StubOutWithMock(utils, 'is_moblab')
+        utils.is_moblab().AndReturn(is_moblab)
+
+
+    def testMoblabOnlyDecorator(self):
+        """Ensure the moblab only decorator gates functions properly."""
+        self.setIsMoblab(False)
+        self.mox.ReplayAll()
+        self.assertRaises(error.RPCException,
+                          site_rpc_interface.get_config_values)
+
+
+    def testGetConfigValues(self):
+        """Ensure that the config object is properly converted to a dict."""
+        self.setIsMoblab(True)
+        config_mock = self.mox.CreateMockAnything()
+        site_rpc_interface._CONFIG = config_mock
+        config_mock.get_sections().AndReturn(['section1', 'section2'])
+        config_mock.config = self.mox.CreateMockAnything()
+        config_mock.config.items('section1').AndReturn([('item1', 'value1'),
+                                                        ('item2', 'value2')])
+        config_mock.config.items('section2').AndReturn([('item3', 'value3'),
+                                                        ('item4', 'value4')])
+
+        r = self.mox.CreateMock(SiteRpcInterfaceTest.rpc_utils)
+        r = mox.MockAnything()
+        r.prepare_for_serialization({'section1' : [('item1', 'value1'),
+                                                   ('item2', 'value2')],
+                                     'section2' : [('item3', 'value3'),
+                                                   ('item4', 'value4')]})
+        self.mox.StubOutWithMock(site_rpc_interface, '_rpc_utils')
+        site_rpc_interface._rpc_utils().AndReturn(r)
+        self.mox.ReplayAll()
+        site_rpc_interface.get_config_values()
+
+
+    def testUpdateConfig(self):
+        """Ensure that updating the config works as expected."""
+        self.setIsMoblab(True)
+        # Reset the config.
+        site_rpc_interface._CONFIG = global_config.global_config
+        site_rpc_interface._CONFIG.shadow_file = 'fake_shadow'
+        site_rpc_interface._CONFIG.config = ConfigParser.ConfigParser()
+        site_rpc_interface._CONFIG.config.add_section('section1')
+        site_rpc_interface._CONFIG.config.add_section('section2')
+        site_rpc_interface.os = self.mox.CreateMockAnything()
+        site_rpc_interface.os.path = self.mox.CreateMockAnything()
+        site_rpc_interface.os.path.exists(
+                site_rpc_interface._CONFIG.shadow_file).AndReturn(
+                True)
+
+        self.mox.StubOutWithMock(__builtin__, 'open')
+        mockFile = self.mox.CreateMockAnything()
+        file_contents = StringIO.StringIO()
+        mockFile.__enter__().AndReturn(file_contents)
+        mockFile.__exit__(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg())
+        open(site_rpc_interface._CONFIG.shadow_file, 'w').AndReturn(mockFile)
+
+        site_rpc_interface.os.system('sudo reboot')
+        self.mox.ReplayAll()
+        site_rpc_interface.update_config_handler(
+                {'section1' : [('item1', 'value1'),
+                               ('item2', 'value2')],
+                 'section2' : [('item3', 'value3'),
+                               ('item4', 'value4')]})
+        self.assertEquals(
+                file_contents.getvalue(),
+                '[section1]\nitem1 = value1\nitem2 = value2\n\n'
+                '[section2]\nitem3 = value3\nitem4 = value4\n\n')
+
+
+    def testResetConfig(self):
+        """Ensure that reset opens the shadow_config file for writing."""
+        self.setIsMoblab(True)
+        config_mock = self.mox.CreateMockAnything()
+        site_rpc_interface._CONFIG = config_mock
+        config_mock.shadow_file = 'shadow_config.ini'
+        self.mox.StubOutWithMock(__builtin__, 'open')
+        mockFile = self.mox.CreateMockAnything()
+        file_contents = self.mox.CreateMockAnything()
+        mockFile.__enter__().AndReturn(file_contents)
+        mockFile.__exit__(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg())
+        open(config_mock.shadow_file, 'w').AndReturn(mockFile)
+        site_rpc_interface.os = self.mox.CreateMockAnything()
+        site_rpc_interface.os.system('sudo reboot')
+        self.mox.ReplayAll()
+        site_rpc_interface.reset_config_settings()
+
+
+    def testSetBotoKey(self):
+        """Ensure that the botokey path supplied is copied correctly."""
+        self.setIsMoblab(True)
+        boto_key = '/tmp/boto'
+        site_rpc_interface.os.path = self.mox.CreateMockAnything()
+        site_rpc_interface.os.path.exists(boto_key).AndReturn(
+                True)
+        site_rpc_interface.shutil = self.mox.CreateMockAnything()
+        site_rpc_interface.shutil.copyfile(
+                boto_key, site_rpc_interface.MOBLAB_BOTO_LOCATION)
+        self.mox.ReplayAll()
+        site_rpc_interface.set_boto_key(boto_key)
+
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/frontend/afe/urls.py b/frontend/afe/urls.py
index 55fe0f0..c5e4228 100644
--- a/frontend/afe/urls.py
+++ b/frontend/afe/urls.py
@@ -62,6 +62,10 @@
 urlpatterns += defaults.patterns(
         '', (r'^resources/', defaults.include(resource_patterns)))
 
+# File upload
+urlpatterns += defaults.patterns(
+        '', (r'^upload/', 'frontend.afe.views.handle_file_upload'))
+
 # Job feeds
 debug_patterns += defaults.patterns(
         '',
diff --git a/frontend/afe/views.py b/frontend/afe/views.py
index f304bcb..44feef0 100644
--- a/frontend/afe/views.py
+++ b/frontend/afe/views.py
@@ -1,4 +1,4 @@
-import httplib2, sys, traceback, cgi
+import httplib2, os, sys, traceback, cgi
 
 from django.http import HttpResponse, HttpResponsePermanentRedirect
 from django.http import HttpResponseServerError
@@ -62,3 +62,25 @@
         'traceback': cgi.escape(trace)
     })
     return HttpResponseServerError(t.render(context))
+
+
+def handle_file_upload(request):
+    """Handler for uploading files.
+
+    Saves the files to /tmp and returns the resulting paths on disk.
+
+    @param request: request containing the file data.
+
+    @returns HttpResponse: with the paths of the saved files.
+    """
+    if request.method == 'POST':
+        TEMPT_DIR = '/tmp/'
+        file_paths = []
+        for file_name, upload_file in request.FILES.iteritems():
+            file_path = os.path.join(
+                    TEMPT_DIR, '_'.join([file_name, upload_file.name]))
+            with open(file_path, 'wb+') as destination:
+                for chunk in upload_file.chunks():
+                    destination.write(chunk)
+            file_paths.append(file_path)
+        return HttpResponse(rpc_utils.prepare_for_serialization(file_paths))
diff --git a/frontend/client/src/autotest/MoblabSetupClient.gwt.xml b/frontend/client/src/autotest/MoblabSetupClient.gwt.xml
new file mode 100644
index 0000000..97b2859
--- /dev/null
+++ b/frontend/client/src/autotest/MoblabSetupClient.gwt.xml
@@ -0,0 +1,13 @@
+<module>
+    <inherits name='com.google.gwt.user.User'/>
+    <inherits name='com.google.gwt.json.JSON'/>
+    <inherits name='com.google.gwt.http.HTTP'/>
+
+    <source path="moblab"/>
+    <source path="common"/>
+    <entry-point class='autotest.moblab.MoblabSetupClient'/>
+
+    <stylesheet src='common.css'/>
+    <stylesheet src='afeclient.css'/>
+    <stylesheet src='standard.css'/>
+</module>
diff --git a/frontend/client/src/autotest/afe/AfeClient.java b/frontend/client/src/autotest/afe/AfeClient.java
index 48455e2..df7b77a 100644
--- a/frontend/client/src/autotest/afe/AfeClient.java
+++ b/frontend/client/src/autotest/afe/AfeClient.java
@@ -65,6 +65,11 @@
                 "href", wmatrixUrl);
             Document.get().getElementById("wmatrix").removeClassName("hidden");
         }
+        boolean is_moblab = StaticDataRepository.getRepository().getData(
+            "is_moblab").isBoolean().booleanValue();
+        if (is_moblab) {
+            Document.get().getElementById("moblab_setup").removeClassName("hidden");
+        }
 
         jobList = new JobListView(new JobSelectListener() {
             public void onJobSelected(int jobId) {
diff --git a/frontend/client/src/autotest/moblab/BotoKeyView.java b/frontend/client/src/autotest/moblab/BotoKeyView.java
new file mode 100644
index 0000000..d8f6c48
--- /dev/null
+++ b/frontend/client/src/autotest/moblab/BotoKeyView.java
@@ -0,0 +1,75 @@
+package autotest.moblab;
+
+import autotest.common.JsonRpcCallback;
+import autotest.common.JsonRpcProxy;
+import autotest.common.SimpleCallback;
+import autotest.common.ui.TabView;
+import autotest.common.ui.NotifyManager;
+
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FileUpload;
+import com.google.gwt.user.client.ui.FormPanel;
+import com.google.gwt.user.client.ui.FormPanel.SubmitCompleteEvent;
+import com.google.gwt.user.client.ui.FormPanel.SubmitCompleteHandler;
+import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
+import com.google.gwt.user.client.ui.FormPanel.SubmitHandler;
+
+
+public class BotoKeyView extends TabView {
+    private FileUpload botoKeyUpload;
+    private Button submitButton;
+    private FormPanel botoKeyUploadForm;
+
+    @Override
+    public String getElementId() {
+        return "boto_key";
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+
+        botoKeyUpload = new FileUpload();
+        botoKeyUpload.setName("botokey");
+
+        botoKeyUploadForm = new FormPanel();
+        botoKeyUploadForm.setAction(JsonRpcProxy.AFE_BASE_URL + "upload/");
+        botoKeyUploadForm.setEncoding(FormPanel.ENCODING_MULTIPART);
+        botoKeyUploadForm.setMethod(FormPanel.METHOD_POST);
+        botoKeyUploadForm.setWidget(botoKeyUpload);
+
+        submitButton = new Button("Submit", new ClickHandler() {
+            public void onClick(ClickEvent event) {
+                botoKeyUploadForm.submit();
+            }
+        });
+
+        botoKeyUploadForm.addSubmitCompleteHandler(new SubmitCompleteHandler() {
+            public void onSubmitComplete(SubmitCompleteEvent event) {
+                String fileName = event.getResults();
+                JSONObject params = new JSONObject();
+                params.put("boto_key", new JSONString(fileName));
+                rpcCall(params);
+            }
+        });
+
+        addWidget(botoKeyUploadForm, "view_boto_key");
+        addWidget(submitButton, "view_submit_boto_key");
+    }
+
+    public void rpcCall(JSONObject params) {
+        JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+        rpcProxy.rpcCall("set_boto_key", params, new JsonRpcCallback() {
+            @Override
+            public void onSuccess(JSONValue result) {
+                NotifyManager.getInstance().showMessage("Boto key uploaded.");
+            }
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/frontend/client/src/autotest/moblab/ConfigSettingsView.java b/frontend/client/src/autotest/moblab/ConfigSettingsView.java
new file mode 100644
index 0000000..06cb540
--- /dev/null
+++ b/frontend/client/src/autotest/moblab/ConfigSettingsView.java
@@ -0,0 +1,186 @@
+package autotest.moblab;
+
+import autotest.common.JsonRpcCallback;
+import autotest.common.JsonRpcProxy;
+import autotest.common.SimpleCallback;
+import autotest.common.ui.TabView;
+import autotest.common.ui.NotifyManager;
+
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+public class ConfigSettingsView extends TabView {
+    private Button submitButton;
+    private Button resetButton;
+    private HashMap<String, HashMap<String, TextBox> > configValueTextBoxes;
+    private FlexTable configValueTable;
+    private PopupPanel resetConfirmPanel;
+    private Button resetConfirmButton;
+    private PopupPanel submitConfirmPanel;
+    private Button submitConfirmButton;
+
+    @Override
+    public void refresh() {
+        super.refresh();
+        configValueTable.removeAllRows();
+        fetchConfigData(new SimpleCallback() {
+            public void doCallback(Object source) {
+                loadData((JSONValue) source);
+            }
+        });
+        resetConfirmPanel.hide();
+    }
+
+    @Override
+    public String getElementId() {
+        return "config_settings";
+    }
+
+    private PopupPanel getAlertPanel(String alertMessage, Button confirmButton){
+        PopupPanel alertPanel = new PopupPanel(true);
+        VerticalPanel alertInnerPanel = new VerticalPanel();
+        alertInnerPanel.add(new Label(alertMessage));
+        alertInnerPanel.add(confirmButton);
+        alertPanel.setWidget(alertInnerPanel);
+        return alertPanel;
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        configValueTable = new FlexTable();
+
+        resetConfirmButton = new Button("Confirm Reset", new ClickHandler() {
+            public void onClick(ClickEvent event) {
+                rpcCallReset();
+                resetConfirmPanel.hide();
+            }
+        });
+
+        resetConfirmPanel =getAlertPanel(
+                "Restoring Default Settings requires rebooting the MobLab. Are you sure?",
+                resetConfirmButton);
+
+        submitConfirmButton = new Button("Confirm Save", new ClickHandler() {
+            public void onClick(ClickEvent event) {
+                JSONObject params = new JSONObject();
+                JSONObject configValues = new JSONObject();
+                for (Entry<String, HashMap<String, TextBox> > sections : configValueTextBoxes.entrySet()) {
+                    JSONArray sectionValue = new JSONArray();
+                    for (Entry<String, TextBox> configValue : sections.getValue().entrySet()) {
+                        JSONArray configValuePair = new JSONArray();
+                        configValuePair.set(0, new JSONString(configValue.getKey()));
+                        configValuePair.set(1, new JSONString(configValue.getValue().getText()));
+                        sectionValue.set(sectionValue.size(), configValuePair);
+                    }
+                    configValues.put(sections.getKey(), sectionValue);
+                }
+                params.put("config_values", configValues);
+                rpcCallSubmit(params);
+                submitConfirmPanel.hide();
+            }
+        });
+
+        submitConfirmPanel = getAlertPanel(
+                "Saving settings requires rebooting the MobLab. Are you sure?",
+                submitConfirmButton);
+
+        submitButton = new Button("Submit", new ClickHandler() {
+            public void onClick(ClickEvent event) {
+                submitConfirmPanel.center();
+            }
+        });
+
+        resetButton = new Button("Restore Defaults", new ClickHandler() {
+            public void onClick(ClickEvent event) {
+                resetConfirmPanel.center();
+            }
+        });
+
+        addWidget(configValueTable, "view_config_values");
+        addWidget(submitButton, "view_submit");
+        addWidget(resetButton, "view_reset");
+    }
+
+    private void fetchConfigData(final SimpleCallback callBack) {
+        JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+        rpcProxy.rpcCall("get_config_values", null, new JsonRpcCallback() {
+            @Override
+            public void onSuccess(JSONValue result) {
+                if (callBack != null)
+                    callBack.doCallback(result);
+            }
+        });
+    }
+
+    private void loadData(JSONValue result) {
+        configValueTextBoxes = new HashMap<String, HashMap<String, TextBox> >();
+        JSONObject resultObject = result.isObject();
+        for (String section : resultObject.keySet()) {
+            JSONArray sectionArray = resultObject.get(section).isArray();
+            HashMap<String, TextBox> sectionKeyValues = new HashMap<String, TextBox>();
+
+            Label sectionLabel = new Label(section);
+            sectionLabel.addStyleName("field-name");
+            configValueTable.setWidget(configValueTable.getRowCount(), 0, sectionLabel);
+
+            for (int i = 0; i < sectionArray.size(); i++) {
+                JSONArray configPair = sectionArray.get(i).isArray();
+                String configKey = configPair.get(0).isString().stringValue();
+                String configValue = configPair.get(1).isString().stringValue();
+
+                TextBox configInput = new TextBox();
+                configInput.setVisibleLength(100);
+
+                int row = configValueTable.getRowCount();
+                configValueTable.setWidget(row, 0, new Label(configKey));
+                configValueTable.setWidget(row, 1, configInput);
+                configInput.setText(configValue);
+                sectionKeyValues.put(configKey, configInput);
+            }
+
+            if (sectionArray.size() == 0) {
+                configValueTable.setText(configValueTable.getRowCount(), 0,
+                                         "No config values in this section.");
+            }
+
+            configValueTextBoxes.put(section, sectionKeyValues);
+            // Add an empty row after each section.
+            configValueTable.setText(configValueTable.getRowCount(), 0, "");
+        }
+    }
+
+    public void rpcCallSubmit(JSONObject params) {
+        JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+        rpcProxy.rpcCall("update_config_handler", params, new JsonRpcCallback() {
+            @Override
+            public void onSuccess(JSONValue result) {
+                NotifyManager.getInstance().showMessage("Setup completed.");
+            }
+        });
+    }
+
+    public void rpcCallReset() {
+        JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+        rpcProxy.rpcCall("reset_config_settings", null, new JsonRpcCallback() {
+            @Override
+            public void onSuccess(JSONValue result) {
+                NotifyManager.getInstance().showMessage("Reset completed.");
+            }
+        });
+    }
+
+}
diff --git a/frontend/client/src/autotest/moblab/MoblabSetupClient.java b/frontend/client/src/autotest/moblab/MoblabSetupClient.java
new file mode 100644
index 0000000..3b07b15
--- /dev/null
+++ b/frontend/client/src/autotest/moblab/MoblabSetupClient.java
@@ -0,0 +1,36 @@
+package autotest.moblab;
+
+import autotest.common.JsonRpcProxy;
+import autotest.common.Utils;
+import autotest.common.ui.CustomTabPanel;
+import autotest.common.ui.NotifyManager;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.user.client.ui.RootPanel;
+
+
+public class MoblabSetupClient implements EntryPoint {
+    private ConfigSettingsView configSettingsView;
+    private BotoKeyView botoKeyView;
+
+    public CustomTabPanel mainTabPanel = new CustomTabPanel();
+
+    /**
+     * Application entry point.
+     */
+    public void onModuleLoad() {
+        JsonRpcProxy.setDefaultBaseUrl(JsonRpcProxy.AFE_BASE_URL);
+        NotifyManager.getInstance().initialize();
+
+        configSettingsView = new ConfigSettingsView();
+        botoKeyView = new BotoKeyView();
+        mainTabPanel.addTabView(configSettingsView);
+        mainTabPanel.addTabView(botoKeyView);
+
+        final RootPanel rootPanel = RootPanel.get("tabs");
+        rootPanel.add(mainTabPanel);
+        mainTabPanel.initialize();
+        rootPanel.setStyleName("");
+    }
+
+}
\ No newline at end of file
diff --git a/frontend/client/src/autotest/public/AfeClient.html b/frontend/client/src/autotest/public/AfeClient.html
index 5bca203..f278da4 100644
--- a/frontend/client/src/autotest/public/AfeClient.html
+++ b/frontend/client/src/autotest/public/AfeClient.html
@@ -22,6 +22,9 @@
         <span id="wmatrix" class="hidden">
          | <a id="wmatrix-link">WMatrix</a>
         </span>
+        <span id="moblab_setup" class="hidden">
+         | <a href="/moblab_setup">Moblab Setup</a>
+        </span>
         <div id="motd" class="motd"></div>
       </span>
       <img alt="Autotest" src="header.png" class="logo" />
diff --git a/frontend/client/src/autotest/public/MoblabSetupClient.html b/frontend/client/src/autotest/public/MoblabSetupClient.html
new file mode 100644
index 0000000..7f1599b
--- /dev/null
+++ b/frontend/client/src/autotest/public/MoblabSetupClient.html
@@ -0,0 +1,33 @@
+<html>
+  <head>
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+    <title>MobLab Setup</title>
+    <script type="text/javascript" language='javascript'
+      src='autotest.MoblabSetupClient.nocache.js'>
+    </script>
+  </head>
+
+  <body>
+    <!-- gwt history support -->
+    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1'
+      style="position:absolute;width:0;height:0;border:0"></iframe>
+
+    <h1>MobLab Setup</h1>
+    <div id="tabs">
+      <div id="config_settings" title="Config Settings">
+        <span id="view_config_values"></span><br>
+        <span id="view_submit"></span>
+        <span id="view_reset"></span>
+      </div>
+
+      <div id="boto_key" title="Boto Key">
+        <span class="field-name">Boto Key: </span>
+        <span id="view_boto_key"></span>
+        <span id="view_submit_boto_key"></span>
+      </div>
+    </div>
+
+    <br>
+    <div id="error_log"></div>
+  </body>
+</html>
\ No newline at end of file