| # Copyright 2014 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. |
| |
| """Module contains a simple implementation of the devices RPC.""" |
| |
| from cherrypy import tools |
| import logging |
| import time |
| |
| import common |
| from fake_device_server import common_util |
| from fake_device_server import resource_method |
| from fake_device_server import server_errors |
| |
| |
| # TODO(sosa): All access to this object should technically require auth. Create |
| # setters/getters for the auth token for testing. |
| |
| DEVICES_PATH = 'devices' |
| |
| |
| class Devices(resource_method.ResourceMethod): |
| """A simple implementation of the device interface. |
| |
| A common workflow of using this API is: |
| |
| POST .../ # Creates a new device with id <id>. |
| PATCH ..../<id> # Update device state. |
| GET .../<id> # Get device state. |
| DELETE .../<id> # Delete the device. |
| """ |
| |
| # Needed for cherrypy to expose this to requests. |
| exposed = True |
| |
| |
| def __init__(self, resource, commands_instance, oauth_instance, |
| fail_control_handler): |
| """Initializes a registration ticket. |
| |
| @param resource: A resource delegate for storing devices. |
| @param commands_instance: Instance of commands method class. |
| @param oauth_instance: Instance of oauth class. |
| @param fail_control_handler: Instance of FailControl. |
| """ |
| super(Devices, self).__init__(resource) |
| self.commands_instance = commands_instance |
| self._oauth = oauth_instance |
| self._fail_control_handler = fail_control_handler |
| |
| |
| def _handle_state_patch(self, device_id, api_key, data): |
| """Patch a device's state with the given update data. |
| |
| @param device_id: string device id to update. |
| @param api_key: string api_key to support this resource delegate. |
| @param data: json blob provided to patchState API. |
| |
| """ |
| # TODO(wiley) this. |
| |
| |
| def _validate_device_resource(self, resource): |
| # Verify required keys exist in the device draft. |
| if not resource: |
| raise server_errors.HTTPError(400, 'Empty device resource.') |
| |
| for key in ['name', 'channel']: |
| if key not in resource: |
| raise server_errors.HTTPError(400, 'Must specify %s' % key) |
| |
| # Add server fields. |
| resource['kind'] = 'clouddevices#device' |
| current_time_ms = str(int(round(time.time() * 1000))) |
| resource['creationTimeMs'] = current_time_ms |
| resource['lastUpdateTimeMs'] = current_time_ms |
| resource['lastSeenTimeMs'] = current_time_ms |
| |
| |
| def create_device(self, api_key, device_config): |
| """Creates a new device given the device_config. |
| |
| @param api_key: Api key for the application. |
| @param device_config: Json dict for the device. |
| @raises server_errors.HTTPError: if the config is missing a required key |
| """ |
| logging.info('Creating device with api_key=%s and device_config=%r', |
| api_key, device_config) |
| self._validate_device_resource(device_config) |
| new_device = self.resource.update_data_val(None, api_key, |
| data_in=device_config) |
| self.commands_instance.new_device(new_device['id']) |
| return new_device |
| |
| |
| @tools.json_out() |
| def GET(self, *args, **kwargs): |
| """GET .../(device_id) gets device info or lists all devices. |
| |
| Supports both the GET / LIST commands for devices. List lists all |
| devices a user has access to, however, this implementation just returns |
| all devices. |
| |
| Raises: |
| server_errors.HTTPError if the device doesn't exist. |
| """ |
| self._fail_control_handler.ensure_not_in_failure_mode() |
| id, api_key, _ = common_util.parse_common_args(args, kwargs) |
| if not api_key: |
| access_token = common_util.get_access_token() |
| api_key = self._oauth.get_api_key_from_access_token(access_token) |
| if id: |
| return self.resource.get_data_val(id, api_key) |
| else: |
| # Returns listing (ignores optional parameters). |
| listing = {'kind': 'clouddevices#devicesListResponse'} |
| listing['devices'] = self.resource.get_data_vals() |
| return listing |
| |
| |
| @tools.json_out() |
| def POST(self, *args, **kwargs): |
| """Handle POSTs for a device. |
| |
| Supported APIs include: |
| |
| POST /devices/<device-id>/patchState |
| |
| """ |
| self._fail_control_handler.ensure_not_in_failure_mode() |
| args = list(args) |
| device_id = args.pop(0) if args else None |
| operation = args.pop(0) if args else None |
| if device_id is None or operation != 'patchState': |
| raise server_errors.HTTPError(400, 'Unsupported operation.') |
| data = common_util.parse_serialized_json() |
| access_token = common_util.get_access_token() |
| api_key = self._oauth.get_api_key_from_access_token(access_token) |
| self._handle_state_patch(device_id, api_key, data) |
| return {'state': self.resource.get_data_val(device_id, |
| api_key)['state']} |
| |
| |
| @tools.json_out() |
| def PUT(self, *args, **kwargs): |
| """Update an existing device using the incoming json data. |
| |
| On startup, devices make a request like: |
| |
| PUT http://<server-host>/devices/<device-id> |
| |
| {'channel': {'supportedType': 'xmpp'}, |
| 'commandDefs': {}, |
| 'description': 'test_description ', |
| 'displayName': 'test_display_name ', |
| 'id': '4471f7', |
| 'location': 'test_location ', |
| 'name': 'test_device_name', |
| 'state': {'base': {'firmwareVersion': '6771.0.2015_02_09_1429', |
| 'isProximityTokenRequired': False, |
| 'localDiscoveryEnabled': False, |
| 'manufacturer': '', |
| 'model': '', |
| 'serialNumber': '', |
| 'supportUrl': '', |
| 'updateUrl': ''}}} |
| |
| This PUT has no API key, but comes with an OAUTH access token. |
| |
| """ |
| self._fail_control_handler.ensure_not_in_failure_mode() |
| device_id, _, _ = common_util.parse_common_args(args, kwargs) |
| access_token = common_util.get_access_token() |
| if not access_token: |
| raise server_errors.HTTPError(401, 'Access denied.') |
| api_key = self._oauth.get_api_key_from_access_token(access_token) |
| data = common_util.parse_serialized_json() |
| self._validate_device_resource(data) |
| |
| logging.info('Updating device with id=%s and device_config=%r', |
| device_id, data) |
| new_device = self.resource.update_data_val(device_id, api_key, |
| data_in=data) |
| return data |
| |
| |
| def DELETE(self, *args, **kwargs): |
| """Deletes the given device. |
| |
| Format of this call is: |
| DELETE .../device_id |
| |
| Raises: |
| server_errors.HTTPError if the device doesn't exist. |
| """ |
| self._fail_control_handler.ensure_not_in_failure_mode() |
| id, api_key, _ = common_util.parse_common_args(args, kwargs) |
| self.resource.del_data_val(id, api_key) |
| self.commands_instance.remove_device(id) |