def _accepts_attrs(attr_names: Set[str], filter_attrs: Attributes,
                       filter_attr_transitions: Dict[str, Tuple[Attribute,
                                                                Attribute]],
                       old_attrs: Attributes, new_attrs: Attributes) -> bool:

        for name in attr_names:
            old_value = old_attrs.get(name)
            new_value = new_attrs.get(name)

            filter_transition = filter_attr_transitions.get(name)
            if filter_transition is not None:
                old_filter_value, new_filter_value = filter_transition
                if ((old_filter_value != old_value
                     and old_filter_value is not None)
                        or (new_filter_value != new_value
                            and new_filter_value is not None)):

                    return False

            filter_value = filter_attrs.get(name)
            if filter_value is not None:
                if isinstance(filter_value, list):  # A list of accepted values
                    if new_value not in filter_value:
                        return False

                elif new_value != filter_value:  # A single value
                    return False

        return True
Beispiel #2
0
async def put_device(request: core_api.APIRequest, params: Attributes) -> None:
    core_api_schema.validate(params, core_device_attrs.get_schema(loose=True))

    # Password fields must explicitly be ignored, so we pop them from supplied data
    for f in ('admin', 'normal', 'viewonly'):
        params.pop(f'{f}_password', None)

    # Ignore the date attribute
    params.pop('date', None)

    # Reset device attributes
    await core_device.reset(preserve_attrs=[
        'admin_password_hash', 'normal_password_hash', 'viewonly_password_hash'
    ])
    await core_device.load()

    try:
        core_device_attrs.set_attrs(params, ignore_extra=True)

    except core_device_attrs.DeviceAttributeError as e:
        raise core_api.APIError(400, e.error, attribute=e.attribute)

    except Exception as e:
        raise core_api.APIError(500, 'unexpected-error', message=str(e)) from e

    await core_device.save()
    await core_device_events.trigger_update()
    def _make_changed_added_removed(
        old_attrs: Attributes, new_attrs: Attributes
    ) -> Tuple[Dict[str, Tuple[Attribute, Attribute]], Attributes, Attributes]:

        changed_attrs = {}
        added_attrs = {}
        removed_attrs = {}

        all_attr_names = set(old_attrs) | set(new_attrs)
        for n in all_attr_names:
            old_v = old_attrs.get(n)
            new_v = new_attrs.get(n)
            if old_v == new_v:
                continue

            if old_v is None:
                if new_v is not None:
                    added_attrs[n] = new_v

            elif new_v is None:
                removed_attrs[n] = old_v

            else:
                changed_attrs[n] = (old_v, new_v)

        return changed_attrs, added_attrs, removed_attrs
Beispiel #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()
Beispiel #5
0
    def update_cached_attrs(self, attrs: Attributes) -> None:
        # Use fire-and-forget here to enable/disable ports, as this method cannot be async
        if attrs.get('enabled') and not self.is_enabled():
            asyncio.create_task(self.enable())

        elif not attrs.get('enabled') and self.is_enabled():
            asyncio.create_task(self.disable())

        self._cached_attrs = dict(attrs)

        # Value can be found among attrs but we don't want it as attribute
        if 'value' in attrs:
            self._cached_value = self._cached_attrs.pop('value')

        self.invalidate_attrs()
        self.invalidate_attrdefs()
Beispiel #6
0
def set_attrs(attrs: Attributes, ignore_extra: bool = False) -> bool:
    core_device_attrs = sys.modules[__name__]

    reboot_required = False
    attrdefs = get_attrdefs()

    wifi_attrs = {}
    ip_attrs = {}

    for n, value in attrs.items():
        # A few attributes may carry sensitive information, so treat them separately and do not log their values
        if n.count('password') or n == 'wifi_key':
            logger.debug('setting device attribute %s', n)

        else:
            logger.debug('setting device attribute %s = %s', n,
                         json_utils.dumps(value))

        try:
            attrdef = attrdefs[n]

        except KeyError:
            if ignore_extra:
                continue

            else:
                raise

        if not attrdef.get('modifiable'):
            if not ignore_extra:
                raise DeviceAttributeError('attribute-not-modifiable', n)

        # Treat passwords separately, as they are not persisted as given, but hashed first
        if n.endswith('_password') and hasattr(core_device_attrs, f'{n}_hash'):
            # Call password set command, if available
            if settings.core.passwords.set_cmd:
                run_set_cmd(settings.core.passwords.set_cmd,
                            cmd_name='password',
                            log_values=False,
                            username=n[:-9],
                            password=value)

            value = hashlib.sha256(value.encode()).hexdigest()
            n += '_hash'

            setattr(core_device_attrs, n, value)
            continue

        elif n.endswith('_password_hash') and hasattr(core_device_attrs, n):
            # FIXME: Password set command cannot be called with hash and we don't have clear-text password here.
            #        A solution would be to use sha256 crypt algorithm w/o salt for Unix password (watch for the special
            #        alphabet and for number of rounds defaulting to 5000)
            setattr(core_device_attrs, n, value)
            continue

        persisted = attrdef.get('persisted', attrdef.get('modifiable'))
        if persisted:
            setattr(core_device_attrs, n, value)

        if n == 'name' and settings.core.device_name.set_cmd:
            run_set_cmd(settings.core.device_name.set_cmd,
                        cmd_name='device name',
                        name=value)

        elif n == 'date' and system.date.has_set_date_support():
            date = datetime.datetime.utcfromtimestamp(value)
            system.date.set_date(date)

        elif n == 'timezone' and system.date.has_timezone_support():
            system.date.set_timezone(value)

        elif n in ('wifi_ssid', 'wifi_key',
                   'wifi_bssid') and system.net.has_wifi_support():
            k = n[5:]
            k = {'key': 'psk'}.get(k, k)
            wifi_attrs[k] = value

        elif n in ('ip_address', 'ip_netmask', 'ip_gateway',
                   'ip_dns') and system.net.has_ip_support():
            k = n[3:]
            ip_attrs[k] = value

    if wifi_attrs:
        wifi_config = system.net.get_wifi_config()

        for k, v in wifi_attrs.items():
            wifi_config[k] = v
            wifi_config = {
                k: v
                for k, v in wifi_config.items() if not k.endswith('_current')
            }

        system.net.set_wifi_config(**wifi_config)
        reboot_required = True

    if ip_attrs:
        ip_config = system.net.get_ip_config()

        for k, v in ip_attrs.items():
            ip_config[k] = v
            ip_config = {
                k: v
                for k, v in ip_config.items() if not k.endswith('_current')
            }
            ip_config['netmask'] = str(ip_config['netmask'])

        system.net.set_ip_config(**ip_config)
        reboot_required = True

    return reboot_required
Beispiel #7
0
async def configure(discovered_device: DiscoveredDevice, attrs: Attributes) -> DiscoveredDevice:
    ap_client = discovered_device.ap_client

    # Wi-Fi settings of the real (target) network
    if 'wifi_ssid' not in attrs:
        logger.warning('no SSID/PSK available to configure device')

    if 'admin_password' not in attrs:
        logger.debug('generating password for %s', discovered_device)
        discovered_device.admin_password = salves_utils.generate_password(
            core_device_attrs.admin_password_hash,
            discovered_device.ap_client.mac_address,
            'admin'
        )

        attrs['admin_password'] = discovered_device.admin_password

        # Also set normal and view-only password, if exposed
        if 'normal_password' in discovered_device.attrs:
            attrs['normal_password'] = salves_utils.generate_password(
                core_device_attrs.admin_password_hash,
                discovered_device.ap_client.mac_address,
                'normal'
            )

        if 'viewonly_password' in discovered_device.attrs:
            attrs['viewonly_password'] = salves_utils.generate_password(
                core_device_attrs.admin_password_hash,
                discovered_device.ap_client.mac_address,
                'viewonly'
            )

    else:
        logger.debug('using supplied password for %s', discovered_device)
        discovered_device.admin_password = attrs['admin_password']

    network_configured = attrs.get('wifi_ssid') is not None
    if network_configured:
        # Find client's future IP address first
        dhcp_interface = settings.slaves.discover.dhcp_interface or net.get_default_interface()
        if not dhcp_interface:
            raise DiscoverException('No DHCP interface')

        try:
            reply = await dhcp.request(
                interface=dhcp_interface,
                timeout=settings.slaves.discover.dhcp_timeout,
                mac_address=ap_client.mac_address,
                hostname=ap_client.hostname
            )

        except dhcp.DHCPTimeout:
            logger.warning('could not determine future device IP address of %s', discovered_device)
            reply = None

        if reply:
            adjusted_ap_client = ap.APClient(
                mac_address=ap_client.mac_address,
                ip_address=reply.ip_address,
                hostname=ap_client.hostname,
                moment=ap_client.moment
            )

            discovered_device = DiscoveredDevice(
                ap_client=adjusted_ap_client,
                scheme=discovered_device.scheme,
                port=discovered_device.port,
                path=discovered_device.path,
                admin_password=discovered_device.admin_password,
                attrs=discovered_device.attrs
            )

            dns.set_custom_dns_mapping(ap_client.hostname, reply.ip_address, timeout=60)

    logger.debug('configuring %s', discovered_device)
    await ap_client.request('PATCH', f'{discovered_device.path}/device', body=attrs)
    logger.debug('%s successfully configured', discovered_device)

    # Remove configured device from discovered list
    _discovered_devices.pop(discovered_device.attrs['name'])

    if network_configured:
        logger.debug('waiting for %s to connect to new network', discovered_device)
        await asyncio.sleep(5)  # Device requires at least 5 seconds to connect to new network
        start_time = time.time()
        while True:
            try:
                await discovered_device.ap_client.request(
                    'GET',
                    '/device',
                    no_log=True,
                    admin_password=discovered_device.admin_password
                )
                logger.debug('%s connected to new network', discovered_device)
                break

            except Exception:
                if time.time() - start_time > settings.slaves.long_timeout:
                    logger.error('timeout waiting for %s to connect to new network', discovered_device)
                    break

                await asyncio.sleep(1)

    return discovered_device
Beispiel #8
0
def set_attrs(attrs: Attributes) -> bool:
    core_device_attrs = sys.modules[__name__]

    reboot_required = False

    for name, value in attrs.items():
        # A few attributes may carry sensitive information, so treat them separately and do not log their values
        if name.count('password'):
            logger.debug('setting device attribute %s', name)

        elif name == 'network_wifi':
            logger.debug('setting device attribute %s = [hidden]', name)

        else:
            logger.debug('setting device attribute %s = %s', name,
                         json_utils.dumps(value))

        attrdef = ATTRDEFS[name]

        if not attrdef.get('modifiable'):
            raise DeviceAttributeError(f'attribute not modifiable: {name}')

        # Treat passwords separately, as they are not persisted as given, but hashed first
        if name.endswith('_password') and hasattr(core_device_attrs,
                                                  name + '_hash'):
            # Call password set command, if available
            if settings.password_set_cmd:
                run_set_cmd(settings.password_set_cmd,
                            cmd_name='password',
                            log_values=False,
                            username=name[:-9],
                            password=value)

            value = hashlib.sha256(value.encode()).hexdigest()
            name += '_hash'

            setattr(core_device_attrs, name, value)
            continue

        persisted = attrdef.get('persisted', attrdef.get('modifiable'))
        if callable(persisted):
            persisted = persisted()
        if persisted:
            setattr(core_device_attrs, name, value)

        if name == 'name' and settings.device_name.set_cmd:
            run_set_cmd(settings.device_name.set_cmd,
                        cmd_name='device name',
                        name=value)

        elif name == 'date' and system.date.has_date_support():
            try:
                date = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')

            except ValueError:
                raise DeviceAttributeError(f'invalid field: {name}')

            system.date.set_date(date)

        elif name == 'timezone' and system.date.has_timezone_support():
            system.date.set_timezone(value)

        elif name == 'network_wifi' and system.net.has_network_wifi_support():
            parts = value.split(':')
            i = 0
            while i < len(parts):
                if len(parts[i]) and parts[i][-1] == '\\':
                    parts[i] = parts[i][:-1] + ':' + parts[i + 1]
                    del parts[i + 1]
                i += 1

            parts = [p.replace('\\\\', '\\') for p in parts]
            while len(parts) < 3:
                parts.append('')

            ssid, psk, bssid = parts[:3]
            bssid = bssid.lower()
            bssid = re.sub('([a-f0-9]{2})', '\\1:',
                           bssid).strip(':')  # Add colons

            system.net.set_wifi_config(ssid, psk, bssid)
            reboot_required = True

        elif name == 'network_ip' and system.net.has_network_ip_support():
            if value:
                parts = value.split(':')
                ip_mask, gw, dns = parts
                ip, mask = ip_mask.split('/')
                system.net.set_ip_config(ip, mask, gw, dns)

            else:
                system.net.set_ip_config(ip='', mask='', gw='', dns='')

            reboot_required = True

    return reboot_required