Ejemplo n.º 1
0
class CoapServer(ThreadedApplication):
    """
    This special Application runs a CoAP server so that other modules may use it to store CoAP resources
    for external nodes (e.g. those running a CoapSensor) to GET/POST/PUT/etc.  It allows for
    defining custom CoapResources that can handle application-specific logic.

    NOTE: much of the logic for how the server interacts with external requests resides in the CoapResource classes.
    They actually handle exposing APIs, calling callbacks when they're activated, and managing SensedEvents from
    remote sources.
    """

    _DEFAULT_COAP_SERVER_NAME = '__default_scale_coap_server__'

    def __init__(self,
                 broker,
                 events_root=None,
                 server_name=_DEFAULT_COAP_SERVER_NAME,
                 hostname="0.0.0.0",
                 port=DEFAULT_COAP_PORT,
                 multicast=False,
                 **kwargs):
        """
        Simple constructor.  When on_start is called, the server will actually be run.
        :param broker: internal scale_client broker
        :param events_root: if specified, will allow remote clients to store events at this root
        :param server_name: the user-assigned name for this server so as to distinguish between multiple running ones
        (if unspecified only a single server without an explicit name can be run)
        :param hostname: hostname/IP address to bind server to
        :param port: port to run server on
        :param multicast: optionally enable handling multicast requests
        :param kwargs:
        """
        super(CoapServer, self).__init__(broker=broker, **kwargs)

        self._server = None  # Type: coapthon.server.coap.CoAP
        self._events_root = events_root

        self._hostname = hostname
        self._port = port
        self._multicast = multicast
        if multicast and hostname != coapthon.defines.ALL_COAP_NODES:
            log.warning(
                "underlying CoAPthon library currently only supports the ALL_COAP_NODES multicast address of %s"
                % coapthon.defines.ALL_COAP_NODES)

        self._server_running = False
        self._is_connected = False

        # Users need to access the server in their other CoAP-based modules,
        # so we keep a registry of them indexed by the user-assigned name.
        # XXX: we just set this as an object on the broker instance so it doesn't persist across ScaleClients (mostly for testing)
        # TODO: use Application.name and a registration service for this
        try:
            instances = broker._coap_server_instances
        except AttributeError:
            instances = broker._coap_server_instances = dict()

        # Register this server as a currently-running instance
        if server_name in instances:
            raise ValueError(
                "A CoapServer with server_name %s already exists!  Aborting creation of the second one..."
                % server_name)
        instances[server_name] = self
        self._server_name = server_name

    def __run_server(self):
        log.debug("starting CoAP server at IP:port %s:%d" %
                  (self._hostname, self._port))

        try:
            self._server = CoapthonServer(self._hostname, self._port,
                                          self._multicast)
        except TypeError:
            # coapthon 4.0.2 has a different constructor API
            self._server = CoapthonServer((self._hostname, self._port),
                                          self._multicast)

        if self._events_root is not None:
            root_event = self.make_event(source=self._events_root,
                                         data='root of SCALE events resources',
                                         priority=1)
            self.store_event(root_event, self._events_root)

        self._server_running = True
        self._notify_running()

        # Listen for remote connections GETting data, etc.
        self._server.listen()

    # CIRCUITS-SPECIFIC: not entirely, but may be broker by switch to another runtime...
    # TODO: document this!
    from scale_client.core.sensed_event import Event

    class CoapServerRunning(Event):
        pass

    def _notify_running(self):
        """Publishes an event to let other apps know that this server is now up and running.
        They should be careful to not use this server until this notification!"""
        event = self.CoapServerRunning(self)
        self.publish(event)

    def on_stop(self):
        self._server.close()
        self._server_running = False
        super(CoapServer, self).on_stop()

    def on_start(self):
        self.run_in_background(self.__run_server)

    def store_event(self,
                    event,
                    path=None,
                    disable_post=False,
                    disable_put=False,
                    disable_delete=False):
        """
        Stores the event as a resource at the given path in the CoAP server.
        A SensedEventCoapResource stored this way will be exposed externally at the given CoAP
        path so that GET, PUT, etc. can be called on it unless you disable them through
        the parameters.

        :param event:
        :type event: scale_client.core.sensed_event.SensedEvent
        :param path: the path at which the event should be stored (default=event.get_type())
        :param disable_post:
        :param disable_put:
        :param disable_delete:
        :return:
        """

        if path is None:
            path = event.event_type
        # XXX: Ensure path is formatted properly for CoAP's internals
        if not path.startswith('/'):
            path = '/' + path
        if path.endswith('/'):
            path = path[:-1]

        assert isinstance(self._server, CoapthonServer)  # for type annotation

        # Update the resource if it exists and notify possible observers (unfortunately,
        # CoAPthon doesn't have a clean way to do exactly this since most of the lower APIs
        # assume request/response/transaction objects).
        # Furthermore, we don't want the render_PUT API to fire as we have it currently
        # since that would publish the event internally again.
        try:
            res = self._server.root[path]
            assert isinstance(res, SensedEventCoapResource)
            res.event = event
            self._server.notify(res)
            log.debug("updated resource at path: %s" % path)

        # Create the resource since it didn't exist.
        # We add callbacks so modifications to the resource are published internally.
        except KeyError:
            # post_cb = None if disable_post else lambda req, res: self.publish(res.event)
            # put_cb = None if disable_put else lambda req, res: self.publish(res.event)
            def __default_event_callback(request, resource):
                """
                Default callback to enable logging and internally publish new events.
                :type request: coapthon.messages.request.Request
                :return:
                """
                log.debug("new event received via %s: %s" %
                          (coap_code_to_name(request.code), resource.event))
                return self.publish(resource.event)

            post_cb = None if disable_post else __default_event_callback
            put_cb = None if disable_put else __default_event_callback

            new_resource = SensedEventCoapResource(
                event,
                name=event.event_type,
                get_callback=lambda x, y: y,  # always enabled
                post_callback=post_cb,
                put_callback=put_cb,
                delete_callback=None if disable_delete else lambda x, y: True)
            self.store_resource(path, resource=new_resource)

    def store_resource(self, path, resource):
        """
        Stores a CoapResource in the server at the given path.
        :param path:
        :param resource:
        :return:
        """

        res = self._server.add_resource(path, resource)
        log.debug("%s added resource to path: %s" %
                  ('successfully' if res else 'unsuccessfully', path))

    def is_running(self):
        return self._server_running

    def register_api(self,
                     path,
                     name,
                     get_callback=None,
                     put_callback=None,
                     post_callback=None,
                     delete_callback=None,
                     error_callback=None,
                     observable=False,
                     allow_children=False,
                     visible=True):
        """
        Registers the specified callbacks at the given path to create a custom API.  This will create a Resource
        at that path with the given properties so that a CoAP client can GET/PUT/POST/DELETE it and have the
        corresponding callback fired in response.  If no callback is specified for that method, it returns a
        METHOD_NOT_ALLOWED response.  If an exception is encountered, error_callback is called if specified
        where the default is to simply log the error.

        CALLBACK DEFINITIONS:
        The callbacks should accept the CoAP Request object and the relevant resource as parameters.  The
        error_callback should additionally accept the exception encountered as its third parameter.  If you
        can recover from the error, simply return the resource from the callback after doing so.  Otherwise,
        raise an exception noting that raising NotImplementedError will return a METHOD_NOT_ALLOWED response.
        Thus the callbacks have the following form:

        callback(coapthon.messages.request.Request, ScaleCoapResource, [Exception]) -> ScaleCoapResource

        :param path: full pathname e.g. /sensors/temp0/interval
        :param name: name of the API endpoint (useful when a client does a DISCOVER request)
        :param get_callback:
        :param put_callback:
        :param post_callback:
        :param delete_callback:
        :param error_callback:
        :param observable:
        :param allow_children:
        :param visible:
        :return: the newly added resource
        """
        # TODO: maybe we should use the advanced interface, optionally the separate one, in order to modify response?

        if error_callback is None:
            error_callback = lambda req, res, err: log.error(
                "coap api error with resource %s while answering request %s:\n %s"
                % (res, req, err))
        res = ScaleCoapResource(name,
                                get_callback=get_callback,
                                put_callback=put_callback,
                                post_callback=post_callback,
                                delete_callback=delete_callback,
                                error_callback=error_callback,
                                coap_server=self._server,
                                visible=visible,
                                observable=observable,
                                allow_children=allow_children)
        self.store_resource(path, res)
        return res
Ejemplo n.º 2
0
class CoapServer(ThreadedApplication):
    """
    This special Application runs a CoAP server so that other modules may use it to store CoAP resources
    for external nodes (e.g. those running a CoapSensor) to GET/POST/PUT/etc.  It allows for
    defining custom CoapResources that can handle application-specific logic.

    NOTE: much of the logic for how the server interacts with external requests resides in the CoapResource classes.
    They actually handle exposing APIs, calling callbacks when they're activated, and managing SensedEvents from
    remote sources.
    """

    _DEFAULT_COAP_SERVER_NAME = '__default_scale_coap_server__'

    def __init__(self, broker,
                 events_root=None,
                 server_name=_DEFAULT_COAP_SERVER_NAME,
                 hostname="0.0.0.0",
                 port=DEFAULT_COAP_PORT,
                 multicast=False,
                 **kwargs):
        """
        Simple constructor.  When on_start is called, the server will actually be run.
        :param broker: internal scale_client broker
        :param events_root: if specified, will allow remote clients to store events at this root
        :param server_name: the user-assigned name for this server so as to distinguish between multiple running ones
        (if unspecified only a single server without an explicit name can be run)
        :param hostname: hostname/IP address to bind server to
        :param port: port to run server on
        :param multicast: optionally enable handling multicast requests
        :param kwargs:
        """
        super(CoapServer, self).__init__(broker=broker, **kwargs)

        self._server = None  # Type: coapthon.server.coap.CoAP
        self._events_root = events_root

        self._hostname = hostname
        self._port = port
        self._multicast = multicast
        if multicast and hostname != coapthon.defines.ALL_COAP_NODES:
            log.warning("underlying CoAPthon library currently only supports the ALL_COAP_NODES multicast address of %s" % coapthon.defines.ALL_COAP_NODES)

        self._server_running = False
        self._is_connected = False

        # Users need to access the server in their other CoAP-based modules,
        # so we keep a registry of them indexed by the user-assigned name.
        # XXX: we just set this as an object on the broker instance so it doesn't persist across ScaleClients (mostly for testing)
        # TODO: use Application.name and a registration service for this
        try:
            instances = broker._coap_server_instances
        except AttributeError:
            instances = broker._coap_server_instances = dict()

        # Register this server as a currently-running instance
        if server_name in instances:
            raise ValueError("A CoapServer with server_name %s already exists!  Aborting creation of the second one..." % server_name)
        instances[server_name] = self
        self._server_name = server_name

    def __run_server(self):
        log.debug("starting CoAP server at IP:port %s:%d" % (self._hostname, self._port))

        try:
            self._server = CoapthonServer(self._hostname, self._port, self._multicast)
        except TypeError:
            # coapthon 4.0.2 has a different constructor API
            self._server = CoapthonServer((self._hostname, self._port), self._multicast)

        if self._events_root is not None:
            root_event = self.make_event(source=self._events_root, data='root of SCALE events resources', priority=1)
            self.store_event(root_event, self._events_root)

        self._server_running = True
        self._notify_running()

        # Listen for remote connections GETting data, etc.
        self._server.listen()

    # CIRCUITS-SPECIFIC: not entirely, but may be broker by switch to another runtime...
    # TODO: document this!
    from scale_client.core.sensed_event import Event
    class CoapServerRunning(Event):
        pass

    def _notify_running(self):
        """Publishes an event to let other apps know that this server is now up and running.
        They should be careful to not use this server until this notification!"""
        event = self.CoapServerRunning(self)
        self.publish(event)

    def on_stop(self):
        self._server.close()
        self._server_running = False
        super(CoapServer, self).on_stop()

    def on_start(self):
        self.run_in_background(self.__run_server)

    def store_event(self, event, path=None, disable_post=False, disable_put=False, disable_delete=False):
        """
        Stores the event as a resource at the given path in the CoAP server.
        A SensedEventCoapResource stored this way will be exposed externally at the given CoAP
        path so that GET, PUT, etc. can be called on it unless you disable them through
        the parameters.

        :param event:
        :type event: scale_client.core.sensed_event.SensedEvent
        :param path: the path at which the event should be stored (default=event.get_type())
        :param disable_post:
        :param disable_put:
        :param disable_delete:
        :return:
        """

        if path is None:
            path = event.event_type
        # XXX: Ensure path is formatted properly for CoAP's internals
        if not path.startswith('/'):
            path = '/' + path
        if path.endswith('/'):
            path = path[:-1]

        assert isinstance(self._server, CoapthonServer)  # for type annotation

        # Update the resource if it exists and notify possible observers (unfortunately,
        # CoAPthon doesn't have a clean way to do exactly this since most of the lower APIs
        # assume request/response/transaction objects).
        # Furthermore, we don't want the render_PUT API to fire as we have it currently
        # since that would publish the event internally again.
        try:
            res = self._server.root[path]
            assert isinstance(res, SensedEventCoapResource)
            res.event = event
            self._server.notify(res)
            log.debug("updated resource at path: %s" % path)

        # Create the resource since it didn't exist.
        # We add callbacks so modifications to the resource are published internally.
        except KeyError:
            # post_cb = None if disable_post else lambda req, res: self.publish(res.event)
            # put_cb = None if disable_put else lambda req, res: self.publish(res.event)
            def __default_event_callback(request, resource):
                """
                Default callback to enable logging and internally publish new events.
                :type request: coapthon.messages.request.Request
                :return:
                """
                log.debug("new event received via %s: %s" % (coap_code_to_name(request.code), resource.event))
                return self.publish(resource.event)

            post_cb = None if disable_post else __default_event_callback
            put_cb = None if disable_put else __default_event_callback

            new_resource = SensedEventCoapResource(event, name=event.event_type,
                                                   get_callback=lambda x, y: y,  # always enabled
                                                   post_callback=post_cb, put_callback=put_cb,
                                                   delete_callback=None if disable_delete else lambda x, y: True)
            self.store_resource(path, resource=new_resource)

    def store_resource(self, path, resource):
        """
        Stores a CoapResource in the server at the given path.
        :param path:
        :param resource:
        :return:
        """

        res = self._server.add_resource(path, resource)
        log.debug("%s added resource to path: %s" % ('successfully' if res else 'unsuccessfully', path))

    def is_running(self):
        return self._server_running

    def register_api(self, path, name, get_callback=None, put_callback=None,
                     post_callback=None, delete_callback=None, error_callback=None,
                     observable=False, allow_children=False, visible=True):
        """
        Registers the specified callbacks at the given path to create a custom API.  This will create a Resource
        at that path with the given properties so that a CoAP client can GET/PUT/POST/DELETE it and have the
        corresponding callback fired in response.  If no callback is specified for that method, it returns a
        METHOD_NOT_ALLOWED response.  If an exception is encountered, error_callback is called if specified
        where the default is to simply log the error.

        CALLBACK DEFINITIONS:
        The callbacks should accept the CoAP Request object and the relevant resource as parameters.  The
        error_callback should additionally accept the exception encountered as its third parameter.  If you
        can recover from the error, simply return the resource from the callback after doing so.  Otherwise,
        raise an exception noting that raising NotImplementedError will return a METHOD_NOT_ALLOWED response.
        Thus the callbacks have the following form:

        callback(coapthon.messages.request.Request, ScaleCoapResource, [Exception]) -> ScaleCoapResource

        :param path: full pathname e.g. /sensors/temp0/interval
        :param name: name of the API endpoint (useful when a client does a DISCOVER request)
        :param get_callback:
        :param put_callback:
        :param post_callback:
        :param delete_callback:
        :param error_callback:
        :param observable:
        :param allow_children:
        :param visible:
        :return: the newly added resource
        """
        # TODO: maybe we should use the advanced interface, optionally the separate one, in order to modify response?

        if error_callback is None:
            error_callback = lambda req, res, err: log.error("coap api error with resource %s while answering request %s:\n %s" % (res, req, err))
        res = ScaleCoapResource(name, get_callback=get_callback, put_callback=put_callback,
                                post_callback=post_callback, delete_callback=delete_callback,
                                error_callback=error_callback,
                                coap_server=self._server, visible=visible,
                                observable=observable, allow_children=allow_children)
        self.store_resource(path, res)
        return res