1# Copyright 2015 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import dbus 6import json 7import logging 8import os 9import shutil 10import tempfile 11import time 12 13from autotest_lib.client.cros import dbus_util 14from autotest_lib.client.common_lib import error 15from autotest_lib.client.common_lib import utils 16from autotest_lib.client.common_lib.cros.fake_device_server import \ 17 fake_gcd_helper 18from autotest_lib.client.common_lib.cros.fake_device_server.client_lib import \ 19 commands 20from autotest_lib.client.common_lib.cros.fake_device_server.client_lib import \ 21 devices 22from autotest_lib.client.common_lib.cros.fake_device_server.client_lib import \ 23 fail_control 24from autotest_lib.client.common_lib.cros.fake_device_server.client_lib import \ 25 oauth 26from autotest_lib.client.common_lib.cros.fake_device_server.client_lib import \ 27 registration 28from autotest_lib.client.common_lib.cros.tendo import buffet_config 29from autotest_lib.client.common_lib.cros.tendo import buffet_dbus_helper 30 31 32TEST_NAME = 'test_name ' 33TEST_DESCRIPTION = 'test_description ' 34TEST_LOCATION = 'test_location ' 35 36TEST_COMMAND_CATEGORY = 'registration_test' 37TEST_COMMAND_NAME = '_TestEcho' 38TEST_COMMAND_PARAM = 'message' 39TEST_COMMAND_DEFINITION = { 40 TEST_COMMAND_CATEGORY: { 41 TEST_COMMAND_NAME: { 42 'parameters': { TEST_COMMAND_PARAM: { 'type': 'string' } }, 43 'results': {}, 44 'name': 'Test Echo Command', 45 } 46 } 47} 48 49STATUS_UNCONFIGURED = 'unconfigured' 50STATUS_CONNECTING = 'connecting' 51STATUS_CONNECTED = 'connected' 52STATUS_INVALID_CREDENTIALS = 'invalid_credentials' 53 54def _assert_has(resource, key, value, resource_description): 55 if resource is None: 56 raise error.TestFail('Wanted %s[%s]=%r, but %s is None.' % 57 (resource_description, key, value)) 58 if key not in resource: 59 raise error.TestFail('%s not in %s' % (key, resource_description)) 60 61 if resource[key] != value: 62 raise error.TestFail('Wanted %s[%s]=%r, but got %r' % 63 (resource_description, key, value, resource[key])) 64 65 66class BuffetTester(object): 67 """Helper class for buffet tests.""" 68 69 70 def __init__(self): 71 """Initialization routine.""" 72 # We're going to confirm buffet is polling by issuing commands to 73 # the mock GCD server, then checking that buffet gets them. The 74 # commands are test.TestEcho commands with a single parameter 75 # |message|. |self._expected_messages| is a list of these messages. 76 self._expected_messages = [] 77 # We store our command definitions under this root. 78 self._temp_dir_path = None 79 # Spin up our mock server. 80 self._gcd = fake_gcd_helper.FakeGCDHelper() 81 self._gcd.start() 82 # Create the command definition we want to use. 83 self._temp_dir_path = tempfile.mkdtemp() 84 commands_dir = os.path.join(self._temp_dir_path, 'commands') 85 os.mkdir(commands_dir) 86 command_definition_path = os.path.join( 87 commands_dir, '%s.json' % TEST_COMMAND_CATEGORY) 88 with open(command_definition_path, 'w') as f: 89 f.write(json.dumps(TEST_COMMAND_DEFINITION)) 90 utils.run('chown -R buffet:buffet %s' % self._temp_dir_path) 91 logging.debug('Created test commands definition: %s', 92 command_definition_path) 93 # Create client proxies for interacting with oyr fake server. 94 self._registration_client = registration.RegistrationClient( 95 server_url=buffet_config.LOCAL_SERVICE_URL, 96 api_key=buffet_config.TEST_API_KEY) 97 self._device_client = devices.DevicesClient( 98 server_url=buffet_config.LOCAL_SERVICE_URL, 99 api_key=buffet_config.TEST_API_KEY) 100 self._oauth_client = oauth.OAuthClient( 101 server_url=buffet_config.LOCAL_SERVICE_URL, 102 api_key=buffet_config.TEST_API_KEY) 103 self._fail_control_client = fail_control.FailControlClient( 104 server_url=buffet_config.LOCAL_SERVICE_URL, 105 api_key=buffet_config.TEST_API_KEY) 106 self._command_client = commands.CommandsClient( 107 server_url=buffet_config.LOCAL_SERVICE_URL, 108 api_key=buffet_config.TEST_API_KEY) 109 self._config = buffet_config.BuffetConfig( 110 log_verbosity=3, 111 test_definitions_dir=self._temp_dir_path) 112 113 114 def check_buffet_status_is(self, expected_status, 115 expected_device_id='', 116 timeout_seconds=0): 117 """Assert that buffet has the given registration status. 118 119 Optionally, a timeout can be specified to wait until the 120 status changes. 121 122 @param expected_device_id: device id created during registration. 123 @param expected_status: the status to wait for. 124 @param timeout_seconds: number of seconds to wait for status to change. 125 126 """ 127 buffet = buffet_dbus_helper.BuffetDBusHelper() 128 start_time = time.time() 129 while True: 130 actual_status = buffet.status 131 actual_device_id = buffet.device_id 132 if (actual_status == expected_status and 133 actual_device_id == expected_device_id): 134 return 135 time_spent = time.time() - start_time 136 if time_spent > timeout_seconds: 137 if actual_status != expected_status: 138 raise error.TestFail('Buffet should be %s, but is %s ' 139 '(waited %.1f seconds).' % 140 (expected_status, actual_status, 141 time_spent)) 142 if actual_device_id != expected_device_id: 143 raise error.TestFail('Device ID should be %s, but is %s ' 144 '(waited %.1f seconds).' % 145 (expected_device_id, actual_device_id, 146 time_spent)) 147 time.sleep(0.5) 148 149 150 def check_buffet_is_polling(self, device_id, timeout_seconds=30): 151 """Assert that buffet is polling for new commands. 152 153 @param device_id: string device id created during registration. 154 @param timeout_seconds: number of seconds to wait for polling 155 to start. 156 157 """ 158 new_command_message = ('This is message %d' % 159 len(self._expected_messages)) 160 command_resource = { 161 'name': '%s.%s' % (TEST_COMMAND_CATEGORY, TEST_COMMAND_NAME), 162 'deviceId': device_id, 163 'parameters': {TEST_COMMAND_PARAM: new_command_message} 164 } 165 self._expected_messages.append(new_command_message) 166 self._command_client.create_command(device_id, command_resource) 167 # Confirm that the command eventually appears on buffet. 168 buffet = buffet_dbus_helper.BuffetDBusHelper() 169 polling_interval_seconds = 0.5 170 start_time = time.time() 171 while time.time() - start_time < timeout_seconds: 172 objects = dbus_util.dbus2primitive( 173 buffet.object_manager.GetManagedObjects()) 174 cmds = [interfaces[buffet_dbus_helper.COMMAND_INTERFACE] 175 for path, interfaces in objects.iteritems() 176 if buffet_dbus_helper.COMMAND_INTERFACE in interfaces] 177 messages = [cmd['Parameters'][TEST_COMMAND_PARAM] for cmd in cmds 178 if (cmd['Name'] == '%s.%s' % (TEST_COMMAND_CATEGORY, 179 TEST_COMMAND_NAME))] 180 # |cmds| is a list of property sets 181 if len(messages) != len(self._expected_messages): 182 # Still waiting for our pending command to show up. 183 time.sleep(polling_interval_seconds) 184 continue 185 logging.debug('Finally saw the right number of commands over ' 186 'DBus: %r', cmds) 187 if sorted(messages) != sorted(self._expected_messages): 188 raise error.TestFail( 189 'Expected commands with messages=%r but got %r.' % 190 (self._expected_messages, messages)) 191 logging.info('Buffet has DBus proxies for commands with ' 192 'messages: %r', self._expected_messages) 193 return 194 raise error.TestFail('Timed out waiting for Buffet to expose ' 195 'pending commands with messages: %r' % 196 self._expected_messages) 197 198 199 def register_with_server(self): 200 """Make buffet register with the cloud server. 201 202 This includes the whole registration flow and ends with buffet 203 obtained an access token for future interactions. The status 204 is guaranteed to be STATUS_CONNECTED when this 205 method returns. 206 207 @return string: the device_id obtained during registration. 208 209 """ 210 ticket = self._registration_client.create_registration_ticket() 211 logging.info('Created ticket: %r', ticket) 212 buffet = buffet_dbus_helper.BuffetDBusHelper() 213 buffet.manager.UpdateDeviceInfo(dbus.String(TEST_NAME), 214 dbus.String(TEST_DESCRIPTION), 215 dbus.String(TEST_LOCATION)) 216 device_id = dbus_util.dbus2primitive( 217 buffet.manager.RegisterDevice(dbus.String(ticket['id']))) 218 # Confirm that registration has populated some fields. 219 device_resource = self._device_client.get_device(device_id) 220 logging.debug('Got device resource=%r', device_resource) 221 _assert_has(device_resource, 'name', TEST_NAME, 222 'device resource') 223 _assert_has(device_resource, 'modelManifestId', 'AATST', 224 'device resource') 225 logging.info('Registration successful') 226 self.check_buffet_status_is(STATUS_CONNECTED, 227 expected_device_id=device_id, 228 timeout_seconds=5) 229 return device_id 230 231 232 def restart_buffet(self, reset_state): 233 """Function for restarting the buffet daemon. 234 235 @param reset_state: If True, all local buffet state will be deleted. 236 """ 237 self._config.restart_with_config(clean_state=reset_state) 238 239 240 def close(self): 241 """Cleanup to be used when done with this instance.""" 242 buffet_config.naive_restart() 243 self._gcd.close() 244 if self._temp_dir_path is not None: 245 shutil.rmtree(self._temp_dir_path, True) 246