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 devices RPC."""
6
7from cherrypy import tools
8import logging
9import time
10
11import common
12from fake_device_server import common_util
13from fake_device_server import resource_method
14from fake_device_server import server_errors
15
16
17# TODO(sosa): All access to this object should technically require auth. Create
18# setters/getters for the auth token for testing.
19
20DEVICES_PATH = 'devices'
21
22
23class Devices(resource_method.ResourceMethod):
24    """A simple implementation of the device interface.
25
26    A common workflow of using this API is:
27
28    POST .../ # Creates a new device with id <id>.
29    PATCH ..../<id> # Update device state.
30    GET .../<id> # Get device state.
31    DELETE .../<id> # Delete the device.
32    """
33
34    # Needed for cherrypy to expose this to requests.
35    exposed = True
36
37
38    def __init__(self, resource, commands_instance, oauth_instance,
39                 fail_control_handler):
40        """Initializes a registration ticket.
41
42        @param resource: A resource delegate for storing devices.
43        @param commands_instance: Instance of commands method class.
44        @param oauth_instance: Instance of oauth class.
45        @param fail_control_handler: Instance of FailControl.
46        """
47        super(Devices, self).__init__(resource)
48        self.commands_instance = commands_instance
49        self._oauth = oauth_instance
50        self._fail_control_handler = fail_control_handler
51
52
53    def _handle_state_patch(self, device_id, api_key, data):
54        """Patch a device's state with the given update data.
55
56        @param device_id: string device id to update.
57        @param api_key: string api_key to support this resource delegate.
58        @param data: json blob provided to patchState API.
59
60        """
61        # TODO(wiley) this.
62
63
64    def _validate_device_resource(self, resource):
65        # Verify required keys exist in the device draft.
66        if not resource:
67            raise server_errors.HTTPError(400, 'Empty device resource.')
68
69        for key in ['name', 'channel']:
70            if key not in resource:
71                raise server_errors.HTTPError(400, 'Must specify %s' % key)
72
73        # Add server fields.
74        resource['kind'] = 'clouddevices#device'
75        current_time_ms = str(int(round(time.time() * 1000)))
76        resource['creationTimeMs'] = current_time_ms
77        resource['lastUpdateTimeMs'] = current_time_ms
78        resource['lastSeenTimeMs'] = current_time_ms
79
80
81    def create_device(self, api_key, device_config):
82        """Creates a new device given the device_config.
83
84        @param api_key: Api key for the application.
85        @param device_config: Json dict for the device.
86        @raises server_errors.HTTPError: if the config is missing a required key
87        """
88        logging.info('Creating device with api_key=%s and device_config=%r',
89                     api_key, device_config)
90        self._validate_device_resource(device_config)
91        new_device = self.resource.update_data_val(None, api_key,
92                                                   data_in=device_config)
93        self.commands_instance.new_device(new_device['id'])
94        return new_device
95
96
97    @tools.json_out()
98    def GET(self, *args, **kwargs):
99        """GET .../(device_id) gets device info or lists all devices.
100
101        Supports both the GET / LIST commands for devices. List lists all
102        devices a user has access to, however, this implementation just returns
103        all devices.
104
105        Raises:
106            server_errors.HTTPError if the device doesn't exist.
107        """
108        self._fail_control_handler.ensure_not_in_failure_mode()
109        id, api_key, _ = common_util.parse_common_args(args, kwargs)
110        if not api_key:
111            access_token = common_util.get_access_token()
112            api_key = self._oauth.get_api_key_from_access_token(access_token)
113        if id:
114            return self.resource.get_data_val(id, api_key)
115        else:
116            # Returns listing (ignores optional parameters).
117            listing = {'kind': 'clouddevices#devicesListResponse'}
118            listing['devices'] = self.resource.get_data_vals()
119            return listing
120
121
122    @tools.json_out()
123    def POST(self, *args, **kwargs):
124        """Handle POSTs for a device.
125
126        Supported APIs include:
127
128        POST /devices/<device-id>/patchState
129
130        """
131        self._fail_control_handler.ensure_not_in_failure_mode()
132        args = list(args)
133        device_id = args.pop(0) if args else None
134        operation = args.pop(0) if args else None
135        if device_id is None or operation != 'patchState':
136            raise server_errors.HTTPError(400, 'Unsupported operation.')
137        data = common_util.parse_serialized_json()
138        access_token = common_util.get_access_token()
139        api_key = self._oauth.get_api_key_from_access_token(access_token)
140        self._handle_state_patch(device_id, api_key, data)
141        return {'state': self.resource.get_data_val(device_id,
142                                                    api_key)['state']}
143
144
145    @tools.json_out()
146    def PUT(self, *args, **kwargs):
147        """Update an existing device using the incoming json data.
148
149        On startup, devices make a request like:
150
151        PUT http://<server-host>/devices/<device-id>
152
153        {'channel': {'supportedType': 'xmpp'},
154         'commandDefs': {},
155         'description': 'test_description ',
156         'displayName': 'test_display_name ',
157         'id': '4471f7',
158         'location': 'test_location ',
159         'name': 'test_device_name',
160         'state': {'base': {'firmwareVersion': '6771.0.2015_02_09_1429',
161                            'isProximityTokenRequired': False,
162                            'localDiscoveryEnabled': False,
163                            'manufacturer': '',
164                            'model': '',
165                            'serialNumber': '',
166                            'supportUrl': '',
167                            'updateUrl': ''}}}
168
169        This PUT has no API key, but comes with an OAUTH access token.
170
171        """
172        self._fail_control_handler.ensure_not_in_failure_mode()
173        device_id, _, _ = common_util.parse_common_args(args, kwargs)
174        access_token = common_util.get_access_token()
175        if not access_token:
176            raise server_errors.HTTPError(401, 'Access denied.')
177        api_key = self._oauth.get_api_key_from_access_token(access_token)
178        data = common_util.parse_serialized_json()
179        self._validate_device_resource(data)
180
181        logging.info('Updating device with id=%s and device_config=%r',
182                     device_id, data)
183        new_device = self.resource.update_data_val(device_id, api_key,
184                                                   data_in=data)
185        return data
186
187
188    def DELETE(self, *args, **kwargs):
189        """Deletes the given device.
190
191        Format of this call is:
192        DELETE .../device_id
193
194        Raises:
195            server_errors.HTTPError if the device doesn't exist.
196        """
197        self._fail_control_handler.ensure_not_in_failure_mode()
198        id, api_key, _ = common_util.parse_common_args(args, kwargs)
199        self.resource.del_data_val(id, api_key)
200        self.commands_instance.remove_device(id)
201