예제 #1
0
async def patch_port(request: core_api.APIRequest, port_id: str,
                     params: Attributes) -> None:
    port = core_ports.get(port_id)
    if port is None:
        raise core_api.APIError(404, 'no-such-port')

    await set_port_attrs(port, params, ignore_extra_attrs=False)
예제 #2
0
async def delete_port_history(request: core_api.APIRequest, port_id: str) -> None:
    port = core_ports.get(port_id)
    if port is None:
        raise core_api.APIError(404, 'no-such-port')

    query = request.query

    from_str = query.get('from')
    if from_str is None:
        raise core_api.APIError(400, 'missing-field', field='from')

    try:
        from_timestamp = int(from_str)

    except ValueError:
        raise core_api.APIError(400, 'invalid-field', field='from') from None

    if from_timestamp < 0:
        raise core_api.APIError(400, 'invalid-field', field='from')

    to_str = query.get('to')
    if to_str is None:
        raise core_api.APIError(400, 'missing-field', field='to')

    try:
        to_timestamp = int(to_str)

    except ValueError:
        raise core_api.APIError(400, 'invalid-field', field='to') from None

    if to_timestamp < 0:
        raise core_api.APIError(400, 'invalid-field', field='to')

    await core_history.remove_samples([port], from_timestamp, to_timestamp)
    async def periodic_send_values(self) -> None:
        while True:
            ports = [
                core_ports.get(port_id) for port_id in self._fields.keys()
            ]
            port_values = {p.get_id(): p.get_last_read_value() for p in ports}
            field_values = {
                self._fields[id_]: value
                for id_, value in port_values.items()
            }

            try:
                if field_values:
                    await self.send_values(field_values,
                                           datetime.datetime.utcnow())

                else:
                    self.debug('not sending empty values')

            except asyncio.CancelledError:
                self.debug('periodic send values loop cancelled')
                break

            except Exception as e:
                self.error('sending values failed: %s', e, exc_info=True)

            await asyncio.sleep(self._period)
예제 #4
0
async def patch_port(request: core_api.APIRequest, port_id: str, params: Attributes) -> None:
    port = core_ports.get(port_id)
    if port is None:
        raise core_api.APIError(404, 'no such port')

    def unexpected_field_msg(field: str) -> str:
        if field in port.get_non_modifiable_attrs():
            return 'attribute not modifiable: {field}'

        else:
            return 'no such attribute: {field}'

    core_api_schema.validate(params, await port.get_schema(), unexpected_field_msg=unexpected_field_msg)

    # Step validation
    for name, value in params.items():
        attrdef = port.ATTRDEFS[name]
        step = attrdef.get('step')
        _min = attrdef.get('min')
        if None not in (step, _min) and step != 0 and (value - _min) % step:
            raise core_api.APIError(400, f'invalid field: {name}')

    errors_by_name = {}

    async def set_attr(attr_name: str, attr_value: Attribute) -> None:
        core_api.logger.debug('setting attribute %s = %s on %s', attr_name, json_utils.dumps(attr_value), port)

        try:
            await port.set_attr(attr_name, attr_value)

        except Exception as e:
            errors_by_name[attr_name] = e

    if params:
        await asyncio.wait([set_attr(name, value) for name, value in params.items()])

    if errors_by_name:
        name, error = next(iter(errors_by_name.items()))

        if isinstance(error, core_api.APIError):
            raise error

        elif isinstance(error, core_ports.InvalidAttributeValue):
            raise core_api.APIError(400, f'invalid field: {name}')

        elif isinstance(error, core_ports.PortTimeout):
            raise core_api.APIError(504, 'port timeout')

        elif isinstance(error, core_ports.PortError):
            raise core_api.APIError(502, f'port error: {error}')

        else:
            # Transform any unhandled exception into APIError(500)
            raise core_api.APIError(500, str(error))

    await port.save()
예제 #5
0
async def delete_port(request: core_api.APIRequest, port_id: str) -> None:
    port = core_ports.get(port_id)
    if not port:
        raise core_api.APIError(404, 'no-such-port')

    if not isinstance(port, core_vports.VirtualPort):
        raise core_api.APIError(400, 'port-not-removable')

    await port.remove()
    await core_vports.remove(port_id)
예제 #6
0
async def patch_port_value(request: core_api.APIRequest, port_id: str,
                           params: PortValue) -> None:
    port = core_ports.get(port_id)
    if port is None:
        raise core_api.APIError(404, 'no-such-port')

    try:
        core_api_schema.validate(params, await port.get_value_schema())

    except core_api.APIError:
        # Transform any validation error into an invalid-field APIError for value
        raise core_api.APIError(400, 'invalid-value') from None

    value = params

    # Step validation
    step = await port.get_attr('step')
    min_ = await port.get_attr('min')
    if None not in (step, min_) and step != 0 and (value - min_) % step:
        raise core_api.APIError(400, 'invalid-value')

    if not port.is_enabled():
        raise core_api.APIError(400, 'port-disabled')

    if not await port.is_writable():
        raise core_api.APIError(400, 'read-only-port')

    old_value = port.get_last_read_value()

    try:
        await port.write_transformed_value(value,
                                           reason=core_ports.CHANGE_REASON_API)

    except core_ports.PortTimeout as e:
        raise core_api.APIError(504, 'port-timeout') from e

    except core_ports.PortError as e:
        raise core_api.APIError(502, 'port-error', code=str(e)) from e

    except core_api.APIError:
        raise

    except Exception as e:
        # Transform any unhandled exception into APIError(500)
        raise core_api.APIError(500, 'unexpected-error', message=str(e)) from e

    await core_main.update()

    # If port value hasn't really changed, use 202 Accepted to inform consumer that new value hasn't been applied yet
    current_value = port.get_last_read_value()
    if (old_value == current_value) and (old_value != value):
        port.debug("API supplied value hasn't been applied right away")
        raise core_api.APIAccepted()
예제 #7
0
async def get_port_value(request: core_api.APIRequest, port_id: str) -> NullablePortValue:
    port = core_ports.get(port_id)
    if port is None:
        raise core_api.APIError(404, 'no-such-port')

    if not port.is_enabled():
        return

    # TODO
    # Given the fact that get_last_read_value() simply returns last cached read value, and the fact that API specs
    # indicate that 502/504 be returned by GET /port/[id]/value in case of errors, we should remember the last error
    # generated by read_value() and return it here, if any.

    return port.get_last_read_value()
예제 #8
0
async def patch_port_value(request: core_api.APIRequest, port_id: str, params: PortValue) -> None:
    port = core_ports.get(port_id)
    if port is None:
        raise core_api.APIError(404, 'no such port')

    core_api_schema.validate(params, await port.get_value_schema(), invalid_request_msg='invalid value')

    value = params

    # Step validation
    step = await port.get_attr('step')
    _min = await port.get_attr('min')
    if None not in (step, _min) and step != 0 and (value - _min) % step:
        raise core_api.APIError(400, 'invalid field: value')

    if not port.is_enabled():
        raise core_api.APIError(400, 'port disabled')

    if not await port.is_writable():
        raise core_api.APIError(400, 'read-only port')

    old_value = port.get_value()

    try:
        await port.write_transformed_value(value, reason=core_ports.CHANGE_REASON_API)

    except core_ports.PortTimeout as e:
        raise core_api.APIError(504, 'port timeout') from e

    except core_ports.PortError as e:
        raise core_api.APIError(502, f'port error: {e}') from e

    except core_api.APIError:
        raise

    except Exception as e:
        # Transform any unhandled exception into APIError(500)
        raise core_api.APIError(500, str(e)) from e

    await main.update()

    # If port value hasn't really changed, trigger a value-change to inform consumer that new value has been ignored
    current_value = port.get_value()
    if old_value == current_value:
        port.debug('API supplied value was ignored')
        port.trigger_value_change()
예제 #9
0
async def patch_port_sequence(request: core_api.APIRequest, port_id: str,
                              params: GenericJSONDict) -> None:
    port = core_ports.get(port_id)
    if port is None:
        raise core_api.APIError(404, 'no-such-port')

    core_api_schema.validate(params, core_api_schema.PATCH_PORT_SEQUENCE)

    values = params['values']
    delays = params['delays']
    repeat = params['repeat']

    if len(values) != len(delays):
        raise core_api.APIError(400, 'invalid-field', field='delays')

    value_schema = await port.get_value_schema()
    step = await port.get_attr('step')
    min_ = await port.get_attr('min')
    for value in values:
        # Translate any APIError generated when validating value schema into an invalid-field APIError on value
        try:
            core_api_schema.validate(value, value_schema)

        except core_api.APIError:
            raise core_api.APIError(400, 'invalid-field',
                                    field='values') from None

        # Step validation
        if None not in (step, min_) and step != 0 and (value - min_) % step:
            raise core_api.APIError(400, 'invalid-field', field='values')

    if not port.is_enabled():
        raise core_api.APIError(400, 'port-disabled')

    if not await port.is_writable():
        raise core_api.APIError(400, 'read-only-port')

    if await port.get_attr('expression'):
        raise core_api.APIError(400, 'port-with-expression')

    try:
        await port.set_sequence(values, delays, repeat)

    except Exception as e:
        # Transform any unhandled exception into APIError(500)
        raise core_api.APIError(500, 'unexpected-error', message=str(e)) from e
예제 #10
0
async def post_port_sequence(request: core_api.APIRequest, port_id: str, params: GenericJSONDict) -> None:
    port = core_ports.get(port_id)
    if port is None:
        raise core_api.APIError(404, 'no such port')

    core_api_schema.validate(params, core_api_schema.POST_PORT_SEQUENCE)

    values = params['values']
    delays = params['delays']
    repeat = params['repeat']

    if len(values) != len(delays):
        raise core_api.APIError(400, 'invalid field: delays')

    value_schema = await port.get_value_schema()
    step = await port.get_attr('step')
    _min = await port.get_attr('min')
    for value in values:
        core_api_schema.validate(value, value_schema, invalid_request_msg='invalid field: values')

        # Step validation
        if None not in (step, _min) and step != 0 and (value - _min) % step:
            raise core_api.APIError(400, 'invalid field: values')

    if not port.is_enabled():
        raise core_api.APIError(400, 'port disabled')

    if not await port.is_writable():
        raise core_api.APIError(400, 'read-only port')

    if await port.get_attr('expression'):
        raise core_api.APIError(400, 'port with expression')

    try:
        await port.set_sequence(values, delays, repeat)

    except Exception as e:
        # Transform any unhandled exception into APIError(500)
        raise core_api.APIError(500, str(e)) from e
예제 #11
0
async def add_virtual_port(attrs: GenericJSONDict) -> core_ports.BasePort:
    id_ = attrs['id']
    type_ = attrs['type']
    min_ = attrs.get('min')
    max_ = attrs.get('max')
    integer = attrs.get('integer')
    step = attrs.get('step')
    choices = attrs.get('choices')

    core_api.logger.debug('adding port "%s"', id_)

    if core_ports.get(id_):
        raise core_api.APIError(400, 'duplicate-port')

    if len(core_vports.all_port_args()) >= settings.core.virtual_ports:
        raise core_api.APIError(400, 'too-many-ports')

    await core_vports.add(id_, type_, min_, max_, integer, step, choices)
    port = await core_ports.load_one(
        'qtoggleserver.core.vports.VirtualPort',
        {
            'id_': id_,
            'type_': type_,
            'min_': min_,
            'max_': max_,
            'integer': integer,
            'step': step,
            'choices': choices
        },
        trigger_add=
        False  # Will trigger add event manually, later, after we've enabled the port
    )

    # A virtual port is enabled by default
    await port.enable()
    await port.save()
    await port.trigger_add()

    return port
예제 #12
0
async def post_ports(request: core_api.APIRequest, params: GenericJSONDict) -> Attributes:
    core_api_schema.validate(params, core_api_schema.POST_PORTS)

    port_id = params['id']
    port_type = params['type']
    _min = params.get('min')
    _max = params.get('max')
    integer = params.get('integer')
    step = params.get('step')
    choices = params.get('choices')

    if core_ports.get(port_id):
        raise core_api.APIError(400, 'duplicate port')

    if len(core_vports.all_settings()) >= settings.core.virtual_ports:
        raise core_api.APIError(400, 'too many ports')

    core_vports.add(port_id, port_type, _min, _max, integer, step, choices)
    port = await core_ports.load_one(
        'qtoggleserver.core.vports.VirtualPort',
        {
            'port_id': port_id,
            '_type': port_type,
            '_min': _min,
            '_max': _max,
            'integer': integer,
            'step': step,
            'choices': choices
        }
    )

    # A virtual port is enabled by default
    await port.enable()
    await port.save()

    return await port.to_json()
예제 #13
0
async def get_port_history(request: core_api.APIRequest, port_id: str) -> GenericJSONList:
    port = core_ports.get(port_id)
    if port is None:
        raise core_api.APIError(404, 'no-such-port')

    query = request.query

    from_str = query.get('from')
    timestamps_str = query.get('timestamps')

    if from_str is None and timestamps_str is None:
        raise core_api.APIError(400, 'missing-field', field='from')

    if from_str:
        try:
            from_timestamp = int(from_str)

        except ValueError:
            raise core_api.APIError(400, 'invalid-field', field='from')

        if from_timestamp < 0:
            raise core_api.APIError(400, 'invalid-field', field='from')

    else:
        from_timestamp = None

    to_str = query.get('to')
    to_timestamp = int(time.time() * 1000)
    if to_str is not None:
        try:
            to_timestamp = int(to_str)

        except ValueError:
            raise core_api.APIError(400, 'invalid-field', field='to') from None

        if to_timestamp < 0:
            raise core_api.APIError(400, 'invalid-field', field='to')

    limit_str = query.get('limit')
    limit = 1000  # default
    if limit_str is not None:
        try:
            limit = int(limit_str)

        except ValueError:
            raise core_api.APIError(400, 'invalid-field', field='limit') from None

        if limit < 1 or limit > 10000:
            raise core_api.APIError(400, 'invalid-field', field='limit')

    timestamps = None
    if timestamps_str is not None:
        timestamps = timestamps_str.split(',')
        try:
            timestamps = [int(t) for t in timestamps]

        except ValueError:
            raise core_api.APIError(400, 'invalid-field', field='timestamps')

        if any((t < 0) for t in timestamps):
            raise core_api.APIError(400, 'invalid-field', field='timestamps')

    if timestamps is not None:
        samples = await core_history.get_samples_by_timestamp(port, timestamps)

    else:
        samples = await core_history.get_samples_slice(port, from_timestamp, to_timestamp, limit)

    return list(samples)
예제 #14
0
async def put_ports(request: core_api.APIRequest, params: GenericJSONList) -> None:
    if not settings.core.backup_support:
        raise core_api.APIError(404, 'no-such-function')

    core_api_schema.validate(
        params,
        core_api_schema.PUT_PORTS
    )

    core_api.logger.debug('restoring ports')

    # Disable event handling during the processing of this request, as we're going to trigger a full-update at the end
    core_events.disable()

    # Temporarily disable core updating (port polling, expression evaluating and value-change handling)
    core_main.disable_updating()

    try:
        # Remove all (local) virtual ports
        for port in core_ports.get_all():
            if not isinstance(port, core_vports.VirtualPort):
                continue

            await port.remove()
            await core_vports.remove(port.get_id())

        # Reset ports
        await core_ports.reset()
        if settings.slaves.enabled:
            await slaves.reset_ports()
        for port in core_ports.get_all():
            await port.reset()

        add_port_schema = dict(core_api_schema.POST_PORTS)
        add_port_schema['additionalProperties'] = True

        # Restore supplied attributes
        for attrs in params:
            id_ = attrs.get('id')
            if id_ is None:
                core_api.logger.warning('ignoring entry without id')
                continue

            port = core_ports.get(id_)

            # Virtual ports must be added first (unless they belong to a slave)
            virtual = attrs.get('virtual')
            if port is not None:  # Port already exists so it probably belongs to a slave
                virtual = False
            for slave in slaves_devices.get_all():
                if id_.startswith(f'{slave.get_name()}.'):  # id indicates that port belongs to a slave
                    virtual = False
                    break
            if 'provisioning' in attrs:  # A clear indication that port belongs to a slave
                virtual = False

            if virtual:
                await wrap_error_with_port_id(
                    id_,
                    core_api_schema.validate,
                    attrs,
                    add_port_schema
                )
                port = await wrap_error_with_port_id(
                    id_,
                    add_virtual_port,
                    attrs
                )

            if port is None:
                core_api.logger.warning('ignoring unknown port id "%s"', id_)
                continue

            if isinstance(port, slaves_ports.SlavePort):
                core_api.logger.debug('restoring slave port "%s"', id_)

                # For slave ports, ignore any attributes that are not kept on master
                attrs = {n: v for n, v in attrs.items() if n in ('tag', 'expression', 'expires')}

            else:
                core_api.logger.debug('restoring local port "%s"', id_)

            await wrap_error_with_port_id(
                id_,
                set_port_attrs,
                port,
                attrs,
                ignore_extra_attrs=True
            )

    finally:
        core_main.enable_updating()
        core_events.enable()

    await core_events.trigger_full_update()

    core_api.logger.debug('ports restore done')
예제 #15
0
 def get_port(self) -> core_ports.BasePort:
     return core_ports.get(self.port_id)