[autotest][cfm] Adds utility for interacting with bond

Tests will be migrated in future CLs.

BUG=b:116194847
TEST=Tested against local setup

Change-Id: Iec215b03e4f36d4cde0c01518988c0f84479ec10
Reviewed-on: https://chromium-review.googlesource.com/1270975
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Tested-by: Kristoffer Erlandsson <kerl@google.com>
Reviewed-by: Kristoffer Erlandsson <kerl@google.com>
diff --git a/server/cros/cfm/utils/__init__.py b/server/cros/cfm/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/server/cros/cfm/utils/__init__.py
diff --git a/server/cros/cfm/utils/bond_http_api.py b/server/cros/cfm/utils/bond_http_api.py
new file mode 100644
index 0000000..2d12de8
--- /dev/null
+++ b/server/cros/cfm/utils/bond_http_api.py
@@ -0,0 +1,142 @@
+"""Utility class for sending requests to BonD, adding and controlling bots."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import json
+import requests
+
+from datetime import datetime
+from oauth2client.client import GoogleCredentials
+
+
+_BOND_API_URL = 'https://bond-pa.sandbox.googleapis.com'
+_HANGOUTS_API_URL = 'https://www.googleapis.com/hangouts/v1_meetings/'
+_MEETINGS_API_URL = 'https://meetings.googleapis.com'
+
+_TOKEN_TTL_SECONDS = 3500
+_SERVICE_CREDS_FILE = '/creds/service_accounts/bond_service_account.json'
+
+_BOT_CREDS_FILE = '/creds/service_accounts/bond_bot_credentials.json'
+
+
+class BondHttpApi(object):
+  """Utility class for sending requests to BonD for bots."""
+
+  def __init__(self):
+    self._bot_creds = self._GetBotCredentials()
+    self._last_token_request_time = None
+    self._last_token = None
+
+  def GetAvailableWorkers(self):
+    """Gets the number of available workers from a conference.
+
+    Returns:
+      GetNumOfAvailableWorkersResponse denoting failure or success of
+      the request.
+    """
+    token = self._GetAccessToken()
+    resp = requests.get(
+        '%s/v1/workers:count' % _BOND_API_URL,
+        headers={
+            'Content-Type': 'application/json',
+            'Authorization': 'Bearer %s' % token
+        })
+    return resp.text
+
+  def ExecuteScript(self, script, meeting_code):
+    """Executes the specified script.
+
+    Args:
+      script: Script to execute.
+      meeting_code: The meeting to execute the script for.
+
+    Returns:
+      RunScriptRequest denoting failure or success of the request.
+    """
+    token = self._GetAccessToken()
+
+    request_data = """{
+      "script": "%s",
+      "conference": {
+        "conference_code": "%s"
+      }}""" % (script, meeting_code)
+
+    resp = requests.post(
+        '%s/v1/conference/%s/script' % (_BOND_API_URL, meeting_code),
+        headers={
+            'Content-Type': 'application/json',
+            'Authorization': 'Bearer %s' % token,
+        },
+        data=request_data)
+    return resp.text
+
+  def AddBotsRequest(self, meeting_code, number_of_bots, ttl_secs):
+    """Adds a number of bots to a meeting for a specified period of time.
+
+    Args:
+      meeting_code: The meeting to join.
+      number_of_bots: The number of bots to add to the meeting.
+      ttl_secs: The time in seconds that the bots will stay in the meeting after
+        joining.
+
+    Returns:
+      AddBotsResponse denoting failure or success of the request.
+    """
+    token = self._GetAccessToken()
+
+    request_data = {
+      'num_of_bots': number_of_bots,
+      'ttl_secs': ttl_secs,
+      'video_call_options': {},
+      'media_options': {
+        'audio_file_path': "audio_32bit_48k_stereo.raw",
+        'mute_audio': True,
+        'video_file_path': "jamboard_two_far_video_hd.1280_720.yuv",
+        'video_fps': 30,
+        'mute_video': False
+      },
+      'backend_options': {
+        'mesi_apiary_url': _HANGOUTS_API_URL,
+        'mas_one_platform_url': _MEETINGS_API_URL
+      },
+      'conference': {
+        'conference_code': meeting_code
+      },
+      'bot_type': "MEETINGS",
+      'authentication_options': {
+        'is_anonymous': False,
+        'oauth_credentials': self._bot_creds
+      }
+    }
+
+    resp = requests.post(
+        '%s/v1/conference/%s/bots:add' % (_BOND_API_URL, meeting_code),
+        headers={
+            'Content-Type': 'application/json',
+            'Authorization': 'Bearer %s' % token
+        },
+        data=json.dumps(request_data))
+
+    return resp.text
+
+  def _GetAccessToken(self):
+    if self._last_token is None or self._CheckTokenExpired():
+      credentials = self._CreateApiCredentials()
+      scope = 'https://www.googleapis.com/auth/meetings'
+      credentials = credentials.create_scoped(scope)
+      self._last_token_request_time = datetime.now()
+      self._last_token = credentials.get_access_token().access_token
+    return self._last_token
+
+  def _CreateApiCredentials(self):
+    return GoogleCredentials.from_stream(_SERVICE_CREDS_FILE)
+
+  def _GetBotCredentials(self):
+    with open(_BOT_CREDS_FILE) as f:
+      return json.load(f)
+
+  def _CheckTokenExpired(self):
+    elapsed = datetime.now() - self._last_token_request_time
+    return elapsed.total_seconds() > _TOKEN_TTL_SECONDS