Exemplo n.º 1
0
class StaticRouteForm(MAASModelForm):
    """Static route creation/edition form."""

    source = SpecifierOrModelChoiceField(
        label="Source",
        queryset=Subnet.objects.all(),
        required=True,
        help_text="The source subnet for the route.",
    )

    destination = SpecifierOrModelChoiceField(
        label="Destination",
        queryset=Subnet.objects.all(),
        required=True,
        help_text="The destination subnet for the route.",
    )

    class Meta:
        model = StaticRoute
        fields = ("source", "destination", "gateway_ip", "metric")

    def __init__(self, *args, **kwargs):
        super(StaticRouteForm, self).__init__(*args, **kwargs)
        # Metric field is not a required field, but is required in the model.
        self.fields["metric"].required = False

    def clean(self):
        gateway_ip = self.cleaned_data.get("gateway_ip")
        source = self.cleaned_data.get("source")
        if gateway_ip:
            # This will not raise an AddrFormatErorr because it is validated at
            # the field first and if that fails the gateway_ip will be blank.
            if IPAddress(gateway_ip) not in source.get_ipnetwork():
                set_form_error(
                    self,
                    "gateway_ip",
                    "Enter an IP address in %s." % source.cidr,
                )

    def save(self):
        static_route = super().save(commit=False)
        if static_route.metric is None:
            # Set the initial value for the model.
            static_route.metric = 0
        static_route.save()
        return static_route
Exemplo n.º 2
0
class DHCPSnippetForm(MAASModelForm):
    """DHCP snippet creation/edition form."""

    name = forms.CharField(label="Name",
                           required=False,
                           help_text="The name of the DHCP snippet.")

    value = VersionedTextFileField(label="DHCP Snippet",
                                   required=False,
                                   help_text="The DHCP Snippet")

    description = forms.CharField(
        label="Description",
        required=False,
        help_text="The description of what the DHCP snippet does.",
    )

    enabled = forms.BooleanField(
        label="Enabled",
        required=False,
        help_text="Whether or not the DHCP snippet is enabled.",
    )

    node = NodeChoiceField(
        label="Node",
        queryset=Node.objects.all(),
        required=False,
        initial=None,
        help_text="The node which the DHCP snippet is for.",
    )

    subnet = SpecifierOrModelChoiceField(
        label="Subnet",
        queryset=Subnet.objects.all(),
        required=False,
        help_text="The subnet which the DHCP snippet is for.",
    )

    global_snippet = forms.BooleanField(
        label="Global DHCP Snippet",
        required=False,
        help_text=(
            "Set the DHCP snippet to be global, removes links to nodes or "
            "subnets"),
    )

    class Meta:
        model = DHCPSnippet
        fields = (
            "name",
            "value",
            "description",
            "enabled",
            "node",
            "subnet",
            "global_snippet",
        )

    def __init__(self, instance=None, request=None, **kwargs):
        super().__init__(instance=instance, **kwargs)
        if instance is None:
            for field in ["name", "value"]:
                self.fields[field].required = True
            self.initial["enabled"] = True
        else:
            self.fields["value"].initial = self.instance.value
        if instance is not None and instance.node is not None:
            self.initial["node"] = self.instance.node.system_id

    def clean(self):
        cleaned_data = super().clean()
        if cleaned_data.get("global_snippet", False):
            cleaned_data["node"] = None
            self.instance.node = None
            cleaned_data["subnet"] = None
            self.instance.subnet = None
        elif (self.instance.subnet == cleaned_data.get("subnet")
              and cleaned_data.get("node") is not None):
            cleaned_data["subnet"] = None
            self.instance.subnet = None
        elif (self.instance.node == cleaned_data.get("node")
              and cleaned_data.get("subnet") is not None):
            cleaned_data["node"] = None
            self.instance.node = None
        return cleaned_data

    def is_valid(self):
        valid = super().is_valid()
        if valid:
            # Often the first error can cause cascading errors. Showing all of
            # these errors can be confusing so only show the first if there is
            # one.
            first_error = None
            for error in validate_dhcp_config(self.instance):
                valid = False
                if first_error is None:
                    first_error = error
                else:
                    if error["line_num"] < first_error["line_num"]:
                        first_error = error
            if first_error is not None:
                set_form_error(self, "value", first_error["error"])

        # If the DHCPSnippet isn't valid cleanup the value
        if not valid and self.initial.get("value") != self.instance.value_id:
            self.instance.value.delete()
        return valid

    def save(self, endpoint, request):
        dhcp_snippet = super(DHCPSnippetForm, self).save()
        create_audit_event(
            EVENT_TYPES.SETTINGS,
            endpoint,
            request,
            None,
            description=("%s DHCP snippet '%s'." % (
                "Updated" if self.is_update else "Created",
                dhcp_snippet.name,
            )),
        )
        return dhcp_snippet
Exemplo n.º 3
0
class InterfaceLinkForm(forms.Form):
    """Interface link form."""

    subnet = SpecifierOrModelChoiceField(queryset=None, required=False)

    ip_address = forms.GenericIPAddressField(required=False)

    default_gateway = forms.BooleanField(initial=False, required=False)

    def __init__(self, *args, **kwargs):
        # Get list of allowed modes for this interface.
        allowed_modes = kwargs.pop("allowed_modes", [
            INTERFACE_LINK_TYPE.AUTO,
            INTERFACE_LINK_TYPE.DHCP,
            INTERFACE_LINK_TYPE.STATIC,
            INTERFACE_LINK_TYPE.LINK_UP,
        ])
        self.force = kwargs.pop("force", False)
        mode_choices = [
            (key, value)
            for key, value in INTERFACE_LINK_TYPE_CHOICES
            if key in allowed_modes
        ]

        self.instance = kwargs.pop("instance")
        super(InterfaceLinkForm, self).__init__(*args, **kwargs)

        # Create the mode field and setup the queryset on the subnet.
        self.fields['mode'] = CaseInsensitiveChoiceField(
            choices=mode_choices, required=True,
            error_messages={
                'invalid_choice': compose_invalid_choice_text(
                    'mode', mode_choices),
            })
        if self.instance.vlan is None or self.force is True:
            self.fields['subnet'].queryset = Subnet.objects.all()
        else:
            self.fields['subnet'].queryset = (
                self.instance.vlan.subnet_set.all())

    def clean(self):
        for interface_set in self.instance.interface_set.all():
            if isinstance(interface_set, BondInterface):
                set_form_error(
                    self, "bond",
                    ("Cannot link interface(%s) when interface is in a "
                     "bond(%s)." % (self.instance.name, interface_set.name)))
        cleaned_data = super(InterfaceLinkForm, self).clean()
        mode = cleaned_data.get("mode", None)
        if mode is None:
            return cleaned_data
        elif mode == INTERFACE_LINK_TYPE.AUTO:
            self._clean_mode_auto(cleaned_data)
        elif mode == INTERFACE_LINK_TYPE.DHCP:
            self._clean_mode_dhcp()
        elif mode == INTERFACE_LINK_TYPE.STATIC:
            self._clean_mode_static(cleaned_data)
        elif mode == INTERFACE_LINK_TYPE.LINK_UP:
            self._clean_mode_link_up()
        self._clean_default_gateway(cleaned_data)
        return cleaned_data

    def _clean_mode_auto(self, cleaned_data):
        subnet = cleaned_data.get("subnet", None)
        if subnet is None:
            set_form_error(self, "subnet", "This field is required.")

    def _clean_mode_dhcp(self):
        # Can only have one DHCP link on an interface.
        dhcp_address = get_one(
            self.instance.ip_addresses.filter(
                alloc_type=IPADDRESS_TYPE.DHCP))
        if dhcp_address is not None:
            if dhcp_address.subnet is not None:
                set_form_error(
                    self, "mode",
                    "Interface is already set to DHCP from '%s'." % (
                        dhcp_address.subnet))
            else:
                set_form_error(
                    self, "mode", "Interface is already set to DHCP.")

    def _clean_mode_static(self, cleaned_data):
        subnet = cleaned_data.get("subnet", None)
        ip_address = cleaned_data.get("ip_address", None)
        if subnet is None:
            set_form_error(self, "subnet", "This field is required.")
        elif ip_address:
            ip_address = IPAddress(ip_address)
            if ip_address not in subnet.get_ipnetwork():
                set_form_error(
                    self, "ip_address",
                    "IP address is not in the given subnet '%s'." % subnet)
            ip_range = subnet.get_dynamic_range_for_ip(ip_address)
            if ip_range is not None:
                set_form_error(
                    self, "ip_address",
                    "IP address is inside a dynamic range "
                    "%s to %s." % (ip_range.start_ip, ip_range.end_ip))

    def _clean_mode_link_up(self):
        # Cannot set LINK_UP unless no other IP address are attached to
        # this interface. But exclude STICKY addresses where the IP address is
        # null, because the user could be trying to change the subnet for a
        # LINK_UP address. And exclude DISCOVERED because what MAAS discovered
        # doesn't matter with regard to the user's intention.
        exclude_types = (
            IPADDRESS_TYPE.STICKY, IPADDRESS_TYPE.DISCOVERED
        )
        has_active_links = self.instance.ip_addresses.exclude(
            alloc_type__in=exclude_types, ip__isnull=True).count() > 0
        if has_active_links and self.force is not True:
            set_form_error(
                self, "mode",
                "Cannot configure interface to link up (with no IP address) "
                "while other links are already configured. Specify force=True "
                "to override this behavior and delete all links.")

    def _clean_default_gateway(self, cleaned_data):
        mode = cleaned_data.get("mode", None)
        subnet = cleaned_data.get("subnet", None)
        default_gateway = cleaned_data.get("default_gateway", False)
        if not default_gateway:
            return
        if mode not in GATEWAY_OPTION_MODES:
            set_form_error(
                self, "default_gateway", "Cannot use in mode '%s'." % mode)
        else:
            if subnet is None:
                set_form_error(
                    self, "default_gateway",
                    "Subnet is required when default_gateway is True.")
            elif not subnet.gateway_ip:
                set_form_error(
                    self, "default_gateway",
                    "Cannot set as default gateway because subnet "
                    "%s doesn't provide a gateway IP address." % subnet)

    def save(self):
        mode = self.cleaned_data.get("mode", None)
        subnet = self.cleaned_data.get("subnet", None)
        ip_address = self.cleaned_data.get("ip_address", None)
        default_gateway = self.cleaned_data.get("default_gateway", False)
        # If force=True, allow the user to select any subnet (this will
        # implicitly change the VLAN).
        if mode == INTERFACE_LINK_TYPE.LINK_UP or self.force is True:
            # We're either setting the LINK_UP to a new subnet, or we're
            # forcing the issue.
            self.instance.clear_all_links(clearing_config=True)
        # If the user wants to force a particular subnet to be linked, clear
        # out the VLAN so that link_subnet() will reset the interface's subnet
        # to be correct.
        should_clear_vlan = (
            self.force is True and subnet is not None and
            self.instance.vlan is not None
        )
        if should_clear_vlan:
            self.instance.vlan = None
        if not ip_address:
            ip_address = None
        link_ip = self.instance.link_subnet(
            mode, subnet, ip_address=ip_address)
        if default_gateway:
            node = self.instance.get_node()
            network = subnet.get_ipnetwork()
            if network.version == IPADDRESS_FAMILY.IPv4:
                node.gateway_link_ipv4 = link_ip
            elif network.version == IPADDRESS_FAMILY.IPv6:
                node.gateway_link_ipv6 = link_ip
            else:
                raise ValueError(
                    "Unknown subnet IP version: %s" % network.version)
            node.save()
        return Interface.objects.get(id=self.instance.id)
Exemplo n.º 4
0
class VLANForm(MAASModelForm):
    """VLAN creation/edition form."""

    # Linux doesn't allow lower than 552 for the MTU.
    mtu = forms.IntegerField(min_value=552, required=False)

    space = SpecifierOrModelChoiceField(queryset=Space.objects.all(),
                                        required=False,
                                        empty_label="")

    fabric = SpecifierOrModelChoiceField(queryset=Fabric.objects.all(),
                                         required=False,
                                         empty_label="")

    class Meta:
        model = VLAN
        fields = (
            "name",
            "description",
            "vid",
            "mtu",
            "dhcp_on",
            "primary_rack",
            "secondary_rack",
            "relay_vlan",
            "space",
            "fabric",
        )

    def __init__(self, *args, **kwargs):
        self.fabric = kwargs.pop("fabric", None)
        super().__init__(*args, **kwargs)
        instance = kwargs.get("instance")
        if instance is None and self.fabric is None:
            raise ValueError("Form requires either a instance or a fabric.")
        self._set_up_rack_fields()
        self._set_up_relay_vlan()

    def _set_up_rack_fields(self):
        qs = RackController.objects.filter_by_vids([self.instance.vid])
        self.fields["primary_rack"] = NodeChoiceField(
            required=False,
            initial=None,
            empty_label="No rack controller",
            queryset=qs,
        )
        self.fields["secondary_rack"] = NodeChoiceField(
            required=False,
            initial=None,
            empty_label="No rack controller",
            queryset=qs,
        )

        # Convert the initial values pulled from the database from id to
        # system_id so form validation doesn't complain
        primary_rack_id = self.initial.get("primary_rack")
        if primary_rack_id is not None:
            primary_rack = RackController.objects.get(id=primary_rack_id)
            self.initial["primary_rack"] = primary_rack.system_id
        secondary_rack_id = self.initial.get("secondary_rack")
        if secondary_rack_id is not None:
            secondary_rack = RackController.objects.get(id=secondary_rack_id)
            self.initial["secondary_rack"] = secondary_rack.system_id

    def _set_up_relay_vlan(self):
        # Configure the relay_vlan fields to include only VLAN's that are
        # not already on a relay_vlan. If this is an update then it cannot
        # be itself or never set when dhcp_on is True.
        possible_relay_vlans = VLAN.objects.filter(relay_vlan__isnull=True)
        if self.instance is not None:
            possible_relay_vlans = possible_relay_vlans.exclude(
                id=self.instance.id)
            if self.instance.dhcp_on:
                possible_relay_vlans = VLAN.objects.none()
                if self.instance.relay_vlan is not None:
                    possible_relay_vlans = VLAN.objects.filter(
                        id=self.instance.relay_vlan.id)
        self.fields["relay_vlan"] = forms.ModelChoiceField(
            queryset=possible_relay_vlans, required=False)

    def clean(self):
        cleaned_data = super().clean()
        # Automatically promote the secondary rack controller to the primary
        # if the primary is removed.
        if (not cleaned_data.get("primary_rack")
                and self.instance.secondary_rack is not None):
            cleaned_data["primary_rack"] = self.instance.secondary_rack
            cleaned_data["secondary_rack"] = None
        # If the primary is set to the secondary remove the secondary
        if (cleaned_data.get("primary_rack") and cleaned_data["primary_rack"]
                == self.instance.secondary_rack):
            cleaned_data["secondary_rack"] = None
        # Disallow setting the secondary to the existing primary
        if (cleaned_data.get("secondary_rack") and
                cleaned_data["secondary_rack"] == self.instance.primary_rack):
            raise ValidationError(
                "%s is already set as the primary rack controller" %
                cleaned_data["secondary_rack"].system_id)
        if (cleaned_data.get("primary_rack")
                and cleaned_data.get("secondary_rack")
                and cleaned_data.get("primary_rack")
                == cleaned_data.get("secondary_rack")):
            raise ValidationError(
                "The primary and secondary rack must be different")

        # Fix LP: #1798476 - When setting the secondary rack and the primary
        # rack was originally set (and not being changed), require the primary
        # rack to be up and running.
        primary_rack = cleaned_data.get("primary_rack")
        if (primary_rack and primary_rack == self.instance.primary_rack
                and cleaned_data.get("secondary_rack")
                and cleaned_data["secondary_rack"] !=
            (self.instance.secondary_rack)):
            # Uses the `rackd` service not `dhcpd` or `dhcpd6` because if
            # the rackd is on it will ensure those services make it to a good
            # state.
            rackd_service = Service.objects.filter(node=primary_rack,
                                                   name="rackd").first()
            if rackd_service and rackd_service.status == SERVICE_STATUS.DEAD:
                raise ValidationError(
                    "The primary rack controller must be up and running to "
                    "set a secondary rack controller. Without the primary "
                    "the secondary DHCP service will not be able to "
                    "synchronize, preventing it from responding to DHCP "
                    "requests.")

        # Only allow dhcp_on when the primary_rack is set
        if (cleaned_data.get("dhcp_on")
                and not self.cleaned_data.get("primary_rack")
                and not self.instance.primary_rack):
            raise ValidationError(
                "dhcp can only be turned on when a primary rack controller "
                "is set.")
        # XXX ltrager 2016-02-09 - Hack to get around
        # https://code.djangoproject.com/ticket/25349
        # https://github.com/django/django/pull/5658
        if (cleaned_data.get("primary_rack") is None
                and self.instance.primary_rack is not None):
            self.instance.primary_rack = None
        if (cleaned_data.get("secondary_rack") is None
                and self.instance.secondary_rack is not None):
            self.instance.secondary_rack = None
        if cleaned_data.get("space") == "" and self.instance.space is not None:
            self.instance.space = None
        return cleaned_data

    def clean_dhcp_on(self):
        dhcp_on = self.cleaned_data.get("dhcp_on")
        if not dhcp_on:
            return dhcp_on
        for subnet in self.instance.subnet_set.all():
            if subnet.get_dynamic_ranges():
                return dhcp_on
        raise ValidationError(
            "dhcp can only be turned on when a dynamic IP range is defined.")

    def save(self):
        """Persist the VLAN into the database."""
        vlan = super().save(commit=False)
        if self.fabric is not None:
            vlan.fabric = self.fabric
        if "space" in self.data and not self.cleaned_data.get("space"):
            # 'space' is being cleared.
            vlan.space = None
        if "relay_vlan" in self.data and not self.cleaned_data.get(
                "relay_vlan"):
            # 'relay_vlan' is being cleared.
            vlan.relay_vlan = None
        if vlan.dhcp_on:
            # 'relay_vlan' cannot be set when dhcp is on.
            vlan.relay_vlan = None
        vlan.save()
        return vlan
Exemplo n.º 5
0
class VLANForm(MAASModelForm):
    """VLAN creation/edition form."""

    # Linux doesn't allow lower than 552 for the MTU.
    mtu = forms.IntegerField(min_value=552, required=False)

    space = SpecifierOrModelChoiceField(
        queryset=Space.objects.all(), required=False, empty_label="")

    class Meta:
        model = VLAN
        fields = (
            'name',
            'description',
            'vid',
            'mtu',
            'dhcp_on',
            'primary_rack',
            'secondary_rack',
            'relay_vlan',
            'space',
            )

    def __init__(self, *args, **kwargs):
        self.fabric = kwargs.pop('fabric', None)
        super(VLANForm, self).__init__(*args, **kwargs)
        instance = kwargs.get('instance')
        if instance is None and self.fabric is None:
            raise ValueError("Form requires either a instance or a fabric.")
        self._set_up_rack_fields()
        self._set_up_relay_vlan()

    def _set_up_rack_fields(self):
        qs = RackController.objects.filter_by_vids([self.instance.vid])
        self.fields['primary_rack'] = NodeChoiceField(
            required=False, initial=None, empty_label='No rack controller',
            queryset=qs)
        self.fields['secondary_rack'] = NodeChoiceField(
            required=False, initial=None, empty_label='No rack controller',
            queryset=qs)

        # Convert the initial values pulled from the database from id to
        # system_id so form validation doesn't complain
        primary_rack_id = self.initial.get('primary_rack')
        if primary_rack_id is not None:
            primary_rack = RackController.objects.get(id=primary_rack_id)
            self.initial['primary_rack'] = primary_rack.system_id
        secondary_rack_id = self.initial.get('secondary_rack')
        if secondary_rack_id is not None:
            secondary_rack = RackController.objects.get(id=secondary_rack_id)
            self.initial['secondary_rack'] = secondary_rack.system_id

    def _set_up_relay_vlan(self):
        # Configure the relay_vlan fields to include only VLAN's that are
        # not already on a relay_vlan. If this is an update then it cannot
        # be itself or never set when dhcp_on is True.
        possible_relay_vlans = VLAN.objects.filter(relay_vlan__isnull=True)
        if self.instance is not None:
            possible_relay_vlans = possible_relay_vlans.exclude(
                id=self.instance.id)
            if self.instance.dhcp_on:
                possible_relay_vlans = VLAN.objects.none()
                if self.instance.relay_vlan is not None:
                    possible_relay_vlans = VLAN.objects.filter(
                        id=self.instance.relay_vlan.id)
        self.fields['relay_vlan'] = forms.ModelChoiceField(
            queryset=possible_relay_vlans, required=False)

    def clean(self):
        cleaned_data = super(VLANForm, self).clean()
        # Automatically promote the secondary rack controller to the primary
        # if the primary is removed.
        if (not cleaned_data.get('primary_rack') and
                self.instance.secondary_rack is not None):
            cleaned_data['primary_rack'] = self.instance.secondary_rack
            cleaned_data['secondary_rack'] = None
        # If the primary is set to the secondary remove the secondary
        if (cleaned_data.get('primary_rack') and
                cleaned_data['primary_rack'] == self.instance.secondary_rack):
            cleaned_data['secondary_rack'] = None
        # Disallow setting the secondary to the existing primary
        if (cleaned_data.get('secondary_rack') and
                cleaned_data['secondary_rack'] == self.instance.primary_rack):
            raise ValidationError(
                "%s is already set as the primary rack controller" %
                cleaned_data['secondary_rack'].system_id
            )
        if (cleaned_data.get('primary_rack') and
                cleaned_data.get('secondary_rack') and
                cleaned_data.get('primary_rack') ==
                cleaned_data.get('secondary_rack')):
            raise ValidationError(
                "The primary and secondary rack must be different"
            )
        # Only allow dhcp_on when the primary_rack is set
        if (cleaned_data.get('dhcp_on') and
                not self.cleaned_data.get('primary_rack') and
                not self.instance.primary_rack):
            raise ValidationError(
                "dhcp can only be turned on when a primary rack controller"
                "is set.")
        # XXX ltrager 2016-02-09 - Hack to get around
        # https://code.djangoproject.com/ticket/25349
        # https://github.com/django/django/pull/5658
        if (cleaned_data.get('primary_rack') is None and
                self.instance.primary_rack is not None):
            self.instance.primary_rack = None
        if (cleaned_data.get('secondary_rack') is None and
                self.instance.secondary_rack is not None):
            self.instance.secondary_rack = None
        if (cleaned_data.get('space') is "" and
                self.instance.space is not None):
            self.instance.space = None
        return cleaned_data

    def clean_dhcp_on(self):
        dhcp_on = self.cleaned_data.get('dhcp_on')
        if not dhcp_on:
            return dhcp_on
        for subnet in self.instance.subnet_set.all():
            if subnet.get_dynamic_ranges():
                return dhcp_on
        raise ValidationError(
            "dhcp can only be turned on when a dynamic IP range is defined.")

    def save(self):
        """Persist the VLAN into the database."""
        vlan = super(VLANForm, self).save(commit=False)
        if self.fabric is not None:
            vlan.fabric = self.fabric
        if ('space' in self.data and
                not self.cleaned_data.get('space')):
            # 'space' is being cleared.
            vlan.space = None
        if ('relay_vlan' in self.data and
                not self.cleaned_data.get('relay_vlan')):
            # 'relay_vlan' is being cleared.
            vlan.relay_vlan = None
        if vlan.dhcp_on:
            # 'relay_vlan' cannot be set when dhcp is on.
            vlan.relay_vlan = None
        vlan.save()
        return vlan
Exemplo n.º 6
0
class DHCPSnippetForm(MAASModelForm):
    """DHCP snippet creation/edition form."""

    name = forms.CharField(label="Name",
                           required=False,
                           help_text=("The name of the DHCP snippet."))

    value = VersionedTextFileField(label="DHCP Snippet",
                                   required=False,
                                   help_text="The DHCP Snippet")

    description = forms.CharField(
        label="Description",
        required=False,
        help_text=("The description of what the DHCP snippet does."))

    enabled = forms.BooleanField(
        label="Enabled",
        required=False,
        help_text=("Whether or not the DHCP snippet is enabled."))

    node = NodeChoiceField(
        label="Node",
        queryset=Node.objects.all(),
        required=False,
        initial=None,
        help_text=("The node which the DHCP snippet is for."))

    subnet = SpecifierOrModelChoiceField(
        label="Subnet",
        queryset=Subnet.objects.all(),
        required=False,
        help_text="The subnet which the DHCP snippet is for.")

    global_snippet = forms.BooleanField(
        label="Global DHCP Snippet",
        required=False,
        help_text=(
            "Set the DHCP snippet to be global, removes links to nodes or "
            "subnets"))

    class Meta:
        model = DHCPSnippet
        fields = (
            'name',
            'value',
            'description',
            'enabled',
            'node',
            'subnet',
            'global_snippet',
        )

    def __init__(self, instance=None, request=None, **kwargs):
        super().__init__(instance=instance, **kwargs)
        if instance is None:
            for field in ['name', 'value']:
                self.fields[field].required = True
            self.initial['enabled'] = True
        else:
            self.fields['value'].initial = self.instance.value
        if instance is not None and instance.node is not None:
            self.initial['node'] = self.instance.node.system_id

    def clean(self):
        cleaned_data = super().clean()
        if cleaned_data.get('global_snippet', False):
            cleaned_data['node'] = None
            self.instance.node = None
            cleaned_data['subnet'] = None
            self.instance.subnet = None
        elif (self.instance.subnet == cleaned_data.get('subnet')
              and cleaned_data.get('node') is not None):
            cleaned_data['subnet'] = None
            self.instance.subnet = None
        elif (self.instance.node == cleaned_data.get('node')
              and cleaned_data.get('subnet') is not None):
            cleaned_data['node'] = None
            self.instance.node = None
        return cleaned_data

    def is_valid(self):
        valid = super().is_valid()
        if valid:
            # Often the first error can cause cascading errors. Showing all of
            # these errors can be confusing so only show the first if there is
            # one.
            first_error = None
            for error in validate_dhcp_config(self.instance):
                valid = False
                if first_error is None:
                    first_error = error
                else:
                    if error['line_num'] < first_error['line_num']:
                        first_error = error
            if first_error is not None:
                set_form_error(self, 'value', first_error['error'])

        # If the DHCPSnippet isn't valid cleanup the value
        if not valid and self.initial.get('value') != self.instance.value_id:
            self.instance.value.delete()
        return valid