Merge "cloudbuild:  Update the build version to be the same as run version." into main
diff --git a/src/cloudbuild_v2.yaml b/src/cloudbuild_v2.yaml
index 12c08b9..4b1c8b1 100644
--- a/src/cloudbuild_v2.yaml
+++ b/src/cloudbuild_v2.yaml
@@ -27,7 +27,7 @@
   _LOGROTATE: ""
   _DUT_MANAGER: ""
   _DUT_MANAGER_STORAGE: ""
-  _TEST_LAB_WIRING: ""
+  _INVENTORY: ""
 
 steps:
   - id: buster-slim
@@ -616,7 +616,7 @@
         "--build-arg", "LOGROTATE_VER=${_LOGROTATE}",
         "--build-arg", "DUT_MANAGER_VER=${_DUT_MANAGER}",
         "--build-arg", "DUT_MANAGER_STORAGE_VER=${_DUT_MANAGER_STORAGE}",
-        "--build-arg", "TEST_LAB_WIRING_VER=${_TEST_LAB_WIRING}",
+        "--build-arg", "INVENTORY_VER=${_INVENTORY}",
 
         "--label=version=${__BUILD_VERSION}",
 
@@ -756,7 +756,7 @@
       - "moblab_common"
       - "infra-protos"
 
-  - id: test-lab-wiring
+  - id: inventory
     name: "docker:19.03.8"
     args:
       [
@@ -766,9 +766,9 @@
         "--build-arg",
         "LABEL=${__BUILD_VERSION}",
         "-f",
-        "/workspace/src/dockerfiles/test_lab_wiring/Dockerfile",
+        "/workspace/src/dockerfiles/inventory/Dockerfile",
         "-t",
-        "${_REGISTRY_URI}/test-lab-wiring:${__BUILD_VERSION}",
+        "${_REGISTRY_URI}/inventory:${__BUILD_VERSION}",
         "/workspace/src/",
       ]
     env:
@@ -809,7 +809,7 @@
     "${_REGISTRY_URI}/dut-manager:${__BUILD_VERSION}",
     "${_REGISTRY_URI}/dut-manager-storage:${__BUILD_VERSION}",
     "${_REGISTRY_URI}/infra-protos:${__BUILD_VERSION}",
-    "${_REGISTRY_URI}/test-lab-wiring:${__BUILD_VERSION}",
+    "${_REGISTRY_URI}/inventory:${__BUILD_VERSION}",
   ]
 
 options:
diff --git a/src/common.mk b/src/common.mk
index df5f6c8..4d2bd62 100644
--- a/src/common.mk
+++ b/src/common.mk
@@ -31,7 +31,7 @@
 		ssp \
 		ui \
 		watchtower \
-		test-lab-wiring
+		inventory
 
 #TODO refactor the common rules into a single rule.
 
@@ -225,13 +225,11 @@
 		-f dockerfiles/utilities/Dockerfile.infra_protos dockerfiles/utilities
 	docker push ${REGISTRY_URI}/infra-protos:${LABEL}
 
-
-test-lab-wiring: export DOCKER_BUILDKIT := 1
-test-lab-wiring: infra-protos
-	docker build ${EXTRA_ARGS} -t ${REGISTRY_URI}/test-lab-wiring:${LABEL} \
-		-f dockerfiles/test_lab_wiring/Dockerfile .
-	docker push ${REGISTRY_URI}/test-lab-wiring:${LABEL}
-
+inventory: export DOCKER_BUILDKIT := 1
+inventory: infra-protos
+	docker build ${EXTRA_ARGS} -t ${REGISTRY_URI}/inventory:${LABEL} \
+		-f dockerfiles/inventory/Dockerfile .
+	docker push ${REGISTRY_URI}/inventory:${LABEL}
 
 moblab:	autotest \
 		common \
diff --git a/src/dockerfiles/compose/.env b/src/dockerfiles/compose/.env
index daf8b37..d3e8d3d 100644
--- a/src/dockerfiles/compose/.env
+++ b/src/dockerfiles/compose/.env
@@ -29,4 +29,4 @@
 DUT_MANAGER=autopush
 DUT_MANAGER_STORAGE=autopush
 
-TEST_LAB_WIRING=autopush
\ No newline at end of file
+INVENTORY=autopush
\ No newline at end of file
diff --git a/src/dockerfiles/compose/Dockerfile b/src/dockerfiles/compose/Dockerfile
index a38d1c5..59cf709 100644
--- a/src/dockerfiles/compose/Dockerfile
+++ b/src/dockerfiles/compose/Dockerfile
@@ -34,7 +34,7 @@
 ARG LOGROTATE_VER
 ARG DUT_MANAGER_VER
 ARG DUT_MANAGER_STORAGE_VER
-ARG TEST_LAB_WIRING_VER
+ARG INVENTORY_VER
 
 
 ENV LABEL="${LABEL}"
@@ -75,7 +75,7 @@
 RUN ./override_service_version.sh LOGROTATE ${LOGROTATE_VER:-${BUILD_VERSION}}
 RUN ./override_service_version.sh DUT_MANAGER ${DUT_MANAGER_VER:-${BUILD_VERSION}}
 RUN ./override_service_version.sh DUT_MANAGER_STORAGE ${DUT_MANAGER_STORAGE_VER:-${BUILD_VERSION}}
-RUN ./override_service_version.sh TEST_LAB_WIRING ${TEST_LAB_WIRING_VER:-${BUILD_VERSION}}
+RUN ./override_service_version.sh INVENTORY ${INVENTORY_VER:-${BUILD_VERSION}}
 
 
 ENTRYPOINT ["/compose_startup.sh"]
diff --git a/src/dockerfiles/compose/docker-compose.yaml b/src/dockerfiles/compose/docker-compose.yaml
index 12e3b33..872ca37 100644
--- a/src/dockerfiles/compose/docker-compose.yaml
+++ b/src/dockerfiles/compose/docker-compose.yaml
@@ -554,9 +554,9 @@
     dns:
       - 192.168.100.51
 ##############################################################################
-  test-lab-wiring:
-    container_name:  test-lab-wiring
-    image: ${REGISTRY_URI:-gcr.io/chromeos-partner-moblab}/test-lab-wiring:${TEST_LAB_WIRING:-release}
+  inventory:
+    container_name: inventory
+    image: ${REGISTRY_URI:-gcr.io/chromeos-partner-moblab}/inventory:${INVENTORY:-release}
     user:  moblab
     restart: unless-stopped
     volumes:
@@ -571,6 +571,11 @@
 
     dns:
       - 192.168.100.51
+
+    environment:
+      - GRPC_SERVER_PORT=7005
+      - DEBUG_LOGS=True
+      - LOGFILE=/var/log/moblab/inventory.log
 ##############################################################################
 
 volumes:
diff --git a/src/dockerfiles/test_lab_wiring/Dockerfile b/src/dockerfiles/inventory/Dockerfile
similarity index 79%
rename from src/dockerfiles/test_lab_wiring/Dockerfile
rename to src/dockerfiles/inventory/Dockerfile
index 8e8ef96..896b932 100644
--- a/src/dockerfiles/test_lab_wiring/Dockerfile
+++ b/src/dockerfiles/inventory/Dockerfile
@@ -17,12 +17,11 @@
 RUN addgroup --gid 246 moblab && adduser --ingroup moblab --uid 246 \
     --disabled-password --gecos "" moblab
 
-WORKDIR /etc/moblab/test_lab_wiring
-COPY --chown=moblab test_lab_wiring/ .
+WORKDIR /etc/moblab/inventory
+COPY --chown=moblab inventory/ .
 COPY --from=protos /protos .
 
-WORKDIR /etc/moblab/test_lab_wiring
-COPY --chown=moblab dockerfiles/test_lab_wiring/requirements.txt ./
+COPY --chown=moblab dockerfiles/inventory/requirements.txt ./
 RUN pip install --upgrade pip
 RUN pip install --no-cache-dir -r requirements.txt
 
@@ -30,4 +29,4 @@
 RUN chown moblab:moblab /var/log/moblab
 USER moblab
 
-CMD ["python", "test_lab_wiring.py", "-v"]
+CMD ["python", "inventory.py"]
diff --git a/src/dockerfiles/test_lab_wiring/requirements.in b/src/dockerfiles/inventory/requirements.in
similarity index 61%
rename from src/dockerfiles/test_lab_wiring/requirements.in
rename to src/dockerfiles/inventory/requirements.in
index cf9ad88..fb49f54 100644
--- a/src/dockerfiles/test_lab_wiring/requirements.in
+++ b/src/dockerfiles/inventory/requirements.in
@@ -1,3 +1,2 @@
 grpcio-tools
-grpcio
-portpicker
+grpcio
\ No newline at end of file
diff --git a/src/dockerfiles/inventory/requirements.txt b/src/dockerfiles/inventory/requirements.txt
new file mode 100644
index 0000000..c307ba3
--- /dev/null
+++ b/src/dockerfiles/inventory/requirements.txt
@@ -0,0 +1,13 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    pip-compile requirements.in
+#
+grpcio-tools==1.39.0      # via -r requirements.in
+grpcio==1.39.0            # via -r requirements.in, grpcio-tools
+protobuf==3.17.3          # via grpcio-tools
+six==1.16.0               # via grpcio, protobuf
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/src/dockerfiles/test_lab_wiring/requirements.txt b/src/dockerfiles/test_lab_wiring/requirements.txt
deleted file mode 100644
index 175a017..0000000
--- a/src/dockerfiles/test_lab_wiring/requirements.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-#
-# This file is autogenerated by pip-compile
-# To update, run:
-#
-#    pip-compile requirements.in
-#
-grpcio-tools==1.38.0      # via -r requirements.in
-grpcio==1.38.0            # via -r requirements.in, grpcio-tools
-portpicker==1.4.0         # via -r requirements.in
-protobuf==3.17.2          # via grpcio-tools
-six==1.16.0               # via grpcio, protobuf
-
-# The following packages are considered to be unsafe in a requirements file:
-# setuptools
diff --git a/src/dockerfiles/utilities/Dockerfile.infra_protos b/src/dockerfiles/utilities/Dockerfile.infra_protos
index ab0c339..a4cb77e 100644
--- a/src/dockerfiles/utilities/Dockerfile.infra_protos
+++ b/src/dockerfiles/utilities/Dockerfile.infra_protos
@@ -42,7 +42,6 @@
     --grpc_python_out=. \
     --plugin=protoc-gen-grpc_python=/usr/local/bin/grpc_python_plugin \
     /proto/src/lab/*.proto \
-    /config/proto/chromiumos/config/api/test/tls/*.proto \
     /config/proto/chromiumos/longrunning/*.proto
 
 
diff --git a/src/inventory/inventory.py b/src/inventory/inventory.py
new file mode 100644
index 0000000..91100d7
--- /dev/null
+++ b/src/inventory/inventory.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import argparse
+from concurrent import futures
+import logging
+import sys
+import os
+import time
+import grpc
+
+from inventory_rpcservice import (
+    InventoryRpcService,
+)
+from chromiumos.test.lab.api import inventory_service_pb2_grpc
+
+_LOGGER = logging.getLogger("inventory")
+
+
+class Inventory:
+    """Class that implements the Inventory RPC server."""
+
+    def setup_logging(self, level, log_filename):
+        """Set up the custom file log handler.
+
+        Logs to bootup as that is mounted in the moblab software debug
+        container.
+
+        Args:
+            level (integer): A valid python logging level.
+        """
+        _LOGGER.setLevel(level)
+        if log_filename:
+            handler = logging.FileHandler(log_filename)
+        else:
+            handler = logging.StreamHandler()
+        handler.setFormatter(
+            logging.Formatter(
+                "%(asctime)s %(filename)s:%(lineno)d %(levelname)s:"
+                " %(message)s"
+            )
+        )
+        # Some code runs before this function may have created some handler
+        # and we don't want them.
+        _LOGGER.handlers = [handler]
+
+    def parse_arguments(self, argv):
+        """Parse arguments passed to the server.
+
+        Args:
+            argv (list): Arguments passed to to the server.
+
+        Returns:
+            Namespace: namespace mapping for each command line argument.
+        """
+        parser = argparse.ArgumentParser(description=__doc__)
+
+        parser.add_argument(
+            "-p",
+            "--grpc_port",
+            help="Grpc port to run the server on.",
+        )
+        parser.add_argument(
+            "-v",
+            "--verbose",
+            action="store_true",
+            help="Turn on debug logging.",
+        )
+        parser.add_argument(
+            "-l",
+            "--logfile",
+            type=str,
+            required=False,
+            help="Full path to logfile. By default will log to console.",
+        )
+        return parser.parse_args(argv)
+
+    def serve(self, args):
+        """Run the Inventory GRPC server.
+
+        Args:
+            args (): Parameters passed to the server.
+        """
+        options = self.parse_arguments(args)
+        logging_severity = logging.INFO
+
+        # Environment variables take presedence over parameters
+        # so that the values can be overriden in compose file
+
+        verbose_logging = os.getenv("DEBUG_LOGS")
+        if verbose_logging or options.verbose:
+            logging_severity = logging.DEBUG
+
+        # Logs to console if file is not configured
+        logfile = os.environ.get("LOGFILE") or options.logfile
+        self.setup_logging(logging_severity, logfile)
+
+        server = grpc.server(futures.ThreadPoolExecutor(max_workers=500))
+
+        port = os.getenv("GRPC_SERVER_PORT") or options.grpc_port
+        if not port:
+            raise argparse.ArgumentError("Port is a required argument!")
+
+        inventory_service = InventoryRpcService()
+        try:
+            inventory_service_pb2_grpc.add_InventoryServiceServicer_to_server(
+                inventory_service, server
+            )
+
+            server.add_insecure_port("[::]:%s" % port)
+            server.start()
+            _LOGGER.info("Starting server on %s", port)
+            while True:
+                _LOGGER.info("Server sleeping")
+                time.sleep(60 * 60 * 24)
+
+        except KeyboardInterrupt:
+            server.stop(0)
+
+
+if __name__ == "__main__":
+    Inventory().serve(sys.argv[1:])
diff --git a/src/inventory/inventory_rpcservice.py b/src/inventory/inventory_rpcservice.py
new file mode 100644
index 0000000..210c5b6
--- /dev/null
+++ b/src/inventory/inventory_rpcservice.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+
+from chromiumos.test.lab.api.inventory_service_pb2_grpc import (
+    InventoryServiceServicer,
+)
+
+_LOGGER = logging.getLogger("inventory")
+
+
+class InventoryRpcService(InventoryServiceServicer):
+    """Class that implements the lab inventory RPC servicer."""
+
+    def __init__(self):
+        """Initialize the inventory service."""
+        super(InventoryRpcService, self).__init__()
+
+    def GetDutTopology(self, request, context):
+        """Get the DUT topology."""
+        pass
diff --git a/src/moblab-ui/src/app/manage-duts/manage-duts.module.ts b/src/moblab-ui/src/app/manage-duts/manage-duts.module.ts
index 0a9bda0..93c06f2 100644
--- a/src/moblab-ui/src/app/manage-duts/manage-duts.module.ts
+++ b/src/moblab-ui/src/app/manage-duts/manage-duts.module.ts
@@ -37,6 +37,7 @@
 
 import {PipesModule} from 'app/pipes/pipes.module';
 import {ProvisionDialogComponent} from './provision-dialog/provision-dialog.component';
+import {StageBuildDialogComponent} from './stage-build-dialog/stage-build-dialog.component';
 
 @NgModule({
   imports: [
@@ -76,6 +77,7 @@
     FirmwareUpdateConfirmDialog,
     FirmwareUpdateResultDialog,
     ProvisionDialogComponent,
+    StageBuildDialogComponent,
   ],
   exports: [ManageDutsComponent],
 })
diff --git a/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.html b/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.html
new file mode 100644
index 0000000..2624ffd
--- /dev/null
+++ b/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.html
@@ -0,0 +1,23 @@
+<div class="loading-overlay-wrapper">
+    <app-loading-overlay></app-loading-overlay>
+</div>
+<mat-card *ngIf="stageBuildNotification" class="notification-banner {{ stageBuildNotification.cssClass }}">
+    <mat-icon class="notification-icon {{ stageBuildNotification.cssClass }}">{{ stageBuildNotification.icon }}
+    </mat-icon>
+    {{ stageBuildNotification.message }}
+</mat-card>
+<h2 mat-dialog-title>Access Test Build</h2>
+<mat-dialog-content class="mat-typography">
+    <app-build-select-forms [hidePoolSelector]="true" (allRequiredFieldsSet)="onBuildSet($event)"
+        (formLoading)="updateLoadingOverlay($event.message)" (formLoaded)="hideLoadingOverlay()"
+        (buildVersionUnselected)="onBuildVersionUnselected()">
+    </app-build-select-forms>
+
+</mat-dialog-content>
+<mat-dialog-actions align="end">
+    <button mat-button mat-dialog-close>Cancel</button>
+    <button *ngIf="!isStageBuildSucceeded()" mat-button (click)="stageBuild()"
+        [disabled]="this.stageBuildButtonDisabled">Start</button>
+    <a href="{{buildBucketUrl}}" target="_blank" *ngIf="isStageBuildSucceeded()" [mat-dialog-close]="returnMessage"
+        mat-button>Go to Build Bucket</a>
+</mat-dialog-actions>
\ No newline at end of file
diff --git a/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.scss b/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.scss
new file mode 100644
index 0000000..f0e0a97
--- /dev/null
+++ b/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.scss
@@ -0,0 +1,32 @@
+.loading-overlay-wrapper {
+  display: block;
+  font-size: 13px;
+  position: relative;
+  margin-bottom: 35px;
+  margin-top: 0px;
+}
+
+.notification-banner {
+  margin-bottom: 16px;
+}
+
+.notification-banner.success {
+  background-color: #CEEAD6;
+}
+
+.notification-banner.error {
+  background-color: #FAD2CF;
+}
+
+.notification-icon {
+  vertical-align: middle;
+}
+
+.notification-icon.success {
+  color: #0D652D;
+  vertical-align: middle;
+}
+
+.notification-icon.error {
+  color: #A50E0E;
+}
diff --git a/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.spec.ts b/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.spec.ts
new file mode 100644
index 0000000..0695d7b
--- /dev/null
+++ b/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { StageBuildDialogComponent } from './stage-build-dialog.component';
+
+describe('StageBuildDialogComponent', () => {
+  let component: StageBuildDialogComponent;
+  let fixture: ComponentFixture<StageBuildDialogComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ StageBuildDialogComponent ]
+    })
+    .compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(StageBuildDialogComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.ts b/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.ts
new file mode 100644
index 0000000..052a8cd
--- /dev/null
+++ b/src/moblab-ui/src/app/manage-duts/stage-build-dialog/stage-build-dialog.component.ts
@@ -0,0 +1,139 @@
+import {Component, ViewChild} from '@angular/core';
+import {
+  LoadingOverlayComponent,
+  LoadingOverlayStyle,
+} from 'app/widgets/loading-overlay/loading-overlay.component';
+import {MoblabGrpcService} from 'app/services/moblab-grpc.service';
+import {BuildSelectFormsComponent} from 'app/run-suite/common/build-select-forms/build-select-forms.component';
+
+export enum StageBuildStatus {
+  SUCCESS,
+  ERROR,
+}
+
+const STAGE_BUILD_SUCCESS_MESSAGE = 'Successfully staged the test build.';
+const STAGE_BUILD_ERROR_MESSAGE =
+  'Failed to stage the test build, please try again.';
+const SUCCESS_ICON = 'check_circle';
+const ERROR_ICON = 'error';
+
+export class StageBuildNotification {
+  constructor(
+    public message: string,
+    public status: StageBuildStatus,
+    public icon: string,
+    public cssClass: string
+  ) { }
+}
+const GCS_URL = 'https://console.developers.google.com/storage/';
+
+@Component({
+  selector: 'app-stage-build-dialog',
+  templateUrl: 'stage-build-dialog.component.html',
+  styleUrls: ['stage-build-dialog.component.scss'],
+})
+export class StageBuildDialogComponent {
+  @ViewChild(LoadingOverlayComponent) loadingOverlay: LoadingOverlayComponent;
+  @ViewChild(BuildSelectFormsComponent)
+  buildSelectForm: BuildSelectFormsComponent;
+
+  stageBuildNotification: StageBuildNotification;
+  stageBuildButtonDisabled = true;
+  buildBucketUrl: string;
+  returnMessage: string;
+
+  selectedBuild = '';
+  selectedBuildTarget = '';
+  selectedMilestone = '';
+  selectedModel = '';
+
+  constructor(public moblabGrpcService: MoblabGrpcService) { }
+
+  onBuildSet(event) {
+    this.selectedModel = event.model;
+    this.selectedBuildTarget = event.board;
+    this.selectedMilestone = event.milestone;
+    this.selectedBuild = event.build;
+    this.stageBuildButtonDisabled = false;
+  }
+
+  onBuildVersionUnselected() {
+    this.stageBuildButtonDisabled = true;
+  }
+
+  async stageBuild() {
+    const buildId = this._formatBuildId();
+    this.updateLoadingOverlay('Staging the test build...');
+    this.disableAllSelectors();
+    try {
+      const buildBucket = await this.moblabGrpcService.stageBuildPromise(
+        this.selectedModel,
+        this.selectedBuildTarget,
+        this.selectedBuild
+      );
+      this.stageBuildNotification = new StageBuildNotification(
+        STAGE_BUILD_SUCCESS_MESSAGE,
+        StageBuildStatus.SUCCESS,
+        SUCCESS_ICON,
+        'success'
+      );
+      this.buildBucketUrl = this._formatBuildBucketUrl(buildBucket);
+      this.returnMessage = `Successfully staged the test build ${buildId} to gcs bucket: ${buildBucket}`;
+    } catch (error) {
+      this.enableAllSelectors();
+      this.stageBuildNotification = new StageBuildNotification(
+        STAGE_BUILD_ERROR_MESSAGE,
+        StageBuildStatus.ERROR,
+        ERROR_ICON,
+        'error'
+      );
+    }
+    this.hideLoadingOverlay();
+  }
+
+  hideLoadingOverlay() {
+    this.loadingOverlay.hide();
+  }
+
+  updateLoadingOverlay(message: string) {
+    this.loadingOverlay.show();
+    this.loadingOverlay.updateStatus(message, LoadingOverlayStyle.Top);
+  }
+
+  enableAllSelectors() {
+    this.stageBuildButtonDisabled = false;
+    this.buildSelectForm.modelSelector.enable();
+    this.buildSelectForm.boardSelector.enable();
+    this.buildSelectForm.milestoneSelector.enable();
+    this.buildSelectForm.buildSelector.enable();
+  }
+
+  disableAllSelectors() {
+    this.stageBuildButtonDisabled = true;
+    this.buildSelectForm.modelSelector.disable();
+    this.buildSelectForm.boardSelector.disable();
+    this.buildSelectForm.milestoneSelector.disable();
+    this.buildSelectForm.buildSelector.disable();
+  }
+
+  isStageBuildSucceeded(): boolean {
+    if (
+      !this.stageBuildNotification ||
+      this.stageBuildNotification.status === StageBuildStatus.ERROR
+    ) {
+      return false;
+    }
+    return true;
+  }
+
+  _formatBuildBucketUrl(buildBucket: string): string {
+    const buildId = this._formatBuildId();
+    return (
+      GCS_URL + `${buildBucket}/${this.selectedBuildTarget}-release/${buildId}`
+    );
+  }
+
+  _formatBuildId(): string {
+    return `R${this.selectedMilestone}-${this.selectedBuild}`;
+  }
+}
diff --git a/src/moblab-ui/src/app/manage-duts/view-duts/view-duts.component.html b/src/moblab-ui/src/app/manage-duts/view-duts/view-duts.component.html
index d98d79d..82ffa23 100644
--- a/src/moblab-ui/src/app/manage-duts/view-duts/view-duts.component.html
+++ b/src/moblab-ui/src/app/manage-duts/view-duts/view-duts.component.html
@@ -40,7 +40,12 @@
 
     <ng-container matColumnDef="model">
       <th class="m-column" mat-header-cell mat-sort-header *matHeaderCellDef>Model</th>
-      <td mat-cell *matCellDef="let element">{{element.getModel()}}</td>
+      <td mat-cell *matCellDef="let element">
+        {{element.getModel()}}
+        <button mat-button color="primary" (click)="accessTestBuild()" *ngIf="!element.getIsConnected()">
+          Access Test Build
+        </button>
+      </td>
     </ng-container>
 
     <ng-container matColumnDef="status">
diff --git a/src/moblab-ui/src/app/manage-duts/view-duts/view-duts.component.ts b/src/moblab-ui/src/app/manage-duts/view-duts/view-duts.component.ts
index 634b75d..9dba4dd 100644
--- a/src/moblab-ui/src/app/manage-duts/view-duts/view-duts.component.ts
+++ b/src/moblab-ui/src/app/manage-duts/view-duts/view-duts.component.ts
@@ -1,11 +1,13 @@
 import {Component, Input, OnInit, ViewChild} from '@angular/core';
-import {MatSort, Sort} from '@angular/material/sort';
+import {MatSort} from '@angular/material/sort';
 import {MatTableDataSource} from '@angular/material/table';
+import {MatDialog} from '@angular/material/dialog';
 import {SelectionModel} from '@angular/cdk/collections';
 import {ConnectedDutInfo} from 'app/services/moblabrpc_pb';
 import {MoblabGrpcService} from 'app/services/moblab-grpc.service';
 import {NotificationsService} from 'app/services/notifications.service';
 import {BuildTargetAccessService} from 'app/services/build-target-access.service';
+import {StageBuildDialogComponent} from '../stage-build-dialog/stage-build-dialog.component'
 
 import {INT_TO_DUT_STATUS} from '../../utils/proto_helpers';
 import {TableHeaderSelectorComponent} from '../../widgets/table-header-selector/table-header-selector.component';
@@ -50,6 +52,7 @@
 
   constructor(
     public moblabGrpcService: MoblabGrpcService,
+    public dialog: MatDialog,
     private notificationsService: NotificationsService,
     private buildTargetAccessService: BuildTargetAccessService
   ) { }
@@ -194,4 +197,16 @@
       });
     }
   }
+
+  async accessTestBuild() {
+    const dialogRef = this.dialog.open(StageBuildDialogComponent);
+    try {
+      const responseMessage = await dialogRef.afterClosed().toPromise();
+      if (responseMessage) {
+        this.notificationsService.notify(responseMessage);
+      }
+    } catch (error) {
+      this.notificationsService.error(error);
+    }
+  }
 }
diff --git a/src/moblab-ui/src/app/run-suite/common/build-select-forms/build-select-forms.component.html b/src/moblab-ui/src/app/run-suite/common/build-select-forms/build-select-forms.component.html
index 6eebe15..563cc4b 100644
--- a/src/moblab-ui/src/app/run-suite/common/build-select-forms/build-select-forms.component.html
+++ b/src/moblab-ui/src/app/run-suite/common/build-select-forms/build-select-forms.component.html
@@ -19,5 +19,6 @@
 </app-basic-autocomplete-selector>
 
 <app-basic-autocomplete-selector #poolSelector id="poolSelector" [title]="'Pool (Optional):'"
-    [placeholder]="['Select a pool']" [options]="pools" [isShown]="true" (select)="poolChanged($event.value)">
+    [placeholder]="['Select a pool']" [options]="pools" [isShown]="true" (select)="poolChanged($event.value)"
+    *ngIf="!this.hidePoolSelector">
 </app-basic-autocomplete-selector>
\ No newline at end of file
diff --git a/src/moblab-ui/src/app/run-suite/common/build-select-forms/build-select-forms.component.ts b/src/moblab-ui/src/app/run-suite/common/build-select-forms/build-select-forms.component.ts
index fc84e4e..8d07a9f 100644
--- a/src/moblab-ui/src/app/run-suite/common/build-select-forms/build-select-forms.component.ts
+++ b/src/moblab-ui/src/app/run-suite/common/build-select-forms/build-select-forms.component.ts
@@ -3,6 +3,7 @@
   ChangeDetectorRef,
   Component,
   EventEmitter,
+  Input,
   Output,
   ViewChild,
 } from '@angular/core';
@@ -32,6 +33,7 @@
   @ViewChild('buildSelector') buildSelector;
   @ViewChild('poolSelector') poolSelector;
 
+  @Input() hidePoolSelector?: boolean = false;
   @Output() formLoading = new EventEmitter();
   @Output() formLoaded = new EventEmitter();
   @Output() allRequiredFieldsSet = new EventEmitter();
@@ -64,7 +66,9 @@
     this.boardSelector.disable(SELECT_MODEL_PROMPT);
     this.milestoneSelector.disable(SELECT_BUILD_TARGET_PROMPT);
     this.buildSelector.disable(SELECT_MILESTONE_PROMPT);
-    this.poolSelector.disable(SELECT_BUILD_VERSION_PROMPT);
+    if (!this.hidePoolSelector) {
+      this.poolSelector.disable(SELECT_BUILD_VERSION_PROMPT);
+    }
     this.changeDetector.detectChanges();
   }
 
@@ -206,7 +210,9 @@
 
   buildVersionChanged(newBuildVersion: string): void {
     this.buildVersionName = newBuildVersion;
-    this.poolSelector.enable();
+    if (!this.hidePoolSelector) {
+      this.poolSelector.enable();
+    }
     this.buildChanged(this.getBuildFormattedString(), this.buildTargetName);
     this.emitBuildSelections();
   }
@@ -214,6 +220,9 @@
   buildChanged(build: string, board: string) {
     if (build) {
       this.selectedBuild = build;
+      if (this.hidePoolSelector) {
+        return;
+      }
       this.moblabGrpcService.listPools(
         (pools: string[]) => {
           this.updatePools(pools);
@@ -272,7 +281,9 @@
   unselectBuildVersion() {
     this.buildVersionName = null;
     this.buildSelector.clearSelection();
-    this.poolSelector.disable(SELECT_BUILD_VERSION_PROMPT);
+    if (!this.hidePoolSelector) {
+      this.poolSelector.disable(SELECT_BUILD_VERSION_PROMPT);
+    }
     this.buildVersionUnselected.emit();
   }
 }
diff --git a/src/moblab-ui/src/app/run-suite/run-suite.module.ts b/src/moblab-ui/src/app/run-suite/run-suite.module.ts
index 83189cf..16f70f1 100644
--- a/src/moblab-ui/src/app/run-suite/run-suite.module.ts
+++ b/src/moblab-ui/src/app/run-suite/run-suite.module.ts
@@ -58,6 +58,7 @@
   ],
   exports: [
     BasicAutocompleteSelectorComponent,
+    BuildSelectFormsComponent,
     BvtComponent,
     CtsRunComponent,
     MemoryQualComponent,
diff --git a/src/moblab-ui/src/app/services/moblab-grpc.service.ts b/src/moblab-ui/src/app/services/moblab-grpc.service.ts
index 61f055b..e312246 100644
--- a/src/moblab-ui/src/app/services/moblab-grpc.service.ts
+++ b/src/moblab-ui/src/app/services/moblab-grpc.service.ts
@@ -98,6 +98,8 @@
   FirmwareUpdateCommandOutput,
   ValidateStorageQualSetupRequest,
   ProvisionDutsRequest,
+  StageBuildRequest,
+  StageBuildResponse,
   BuildItem,
 } from './moblabrpc_pb';
 
@@ -464,6 +466,23 @@
     );
   }
 
+  async stageBuildPromise(
+    model: string,
+    build_target: string,
+    build_version: string
+  ): Promise<string> {
+    /** Stage the given build on the bucket configured on this moblab.
+     * */
+    const request = new StageBuildRequest();
+    request.setModel(model);
+    request.setBuildTarget(build_target);
+    request.setBuildVersion(build_version);
+    const response = await this.moblabRpcServicePromiseClient.stage_build(
+      request
+    );
+    return response.getBuildBucket();
+  }
+
   listConnectedDuts(
     callback: (x: ConnectedDutInfo[]) => void,
     error_callback: (err: string) => void
diff --git a/src/moblab_common/protos/moblabrpc.proto b/src/moblab_common/protos/moblabrpc.proto
index 19f305b..712dc01 100644
--- a/src/moblab_common/protos/moblabrpc.proto
+++ b/src/moblab_common/protos/moblabrpc.proto
@@ -16,6 +16,7 @@
   rpc run_faft_suite (RunFAFTSuiteRequest)
     returns (RunSuiteResponse) {}
   rpc provision_duts (ProvisionDutsRequest) returns (google.protobuf.Empty) {}
+  rpc stage_build (StageBuildRequest) returns (StageBuildResponse) {}
 
   rpc list_connected_duts (ListConnectedDutsRequest) returns (ListConnectedDutsResponse) {}
   rpc list_build_targets (ListBuildTargetsRequest) returns (ListBuildTargetsResponse) {}
@@ -114,6 +115,18 @@
   string pool = 3;
 }
 
+// NEXT_TAG = 4
+message StageBuildRequest {
+  string model = 1;
+  string build_target = 2;
+  string build_version = 3;
+}
+
+// NEXT_TAG = 2
+message StageBuildResponse {
+  string build_bucket = 1;
+}
+
 // NEXT_TAG = 8
 message RunCtsSuiteRequest {
   string android_version = 1;
diff --git a/src/moblab_rpcserver/moblab_rpcservice.py b/src/moblab_rpcserver/moblab_rpcservice.py
index a31c08d..0c6bd98 100755
--- a/src/moblab_rpcserver/moblab_rpcservice.py
+++ b/src/moblab_rpcserver/moblab_rpcservice.py
@@ -181,6 +181,27 @@
             context.set_details(message)
         return empty_pb2.Empty()
 
+    def stage_build(self, request, context):
+        """Stage the build to the bucket configured for this moblab.
+
+        Args:
+          request: StageBuildRequest object with parameters describing the
+          model, build target and build version to be staged.
+          context: grpc.server.Context
+        Returns:
+          StageBuildRequest object with a string bucket name.
+        """
+        try:
+            return moblabrpc_pb2.StageBuildResponse(
+                build_bucket=self.service._stage_build(
+                    request.model, request.build_target, request.build_version
+                )
+            )
+        except moblab_service.MoblabRpcError as ex:
+            context.set_code(grpc.StatusCode.INTERNAL)
+            context.set_details(str(ex))
+            raise
+
     def run_cts_suite(self, request, _context):
         """Sets off CTS suite run.
 
diff --git a/src/moblab_rpcserver/moblab_service.py b/src/moblab_rpcserver/moblab_service.py
index 1fab509..d27bd54 100644
--- a/src/moblab_rpcserver/moblab_service.py
+++ b/src/moblab_rpcserver/moblab_service.py
@@ -717,7 +717,7 @@
             build_version (string): Build version to be applied to DUT for
                 testing.
         Returns:
-            Nothing.
+            gcs bucket name configured on this moblab.
         """
         self.moblab_build_connector.stage_build(
             build_target, model, build_version, self.moblab_bucket_name
@@ -729,7 +729,7 @@
             if self.moblab_build_connector.check_build_stage_status(
                 build_target, model, build_version, self.moblab_bucket_name
             ):
-                return
+                return self.moblab_bucket_name
             time.sleep(sleep_interval)
             # every failure, increase wait interval by 1.5 times
             # ( exponential backoff, of sorts )
diff --git a/src/test_lab_wiring/test_lab_wiring.py b/src/test_lab_wiring/test_lab_wiring.py
deleted file mode 100644
index 9aa5148..0000000
--- a/src/test_lab_wiring/test_lab_wiring.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2021 The Chromium OS Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import argparse
-import concurrent
-import logging
-import sys
-import time
-
-import grpc
-
-import test_lab_wiring_rpcservice
-
-from chromiumos.config.api.test.tls.wiring_pb2_grpc import (
-    add_WiringServicer_to_server,
-)
-
-_LOGGER = logging.getLogger("test-lab-wiring")
-
-
-class TestLabWiring(object):
-    """Class that implements the test lab wiring RPC server."""
-
-    def setup_logging(self, level):
-        """Set up the custom file log handler.
-
-        Logs to bootup as that is mounted in the moblab software debug
-        container.
-
-        Args:
-            level (integer): A valid python logging level.
-        """
-        _LOGGER.setLevel(level)
-        handler = logging.FileHandler(
-            "/var/log/moblab/test-lab-wiring.log"
-        )
-        handler.setFormatter(
-            logging.Formatter(
-                "%(asctime)s %(filename)s:%(lineno)d %(levelname)s:"
-                " %(message)s"
-            )
-        )
-        # Some code runs before this function may have created some handler
-        # and we don't want them.
-        _LOGGER.handlers = [handler]
-
-    def parse_arguments(self, argv):
-        """Parse arguments passed to the server.
-
-        Args:
-            argv (list): Arguments passed to to the server.
-
-        Returns:
-            [type]: [description]
-        """
-        parser = argparse.ArgumentParser(description=__doc__)
-
-        parser.add_argument(
-            "-v",
-            "--verbose",
-            action="store_true",
-            help="Turn on debug logging.",
-        )
-        return parser.parse_args(argv)
-
-    def serve(self, args):
-        """Run the test lab wiring GRPC server.
-
-        Args:
-            args (): Parameters passed to the server.
-        """
-        options = self.parse_arguments(args)
-        logging_severity = logging.INFO
-        if options.verbose:
-            logging_severity = logging.DEBUG
-        self.setup_logging(logging_severity)
-
-        server = grpc.server(
-            concurrent.futures.ThreadPoolExecutor(max_workers=500)
-        )
-
-        servicer = test_lab_wiring_rpcservice.TestLabWiringRpcService()
-
-        try:
-            add_WiringServicer_to_server(servicer, server)
-
-            server.add_insecure_port("[::]:7005")
-            logging.info("Starting server on 7005")
-            server.start()
-            while True:
-                logging.info("Server sleeping")
-                time.sleep(60 * 60 * 24)
-        except KeyboardInterrupt:
-            server.stop(0)
-
-
-if __name__ == "__main__":
-    host_server = TestLabWiring()
-    host_server.serve(sys.argv[1:])
diff --git a/src/test_lab_wiring/test_lab_wiring_rpcservice.py b/src/test_lab_wiring/test_lab_wiring_rpcservice.py
deleted file mode 100644
index fc155d4..0000000
--- a/src/test_lab_wiring/test_lab_wiring_rpcservice.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2021 The Chromium OS Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import logging
-import grpc
-
-from chromiumos.config.api.test.tls.wiring_pb2_grpc import WiringServicer
-
-
-_LOGGER = logging.getLogger("dut-manager-storage")
-
-
-class TestLabWiringRpcService(WiringServicer):
-    """Class that implements the DUT manager storage RPC servicer."""
-
-    def GetDut(self, request, context):
-        context.abort(grpc.StatusCode.UNIMPLEMENTED, "Not Implemented")
-        raise NotImplementedError
-
-    def SetDutPowerSupply(self, request, context):
-        context.abort(grpc.StatusCode.UNIMPLEMENTED, "Not Implemented")
-        raise NotImplementedError
-
-    def CacheForDut(self, request, context):
-        context.abort(grpc.StatusCode.UNIMPLEMENTED, "Not Implemented")
-        raise NotImplementedError
-
-    def CallServo(self, request, context):
-        context.abort(grpc.StatusCode.UNIMPLEMENTED, "Not Implemented")
-        raise NotImplementedError
-
-    def ExposePortToDut(self, request, context):
-        context.abort(grpc.StatusCode.UNIMPLEMENTED, "Not Implemented")
-        raise NotImplementedError