1# Copyright 2014 The Chromium OS 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 5"""Module contains a simple implementation of the devices RPC.""" 6 7from cherrypy import tools 8import logging 9import time 10 11import common 12from fake_device_server import common_util 13from fake_device_server import resource_method 14from fake_device_server import server_errors 15 16 17# TODO(sosa): All access to this object should technically require auth. Create 18# setters/getters for the auth token for testing. 19 20DEVICES_PATH = 'devices' 21 22 23class Devices(resource_method.ResourceMethod): 24 """A simple implementation of the device interface. 25 26 A common workflow of using this API is: 27 28 POST .../ # Creates a new device with id <id>. 29 PATCH ..../<id> # Update device state. 30 GET .../<id> # Get device state. 31 DELETE .../<id> # Delete the device. 32 """ 33 34 # Needed for cherrypy to expose this to requests. 35 exposed = True 36 37 38 def __init__(self, resource, commands_instance, oauth_instance, 39 fail_control_handler): 40 """Initializes a registration ticket. 41 42 @param resource: A resource delegate for storing devices. 43 @param commands_instance: Instance of commands method class. 44 @param oauth_instance: Instance of oauth class. 45 @param fail_control_handler: Instance of FailControl. 46 """ 47 super(Devices, self).__init__(resource) 48 self.commands_instance = commands_instance 49 self._oauth = oauth_instance 50 self._fail_control_handler = fail_control_handler 51 52 53 def _handle_state_patch(self, device_id, api_key, data): 54 """Patch a device's state with the given update data. 55 56 @param device_id: string device id to update. 57 @param api_key: string api_key to support this resource delegate. 58 @param data: json blob provided to patchState API. 59 60 """ 61 # TODO(wiley) this. 62 63 64 def _validate_device_resource(self, resource): 65 # Verify required keys exist in the device draft. 66 if not resource: 67 raise server_errors.HTTPError(400, 'Empty device resource.') 68 69 for key in ['name', 'channel']: 70 if key not in resource: 71 raise server_errors.HTTPError(400, 'Must specify %s' % key) 72 73 # Add server fields. 74 resource['kind'] = 'clouddevices#device' 75 current_time_ms = str(int(round(time.time() * 1000))) 76 resource['creationTimeMs'] = current_time_ms 77 resource['lastUpdateTimeMs'] = current_time_ms 78 resource['lastSeenTimeMs'] = current_time_ms 79 80 81 def create_device(self, api_key, device_config): 82 """Creates a new device given the device_config. 83 84 @param api_key: Api key for the application. 85 @param device_config: Json dict for the device. 86 @raises server_errors.HTTPError: if the config is missing a required key 87 """ 88 logging.info('Creating device with api_key=%s and device_config=%r', 89 api_key, device_config) 90 self._validate_device_resource(device_config) 91 new_device = self.resource.update_data_val(None, api_key, 92 data_in=device_config) 93 self.commands_instance.new_device(new_device['id']) 94 return new_device 95 96 97 @tools.json_out() 98 def GET(self, *args, **kwargs): 99 """GET .../(device_id) gets device info or lists all devices. 100 101 Supports both the GET / LIST commands for devices. List lists all 102 devices a user has access to, however, this implementation just returns 103 all devices. 104 105 Raises: 106 server_errors.HTTPError if the device doesn't exist. 107 """ 108 self._fail_control_handler.ensure_not_in_failure_mode() 109 id, api_key, _ = common_util.parse_common_args(args, kwargs) 110 if not api_key: 111 access_token = common_util.get_access_token() 112 api_key = self._oauth.get_api_key_from_access_token(access_token) 113 if id: 114 return self.resource.get_data_val(id, api_key) 115 else: 116 # Returns listing (ignores optional parameters). 117 listing = {'kind': 'clouddevices#devicesListResponse'} 118 listing['devices'] = self.resource.get_data_vals() 119 return listing 120 121 122 @tools.json_out() 123 def POST(self, *args, **kwargs): 124 """Handle POSTs for a device. 125 126 Supported APIs include: 127 128 POST /devices/<device-id>/patchState 129 130 """ 131 self._fail_control_handler.ensure_not_in_failure_mode() 132 args = list(args) 133 device_id = args.pop(0) if args else None 134 operation = args.pop(0) if args else None 135 if device_id is None or operation != 'patchState': 136 raise server_errors.HTTPError(400, 'Unsupported operation.') 137 data = common_util.parse_serialized_json() 138 access_token = common_util.get_access_token() 139 api_key = self._oauth.get_api_key_from_access_token(access_token) 140 self._handle_state_patch(device_id, api_key, data) 141 return {'state': self.resource.get_data_val(device_id, 142 api_key)['state']} 143 144 145 @tools.json_out() 146 def PUT(self, *args, **kwargs): 147 """Update an existing device using the incoming json data. 148 149 On startup, devices make a request like: 150 151 PUT http://<server-host>/devices/<device-id> 152 153 {'channel': {'supportedType': 'xmpp'}, 154 'commandDefs': {}, 155 'description': 'test_description ', 156 'displayName': 'test_display_name ', 157 'id': '4471f7', 158 'location': 'test_location ', 159 'name': 'test_device_name', 160 'state': {'base': {'firmwareVersion': '6771.0.2015_02_09_1429', 161 'isProximityTokenRequired': False, 162 'localDiscoveryEnabled': False, 163 'manufacturer': '', 164 'model': '', 165 'serialNumber': '', 166 'supportUrl': '', 167 'updateUrl': ''}}} 168 169 This PUT has no API key, but comes with an OAUTH access token. 170 171 """ 172 self._fail_control_handler.ensure_not_in_failure_mode() 173 device_id, _, _ = common_util.parse_common_args(args, kwargs) 174 access_token = common_util.get_access_token() 175 if not access_token: 176 raise server_errors.HTTPError(401, 'Access denied.') 177 api_key = self._oauth.get_api_key_from_access_token(access_token) 178 data = common_util.parse_serialized_json() 179 self._validate_device_resource(data) 180 181 logging.info('Updating device with id=%s and device_config=%r', 182 device_id, data) 183 new_device = self.resource.update_data_val(device_id, api_key, 184 data_in=data) 185 return data 186 187 188 def DELETE(self, *args, **kwargs): 189 """Deletes the given device. 190 191 Format of this call is: 192 DELETE .../device_id 193 194 Raises: 195 server_errors.HTTPError if the device doesn't exist. 196 """ 197 self._fail_control_handler.ensure_not_in_failure_mode() 198 id, api_key, _ = common_util.parse_common_args(args, kwargs) 199 self.resource.del_data_val(id, api_key) 200 self.commands_instance.remove_device(id) 201