diff --git a/interfaces/forms.py b/interfaces/forms.py index c62f1816..83923c8c 100644 --- a/interfaces/forms.py +++ b/interfaces/forms.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ class AddInterface(forms.Form): - name = forms.CharField(max_length=10, required=True) + name = forms.CharField(max_length=15, required=True) itype = forms.ChoiceField( required=True, choices=(("bridge", "bridge"), ("ethernet", "ethernet")) ) @@ -19,7 +19,8 @@ class AddInterface(forms.Form): required=True, choices=(("dhcp", "dhcp"), ("static", "static"), ("none", "none")), ) - ipv4_addr = forms.CharField(max_length=18, required=False) + # room for a dotted netmask: 192.0.2.10/255.255.255.0 + ipv4_addr = forms.CharField(max_length=31, required=False) ipv4_gw = forms.CharField(max_length=15, required=False) ipv6_type = forms.ChoiceField( required=True, @@ -28,81 +29,92 @@ class AddInterface(forms.Form): ipv6_addr = forms.CharField(max_length=100, required=False) ipv6_gw = forms.CharField(max_length=100, required=False) stp = forms.ChoiceField(required=False, choices=(("on", "on"), ("off", "off"))) - delay = forms.IntegerField(required=False) + delay = forms.IntegerField(required=False, min_value=0) + + def __init__(self, *args, netdevs=(), **kwargs): + # The host's network devices: a bridge member, or the device an + # ethernet interface configures, must be one of them. + super().__init__(*args, **kwargs) + self.netdevs = set(netdevs) def clean_ipv4_addr(self): ipv4_addr = self.cleaned_data["ipv4_addr"] - have_symbol = re.match("^[0-9./]+$", ipv4_addr) - if not have_symbol: - raise forms.ValidationError( - _("The IPv4 address must not contain any special characters") - ) - elif len(ipv4_addr) > 20: - raise forms.ValidationError( - _("The IPv4 address must not exceed 20 characters") - ) - return ipv4_addr + if not ipv4_addr: + return ipv4_addr + try: + # Always address/prefix, as the interface XML needs both + return str(ipaddress.IPv4Interface(ipv4_addr)) + except ValueError: + raise forms.ValidationError(_("The IPv4 address is not valid (e.g. 192.0.2.10/24)")) def clean_ipv4_gw(self): ipv4_gw = self.cleaned_data["ipv4_gw"] - have_symbol = re.match("^[0-9.]+$", ipv4_gw) - if not have_symbol: - raise forms.ValidationError( - _("The IPv4 gateway must not contain any special characters") - ) - elif len(ipv4_gw) > 20: - raise forms.ValidationError( - _("The IPv4 gateway must not exceed 20 characters") - ) - return ipv4_gw + if not ipv4_gw: + return ipv4_gw + try: + return str(ipaddress.IPv4Address(ipv4_gw)) + except ValueError: + raise forms.ValidationError(_("The IPv4 gateway is not valid")) + # libvirt's interface schema has no IPv6 scope id (fe80::1%eth0) def clean_ipv6_addr(self): ipv6_addr = self.cleaned_data["ipv6_addr"] if not ipv6_addr: return ipv6_addr try: - # Always address/prefix, as the interface XML needs both - return str(ipaddress.IPv6Interface(ipv6_addr)) + address = ipaddress.IPv6Interface(ipv6_addr) except ValueError: + address = None + if address is None or address.scope_id is not None: raise forms.ValidationError(_("The IPv6 address is not valid (e.g. 2001:db8::10/64)")) + # Always address/prefix, as the interface XML needs both + return str(address) def clean_ipv6_gw(self): ipv6_gw = self.cleaned_data["ipv6_gw"] if not ipv6_gw: return ipv6_gw try: - return str(ipaddress.IPv6Address(ipv6_gw)) + gateway = ipaddress.IPv6Address(ipv6_gw) except ValueError: + gateway = None + if gateway is None or gateway.scope_id is not None: raise forms.ValidationError(_("The IPv6 gateway is not valid")) - - def clean(self): - cleaned = super().clean() - if cleaned.get("ipv6_type") == "static" and not cleaned.get("ipv6_addr") and "ipv6_addr" not in self.errors: - self.add_error("ipv6_addr", _("A static IPv6 configuration needs an address")) - return cleaned + return str(gateway) def clean_name(self): + # A Linux interface name: up to 15 characters, no spaces or slashes name = self.cleaned_data["name"] - have_symbol = re.match("^[a-z0-9.]+$", name) - if not have_symbol: + if not re.fullmatch(r"[A-Za-z0-9_.-]{1,15}", name) or name in (".", ".."): raise forms.ValidationError( - _("The interface must not contain any special characters") - ) - elif len(name) > 10: - raise forms.ValidationError( - _("The interface must not exceed 10 characters") + _("The interface name may have up to 15 letters, digits and . _ -") ) return name def clean_netdev(self): netdev = self.cleaned_data["netdev"] - have_symbol = re.match("^[a-z0-9.:]+$", netdev) - if not have_symbol: + if not re.fullmatch(r"[A-Za-z0-9_.-]{1,15}", netdev) or netdev in (".", ".."): raise forms.ValidationError( - _("The interface must not contain any special characters") - ) - elif len(netdev) > 10: - raise forms.ValidationError( - _("The interface must not exceed 10 characters") + _("The device name may have up to 15 letters, digits and . _ -") ) return netdev + + def clean(self): + cleaned = super().clean() + for family in ("ipv4", "ipv6"): + addr = f"{family}_addr" + if cleaned.get(f"{family}_type") == "static" and not cleaned.get(addr) and addr not in self.errors: + self.add_error(addr, _("A static configuration needs an address")) + name, netdev = cleaned.get("name"), cleaned.get("netdev") + if cleaned.get("itype") == "bridge": + if not cleaned.get("stp") and "stp" not in self.errors: + self.add_error("stp", _("A bridge needs STP on or off")) + if cleaned.get("delay") is None and "delay" not in self.errors: + self.add_error("delay", _("A bridge needs a forward delay")) + if netdev and netdev not in self.netdevs: + self.add_error("netdev", _("The device is not on this host")) + elif netdev and netdev == name: + self.add_error("netdev", _("A bridge cannot contain itself")) + elif cleaned.get("itype") == "ethernet" and name and name not in self.netdevs: + self.add_error("name", _("An ethernet interface configures a device of this host")) + return cleaned diff --git a/interfaces/templates/create_iface_block.html b/interfaces/templates/create_iface_block.html index aad6c07d..9837ba28 100644 --- a/interfaces/templates/create_iface_block.html +++ b/interfaces/templates/create_iface_block.html @@ -18,7 +18,7 @@