Functional Weave
Code in Python

net.device-validate@1.0.0

impl/python.py

4,887 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import Any, List, Mapping, Optional

from .net_cidr import cidr_info, is_cidr  ← from net.cidr ^1.0.0 · built alongside by fune
from .net_device_validate_types import DeviceValidation
from .net_hostname_validate import validate_hostname  ← from net.hostname-validate ^1.0.0 · built alongside by fune
from .net_ipv4 import is_ipv4  ← from net.ipv4 ^1.0.0 · built alongside by fune
from .net_ipv6 import format_ipv6, is_ipv6, parse_ipv6  ← from net.ipv6 ^1.0.0 · built alongside by fune
from .net_mac_address import validate_mac_address  ← from net.mac-address ^1.0.0 · built alongside by fune
from .net_port import is_port  ← from net.port ^1.0.0 · built alongside by fune

KNOWN = ("name", "host", "ports", "mac", "subnet")


def _present(entry: Mapping[str, Any], key: str) -> bool:
    return key in entry and entry[key] is not None


def _whole_number(value: Any) -> Optional[int]:
    """A whole number, whichever way the JSON reader stored it (22 and 22.0 are the same port)."""
    if isinstance(value, bool):
        return None
    if isinstance(value, int):
        return value
    if isinstance(value, float) and value == value and value not in (float("inf"), float("-inf")) and value.is_integer():
        return int(value)
    return None


def _canonical_ipv6_block(text: str) -> str:
    """An IPv6 block with its host bits cleared and its address in RFC 5952 form."""
    address, _, prefix_text = text.partition("/")
    prefix = int(prefix_text)
    groups = []
    for i, g in enumerate(parse_ipv6(address)):
        bits = min(16, max(0, prefix - 16 * i))
        groups.append(0 if bits == 0 else g & ((0xFFFF << (16 - bits)) & 0xFFFF))
    return "%s/%d" % (format_ipv6(groups), prefix)


def validate_device(entry: Mapping[str, Any]) -> DeviceValidation:
    """Check one inventory entry and say everything that is wrong with it at once,
    so a person fixing a file does not play whack-a-mole one error per run.
    Never raises: a bad entry is a result, not a crash.
    """
    errors: List[str] = []

    name: Optional[str] = None
    if not _present(entry, "name"):
        errors.append("name is required")
    elif not isinstance(entry["name"], str):
        errors.append("name must be text")
    elif entry["name"].strip(" \t\n\r\f\v") == "":
        errors.append("name must not be empty")
    else:
        name = entry["name"].strip(" \t\n\r\f\v")

    host: Optional[str] = None
    host_kind: Optional[str] = None
    if not _present(entry, "host"):
        errors.append("host is required")
    elif not isinstance(entry["host"], str):
        errors.append("host must be text")
    elif is_ipv4(entry["host"]):
        host = entry["host"]
        host_kind = "ipv4"
    elif is_ipv6(entry["host"]):
        host = format_ipv6(parse_ipv6(entry["host"]))
        host_kind = "ipv6"
    else:
        check = validate_hostname(entry["host"])
        if check.valid:
            host = check.hostname
            host_kind = "hostname"
        else:
            errors.append('host "%s" is not an IPv4 address, IPv6 address or host name: %s' % (entry["host"], check.reason))

    ports: List[int] = []
    if not _present(entry, "ports"):
        errors.append("ports is required")
    elif not isinstance(entry["ports"], (list, tuple)):
        errors.append("ports must be a list")
    elif len(entry["ports"]) == 0:
        errors.append("ports must not be empty")
    else:
        for i, item in enumerate(entry["ports"]):
            port = _whole_number(item)
            if port is None:
                errors.append("ports[%d] must be a whole number" % i)
            elif not is_port(port):
                errors.append("ports[%d] %d is not a port (1-65535)" % (i, port))
            elif port in ports:
                errors.append("ports[%d] %d is listed twice" % (i, port))
            else:
                ports.append(port)

    mac: Optional[str] = None
    if _present(entry, "mac"):
        if not isinstance(entry["mac"], str):
            errors.append("mac must be text")
        else:
            check = validate_mac_address(entry["mac"])
            if check.valid:
                mac = check.canonical
            else:
                errors.append('mac "%s" is not valid: %s' % (entry["mac"], check.reason))

    subnet: Optional[str] = None
    if _present(entry, "subnet"):
        if not isinstance(entry["subnet"], str):
            errors.append("subnet must be text")
        elif not is_cidr(entry["subnet"]):
            errors.append('subnet "%s" is not a CIDR block' % entry["subnet"])
        elif ":" in entry["subnet"]:
            subnet = _canonical_ipv6_block(entry["subnet"])
        else:
            subnet = cidr_info(entry["subnet"]).cidr

    # Sorted, not in file order: JavaScript lists integer-like keys first, so
    # "file order" would differ between languages.
    for key in sorted(k for k in entry.keys() if k not in KNOWN):
        errors.append('unknown field "%s"' % key)

    return DeviceValidation(
        valid=len(errors) == 0,
        errors=errors,
        name=name,
        host=host,
        host_kind=host_kind,
        ports=ports,
        mac=mac,
        subnet=subnet,
    )