def __init__(self, address, args, device=None): name = args['name'] service = args['service'] url = args.get('url', 'ws://127.0.0.1:9400/services') super(ServiceDelegateTile, self).__init__(address, name) self._service = service self._client = ServiceStatusClient(url)
def _try_initialize_supervisor(self): """Check for the existence of a supervisor""" try: self.supervisor = ServiceStatusClient('ws://localhost:9400/services') self.supervisor.register_service('gateway', 'Device Gateway') self.supervisor.post_info('gateway', "Service started successfully") self.supervisor.post_headline('gateway', states.INFO_LEVEL, 'Started successfully') return True except Exception: # pylint: disable=W0703 self._logger.info("No supervisor present") return False
def rpc_agent(supervisor): """Register an RPC agent on one of the clients.""" visor, _client1, client2 = supervisor port = visor.port client1 = ServiceStatusClient('ws://127.0.0.1:%d/services' % port, dispatcher=BasicRPCDispatcher(), agent='service_1') yield visor, client1, client2 client1.stop()
def supervisor(): """A running supervisor with two connected status clients.""" info = { 'expected_services': [{ "short_name": "service_1", "long_name": "A test service" }, { "short_name": "service_2", "long_name": "A second test service" }], 'port': 'unused' # Bind an unused port for testing, the value will appear on visor.port after visor.loaded is set } visor = IOTileSupervisor(info) visor.start() signaled = visor.loaded.wait(2.0) if not signaled: raise ValueError("Could not start supervisor service") port = visor.port client1 = ServiceStatusClient('ws://127.0.0.1:%d/services' % port) client2 = ServiceStatusClient('ws://127.0.0.1:%d/services' % port) yield visor, client1, client2 client1.stop() client2.stop() visor.stop()
def _try_report_status(self): """Periodic callback to report our gateway's status.""" if self.supervisor is None: try: self.supervisor = ServiceStatusClient( 'ws://localhost:9400/services') self.supervisor.register_service('gateway', 'Device Gateway') self.supervisor.post_info('gateway', "Service started successfully") self.supervisor.post_headline('gateway', states.INFO_LEVEL, 'Started successfully') except Exception: # pylint: disable=W0703 self._logger.exception( "Exception trying to create a ServiceStatusClient") return self.supervisor.update_state('gateway', states.RUNNING) self.supervisor.send_heartbeat('gateway')
class ServiceDelegateTile(VirtualTile): """A tile that delegates all RPCs to a service using the IOTileSupervisor. Args: address (int): The address that we are being created at args (dict): Configuration arguments with the following required keys: name (str): The 6 byte name we should return for this tile service (str): The short name of the service that we should forward RPCs on to when someone calls an RPC on us. url (str): The URL of the supervisor that we should connect to. If no url is specified, the default is 127.0.0.1:9400/services which is the supervisor default. """ def __init__(self, address, args, device=None): name = args['name'] service = args['service'] url = args.get('url', 'ws://127.0.0.1:9400/services') super(ServiceDelegateTile, self).__init__(address, name) self._service = service self._client = ServiceStatusClient(url) def has_rpc(self, rpc_id): """Check if an RPC is defined. Args: rpc_id (int): The RPC to check Returns: bool: Whether it exists """ # Since we don't know what RPCs are defined in the service on the # other side of the supervisor, we need to actually try to call # each RPC and fail at that point if they don't exist. return True def call_rpc(self, rpc_id, payload=bytes()): """Call an RPC by its ID. Args: rpc_id (int): The number of the RPC payload (bytes): A byte string of payload parameters up to 20 bytes Returns: str: The response payload from the RPC """ # If we define the RPC locally, call that one. We use this for reporting # our status if super(ServiceDelegateTile, self).has_rpc(rpc_id): return super(ServiceDelegateTile, self).call_rpc(rpc_id, payload) # FIXME: We set the timeout here to a very large number since we don't # know what an appropriate timeout is and don't want to restrict the # run time of RPCs that could be long running. The caller of the RPC # through the tile will know what an appropriate timeout is for the # RPC that they are trying to call. resp = self._client.send_rpc(self._service, rpc_id, payload, timeout=120.0) result = resp['result'] if result == 'success': return resp['response'] elif result == 'service_not_found': raise TileNotFoundError("Could not find service by name", name=self._service) elif result == 'rpc_not_found': raise RPCNotFoundError("Could not find RPC on service", name=self._service, rpc_id=rpc_id) elif result == 'invalid_arguments': raise RPCInvalidArgumentsError("Invalid arguments to RPC", name=self._service, rpc_id=rpc_id) elif result == 'invalid_response': raise RPCInvalidReturnValueError("Invalid response from RPC", name=self._service, rpc_id=rpc_id) elif result == 'execution_exception': raise InternalError("Exception raised during processing RPC", name=self._service, rpc_id=rpc_id) else: raise InternalError("Unknown response received from delegated RPC", name=self._service, rpc_id=rpc_id, result=result)
class IOTileGateway(threading.Thread): """A gateway that finds IOTile devices using device adapters and serves them using agents. The gateway runs in separate thread in a tornado IOLoop and you can call the synchronous wait function to wait for it to quit. It will loop forever unless you stop it by calling the stop() or stop_from_signal() methods. These functions add a task to the gateway's event loop and implicitly call wait to synchronously wait until the gateway loop actually stops. IOTileGateway should be thought of as a turn-key gateway object that translates requests for IOTile Device access received from one or more GatewayAgents into commands sent to one or more DeviceAdapters. It is a multi-device, multi-user, multi-protocol system that can have many connections in flight at the same time, limited only by the available resources on the computer that hosts it. The arguments dictionary to IOTileGateway class has the same format as the json parameters passed to the iotile-gateway script that is just a thin wrapper around this class. Args: config (dict): The configuration of the gateway. There should be two keys set: agents (list): a list of dictionaries with the name of the agent and any arguments that should be passed to create it. adapters (list): a list of dictionaries with the device adapters to add into the gateway and any arguments that should be use to create each one. """ def __init__(self, config): self.loop = None self.device_manager = None self.agents = [] self.supervisor = None self.loaded = threading.Event() self._config = config self._logger = logging.getLogger(__name__) if 'agents' not in config: self._config['agents'] = [] self._logger.warn("No agents defined in arguments to iotile-gateway, this is likely not what you want") elif 'adapters' not in config: self._config['adapters'] = [] self._logger.warn("No device adapters defined in arguments to iotile-gateway, this is likely not what you want") super(IOTileGateway, self).__init__() def run(self): """Start the gateway and run it to completion in another thread.""" self.loop = tornado.ioloop.IOLoop(make_current=True) # To create a loop for each thread self.device_manager = device.DeviceManager(self.loop) # If we have an initialization error, stop trying to initialize more things and # just shut down cleanly should_close = False # Load in all of the gateway agents that are supposed to provide access to # the devices in this gateway for agent_info in self._config['agents']: if 'name' not in agent_info: self._logger.error("Invalid agent information in gateway config, info=%s, missing_key=%s", str(agent_info), 'name') should_close = True break agent_name = agent_info['name'] agent_args = agent_info.get('args', {}) self._logger.info("Loading agent by name '%s'", agent_name) agent_class = find_entry_point('iotile.gateway_agent', agent_name) try: agent = agent_class(agent_args, self.device_manager, self.loop) agent.start() self.agents.append(agent) except Exception: # pylint: disable=W0703 self._logger.exception("Could not load gateway agent %s, quitting", agent_name) should_close = True break # Load in all of the device adapters that provide access to actual devices if not should_close: for adapter_info in self._config['adapters']: if 'name' not in adapter_info: self._logger.error("Invalid adapter information in gateway config, info=%s, missing_key=%s", str(adapter_info), 'name') should_close = True break adapter_name = adapter_info['name'] port_string = adapter_info.get('port', None) self._logger.info("Loading device adapter by name '%s' and port '%s'", adapter_name, port_string) try: adapter_class = find_entry_point('iotile.device_adapter', adapter_name) adapter = adapter_class(port_string) self.device_manager.add_adapter(adapter) except Exception: # pylint: disable=W0703 self._logger.exception("Could not load device adapter %s, quitting", adapter_name) should_close = True if should_close: self.loop.add_callback(self._stop_loop) else: # Notify that we have now loaded all plugins and are starting operation (once the loop starts) self.loop.add_callback(lambda: self.loaded.set()) # Try to regularly update a supervisor about our status if a supervisor is running if self._try_initialize_supervisor(): callback = tornado.ioloop.PeriodicCallback(self._try_report_status, 60000) callback.start() self.loop.start() # The loop has been closed, finish and quit self._logger.critical("Done stopping loop") def _try_initialize_supervisor(self): """Check for the existence of a supervisor""" try: self.supervisor = ServiceStatusClient('ws://localhost:9400/services') self.supervisor.register_service('gateway', 'Device Gateway') self.supervisor.post_info('gateway', "Service started successfully") self.supervisor.post_headline('gateway', states.INFO_LEVEL, 'Started successfully') return True except Exception: # pylint: disable=W0703 self._logger.info("No supervisor present") return False def _try_report_status(self): """Periodic callback to report our gateway's status.""" self.supervisor.update_state('gateway', states.RUNNING) self.supervisor.send_heartbeat('gateway') def _stop_loop(self): """Cleanly stop the gateway and close down the IOLoop. This function must be called only by being added to our event loop using add_callback. """ self._logger.critical("Stopping gateway") self._logger.info("Stopping gateway agents") for agent in self.agents: try: agent.stop() except Exception: # pylint: disable=W0703 self._logger.exception("Error stopping gateway agent") self._logger.critical('Stopping device adapters') try: self.device_manager.stop() except Exception: # pylint: disable=W0703 self._logger.exception("Error stopping device adapters") if self.supervisor: try: self.supervisor.update_state('gateway', states.STOPPED) self.supervisor.post_headline('gateway', states.INFO_LEVEL, 'Stoppped by supervisor') except Exception: # pylint: disable=W0703 self._logger.exception("Error updating service status to stopped") try: self.supervisor.stop() except Exception: # pylint: disable=W0703 self._logger.exception("Error stopping IOLoop") self.loop.stop() self._logger.critical('Stopping event loop and shutting down') def stop(self): """Stop the gateway manager and synchronously wait for it to stop.""" self.loop.add_callback(self._stop_loop) self.wait() def wait(self): """Wait for this gateway to shut down. We need this special function because waiting inside join will cause signals to not get handled. """ while self.is_alive(): try: self.join(timeout=0.1) except IOError: pass # IOError comes when this call is interrupted in a signal handler def stop_from_signal(self): """Stop the gateway from a signal handler, not waiting for it to stop.""" self.loop.add_callback_from_signal(self._stop_loop)