# 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 commands RPC."""
from cherrypy import tools
import logging
import uuid
import common
from fake_device_server import common_util
from fake_device_server import constants
from fake_device_server import server_errors
COMMANDS_PATH = 'commands'
# TODO(sosa) Support upload method (and mediaPath parameter).
class Commands(object):
"""A simple implementation of the commands interface."""
# Needed for cherrypy to expose this to requests.
exposed = True
# Roots of command resource representation that might contain commands.
_COMMAND_ROOTS = set(['base', 'aggregator', 'printer', 'storage', 'test'])
def __init__(self, oauth_handler, fail_control_handler):
"""Initializes a Commands handler."""
# A map of device_id's to maps of command ids to command resources
self.device_commands = dict()
self._num_commands_created = 0
self._oauth_handler = oauth_handler
self._fail_control_handler = fail_control_handler
def _generate_command_id(self):
"""@return unique command ID."""
command_id = '%s_%03d' % (uuid.uuid4().hex[0:6],
self._num_commands_created)
self._num_commands_created += 1
return command_id
def new_device(self, device_id):
"""Adds knowledge of a device with the given |device_id|.
This method should be called whenever a new device is created. It
populates an empty command dict for each device state.
@param device_id: Device id to add.
"""
self.device_commands[device_id] = {}
def remove_device(self, device_id):
"""Removes knowledge of the given device.
@param device_id: Device id to remove.
"""
del self.device_commands[device_id]
def create_command(self, command_resource):
"""Creates, queues and returns a new command.
@param api_key: Api key for the application.
@param device_id: Device id of device to send command.
@param command_resource: Json dict for command.
"""
device_id = command_resource.get('deviceId', None)
if not device_id:
raise server_errors.HTTPError(
400, 'Can only create a command if you provide a deviceId.')
if device_id not in self.device_commands:
raise server_errors.HTTPError(
400, 'Unknown device with id %s' % device_id)
if 'name' not in command_resource:
raise server_errors.HTTPError(
400, 'Missing command name.')
# Print out something useful (command base.Reboot)
logging.info('Received command %s', command_resource['name'])
# TODO(sosa): Check to see if command is in devices CDD.
# Queue command, create it and insert to device->command mapping.
command_id = self._generate_command_id()
command_resource['id'] = command_id
command_resource['state'] = constants.QUEUED_STATE
self.device_commands[device_id][command_id] = command_resource
return command_resource
@tools.json_out()
def GET(self, *args, **kwargs):
"""Handle GETs against the command API.
GET .../(command_id) returns a command resource
GET .../queue?deviceId=... returns the command queue
GET .../?deviceId=... returns the command queue
Supports both the GET / LIST commands for commands. 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()
args = list(args)
requested_command_id = args.pop(0) if args else None
device_id = kwargs.get('deviceId', None)
if args:
raise server_errors.HTTPError(400, 'Unsupported API')
if not device_id or device_id not in self.device_commands:
raise server_errors.HTTPError(
400, 'Can only list commands by valid deviceId.')
if requested_command_id is None:
requested_command_id = 'queue'
if not self._oauth_handler.is_request_authorized():
raise server_errors.HTTPError(401, 'Access denied.')
if requested_command_id == 'queue':
# Returns listing (ignores optional parameters).
listing = {'kind': 'clouddevices#commandsListResponse'}
requested_state = kwargs.get('state', None)
listing['commands'] = []
for _, command in self.device_commands[device_id].iteritems():
# Check state for match (if None, just append all of them).
if (requested_state is None or
requested_state == command['state']):
listing['commands'].append(command)
logging.info('Returning queue of commands: %r', listing)
return listing
for command_id, resource in self.device_commands[device_id].iteritems():
if command_id == requested_command_id:
return self.device_commands[device_id][command_id]
raise server_errors.HTTPError(
400, 'No command with ID=%s found' % requested_command_id)
@tools.json_out()
def POST(self, *args, **kwargs):
"""Creates a new command using the incoming json data."""
# TODO(wiley) We could check authorization here, which should be
# a client/owner of the device.
self._fail_control_handler.ensure_not_in_failure_mode()
data = common_util.parse_serialized_json()
if not data:
raise server_errors.HTTPError(400, 'Require JSON body')
return self.create_command(data)