1fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa# Copyright 2014 The Chromium OS Authors. All rights reserved. 2fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa# Use of this source code is governed by a BSD-style license that can be 3fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa# found in the LICENSE file. 4fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 5fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa"""Module contains a simple implementation of the devices RPC.""" 6fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 7626d491db01f46f7ac9943a2508b1e8e9844d39cChris Sosafrom cherrypy import tools 80f333af62f6f39a9cdc1bd0521345cd3a39c29e3Christopher Wileyimport logging 9c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenkoimport time 10fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 11fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosaimport common 128e9f21c005c170c5868576d86870bc175af8c610Chris Sosafrom fake_device_server import common_util 138e9f21c005c170c5868576d86870bc175af8c610Chris Sosafrom fake_device_server import resource_method 148e9f21c005c170c5868576d86870bc175af8c610Chris Sosafrom fake_device_server import server_errors 15fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 16fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 17fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa# TODO(sosa): All access to this object should technically require auth. Create 18fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa# setters/getters for the auth token for testing. 19fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 20feedba871f9ea080e3562f7d9875f09e63468c55Chris SosaDEVICES_PATH = 'devices' 2112dd813c417478dffb4bf66994eebbb125db7a27Chris Sosa 22fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 23fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosaclass Devices(resource_method.ResourceMethod): 24fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa """A simple implementation of the device interface. 25fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 26fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa A common workflow of using this API is: 27fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 28fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa POST .../ # Creates a new device with id <id>. 29fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa PATCH ..../<id> # Update device state. 30fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa GET .../<id> # Get device state. 31fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa DELETE .../<id> # Delete the device. 32fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa """ 33fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 34fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa # Needed for cherrypy to expose this to requests. 35fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa exposed = True 36fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 37fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 38e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen def __init__(self, resource, commands_instance, oauth_instance, 39e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen fail_control_handler): 40fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa """Initializes a registration ticket. 41fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 42fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa @param resource: A resource delegate for storing devices. 43fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa @param commands_instance: Instance of commands method class. 44e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen @param oauth_instance: Instance of oauth class. 45e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen @param fail_control_handler: Instance of FailControl. 46fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa """ 47fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa super(Devices, self).__init__(resource) 48fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa self.commands_instance = commands_instance 493c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley self._oauth = oauth_instance 50e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen self._fail_control_handler = fail_control_handler 513c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 523c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 533c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley def _handle_state_patch(self, device_id, api_key, data): 543c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley """Patch a device's state with the given update data. 553c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 563c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley @param device_id: string device id to update. 573c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley @param api_key: string api_key to support this resource delegate. 583c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley @param data: json blob provided to patchState API. 593c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 603c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley """ 613c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley # TODO(wiley) this. 623c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 633c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 643c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley def _validate_device_resource(self, resource): 653c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley # Verify required keys exist in the device draft. 663c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley if not resource: 673c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley raise server_errors.HTTPError(400, 'Empty device resource.') 683c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 69735fec04a0041878cddd4877433414f528fac68dVitaly Buka for key in ['name', 'channel']: 703c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley if key not in resource: 713c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley raise server_errors.HTTPError(400, 'Must specify %s' % key) 723c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 733c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley # Add server fields. 743c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley resource['kind'] = 'clouddevices#device' 75c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko current_time_ms = str(int(round(time.time() * 1000))) 76c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko resource['creationTimeMs'] = current_time_ms 77c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko resource['lastUpdateTimeMs'] = current_time_ms 78c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko resource['lastSeenTimeMs'] = current_time_ms 79fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 80fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 81fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa def create_device(self, api_key, device_config): 82fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa """Creates a new device given the device_config. 83fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 84fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa @param api_key: Api key for the application. 85fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa @param device_config: Json dict for the device. 86fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa @raises server_errors.HTTPError: if the config is missing a required key 87fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa """ 880f333af62f6f39a9cdc1bd0521345cd3a39c29e3Christopher Wiley logging.info('Creating device with api_key=%s and device_config=%r', 890f333af62f6f39a9cdc1bd0521345cd3a39c29e3Christopher Wiley api_key, device_config) 903c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley self._validate_device_resource(device_config) 91fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa new_device = self.resource.update_data_val(None, api_key, 92fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa data_in=device_config) 933c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley self.commands_instance.new_device(new_device['id']) 94fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa return new_device 95fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 96fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 97626d491db01f46f7ac9943a2508b1e8e9844d39cChris Sosa @tools.json_out() 98fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa def GET(self, *args, **kwargs): 99fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa """GET .../(device_id) gets device info or lists all devices. 100fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 101fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa Supports both the GET / LIST commands for devices. List lists all 102fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa devices a user has access to, however, this implementation just returns 103fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa all devices. 104fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 105fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa Raises: 106fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa server_errors.HTTPError if the device doesn't exist. 107fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa """ 108e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen self._fail_control_handler.ensure_not_in_failure_mode() 109fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa id, api_key, _ = common_util.parse_common_args(args, kwargs) 110c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko if not api_key: 111c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko access_token = common_util.get_access_token() 112c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko api_key = self._oauth.get_api_key_from_access_token(access_token) 113fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa if id: 114626d491db01f46f7ac9943a2508b1e8e9844d39cChris Sosa return self.resource.get_data_val(id, api_key) 115fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa else: 116fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa # Returns listing (ignores optional parameters). 117fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa listing = {'kind': 'clouddevices#devicesListResponse'} 118fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa listing['devices'] = self.resource.get_data_vals() 119626d491db01f46f7ac9943a2508b1e8e9844d39cChris Sosa return listing 120fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 121fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 122626d491db01f46f7ac9943a2508b1e8e9844d39cChris Sosa @tools.json_out() 123fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa def POST(self, *args, **kwargs): 1243c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley """Handle POSTs for a device. 1253c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 1263c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley Supported APIs include: 1273c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 1283c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley POST /devices/<device-id>/patchState 1293c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 1303c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley """ 131e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen self._fail_control_handler.ensure_not_in_failure_mode() 1323c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley args = list(args) 1333c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley device_id = args.pop(0) if args else None 1343c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley operation = args.pop(0) if args else None 1353c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley if device_id is None or operation != 'patchState': 1363c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley raise server_errors.HTTPError(400, 'Unsupported operation.') 137fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa data = common_util.parse_serialized_json() 1383c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley access_token = common_util.get_access_token() 1393c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley api_key = self._oauth.get_api_key_from_access_token(access_token) 1403c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley self._handle_state_patch(device_id, api_key, data) 1413c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley return {'state': self.resource.get_data_val(device_id, 1423c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley api_key)['state']} 143fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 144fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 1453c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley @tools.json_out() 1463c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley def PUT(self, *args, **kwargs): 1473c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley """Update an existing device using the incoming json data. 1483c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 1493c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley On startup, devices make a request like: 1503c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 1513c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley PUT http://<server-host>/devices/<device-id> 1523c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 1533c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley {'channel': {'supportedType': 'xmpp'}, 1543c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'commandDefs': {}, 1553c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'description': 'test_description ', 1563c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'displayName': 'test_display_name ', 1573c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'id': '4471f7', 1583c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'location': 'test_location ', 1593c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'name': 'test_device_name', 1603c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'state': {'base': {'firmwareVersion': '6771.0.2015_02_09_1429', 1613c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'isProximityTokenRequired': False, 1623c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'localDiscoveryEnabled': False, 1633c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'manufacturer': '', 1643c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'model': '', 1653c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'serialNumber': '', 1663c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'supportUrl': '', 1673c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 'updateUrl': ''}}} 1683c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 1693c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley This PUT has no API key, but comes with an OAUTH access token. 1703c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 1713c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley """ 172e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen self._fail_control_handler.ensure_not_in_failure_mode() 1733c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley device_id, _, _ = common_util.parse_common_args(args, kwargs) 1743c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley access_token = common_util.get_access_token() 17557f2ed1cddace69c180e956d12b56a60150112ceNathan Bullock if not access_token: 17657f2ed1cddace69c180e956d12b56a60150112ceNathan Bullock raise server_errors.HTTPError(401, 'Access denied.') 1773c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley api_key = self._oauth.get_api_key_from_access_token(access_token) 1783c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley data = common_util.parse_serialized_json() 1793c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley self._validate_device_resource(data) 1803c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley 1813c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley logging.info('Updating device with id=%s and device_config=%r', 1823c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley device_id, data) 1833c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley new_device = self.resource.update_data_val(device_id, api_key, 1843c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley data_in=data) 185c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko return data 186fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 187fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 188fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa def DELETE(self, *args, **kwargs): 189fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa """Deletes the given device. 190fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 191fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa Format of this call is: 192fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa DELETE .../device_id 193fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa 194fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa Raises: 195fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa server_errors.HTTPError if the device doesn't exist. 196fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa """ 197e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen self._fail_control_handler.ensure_not_in_failure_mode() 198fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa id, api_key, _ = common_util.parse_common_args(args, kwargs) 199fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa self.resource.del_data_val(id, api_key) 2003c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley self.commands_instance.remove_device(id) 201