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