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 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 test_synse_error_request_invalid_arguments(): """Check for INVALID_ARGUMENTS error""" e = errors.InvalidArgumentsError('message') assert isinstance(e, exceptions.InvalidUsage) assert isinstance(e, errors.SynseError) assert isinstance(e, errors.SynseInvalidUsageError) assert e.status_code == 400 assert e.error_id == errors.INVALID_ARGUMENTS assert e.args[0] == 'message'
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()
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()
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()
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)
async def inner(request, *args, **kwargs): # pylint: disable=missing-docstring if len(request.raw_args) != 0: raise errors.InvalidArgumentsError( _('Endpoint does not support query parameters but got: {}' ).format(request.raw_args)) return await f(request, *args, **kwargs)
async def fan_route(request, rack, board, device): """Endpoint to read/write fan 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: speed: The fan speed to set, in RPM. speed_percent: The fan speed to set, in percent. Args: request (sanic.request.Request): The incoming request. rack (str): The rack which the fan device resides on. board (str): The board which the fan device resides on. device (str): The fan device. Returns: sanic.response.HTTPResponse: The endpoint response. """ await validate.validate_device_type(const.TYPE_FAN, 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, 'speed', # speed in rpm 'speed_percent' # speed of 0 (off) or 10% to 100% ) param_speed_rpm = qparams.get('speed') param_speed_percent = qparams.get('speed_percent') # Only one of 'speed' and 'speed_percent' can be specified at a time. # TODO (etd): this could be generalized and incorporated into the validation # done above, e.g. validate_query_params(request.raw_args, OneOf(['speed', 'speed_percent'])) if all((param_speed_rpm, param_speed_percent)): raise errors.InvalidArgumentsError( _('Invalid query params: Can only specify one of "speed" and ' '"speed_percent", but both were given')) # If either of the parameters are specified, this will be a write request # using those parameters. if any((param_speed_rpm, param_speed_percent)): logger.debug( _('Fan alias route: writing (query parameters: {})').format( qparams)) # Set the fan speed by RPM. No validation on the fan speed is done here, # as it is up to the underlying implementation to validate and fail as # needed. The max and min allowable speeds vary by fan motor. if param_speed_rpm: logger.debug(_('Setting fan speed by RPM')) data = { 'action': 'speed', 'raw': param_speed_rpm, } transaction = await commands.write(rack, board, device, data) return transaction.to_json() # Set the fan speed by percent (duty cycle). No validation on the fan # speed is done here, as it is up to the underlying implementation to # validate and fail as needed. The max and min allowable speeds vary # by fan motor. if param_speed_percent: logger.debug(_('Setting fan speed by percent')) data = { 'action': 'speed_percent', 'raw': param_speed_percent, } transaction = await commands.write(rack, board, device, data) return transaction.to_json() # Otherwise, we just read from the device. else: logger.debug(_('Fan alias route: reading')) reading = await commands.read(rack, board, device) return reading.to_json()
async def led_route(request, rack, board, device): """Endpoint to read/write LED 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 LED state to set (on|off|blink) color: The LED color to set. This must be a hexadecimal string between 000000 and ffffff. Args: request (sanic.request.Request): The incoming request. rack (str): The rack which the led device resides on. board (str): The board which the led device resides on. device (str): The LED device. Returns: sanic.response.HTTPResponse: The endpoint response. """ await validate.validate_device_type(const.TYPE_LED, 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', 'color') param_state = qparams.get('state') param_color = qparams.get('color') # If any of the parameters are specified, this will be a write request # using those parameters. if any((param_state, param_color)): logger.debug( _('LED alias route: writing (query parameters: {})').format( qparams)) data = [] if param_state: if param_state not in const.led_states: raise errors.InvalidArgumentsError( _('Invalid LED state "{}". Must be one of: {}').format( param_state, const.led_states)) data.append({'action': 'state', 'raw': param_state}) if param_color: try: assert 0x000000 <= int(param_color, 16) <= 0xFFFFFF except Exception as e: raise errors.InvalidArgumentsError( _('Invalid color value ({}). Must be a hexadecimal ' 'string between 000000 and FFFFFF').format( param_color)) from e data.append({'action': 'color', 'raw': param_color}) logger.debug(_('LED data to write: {}').format(data)) transactions = None for d in data: t = await commands.write(rack, board, device, d) if not transactions: transactions = t else: transactions.data.extend(t.data) return transactions.to_json() # Otherwise, we just read from the device. else: logger.debug(_('LED alias route: reading')) reading = await commands.read(rack, board, device) return reading.to_json()
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)