From 4a66c896f254e5911d41ebfdeffd1b34e8fdb8aa Mon Sep 17 00:00:00 2001
From: Megan Henning <meganhenning@flywheel.io>
Date: Mon, 28 Nov 2016 17:19:15 -0600
Subject: [PATCH] Add device handler

---
 api/api.py                    |   8 +++
 api/base.py                   |   5 +-
 api/handlers/devicehandler.py | 124 ++++++++++++++++++++++++++++++++++
 3 files changed, 135 insertions(+), 2 deletions(-)
 create mode 100644 api/handlers/devicehandler.py

diff --git a/api/api.py b/api/api.py
index 721415be..52d9a2f5 100644
--- a/api/api.py
+++ b/api/api.py
@@ -25,6 +25,7 @@ from .handlers import resolvehandler
 from .handlers import searchhandler
 from .handlers import schemahandler
 from .handlers import reporthandler
+from .handlers import devicehandler
 from .request import SciTranRequest
 
 log = config.log
@@ -115,6 +116,13 @@ routes = [
         webapp2.Route(_format(r'/<uid:{user_id_re}>/groups'),   grouphandler.GroupHandler, handler_method='get_all', methods=['GET'], name='groups'),
         webapp2.Route(_format(r'/<uid:{user_id_re}>/avatar'),   userhandler.UserHandler, handler_method='avatar', methods=['GET'], name='avatar'),
     ]),
+    webapp2.Route(r'/api/devices',                              devicehandler.DeviceHandler, name='device_list', handler_method='get_all', methods=['GET']),
+    webapp2.Route(r'/api/devices',                              devicehandler.DeviceHandler, methods=['POST']),
+    webapp2_extras.routes.PathPrefixRoute(r'/api/devices', [
+        webapp2.Route(r'/status',                               devicehandler.DeviceHandler, handler_method='get_status', methods=['GET']),
+        webapp2.Route(r'/self',                                 devicehandler.DeviceHandler, handler_method='get_self', methods=['GET']),
+        webapp2.Route(_format(r'/<device_id:[^/]+>'),           devicehandler.DeviceHandler, name='device_details', methods=['GET']),
+    ]),
     webapp2.Route(r'/api/jobs',             JobsHandler),
     webapp2_extras.routes.PathPrefixRoute(r'/api/jobs', [
         webapp2.Route(r'/next',             JobsHandler, handler_method='next', methods=['GET']),
diff --git a/api/base.py b/api/base.py
index 7412d634..1ff301d0 100644
--- a/api/base.py
+++ b/api/base.py
@@ -241,9 +241,10 @@ class RequestHandler(webapp2.RequestHandler):
                 }, {
                     '$set': {
                         '_id': self.origin['id'],
-                        'last-seen': datetime.datetime.utcnow(),
+                        'last_seen': datetime.datetime.utcnow(),
                         'method': self.origin['method'],
-                        'name': self.origin['name']
+                        'name': self.origin['name'],
+                        'errors': None # Reset errors list if device checks in
                     }
                 },
                 upsert=True,
diff --git a/api/handlers/devicehandler.py b/api/handlers/devicehandler.py
new file mode 100644
index 00000000..1890abc8
--- /dev/null
+++ b/api/handlers/devicehandler.py
@@ -0,0 +1,124 @@
+import datetime as dt
+
+from .. import base
+from .. import config
+from .. import util
+from ..dao import containerstorage, APIPermissionException, APINotFoundException
+from ..types import Origin
+
+log = config.log
+
+Status = util.Enum('Origin', {
+    'ok':       'ok',       # Device's last seen time is shorter than expected interval for checkin, no errors listed.
+    'missing':  'missing',  # Device's last seen time is longer than the expected interval for checkin, but no errors listed.
+    'error':    'error' ,   # Device has errors listed.
+    'unknown':  'unknown'   # Device did not set an expected checkin interval.
+})
+
+
+def require_login(handler_method):
+    """
+    A decorator to ensure the request is not a public request.
+
+    Accepts superuser and non-superuser requests.
+    Accepts drone and user requests.
+    """
+    def check_login(self, *args, **kwargs):
+        if self.public_request:
+            raise APIPermissionException('Login required.')
+        return handler_method(self, *args, **kwargs)
+    return check_login
+
+def require_superuser(handler_method):
+    """
+    A decorator to ensure the request is made as superuser.
+
+    Accepts drone and user requests.
+    """
+    def check_superuser(self, *args, **kwargs):
+        if not self.superuser_request:
+            raise APIPermissionException('Superuser required.')
+        return handler_method(self, *args, **kwargs)
+    return check_superuser
+
+def require_drone(handler_method):
+    """
+    A decorator to ensure the request is made as a drone.
+
+    Will also ensure superuser, which is implied with a drone request.
+    """
+    def check_drone(self, *args, **kwargs):
+        if self.origin.get('type', '') != Origin.device:
+            raise APIPermissionException('Drone request required.')
+        if not self.superuser_request:
+            raise APIPermissionException('Superuser required.')
+        return handler_method(self, *args, **kwargs)
+    return check_drone
+
+
+class DeviceHandler(base.RequestHandler):
+
+    def __init__(self, request=None, response=None):
+        super(DeviceHandler, self).__init__(request, response)
+        self.storage = containerstorage.ContainerStorage('devices', use_object_id=False)
+
+    @require_login
+    def get(self, device_id):
+        return self.storage.get_container(device_id)
+
+    @require_login
+    def get_all(self):
+        return self.storage.get_all_el(None, None, None)
+
+    @require_drone
+    def get_self(self):
+        device_id = self.origin.get('id', '')
+        return self.storage.get_container(device_id)
+
+    @require_drone
+    def post(self):
+        device_id = self.origin.get('id', '')
+        payload = self.request.json_body
+        #validators.validate_data(payload, 'device.json', 'input', 'PUT', optional=True)
+
+        result = self.storage.update_el(device_id, payload)
+        if result.modified_count == 1:
+            return {'modified': result.modified_count}
+        else:
+            raise APINotFoundException('Device with id {} not found, state not updated'.format(device_id))
+
+    @require_superuser
+    def delete(self, device_id):
+        raise NotImplementedError()
+
+    def get_status(self):
+        devices = self.storage.get_all_el(None, None, None)
+        response = {}
+        now = dt.datetime.now()
+        for d in devices:
+            d_obj = {}
+            d_obj['last_seen'] =  d.get('last_seen')
+
+            if d.get('errors'):
+                d_obj['status'] = str(Status.error)
+                d_obj['errors'] = d.get('errors')
+                response[d.get('_id')] = d_obj.copy()
+
+            elif not d.get('interval'):
+                d_obj['status'] = str(Status.unknown)
+                response[d.get('_id')] = d_obj.copy()
+
+            elif (now-d.get('last_seen', now)).seconds > d.get('interval'):
+                d_obj['status'] = str(Status.missing)
+                response[d.get('_id')] = d_obj.copy()
+
+            else:
+                d_obj['status'] = str(Status.ok)
+                response[d.get('_id')] = d_obj.copy()
+
+        return response
+
+
+
+
+
-- 
GitLab