async def _build_capabilities_cache(): """Construct the list that will become the device capabilities cache. Returns: list: A list of dictionaries, where each dictionary corresponds to a registered plugin. The plugin dict will identify the plugin and enumerate the device kinds it supports and the output types supported by those device kinds. Raises: errors.InternalApiError: All plugins failed the capabilities request. """ logger.debug(_('Building the device capabilities cache')) capabilities = [] # First, we want to iterate through all of the known plugins and use # their clients to get the capability info for each plugin. plugin_count = len(Plugin.manager.plugins) if plugin_count == 0: logger.debug(_('Manager has no plugins - registering plugins')) register_plugins() plugin_count = len(Plugin.manager.plugins) logger.debug(_('Plugins to get capabilities for: {}').format(plugin_count)) # Track which plugins failed to provide capability info for any reason. failures = {} # FIXME (etd): as of pylint 2.1.1, this gets marked with 'not-an-iterable' # It still appears to work just fine, so need to figure out why it is getting # marked as such and what should be done to fix it. async for plugin_id, plugin in get_plugins(): # pylint: disable=not-an-iterable logger.debug('{} - {}'.format(plugin_id, plugin)) devices = [] try: for capability in plugin.client.capabilities(): devices.append({ 'kind': capability.kind, 'outputs': capability.outputs }) except grpc.RpcError as ex: failures[plugin_id] = ex logger.warning( _('Failed to get capability for plugin: {}').format(plugin_id)) logger.warning(ex) continue capabilities.append({'plugin': plugin.tag, 'devices': devices}) # If we fail to read from all plugins (assuming there were any), then we # can raise an error since it is likely something is mis-configured. if plugin_count != 0 and plugin_count == len(failures): raise errors.InternalApiError( _('Failed to get capabilities for all plugins: {}').format( failures)) return capabilities
async def write_route(request, rack, board, device): """Write data to a known device. The data POSTed here should be JSON with an 'action' field and 'raw' field, if applicable. If no data is posted, the write will fail. Args: request (sanic.request.Request): The incoming request. rack (str): The identifier of the rack which the device resides on. board (str): The identifier of the board which the device resides on. device (str): The identifier of the device to write to. Returns: sanic.response.HTTPResponse: The endpoint response. """ try: data = request.json except Exception as e: raise errors.InvalidJsonError( _('Invalid JSON specified: {}').format(request.body)) from e logger.debug(_('Write route: POSTed JSON: {}').format(data)) if not any([x in data for x in ['action', 'raw']]): raise errors.InvalidArgumentsError( _('Invalid data POSTed for write. Must contain "action" and/or "raw"' )) response = await commands.write(rack, board, device, data) return response.to_json()
def add(self, plugin): """Add a new Plugin to the manager. All plugins should be added to the Manager, since this is how they are accessed later. An error will be raised if a Plugin with the same name is already tracked by the manager. Args: plugin (Plugin): The plugin instance to add to the manager. Raises: errors.PluginStateError: The manager was not able to add the plugin either because the given object was not a plugin or a plugin with the same name is already being tracked. """ if not isinstance(plugin, Plugin): raise errors.PluginStateError( _('Only Plugin instances can be added to the manager') ) plugin_id = plugin.id() if plugin_id in self.plugins: raise errors.PluginStateError( _('Plugin ("{}") already exists in the manager').format(plugin_id) ) self.plugins[plugin_id] = plugin
def register_plugins(): """Register all of the configured plugins. Plugins can either use a unix socket or TCP for communication. Unix socket based plugins will be detected from the presence of the socket file in a well-known directory, or via configuration. TCP based plugins will need to be made known to Synse Server via configuration. Upon initialization, the Plugin instances are automatically registered with the PluginManager. """ # Register plugins from local config (file, env) unix = register_unix() tcp = register_tcp() # Get addresses of plugins to register via service discovery discovered = [] addresses = kubernetes.discover() for address in addresses: plugin_id = register_plugin(address, 'tcp') if plugin_id is None: logger.error(_('Failed to register plugin with address: {}').format(address)) continue discovered.append(plugin_id) diff = set(Plugin.manager.plugins) - set(unix + tcp + discovered) # Now that we have found all current plugins, we will want to clear out # any old plugins which may no longer be present. logger.debug(_('Plugins to purge from manager: {}').format(diff)) Plugin.manager.purge(diff) logger.debug(_('Plugin registration complete'))
def register_tcp_plugins(): """Register the plugins that use TCP for communication. Return: list[str]: The names of all plugins that were registered. """ logger.debug(_('Registering plugins (tcp)')) configured = config.options.get('plugin.tcp', {}) logger.debug(_('TCP plugins in configuration: {}').format(configured)) if not configured: return [] manager = Plugin.manager # Track the names of all plugins that are registered. registered = [] for name, address in configured.items(): if manager.get(name) is None: # A new plugin gets added to the manager on initialization. plugin = Plugin(name=name, address=address, mode='tcp') logger.debug(_('Created new plugin (tcp): {}').format(plugin)) else: logger.debug( _('TCP Plugin "{}" already exists - will not re-register'). format(name)) registered.append(name) return list(set(registered))
async def check_transaction(transaction_id): """The handler for the Synse Server "transaction" API command. Args: transaction_id (str|None): The id of the transaction to check. If the ID is None, a list of all transactions currently in the cache is returned. Returns: TransactionResponse: The "transaction" response scheme model. TransactionListResponse: The list of all transactions. """ logger.debug(_('Transaction Command (args: {})').format(transaction_id)) # If we are not given a transaction ID, then we want to return # the list of all actively tracked transactions. if transaction_id is None: all_keys = cache.transaction_cache._cache.keys() # Keys in the cache are prepended with 'transaction', so here we get all # keys with that prefix and strip the prefix. transaction_ids = [ k[11:] for k in all_keys if k.startswith('transaction') ] return scheme.TransactionListResponse(transaction_ids) # Otherwise, get the specified transaction. transaction = await cache.get_transaction(transaction_id) if not transaction: raise errors.TransactionNotFoundError( _('Transaction with id "{}" not found').format(transaction_id)) plugin_name = transaction.get('plugin') context = transaction.get('context') if not plugin_name: # TODO - in the future, what we could do is attempt sending the transaction # request to *all* of the known plugins. this could be useful in the event # that synse goes down. since everything is just stored in memory, a new # synse instance will have lost the transaction cache. # # alternatively, we could think about having an internal api command to # essentially dump the active transactions so that we can rebuild the cache. raise errors.TransactionNotFoundError( _('Unable to determine managing plugin for transaction {}.'). format(transaction_id)) _plugin = plugin.get_plugin(plugin_name) if not _plugin: raise errors.PluginNotFoundError( _('Unable to find plugin "{}"').format(plugin_name)) try: resp = _plugin.client.transaction(transaction_id) except grpc.RpcError as ex: raise errors.FailedTransactionCommandError(str(ex)) from ex return scheme.TransactionResponse(transaction_id, context, resp[0])
async def scan(rack=None, board=None, force=False): """The handler for the Synse Server "scan" API command. Args: rack (str): The rack to filter the scan results by. board (str): The board to filter the scan results by. force (bool): Force a re-scan of the meta-information. Returns: ScanResponse: The "scan" response scheme model. """ logger.debug(_('Scan Command (args: {}, {}, force: {})').format(rack, board, force)) if force: await cache.clear_all_meta_caches() # Plugins are registered on scan. If no plugins exist and a scan is # performed (e.g. on startup), we will find and register plugins. # Additionally, if we are forcing re-scan, we will re-register plugins. # This allows us to pick up any dynamically added plugins and clear out # any plugins that were removed. if len(plugin.Plugin.manager.plugins) == 0 or force: logger.debug(_('Re-registering plugins')) plugin.register_plugins() cache_data = await cache.get_scan_cache() # Filter the scan results by rack. if rack is not None: if not cache_data: raise errors.FailedScanCommandError( _('Unable to filter by resource - no scan results returned') ) for r in cache_data['racks']: if r['id'] == rack: cache_data = r break else: raise errors.RackNotFoundError( _('Rack "{}" not found in scan results').format(rack) ) # Filter the rack results by board. if board is not None: for b in cache_data['boards']: if b['id'] == board: cache_data = b break else: raise errors.BoardNotFoundError( _('Board "{}" not found in scan results').format(board) ) return ScanResponse( data=cache_data )
async def read_cached(start=None, end=None): """The handler for the Synse Server "readcached" API command. Args: start (str): An RFC3339 or RFC3339Nano formatted timestamp which defines a starting bound on the cache data to return. If no timestamp is specified, there will not be a starting bound. (default: None) end (str): An RFC3339 or RFC3339Nano formatted timestamp which defines an ending bound on the cache data to return. If no timestamp is specified, there will not be an ending bound. (default: None) Yields: ReadCachedResponse: The cached reading from the plugin. """ start, end = start or '', end or '' logger.debug( _('Read Cached command (start: {}, end: {})').format(start, end)) # If the plugins have not yet been registered, register them now. if len(plugin.Plugin.manager.plugins) == 0: logger.debug(_('Re-registering plugins')) plugin.register_plugins() # For each plugin, we'll want to request a dump of its readings cache. async for plugin_name, plugin_handler in plugin.get_plugins(): # pylint: disable=not-an-iterable logger.debug( _('Getting readings cache for plugin: {}').format(plugin_name)) # Get the cached data from the plugin try: for reading in plugin_handler.client.read_cached(start, end): # If there is no reading, we're done iterating if reading is None: return try: __, device = await cache.get_device_info( # pylint: disable=unused-variable reading.rack, reading.board, reading.device) except errors.DeviceNotFoundError: logger.info( _('Did not find device {}-{}-{} locally. Skipping device; ' 'server cache may be out of sync.')) continue yield ReadCachedResponse( device=device, device_reading=reading, ) except grpc.RpcError as ex: raise errors.FailedReadCachedCommandError(str(ex)) from ex
def _channel(self): """Convenience method to create the client gRPC channel.""" if self.mode == 'unix': target = 'unix:{}'.format(os.path.join(SOCKET_DIR, self.name + '.sock')) elif self.mode == 'tcp': target = self.addr else: raise errors.InvalidArgumentsError( _('Invalid gRPC client mode: {}').format(self.mode) ) logger.debug(_('Client gRPC channel: {}').format(target)) return grpc.insecure_channel(target)
async def boot_target_route(request, rack, board, device): """Endpoint to read/write boot target device data. This route is an alias for the core `read` functionality when there are no valid query parameters specified. It is an alias for the core `write` functionality when there are valid query parameters specified. Supported Query Parameters: target: The boot target to set (hdd|pxe) Args: request (sanic.request.Request): The incoming request. rack (str): The rack which the system resides on. board (str): The board which the system resides on. device (str): The system device. Returns: sanic.response.HTTPResponse: The endpoint response. """ await validate.validate_device_type(const.BOOT_TARGET_TYPES, rack, board, device) # Get the valid query parameters. If unsupported query parameters # are specified, this will raise an error. qparams = validate.validate_query_params(request.raw_args, 'target') param_target = qparams.get('target') # If any of the parameters are specified, this will be a write request # using those parameters. if param_target is not None: logger.debug( _('Boot target alias route: writing (query parameters: {})'). format(qparams)) if param_target not in const.boot_targets: raise errors.InvalidArgumentsError( _('Invalid boot target "{}". Must be one of: {}').format( param_target, const.boot_targets)) data = {'action': 'target', 'raw': param_target} transaction = await commands.write(rack, board, device, data) return transaction.to_json() # Otherwise, we just read from the device. else: logger.debug(_('Boot target alias route: reading')) reading = await commands.read(rack, board, device) return reading.to_json()
def format_readings(self): """Format the instance's readings to the read response scheme. Returns: dict: A properly formatted Read response. """ formatted = [] dev_output = self.device.output for reading in self.readings: rt = reading.type # These fields may not be specified, e.g. in cases where it wouldn't # make sense for a reading unit, e.g. LED state (on/off) unit = None precision = None found = False for out in dev_output: if out.type == rt: symbol = out.unit.symbol name = out.unit.name precision = out.precision if symbol or name: unit = {'symbol': symbol, 'name': name} found = True break # If the reading type does not match the supported types, we will not # return it, and instead will just just skip over it. if not found: logger.warning( _('Found unexpected reading type "{}" for device {}'). format(rt, self.device)) continue # The value is stored in a protobuf oneof block, so we need to figure out # which field it is in, and extract it. If no field is set, take the reading # value to be None. value = None field = reading.WhichOneof('value') if field is not None: value = getattr(reading, field) # Set the specified precision, if specified if precision and isinstance(value, float): value = round(value, precision) formatted.append({ 'value': value, 'timestamp': reading.timestamp, 'unit': unit, 'type': rt, 'info': reading.info, }) return formatted
async def validate_device_type(device_type, rack, board, device): """Validate that the device associated with the given routing info (rack, board device) matches the given device type. This checks that the lower-cased device type matches the lower-cased expected type, so casing on device type(s) should not matter. Args: device_type (list[str]): The device types that are permissible, e.g. "led", "fan", etc. rack (str): The rack which the device belongs to. board (str): The board which the device belongs to. device (str): The ID of the device. Raises: errors.InvalidDeviceType: The device does not match the given type. errors.DeviceNotFoundError: The specified device is not found. """ __, device = await cache.get_device_info(rack, board, device) # pylint: disable=unused-variable # The type of a device should be the last element in it's kind namespace. _type = device.kind.split('.')[-1].lower() if _type not in [t.lower() for t in device_type]: raise errors.InvalidDeviceType( _('Device ({}) is not a supported type {}').format( _type, device_type))
def write(self, rack, board, device, data): """Write data to the specified device. Args: rack (str): The rack which the device resides on. board (str): The board which the device resides on. device (str): The identifier for the device to write to. data (list[WriteData]): The data to write to the device. Returns: synse_grpc.api.Transactions: The transactions that can be used to track the given write request(s). """ logger.debug(_('Issuing gRPC write request')) req = synse_grpc.api.WriteInfo( deviceFilter=synse_grpc.api.DeviceFilter( device=device, board=board, rack=rack, ), data=[d.to_grpc() for d in data]) timeout = config.options.get('grpc.timeout', None) resp = self.grpc.Write(req, timeout=timeout) return resp
def read_cached(self, start=None, end=None): """Get the cached readings from a plugin. If caching readings is disabled for the plugin, this will get a dump of the current reading state. Args: start (str): An RFC3339 or RFC3339Nano formatted timestamp which defines a starting bound on the cache data to return. If no timestamp is specified, there will not be a starting bound. (default: None) end (str): An RFC3339 or RFC3339Nano formatted timestamp which defines an ending bound on the cache data to return. If no timestamp is specified, there will not be an ending bound. (default: None) Yields: synse_grpc.api.DeviceReading: A cached reading value with its associated device routing info. """ logger.debug(_('Issuing gRPC read cached request')) bounds = synse_grpc.api.Bounds( start=start or '', end=end or '', ) timeout = config.options.get('grpc.timeout', None) for reading in self.grpc.ReadCached(bounds, timeout=timeout): yield reading
def discover(): """Discover plugins for kubernetes based on the kubernetes service discovery configuration(s). Returns: list[str]: A list of host:port addresses for plugins discovered via kubernetes. """ addresses = [] cfg = config.options.get('plugin.discover.kubernetes') if not cfg: return addresses # Currently, everything we want to be able to discover (namely, endpoints) # should all be in the same namespace, so we define it globally. If other # methods of lookup are added later, we could also have a namespace per # resource, e.g. so we can look for endpoints in namespace X and pods in # namespace Y, etc. # # If no namespace is provided via user configuration, if will default to # the 'default' namespace. ns = config.options.get('plugin.discover.kubernetes.namespace', 'default') logger.debug(_('Using namespace "{}" for k8s discovery').format(ns)) # Currently, we only support plugin discovery via kubernetes service # endpoints, under the `plugin.discover.kubernetes.endpoints` config # field. # # We can support other means later. endpoints_cfg = cfg.get('endpoints') if endpoints_cfg: addresses.extend(_register_from_endpoints(ns=ns, cfg=endpoints_cfg)) return addresses
def validate_query_params(raw_args, *valid_params): """Validate that the incoming request's query parameters are valid. Any unsupported query parameter will cause an error to be raised. Absence of a supported query parameter will not cause an error. If a supported query parameter is found, it is added to the response dictionary. Args: raw_args: An incoming Sanic request's `raw_args`, which contains the query params that came in as part of the request. *valid_params: The query parameter keys that are valid for the request. Returns: dict: A dictionary that maps the supported query parameters found in the request with their values. Raises: errors.InvalidArgumentsError: An invalid query parameter was detected. """ params = {} for k, v in raw_args.items(): if k not in valid_params: raise errors.InvalidArgumentsError( _('Invalid query param: {} (valid params: {})').format( k, valid_params)) params[k] = v return params
def metainfo(self, rack=None, board=None): """Get all meta-information from a plugin. Args: rack (str): The rack to filter by. board (str): The board to filter by. Returns: list[synse_plugin.api.MetainfoResponse]: All device meta-information provided by the plugin. """ logger.debug(_('Issuing gRPC metainfo request')) # If the rack or board is not specified, pass it through as an # empty string. rack = rack if rack is not None else '' board = board if board is not None else '' req = synse_api.MetainfoRequest( rack=rack, board=board ) timeout = config.options.get('grpc.timeout', None) resp = [r for r in self.stub.Metainfo(req, timeout=timeout)] return resp
async def get_device_info(rack, board, device): """Get the device information for a device. Args: rack (str): The rack which the device resides on. board (str): The board which the device resides on. device (str): The ID of the device to get meta-info for. Returns: tuple(str, Device): A tuple where the first item is the name of the plugin that the device is associated with and the second item is the device information for that device. Raises: errors.DeviceNotFoundError: The given rack-board-device combination does not correspond to a known device. """ cid = utils.composite(rack, board, device) # This also builds the plugins cache _cache = await get_device_info_cache() dev = _cache.get(cid) if dev is None: raise errors.DeviceNotFoundError( _('{} does not correspond with a known device').format('/'.join( [rack, board, device]))) # If the device exists, it will have come from a plugin, so we should # always have the plugin name here. pcache = await _plugins_cache.get(PLUGINS_CACHE_KEY) return pcache.get(cid), dev
async def add_transaction(transaction_id, context, plugin_name): """Add a new transaction to the transaction cache. This cache tracks transactions and maps them to the plugin from which they originated, as well as the context of the transaction. Args: transaction_id (str): The ID of the transaction. context (dict): The action/raw data of the write transaction that can be used to help identify the transaction. plugin_name (str): The name of the plugin to associate with the transaction. Returns: bool: True if successful; False otherwise. """ ttl = config.options.get('cache.transaction.ttl', None) logger.debug( _('Caching transaction {} from plugin {} ({})').format( transaction_id, plugin_name, context)) return await transaction_cache.set(transaction_id, { 'plugin': plugin_name, 'context': context }, ttl=ttl)
async def power_route(request, rack, board, device): """Endpoint to read/write power device data. This route is an alias for the core `read` functionality when there are no valid query parameters specified. It is an alias for the core `write` functionality when there are valid query parameters specified. Supported Query Parameters: state: The power state to set (on|off|cycle) Args: request (sanic.request.Request): The incoming request. rack (str): The rack which the power device resides on. board (str): The board which the power device resides on. device (str): The power device. Returns: sanic.response.HTTPResponse: The endpoint response. """ await validate.validate_device_type(const.TYPE_POWER, rack, board, device) # Get the valid query parameters. If unsupported query parameters # are specified, this will raise an error. qparams = validate.validate_query_params(request.raw_args, 'state') param_state = qparams.get('state') # If any of the parameters are specified, this will be a write request # using those parameters. if param_state is not None: logger.debug( _('Power alias route: writing (query parameters: {})').format( qparams)) if param_state not in const.power_actions: raise errors.InvalidArgumentsError( _('Invalid power state "{}". Must be one of: {}').format( param_state, const.power_actions)) data = {'action': 'state', 'raw': param_state} transaction = await commands.write(rack, board, device, data) return transaction.to_json() # Otherwise, we just read from the device. else: logger.debug(_('Power alias route: reading')) reading = await commands.read(rack, board, device) return reading.to_json()
def _validate_mode(self): """Validate the plugin mode. Raises: errors.PluginStateError: The plugin mode is unsupported. This will also be raised if the mode is 'unix', but the socket does not exist. """ if self.mode not in ['tcp', 'unix']: raise errors.PluginStateError( _('The given mode ({}) must be one of: tcp, unix').format( self.mode)) if self.mode == 'unix': if not os.path.exists(self.addr): raise errors.PluginStateError( _('Unix socket ({}) does not exist').format(self.addr))
def make_channel(self): """Make the channel for the grpc client stub.""" # If Synse Server is configured to communicate with the plugin using # TLS, set up a secure channel, otherwise use an insecure channel. # FIXME (etd) - we'll probably want to support using a CA here? if config.options.get('grpc.tls'): logger.info(_('TLS enabled for gRPC')) cert = config.options.get('grpc.tls.cert') logger.info(_('Using cert file: {}').format(cert)) with open(cert, 'rb') as f: plugin_cert = f.read() creds = grpc.ssl_channel_credentials(root_certificates=plugin_cert) self.channel = grpc.secure_channel(self._fmt_address(), creds) else: self.channel = grpc.insecure_channel(self._fmt_address())
async def lock_route(request, rack, board, device): # pylint: disable=unused-argument """Endpoint to read/write lock device data. This route is an alias for the core `read` functionality when there are no valid query parameters specified. It is an alias for the core `write` functionality when there are valid query parameters specified. Args: request (sanic.request.Request): The incoming request. rack (str): The rack which the lock resides on. board (str): The board which the lock resides on. device (str): The lock device. Returns: sanic.response.HTTPResponse: The endpoint response. """ await validate.validate_device_type(const.LOCK_TYPES, rack, board, device) # Get the valid query parameters. If unsupported query parameters # are specified, this will raise an error. qparams = validate.validate_query_params(request.raw_args, 'action') param_action = qparams.get('action') # If any of the parameters are specified, this will be a write request # using those parameters. if param_action is not None: logger.debug( _('Lock alias route: writing (query parameters: {})').format( qparams)) if param_action not in const.lock_actions: raise errors.InvalidArgumentsError( _('Invalid boot target "{}". Must be one of: {}').format( param_action, const.lock_actions)) data = { 'action': param_action, } transaction = await commands.write(rack, board, device, data) return transaction.to_json() # Otherwise, we just read from the device. else: logger.debug(_('Lock alias route: reading')) reading = await commands.read(rack, board, device) return reading.to_json()
def purge(self, ids): """Remove all of the specified Plugins from the manager. Args: ids (list[str]): The ids of the Plugins to remove. """ for plugin_id in ids: if plugin_id in self.plugins: del self.plugins[plugin_id] logger.debug(_('PluginManager purged plugins: {}').format(ids))
async def capabilities(): """The handler for the Synse Server "capabilities" API command. Returns: CapabilitiesResponse: The "capabilities" response scheme model. """ logger.debug(_('Capabilities Command')) cache_data = await cache.get_capabilities_cache() return CapabilitiesResponse(data=cache_data)
def purge(self, names): """Remove all of the specified Plugins from the manager. Args: names (list[str]): The names of the Plugins to remove. """ for name in names: if name in self.plugins: del self.plugins[name] logger.debug(_('PluginManager purged plugins: {}').format(names))
async def info(rack, board=None, device=None): """The handler for the Synse Server "info" API command. Args: rack (str): The rack to get information for. board (str): The board to get information for. device (str): The device to get information for. Returns: InfoResponse: The "info" response scheme model. """ logger.debug( _('Info Command (args: {}, {}, {})').format(rack, board, device)) if rack is None: raise errors.InvalidArgumentsError( _('No rack specified when issuing info command')) _cache = await cache.get_resource_info_cache() r, b, d = get_resources(_cache, rack, board, device) if board is not None: # We have: rack, board, device if device is not None: response = d # We have: rack, board else: response = { 'board': b['board'], 'location': { 'rack': r['rack'] }, 'devices': list(b['devices'].keys()) } else: # We have: rack response = {'rack': r['rack'], 'boards': list(r['boards'].keys())} return InfoResponse(response)
def metainfo(self): """Get the plugin metainfo. Returns: synse_grpc.api.Metadata: The plugin's metadata. """ logger.debug(_('Issuing gRPC metainfo request')) req = synse_grpc.api.Empty() timeout = config.options.get('grpc.timeout', None) resp = self.grpc.Metainfo(req, timeout=timeout) return resp
async def clear_cache(namespace): """Clear the cache with the given namespace. Cache namespaces are defined in the cache module as variables with a "NS_" prefix. Args: namespace (str): The namespace of the cache to clear. """ logger.debug(_('Invalidating cache: {}').format(namespace)) _cache = aiocache.caches.get('default') return await _cache.clear(namespace=namespace)
def register_plugins(): """Register all of the configured plugins. Plugins can either use a unix socket or TCP for communication. Unix socket based plugins will be detected from the presence of the socket file in a well-known directory, or via configuration. TCP based plugins will need to be made known to Synse Server via configuration. Upon initialization, the Plugin instances are automatically registered with the PluginManager. """ unix = register_unix_plugins() tcp = register_tcp_plugins() diff = set(Plugin.manager.plugins) - set(unix + tcp) # Now that we have found all current plugins, we will want to clear out # any old plugins which may no longer be present. logger.debug(_('Plugins to purge from manager: {}').format(diff)) Plugin.manager.purge(diff) logger.debug(_('Plugin registration complete'))