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])
def test_synse_error_plugin_not_found(): """Check for PLUGIN_NOT_FOUND error""" e = errors.PluginNotFoundError('message') assert isinstance(e, exceptions.NotFound) assert isinstance(e, errors.SynseError) assert isinstance(e, errors.SynseNotFoundError) assert e.status_code == 404 assert e.error_id == errors.PLUGIN_NOT_FOUND assert e.args[0] == 'message'
async def read(rack, board, device): """The handler for the Synse Server "read" API command. Args: rack (str): The rack which the device resides on. board (str): The board which the device resides on. device (str): The device to read. Returns: ReadResponse: The "read" response scheme model. """ logger.debug( _('Read Command (args: {}, {}, {})').format(rack, board, device)) # Lookup the known info for the specified device. plugin_name, dev = await cache.get_device_info(rack, board, device) logger.debug( _('Device {} is managed by plugin {}').format(device, plugin_name)) # Get the plugin context for the device's specified protocol. _plugin = plugin.get_plugin(plugin_name) logger.debug(_('Got plugin: {}').format(_plugin)) if not _plugin: raise errors.PluginNotFoundError( _('Unable to find plugin named "{}" to read').format(plugin_name)) try: # Perform a gRPC read on the device's managing plugin read_data = _plugin.client.read(rack, board, device) except grpc.RpcError as ex: # FIXME (etd) - this isn't the nicest way of doing this check. # this string is returned from the SDK, and its not likely to change # anytime soon, so this is "safe" for now, but we should see if there # is a better way to check this other than comparing strings.. if hasattr(ex, 'code') and hasattr(ex, 'details'): if grpc.StatusCode.NOT_FOUND == ex.code( ) and 'no readings found' in ex.details().lower(): # FIXME (etd) - with SDK v1.0, is the below correct? We should not longer # have to pass the "null" string. I think an empty string should also not # indicate no readings.. it should be the NOT_FOUND error (or, at least # some kind of error). # Currently, in the SDK, there are three different behaviors for # devices that do not have readings. Either (a). "null" is returned, # (b). an empty string ("") is returned, or (c). a gRPC error is # returned with the NOT_FOUND status code. Cases (a) and (b) are # handled in the ReadResponse initialization (below). This block # handles case (c). # # The reason for the difference between (a) and (b) is just one # of implementation. The empty string is the default value for the # gRPC read response, but sometimes it is useful to have an explict # value set to make things easier to read. # # The difference between those and (c) is more distinct. (c) should # only happen when a configured device is not being read from at all. # Essentially, (c) is the fallback for when device-specific handlers # fail to read a configured device. # # To summarize: # (a), (b) # A device is configured and the plugin's device handlers # can operate on the device. This indicates that the plugin # is working, but the device could be failing or disconnected. # # (c) # A device is configured, but the plugin's device handler # can not (or is not) able to operate on the device. This # could indicate either a plugin configuration error or # an error with the plugin logic itself. # Create empty readings for each of the device's readings. logger.warning( _('Read for {}/{}/{} returned gRPC "no readings found". Will ' 'apply None as reading value. Note that this response might ' 'indicate plugin error/misconfiguration.').format( rack, board, device)) read_data = [] for output in dev.output: read_data.append( api.Reading( timestamp=utils.rfc3339now(), type=output.type, )) else: raise errors.FailedReadCommandError(str(ex)) from ex else: raise errors.FailedReadCommandError(str(ex)) from ex return ReadResponse(device=dev, readings=read_data)
async def write(rack, board, device, data): """The handler for the Synse Server "write" API command. Args: rack (str): The rack which the device resides on. board (str): The board which the device resides on. device (str): The device to write to. data (dict): The data to write to the device. Returns: WriteResponse: The "write" response scheme model. """ logger.debug( _('Write Command (args: {}, {}, {}, data: {})').format( rack, board, device, data)) # Lookup the known info for the specified device plugin_name, __ = await cache.get_device_info(rack, board, device) # pylint: disable=unused-variable # Get the plugin context for the device's specified protocol _plugin = plugin.get_plugin(plugin_name) if not _plugin: raise errors.PluginNotFoundError( _('Unable to find plugin named "{}"').format(plugin_name)) # The data comes in as the POSTed dictionary which includes an 'action' # and/or 'raw'/'data' field. Here, we convert it to the appropriate modeling for # transport to the plugin. write_action = data.get('action') if not isinstance(write_action, str): raise errors.InvalidArgumentsError( _('"action" value must be a string, but was {}').format( type(write_action))) # Get the data out. If the 'data' field is present, we will use it. Otherwise, we will # look for a 'raw' field, for backwards compatibility. If 'data' exists, 'raw' is ignored. write_data = data.get('data') if write_data is None: write_data = data.get('raw') if write_data is not None: # The data should be an instance of bytes, which in python is a string if not isinstance(write_data, str): raise errors.InvalidArgumentsError( _('"raw"/"data" value must be a string, but was {}').format( type(write_data))) write_data = str.encode(write_data) wd = WriteData(action=write_action, data=write_data) logger.info( _('Writing to {}: {}').format('/'.join((rack, board, device)), wd)) # Perform a gRPC write on the device's managing plugin try: t = _plugin.client.write(rack, board, device, [wd]) except grpc.RpcError as ex: raise errors.FailedWriteCommandError(str(ex)) from ex # Now that we have the transaction info, we want to map it to the corresponding # process so any subsequent transaction check will know where to look. for _id, ctx in t.transactions.items(): context = {'action': ctx.action, 'data': ctx.data} ok = await cache.add_transaction(_id, context, _plugin.id()) if not ok: logger.error( _('Failed to add transaction {} to the cache').format(_id)) return WriteResponse(transactions=t.transactions)