示例#1
0
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()
示例#2
0
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
示例#3
0
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'
示例#4
0
    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)
示例#5
0
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()
示例#6
0
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()
示例#7
0
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()
示例#8
0
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)
示例#9
0
 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)
示例#10
0
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()
示例#11
0
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()
示例#12
0
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)