scheduler_stages: Update scheduler to utilize Buildbucket V2

Update cbuildbot scheduler_stages to utilize Buildbucket V2 calls,
factoring out Buildbucket V1 calls.  This change will remove V1 calls
from scheduler stages as an initial pass.

BUG=chromium:1113571,b:178485814
TEST=`stabilize branch => cros tryjob --production master-release`

Change-Id: Iee6d59e3dd17409b0cae3dbdb24a8cbff7db1bff
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2752600
Reviewed-by: Dhanya Ganesh <dhanyaganesh@chromium.org>
Commit-Queue: Mike Nichols <mikenichols@chromium.org>
Tested-by: Mike Nichols <mikenichols@chromium.org>
diff --git a/cbuildbot/stages/scheduler_stages.py b/cbuildbot/stages/scheduler_stages.py
index a2e88f2..dd4f5ba 100644
--- a/cbuildbot/stages/scheduler_stages.py
+++ b/cbuildbot/stages/scheduler_stages.py
@@ -9,14 +9,19 @@
 
 import time
 
+from google.protobuf import field_mask_pb2
 from chromite.cbuildbot.stages import generic_stages
 from chromite.lib import buildbucket_lib
+from chromite.lib import buildbucket_v2
 from chromite.lib import build_requests
 from chromite.lib import constants
 from chromite.lib import cros_logging as logging
 from chromite.lib import failures_lib
 from chromite.lib import request_build
 
+from infra_libs.buildbucket.proto import builder_pb2, builds_service_pb2
+from infra_libs.buildbucket.proto import common_pb2
+
 
 class ScheduleSlavesStage(generic_stages.BuilderStage):
   """Stage that schedules slaves for the master build."""
@@ -26,45 +31,53 @@
   def __init__(self, builder_run, buildstore, sync_stage, **kwargs):
     super(ScheduleSlavesStage, self).__init__(builder_run, buildstore, **kwargs)
     self.sync_stage = sync_stage
-    self.buildbucket_client = self.GetBuildbucketClient()
+    self.buildbucket_client = buildbucket_v2.BuildbucketV2()
 
   def _FindMostRecentBotId(self, build_config, branch):
-    buildbucket_client = self.GetBuildbucketClient()
-    if not buildbucket_client:
+    if not self.buildbucket_client:
       logging.info('No buildbucket_client, no bot found.')
       return None
 
-    previous_builds = buildbucket_client.SearchAllBuilds(
-        self._run.options.debug,
-        buckets=constants.ACTIVE_BUCKETS,
-        limit=1,
-        tags=['cbb_config:%s' % build_config,
-              'cbb_branch:%s' % branch],
-        status=constants.BUILDBUCKET_BUILDER_STATUS_COMPLETED)
+    builder = builder_pb2.BuilderID(project='chromeos',
+                                  bucket='general')
+    tags = [common_pb2.StringPair(key='cbb_config',
+                                  value=build_config),
+            common_pb2.StringPair(key='cbb_branch',
+                                  value=branch)]
+    predicate = builds_service_pb2.BuildPredicate(
+      builder=builder,
+      status=common_pb2.SUCCESS,
+      tags=tags)
+    field_mask = field_mask_pb2.FieldMask(
+      paths=['builds.*.infra.swarming.bot_dimensions.*']
+    )
+    previous_builds = self.buildbucket_client.SearchBuild(
+        build_predicate=predicate,
+        fields=field_mask,
+        page_size=1)
 
     if not previous_builds:
       logging.info('No previous build found, no bot found.')
       return None
 
-    bot_id = buildbucket_lib.GetBotId(previous_builds[0])
+    bot_id = buildbucket_v2.GetBotId(previous_builds[0])
     if not bot_id:
       logging.info('Previous build has no bot.')
       return None
 
     return bot_id
 
-  def _CreateRequestBuild(self,
+  def _CreateScheduledBuild(self,
                           build_name,
                           build_config,
                           master_build_id,
                           master_buildbucket_id,
-                          requested_bot):
+                          requested_bot=None):
     if build_config.build_affinity:
       requested_bot = self._FindMostRecentBotId(build_config.name,
                                                 self._run.manifest_branch)
       logging.info('Requesting build affinity for %s against %s',
                    build_config.name, requested_bot)
-
     cbb_extra_args = ['--buildbot']
     if master_buildbucket_id is not None:
       cbb_extra_args.append('--master-buildbucket-id')
@@ -99,11 +112,11 @@
                                   master_build_id,
                                   master_buildbucket_id,
                                   dryrun=False):
-    """Send a Put slave build request to Buildbucket.
+    """Scehdule a build within Buildbucket.
 
     Args:
-      build_name: Slave build name to put to Buildbucket.
-      build_config: Slave build config to put to Buildbucket.
+      build_name: Slave build name to schedule.
+      build_config: Slave build config.
       master_build_id: CIDB id of the master scheduling the slave build.
       master_buildbucket_id: buildbucket id of the master scheduling the
                              slave build.
@@ -114,21 +127,31 @@
         buildbucket_id
         created_ts
     """
-    requested_bot = None
-    request = self._CreateRequestBuild(
+    request = self._CreateScheduledBuild(
         build_name,
         build_config,
         master_build_id,
-        master_buildbucket_id,
-        requested_bot
-    )
-    result = request.Submit(dryrun=dryrun)
+        master_buildbucket_id
+    ).CreateBuildRequest()
+
+    if dryrun:
+      return (str(master_build_id), '1')
+
+    result = self.buildbucket_client.ScheduleBuild(
+      request_id=str(request['request_id']),
+      builder=request['builder'],
+      properties=request['properties'],
+      tags=request['tags'],
+      dimensions=request['dimensions'])
 
     logging.info('Build_name %s buildbucket_id %s created_timestamp %s',
-                 result.build_config, result.buildbucket_id, result.created_ts)
-    logging.PrintBuildbotLink(result.build_config, result.url)
+                 build_config, result.id,
+                 result.create_time.ToJsonString())
+    logging.PrintBuildbotLink(build_config,
+                             '{}{}'.format(constants.CHROMEOS_MILO_HOST,
+                                           result.id))
 
-    return (result.buildbucket_id, result.created_ts)
+    return (result.id, result.create_time.ToJsonString())
 
   def ScheduleSlaveBuildsViaBuildbucket(self,
                                         important_only=False,
@@ -166,12 +189,16 @@
     slave_config_map = self._GetSlaveConfigMap(important_only)
     for slave_config_name, slave_config in sorted(slave_config_map.items()):
       try:
-        buildbucket_id, created_ts = self.PostSlaveBuildToBuildbucket(
-            slave_config_name,
-            slave_config,
-            build_id,
-            master_buildbucket_id,
-            dryrun=dryrun)
+        if dryrun:
+          buildbucket_id = '1'
+          created_ts = '1'
+        else:
+          buildbucket_id, created_ts = self.PostSlaveBuildToBuildbucket(
+              slave_config_name,
+              slave_config,
+              build_id,
+              master_buildbucket_id,
+              dryrun=dryrun)
         request_reason = None
 
         if slave_config.important:
diff --git a/cbuildbot/stages/scheduler_stages_unittest.py b/cbuildbot/stages/scheduler_stages_unittest.py
index b144743..3113311 100644
--- a/cbuildbot/stages/scheduler_stages_unittest.py
+++ b/cbuildbot/stages/scheduler_stages_unittest.py
@@ -13,8 +13,6 @@
 from chromite.cbuildbot.stages import generic_stages_unittest
 from chromite.cbuildbot.stages import scheduler_stages
 from chromite.cbuildbot import cbuildbot_run
-from chromite.lib import auth
-from chromite.lib import buildbucket_lib
 from chromite.lib import cidb
 from chromite.lib import config_lib
 from chromite.lib import constants
@@ -22,22 +20,12 @@
 from chromite.lib.buildstore import FakeBuildStore
 
 
-class ScheduleSalvesStageTest(generic_stages_unittest.AbstractStageTestCase):
+class ScheduleSlavesStageTest(generic_stages_unittest.AbstractStageTestCase):
   """Unit tests for ScheduleSalvesStage."""
 
   BOT_ID = 'master-release'
 
   def setUp(self):
-    self.PatchObject(buildbucket_lib, 'GetServiceAccount',
-                     return_value='server_account')
-    self.PatchObject(auth.AuthorizedHttp, '__init__',
-                     return_value=None)
-    self.PatchObject(buildbucket_lib.BuildbucketClient,
-                     '_GetHost',
-                     return_value=buildbucket_lib.BUILDBUCKET_TEST_HOST)
-    self.PatchObject(buildbucket_lib.BuildbucketClient,
-                     'SendBuildbucketRequest',
-                     return_value=None)
     # pylint: disable=protected-access
     self.PatchObject(cbuildbot_run._BuilderRunBase,
                      'GetVersion',
@@ -62,8 +50,12 @@
         boards=['board_A'], build_type='paladin')
 
     stage = self.ConstructStage()
+    self.PatchObject(scheduler_stages.ScheduleSlavesStage,
+                     '_FindMostRecentBotId',
+                     return_value='chromeos-ci-test-1')
     # pylint: disable=protected-access
-    request = stage._CreateRequestBuild('child', config, 0, 'master_bb_0', None)
+    request = stage._CreateScheduledBuild('child', config, 0,
+                                          'master_bb_0', None)
     self.assertEqual(request.build_config, 'child')
     self.assertEqual(request.master_buildbucket_id, 'master_bb_0')
     self.assertEqual(request.extra_args, ['--buildbot',
@@ -79,10 +71,14 @@
         boards=['board_A'], build_type='paladin')
 
     stage = self.ConstructStage()
+    self.PatchObject(scheduler_stages.ScheduleSlavesStage,
+                     '_FindMostRecentBotId',
+                     return_value='chromeos-ci-test-1')
     # Set the annealing snapshot revision to pass to the child builders.
     # pylint: disable=protected-access
     stage._run.options.cbb_snapshot_revision = 'hash1234'
-    request = stage._CreateRequestBuild('child', config, 0, 'master_bb_1', None)
+    request = stage._CreateScheduledBuild('child', config, 0,
+                                          'master_bb_1', None)
     self.assertEqual(request.build_config, 'child')
     self.assertEqual(request.master_buildbucket_id, 'master_bb_1')
     expected_extra_args = ['--buildbot',
@@ -90,68 +86,6 @@
                            '--cbb_snapshot_revision', 'hash1234']
     self.assertEqual(request.extra_args, expected_extra_args)
 
-  def testPerformStage(self):
-    """Test PerformStage."""
-    stage = self.ConstructStage()
-    self.PatchObject(buildbucket_lib.BuildbucketClient,
-                     '_GetHost',
-                     return_value=buildbucket_lib.BUILDBUCKET_TEST_HOST)
-
-    stage.PerformStage()
-
-  def testScheduleImportantSlaveBuildsFailure(self):
-    """Test ScheduleSlaveBuilds with important slave failures."""
-    stage = self.ConstructStage()
-    self.PatchObject(scheduler_stages.ScheduleSlavesStage,
-                     'PostSlaveBuildToBuildbucket',
-                     side_effect=buildbucket_lib.BuildbucketResponseException)
-
-    slave_config_map_1 = {
-        'slave_external': config_lib.BuildConfig(important=True)}
-    self.PatchObject(generic_stages.BuilderStage, '_GetSlaveConfigMap',
-                     return_value=slave_config_map_1)
-    self.assertRaises(
-        buildbucket_lib.BuildbucketResponseException,
-        stage.ScheduleSlaveBuildsViaBuildbucket,
-        important_only=False, dryrun=True)
-
-  def testScheduleUnimportantSlaveBuildsFailure(self):
-    """Test ScheduleSlaveBuilds with unimportant slave failures."""
-    stage = self.ConstructStage()
-    self.PatchObject(scheduler_stages.ScheduleSlavesStage,
-                     'PostSlaveBuildToBuildbucket',
-                     side_effect=buildbucket_lib.BuildbucketResponseException)
-
-    slave_config_map = {
-        'slave_external': config_lib.BuildConfig(important=False),}
-    self.PatchObject(generic_stages.BuilderStage, '_GetSlaveConfigMap',
-                     return_value=slave_config_map)
-    stage.ScheduleSlaveBuildsViaBuildbucket(important_only=False, dryrun=True)
-
-    scheduled_slaves = self._run.attrs.metadata.GetValue(
-        constants.METADATA_SCHEDULED_IMPORTANT_SLAVES)
-    self.assertEqual(len(scheduled_slaves), 0)
-    unscheduled_slaves = self._run.attrs.metadata.GetValue(
-        constants.METADATA_UNSCHEDULED_SLAVES)
-    self.assertEqual(len(unscheduled_slaves), 1)
-
-  def testScheduleSlaveBuildsFailure(self):
-    """Test ScheduleSlaveBuilds with mixed slave failures."""
-    stage = self.ConstructStage()
-    self.PatchObject(scheduler_stages.ScheduleSlavesStage,
-                     'PostSlaveBuildToBuildbucket',
-                     side_effect=buildbucket_lib.BuildbucketResponseException)
-
-    slave_config_map = {
-        'slave_1': config_lib.BuildConfig(important=False),
-        'slave_2': config_lib.BuildConfig(important=True),}
-    self.PatchObject(generic_stages.BuilderStage, '_GetSlaveConfigMap',
-                     return_value=slave_config_map)
-    self.assertRaises(
-        buildbucket_lib.BuildbucketResponseException,
-        stage.ScheduleSlaveBuildsViaBuildbucket,
-        important_only=False, dryrun=True)
-
   def testScheduleSlaveBuildsSuccess(self):
     """Test ScheduleSlaveBuilds with success."""
     stage = self.ConstructStage()
@@ -180,9 +114,6 @@
 
   def testPostSlaveBuildToBuildbucket(self):
     """Test PostSlaveBuildToBuildbucket on builds with a single board."""
-    content = {'build': {'id': 'bb_id_1', 'created_ts': 1}}
-    self.PatchObject(buildbucket_lib.BuildbucketClient, 'PutBuildRequest',
-                     return_value=content)
     slave_config = config_lib.BuildConfig(
         name='slave',
         build_affinity=True,
@@ -191,8 +122,12 @@
         boards=['board_A'], build_type='paladin')
 
     stage = self.ConstructStage()
+    self.PatchObject(scheduler_stages.ScheduleSlavesStage,
+                     '_FindMostRecentBotId',
+                     return_value='chromeos-ci-test-1')
+
     buildbucket_id, created_ts = stage.PostSlaveBuildToBuildbucket(
         'slave', slave_config, 0, 'master_bb_id', dryrun=True)
 
-    self.assertEqual(buildbucket_id, 'bb_id_1')
-    self.assertEqual(created_ts, 1)
+    self.assertEqual(buildbucket_id, '0')
+    self.assertEqual(created_ts, '1')
diff --git a/cli/cros/cros_tryjob.py b/cli/cros/cros_tryjob.py
index 7423c21..2e3066f 100644
--- a/cli/cros/cros_tryjob.py
+++ b/cli/cros/cros_tryjob.py
@@ -352,7 +352,8 @@
     print('Tryjob submitted!')
     print('To view your tryjobs, visit:')
     for r in results:
-      print('  %s' % r.url)
+      print('{}{}'.format(constants.CHROMEOS_MILO_HOST,
+                                           r.id))
 
 
 def AdjustOptions(options):
diff --git a/config/luci-scheduler.cfg b/config/luci-scheduler.cfg
index b9292c0..03b1596 100644
--- a/config/luci-scheduler.cfg
+++ b/config/luci-scheduler.cfg
@@ -1727,11 +1727,11 @@
     server: "cr-buildbucket.appspot.com"
     bucket: "luci.chromeos.general"
     builder: "Factory"
-    tags: "cbb_branch:master"
+    tags: "cbb_branch:main"
     tags: "cbb_config:firmware-icarus-13854.B-firmwarebranch"
     tags: "cbb_display_label:firmware"
     tags: "cbb_workspace_branch:firmware-icarus-13854.B"
-    properties: "cbb_branch:master"
+    properties: "cbb_branch:main"
     properties: "cbb_config:firmware-icarus-13854.B-firmwarebranch"
     properties: "cbb_display_label:firmware"
     properties: "cbb_workspace_branch:firmware-icarus-13854.B"
diff --git a/lib/buildbucket_lib.py b/lib/buildbucket_lib.py
index 2ca23ef..a5ca1e5 100644
--- a/lib/buildbucket_lib.py
+++ b/lib/buildbucket_lib.py
@@ -540,23 +540,3 @@
       result.append(tag_pair[1])
 
   return result
-
-def GetResultDetails(content):
-  """Return parsed result_details_json blob, or Nones."""
-  json_blob = GetNestedAttr(content, ['result_details_json'])
-  return json.loads(json_blob) if json_blob else None
-
-def GetBotId(content):
-  """Return the bot id that ran a build, or None."""
-  result_details = GetResultDetails(content)
-  if not result_details:
-    return None
-
-  # This produces a list of bot_ids for each build (or None).
-  # I don't think there can ever be more than one entry in the list, but
-  # could be zero.
-  bot_ids = GetNestedAttr(result_details, ['swarming', 'bot_dimensions', 'id'])
-  if not bot_ids:
-    return None
-
-  return bot_ids[0]
diff --git a/lib/buildbucket_lib_unittest.py b/lib/buildbucket_lib_unittest.py
index 23fbd18..33faa68 100644
--- a/lib/buildbucket_lib_unittest.py
+++ b/lib/buildbucket_lib_unittest.py
@@ -524,23 +524,3 @@
         auth.GetAccessToken, buildbucket_lib.BUILDBUCKET_HOST,
         service_account_json=buildbucket_lib.GetServiceAccount(
             constants.CHROMEOS_SERVICE_ACCOUNT))
-
-  @cros_test_lib.NetworkTest()
-  def testSearchAndExtractBotIds(self):
-    buildbucket_client = self.getProdClient()
-    self.assertTrue(buildbucket_client)
-
-    previous_builds = buildbucket_client.SearchAllBuilds(
-        False,
-        buckets=constants.ACTIVE_BUCKETS,
-        limit=10,
-        tags=['cbb_config:success-build',
-              'cbb_branch:main'],
-        status=constants.BUILDBUCKET_BUILDER_STATUS_COMPLETED)
-
-    self.assertEqual(len(previous_builds), 10)
-
-    # This test would fail, if the search results included buildbot builds.
-    for b in previous_builds:
-      self.assertTrue(buildbucket_lib.GetResultDetails(b))
-      self.assertTrue(buildbucket_lib.GetBotId(b).startswith('swarm-cros'))
diff --git a/lib/buildbucket_v2.py b/lib/buildbucket_v2.py
index 8d216fe..1321a27 100644
--- a/lib/buildbucket_v2.py
+++ b/lib/buildbucket_v2.py
@@ -20,16 +20,15 @@
 from google.protobuf import field_mask_pb2
 from six.moves import http_client as httplib
 
+from infra_libs.buildbucket.proto import builder_pb2, builds_service_pb2
+from infra_libs.buildbucket.proto import builds_service_prpc_pb2, common_pb2
+
 from chromite.lib import constants
 from chromite.lib import cros_logging as logging
 from chromite.lib import retry_util
 from chromite.lib.luci import utils
 from chromite.lib.luci.prpc.client import Client, ProtocolError
 
-from infra_libs.buildbucket.proto import builds_service_pb2
-from infra_libs.buildbucket.proto import builder_pb2, common_pb2
-from infra_libs.buildbucket.proto import builds_service_prpc_pb2
-
 
 assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
 
@@ -52,6 +51,46 @@
     68: constants.BUILDER_STATUS_ABORTED
 }
 
+def GetStringPairValue(content, path, key, default=None):
+  """Get the value of a repeated StringValue pair.
+
+  Get the (nested) value from a nested dict.
+
+  Args:
+    content: A dict of (nested) attributes.
+    path: String list presenting the (nested) attribute to get.
+    key: String representing which key to find.
+    default: Default value to return if the attribute doesn't exist.
+
+  Returns:
+    The corresponding value if the attribute exists; else, default.
+  """
+  assert isinstance(path, list), 'nested_attr must be a list.'
+
+  if content is None:
+    return default
+
+  assert isinstance(content, dict), 'content must be a dict.'
+
+  value = None
+  path_value = content
+  for attr in path:
+    assert isinstance(attr, str), 'attribute name must be a string.'
+
+    if not isinstance(path_value, dict):
+      return default
+
+    path_value = path_value.get(attr, default)
+
+  for sp in path_value:
+    dimensions_kv = list(sp.items())
+    for i, (k, v) in enumerate(dimensions_kv):
+      dimensions_kv[i] = (k, [v,])
+      if v == key:
+        if len(dimensions_kv) >= i+1:
+          return dimensions_kv[i+1][i+1]
+  return value
+
 def UpdateSelfBuildPropertiesNonBlocking(key, value):
   """Updates the build.output.properties with key:value through a service.
 
@@ -227,6 +266,25 @@
   return common_pb2.TimeRange(start_time=start_timestamp,
                               end_time=end_timestamp)
 
+def GetBotId(build):
+  """Return the bot id that ran a build, or None.
+
+  Args:
+    build: BuildbucketV2 build
+
+  Returns:
+    hostname: Swarming hostname
+  """
+  # This produces a list of bot_ids for each build (or None).
+  # I don't think there can ever be more than one entry in the list, but
+  # could be zero.
+  bot_id = GetStringPairValue(build, ['infra', 'swarming', 'botDimensions'],
+                             'id')
+  if not bot_id:
+    return None
+
+  return bot_id
+
 class BuildbucketV2(object):
   """Connection to Buildbucket V2 database."""
 
@@ -347,19 +405,19 @@
   @retry_util.WithRetry(max_retry=5, sleep=20.0,
                         exception=httplib.ResponseNotReady)
   def ScheduleBuild(self, request_id, template_build_id=None,
-                    builder=None, experiments=None,
-                    properties=None, gerrit_changes=None,
-                    tags=None, fields=None, critical=True):
-    """GetBuild call of a specific build with buildbucket_id.
+                    builder=None, properties=None,
+                    gerrit_changes=None, tags=None,
+                    dimensions=None, fields=None, critical=True):
+    """ScheduleBuild call of a specific build with buildbucket_id.
 
     Args:
       request_id: unique string used to prevent duplicates.
       template_build_id: ID of a build to retry.
       builder: Tuple (builder.project, builder.bucket) defines build ACL
-      experiments: map of string, bool of experiments to set.
       properties: properties key in parameters_json
       gerrit_changes: Repeated GerritChange message type.
       tags: repeated StringPair of Build.tags to associate with build.
+      dimensions: RequestedDimension Swarming dimension to override in config.
       fields: fields to include in the response.
       critical: bool for build.critical.
 
@@ -371,10 +429,10 @@
         request_id=request_id,
         template_build_id=template_build_id if template_build_id else None,
         builder=builder,
-        experiments=experiments if experiments else None,
         properties=properties if properties else None,
         gerrit_changes=gerrit_changes if gerrit_changes else None,
         tags=tags if tags else None,
+        dimensions=dimensions if dimensions else None,
         fields=field_mask_pb2.FieldMask(paths=[fields]) if fields else None,
         critical=common_pb2.YES if critical else common_pb2.NO)
     return self.client.ScheduleBuild(schedule_build_request)
diff --git a/lib/buildbucket_v2_unittest.py b/lib/buildbucket_v2_unittest.py
index 1a58588..d382a17 100644
--- a/lib/buildbucket_v2_unittest.py
+++ b/lib/buildbucket_v2_unittest.py
@@ -22,12 +22,72 @@
 from chromite.lib.luci.prpc.client import Client, ProtocolError
 
 from infra_libs.buildbucket.proto import build_pb2, builds_service_pb2
-from infra_libs.buildbucket.proto import builder_pb2, common_pb2
-from infra_libs.buildbucket.proto import step_pb2
+from infra_libs.buildbucket.proto import common_pb2
+from infra_libs.buildbucket.proto import builder_pb2, step_pb2
 
 
 assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
 
+SUCCESS_BUILD = {'infra': {
+                    'swarming': {
+                        'botDimensions': [
+                          {
+                              'key': 'cores',
+                              'value': '32'
+                          },
+                          {
+                              'key': 'cpu',
+                              'value': 'x86'
+                          },
+                          {
+                              'key': 'cpu',
+                              'value': 'x86-64'
+                          },
+                          {
+                              'key': 'cpu',
+                              'value': 'x86-64-Haswell_GCE'
+                          },
+                          {
+                              'key': 'cpu',
+                              'value': 'x86-64-avx2'
+                          },
+                          {
+                              'key': 'gce',
+                              'value': '1'
+                          },
+                          {
+                              'key': 'gcp',
+                              'value': 'chromeos-bot'
+                          },
+                          {
+                              'key': 'id',
+                              'value': 'chromeos-ci-test-bot'
+                          },
+                          {
+                              'key': 'image',
+                              'value': 'chromeos-bionic-21021400-a1c0533ad76'
+                          },
+                          {
+                              'key': 'machine_type',
+                              'value': 'e2-standard-32'
+                          },
+                          {
+                              'key': 'pool',
+                              'value': 'ChromeOS'
+                          },
+                          {
+                              'key': 'role',
+                              'value': 'legacy-release'
+                          },
+                          {
+                              'key': 'zone',
+                              'value': 'us-central1-b'
+                          }
+                      ]
+                  }
+              }
+        }
+
 
 class BuildbucketV2Test(cros_test_lib.MockTestCase):
   """Tests for buildbucket_v2."""
@@ -157,10 +217,10 @@
         request_id='1234',
         template_build_id=None,
         builder=fake_builder,
-        experiments=None,
         properties=None,
         gerrit_changes=None,
         tags=fake_tag,
+        dimensions=None,
         fields=fake_field_mask,
         critical=common_pb2.YES)
     self.schedule_build_function.assert_called_with(
@@ -579,3 +639,24 @@
     result = buildbucket_v2.DateToTimeRange(end_date=date_example)
     self.assertEqual(result.end_time.seconds, 1555372740)
     self.assertEqual(result.start_time.seconds, 0)
+
+  def testGetStringPairValue(self):
+    bot_id = buildbucket_v2.GetStringPairValue(
+      SUCCESS_BUILD,
+      ['infra', 'swarming', 'botDimensions'],
+      'id')
+    self.assertEqual(bot_id, 'chromeos-ci-test-bot')
+    pool = buildbucket_v2.GetStringPairValue(
+      SUCCESS_BUILD,
+      ['infra', 'swarming', 'botDimensions'],
+      'pool')
+    self.assertEqual(pool, 'ChromeOS')
+    role = buildbucket_v2.GetStringPairValue(
+      SUCCESS_BUILD,
+      ['infra', 'swarming', 'botDimensions'],
+      'role')
+    self.assertEqual(role, 'legacy-release')
+
+  def testGetBotId(self):
+    bot_id = buildbucket_v2.GetBotId(SUCCESS_BUILD)
+    self.assertEqual(bot_id, 'chromeos-ci-test-bot')
diff --git a/lib/constants.py b/lib/constants.py
index 20b59eb..79f5ef9 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -80,7 +80,7 @@
 CIDB_PROD_BOT_CREDS = os.path.join(HOME_DIRECTORY, '.cidb_creds',
                                    'prod_cidb_bot')
 CIDB_DEBUG_BOT_CREDS = os.path.join(HOME_DIRECTORY, '.cidb_creds',
-                                    'debug_cidb_bot')
+                                   'debug_cidb_bot')
 
 # Crash Server upload API key.
 CRASH_API_KEY = os.path.join('/', 'creds', 'api_keys',
@@ -922,7 +922,10 @@
 # Buildbucket buckets
 CHROMEOS_RELEASE_BUILDBUCKET_BUCKET = 'master.chromeos_release'
 CHROMEOS_BUILDBUCKET_BUCKET = 'master.chromeos'
-INTERNAL_SWARMING_BUILDBUCKET_BUCKET = 'luci.chromeos.general'
+INTERNAL_SWARMING_BUILDBUCKET_BUCKET = 'general'
+
+# Milo URL
+CHROMEOS_MILO_HOST = 'https://ci.chromium.org/b/'
 
 ACTIVE_BUCKETS = [
     CHROMEOS_RELEASE_BUILDBUCKET_BUCKET,
diff --git a/lib/request_build.py b/lib/request_build.py
index 93f5ccc..429c811 100644
--- a/lib/request_build.py
+++ b/lib/request_build.py
@@ -8,16 +8,17 @@
 from __future__ import print_function
 
 import collections
-import json
 import sys
+import uuid
 
-from chromite.lib import auth
-from chromite.lib import buildbucket_lib
+from google.protobuf.struct_pb2 import Struct
+from google.protobuf import duration_pb2
+from infra_libs.buildbucket.proto import build_pb2, builder_pb2
+from infra_libs.buildbucket.proto import common_pb2
+
+from chromite.lib import buildbucket_v2
 from chromite.lib import config_lib
 from chromite.lib import constants
-from chromite.lib import cros_logging as logging
-from chromite.lib import pformat
-from chromite.lib import uri_lib
 
 
 assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
@@ -60,8 +61,8 @@
                build_config,
                luci_builder=None,
                display_label=None,
-               branch='main',
-               extra_args=(),
+               branch='master',
+               extra_args=None,
                extra_properties=None,
                user_email=None,
                email_template=None,
@@ -121,11 +122,11 @@
     self.master_buildbucket_id = master_buildbucket_id
     self.requested_bot = requested_bot
 
-  def _GetRequestBody(self):
-    """Generate the request body for a swarming buildbucket request.
+  def CreateBuildRequest(self):
+    """Generate the details for Buildbucket V2 request.
 
     Returns:
-      buildbucket request properties as a python dict.
+      Parameters for V2 ScheduleBuild.
     """
     tags = {
         # buildset identifies a group of related builders.
@@ -157,91 +158,57 @@
     # Recipe expects it to be a string anyway.
     tags = {k: str(v) for k, v in tags.items() if v}
 
-    # All tags should also be listed as properties.
-    properties = tags.copy()
-    properties['cbb_extra_args'] = self.extra_args
-
-    parameters = {
-        'builder_name': self.luci_builder,
-        'properties': properties,
-    }
-
+    properties = Struct()
+    properties.update({k: str(v) for k, v in tags.items() if v})
+    properties.update({'cbb_extra_args': self.extra_args})
     if self.user_email:
-      parameters['email_notify'] = [{
+      properties.update({'email_notify': [{
           'email': self.user_email,
           'template': self.email_template,
-      }]
+          }]
+      })
+
+    tags_proto = []
+    for k, v in sorted(tags.items()):
+      if v:
+        tags_proto.append(common_pb2.StringPair(key=k,value=v))
+    dimensions = []
 
     # If a specific bot was requested, pass along the request with a
     # 240 second (4 minute) timeout. If the bot isn't available, we
     # will fall back to the general builder restrictions (probably
     # based on role).
     if self.requested_bot:
-      parameters['swarming'] = {
-          'override_builder_cfg': {
-              'dimensions': [
-                  '240:id:%s' % self.requested_bot,
-              ]
-          }
-      }
-
+      dimensions = [common_pb2.RequestedDimension(
+        key='id',
+        value=self.requested_bot,
+        expiration=duration_pb2.Duration(seconds=240))]
     return {
-        'bucket': self.bucket,
-        'parameters_json': pformat.json(parameters, compact=True),
-        # These tags are indexed and searchable in buildbucket.
-        'tags': ['%s:%s' % (k, tags[k]) for k in sorted(tags.keys())],
+        'request_id': uuid.uuid1(),
+        'builder': builder_pb2.BuilderID(project='chromeos',
+                                      bucket=self.bucket,
+                                      builder=self.luci_builder),
+        'properties': properties,
+        'tags': tags_proto,
+        'dimensions': dimensions if dimensions else None,
     }
 
-  def _PutConfigToBuildBucket(self, buildbucket_client, dryrun):
-    """Put the tryjob request to buildbucket.
-
-    Args:
-      buildbucket_client: The buildbucket client instance.
-      dryrun: bool controlling dryrun behavior.
-
-    Returns:
-      ScheduledBuild describing the scheduled build.
-
-    Raises:
-      RemoteRequestFailure.
-    """
-    request_body = self._GetRequestBody()
-    content = buildbucket_client.PutBuildRequest(
-        json.dumps(request_body), dryrun)
-
-    if buildbucket_lib.GetNestedAttr(content, ['error']):
-      raise RemoteRequestFailure(
-          'buildbucket error.\nReason: %s\n Message: %s' %
-          (buildbucket_lib.GetErrorReason(content),
-           buildbucket_lib.GetErrorMessage(content)))
-
-    buildbucket_id = buildbucket_lib.GetBuildId(content)
-    url = uri_lib.ConstructMiloBuildUri(buildbucket_id)
-    created_ts = buildbucket_lib.GetBuildCreated_ts(content)
-
-    result = ScheduledBuild(
-        self.bucket, buildbucket_id, self.build_config, url, created_ts)
-
-    logging.info(self.BUILDBUCKET_PUT_RESP_FORMAT, result._asdict())
-
-    return result
-
-  def Submit(self, testjob=False, dryrun=False):
+  def Submit(self, dryrun=False):
     """Submit the tryjob through Git.
 
     Args:
-      testjob: Submit job to the test branch of the tryjob repo.  The tryjob
-               will be ignored by production master.
       dryrun: Setting to true will run everything except the final submit step.
 
     Returns:
       A ScheduledBuild instance.
     """
-    host = (buildbucket_lib.BUILDBUCKET_TEST_HOST if testjob
-            else buildbucket_lib.BUILDBUCKET_HOST)
-    buildbucket_client = buildbucket_lib.BuildbucketClient(
-        auth.GetAccessToken, host,
-        service_account_json=buildbucket_lib.GetServiceAccount(
-            constants.CHROMEOS_SERVICE_ACCOUNT))
-
-    return self._PutConfigToBuildBucket(buildbucket_client, dryrun)
+    buildbucket_client = buildbucket_v2.BuildbucketV2()
+    request = self.CreateBuildRequest()
+    if dryrun:
+      return build_pb2.Build(id='1')
+    return buildbucket_client.ScheduleBuild(
+      request_id=str(request['request_id']),
+      builder=request['builder'],
+      properties=request['properties'],
+      tags=request['tags'],
+      dimensions=request['dimensions'])
diff --git a/lib/request_build_unittest.py b/lib/request_build_unittest.py
index 2033d2c..b977942 100644
--- a/lib/request_build_unittest.py
+++ b/lib/request_build_unittest.py
@@ -7,13 +7,13 @@
 
 from __future__ import print_function
 
-import json
 import sys
 
-import mock
+from google.protobuf.struct_pb2 import Struct
 
-from chromite.lib import auth
-from chromite.lib import buildbucket_lib
+from infra_libs.buildbucket.proto import build_pb2, builder_pb2, common_pb2
+
+from chromite.lib import buildbucket_v2
 from chromite.lib import config_lib
 from chromite.lib import constants
 from chromite.lib import cros_test_lib
@@ -65,8 +65,8 @@
     return request_build.RequestBuild(
         build_config=self.UNKNOWN_CONFIG,
         display_label=self.DISPLAY_LABEL,
-        branch='main',
-        extra_args=(),
+        branch='master',
+        extra_args=[],
         user_email='default_email',
         master_buildbucket_id=None)
 
@@ -78,12 +78,12 @@
     # This mocks out the class, then creates a return_value for a function on
     # instances of it. We do this instead of just mocking out the function to
     # ensure not real network requests are made in other parts of the class.
-    client_mock = self.PatchObject(buildbucket_lib, 'BuildbucketClient')
-    client_mock().PutBuildRequest.return_value = {
-        'build': {'id': 'fake_buildbucket_id'}
-    }
+    client_mock = self.PatchObject(buildbucket_v2, 'BuildbucketV2')
+    client_mock().ScheduleBuilder.return_value = build_pb2.Build(
+        id=12345,
+    )
 
-  def testMinRequestBody(self):
+  def testMinCreateRequestBody(self):
     """Verify our request body with min options."""
     job = self._CreateJobMin()
 
@@ -91,29 +91,24 @@
     self.assertEqual(job.luci_builder, config_lib.LUCI_BUILDER_TRY)
     self.assertEqual(job.display_label, config_lib.DISPLAY_LABEL_TRYJOB)
 
-    body = job._GetRequestBody()
+    body = job.CreateBuildRequest()
 
-    self.assertEqual(body, {
-        'parameters_json': mock.ANY,
-        'bucket': 'luci.chromeos.general',
-        'tags': [
-            'cbb_branch:main',
-            'cbb_config:amd64-generic-paladin-tryjob',
-            'cbb_display_label:tryjob',
-        ]
-    })
-
-    parameters_parsed = json.loads(body['parameters_json'])
-
-    self.assertEqual(parameters_parsed, {
-        u'builder_name': u'Try',
-        u'properties': {
-            u'cbb_branch': u'main',
-            u'cbb_config': u'amd64-generic-paladin-tryjob',
-            u'cbb_display_label': u'tryjob',
-            u'cbb_extra_args': [],
-        }
-    })
+    self.assertEqual(builder_pb2.BuilderID(
+        project='chromeos',
+        bucket=constants.INTERNAL_SWARMING_BUILDBUCKET_BUCKET,
+        builder=config_lib.LUCI_BUILDER_TRY),
+        body['builder'])
+    self.assertEqual(
+        [common_pb2.StringPair(
+             key='cbb_branch',
+             value='master'),
+         common_pb2.StringPair(
+             key='cbb_config',
+             value='amd64-generic-paladin-tryjob'),
+         common_pb2.StringPair(
+             key='cbb_display_label',
+             value='tryjob'),
+        ], body['tags'])
 
   def testMaxRequestBody(self):
     """Verify our request body with max options."""
@@ -123,90 +118,110 @@
     self.assertEqual(job.luci_builder, self.LUCI_BUILDER)
     self.assertEqual(job.display_label, 'display')
 
-    body = job._GetRequestBody()
+    body = job.CreateBuildRequest()
 
-    self.assertEqual(body, {
-        'parameters_json': mock.ANY,
-        'bucket': self.TEST_BUCKET,
-        'tags': [
-            'buildset:cros/parent_buildbucket_id/master_bb_id',
-            'cbb_branch:test-branch',
-            'cbb_config:amd64-generic-paladin',
-            'cbb_display_label:display',
-            'cbb_email:explicit_email',
-            'cbb_master_build_id:master_cidb_id',
-            'cbb_master_buildbucket_id:master_bb_id',
-            'full_version:R84-13099.77.0',
-            'master:False',
-        ]
+    self.assertEqual(builder_pb2.BuilderID(
+        project='chromeos',
+        bucket=self.TEST_BUCKET,
+        builder=self.LUCI_BUILDER),
+        body['builder'])
+
+    self.assertEqual(
+        [common_pb2.StringPair(
+            key='buildset',
+            value='cros/parent_buildbucket_id/master_bb_id'),
+         common_pb2.StringPair(
+             key='cbb_branch',
+             value='test-branch'),
+         common_pb2.StringPair(
+             key='cbb_config',
+             value='amd64-generic-paladin'),
+         common_pb2.StringPair(
+             key='cbb_display_label',
+             value='display'),
+         common_pb2.StringPair(
+             key='cbb_email',
+             value='explicit_email'),
+         common_pb2.StringPair(
+             key='cbb_master_build_id',
+             value='master_cidb_id'),
+         common_pb2.StringPair(
+             key='cbb_master_buildbucket_id',
+             value='master_bb_id'),
+         common_pb2.StringPair(
+             key='full_version',
+             value='R84-13099.77.0'),
+         common_pb2.StringPair(
+             key='master',
+             value='False'),
+        ],
+        body['tags'],
+    )
+    props = {
+        u'buildset': u'cros/parent_buildbucket_id/master_bb_id',
+        u'cbb_branch': u'test-branch',
+        u'cbb_config': u'amd64-generic-paladin',
+        u'cbb_display_label': u'display',
+        u'cbb_email': u'explicit_email',
+        u'cbb_master_build_id': u'master_cidb_id',
+        u'cbb_master_buildbucket_id': u'master_bb_id',
+        u'full_version': u'R84-13099.77.0',
+        u'master': u'False',
+    }
+    test_properties = Struct()
+    test_properties.update({k: str(v) for k, v in props.items() if v})
+    test_properties.update({'cbb_extra_args': [u'funky', u'cold', u'medina']})
+    test_properties.update({'email_notify': [{
+          'email': 'explicit_email',
+          'template': 'explicit_template',
+          }]
     })
-
-    parameters_parsed = json.loads(body['parameters_json'])
-
-    self.assertEqual(parameters_parsed, {
-        u'builder_name': u'luci_build',
-        u'email_notify': [{u'email': u'explicit_email',
-                           u'template': u'explicit_template'}],
-        u'properties': {
-            u'buildset': u'cros/parent_buildbucket_id/master_bb_id',
-            u'cbb_branch': u'test-branch',
-            u'cbb_config': u'amd64-generic-paladin',
-            u'cbb_display_label': u'display',
-            u'cbb_email': u'explicit_email',
-            u'cbb_extra_args': [u'funky', u'cold', u'medina'],
-            u'cbb_master_build_id': u'master_cidb_id',
-            u'cbb_master_buildbucket_id': u'master_bb_id',
-            u'full_version': u'R84-13099.77.0',
-            u'master': u'False',
-        },
-        u'swarming': {
-            u'override_builder_cfg': {
-                u'dimensions': [
-                    u'240:id:botname',
-                ]
-            }
-        }
-    })
+    self.assertEqual(
+        test_properties,
+        body['properties'],
+    )
 
   def testUnknownRequestBody(self):
     """Verify our request body with max options."""
-    body = self._CreateJobUnknown()._GetRequestBody()
+    job = self._CreateJobUnknown()
+    body = job.CreateBuildRequest()
 
-    self.assertEqual(body, {
-        'parameters_json': mock.ANY,
-        'bucket': 'luci.chromeos.general',
-        'tags': [
-            'cbb_branch:main',
-            'cbb_config:unknown-config',
-            'cbb_display_label:display',
-            'cbb_email:default_email',
-        ]
-    })
-
-    parameters_parsed = json.loads(body['parameters_json'])
-
-    self.assertEqual(parameters_parsed, {
-        u'builder_name': u'Try',
-        u'email_notify': [{u'email': u'default_email',
-                           u'template': u'default'}],
-        u'properties': {
-            u'cbb_branch': u'main',
-            u'cbb_config': u'unknown-config',
+    self.assertEqual(builder_pb2.BuilderID(
+        project='chromeos',
+        bucket=constants.INTERNAL_SWARMING_BUILDBUCKET_BUCKET,
+        builder=config_lib.LUCI_BUILDER_TRY),
+        body['builder'])
+    self.assertEqual(
+        [common_pb2.StringPair(
+            key='cbb_branch',
+            value='master'),
+         common_pb2.StringPair(
+             key='cbb_config',
+             value='unknown-config'),
+         common_pb2.StringPair(
+             key='cbb_display_label',
+             value='display'),
+         common_pb2.StringPair(
+             key='cbb_email',
+             value='default_email'),
+        ],
+        body['tags'],
+    )
+    props = {
             u'cbb_display_label': u'display',
+            u'cbb_branch': u'master',
+            u'cbb_config': u'unknown-config',
             u'cbb_email': u'default_email',
-            u'cbb_extra_args': [],
-        }
+    }
+    test_properties = Struct()
+    test_properties.update({k: str(v) for k, v in props.items() if v})
+    test_properties.update({'cbb_extra_args': job.extra_args})
+    test_properties.update({'email_notify': [{
+          'email': 'default_email',
+          'template': 'default',
+          }]
     })
-
-  def testMinDryRun(self):
-    """Do a dryrun of posting the request, min options."""
-    job = self._CreateJobMin()
-    job.Submit(testjob=True, dryrun=True)
-
-  def testMaxDryRun(self):
-    """Do a dryrun of posting the request, max options."""
-    job = self._CreateJobMax()
-    job.Submit(testjob=True, dryrun=True)
+    self.assertEqual(body['properties'], test_properties)
 
   def testLogGeneration(self):
     """Validate an import log message."""
@@ -226,136 +241,123 @@
 
 class RequestBuildHelperTestsNetork(RequestBuildHelperTestsBase):
   """Perform real buildbucket requests against a test instance."""
-
   def verifyBuildbucketRequest(self,
                                buildbucket_id,
                                expected_bucket,
                                expected_tags,
-                               expected_parameters):
+                               expected_properties):
     """Verify the contents of a push to the TEST buildbucket instance.
 
     Args:
       buildbucket_id: Id to verify.
       expected_bucket: Bucket the push was supposed to go to as a string.
-      expected_tags: List of buildbucket tags as strings.
-      expected_parameters: Python dict equivalent to json string in
-                           parameters_json.
+      expected_tags: List of buildbucket tags.
+      expected_properties: List of buildbucket properties.
     """
-    buildbucket_client = buildbucket_lib.BuildbucketClient(
-        auth.GetAccessToken, buildbucket_lib.BUILDBUCKET_TEST_HOST,
-        service_account_json=buildbucket_lib.GetServiceAccount(
-            constants.CHROMEOS_SERVICE_ACCOUNT))
+    client = buildbucket_v2.BuildbucketV2(test_env=True)
+    request = client.GetBuild(buildbucket_id)
 
-    request = buildbucket_client.GetBuildRequest(buildbucket_id, False)
-
-    self.assertEqual(request['build']['id'], buildbucket_id)
-    self.assertEqual(request['build']['bucket'], expected_bucket)
-    self.assertCountEqual(request['build']['tags'], expected_tags)
-
-    request_parameters = json.loads(request['build']['parameters_json'])
-    self.assertEqual(request_parameters, expected_parameters)
+    self.assertEqual(request.id, buildbucket_id)
+    self.assertEqual(request.builder.bucket, expected_bucket)
+    self.assertCountEqual(request.tags, expected_tags)
+    self.assertCountEqual(request.properties, expected_properties)
 
   @cros_test_lib.NetworkTest()
   def testMinTestBucket(self):
     """Talk to a test buildbucket instance with min job settings."""
     job = self._CreateJobMin()
-    result = job.Submit(testjob=True)
+    request = job.CreateBuildRequest()
+    client = buildbucket_v2.BuildbucketV2(test_env=True)
+    result = client.ScheduleBuild(
+        request_id=request.request_id,
+        builder=request.builder,
+        properties=request.properties,
+        tags=request.tags,
+        dimensions=request.dimensions,
+    )
+    props = {
+        u'builder_name': u'Try',
+        u'properties': {
+          u'cbb_branch': u'master',
+          u'cbb_config': u'amd64-generic-paladin-tryjob',
+          u'cbb_display_label': u'tryjob',
+          u'cbb_extra_args': [],
+        },
+    }
+    expected_properties = Struct()
+    expected_properties.update({k: str(v) for k, v in props.items() if v})
 
     self.verifyBuildbucketRequest(
-        result.buildbucket_id,
+        result.id,
         'luci.chromeos.general',
-        [
-            'builder:Try',
-            'cbb_branch:main',
-            'cbb_config:amd64-generic-paladin-tryjob',
-            'cbb_display_label:tryjob',
+        [common_pb2.StringPair(
+             key='cbb_branch',
+             value='master'),
+         common_pb2.StringPair(
+             key='cbb_config',
+             value='amd64-generic-paladin-tryjob'),
+         common_pb2.StringPair(
+             key='cbb_display_label',
+             value='tryjob'),
         ],
-        {
-            u'builder_name': u'Try',
-            u'properties': {
-                u'cbb_branch': u'main',
-                u'cbb_config': u'amd64-generic-paladin-tryjob',
-                u'cbb_display_label': u'tryjob',
-                u'cbb_extra_args': [],
-            },
-        })
-
-    self.assertEqual(
-        result,
-        request_build.ScheduledBuild(
-            bucket='luci.chromeos.general',
-            buildbucket_id=result.buildbucket_id,
-            build_config='amd64-generic-paladin-tryjob',
-            url=u'https://ci.chromium.org/b/%s' % result.buildbucket_id,
-            created_ts=mock.ANY),
-    )
+        expected_properties)
 
   @cros_test_lib.NetworkTest()
   def testMaxTestBucket(self):
     """Talk to a test buildbucket instance with max job settings."""
     job = self._CreateJobMax()
-    result = job.Submit(testjob=True)
+    request = job.CreateBuildRequest()
+    client = buildbucket_v2.BuildbucketV2(test_env=True)
+    result = client.ScheduleBuild(
+        request_id=request.request_id,
+        builder=request.builder,
+        properties=request.properties,
+        tags=request.tags,
+        dimensions=request.dimensions,
+    )
+    props = {
+        u'buildset': u'cros/parent_buildbucket_id/master_bb_id',
+        u'cbb_branch': u'test-branch',
+        u'cbb_config': u'amd64-generic-paladin',
+        u'cbb_display_label': u'display',
+        u'cbb_email': u'explicit_email',
+        u'cbb_extra_args': [u'funky', u'cold', u'medina'],
+        u'cbb_master_build_id': u'master_cidb_id',
+        u'cbb_master_buildbucket_id': u'master_bb_id',
+        u'master': u'False',
+    }
+    expected_properties = Struct()
+    expected_properties.update({k: str(v) for k, v in props.items() if v})
 
     self.verifyBuildbucketRequest(
-        result.buildbucket_id,
+        result.id,
         self.TEST_BUCKET,
-        [
-            'builder:luci_build',
-            'buildset:cros/parent_buildbucket_id/master_bb_id',
-            'cbb_branch:test-branch',
-            'cbb_display_label:display',
-            'cbb_config:amd64-generic-paladin',
-            'cbb_email:explicit_email',
-            'cbb_master_build_id:master_cidb_id',
-            'cbb_master_buildbucket_id:master_bb_id',
-            'master:False',
+        [common_pb2.StringPair(
+            key='buildset',
+            value='cros/parent_buildbucket_id/master_bb_id'),
+         common_pb2.StringPair(
+             key='cbb_branch',
+             value='test-branch'),
+         common_pb2.StringPair(
+             key='cbb_config',
+             value='amd64-generic-paladin'),
+         common_pb2.StringPair(
+             key='cbb_display_label',
+             value='display'),
+         common_pb2.StringPair(
+             key='cbb_email',
+             value='explicit_email'),
+         common_pb2.StringPair(
+             key='cbb_master_build_id',
+             value='master_cidb_id'),
+         common_pb2.StringPair(
+             key='cbb_master_buildbucket_id',
+             value='master_bb_id'),
+         common_pb2.StringPair(
+             key='full_version',
+             value='R84-13099.77.0'),
+         common_pb2.StringPair(
+             key='master',
+             value='False'),
         ],
-        {
-            u'builder_name': u'luci_build',
-            u'email_notify': [{u'email': u'explicit_email',
-                               u'template': u'explicit_template'}],
-            u'properties': {
-                u'buildset': u'cros/parent_buildbucket_id/master_bb_id',
-                u'cbb_branch': u'test-branch',
-                u'cbb_config': u'amd64-generic-paladin',
-                u'cbb_display_label': u'display',
-                u'cbb_email': u'explicit_email',
-                u'cbb_extra_args': [u'funky', u'cold', u'medina'],
-                u'cbb_master_build_id': u'master_cidb_id',
-                u'cbb_master_buildbucket_id': u'master_bb_id',
-                u'master': u'False',
-            },
-            u'swarming': {
-                u'override_builder_cfg': {
-                    u'dimensions': [
-                        u'240:id:botname',
-                    ]
-                }
-            }
-        })
-
-    self.assertEqual(
-        result,
-        request_build.ScheduledBuild(
-            bucket='luci.chromeos.general',
-            buildbucket_id=result.buildbucket_id,
-            build_config='amd64-generic-paladin',
-            url=u'https://ci.chromium.org/b/%s' % result.buildbucket_id,
-            created_ts=mock.ANY),
-    )
-
-  # pylint: disable=protected-access
-  def testPostConfigToBuildBucket(self):
-    """Check syntax for PostConfigsToBuildBucket."""
-    self.PatchObject(auth, 'Login')
-    self.PatchObject(auth, 'Token')
-    self.PatchObject(request_build.RequestBuild, '_PutConfigToBuildBucket')
-
-    remote_try_job = request_build.RequestBuild(
-        build_config=self.BUILD_CONFIG_MIN,
-        display_label=self.DISPLAY_LABEL,
-        branch='main',
-        extra_args=(),
-        user_email='default_email',
-        master_buildbucket_id=None)
-    remote_try_job.Submit(testjob=True, dryrun=True)
+        expected_properties)
diff --git a/third_party/google/api/field_behavior_pb2.py b/third_party/google/api/field_behavior_pb2.py
new file mode 100644
index 0000000..e6d7acc
--- /dev/null
+++ b/third_party/google/api/field_behavior_pb2.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: google/api/field_behavior.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+    name="google/api/field_behavior.proto",
+    package="google.api",
+    syntax="proto3",
+    serialized_options=b"\n\016com.google.apiB\022FieldBehaviorProtoP\001ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\242\002\004GAPI",
+    create_key=_descriptor._internal_create_key,
+    serialized_pb=b"\n\x1fgoogle/api/field_behavior.proto\x12\ngoogle.api\x1a google/protobuf/descriptor.proto*\x8f\x01\n\rFieldBehavior\x12\x1e\n\x1a\x46IELD_BEHAVIOR_UNSPECIFIED\x10\x00\x12\x0c\n\x08OPTIONAL\x10\x01\x12\x0c\n\x08REQUIRED\x10\x02\x12\x0f\n\x0bOUTPUT_ONLY\x10\x03\x12\x0e\n\nINPUT_ONLY\x10\x04\x12\r\n\tIMMUTABLE\x10\x05\x12\x12\n\x0eUNORDERED_LIST\x10\x06:Q\n\x0e\x66ield_behavior\x12\x1d.google.protobuf.FieldOptions\x18\x9c\x08 \x03(\x0e\x32\x19.google.api.FieldBehaviorBp\n\x0e\x63om.google.apiB\x12\x46ieldBehaviorProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xa2\x02\x04GAPIb\x06proto3",
+    dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR],
+)
+
+_FIELDBEHAVIOR = _descriptor.EnumDescriptor(
+    name="FieldBehavior",
+    full_name="google.api.FieldBehavior",
+    filename=None,
+    file=DESCRIPTOR,
+    create_key=_descriptor._internal_create_key,
+    values=[
+        _descriptor.EnumValueDescriptor(
+            name="FIELD_BEHAVIOR_UNSPECIFIED",
+            index=0,
+            number=0,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
+        ),
+        _descriptor.EnumValueDescriptor(
+            name="OPTIONAL",
+            index=1,
+            number=1,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
+        ),
+        _descriptor.EnumValueDescriptor(
+            name="REQUIRED",
+            index=2,
+            number=2,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
+        ),
+        _descriptor.EnumValueDescriptor(
+            name="OUTPUT_ONLY",
+            index=3,
+            number=3,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
+        ),
+        _descriptor.EnumValueDescriptor(
+            name="INPUT_ONLY",
+            index=4,
+            number=4,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
+        ),
+        _descriptor.EnumValueDescriptor(
+            name="IMMUTABLE",
+            index=5,
+            number=5,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
+        ),
+        _descriptor.EnumValueDescriptor(
+            name="UNORDERED_LIST",
+            index=6,
+            number=6,
+            serialized_options=None,
+            type=None,
+            create_key=_descriptor._internal_create_key,
+        ),
+    ],
+    containing_type=None,
+    serialized_options=None,
+    serialized_start=82,
+    serialized_end=225,
+)
+_sym_db.RegisterEnumDescriptor(_FIELDBEHAVIOR)
+
+FieldBehavior = enum_type_wrapper.EnumTypeWrapper(_FIELDBEHAVIOR)
+FIELD_BEHAVIOR_UNSPECIFIED = 0
+OPTIONAL = 1
+REQUIRED = 2
+OUTPUT_ONLY = 3
+INPUT_ONLY = 4
+IMMUTABLE = 5
+UNORDERED_LIST = 6
+
+FIELD_BEHAVIOR_FIELD_NUMBER = 1052
+field_behavior = _descriptor.FieldDescriptor(
+    name="field_behavior",
+    full_name="google.api.field_behavior",
+    index=0,
+    number=1052,
+    type=14,
+    cpp_type=8,
+    label=3,
+    has_default_value=False,
+    default_value=[],
+    message_type=None,
+    enum_type=None,
+    containing_type=None,
+    is_extension=True,
+    extension_scope=None,
+    serialized_options=None,
+    file=DESCRIPTOR,
+    create_key=_descriptor._internal_create_key,
+)
+
+DESCRIPTOR.enum_types_by_name["FieldBehavior"] = _FIELDBEHAVIOR
+DESCRIPTOR.extensions_by_name["field_behavior"] = field_behavior
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+field_behavior.enum_type = _FIELDBEHAVIOR
+google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(field_behavior)
+
+DESCRIPTOR._options = None
+# @@protoc_insertion_point(module_scope)