# 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)