Initial version of payload build_api endpoint

This is the initial version of the payload build_api endpoint,
and should be considered a work in progress as we fill out the
functionality/tests and get the existing production infrastructure
to a state which will be compatible. The end state is to have
this be called by a recipe that originates in a swarming
task created by a builder.

Further down the road this allows us to decouple the payload
step entirely from the legacy builders.

BUG=chromium:1000894
TEST=./tests

Change-Id: Iee7b27fd416aa24d04f77475cb60e444bcfe41b1
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1910360
Tested-by: George Engelbrecht <engeg@google.com>
Auto-Submit: George Engelbrecht <engeg@google.com>
Reviewed-by: Alex Klein <saklein@chromium.org>
diff --git a/api/controller/payload.py b/api/controller/payload.py
new file mode 100644
index 0000000..a29dd5a
--- /dev/null
+++ b/api/controller/payload.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019 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.
+
+"""Payload API Service."""
+
+from __future__ import print_function
+
+from chromite.api import controller
+from chromite.lib import cros_build_lib
+from chromite.api import faux
+from chromite.api import validate
+from chromite.service import payload
+
+
+_VALID_IMAGE_PAIRS = (('src_signed_image', 'tgt_signed_image'),
+                      ('src_unsigned_image', 'tgt_unsigned_image'),
+                      ('full_update', 'tgt_unsigned_image'),
+                      ('full_update', 'tgt_signed_image'))
+
+
+# We have more fields we might validate however, they're either
+# 'oneof' or allowed to be the empty value by design. If @validate
+# gets more complex in the future we can add more here.
+@faux.all_empty
+@validate.require('bucket')
+def GeneratePayload(input_proto, output_proto, config):
+  """Generate a update payload ('do paygen').
+  Args:
+      input_proto (PayloadGenerationRequest): Input proto.
+      output_proto (PayloadGenerationResult): Output proto.
+      config: (api.config.ApiConfig): The API call config.
+  Returns: A controller return code (e.g. controller.RETURN_CODE_SUCCESS).
+  """
+
+  # Resolve the tgt image oneof.
+  tgt_name = input_proto.WhichOneof('tgt_image_oneof')
+  try:
+    tgt_image = getattr(input_proto, tgt_name)
+  except AttributeError:
+    cros_build_lib.Die('%s is not a known tgt image type' % (tgt_name,))
+
+  # Resolve the src image oneof.
+  src_name = input_proto.WhichOneof('src_image_oneof')
+
+  # If the source image is 'full_update' we lack a source entirely.
+  if src_name == 'full_update':
+    src_image = None
+  # Otherwise we have an image.
+  else:
+    try:
+      src_image = getattr(input_proto, src_name)
+    except AttributeError:
+      cros_build_lib.Die('%s is not a known src image type' % (src_name,))
+
+  # Ensure they are compatible oneofs.
+  if (src_name, tgt_name) not in _VALID_IMAGE_PAIRS:
+    cros_build_lib.Die('%s and %s are not valid image pairs' %
+                       (src_image, tgt_image))
+
+  # Find the value of bucket or default to 'chromeos-releases'.
+  destination_bucket = input_proto.bucket or 'chromeos-releases'
+
+  # There's a potential that some paygen_lib library might raise here, but since
+  # we're still involved in config we'll keep it before the validate_only.
+  payload_config = payload.PayloadConfig(
+      tgt_image,
+      src_image,
+      destination_bucket,
+      input_proto.verify,
+      input_proto.keyset)
+
+  # If configured for validation only we're done here.
+  if config.validate_only:
+    return controller.RETURN_CODE_VALID_INPUT
+
+  # Do payload generation.
+  paygen_ok = payload_config.GeneratePayload()
+  _SetGeneratePayloadOutputProto(output_proto, paygen_ok)
+
+  if paygen_ok:
+    return controller.RETURN_CODE_SUCCESS
+  else:
+    return controller.RETURN_CODE_COMPLETED_UNSUCCESSFULLY
+
+
+def _SetGeneratePayloadOutputProto(output_proto, generate_payload_ok):
+  """Set the output proto with the results from the service class.
+  Args:
+      output_proto (PayloadGenerationResult_pb2): The output proto.
+      generate_payload_ok (bool): value to set output_proto.success.
+  """
+  output_proto.success = generate_payload_ok
diff --git a/api/controller/payload_unittest b/api/controller/payload_unittest
new file mode 120000
index 0000000..ef3e37b
--- /dev/null
+++ b/api/controller/payload_unittest
@@ -0,0 +1 @@
+../../scripts/wrapper.py
\ No newline at end of file
diff --git a/api/controller/payload_unittest.py b/api/controller/payload_unittest.py
new file mode 100644
index 0000000..16cfe47
--- /dev/null
+++ b/api/controller/payload_unittest.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019 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.
+
+"""Payload operations."""
+
+from __future__ import print_function
+
+from chromite.api import api_config
+from chromite.api import controller
+from chromite.api.controller import payload
+from chromite.api.gen.chromite.api import payload_pb2
+from chromite.api.gen.chromiumos import common_pb2
+from chromite.lib import cros_test_lib
+from chromite.lib.paygen import paygen_payload_lib
+
+
+class PayloadApiTests(cros_test_lib.MockTestCase, api_config.ApiConfigMixin):
+  """Unittests for SetBinhost."""
+
+  def setUp(self):
+    self.response = payload_pb2.PayloadGenerationResult()
+
+    src_build = payload_pb2.Build(version='1.0.0', bucket='test',
+                                  channel='test-channel', build_target=
+                                  common_pb2.BuildTarget(name='cave'))
+
+    src_image = payload_pb2.UnsignedImage(
+        build=src_build, image_type=6, milestone='R70')
+
+    tgt_build = payload_pb2.Build(version='2.0.0', bucket='test',
+                                  channel='test-channel', build_target=
+                                  common_pb2.BuildTarget(name='cave'))
+
+    tgt_image = payload_pb2.UnsignedImage(
+        build=tgt_build, image_type=6, milestone='R70')
+
+    self.req = payload_pb2.PayloadGenerationRequest(
+        tgt_unsigned_image=tgt_image, src_unsigned_image=src_image,
+        bucket='test-destination-bucket', verify=True, keyset='update_signer')
+
+    self.result = payload_pb2.PayloadGenerationResult()
+
+  def testValidateOnly(self):
+    """Sanity check that a validate only call does not execute any logic."""
+
+    res = payload.GeneratePayload(self.req, self.result,
+                                  self.validate_only_config)
+    self.assertEqual(res, controller.RETURN_CODE_VALID_INPUT)
+
+  def testCallSucceeds(self):
+    """Check that a call is made succesfully."""
+    # Deep patch the paygen lib, this is a full run through service as well.
+    self.PatchObject(paygen_payload_lib, 'PaygenPayload')
+    res = payload.GeneratePayload(self.req, self.result, self.api_config)
+    self.assertEqual(res, controller.RETURN_CODE_SUCCESS)