[autotest] Add more build option in autotest create_job tab for FAFT

Add an Advanced Build Options panel for user to specify:
firmware build (RW), firmware RO build, and test source build.

Then create a suite job from AFE to update both CrOS and firmware.

BUG=chromium:493429
TEST=local test
http://dshi.mtv/afe/#tab_id=create_job
1. check AFE shows the Advanced Build Options panel
2. Confirm the builds specified in the options are used in the sutie job run.
3. Confirm non-suite job can't be created with firmware image specified.
sample cros build: veyron_jerry-release/R45-7201.0.0
sample firmware build: veyron_jerry-firmware/R41-6588.106.0
DEPLOY=afe,apache

Change-Id: I81e1f5f81aaa9dfb5fe7b9f2f3677b8fb437130c
Reviewed-on: https://chromium-review.googlesource.com/281021
Trybot-Ready: Dan Shi <dshi@chromium.org>
Tested-by: Dan Shi <dshi@chromium.org>
Reviewed-by: Simran Basi <sbasi@chromium.org>
Commit-Queue: Dan Shi <dshi@chromium.org>
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index 64e8ca7..1a19802 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -45,6 +45,7 @@
 from autotest_lib.frontend.tko import rpc_interface as tko_rpc_interface
 from autotest_lib.server import frontend
 from autotest_lib.server import utils
+from autotest_lib.server.cros import provision
 from autotest_lib.server.cros.dynamic_suite import tools
 from autotest_lib.site_utils import status_history
 
@@ -779,7 +780,9 @@
 
 
 def create_job_page_handler(name, priority, control_file, control_type,
-                            image=None, hostless=False, **kwargs):
+                            image=None, hostless=False, firmware_rw_build=None,
+                            firmware_ro_build=None, test_source_build=None,
+                            **kwargs):
     """\
     Create and enqueue a job.
 
@@ -787,6 +790,13 @@
     @param priority Integer priority of this job.  Higher is more important.
     @param control_file String contents of the control file.
     @param control_type Type of control file, Client or Server.
+    @param image: ChromeOS build to be installed in the dut. Default to None.
+    @param firmware_rw_build: Firmware build to update RW firmware. Default to
+                              None, i.e., RW firmware will not be updated.
+    @param firmware_ro_build: Firmware build to update RO firmware. Default to
+                              None, i.e., RO firmware will not be updated.
+    @param test_source_build: Build to be used to retrieve test code. Default
+                              to None.
     @param kwargs extra args that will be required by create_suite_job or
                   create_job.
 
@@ -798,9 +808,15 @@
                 'control_file' : "Control file cannot be empty"})
 
     if image and hostless:
+        builds = {}
+        builds[provision.CROS_VERSION_PREFIX] = image
+        if firmware_rw_build:
+            builds[provision.FW_VERSION_PREFIX] = firmware_rw_build
+        if firmware_ro_build:
+            builds[provision.FW_RO_VERSION_PREFIX] = firmware_ro_build
         return site_rpc_interface.create_suite_job(
                 name=name, control_file=control_file, priority=priority,
-                build=image, **kwargs)
+                builds=builds, test_source_build=test_source_build, **kwargs)
     return create_job(name, priority, control_file, control_type, image=image,
                       hostless=hostless, **kwargs)
 
@@ -886,6 +902,8 @@
             test_parameter=image_parameter, parameter_value=image,
             parameter_type='string')
 
+    # TODO(crbug.com/502638): save firmware build etc to parameterized_job.
+
     # By passing a parameterized_job to create_job_common the job entry in
     # the afe_jobs table will have the field parameterized_job_id set.
     # The scheduler uses this id in the afe_parameterized_jobs table to
diff --git a/frontend/afe/site_rpc_interface.py b/frontend/afe/site_rpc_interface.py
index beb69f9..1daab8f 100644
--- a/frontend/afe/site_rpc_interface.py
+++ b/frontend/afe/site_rpc_interface.py
@@ -212,7 +212,7 @@
     max_runtime_mins = max_runtime_mins or timeout * 60
 
     if not board:
-        board = utils.ParseBuildName(build)[0]
+        board = utils.ParseBuildName(builds[provision.CROS_VERSION_PREFIX])[0]
 
     # TODO(dshi): crbug.com/496782 Remove argument build and its reference after
     # R45 falls out of stable channel.
diff --git a/frontend/client/src/autotest/afe/create/CreateJobViewDisplay.java b/frontend/client/src/autotest/afe/create/CreateJobViewDisplay.java
index 881bde1..add5097 100644
--- a/frontend/client/src/autotest/afe/create/CreateJobViewDisplay.java
+++ b/frontend/client/src/autotest/afe/create/CreateJobViewDisplay.java
@@ -127,6 +127,23 @@
         "?",
         "Example: \"device_addrs=00:1F:20:33:6A:1E, arg2=value2, arg3=value3\". " +
         "Separate multiple args with commas.");
+    private TextBoxImpl firmwareRWBuild = new TextBoxImpl();
+    private ToolTip firmwareRWBuildToolTip = new ToolTip(
+        "?",
+        "Name of the firmware build to update RW firmware of the DUT. Example: " +
+        "\"x86-alex-firmware/R41-6588.9.0\". If no firmware build is specified, " +
+        "the RW firmware of the DUT will not be updated.");
+    private TextBoxImpl firmwareROBuild = new TextBoxImpl();
+    private ToolTip firmwareROBuildToolTip = new ToolTip(
+        "?",
+        "Name of the firmware build to update RO firmware of the DUT. Example: " +
+        "\"x86-alex-firmware/R41-6588.9.0\". If no firmware RO build is specified, " +
+        "the RO firmware of the DUT will not be updated.");
+    private ExtendedListBox testSourceBuildList = new ExtendedListBox();
+    private ToolTip testSourceBuildListToolTip = new ToolTip(
+        "?",
+        "The image/build from which the tests will be fetched and ran from. It can " +
+        "be one of the specified Build Image, Firmware RW Build or the Firmware RO Build.");
     private TestSelectorDisplay testSelector = new TestSelectorDisplay();
     private CheckBoxPanelDisplay profilersPanel = new CheckBoxPanelDisplay(CHECKBOX_PANEL_COLUMNS);
     private CheckBoxImpl runNonProfiledIteration =
@@ -143,6 +160,7 @@
     private Button resetButton = new Button("Reset");
     private Label viewLink = new Label("");
     private DisclosurePanel advancedOptionsPanel = new DisclosurePanel("");
+    private DisclosurePanel firmwareBuildOptionsPanel = new DisclosurePanel("");
 
     public void initialize(HTMLPanel panel) {
         jobName.addStyleName("jobname-image-boundedwidth");
@@ -290,16 +308,48 @@
           }
 
         }
-
         advancedOptionsLayout.setWidth("100%");
         advancedOptionsPanel.addStyleName("panel-boundedwidth");
         advancedOptionsPanel.add(advancedOptionsLayout);
 
+        // Setup the Firmware Build options panel
+        firmwareBuildOptionsPanel.getHeaderTextAccessor().setText("Firmware Build Options (optional)");
+        FlexTable firmwareBuildOptionsLayout = new FlexTable();
+
+        firmwareBuildOptionsLayout.getFlexCellFormatter().setColSpan(0, 0, 2);
+        firmwareBuildOptionsLayout.setWidget(0, 0, new Label("Image URL/Build must be specified for " +
+            "updating the firmware of the test device with given firmware build. A servo may be " +
+            "required to be attached to the test device in order to have firmware updated."));
+
+        Panel firmwareRWBuildPanel = new HorizontalPanel();
+        firmwareRWBuild.addStyleName("jobname-image-boundedwidth");
+        firmwareRWBuildPanel.add(firmwareRWBuild);
+        firmwareRWBuildPanel.add(firmwareRWBuildToolTip);
+        firmwareBuildOptionsLayout.setWidget(1, 0, new Label("Firmware RW build:"));
+        firmwareBuildOptionsLayout.setWidget(1, 1, firmwareRWBuildPanel);
+
+        Panel firmwareROBuildPanel = new HorizontalPanel();
+        firmwareROBuild.addStyleName("jobname-image-boundedwidth");
+        firmwareROBuildPanel.add(firmwareROBuild);
+        firmwareROBuildPanel.add(firmwareROBuildToolTip);
+        firmwareBuildOptionsLayout.setWidget(2, 0, new Label("Firmware RO build:"));
+        firmwareBuildOptionsLayout.setWidget(2, 1, firmwareROBuildPanel);
+
+        firmwareBuildOptionsLayout.setWidth("100%");
+        firmwareBuildOptionsPanel.addStyleName("panel-boundedwidth");
+        firmwareBuildOptionsPanel.add(firmwareBuildOptionsLayout);
+        firmwareRWBuild.setEnabled(false);
+        firmwareROBuild.setEnabled(false);
+
+        testSourceBuildList.getElement().getStyle().setProperty("minWidth", "15em");
+
         // Add the remaining widgets to the main panel
         panel.add(jobName, "create_job_name");
         panel.add(jobNameToolTip, "create_job_name");
         panel.add(image_url, "create_image_url");
         panel.add(image_urlToolTip, "create_image_url");
+        panel.add(testSourceBuildList, "create_test_source_build");
+        panel.add(testSourceBuildListToolTip, "create_test_source_build");
         panel.add(fetchImageTestsButton, "fetch_image_tests");
         panel.add(testSelector, "create_tests");
         panel.add(controlFilePanel, "create_edit_control");
@@ -310,6 +360,7 @@
         panel.add(droneSet, "create_drone_set");
 
         panel.add(advancedOptionsPanel, "create_advanced_options");
+        panel.add(firmwareBuildOptionsPanel, "create_firmware_build_options");
     }
 
     public CheckBoxPanel.Display getCheckBoxPanelDisplay() {
@@ -451,4 +502,16 @@
     public HasClickHandlers getFetchImageTestsButton() {
         return fetchImageTestsButton;
     }
+
+    public ITextBox getFirmwareRWBuild() {
+      return firmwareRWBuild;
+    }
+
+    public ITextBox getFirmwareROBuild() {
+      return firmwareROBuild;
+    }
+
+    public ExtendedListBox getTestSourceBuildList() {
+      return testSourceBuildList;
+    }
 }
diff --git a/frontend/client/src/autotest/afe/create/CreateJobViewPresenter.java b/frontend/client/src/autotest/afe/create/CreateJobViewPresenter.java
index 104b815..59d3c19 100644
--- a/frontend/client/src/autotest/afe/create/CreateJobViewPresenter.java
+++ b/frontend/client/src/autotest/afe/create/CreateJobViewPresenter.java
@@ -94,12 +94,18 @@
         public HasClickHandlers getCreateTemplateJobButton();
         public HasClickHandlers getResetButton();
         public HasClickHandlers getFetchImageTestsButton();
+        public ITextBox getFirmwareRWBuild();
+        public ITextBox getFirmwareROBuild();
+        public ExtendedListBox getTestSourceBuildList();
     }
 
     private static final String EDIT_CONTROL_STRING = "Edit control file";
     private static final String UNEDIT_CONTROL_STRING= "Revert changes";
     private static final String VIEW_CONTROL_STRING = "View control file";
     private static final String HIDE_CONTROL_STRING = "Hide control file";
+    private static final String FIRMWARE_RW_BUILD = "firmware_rw_build";
+    private static final String FIRMWARE_RO_BUILD = "firmware_ro_build";
+    private static final String TEST_SOURCE_BUILD = "test_source_build";
 
     public interface JobCreateListener {
         public void onJobCreated(int jobId);
@@ -167,8 +173,37 @@
 
         display.getJobName().setText(jobObject.get("name").isString().stringValue());
 
+        ArrayList<String> builds = new ArrayList<String>();
         if (jobObject.containsKey("image")) {
-            display.getImageUrl().setText(jobObject.get("image").isString().stringValue());
+            String image = jobObject.get("image").isString().stringValue();
+            builds.add(image);
+            display.getImageUrl().setText(image);
+            display.getFirmwareRWBuild().setEnabled(true);
+            display.getFirmwareROBuild().setEnabled(true);
+            display.getTestSourceBuildList().setEnabled(true);
+        }
+
+        if (jobObject.containsKey(FIRMWARE_RW_BUILD)) {
+            String firmwareRWBuild = jobObject.get(FIRMWARE_RW_BUILD).isString().stringValue();
+            builds.add(firmwareRWBuild);
+            display.getFirmwareRWBuild().setText(firmwareRWBuild);
+        }
+
+        if (jobObject.containsKey(FIRMWARE_RO_BUILD)) {
+            String firmwareROBuild = jobObject.get(FIRMWARE_RO_BUILD).isString().stringValue();
+            builds.add(firmwareROBuild);
+            display.getFirmwareROBuild().setText(firmwareROBuild);
+        }
+
+        for (String build : builds) {
+            display.getTestSourceBuildList().addItem(build);
+        }
+
+        if (jobObject.containsKey(TEST_SOURCE_BUILD)) {
+            String testSourceBuild = jobObject.get(TEST_SOURCE_BUILD).isString().stringValue();
+            if (builds.indexOf(testSourceBuild) >= 0) {
+                display.getTestSourceBuildList().setSelectedIndex(builds.indexOf(testSourceBuild));
+            }
         }
 
         Double priorityValue = jobObject.get("priority").isNumber().getValue();
@@ -431,6 +466,47 @@
         return maxRetries;
     }
 
+    protected void handleBuildChange() {
+        ChangeHandler handler = new ChangeHandler() {
+            @Override
+            public void onChange(ChangeEvent event) {
+                String image = display.getImageUrl().getText();
+                if (image.isEmpty()) {
+                    display.getFirmwareRWBuild().setText("");
+                    display.getFirmwareROBuild().setText("");
+                    display.getTestSourceBuildList().clear();
+                    display.getFirmwareRWBuild().setEnabled(false);
+                    display.getFirmwareROBuild().setEnabled(false);
+                    display.getTestSourceBuildList().setEnabled(false);
+                }
+                else {
+                    display.getFirmwareRWBuild().setEnabled(true);
+                    display.getFirmwareROBuild().setEnabled(true);
+                    display.getTestSourceBuildList().setEnabled(true);
+                    ArrayList<String> builds = new ArrayList<String>();
+                    builds.add(image);
+                    if (!display.getFirmwareRWBuild().getText().isEmpty())
+                        builds.add(display.getFirmwareRWBuild().getText());
+                    if (!display.getFirmwareROBuild().getText().isEmpty())
+                        builds.add(display.getFirmwareROBuild().getText());
+                    String currentTestSourceBuild = display.getTestSourceBuildList().getSelectedValue();
+                    int testSourceBuildIndex = builds.indexOf(currentTestSourceBuild);
+                    display.getTestSourceBuildList().clear();
+                    for (String build : builds) {
+                        display.getTestSourceBuildList().addItem(build);
+                    }
+                    if (testSourceBuildIndex >= 0) {
+                        display.getTestSourceBuildList().setSelectedIndex(testSourceBuildIndex);
+                    }
+                }
+            }
+        };
+
+        display.getImageUrl().addChangeHandler(handler);
+        display.getFirmwareRWBuild().addChangeHandler(handler);
+        display.getFirmwareROBuild().addChangeHandler(handler);
+    }
+
     protected void setInputsEnabled() {
         testSelector.setEnabled(true);
         profilersPanel.setEnabled(true);
@@ -586,6 +662,8 @@
             }
         });
 
+        handleBuildChange();
+
         reset();
 
         if (staticData.getData("drone_sets_enabled").isBoolean().booleanValue()) {
@@ -743,6 +821,29 @@
                     }
                 }
 
+                String firmwareRWBuild = display.getFirmwareRWBuild().getText();
+                String firmwareROBuild = display.getFirmwareROBuild().getText();
+                String testSourceBuild = display.getTestSourceBuildList().getSelectedValue();
+                if (!firmwareRWBuild.isEmpty() || !firmwareROBuild.isEmpty()) {
+                    String error = "";
+                    if (testSourceBuild.isEmpty())
+                        error = "You must specify which build should be used to retrieve test code.";
+
+                    // TODO(crbug.com/502638): Enable create test job with updating firmware.
+                    if (!display.getHostless().getValue())
+                        error = "Only suite job supports updating both ChromeOS build and firmware build.";
+                    if (error != "") {
+                        display.getSubmitJobButton().setEnabled(true);
+                        NotifyManager.getInstance().showError(error);
+                        return;
+                    }
+                    if (!firmwareRWBuild.isEmpty())
+                        args.put(FIRMWARE_RW_BUILD, new JSONString(firmwareRWBuild));
+                    if (!firmwareROBuild.isEmpty())
+                        args.put(FIRMWARE_RO_BUILD, new JSONString(firmwareROBuild));
+                    args.put(TEST_SOURCE_BUILD, new JSONString(testSourceBuild));
+                }
+
                 rpcProxy.rpcCall("create_job_page_handler", args, new JsonRpcCallback() {
                     @Override
                     public void onSuccess(JSONValue result) {
diff --git a/frontend/client/src/autotest/public/AfeClient.html b/frontend/client/src/autotest/public/AfeClient.html
index 9d3d9b3..00c5079 100644
--- a/frontend/client/src/autotest/public/AfeClient.html
+++ b/frontend/client/src/autotest/public/AfeClient.html
@@ -185,6 +185,17 @@
           <tr class="data-row data-row-alternate">
             <td class="field-name">Image URL/Build: (optional)</td>
             <td class="has-tooltip" id="create_image_url"></td>
+          </tr>
+        </table>
+        <br>
+
+        <div id="create_firmware_build_options"></div>
+        <br>
+
+        <table class="data-table data-table-outlined-gray panel-boundedwidth">
+          <tr class="data-row data-row-alternate">
+            <td class="field-name">Test source build: (optional)</td>
+            <td class="has-tooltip" id="create_test_source_build"></td>
             <td class="button" id="fetch_image_tests"></td>
           </tr>
         </table>