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 commands RPC.""" 6 7from cherrypy import tools 8import logging 9import uuid 10 11import common 12from fake_device_server import common_util 13from fake_device_server import constants 14from fake_device_server import server_errors 15 16COMMANDS_PATH = 'commands' 17 18 19# TODO(sosa) Support upload method (and mediaPath parameter). 20class Commands(object): 21 """A simple implementation of the commands interface.""" 22 23 # Needed for cherrypy to expose this to requests. 24 exposed = True 25 26 # Roots of command resource representation that might contain commands. 27 _COMMAND_ROOTS = set(['base', 'aggregator', 'printer', 'storage', 'test']) 28 29 30 def __init__(self, oauth_handler, fail_control_handler): 31 """Initializes a Commands handler.""" 32 # A map of device_id's to maps of command ids to command resources 33 self.device_commands = dict() 34 self._num_commands_created = 0 35 self._oauth_handler = oauth_handler 36 self._fail_control_handler = fail_control_handler 37 38 39 def _generate_command_id(self): 40 """@return unique command ID.""" 41 command_id = '%s_%03d' % (uuid.uuid4().hex[0:6], 42 self._num_commands_created) 43 self._num_commands_created += 1 44 return command_id 45 46 def new_device(self, device_id): 47 """Adds knowledge of a device with the given |device_id|. 48 49 This method should be called whenever a new device is created. It 50 populates an empty command dict for each device state. 51 52 @param device_id: Device id to add. 53 54 """ 55 self.device_commands[device_id] = {} 56 57 58 def remove_device(self, device_id): 59 """Removes knowledge of the given device. 60 61 @param device_id: Device id to remove. 62 63 """ 64 del self.device_commands[device_id] 65 66 67 def create_command(self, command_resource): 68 """Creates, queues and returns a new command. 69 70 @param api_key: Api key for the application. 71 @param device_id: Device id of device to send command. 72 @param command_resource: Json dict for command. 73 """ 74 device_id = command_resource.get('deviceId', None) 75 if not device_id: 76 raise server_errors.HTTPError( 77 400, 'Can only create a command if you provide a deviceId.') 78 79 if device_id not in self.device_commands: 80 raise server_errors.HTTPError( 81 400, 'Unknown device with id %s' % device_id) 82 83 if 'name' not in command_resource: 84 raise server_errors.HTTPError( 85 400, 'Missing command name.') 86 87 # Print out something useful (command base.Reboot) 88 logging.info('Received command %s', command_resource['name']) 89 90 # TODO(sosa): Check to see if command is in devices CDD. 91 # Queue command, create it and insert to device->command mapping. 92 command_id = self._generate_command_id() 93 command_resource['id'] = command_id 94 command_resource['state'] = constants.QUEUED_STATE 95 self.device_commands[device_id][command_id] = command_resource 96 return command_resource 97 98 99 @tools.json_out() 100 def GET(self, *args, **kwargs): 101 """Handle GETs against the command API. 102 103 GET .../(command_id) returns a command resource 104 GET .../queue?deviceId=... returns the command queue 105 GET .../?deviceId=... returns the command queue 106 107 Supports both the GET / LIST commands for commands. List lists all 108 devices a user has access to, however, this implementation just returns 109 all devices. 110 111 Raises: 112 server_errors.HTTPError if the device doesn't exist. 113 114 """ 115 self._fail_control_handler.ensure_not_in_failure_mode() 116 args = list(args) 117 requested_command_id = args.pop(0) if args else None 118 device_id = kwargs.get('deviceId', None) 119 if args: 120 raise server_errors.HTTPError(400, 'Unsupported API') 121 if not device_id or device_id not in self.device_commands: 122 raise server_errors.HTTPError( 123 400, 'Can only list commands by valid deviceId.') 124 if requested_command_id is None: 125 requested_command_id = 'queue' 126 127 if not self._oauth_handler.is_request_authorized(): 128 raise server_errors.HTTPError(401, 'Access denied.') 129 130 if requested_command_id == 'queue': 131 # Returns listing (ignores optional parameters). 132 listing = {'kind': 'clouddevices#commandsListResponse'} 133 requested_state = kwargs.get('state', None) 134 listing['commands'] = [] 135 for _, command in self.device_commands[device_id].iteritems(): 136 # Check state for match (if None, just append all of them). 137 if (requested_state is None or 138 requested_state == command['state']): 139 listing['commands'].append(command) 140 logging.info('Returning queue of commands: %r', listing) 141 return listing 142 143 for command_id, resource in self.device_commands[device_id].iteritems(): 144 if command_id == requested_command_id: 145 return self.device_commands[device_id][command_id] 146 147 raise server_errors.HTTPError( 148 400, 'No command with ID=%s found' % requested_command_id) 149 150 151 @tools.json_out() 152 def POST(self, *args, **kwargs): 153 """Creates a new command using the incoming json data.""" 154 # TODO(wiley) We could check authorization here, which should be 155 # a client/owner of the device. 156 self._fail_control_handler.ensure_not_in_failure_mode() 157 data = common_util.parse_serialized_json() 158 if not data: 159 raise server_errors.HTTPError(400, 'Require JSON body') 160 161 return self.create_command(data) 162