flakiness analyzer: Add apis.

BUG=chromium:818020
TEST=Ran integration tests.
Ran "./bin/run_demo" for demo.
Ran "./bin/deploy_staging", "./bin/deploy_prod" to deploy.

Change-Id: If9a7fd8ffc9f84c8d0e521c1ba31f3c1e71a7d60
Reviewed-on: https://chromium-review.googlesource.com/982516
Commit-Ready: Xixuan Wu <xixuan@chromium.org>
Tested-by: Xixuan Wu <xixuan@chromium.org>
Reviewed-by: Paul Hobbs <phobbs@google.com>
diff --git a/PRESUBMIT.cfg b/PRESUBMIT.cfg
index 97ffba2..8bf25a4 100644
--- a/PRESUBMIT.cfg
+++ b/PRESUBMIT.cfg
@@ -1,5 +1,5 @@
 [Hook Scripts]
-hook0=../../chromite/bin/cros lint ${PRESUBMIT_FILES}
+hook0 = ../../chromite/bin/cros lint ${PRESUBMIT_FILES}
 
 [Hook Overrides]
 json_check: true
diff --git a/Pipfile b/Pipfile
index c351a0e..05ed531 100644
--- a/Pipfile
+++ b/Pipfile
@@ -16,10 +16,13 @@
 
 [packages]
 
+flask = "~=0.12.2"
 google-api-python-client = "~=1.6.5"
 google-cloud-bigquery = "~=0.25.0"
+gunicorn = "~=19.7.1"
+requests = "~=2.18.4"
 
 
 [requires]
 
-python_version = "2.7"
+python_version = "3.5"
diff --git a/Pipfile.lock b/Pipfile.lock
index 22d740f..6e6692b 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,24 +1,24 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "c4c2efe8c83baf7df8434ce50a2244e384ed27afbcf389e0ea00da9459173b18"
+            "sha256": "5e4f52479571e06ac1c17f244f1ed42438a29e6d77dcca54a32a800551ea9d1d"
         },
         "host-environment-markers": {
             "implementation_name": "cpython",
-            "implementation_version": "0",
+            "implementation_version": "3.5.3",
             "os_name": "posix",
             "platform_machine": "x86_64",
             "platform_python_implementation": "CPython",
             "platform_release": "4.9.0-6-amd64",
             "platform_system": "Linux",
-            "platform_version": "#1 SMP Debian 4.9.82-1+deb9u2 (2018-02-21)",
-            "python_full_version": "2.7.13",
-            "python_version": "2.7",
-            "sys_platform": "linux2"
+            "platform_version": "#1 SMP Debian 4.9.82-1+deb9u3 (2018-03-02)",
+            "python_full_version": "3.5.3",
+            "python_version": "3.5",
+            "sys_platform": "linux"
         },
         "pipfile-spec": 6,
         "requires": {
-            "python_version": "2.7"
+            "python_version": "3.5"
         },
         "sources": [
             {
@@ -36,12 +36,40 @@
             ],
             "version": "==2.0.1"
         },
+        "certifi": {
+            "hashes": [
+                "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0",
+                "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7"
+            ],
+            "version": "==2018.4.16"
+        },
+        "chardet": {
+            "hashes": [
+                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691",
+                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
+            ],
+            "version": "==3.0.4"
+        },
+        "click": {
+            "hashes": [
+                "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
+                "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
+            ],
+            "version": "==6.7"
+        },
+        "flask": {
+            "hashes": [
+                "sha256:0749df235e3ff61ac108f69ac178c9770caeaccad2509cb762ce1f65570a8856",
+                "sha256:49f44461237b69ecd901cc7ce66feea0319b9158743dd27a2899962ab214dac1"
+            ],
+            "version": "==0.12.2"
+        },
         "google-api-python-client": {
             "hashes": [
-                "sha256:2cf9ab83fa62e06717363e8855fb027864caeb35a3197cadb7f0de38356881c4",
-                "sha256:95ce394028754ec537e5791e811511fdd5fabe6f1f8879407a8daed71ecb0b4c"
+                "sha256:ed85c8e5c7533bfaaf05082bb9e3b13cec7aab50bb287ba69757b65f043fa8a6",
+                "sha256:ec72991f95201996a4edcea44a079cae0292798086beaadb054d91921632fe1b"
             ],
-            "version": "==1.6.5"
+            "version": "==1.6.6"
         },
         "google-auth": {
             "hashes": [
@@ -77,11 +105,44 @@
             ],
             "version": "==1.5.3"
         },
+        "gunicorn": {
+            "hashes": [
+                "sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6",
+                "sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622"
+            ],
+            "version": "==19.7.1"
+        },
         "httplib2": {
             "hashes": [
-                "sha256:f2176149e1e1c59e0520db62c925715018b787b2ae901358803bae5d816fda0b"
+                "sha256:e71daed9a0e6373642db61166fa70beecc9bf04383477f84671348c02a04cbdf"
             ],
-            "version": "==0.11.1"
+            "version": "==0.11.3"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4",
+                "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f"
+            ],
+            "version": "==2.6"
+        },
+        "itsdangerous": {
+            "hashes": [
+                "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
+            ],
+            "version": "==0.24"
+        },
+        "jinja2": {
+            "hashes": [
+                "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
+                "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
+            ],
+            "version": "==2.10"
+        },
+        "markupsafe": {
+            "hashes": [
+                "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
+            ],
+            "version": "==1.0"
         },
         "oauth2client": {
             "hashes": [
@@ -149,6 +210,13 @@
             ],
             "version": "==0.2.1"
         },
+        "requests": {
+            "hashes": [
+                "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
+                "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
+            ],
+            "version": "==2.18.4"
+        },
         "rsa": {
             "hashes": [
                 "sha256:43f682fea81c452c98d09fc316aae12de6d30c4b5c84226642cf8f8fd1c93abd",
@@ -170,6 +238,20 @@
                 "sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d"
             ],
             "version": "==3.0.0"
+        },
+        "urllib3": {
+            "hashes": [
+                "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
+                "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
+            ],
+            "version": "==1.22"
+        },
+        "werkzeug": {
+            "hashes": [
+                "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b",
+                "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c"
+            ],
+            "version": "==0.14.1"
         }
     },
     "develop": {
@@ -198,6 +280,8 @@
         },
         "pluggy": {
             "hashes": [
+                "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
+                "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5",
                 "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"
             ],
             "version": "==0.6.0"
diff --git a/app_template.yaml b/app_template.yaml
new file mode 100644
index 0000000..090f859
--- /dev/null
+++ b/app_template.yaml
@@ -0,0 +1,19 @@
+runtime: python
+env: flex
+entrypoint: gunicorn -b :$PORT --chdir=test_analyzer main:app
+
+runtime_config:
+  python_version: 3
+
+automatic_scaling:
+  min_num_instances: 1
+  max_num_instances: 15
+  cool_down_period_sec: 180
+  cpu_utilization:
+    target_utilization: 0.6
+
+endpoints_api_service:
+  # The following values are to be replaced by information from the output of
+  # 'gcloud service-management deploy openapi-appengine.yaml' command.
+  name: {{endpoints_service}}
+  config_id: {{endpoints_service_config_id}}
diff --git a/bin/deploy_prod b/bin/deploy_prod
new file mode 100755
index 0000000..bd4a192
--- /dev/null
+++ b/bin/deploy_prod
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Copyright 2018 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.
+#
+# Deploy test_analyzer to prod instance.
+set -eu
+cd "$(dirname "$0")/.."
+
+exec ../../chromite/appengine/deploy_appengine \
+  --project_path "$(pwd)" \
+  --project_id chromiumos-test-analyzer \
+  --skip_paths "test_analyzer/credentials" \
+  --skip_paths "venv" \
+  --endpoints_service chromiumos-test-analyzer.appspot.com \
+  --app_template "app_template.yaml" \
+  --openapi_template "openapi-appengine_template.yaml" "$@"
diff --git a/bin/deploy_staging b/bin/deploy_staging
new file mode 100755
index 0000000..97fddcb
--- /dev/null
+++ b/bin/deploy_staging
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Copyright 2018 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.
+#
+# Deploy test_analyzer to prod instance.
+set -eu
+cd "$(dirname "$0")/.."
+
+exec ../../chromite/appengine/deploy_appengine \
+  --project_path "$(pwd)" \
+  --project_id chromeos-test-analyzer-staging \
+  --skip_paths "test_analyzer/credentials" \
+  --skip_paths "venv" \
+  --endpoints_service chromeos-test-analyzer-staging.appspot.com \
+  --app_template "app_template.yaml" \
+  --openapi_template "openapi-appengine_template.yaml" "$@"
diff --git a/bin/run_demo b/bin/run_demo
new file mode 100755
index 0000000..ba3c5a6
--- /dev/null
+++ b/bin/run_demo
@@ -0,0 +1,11 @@
+#!/bin/bash
+# Copyright 2018 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.
+#
+# Deploy test_analyzer to prod instance.
+set -eu
+cd "$(dirname "$0")/.."
+
+export PYTHONPATH="$(pwd)"
+exec pipenv run python test_analyzer/integration_test/demo_api_test.py "$@"
diff --git a/openapi-appengine_template.yaml b/openapi-appengine_template.yaml
new file mode 100644
index 0000000..ba675b1
--- /dev/null
+++ b/openapi-appengine_template.yaml
@@ -0,0 +1,56 @@
+# [START swagger]
+swagger: "2.0"
+info:
+  description: "APIs for ChromeOS test analyzer."
+  title: "ChromeOS test analyzer"
+  version: "1.0.0"
+host: {{endpoints_service}}
+# [END swagger]
+
+basePath: "/"
+consumes:
+- "application/json"
+produces:
+- "application/json"
+schemes:
+- "https"
+paths:
+  "/hello":
+    get:
+      summary: "Hello word."
+      operationId: "hello"
+      produces:
+      - "application/json"
+      responses:
+        200:
+          description: "Hello to world."
+          schema:
+            $ref: "#/definitions/helloMessage"
+  "/internal_hello":
+    get:
+      description: "Private hello."
+      operationId: "internal_hello"
+      produces:
+      - "application/json"
+      responses:
+        200:
+          description: "Hello to internal world"
+          schema:
+            $ref: "#/definitions/helloMessage"
+      security:
+      - api_key: []
+
+definitions:
+  helloMessage:
+    properties:
+      message:
+        type: "string"
+
+# [START securityDef]
+securityDefinitions:
+  # This section configures basic authentication with an API key.
+  api_key:
+    type: "apiKey"
+    name: "key"
+    in: "query"
+# [END securityDef]
diff --git a/test_analyzer/configs.py b/test_analyzer/configs.py
index b83f154..fa622f0 100644
--- a/test_analyzer/configs.py
+++ b/test_analyzer/configs.py
@@ -26,3 +26,10 @@
   else:
     return os.path.join(_CREDS_PATH,
                         'non_cipher/chromeos-test-analyzer-staging.json')
+
+
+def GetCipherAPIKeyFile(is_stage=True):
+  if not is_stage:
+    return os.path.join(_CREDS_PATH, 'cipher/cipher_api_key.txt')
+  else:
+    return os.path.join(_CREDS_PATH, 'cipher/cipher_api_key_staging.txt')
diff --git a/test_analyzer/integration_test/demo_api_test.py b/test_analyzer/integration_test/demo_api_test.py
new file mode 100644
index 0000000..41f8505
--- /dev/null
+++ b/test_analyzer/integration_test/demo_api_test.py
@@ -0,0 +1,71 @@
+# Copyright 2018 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.
+
+"""Integration tests for demo APIs."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import argparse
+import requests
+
+from test_analyzer import configs
+from test_analyzer import kms
+
+
+_HOST_FORMAT = "%s.appspot.com"
+_REQUEST_FORMAT = 'https://%s/%s'
+
+
+def _PrintHttpResponse(request_url, response):
+  """Print responses from http request."""
+  print('url: %s' % request_url)
+  print(response.text + '\n')
+
+
+def _GetAPIKey(is_stage):
+  """Fetch decrypted API key."""
+  ciphertext_file_name = configs.GetCipherAPIKeyFile(is_stage=is_stage)
+  return kms.DecryptFromFile(kms.DEFAULT_CRYPTO_KEY_FOR_API,
+                             ciphertext_file_name)
+
+
+def _GetHost(is_stage):
+  if is_stage:
+    return _HOST_FORMAT % configs.PROJECT_ID_STAGING
+  else:
+    return _HOST_FORMAT % configs.PROJECT_ID_PROD
+
+
+def MakeRequestToHello(is_stage):
+  api_method = 'hello'
+  request_url = _REQUEST_FORMAT % (_GetHost(is_stage), api_method)
+  response = requests.get(request_url)
+  _PrintHttpResponse(request_url, response)
+
+
+def MakeRequestToInternalHello(is_stage):
+  api_method = 'internal_hello'
+  request_url = _REQUEST_FORMAT % (_GetHost(is_stage), api_method)
+  response = requests.get(request_url, params={'key': _GetAPIKey(is_stage)})
+  _PrintHttpResponse(request_url, response)
+
+
+def _MakeParser():
+  """Return parser for testing APIs."""
+  parser = argparse.ArgumentParser(
+      description=__doc__,
+      formatter_class=argparse.RawDescriptionHelpFormatter)
+  parser.add_argument(
+      '--prod', help='Test prod APIs. Default is to test staging APIs.',
+      action='store_true')
+  return parser
+
+
+if __name__ == '__main__':
+  demo_parser = _MakeParser()
+  args = demo_parser.parse_args()
+  MakeRequestToHello(not args.prod)
+  MakeRequestToInternalHello(not args.prod)
diff --git a/test_analyzer/kms.py b/test_analyzer/kms.py
index f51876d..93a5616 100644
--- a/test_analyzer/kms.py
+++ b/test_analyzer/kms.py
@@ -77,7 +77,7 @@
     plaintext: The string plain text to be encrypted.
 
   Returns:
-    A string cipher text.
+    A string cipher text in bytes.
   """
   kms_client = _CreateKMSClient()
   resource_name = _CreateResourceName(crypto_key_info)
@@ -86,7 +86,7 @@
   crypto_keys = kms_client.projects().locations().keyRings().cryptoKeys()
   request = crypto_keys.encrypt(
       name=resource_name,
-      body={'plaintext': base64.b64encode(plaintext).decode('ascii')})
+      body={'plaintext': base64.b64encode(plaintext.encode()).decode('ascii')})
   response = request.execute()
   return base64.b64decode(response['ciphertext'].encode('ascii'))
 
@@ -99,7 +99,7 @@
 
   Args:
     crypto_key_info: A KMSCryptoKey object, including all required key infos.
-    ciphertext: The string cipher text to be decrypted.
+    ciphertext: The string cipher text to be decrypted in bytes.
 
   Returns:
     A string plain text.
@@ -113,7 +113,7 @@
       name=resource_name,
       body={'ciphertext': base64.b64encode(ciphertext).decode('ascii')})
   response = request.execute()
-  return base64.b64decode(response['plaintext'].encode('ascii'))
+  return base64.b64decode(response['plaintext'].encode('ascii')).decode()
 
 
 def DecryptFromFile(crypto_key_info, ciphertext_file_name):
diff --git a/test_analyzer/main.py b/test_analyzer/main.py
new file mode 100644
index 0000000..b5866ab
--- /dev/null
+++ b/test_analyzer/main.py
@@ -0,0 +1,31 @@
+# Copyright 2018 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.
+"""Main API definitions of test_analyzer."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import flask  # pylint: disable=import-error
+
+
+app = flask.Flask(__name__)
+
+
+@app.route('/hello', methods=['GET'])
+def Hello():
+  """Simple hello service."""
+  return 'hello, world'
+
+
+@app.route('/internal_hello', methods=['GET'])
+def InternalHello():
+  """Simple internal hello service."""
+  return 'hello, internal world'
+
+
+if __name__ == '__main__':
+  # This is used when running locally. Gunicorn is used to run the
+  # application on Google App Engine. See entrypoint in app.yaml.
+  app.run(host='127.0.0.1', port=8080, debug=True)