async def load_from_data(self, data: GenericJSONDict) -> None: attrs_start = ['enabled'] # These will be loaded first, in this order attrs_end = ['expression'] # These will be loaded last, in this order attr_items = data.items() attr_items = [a for a in attr_items if (a[0] not in attrs_start) and (a[0] not in attrs_end)] attr_items_start = [] for n in attrs_start: v = data.get(n) if v is not None: attr_items_start.append((n, v)) # Sort the rest of the attributes alphabetically attr_items.sort(key=lambda i: i[0]) attr_items_end = [] for n in attrs_end: v = data.get(n) if v is not None: attr_items_end.append((n, v)) attr_items = attr_items_start + attr_items + attr_items_end for name, value in attr_items: if name in ('id', 'value'): continue # Value is also among the persisted fields try: self.debug('loading %s = %s', name, json_utils.dumps(value)) await self.set_attr(name, value) except Exception as e: self.error('failed to set attribute %s = %s: %s', name, json_utils.dumps(value), e) # Value if await self.is_persisted() and data.get('value') is not None: self._value = data['value'] self.debug('loaded value = %s', json_utils.dumps(self._value)) if await self.is_writable(): # Write the just-loaded value to the port value = self._value if self._transform_write: value = await self.adapt_value_type(self._transform_write.eval()) await self.write_value(value) elif self.is_enabled(): try: value = await self.read_transformed_value() if value is not None: self._value = value self.debug('read value = %s', json_utils.dumps(self._value)) except Exception as e: self.error('failed to read value: %s', e, exc_info=True)
async def load_from_data(self, data: GenericJSONDict) -> None: attrs = data.get('attrs') if attrs: if 'value' in data: attrs['value'] = data['value'] self.update_cached_attrs(attrs) # Attributes that are kept on master for attr in ('tag', 'expression', 'last_sync', 'expires'): if attr in data: await self.set_attr(attr, data[attr]) self._provisioning = set(data.get('provisioning', []))
async def put_frontend(request: core_api.APIRequest, params: GenericJSONDict) -> None: # core_api.validate(panels, FRONTEND_SCHEMA) TODO: validate against schema logger.debug('restoring frontend configuration') await persist.remove('frontend_prefs') prefs = params.get('prefs') if prefs: for p in prefs: logger.debug('restoring frontend prefs for username "%s"', p.get('id')) await persist.insert('frontend_prefs', p) dashboard_panels = params.get('dashboard_panels', []) logger.debug('restoring dashboard panels') await persist.set_value('dashboard_panels', dashboard_panels) await core_events.trigger(DashboardUpdateEvent(request=request, panels=dashboard_panels))
async def add_slave_device_retry_disabled(properties: GenericJSONDict) -> slaves_devices.Slave: try: return await add_slave_device(properties) except core_api.APIError: if properties.get('enabled', True): core_api.logger.warning('adding device failed, adding it as disabled', exc_info=True) return await add_slave_device(dict(properties, enabled=False)) else: raise
async def load_from_data(self, data: GenericJSONDict) -> None: # Only consider locally persisted attributes for permanently offline devices. For online devices, we always use # fresh attributes received from device. attrs = data.get('attrs') if attrs and self._slave.is_permanently_offline(): if 'value' in data: attrs['value'] = data['value'] self.update_cached_attrs(attrs) # Attributes that are kept on master for attr in MASTER_ATTRS: if attr in data: await self.set_attr(attr, data[attr]) self._history_last_timestamp = data.get('history_last_timestamp', 0) self._provisioning = set(data.get('provisioning', [])) # Enable if enabled remotely await self.update_enabled()
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
async def patch_firmware(request: core_api.APIRequest, params: GenericJSONDict) -> None: core_api_schema.validate(params, core_api_schema.PATCH_FIRMWARE) status = await fwupdate.get_status() if status not in (fwupdate.STATUS_IDLE, fwupdate.STATUS_ERROR): raise core_api.APIError(503, 'busy') if params.get('url'): await fwupdate.update_to_url(params['url']) else: # Assuming params['version'] await fwupdate.update_to_version(params['version'])
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()
async def post_reset(request: core_api.APIRequest, params: GenericJSONDict) -> None: core_api_schema.validate(params, core_api_schema.POST_RESET) factory = params.get('factory') if factory: core_api.logger.info('resetting to factory defaults') core_ports.reset() core_vports.reset() core_device.reset() if settings.webhooks.enabled: core_webhooks.reset() if settings.reverse.enabled: core_reverse.reset() if settings.slaves.enabled: slaves.reset() main.loop.call_later(2, system.reboot)
async def post_introspect(request: core_api.APIRequest, params: GenericJSONDict) -> GenericJSONDict: core_api_schema.validate(params, core_api_schema.POST_INTROSPECT) exc_str = None res_str = None try: imports = params.get('imports', []) extra_locals = {} for imp in imports: extra_locals[imp.split('.')[0]] = importlib.__import__(imp) result = eval(params['code'], globals(), dict(locals(), **extra_locals)) if inspect.isawaitable(result): result = await result res_str = str(result) except Exception: exc_str = traceback.format_exc() return {'result': res_str, 'exception': exc_str}
async def patch_slave_device(request: core_api.APIRequest, name: str, params: GenericJSONDict) -> None: core_api_schema.validate(params, api_schema.PATCH_SLAVE_DEVICE) slave = slaves_devices.get(name) if not slave: raise core_api.APIError(404, 'no such device') if params.get('enabled') is True and not slave.is_enabled(): await slave.enable() elif params.get('enabled') is False and slave.is_enabled(): await slave.disable() if params.get('poll_interval') and params.get('listen_enabled'): raise core_api.APIError(400, 'listening and polling') if params.get('poll_interval') is not None: slave.set_poll_interval(params['poll_interval']) if params.get('listen_enabled') is not None: if params['listen_enabled']: # We need to know if device supports listening; we therefore call GET /device before enabling it if slave.is_enabled(): try: attrs = await slave.api_call('GET', '/device') except Exception as e: raise exceptions.adapt_api_error(e) from e if 'listen' not in attrs['flags']: raise core_api.APIError(400, 'no listen support') slave.enable_listen() else: slave.disable_listen() slave.save() slave.trigger_update()
def _record_from_db(cls, db_record: GenericJSONDict, fields: Optional[Set[str]] = None) -> Record: if fields is not None: return {k: (cls._value_from_db(v) if k != 'id' else v) for k, v in db_record.items() if k in fields} else: return {k: (cls._value_from_db(v) if k != 'id' else v) for k, v in db_record.items()}
async def add_slave_device(properties: GenericJSONDict) -> slaves_devices.Slave: properties = dict(properties) # Work on copy, don't mess up incoming argument scheme = properties.pop('scheme') host = properties.pop('host') port = properties.pop('port') path = properties.pop('path') admin_password = properties.pop('admin_password', None) admin_password_hash = properties.pop('admin_password_hash', None) poll_interval = properties.pop('poll_interval', 0) listen_enabled = properties.pop('listen_enabled', None) # Look for slave duplicate for slave in slaves_devices.get_all(): if (slave.get_scheme() == scheme and slave.get_host() == host and slave.get_port() == port and slave.get_path() == path): raise core_api.APIError(400, 'duplicate-device') if poll_interval and listen_enabled: raise core_api.APIError(400, 'listening-and-polling') # Ensure admin password is supplied, in a way or another if admin_password is None and admin_password_hash is None: raise core_api.APIError(400, 'missing-field', field='admin_password') try: slave = await slaves_devices.add( scheme, host, port, path, poll_interval, listen_enabled, admin_password=admin_password, admin_password_hash=admin_password_hash, **properties ) except (core_responses.HostUnreachable, core_responses.NetworkUnreachable, core_responses.UnresolvableHostname) as e: raise core_api.APIError(502, 'unreachable') from e except core_responses.ConnectionRefused as e: raise core_api.APIError(502, 'connection-refused') from e except core_responses.InvalidJson as e: raise core_api.APIError(502, 'invalid-device') from e except core_responses.Timeout as e: raise core_api.APIError(504, 'device-timeout') from e except slaves_exceptions.InvalidDevice as e: raise core_api.APIError(502, 'invalid-device') from e except slaves_exceptions.NoListenSupport as e: raise core_api.APIError(400, 'no-listen-support') from e except slaves_exceptions.DeviceAlreadyExists as e: raise core_api.APIError(400, 'duplicate-device') from e except core_api.APIError: raise except core_responses.HTTPError as e: # We need to treat the 401/403 slave responses as a 400 if e.code in (401, 403): raise core_api.APIError(400, 'forbidden') from e raise core_api.APIError.from_http_error(e) from e except Exception as e: raise slaves_exceptions.adapt_api_error(e) from e return slave
async def set_port_attrs(port: core_ports.BasePort, attrs: GenericJSONDict, ignore_extra_attrs: bool) -> None: non_modifiable_attrs = await port.get_non_modifiable_attrs() def unexpected_field_code(field: str) -> str: if field in non_modifiable_attrs: return 'attribute-not-modifiable' else: return 'no-such-attribute' schema = await port.get_schema() if ignore_extra_attrs: schema = dict(schema) schema['additionalProperties'] = True # Ignore non-existent and non-modifiable attributes core_api_schema.validate( attrs, schema, unexpected_field_code=unexpected_field_code, unexpected_field_name='attribute' ) # Step validation attrdefs = await port.get_attrdefs() for name, value in attrs.items(): attrdef = attrdefs.get(name) if attrdef is None: continue 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, 'invalid-field', 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 e1: errors_by_name[attr_name] = e1 value = attrs.pop('value', None) if attrs: await asyncio.wait([set_attr(n, v) for n, v in attrs.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, 'invalid-field', field=name, details=error.details) elif isinstance(error, core_ports.PortTimeout): raise core_api.APIError(504, 'port-timeout') elif isinstance(error, core_ports.PortError): raise core_api.APIError(502, 'port-error', code=str(error)) else: # Transform any unhandled exception into APIError(500) raise core_api.APIError(500, 'unexpected-error', message=str(error)) from error # If value is supplied among attrs, use it to update port value, but in background and ignoring any errors if value is not None and port.is_enabled(): asyncio.create_task(port.write_transformed_value(value, reason=core_ports.CHANGE_REASON_API)) await port.save()
def _record_from_db(cls, db_record: GenericJSONDict) -> Record: return { k: (cls._value_from_db(v) if k != 'id' else v) for k, v in db_record.items() }