[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