Esempio n. 1
0
class Discovery(CleanSave, ViewModel):
    """A `Discovery` object represents the combined data for a network entity
    that MAAS believes has been discovered.

    Note that this class is backed by the `maasserver_discovery` view. Any
    updates to this model must be reflected in `maasserver/dbviews.py` under
    the `maasserver_discovery` view.
    """
    class Meta(DefaultViewMeta):
        # When managed is False, Django will not create a migration for this
        # model class. This is required for model classes based on views.
        verbose_name = "Discovery"
        verbose_name_plural = "Discoveries"

    def __str__(self):
        return "<Discovery: %s at %s via %s>" % (
            self.ip,
            self.last_seen,
            self.observer_interface.get_log_string(),
        )

    discovery_id = CharField(max_length=256,
                             editable=False,
                             null=True,
                             blank=False,
                             unique=True)

    neighbour = ForeignKey(
        "Neighbour",
        unique=False,
        blank=False,
        null=False,
        editable=False,
        on_delete=DO_NOTHING,
    )

    # Observed IP address.
    ip = MAASIPAddressField(
        unique=False,
        null=True,
        editable=False,
        blank=True,
        default=None,
        verbose_name="IP",
    )

    mac_address = MACAddressField(unique=False,
                                  null=True,
                                  blank=True,
                                  editable=False)

    first_seen = DateTimeField(editable=False)

    last_seen = DateTimeField(editable=False)

    mdns = ForeignKey(
        "MDNS",
        unique=False,
        blank=True,
        null=True,
        editable=False,
        on_delete=DO_NOTHING,
    )

    # Hostname observed from mDNS-browse.
    hostname = CharField(max_length=256,
                         editable=False,
                         null=True,
                         blank=False,
                         unique=False)

    observer = ForeignKey(
        "Node",
        unique=False,
        blank=False,
        null=False,
        editable=False,
        on_delete=DO_NOTHING,
    )

    observer_system_id = CharField(max_length=41, unique=False, editable=False)

    # The hostname of the node that made the discovery.
    observer_hostname = DomainNameField(max_length=256,
                                        editable=False,
                                        null=True,
                                        blank=False,
                                        unique=False)

    # Rack interface the discovery was observed on.
    observer_interface = ForeignKey(
        "Interface",
        unique=False,
        blank=False,
        null=False,
        editable=False,
        on_delete=DO_NOTHING,
    )

    observer_interface_name = CharField(blank=False,
                                        editable=False,
                                        max_length=255)

    fabric = ForeignKey(
        "Fabric",
        unique=False,
        blank=False,
        null=False,
        editable=False,
        on_delete=DO_NOTHING,
    )

    fabric_name = CharField(max_length=256,
                            editable=False,
                            null=True,
                            blank=True,
                            unique=False)

    vlan = ForeignKey(
        "VLAN",
        unique=False,
        blank=False,
        null=False,
        editable=False,
        on_delete=DO_NOTHING,
    )

    vid = IntegerField(null=True, blank=True)

    # These will only be non-NULL if we found a related Subnet.
    subnet = ForeignKey(
        "Subnet",
        unique=False,
        blank=True,
        null=True,
        editable=False,
        on_delete=DO_NOTHING,
    )

    subnet_cidr = CIDRField(blank=True,
                            unique=False,
                            editable=False,
                            null=True)

    is_external_dhcp = NullBooleanField(blank=True,
                                        unique=False,
                                        editable=False,
                                        null=True)

    objects = DiscoveryManager()

    @property
    def mac_organization(self):
        return get_mac_organization(str(self.mac_address))
Esempio n. 2
0
class Subnet(CleanSave, TimestampedModel):
    def __init__(self, *args, **kwargs):
        assert "space" not in kwargs, "Subnets can no longer be in spaces."
        super().__init__(*args, **kwargs)

    objects = SubnetManager()

    name = CharField(
        blank=False,
        editable=True,
        max_length=255,
        validators=[SUBNET_NAME_VALIDATOR],
        help_text="Identifying name for this subnet.",
    )

    description = TextField(null=False, blank=True)

    vlan = ForeignKey("VLAN",
                      editable=True,
                      blank=False,
                      null=False,
                      on_delete=PROTECT)

    # XXX:fabric: unique constraint should be relaxed once proper support for
    # fabrics is implemented. The CIDR must be unique within a Fabric, not
    # globally unique.
    cidr = CIDRField(blank=False, unique=True, editable=True, null=False)

    rdns_mode = IntegerField(choices=RDNS_MODE_CHOICES,
                             editable=True,
                             default=RDNS_MODE.DEFAULT)

    gateway_ip = GenericIPAddressField(blank=True, editable=True, null=True)

    dns_servers = ArrayField(TextField(),
                             blank=True,
                             editable=True,
                             null=True,
                             default=list)

    allow_dns = BooleanField(editable=True,
                             blank=False,
                             null=False,
                             default=True)

    allow_proxy = BooleanField(editable=True,
                               blank=False,
                               null=False,
                               default=True)

    active_discovery = BooleanField(editable=True,
                                    blank=False,
                                    null=False,
                                    default=False)

    managed = BooleanField(editable=True,
                           blank=False,
                           null=False,
                           default=True)

    @property
    def label(self):
        """Returns a human-friendly label for this subnet."""
        cidr = str(self.cidr)
        # Note: there is a not-NULL check for the 'name' field, so this only
        # applies to unsaved objects.
        if self.name is None or self.name == "":
            return cidr
        if cidr not in self.name:
            return "%s (%s)" % (self.name, self.cidr)
        else:
            return self.name

    @property
    def space(self):
        """Backward compatibility shim to get the space for this subnet."""
        return self.vlan.space

    def get_ipnetwork(self) -> IPNetwork:
        return IPNetwork(self.cidr)

    def get_ip_version(self) -> int:
        return self.get_ipnetwork().version

    def update_cidr(self, cidr):
        cidr = str(cidr)
        # If the old name had the CIDR embedded in it, update that first.
        if self.name:
            self.name = self.name.replace(str(self.cidr), cidr)
        else:
            self.name = cidr
        self.cidr = cidr

    def __str__(self):
        return "%s:%s(vid=%s)" % (self.name, self.cidr, self.vlan.vid)

    def validate_gateway_ip(self):
        if self.gateway_ip is None or self.gateway_ip == "":
            return
        gateway_addr = IPAddress(self.gateway_ip)
        network = self.get_ipnetwork()
        if gateway_addr in network:
            # If the gateway is in the network, it is fine.
            return
        elif network.version == 6 and gateway_addr.is_link_local():
            # If this is an IPv6 network and the gateway is in the link-local
            # network (fe80::/64 -- required to be configured by the spec),
            # then it is also valid.
            return
        else:
            # The gateway is not valid for the network.
            message = "Gateway IP must be within CIDR range."
            raise ValidationError({"gateway_ip": [message]})

    def clean_fields(self, *args, **kwargs):
        # XXX mpontillo 2016-03-16: this function exists due to bug #1557767.
        # This workaround exists to prevent potential unintended consequences
        # of making the name optional.
        if (self.name is None or self.name == "") and self.cidr is not None:
            self.name = str(self.cidr)
        super().clean_fields(*args, **kwargs)

    def clean(self, *args, **kwargs):
        self.validate_gateway_ip()

    def delete(self, *args, **kwargs):
        # Check if DHCP is enabled on the VLAN this subnet is attached to.
        if self.vlan.dhcp_on and self.get_dynamic_ranges().exists():
            raise ValidationError(
                "Cannot delete a subnet that is actively servicing a dynamic "
                "IP range. (Delete the dynamic range or disable DHCP first.)")
        super().delete(*args, **kwargs)

    def get_allocated_ips(self):
        """Get all the IPs for the given subnets

        Any StaticIPAddress record that has a non-emtpy ip is considered to
        be allocated.

        It returns a generator producing a 2-tuple with the subnet and a
        list of IP tuples

        An IP tuple consist of the IP as a string and its allocation type.

        The result can be cached by calling cache_allocated_ips().
        """
        ips = getattr(self, "_cached_allocated_ips", None)
        if ips is None:
            [(_, ips)] = list(get_allocated_ips([self]))
        return ips

    def cache_allocated_ips(self, ips):
        """Cache the results of get_allocated_ips().

        This is to be used similar to how prefetching objects on
        queryset works.
        """
        self._cached_allocated_ips = ips

    def _get_ranges_for_allocated_ips(self, ipnetwork: IPNetwork,
                                      ignore_discovered_ips: bool) -> set:
        """Returns a set of MAASIPRange objects created from the set of allocated
        StaticIPAddress objects.
        """
        ranges = set()
        # We work with tuple rather than real model objects, since a
        # subnet may many IPs and creating a model object for each IP is
        # slow.
        ips = self.get_allocated_ips()
        for ip, alloc_type in ips:
            if ip and not (ignore_discovered_ips and
                           (alloc_type == IPADDRESS_TYPE.DISCOVERED)):
                ip = IPAddress(ip)
                if ip in ipnetwork:
                    ranges.add(make_iprange(ip, purpose="assigned-ip"))
        return ranges

    def get_ipranges_in_use(
        self,
        exclude_addresses: IPAddressExcludeList = None,
        ranges_only: bool = False,
        include_reserved: bool = True,
        with_neighbours: bool = False,
        ignore_discovered_ips: bool = False,
        exclude_ip_ranges: list = None,
        cached_staticroutes: list = None,
    ) -> MAASIPSet:
        """Returns a `MAASIPSet` of `MAASIPRange` objects which are currently
        in use on this `Subnet`.

        :param exclude_addresses: Additional addresses to consider "in use".
        :param ignore_discovered_ips: DISCOVERED addresses are not "in use".
        :param ranges_only: if True, filters out gateway IPs, static routes,
            DNS servers, and `exclude_addresses`.
        :param with_neighbours: If True, includes addresses learned from
            neighbour observation.
        """
        if exclude_addresses is None:
            exclude_addresses = []
        ranges = set()
        network = self.get_ipnetwork()
        if network.version == 6:
            # For most IPv6 networks, automatically reserve the range:
            #     ::1 - ::ffff:ffff
            # We expect the administrator will be using ::1 through ::ffff.
            # We plan to reserve ::1:0 through ::ffff:ffff for use by MAAS,
            # so that we can allocate addresses in the form:
            #     ::<node>:<child>
            # For now, just make sure IPv6 addresses are allocated from
            # *outside* both ranges, so that they won't conflict with addresses
            # reserved from this scheme in the future.
            first = str(IPAddress(network.first))
            first_plus_one = str(IPAddress(network.first + 1))
            second = str(IPAddress(network.first + 0xFFFFFFFF))
            if network.prefixlen == 64:
                ranges |= {
                    make_iprange(first_plus_one, second, purpose="reserved")
                }
            # Reserve the subnet router anycast address, except for /127 and
            # /128 networks. (See RFC 6164, and RFC 4291 section 2.6.1.)
            if network.prefixlen < 127:
                ranges |= {
                    make_iprange(first, first, purpose="rfc-4291-2.6.1")
                }
        if not ranges_only:
            if (self.gateway_ip is not None and self.gateway_ip != ""
                    and self.gateway_ip in network):
                ranges |= {make_iprange(self.gateway_ip, purpose="gateway-ip")}
            if self.dns_servers is not None:
                ranges |= set(
                    make_iprange(server, purpose="dns-server")
                    for server in self.dns_servers if server in network)
            if cached_staticroutes is not None:
                static_routes = [
                    static_route for static_route in cached_staticroutes
                    if static_route.source == self
                ]
            else:
                static_routes = StaticRoute.objects.filter(source=self)
            for static_route in static_routes:
                ranges |= {
                    make_iprange(static_route.gateway_ip, purpose="gateway-ip")
                }
            ranges |= self._get_ranges_for_allocated_ips(
                network, ignore_discovered_ips)
            ranges |= set(
                make_iprange(address, purpose="excluded")
                for address in exclude_addresses if address in network)
        if include_reserved:
            ranges |= self.get_reserved_maasipset(
                exclude_ip_ranges=exclude_ip_ranges)
        ranges |= self.get_dynamic_maasipset(
            exclude_ip_ranges=exclude_ip_ranges)
        if with_neighbours:
            ranges |= self.get_maasipset_for_neighbours()
        return MAASIPSet(ranges)

    def get_ipranges_available_for_reserved_range(
            self, exclude_ip_ranges: list = None):
        return self.get_ipranges_not_in_use(
            ranges_only=True, exclude_ip_ranges=exclude_ip_ranges)

    def get_ipranges_available_for_dynamic_range(self,
                                                 exclude_ip_ranges: list = None
                                                 ):
        return self.get_ipranges_not_in_use(
            ranges_only=False,
            ignore_discovered_ips=True,
            exclude_ip_ranges=exclude_ip_ranges,
        )

    def get_ipranges_not_in_use(
        self,
        exclude_addresses: IPAddressExcludeList = None,
        ranges_only: bool = False,
        ignore_discovered_ips: bool = False,
        with_neighbours: bool = False,
        exclude_ip_ranges: list = None,
    ) -> MAASIPSet:
        """Returns a `MAASIPSet` of ranges which are currently free on this
        `Subnet`.

        :param ranges_only: if True, filters out gateway IPs, static routes,
            DNS servers, and `exclude_addresses`.
        :param exclude_addresses: An iterable of addresses not to use.
        :param ignore_discovered_ips: DISCOVERED addresses are not "in use".
        :param with_neighbours: If True, includes addresses learned from
            neighbour observation.
        """
        if exclude_addresses is None:
            exclude_addresses = []
        in_use = self.get_ipranges_in_use(
            exclude_addresses=exclude_addresses,
            ranges_only=ranges_only,
            with_neighbours=with_neighbours,
            ignore_discovered_ips=ignore_discovered_ips,
            exclude_ip_ranges=exclude_ip_ranges,
        )
        if self.managed or ranges_only:
            not_in_use = in_use.get_unused_ranges(self.get_ipnetwork())
        else:
            # The end result we want is a list of unused IP addresses *within*
            # reserved ranges. To get that result, we first need the full list
            # of unused IP addresses on the subnet. This is better illustrated
            # visually below.
            #
            # Legend:
            #     X:  in-use IP addresses
            #     R:  reserved range
            #     Rx: reserved range (with allocated, in-use IP address)
            #
            #             +----+----+----+----+----+----+
            # IP address: | 1  | 2  | 3  | 4  | 5  | 6  |
            #             +----+----+----+----+----+----+
            #     Usages: | X  |    | R  | Rx |    | X  |
            #             +----+----+----+----+----+----+
            #
            # We need a set that just contains `3` in this case. To get there,
            # first calculate the set of all unused addresses on the subnet,
            # then intersect that set with set of in-use addresses *excluding*
            # the reserved range, then calculate which addresses within *that*
            # set are unused:
            #                               +----+----+----+----+----+----+
            #                   IP address: | 1  | 2  | 3  | 4  | 5  | 6  |
            #                               +----+----+----+----+----+----+
            #                       unused: |    | U  |    |    | U  |    |
            #                               +----+----+----+----+----+----+
            #             unmanaged_in_use: | u  |    |    | u  |    | u  |
            #                               +----+----+----+----+----+----+
            #                 |= unmanaged: ===============================
            #                               +----+----+----+----+----+----+
            #             unmanaged_in_use: | u  | U  |    | u  | U  | u  |
            #                               +----+----+----+----+----+----+
            #          get_unused_ranges(): ===============================
            #                               +----+----+----+----+----+----+
            #                   not_in_use: |    |    | n  |    |    |    |
            #                               +----+----+----+----+----+----+
            unused = in_use.get_unused_ranges(
                self.get_ipnetwork(), purpose=MAASIPRANGE_TYPE.UNMANAGED)
            unmanaged_in_use = self.get_ipranges_in_use(
                exclude_addresses=exclude_addresses,
                ranges_only=ranges_only,
                include_reserved=False,
                with_neighbours=with_neighbours,
                ignore_discovered_ips=ignore_discovered_ips,
                exclude_ip_ranges=exclude_ip_ranges,
            )
            unmanaged_in_use |= unused
            not_in_use = unmanaged_in_use.get_unused_ranges(
                self.get_ipnetwork(), purpose=MAASIPRANGE_TYPE.UNUSED)
        return not_in_use

    def get_maasipset_for_neighbours(self) -> MAASIPSet:
        """Return the observed neighbours in this subnet.

        :return: MAASIPSet of neighbours (with the "neighbour" purpose).
        """
        # Circular imports.
        from maasserver.models import Discovery

        # Note: we only need unknown IP addresses here, because the known
        # IP addresses should already be covered by get_ipranges_in_use().
        neighbours = Discovery.objects.filter(subnet=self).by_unknown_ip()
        neighbour_set = {
            make_iprange(neighbour.ip, purpose="neighbour")
            for neighbour in neighbours
        }
        return MAASIPSet(neighbour_set)

    def get_least_recently_seen_unknown_neighbour(self):
        """
        Returns the least recently seen unknown neighbour or this subnet.

        Useful when allocating an IP address, to safeguard against assigning
        an address another host is still using.

        :return: a `maasserver.models.Discovery` object
        """
        # Circular imports.
        from maasserver.models import Discovery

        # Note: for the purposes of this function, being in part of a "used"
        # range (such as a router IP address or reserved range) makes it
        # "known". So we need to avoid those here in order to avoid stepping
        # on network infrastructure, reserved ranges, etc.
        unused = self.get_ipranges_not_in_use(ignore_discovered_ips=True)
        least_recent_neighbours = (Discovery.objects.filter(
            subnet=self).by_unknown_ip().order_by("last_seen"))
        for neighbor in least_recent_neighbours:
            if neighbor.ip in unused:
                return neighbor
        return None

    def get_iprange_usage(self,
                          with_neighbours=False,
                          cached_staticroutes=None) -> MAASIPSet:
        """Returns both the reserved and unreserved IP ranges in this Subnet.
        (This prevents a potential race condition that could occur if an IP
        address is allocated or deallocated between calls.)

        :returns: A tuple indicating the (reserved, unreserved) ranges.
        """
        reserved_ranges = self.get_ipranges_in_use(
            cached_staticroutes=cached_staticroutes)
        if with_neighbours is True:
            reserved_ranges |= self.get_maasipset_for_neighbours()
        return reserved_ranges.get_full_range(self.get_ipnetwork())

    def get_next_ip_for_allocation(
        self,
        exclude_addresses: Optional[Iterable] = None,
        avoid_observed_neighbours: bool = True,
    ):
        """Heuristic to return the "best" address from this subnet to use next.

        :param exclude_addresses: Optional list of addresses to exclude.
        :param avoid_observed_neighbours: Optional parameter to specify if
            known observed neighbours should be avoided. This parameter is not
            intended to be specified by a caller in production code; it is used
            internally to recursively call this method if the first allocation
            attempt fails.
        """
        if exclude_addresses is None:
            exclude_addresses = []
        free_ranges = self.get_ipranges_not_in_use(
            exclude_addresses=exclude_addresses,
            with_neighbours=avoid_observed_neighbours,
        )
        if len(free_ranges) == 0 and avoid_observed_neighbours is True:
            # Try again recursively, but this time consider neighbours to be
            # "free" IP addresses. (We'll pick the least recently seen IP.)
            return self.get_next_ip_for_allocation(
                exclude_addresses, avoid_observed_neighbours=False)
        elif len(free_ranges) == 0:
            raise StaticIPAddressExhaustion(
                "No more IPs available in subnet: %s." % self.cidr)
        # The first time through this function, we aren't trying to avoid
        # observed neighbours. In fact, `free_ranges` only contains completely
        # unused ranges. So we don't need to check for the least recently seen
        # neighbour on the first pass.
        if avoid_observed_neighbours is False:
            # We tried considering neighbours as "in-use" addresses, but the
            # subnet is still full. So make an educated guess about which IP
            # address is least likely to be in-use.
            discovery = self.get_least_recently_seen_unknown_neighbour()
            if discovery is not None:
                maaslog.warning(
                    "Next IP address to allocate from '%s' has been observed "
                    "previously: %s was last claimed by %s via %s at %s." % (
                        self.label,
                        discovery.ip,
                        discovery.mac_address,
                        discovery.observer_interface.get_log_string(),
                        discovery.last_seen,
                    ))
                return str(discovery.ip)
        # The purpose of this is to that we ensure we always get an IP address
        # from the *smallest* free contiguous range. This way, larger ranges
        # can be preserved in case they need to be used for applications
        # requiring them. If two ranges have the same number of IPs, choose the
        # lowest one.
        free_range = min(free_ranges, key=attrgetter("num_addresses", "first"))
        return str(IPAddress(free_range.first))

    def render_json_for_related_ips(self,
                                    with_username=True,
                                    with_summary=True):
        """Render a representation of this subnet's related IP addresses,
        suitable for converting to JSON. Optionally exclude user and node
        information."""
        ip_addresses = self.staticipaddress_set.all()
        if with_username:
            ip_addresses = ip_addresses.prefetch_related("user")
        if with_summary:
            ip_addresses = ip_addresses.prefetch_related(
                "interface_set",
                "interface_set__node",
                "interface_set__node__domain",
                "bmc_set",
                "bmc_set__node_set",
                "dnsresource_set",
                "dnsresource_set__domain",
            )
        return sorted(
            [
                ip.render_json(with_username=with_username,
                               with_summary=with_summary)
                for ip in ip_addresses if ip.ip
            ],
            key=lambda json: IPAddress(json["ip"]),
        )

    def get_dynamic_ranges(self):
        return self.iprange_set.filter(type=IPRANGE_TYPE.DYNAMIC)

    def get_reserved_ranges(self):
        return self.iprange_set.filter(type=IPRANGE_TYPE.RESERVED)

    def is_valid_static_ip(self, *args, **kwargs):
        """Validates that the requested IP address is acceptable for allocation
        in this `Subnet` (assuming it has not already been allocated).

        Returns `True` if the IP address is acceptable, and `False` if not.

        Does not consider whether or not the IP address is already allocated,
        only whether or not it is in the proper network and range.

        :return: bool
        """
        try:
            self.validate_static_ip(*args, **kwargs)
        except MAASAPIException:
            return False
        return True

    def validate_static_ip(self, ip):
        """Validates that the requested IP address is acceptable for allocation
        in this `Subnet` (assuming it has not already been allocated).

        Raises `StaticIPAddressUnavailable` if the address is not acceptable.

        Does not consider whether or not the IP address is already allocated,
        only whether or not it is in the proper network and range.

        :raises StaticIPAddressUnavailable: If the IP address specified is not
            available for allocation.
        """
        if ip not in self.get_ipnetwork():
            raise StaticIPAddressOutOfRange(
                "%s is not within subnet CIDR: %s" % (ip, self.cidr))
        for iprange in self.get_reserved_maasipset():
            if ip in iprange:
                raise StaticIPAddressUnavailable(
                    "%s is within the reserved range from %s to %s" %
                    (ip, IPAddress(iprange.first), IPAddress(iprange.last)))
        for iprange in self.get_dynamic_maasipset():
            if ip in iprange:
                raise StaticIPAddressUnavailable(
                    "%s is within the dynamic range from %s to %s" %
                    (ip, IPAddress(iprange.first), IPAddress(iprange.last)))

    def get_reserved_maasipset(self, exclude_ip_ranges: list = None):
        if exclude_ip_ranges is None:
            exclude_ip_ranges = []
        reserved_ranges = MAASIPSet(iprange.get_MAASIPRange()
                                    for iprange in self.iprange_set.all()
                                    if iprange.type == IPRANGE_TYPE.RESERVED
                                    and iprange not in exclude_ip_ranges)
        return reserved_ranges

    def get_dynamic_maasipset(self, exclude_ip_ranges: list = None):
        if exclude_ip_ranges is None:
            exclude_ip_ranges = []
        dynamic_ranges = MAASIPSet(iprange.get_MAASIPRange()
                                   for iprange in self.iprange_set.all()
                                   if iprange.type == IPRANGE_TYPE.DYNAMIC
                                   and iprange not in exclude_ip_ranges)
        return dynamic_ranges

    def get_dynamic_range_for_ip(self, ip):
        """Return `IPRange` for the provided `ip`."""
        # XXX mpontillo 2016-01-21: for some reason this query doesn't work.
        # I tried it both like this, and with:
        #     start_ip__gte=ip, and end_ip__lte=ip
        # return get_one(self.get_dynamic_ranges().extra(
        #        where=["start_ip >= inet '%s'" % ip,
        # ... which sounds a lot like comment 15 in:
        #     https://code.djangoproject.com/ticket/11442
        for iprange in self.get_dynamic_ranges():
            if ip in iprange.netaddr_iprange:
                return iprange
        return None

    def get_smallest_enclosing_sane_subnet(self):
        """Return the subnet that includes this subnet.

        It must also be at least big enough to be a parent in the RFC2317
        world (/24 in IPv4, /124 in IPv6).

        If no such subnet exists, return None.
        """
        find_rfc2137_parent_query = """
            SELECT * FROM maasserver_subnet
            WHERE
                %s << cidr AND (
                    (family(cidr) = 6 and masklen(cidr) <= 124) OR
                    (family(cidr) = 4 and masklen(cidr) <= 24))
            ORDER BY
                masklen(cidr) DESC
            LIMIT 1
            """
        for s in Subnet.objects.raw(find_rfc2137_parent_query, (self.cidr, )):
            return s
        return None

    def update_allocation_notification(self):
        # Workaround for edge cases in Django. (See bug #1702527.)
        if self.id is None:
            return
        ident = "ip_exhaustion__subnet_%d" % self.id
        # Circular imports.
        from maasserver.models import Config, Notification

        threshold = Config.objects.get_config(
            "subnet_ip_exhaustion_threshold_count")
        notification = Notification.objects.filter(ident=ident).first()
        delete_notification = False
        if threshold > 0:
            full_iprange = self.get_iprange_usage()
            statistics = IPRangeStatistics(full_iprange)
            # Check if there are less available IPs in the subnet than the
            # warning threshold.
            meets_warning_threshold = statistics.num_available <= threshold
            # Check if the warning threshold is appropriate relative to the
            # size of the subnet. It's pointless to warn about address
            # exhaustion on a /30, for example: the admin already knows it's
            # small, so we would just be annoying them.
            subnet_is_reasonably_large_relative_to_threshold = (
                threshold * 3 <= statistics.total_addresses)
            if (meets_warning_threshold
                    and subnet_is_reasonably_large_relative_to_threshold):
                notification_text = (
                    "IP address exhaustion imminent on subnet: %s. "
                    "There are %d free addresses out of %d "
                    "(%s used).") % (
                        self.label,
                        statistics.num_available,
                        statistics.total_addresses,
                        statistics.usage_percentage_string,
                    )
                if notification is None:
                    Notification.objects.create_warning_for_admins(
                        notification_text, ident=ident)
                else:
                    # Note: This will update the notification, but will not
                    # bring it back for those who have dismissed it. Maybe we
                    # should consider creating a new notification if the
                    # situation is now more severe, such as raise it to an
                    # error if it's half remaining threshold.
                    notification.message = notification_text
                    notification.save()
            else:
                delete_notification = True
        else:
            delete_notification = True
        if notification is not None and delete_notification:
            notification.delete()
Esempio n. 3
0
class CIDRTestModel(Model):
    cidr = CIDRField()