Ejemplo n.º 1
0
 def __init__(self):
     self.topic_prefix = 'iottalk/api'
     self.topic = '{}/req/#'.format(self.topic_prefix)
     self.client = JsonClient()
     self.client.on_connect = self._on_connect
     self.client.on_json_message = self.msg_dispatcher
     self.client.connect(config.mqtt_conf['host'],
                         port=config.mqtt_conf['port'])
     self.gapi_client = {}  # graph api client
     self.dapi_client = {}  # device api client
Ejemplo n.º 2
0
def test_JsonClient_pub_sub(lock, topic):
    def on_message(client, userdata, msg):
        on_message.msg = msg
        userdata.release()

    on_message.msg = None

    # setup
    c = JsonClient(userdata=lock)
    c.connect('localhost')
    c.subscribe(topic)

    c.on_json_message = on_message
    c.publish_json(topic, {'answer': 42})

    c.loop_start()

    lock.acquire()

    assert c.on_json_message
    assert on_message.msg.topic == topic
    assert on_message.msg.payload == {'answer': 42}

    # teardown
    c.disconnect()
    c.loop_stop()
Ejemplo n.º 3
0
class APIServer(object):
    '''
    For the request topic, we use the pattern
    ``iottalk/api/req/<client-id>/...``.
    Each request topic has a corresponding *response* topic.

    The available api calls:
        - graph api
        - device api


    Graph API
    ----------------------------------------------------------------------

    The topic of graph api call should be
    ``iottalk/api/req/<client-id>/graph/<graph-id>``.

    The ``detach`` opcode is also available on:

    - ``iottalk/api/req/<client-id>/graph`` - for detaching all graphs
    - ``iottalk/api/req/client_id`` - for detaching all services

    The graph operation has following json format::

        {
            'op': 'operation',
            'other_op_param': 'value',
        }

    The available operation code:
        - ``attach``
        - ``detach``
        - ``add_funcs``
        - ``rm_funcs``
        - ``add_link``
        - ``rm_link``
        - ``set_join``: setup or changing the join function

    ``attach`` and ``detach`` request is for graph creating and destroying::

        {
            'op': 'attach|detach'
        }

    .. note::
        Please mark the ``detach`` as last will and testamnet,
        in order to prevent memory leak. If we do not ``detach`` properly,
        the instance of ``iot.csm.device.Device`` will not be GC.

    ``attach`` and ``detach`` response::

        {
            'op': 'attach|detach',
            'state': 'ok'
        }

    ``add_funcs`` request::

        {
            'op': 'add_funcs',
            'codes': [
                'def f(): ...',
                'def g(): ...'
            ],
            'digests':[
                'SHA256 of f',
                'SHA256 of g'
            ],
        }

    ``add_funcs`` response::

        {
            'op': 'add_funcs',
            'state': 'ok',
            'digests':[
                'SHA256 of f',
                'SHA256 of g'
            ]
        }

    ``rm_funcs`` request::

        {
            'op': 'rm_funcs',
            'digest': [
                '...',
            ],
        }

    ``add_link`` and ``rm_link`` request::

        {
            'op': 'add_link|rm_link',
            'da_id': 'uuid',
            'idf|odf': 'feature_name',
            'func': 'digest',  // optional field
        }

    ``add_link`` and ``rm_link`` response::

        {
            'op': 'add_link|rm_link',
            'da_id': 'uuid',
            'idf|odf': 'feature_name',
            'func': 'digest',  // optional field
            'state': 'ok',
        }

    ``set_join`` request::

        {
            'op': 'set_join',
            'prev': 'function digest',  // ``None`` for new first setup
            'new': 'function digest',
        }

    ``set_join`` response::

        {
            'op': set_join',
            'state': 'ok',
            'new': 'function digest',
        }

    The error response has following general format::

        {
            'op': 'opcode',
            'state': 'error',
            'reason': 'error_reason',
            'other_payload': '...',
        }


    Device API
    ----------------------------------------------------------------------

    The topic of graph api call should be
    ``iottalk/api/req/<client-id>/device``.

    This api endpoint provide notification about device applications.

    The available operation code:

    - ``attach``
    - ``detach``
    - ``anno`` for announcement

    The ``detach`` opcode is also available on:

    - ``iottalk/api/req/<client-id>`` - for detaching all services


    ``attach``
    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    Initialize this service and get the init state of device applications.

    And the following device application state changes will be revealed on this
    channel.

    Request::

        {
            'op': 'attach',
        }

    If attach success, we will got following response.
    If more than one attach request arrive, we will ignore the lagger.

    ::

        {
            'op': 'attach',
            'state': 'ok',
            'da_list': {
                'id': {
                    'name': ...,
                    'id': ...,
                    'idf_list': ...,
                    'odf_list': ...,
                },
            },
        }


    ``detach``
    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    .. note::
        Please mark this ``detach`` as last will and testamnet,
        in order to prevent memory leak. If we do not ``detach`` properly,
        the instance of ``iot.csm.device.Device`` will not be GC.

    Request::

        {
            'op': 'detach',
        }

    Response::

        {
            'op': 'detach',
            'state': 'ok',
        }


    ``anno``
    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    Client will recieve announcement on the response channel if there is any
    D.A. state changes.

    There are some types of announcement message:

        - ``new``

        - ``change``

        - ``remove``

    Type ``new``::

        {
            'op': 'anno',
            'timestamp': '...',
            'type': 'new',
            'da_id': {
                ...  // new and whole state
            }
        }

    Type ``change``::

        {
            'op': 'anno',
            'timestamp': '...',
            'type': 'change',
            'da_id': {
                ...  // changed field(s)
            }
        }

    Type ``remove``::

        {
            'op': 'anno',
            'timestamp': '...',
            'type': 'remove',
            'da_list': ['da_id', ... ],
        }

    '''

    def __init__(self):
        self.topic_prefix = 'iottalk/api'
        self.topic = '{}/req/#'.format(self.topic_prefix)
        self.client = JsonClient()
        self.client.on_connect = self._on_connect
        self.client.on_json_message = self.msg_dispatcher
        self.client.connect(config.mqtt_conf['host'],
                            port=config.mqtt_conf['port'])
        self.gapi_client = {}  # graph api client
        self.dapi_client = {}  # device api client

    def _on_connect(self, client, userdata, flags, rc):
        self.client.subscribe(self.topic, qos=2)
        log.debug('api server connected')

    def loop_forever(self):
        with suppress(KeyboardInterrupt):
            self.client.loop_forever()

    def msg_add_callback(self, topic):
        '''
        Get a helper function for add/remove on_message callback

        Available function attribute:

        :f.client: the mqtt client instance
        :f.callback: the set callback function
        :f.topic: the mqtt topic
        :f.remove_callback: function for remove callback
        '''
        def f(callback):
            if f.callback is not None:
                raise RuntimeError(
                    'message callback for {!r} already set'.format(f.topic))

            f.callback = mqtt_json_payload(callback, raise_error=False)
            f.client.message_callback_add(f.topic, f.callback)
            return f.callback

        f.topic = topic
        f.client = self.client
        f.callback = None
        f.remove_callback = partial(self.client.message_callback_remove,
                                    f.topic)
        return f

    def msg_dispatcher(self, client, userdata, msg):
        log.debug('on api server message: \n%r:\n%r', msg.topic, msg.payload)

        topic = msg.topic.split('/')[3:]

        if len(topic) == 1:
            client_id, api_type = topic[0], 'service'
        elif 2 <= len(topic) <= 3:
            client_id, api_type = topic[:2]
        else:
            log.warning('Invalid topic %r', topic)
            return

        if api_type == 'service':  # accept the detach message
            if msg.payload.get('op') == 'detach':
                return self.detach_all(client_id)

        elif api_type == 'graph':
            if len(topic) == 3:
                graph_id = topic[2]
                return self.attach_graph_api(client_id, graph_id, msg)

            # maybe a detach message
            if msg.payload.get('op') == 'detach':
                return self.detach_graph_api(client_id)

        elif api_type == 'device':
            return self.attach_dev_api(client_id, msg)

        else:
            log.warning('Unknown api type: %r', api_type)
            return

    def graph_res_topic(self, client_id, graph_id=None):
        '''
        The response topic of graph api service

        :param client_id: <clien-id> in the topic
        :param graph_id: <graph-id> in the topic
        '''
        return '{}/res/{}/graph/{}'.format(
            self.topic_prefix, client_id, graph_id if graph_id else '')

    def attach_graph_api(self, client_id, graph_id, msg):
        '''
        Attach to initiate ``iot.csm.graph.Graph``

        :param client_id: <clien-id> in the topic
        :param graph_id: <graph-id> in the topic
        :param msg: the mqtt msg object
        '''
        res_topic = self.graph_res_topic(client_id, graph_id)
        publish = partial(self.client.publish_json, res_topic)

        if client_id not in self.gapi_client:
            log.info('New client connected: %s', client_id)
            # init a empty dict for storing graph map
            # FIXME: potential race condiction when creating gapi_client
            self.gapi_client[client_id] = {}

        api_client = self.gapi_client[client_id]
        # FIXME: if the graph_id in api_client, imply that the Graph in woring.
        #        and maybe no message will be heard here
        if graph_id not in api_client:
            # check attach info
            opcode = msg.payload.get('op')
            if opcode != 'attach':
                reason = 'Unknown graph id {}'.format(graph_id)
                log.warning(reason)
                msg.payload['state'] = 'error'
                msg.payload['reason'] = reason
                publish(msg.payload)
                return

            log.info('New graph added: %s', graph_id)
            # init a new graph object
            api_client[graph_id] = Graph(
                req=self.msg_add_callback(msg.topic),
                res=publish,
                destroy_cb=self.detach_graph_api_cb(client_id, graph_id))

            msg.payload['state'] = 'ok'
            publish(msg.payload)

    def detach_graph_api_cb(self, client_id, graph_id):
        '''
        The function factory of detach callback for graph api

        :param client_id: <clien-id> in the topic
        :param graph_id: <graph-id> in the topic
        '''
        def cb():
            del self.gapi_client[client_id][graph_id]

        return cb

    def detach_graph_api(self, client_id):
        '''
        Handle the last will and testamnet of graph api connection.

        If we got detach msg on ``iottalk/api/<client-id>/graph``,
        delete all the graphs related to this client.
        '''
        log.info('Destroy whole graph api services on client %r', client_id)


        for graph in tuple(self.gapi_client[client_id].values()):
            graph.detach()

        self.gapi_client[client_id].clear()
        del self.gapi_client[client_id]

        topic = self.graph_res_topic(client_id)
        self.client.publish_json(topic, {'op': 'detach', 'state': 'ok'})

    def attach_dev_api(self, client_id, msg):
        '''
        :param client_id: <client-id> part in the topic
        :param msg: the mqtt msg object
        '''
        if self.dapi_client.get(client_id) is not None:
            log.warning('The device api service is not working on %r ?',
                        client_id)
            return

        res_topic = 'iottalk/api/res/{}/device'.format(client_id)
        publish = partial(self.client.publish_json, res_topic)

        log.info('New device api service for %r', client_id)

        self.dapi_client[client_id] = Device(
            req=self.msg_add_callback(msg.topic),
            res=publish,
            destroy_cb=self.detach_dev_api_cb(client_id))

    def detach_dev_api_cb(self, client_id):
        '''
        The function factory of detach callback for device api

        :param client_id: <client-id> part in the topic
        '''
        def cb():
            del self.dapi_client[client_id]

        return cb

    def detach_all(self, client_id):
        '''
        Detach all the api services.
        '''
        if client_id in self.gapi_client:
            self.detach_graph_api(client_id)

        if client_id in self.dapi_client:
            self.dapi_client[client_id].detach()