1fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa# Copyright 2014 The Chromium OS Authors. All rights reserved.
2fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa# Use of this source code is governed by a BSD-style license that can be
3fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa# found in the LICENSE file.
4fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
5fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa"""Module contains a simple implementation of the devices RPC."""
6fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
7626d491db01f46f7ac9943a2508b1e8e9844d39cChris Sosafrom cherrypy import tools
80f333af62f6f39a9cdc1bd0521345cd3a39c29e3Christopher Wileyimport logging
9c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenkoimport time
10fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
11fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosaimport common
128e9f21c005c170c5868576d86870bc175af8c610Chris Sosafrom fake_device_server import common_util
138e9f21c005c170c5868576d86870bc175af8c610Chris Sosafrom fake_device_server import resource_method
148e9f21c005c170c5868576d86870bc175af8c610Chris Sosafrom fake_device_server import server_errors
15fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
16fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
17fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa# TODO(sosa): All access to this object should technically require auth. Create
18fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa# setters/getters for the auth token for testing.
19fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
20feedba871f9ea080e3562f7d9875f09e63468c55Chris SosaDEVICES_PATH = 'devices'
2112dd813c417478dffb4bf66994eebbb125db7a27Chris Sosa
22fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
23fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosaclass Devices(resource_method.ResourceMethod):
24fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    """A simple implementation of the device interface.
25fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
26fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    A common workflow of using this API is:
27fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
28fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    POST .../ # Creates a new device with id <id>.
29fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    PATCH ..../<id> # Update device state.
30fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    GET .../<id> # Get device state.
31fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    DELETE .../<id> # Delete the device.
32fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    """
33fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
34fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    # Needed for cherrypy to expose this to requests.
35fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    exposed = True
36fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
37fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
38e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen    def __init__(self, resource, commands_instance, oauth_instance,
39e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen                 fail_control_handler):
40fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        """Initializes a registration ticket.
41fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
42fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        @param resource: A resource delegate for storing devices.
43fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        @param commands_instance: Instance of commands method class.
44e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen        @param oauth_instance: Instance of oauth class.
45e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen        @param fail_control_handler: Instance of FailControl.
46fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        """
47fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        super(Devices, self).__init__(resource)
48fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        self.commands_instance = commands_instance
493c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        self._oauth = oauth_instance
50e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen        self._fail_control_handler = fail_control_handler
513c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
523c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
533c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley    def _handle_state_patch(self, device_id, api_key, data):
543c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        """Patch a device's state with the given update data.
553c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
563c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        @param device_id: string device id to update.
573c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        @param api_key: string api_key to support this resource delegate.
583c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        @param data: json blob provided to patchState API.
593c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
603c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        """
613c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        # TODO(wiley) this.
623c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
633c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
643c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley    def _validate_device_resource(self, resource):
653c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        # Verify required keys exist in the device draft.
663c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        if not resource:
673c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley            raise server_errors.HTTPError(400, 'Empty device resource.')
683c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
69735fec04a0041878cddd4877433414f528fac68dVitaly Buka        for key in ['name', 'channel']:
703c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley            if key not in resource:
713c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                raise server_errors.HTTPError(400, 'Must specify %s' % key)
723c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
733c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        # Add server fields.
743c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        resource['kind'] = 'clouddevices#device'
75c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko        current_time_ms = str(int(round(time.time() * 1000)))
76c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko        resource['creationTimeMs'] = current_time_ms
77c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko        resource['lastUpdateTimeMs'] = current_time_ms
78c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko        resource['lastSeenTimeMs'] = current_time_ms
79fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
80fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
81fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    def create_device(self, api_key, device_config):
82fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        """Creates a new device given the device_config.
83fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
84fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        @param api_key: Api key for the application.
85fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        @param device_config: Json dict for the device.
86fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        @raises server_errors.HTTPError: if the config is missing a required key
87fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        """
880f333af62f6f39a9cdc1bd0521345cd3a39c29e3Christopher Wiley        logging.info('Creating device with api_key=%s and device_config=%r',
890f333af62f6f39a9cdc1bd0521345cd3a39c29e3Christopher Wiley                     api_key, device_config)
903c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        self._validate_device_resource(device_config)
91fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        new_device = self.resource.update_data_val(None, api_key,
92fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa                                                   data_in=device_config)
933c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        self.commands_instance.new_device(new_device['id'])
94fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        return new_device
95fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
96fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
97626d491db01f46f7ac9943a2508b1e8e9844d39cChris Sosa    @tools.json_out()
98fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    def GET(self, *args, **kwargs):
99fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        """GET .../(device_id) gets device info or lists all devices.
100fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
101fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        Supports both the GET / LIST commands for devices. List lists all
102fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        devices a user has access to, however, this implementation just returns
103fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        all devices.
104fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
105fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        Raises:
106fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa            server_errors.HTTPError if the device doesn't exist.
107fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        """
108e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen        self._fail_control_handler.ensure_not_in_failure_mode()
109fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        id, api_key, _ = common_util.parse_common_args(args, kwargs)
110c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko        if not api_key:
111c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko            access_token = common_util.get_access_token()
112c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko            api_key = self._oauth.get_api_key_from_access_token(access_token)
113fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        if id:
114626d491db01f46f7ac9943a2508b1e8e9844d39cChris Sosa            return self.resource.get_data_val(id, api_key)
115fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        else:
116fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa            # Returns listing (ignores optional parameters).
117fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa            listing = {'kind': 'clouddevices#devicesListResponse'}
118fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa            listing['devices'] = self.resource.get_data_vals()
119626d491db01f46f7ac9943a2508b1e8e9844d39cChris Sosa            return listing
120fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
121fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
122626d491db01f46f7ac9943a2508b1e8e9844d39cChris Sosa    @tools.json_out()
123fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    def POST(self, *args, **kwargs):
1243c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        """Handle POSTs for a device.
1253c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
1263c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        Supported APIs include:
1273c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
1283c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        POST /devices/<device-id>/patchState
1293c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
1303c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        """
131e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen        self._fail_control_handler.ensure_not_in_failure_mode()
1323c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        args = list(args)
1333c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        device_id = args.pop(0) if args else None
1343c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        operation = args.pop(0) if args else None
1353c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        if device_id is None or operation != 'patchState':
1363c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley            raise server_errors.HTTPError(400, 'Unsupported operation.')
137fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        data = common_util.parse_serialized_json()
1383c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        access_token = common_util.get_access_token()
1393c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        api_key = self._oauth.get_api_key_from_access_token(access_token)
1403c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        self._handle_state_patch(device_id, api_key, data)
1413c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        return {'state': self.resource.get_data_val(device_id,
1423c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                                                    api_key)['state']}
143fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
144fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
1453c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley    @tools.json_out()
1463c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley    def PUT(self, *args, **kwargs):
1473c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        """Update an existing device using the incoming json data.
1483c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
1493c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        On startup, devices make a request like:
1503c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
1513c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        PUT http://<server-host>/devices/<device-id>
1523c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
1533c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        {'channel': {'supportedType': 'xmpp'},
1543c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley         'commandDefs': {},
1553c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley         'description': 'test_description ',
1563c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley         'displayName': 'test_display_name ',
1573c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley         'id': '4471f7',
1583c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley         'location': 'test_location ',
1593c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley         'name': 'test_device_name',
1603c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley         'state': {'base': {'firmwareVersion': '6771.0.2015_02_09_1429',
1613c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                            'isProximityTokenRequired': False,
1623c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                            'localDiscoveryEnabled': False,
1633c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                            'manufacturer': '',
1643c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                            'model': '',
1653c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                            'serialNumber': '',
1663c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                            'supportUrl': '',
1673c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                            'updateUrl': ''}}}
1683c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
1693c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        This PUT has no API key, but comes with an OAUTH access token.
1703c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
1713c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        """
172e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen        self._fail_control_handler.ensure_not_in_failure_mode()
1733c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        device_id, _, _ = common_util.parse_common_args(args, kwargs)
1743c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        access_token = common_util.get_access_token()
17557f2ed1cddace69c180e956d12b56a60150112ceNathan Bullock        if not access_token:
17657f2ed1cddace69c180e956d12b56a60150112ceNathan Bullock            raise server_errors.HTTPError(401, 'Access denied.')
1773c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        api_key = self._oauth.get_api_key_from_access_token(access_token)
1783c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        data = common_util.parse_serialized_json()
1793c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        self._validate_device_resource(data)
1803c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley
1813c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        logging.info('Updating device with id=%s and device_config=%r',
1823c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                     device_id, data)
1833c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        new_device = self.resource.update_data_val(device_id, api_key,
1843c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley                                                   data_in=data)
185c5e287d5afa2a565674154437b744acbe77e1912Alex Vakulenko        return data
186fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
187fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
188fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa    def DELETE(self, *args, **kwargs):
189fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        """Deletes the given device.
190fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
191fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        Format of this call is:
192fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        DELETE .../device_id
193fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa
194fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        Raises:
195fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa            server_errors.HTTPError if the device doesn't exist.
196fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        """
197e24dba0976c3e97df4640c6b89502a430cc91ebbDavid Zeuthen        self._fail_control_handler.ensure_not_in_failure_mode()
198fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        id, api_key, _ = common_util.parse_common_args(args, kwargs)
199fb08e82e29ad2232361c6662c3842f5617f4032aChris Sosa        self.resource.del_data_val(id, api_key)
2003c02dbac644d951bc264158d3f4efea577345b84Christopher Wiley        self.commands_instance.remove_device(id)
201