예제 #1
0
파일: mdns.py 프로젝트: zhangrb/maas
class MDNS(CleanSave, TimestampedModel):
    """Represents data gathered from mDNS-browse for a particular IP address.

    At the moment, the only MAAS-relevant data we are storing is the hostname.

    :ivar ip: IP address reported by mDNS-browse.
    :ivar hostanme: Hostname for the IP address reported by mDNS-browse.
    :ivar interface: Interface the mDNS data was observed on.
    :ivar objects: An instance of the class :class:`MDNSManager`.
    """

    class Meta(DefaultMeta):
        verbose_name = "mDNS binding"
        verbose_name_plural = "mDNS bindings"

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

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

    # Rack interface the mDNS data was observed on.
    interface = ForeignKey(
        "Interface", unique=False, blank=False, null=False, editable=False,
        on_delete=CASCADE)

    # The number of times this (hostname, IP) binding has been seen on the
    # interface.
    count = IntegerField(default=1)

    objects = MDNSManager()
class RegionControllerProcessEndpoint(CleanSave, TimestampedModel):
    """`RegionControllerProcessEndpoint` is a RPC endpoint on the
    `RegionControllerProcess` one endpoint is created per IP address on the
    `RegionControllerProcess`.

    :ivar process: `RegionControllerProcess` for this endpoint.
    :ivar address: IP address for the endpoint.
    :ivar port: Port number of the endpoint.
    """
    class Meta(DefaultMeta):
        """Needed recognize this model."""

        unique_together = ("process", "address", "port")

    process = ForeignKey(
        RegionControllerProcess,
        null=False,
        blank=False,
        related_name="endpoints",
        on_delete=CASCADE,
    )
    address = MAASIPAddressField(null=False, blank=False, editable=False)
    port = IntegerField(
        default=0, validators=[MinValueValidator(0),
                               MaxValueValidator(65535)])
예제 #3
0
class RDNS(CleanSave, TimestampedModel):
    """Represents data gathered from reverse DNS for a particular IP address.

    :ivar ip: Observed IP address.
    :ivar hostname: Most recent reverse DNS entry.
    """
    class Meta(DefaultMeta):
        verbose_name = "Reverse-DNS entry"
        verbose_name_plural = "Reverse-DNS entries"
        unique_together = ("ip", "observer")

    objects = RDNSManager()

    # IP address for the reverse-DNS entry.
    ip = MAASIPAddressField(
        unique=False,
        null=False,
        editable=False,
        blank=False,
        verbose_name="IP",
    )

    # "Primary" reverse-DNS hostname. (Since reverse DNS lookups can return
    # more than one entry, we'll need to make an educated guess as to which
    # is the "primary".) This will be coalesced with the other data in the
    # discovery view to present the default hostname for the IP.
    hostname = CharField(max_length=256,
                         editable=True,
                         null=True,
                         blank=False,
                         unique=False)

    # List of all hostnames returned by the lookup. (Useful for
    # support/debugging, in case we guess incorrectly about the "primary"
    # hostname -- and in case we ever want to show them all.)
    hostnames = JSONObjectField()

    # Region controller that observed the hostname.
    observer = ForeignKey(
        "Node",
        unique=False,
        blank=False,
        null=False,
        editable=False,
        on_delete=CASCADE,
    )
예제 #4
0
class StaticRoute(CleanSave, TimestampedModel):
    """Static route between two subnets using a gateway."""
    class Meta(DefaultMeta):
        """Needed for South to recognize this model."""

        unique_together = ("source", "destination", "gateway_ip")

    objects = StaticRouteManager()

    source = ForeignKey("Subnet",
                        blank=False,
                        null=False,
                        related_name="+",
                        on_delete=CASCADE)

    destination = ForeignKey("Subnet",
                             blank=False,
                             null=False,
                             related_name="+",
                             on_delete=CASCADE)

    gateway_ip = MAASIPAddressField(
        unique=False,
        null=False,
        blank=False,
        editable=True,
        verbose_name="Gateway IP",
    )

    metric = PositiveIntegerField(blank=False, null=False)

    def clean(self):
        if self.source_id is not None and self.destination_id is not None:
            if self.source == self.destination:
                raise ValidationError(
                    "source and destination cannot be the same subnet.")
            source_network = self.source.get_ipnetwork()
            source_version = source_network.version
            destination_version = self.destination.get_ipnetwork().version
            if source_version != destination_version:
                raise ValidationError(
                    "source and destination must be the same IP version.")
            if (self.gateway_ip is not None
                    and self.gateway_ip not in source_network):
                raise ValidationError(
                    "gateway_ip must be with in the source subnet.")
예제 #5
0
파일: neighbour.py 프로젝트: zhangrb/maas
class Neighbour(CleanSave, TimestampedModel):
    """A `Neighbour` represents an (IP, MAC) pair seen from an interface.

    :ivar ip: Observed IP address.
    :ivar mac_address: IP address the MAC was observed claiming to own.
    :ivar vid: Observed 802.1Q VLAN ID.
    :ivar count: Number of times this (IP, MAC) pair was seen on the interface.
    :ivar interface: Interface the neighbour was observed on.
    :ivar objects: An instance of the class :class:`SpaceManager`.
    """

    class Meta(DefaultMeta):
        verbose_name = "Neighbour"
        verbose_name_plural = "Neighbours"
        unique_together = (
            ("interface", "vid", "mac_address", "ip")
        )

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

    # Time the observation occurred in seconds since the epoch, as seen from
    # the rack controller.
    time = IntegerField()

    # The observed VID (802.1q VLAN ID). Note that a related VLAN interface
    # on the rack is not guaranteed to exist. Neighbours will be linked to a
    # physical, bond, or virtual bridge interface via the `interface`
    # attribute.
    vid = IntegerField(null=True, blank=True)

    # The number of times this MAC, IP mapping has been seen on the interface.
    count = IntegerField(default=1)

    # Rack interface the neighbour was observed on.
    interface = ForeignKey(
        Interface, unique=False, blank=False, null=False, editable=False,
        on_delete=CASCADE)

    # Observed MAC address.
    mac_address = MACAddressField(
        unique=False, null=True, blank=True, editable=False)

    objects = NeighbourManager()

    @property
    def mac_organization(self):
        return get_mac_organization(str(self.mac_address))

    @property
    def observer_system_id(self):
        """Returns the system_id of the rack this neighbour was observed on."""
        return self.interface.node.system_id

    @property
    def observer_hostname(self):
        """Returns the system_id of the rack this neighbour was observed on."""
        return self.interface.node.hostname

    @property
    def observer_interface_name(self):
        """Returns the interface name this neighbour was observed on."""
        return self.interface.name

    @property
    def observer_interface(self):
        return self.interface

    @property
    def observer_interface_id(self):
        return self.interface_id
예제 #6
0
class StaticIPAddress(CleanSave, TimestampedModel):
    class Meta(DefaultMeta):
        verbose_name = "Static IP Address"
        verbose_name_plural = "Static IP Addresses"
        unique_together = ('alloc_type', 'ip')

    # IP can be none when a DHCP lease has expired: in this case the entry
    # in the StaticIPAddress only materializes the connection between an
    # interface and a subnet.
    ip = MAASIPAddressField(unique=False,
                            null=True,
                            editable=False,
                            blank=True,
                            default=None,
                            verbose_name='IP')

    alloc_type = IntegerField(editable=False,
                              null=False,
                              blank=False,
                              default=IPADDRESS_TYPE.AUTO)

    # Subnet is only null for IP addresses allocate before the new networking
    # model.
    subnet = ForeignKey('Subnet',
                        editable=True,
                        blank=True,
                        null=True,
                        on_delete=CASCADE)

    user = ForeignKey(User,
                      default=None,
                      blank=True,
                      null=True,
                      editable=False,
                      on_delete=PROTECT)

    # Used only by DISCOVERED address to set the lease_time for an active
    # lease. Time is in seconds.
    lease_time = IntegerField(default=0,
                              editable=False,
                              null=False,
                              blank=False)

    # Used to mark a `StaticIPAddress` as temperary until the assignment
    # can be confirmed to be free in the subnet.
    temp_expires_on = DateTimeField(null=True,
                                    blank=True,
                                    editable=False,
                                    db_index=True)

    objects = StaticIPAddressManager()

    def __str__(self):
        # Attempt to show the symbolic alloc_type name if possible.
        type_names = map_enum_reverse(IPADDRESS_TYPE)
        strtype = type_names.get(self.alloc_type, '%s' % self.alloc_type)
        return "%s:type=%s" % (self.ip, strtype)

    @property
    def alloc_type_name(self):
        """Returns a human-readable representation of the `alloc_type`."""
        return IPADDRESS_TYPE_CHOICES_DICT.get(self.alloc_type, "")

    def get_node(self):
        """Return the Node of the first Interface connected to this IP
        address."""
        interface = self.get_interface()
        if interface is not None:
            return interface.get_node()
        else:
            return None

    def get_interface(self):
        """Return the first Interface connected to this IP address."""
        # Note that, while this relationship is modeled as a many-to-many,
        # MAAS currently only relates a single interface per IP address
        # at this time. In the future, we may want to model virtual IPs, in
        # which case this will need to change.
        interface = self.interface_set.first()
        return interface

    def get_interface_link_type(self):
        """Return the `INTERFACE_LINK_TYPE`."""
        if self.alloc_type == IPADDRESS_TYPE.AUTO:
            return INTERFACE_LINK_TYPE.AUTO
        elif self.alloc_type == IPADDRESS_TYPE.DHCP:
            return INTERFACE_LINK_TYPE.DHCP
        elif self.alloc_type == IPADDRESS_TYPE.USER_RESERVED:
            return INTERFACE_LINK_TYPE.STATIC
        elif self.alloc_type == IPADDRESS_TYPE.STICKY:
            if not self.ip:
                return INTERFACE_LINK_TYPE.LINK_UP
            else:
                return INTERFACE_LINK_TYPE.STATIC
        else:
            raise ValueError("Unknown alloc_type.")

    def get_log_name_for_alloc_type(self):
        """Return a nice log name for the `alloc_type` of the IP address."""
        return IPADDRESS_TYPE_CHOICES_DICT[self.alloc_type]

    def is_linked_to_one_unknown_interface(self):
        """Return True if the IP address is only linked to one unknown
        interface."""
        interface_types = [
            interface.type for interface in self.interface_set.all()
        ]
        return interface_types == [INTERFACE_TYPE.UNKNOWN]

    def get_related_discovered_ip(self):
        """Return the related DISCOVERED IP address for this IP address. This
        comes from looking at the DISCOVERED IP addresses assigned to the
        related interfaces.
        """
        interfaces = list(self.interface_set.all())
        discovered_ips = [
            ip for ip in StaticIPAddress.objects.filter(
                interface__in=interfaces,
                alloc_type=IPADDRESS_TYPE.DISCOVERED,
                ip__isnull=False).order_by('-id') if ip.ip
        ]
        if len(discovered_ips) > 0:
            return discovered_ips[0]
        else:
            return None

    def get_ip(self):
        """Return the IP address assigned."""
        ip, subnet = self.get_ip_and_subnet()
        return ip

    def get_ip_and_subnet(self):
        """Return the IP address and subnet assigned.

        For all alloc_types except DHCP it returns `ip` and `subnet`. When
        `alloc_type` is DHCP it returns the associated DISCOVERED `ip` and
        `subnet` on the same linked interfaces.
        """
        if self.alloc_type == IPADDRESS_TYPE.DHCP:
            discovered_ip = self.get_related_discovered_ip()
            if discovered_ip is not None:
                return discovered_ip.ip, discovered_ip.subnet
        return self.ip, self.subnet

    def deallocate(self):
        """Mark this IP address as no longer in use.
        After return, this object is no longer valid.
        """
        self.delete()

    def clean_subnet_and_ip_consistent(self):
        """Validate that the IP address is inside the subnet."""

        # USER_RESERVED addresses must have an IP address specified.
        # Blank AUTO, STICKY and DHCP addresses have a special meaning:
        # - Blank AUTO addresses mean the interface will get an IP address
        #   auto assigned when it goes to be deployed.
        # - Blank STICKY addresses mean the interface should come up and be
        #   associated with a particular Subnet, but no IP address should
        #   be assigned.
        # - DHCP IP addresses are always blank. The model will look for
        #   a DISCOVERED IP address on the same interface to map to the DHCP
        #   IP address with `get_ip()`.
        if self.alloc_type == IPADDRESS_TYPE.USER_RESERVED:
            if not self.ip:
                raise ValidationError(
                    {'ip': ["IP address must be specified."]})
        if self.alloc_type == IPADDRESS_TYPE.DHCP:
            if self.ip:
                raise ValidationError(
                    {'ip': ["IP address must not be specified."]})

        if self.ip and self.subnet and self.subnet.cidr:
            address = self.get_ipaddress()
            network = self.subnet.get_ipnetwork()
            if address not in network:
                raise ValidationError({
                    'ip': [
                        "IP address %s is not within the subnet: %s." %
                        (str(address), str(network))
                    ]
                })

    def get_ipaddress(self):
        """Returns this StaticIPAddress wrapped in an IPAddress object.

        :return: An IPAddress, (or None, if the IP address is unspecified)
        """
        if self.ip:
            return IPAddress(self.ip)
        else:
            return None

    def get_mac_addresses(self):
        """Return set of all MAC's linked to this ip."""
        return set(interface.mac_address
                   for interface in self.interface_set.all())

    def clean(self, *args, **kwargs):
        super(StaticIPAddress, self).clean(*args, **kwargs)
        self.clean_subnet_and_ip_consistent()

    def validate_unique(self, exclude=None):
        """Overrides Django's default for validating unique columns.

        Django's ORM has a misfeature: `Model.validate_unique` -- which our
        CleanSave mix-in calls -- checks every unique key against the database
        before actually saving the row. Django runs READ COMMITTED by default,
        which means there's a racey period between the uniqueness validation
        check and the actual insert.
        """
        pass

    def _set_subnet(self, subnet, interfaces=None):
        """Resets the Subnet for this StaticIPAddress, making sure to update
        the VLAN for a related Interface (if the VLAN has changed).
        """
        self.subnet = subnet
        if interfaces is not None:
            for iface in interfaces:
                if (iface is not None and subnet is not None
                        and iface.vlan_id != subnet.vlan_id):
                    iface.vlan = subnet.vlan
                    iface.save()

    def render_json(self, with_username=False, with_summary=False):
        """Render a representation of this `StaticIPAddress` object suitable
        for converting to JSON. Includes optional parameters wherever a join
        would be implied by including a specific piece of information."""
        # Circular imports.
        # XXX mpontillo 2016-03-11 we should do the formatting client side.
        from maasserver.websockets.base import dehydrate_datetime
        data = {
            "ip": self.ip,
            "alloc_type": self.alloc_type,
            "created": dehydrate_datetime(self.created),
            "updated": dehydrate_datetime(self.updated),
        }
        if with_username and self.user is not None:
            data["user"] = self.user.username
        if with_summary:
            iface = self.get_interface()
            node = self.get_node()
            if node is not None:
                data["node_summary"] = {
                    "system_id": node.system_id,
                    "node_type": node.node_type,
                    "fqdn": node.fqdn,
                    "hostname": node.hostname,
                    "is_container": node.parent_id is not None,
                }
                if iface is not None:
                    data["node_summary"]["via"] = iface.get_name()
                if (with_username
                        and self.alloc_type != IPADDRESS_TYPE.DISCOVERED):
                    # If a user owns this node, overwrite any username we found
                    # earlier. A node's owner takes precedence.
                    if node.owner and node.owner.username:
                        data["user"] = node.owner.username
            if len(self.dnsresource_set.all()) > 0:
                # This IP address is used as DNS resource.
                dns_records = [{
                    "id": resource.id,
                    "name": resource.name,
                    "domain": resource.domain.name,
                } for resource in self.dnsresource_set.all()]
                data["dns_records"] = dns_records
            if self.bmc_set.exists():
                # This IP address is used as a BMC.
                bmcs = [{
                    'id':
                    bmc.id,
                    'power_type':
                    bmc.power_type,
                    'nodes': [{
                        'system_id': node.system_id,
                        'hostname': node.hostname,
                    } for node in bmc.node_set.all()],
                } for bmc in self.bmc_set.all()]
                data["bmcs"] = bmcs
        return data

    def set_ip_address(self, ipaddr, iface=None):
        """Sets the IP address to the specified value, and also updates
        the subnet field.

        The new subnet is determined by calling get_best_subnet_for_ip() on
        the SubnetManager.

        If an interface is supplied, the Interface's VLAN is also updated
        to match the VLAN of the new Subnet.
        """
        self.ip = ipaddr

        # Cases we need to handle:
        # (0) IP address is being cleared out (remains within Subnet)
        # (1) IP address changes to another address within the same Subnet
        # (2) IP address changes to another address with a different Subnet
        # (3) IP address changes to an address within an unknown Subnet

        if not ipaddr:
            # (0) Nothing to be done. We're clearing out the IP address.
            return

        if self.ip and self.subnet:
            if self.get_ipaddress() in self.subnet.get_ipnetwork():
                # (1) Nothing to be done. Already in an appropriate Subnet.
                return
            else:
                # (2) and (3): the Subnet has changed (could be to None)
                subnet = Subnet.objects.get_best_subnet_for_ip(ipaddr)
                # We must save here, otherwise it's possible that we can't
                # traverse the interface_set many-to-many.
                self.save()
                self._set_subnet(subnet, interfaces=self.interface_set.all())
예제 #7
0
파일: iprange.py 프로젝트: tai271828/maas
class IPRange(CleanSave, TimestampedModel):
    """Represents a range of IP addresses used for a particular purpose in
    MAAS, such as a DHCP range or a range of reserved addresses."""

    objects = IPRangeManager()

    subnet = ForeignKey("Subnet",
                        editable=True,
                        blank=False,
                        null=False,
                        on_delete=CASCADE)

    type = CharField(
        max_length=20,
        editable=True,
        choices=IPRANGE_TYPE_CHOICES,
        null=False,
        blank=False,
    )

    start_ip = MAASIPAddressField(null=False,
                                  editable=True,
                                  blank=False,
                                  verbose_name="Start IP")

    end_ip = MAASIPAddressField(null=False,
                                editable=True,
                                blank=False,
                                verbose_name="End IP")

    user = ForeignKey(
        User,
        default=None,
        blank=True,
        null=True,
        editable=True,
        on_delete=PROTECT,
    )

    # In Django 1.8, CharFields with null=True, blank=True had a default
    # of '' (empty string), whereas with at least 1.11 that is None.
    # Force the former behaviour, since the documentation is not very clear
    # on what should happen.
    comment = CharField(max_length=255,
                        null=True,
                        blank=True,
                        editable=True,
                        default="")

    def __repr__(self):
        return ("IPRange(subnet_id=%r, start_ip=%r, end_ip=%r, type=%r, "
                "user_id=%r, comment=%r)") % (
                    self.subnet_id,
                    self.start_ip,
                    self.end_ip,
                    self.type,
                    self.user_id,
                    self.comment,
                )

    def __contains__(self, item):
        return item in self.netaddr_iprange

    def _raise_validation_error(self, message, fields=None):
        if fields is None:
            # By default, highlight the start_ip and the end_ip.
            fields = ["start_ip", "end_ip"]
        validation_errors = {}
        for field in fields:
            validation_errors[field] = [message]
        raise ValidationError(validation_errors)

    def clean(self):
        super().clean()
        try:
            # XXX mpontillo 2015-12-22: I would rather the Django model field
            # just give me back an IPAddress, but changing it to do this was
            # had a much larger impact than I expected.
            start_ip = IPAddress(self.start_ip)
            end_ip = IPAddress(self.end_ip)
        except AddrFormatError:
            # This validation will be called even if the start_ip or end_ip
            # field is missing. So we need to check them again here, before
            # proceeding with the validation (and potentially crashing).
            self._raise_validation_error(
                "Start IP address and end IP address are both required.")
        if end_ip.version != start_ip.version:
            self._raise_validation_error(
                "Start IP address and end IP address must be in the same "
                "address family.")
        if end_ip < start_ip:
            self._raise_validation_error(
                "End IP address must not be less than Start IP address.",
                fields=["end_ip"],
            )
        if self.subnet_id is not None:
            cidr = IPNetwork(self.subnet.cidr)
            if start_ip not in cidr and end_ip not in cidr:
                self._raise_validation_error(
                    "IP addresses must be within subnet: %s." % cidr)
            if start_ip not in cidr:
                self._raise_validation_error(
                    "Start IP address must be within subnet: %s." % cidr,
                    fields=["start_ip"],
                )
            if end_ip not in cidr:
                self._raise_validation_error(
                    "End IP address must be within subnet: %s." % cidr,
                    fields=["end_ip"],
                )
            if cidr.network == start_ip:
                self._raise_validation_error(
                    "Reserved network address cannot be included in IP range.",
                    fields=["start_ip"],
                )
            if cidr.version == 4 and cidr.broadcast == end_ip:
                self._raise_validation_error(
                    "Broadcast address cannot be included in IP range.",
                    fields=["end_ip"],
                )
        if (start_ip.version == 6 and self.type == IPRANGE_TYPE.DYNAMIC
                and netaddr.IPRange(start_ip, end_ip).size < 256):
            self._raise_validation_error(
                "IPv6 dynamic range must be at least 256 addresses in size.")
        self.clean_prevent_dupes_and_overlaps()

    @property
    def netaddr_iprange(self):
        return netaddr.IPRange(self.start_ip, self.end_ip)

    def get_MAASIPRange(self):
        purpose = self.type
        # Using '-' instead of '_' is just for consistency.
        # APIs in previous MAAS releases used '-' in range types.
        purpose = purpose.replace("_", "-")
        return make_iprange(self.start_ip, self.end_ip, purpose=purpose)

    @transactional
    def clean_prevent_dupes_and_overlaps(self):
        """Make sure the new or updated range isn't going to cause a conflict.
        If it will, raise ValidationError.
        """
        # Check against the valid types before going further, since whether
        # or not the range overlaps anything that could cause an error heavily
        # depends on its type.
        valid_types = {choice[0] for choice in IPRANGE_TYPE_CHOICES}

        # If model is incomplete, save() will fail, so don't bother checking.
        if (self.subnet_id is None or self.start_ip is None
                or self.end_ip is None or self.type is None
                or self.type not in valid_types):
            return

        # No dupe checking is required if the object hasn't been materially
        # modified.
        if not self._state.has_any_changed(["type", "start_ip", "end_ip"]):
            return

        # Reserved ranges can overlap allocated IPs but not other ranges.
        # Dynamic ranges cannot overlap anything (no ranges or IPs).
        if self.type == IPRANGE_TYPE.RESERVED:
            unused = self.subnet.get_ipranges_available_for_reserved_range(
                exclude_ip_ranges=[self])
        else:
            unused = self.subnet.get_ipranges_available_for_dynamic_range(
                exclude_ip_ranges=[self])

        if len(unused) == 0:
            self._raise_validation_error(
                "There is no room for any %s ranges on this subnet." %
                (self.type))

        message = "Requested %s range conflicts with an existing " % self.type
        if self.type == IPRANGE_TYPE.RESERVED:
            message += "range."
        else:
            message += "IP address or range."

        # Find unused range for start_ip
        for range in unused:
            if IPAddress(self.start_ip) in range:
                if IPAddress(self.end_ip) in range:
                    # Success, start and end IP are in an unused range.
                    return
                else:
                    self._raise_validation_error(message)
        self._raise_validation_error(message)
예제 #8
0
파일: discovery.py 프로젝트: tai271828/maas
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))
예제 #9
0
파일: models.py 프로젝트: tai271828/maas
class MAASIPAddressFieldModel(Model):
    ip_address = MAASIPAddressField()
예제 #10
0
파일: vlan.py 프로젝트: ocni-dtu/maas
class VLAN(CleanSave, TimestampedModel):
    """A `VLAN`.

    :ivar name: The short-human-identifiable name for this VLAN.
    :ivar vid: The VLAN ID of this VLAN.
    :ivar fabric: The `Fabric` this VLAN belongs to.
    """

    objects = VLANManager()

    class Meta(DefaultMeta):
        """Needed for South to recognize this model."""

        verbose_name = "VLAN"
        verbose_name_plural = "VLANs"
        unique_together = (("vid", "fabric"), )

    name = CharField(
        max_length=256,
        editable=True,
        null=True,
        blank=True,
        validators=[MODEL_NAME_VALIDATOR],
    )

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

    vid = IntegerField(editable=True)

    fabric = ForeignKey("Fabric",
                        blank=False,
                        editable=True,
                        on_delete=CASCADE)

    mtu = IntegerField(default=DEFAULT_MTU)

    dhcp_on = BooleanField(default=False, editable=True)

    external_dhcp = MAASIPAddressField(null=True,
                                       editable=False,
                                       blank=True,
                                       default=None)

    primary_rack = ForeignKey(
        "RackController",
        null=True,
        blank=True,
        editable=True,
        related_name="+",
        on_delete=CASCADE,
    )

    secondary_rack = ForeignKey(
        "RackController",
        null=True,
        blank=True,
        editable=True,
        related_name="+",
        on_delete=CASCADE,
    )

    relay_vlan = ForeignKey(
        "self",
        null=True,
        blank=True,
        editable=True,
        related_name="relay_vlans",
        on_delete=deletion.SET_NULL,
    )

    space = ForeignKey("Space",
                       editable=True,
                       blank=True,
                       null=True,
                       on_delete=SET_NULL)

    def __str__(self):
        return "%s.%s" % (self.fabric.get_name(), self.get_name())

    def clean_vid(self):
        if self.vid is None or self.vid < 0 or self.vid > 4094:
            raise ValidationError({"vid": ["VID must be between 0 and 4094."]})

    def clean_mtu(self):
        # Linux doesn't allow lower than 552 for the MTU.
        if self.mtu < 552 or self.mtu > 65535:
            raise ValidationError(
                {"mtu": ["MTU must be between 552 and 65535."]})

    def clean(self):
        self.clean_vid()
        self.clean_mtu()

    def is_fabric_default(self):
        """Is this the default VLAN in the fabric?"""
        return self.fabric.get_default_vlan() == self

    def get_name(self):
        """Return the name of the VLAN."""
        if self.is_fabric_default():
            return "untagged"
        elif self.name is not None:
            return self.name
        else:
            return str(self.vid)

    def manage_connected_interfaces(self):
        """Deal with connected interfaces:

        - delete all VLAN interfaces.
        - reconnect the other interfaces to the default VLAN of the fabric.
        """
        for interface in self.interface_set.all():
            if isinstance(interface, VLANInterface):
                interface.delete()
            else:
                interface.vlan = self.fabric.get_default_vlan()
                interface.save()

    def manage_connected_subnets(self):
        """Reconnect subnets the default VLAN of the fabric."""
        for subnet in self.subnet_set.all():
            subnet.vlan = self.fabric.get_default_vlan()
            subnet.save()

    def unique_error_message(self, model_class, unique_check):
        if set(unique_check) == {"vid", "fabric"}:
            return ("A VLAN with the specified VID already exists in the "
                    "destination fabric.")
        else:
            return super().unique_error_message(model_class, unique_check)

    def delete(self):
        if self.is_fabric_default():
            raise ValidationError(
                "This VLAN is the default VLAN in the fabric, "
                "it cannot be deleted.")
        self.manage_connected_interfaces()
        self.manage_connected_subnets()
        super(VLAN, self).delete()

    def save(self, *args, **kwargs):
        # Bug 1555759: Raise a Notification if there are no VLANs with DHCP
        # enabled.  Clear it when one gets enabled.
        notifications = Notification.objects.filter(
            ident="dhcp_disabled_all_vlans")
        if self.dhcp_on:
            # No longer true.  Delete the notification.
            notifications.delete()
        elif (not notifications.exists()
              and not VLAN.objects.filter(dhcp_on=True).exists()):
            Notification.objects.create_warning_for_admins(
                "DHCP is not enabled on any VLAN.  This will prevent "
                "machines from being able to PXE boot, unless an external "
                "DHCP server is being used.",
                ident="dhcp_disabled_all_vlans",
            )
        super().save(*args, **kwargs)
        # Circular dependencies.
        from maasserver.models import Fabric

        # Delete any now-empty fabrics.
        fabrics_with_vlan_count = Fabric.objects.annotate(
            vlan_count=Count("vlan"))
        fabrics_with_vlan_count.filter(vlan_count=0).delete()

    def connected_rack_controllers(self, exclude_racks=None):
        """Return list of rack controllers that are connected to this VLAN.

        :param exclude_racks: Exclude these rack controllers from the returned
            connected list.
        :returns: Returns a list of rack controllers that have a connection
            to this VLAN.
        """
        query = self.interface_set.filter(node__node_type__in=[
            NODE_TYPE.RACK_CONTROLLER,
            NODE_TYPE.REGION_AND_RACK_CONTROLLER,
        ])
        if exclude_racks is not None:
            query = query.exclude(node__in=exclude_racks)
        return [nic.node.as_rack_controller() for nic in query]
예제 #11
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',
                      default=get_default_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 = MAASIPAddressField(blank=True, editable=True, null=True)

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

    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_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.
        """
        # Note, the original implementation used .exclude() to filter,
        # but we'll filter at runtime so that prefetch_related in the
        # websocket works properly.
        ranges = set()
        for sip in self.staticipaddress_set.all():
            if sip.ip and not (ignore_discovered_ips and
                               (sip.alloc_type == IPADDRESS_TYPE.DISCOVERED)):
                ip = IPAddress(sip.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) -> 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")
                }
        ipnetwork = self.get_ipnetwork()
        if not ranges_only:
            if (self.gateway_ip is not None and self.gateway_ip != ''
                    and self.gateway_ip in ipnetwork):
                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 ipnetwork)
            for static_route in StaticRoute.objects.filter(source=self):
                ranges |= {
                    make_iprange(static_route.gateway_ip, purpose="gateway-ip")
                }
            ranges |= self._get_ranges_for_allocated_ips(
                ipnetwork, 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) -> 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()
        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.
        free_range = min(free_ranges, key=attrgetter('num_addresses'))
        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',
                '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.get_reserved_ranges()
                                    if 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.get_dynamic_ranges()
                                   if 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()
예제 #12
0
class Event(CleanSave, TimestampedModel):
    """An `Event` represents a MAAS event.

    :ivar type: The event's type.
    :ivar node: The node of the event.
    :ivar node_hostname: The hostname of the node of the event.
    :ivar user: The user responsible for this event.
    :ivar username: The username of the user responsible for this event.
    :ivar ip_address: IP address used in the request for this event.
    :ivar endpoint: Endpoint used in the request for this event.
    :ivar user_agent: User agent used in the request for this event.
    :ivar action: The action of the event.
    :ivar description: A free-form description of the event.
    """

    type = ForeignKey('EventType',
                      null=False,
                      editable=False,
                      on_delete=PROTECT)

    node = ForeignKey('Node', null=True, editable=False, on_delete=SET_NULL)

    # Set on node deletion.
    node_hostname = CharField(max_length=255,
                              default='',
                              blank=True,
                              validators=[validate_hostname])

    user = ForeignKey(User,
                      default=None,
                      blank=True,
                      null=True,
                      editable=False,
                      on_delete=SET_NULL)

    # Set on user deletion.
    username = CharField(max_length=32, blank=True, default='')

    # IP address of the request that caused this event.
    ip_address = MAASIPAddressField(unique=False,
                                    null=True,
                                    editable=False,
                                    blank=True,
                                    default=None)

    # Endpoint of request used to register the event.
    endpoint = IntegerField(choices=ENDPOINT_CHOICES,
                            editable=False,
                            default=ENDPOINT.API)

    # User agent of request used to register the event.
    user_agent = TextField(default='', blank=True, editable=False)

    action = TextField(default='', blank=True, editable=False)

    description = TextField(default='', blank=True, editable=False)

    objects = EventManager()

    class Meta(DefaultMeta):
        verbose_name = "Event record"
        index_together = (("node", "id"), )

    @property
    def endpoint_name(self):
        return ENDPOINT_CHOICES[self.endpoint][1]

    @property
    def owner(self):
        if self.username:
            return self.username
        elif self.user is not None:
            return self.user.username
        else:
            return 'unknown'

    @property
    def hostname(self):
        if self.node_hostname:
            return self.node_hostname
        elif self.node is not None:
            return self.node.hostname
        else:
            return 'unknown'

    @property
    def render_audit_description(self):
        return self.description % {'username': self.owner}

    def __str__(self):
        return "%s (node=%s, type=%s, created=%s)" % (
            self.id, self.node, self.type.name, self.created)

    def validate_unique(self, exclude=None):
        """Override validate unique so nothing is validated.

        Since `Event` is never checked for user validaton let Postgres
        handle the foreign keys instead of Django pre-checking before save.
        """
        pass
예제 #13
0
class Event(CleanSave, TimestampedModel):
    """An `Event` represents a MAAS event.

    :ivar type: The event's type.
    :ivar node: The node of the event.
    :ivar node_system_id: The system_id of the node of the event.
    :ivar node_hostname: The hostname of the node of the event.
    :ivar user_id: The user's id responsible for this event.
    :ivar username: The username of the user responsible for this event.
    :ivar ip_address: IP address used in the request for this event.
    :ivar endpoint: Endpoint used in the request for this event.
    :ivar user_agent: User agent used in the request for this event.
    :ivar action: The action of the event.
    :ivar description: A free-form description of the event.
    """

    type = ForeignKey("EventType",
                      null=False,
                      editable=False,
                      on_delete=PROTECT)

    # This gets set to None if the node gets deleted from the pre_delete signal
    node = ForeignKey("Node", null=True, editable=False, on_delete=DO_NOTHING)

    node_system_id = CharField(max_length=41,
                               blank=True,
                               null=True,
                               editable=False)

    # Set on node deletion.
    node_hostname = CharField(max_length=255,
                              default="",
                              blank=True,
                              validators=[validate_hostname])

    user_id = IntegerField(blank=True, null=True, editable=False)

    username = CharField(max_length=150, blank=True, default="")

    # IP address of the request that caused this event.
    ip_address = MAASIPAddressField(unique=False,
                                    null=True,
                                    editable=False,
                                    blank=True,
                                    default=None)

    # Endpoint of request used to register the event.
    endpoint = IntegerField(choices=ENDPOINT_CHOICES,
                            editable=False,
                            default=ENDPOINT.API)

    # User agent of request used to register the event.
    user_agent = TextField(default="", blank=True, editable=False)

    action = TextField(default="", blank=True, editable=False)

    description = TextField(default="", blank=True, editable=False)

    objects = EventManager()

    class Meta(DefaultMeta):
        verbose_name = "Event record"
        index_together = (("node", "id"), )
        indexes = [
            # Needed to get the latest event for each node on the
            # machine listing page.
            Index(fields=["node", "-created", "-id"])
        ]

    @property
    def endpoint_name(self):
        return ENDPOINT_CHOICES[self.endpoint][1]

    @property
    def owner(self):
        if self.username:
            return self.username
        else:
            return "unknown"

    @property
    def hostname(self):
        if self.node_hostname:
            return self.node_hostname
        elif self.node is not None:
            return self.node.hostname
        else:
            return "unknown"

    @property
    def render_audit_description(self):
        return self.description % {"username": self.owner}

    def __str__(self):
        return "%s (node=%s, type=%s, created=%s)" % (
            self.id,
            self.node,
            self.type.name,
            self.created,
        )

    def validate_unique(self, exclude=None):
        """Override validate unique so nothing is validated.

        Since `Event` is never checked for user validaton let Postgres
        handle the foreign keys instead of Django pre-checking before save.
        """
        pass
예제 #14
0
class VLAN(CleanSave, TimestampedModel):
    """A `VLAN`.

    :ivar name: The short-human-identifiable name for this VLAN.
    :ivar vid: The VLAN ID of this VLAN.
    :ivar fabric: The `Fabric` this VLAN belongs to.
    """

    objects = VLANManager()

    class Meta(DefaultMeta):
        """Needed for South to recognize this model."""
        verbose_name = "VLAN"
        verbose_name_plural = "VLANs"
        unique_together = (('vid', 'fabric'), )

    name = CharField(max_length=256,
                     editable=True,
                     null=True,
                     blank=True,
                     validators=[MODEL_NAME_VALIDATOR])

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

    vid = IntegerField(editable=True)

    fabric = ForeignKey('Fabric',
                        blank=False,
                        editable=True,
                        on_delete=CASCADE)

    mtu = IntegerField(default=DEFAULT_MTU)

    dhcp_on = BooleanField(default=False, editable=True)

    external_dhcp = MAASIPAddressField(null=True,
                                       editable=False,
                                       blank=True,
                                       default=None)

    primary_rack = ForeignKey('RackController',
                              null=True,
                              blank=True,
                              editable=True,
                              related_name='+',
                              on_delete=CASCADE)

    secondary_rack = ForeignKey('RackController',
                                null=True,
                                blank=True,
                                editable=True,
                                related_name='+',
                                on_delete=CASCADE)

    relay_vlan = ForeignKey('self',
                            null=True,
                            blank=True,
                            editable=True,
                            related_name='relay_vlans',
                            on_delete=deletion.SET_NULL)

    space = ForeignKey('Space',
                       editable=True,
                       blank=True,
                       null=True,
                       on_delete=SET_NULL)

    def __str__(self):
        return "%s.%s" % (self.fabric.get_name(), self.get_name())

    def clean_vid(self):
        if self.vid is None or self.vid < 0 or self.vid > 4094:
            raise ValidationError({'vid': ["VID must be between 0 and 4094."]})

    def clean_mtu(self):
        # Linux doesn't allow lower than 552 for the MTU.
        if self.mtu < 552 or self.mtu > 65535:
            raise ValidationError(
                {'mtu': ["MTU must be between 552 and 65535."]})

    def clean(self):
        self.clean_vid()
        self.clean_mtu()

    def is_fabric_default(self):
        """Is this the default VLAN in the fabric?"""
        return self.fabric.get_default_vlan() == self

    def get_name(self):
        """Return the name of the VLAN."""
        if self.is_fabric_default():
            return "untagged"
        elif self.name is not None:
            return self.name
        else:
            return str(self.vid)

    def manage_connected_interfaces(self):
        """Deal with connected interfaces:

        - delete all VLAN interfaces.
        - reconnect the other interfaces to the default VLAN of the fabric.
        """
        for interface in self.interface_set.all():
            if isinstance(interface, VLANInterface):
                interface.delete()
            else:
                interface.vlan = self.fabric.get_default_vlan()
                interface.save()

    def manage_connected_subnets(self):
        """Reconnect subnets the default VLAN of the fabric."""
        for subnet in self.subnet_set.all():
            subnet.vlan = self.fabric.get_default_vlan()
            subnet.save()

    def delete(self):
        if self.is_fabric_default():
            raise ValidationError(
                "This VLAN is the default VLAN in the fabric, "
                "it cannot be deleted.")
        self.manage_connected_interfaces()
        self.manage_connected_subnets()
        super(VLAN, self).delete()

    def save(self, *args, **kwargs):
        # Bug 1555759: Raise a Notification if there are no VLANs with DHCP
        # enabled.  Clear it when one gets enabled.
        notifications = Notification.objects.filter(
            ident="dhcp_disabled_all_vlans")
        if self.dhcp_on:
            # No longer true.  Delete the notification.
            notifications.delete()
        elif (not notifications.exists()
              and not VLAN.objects.filter(dhcp_on=True).exists()):
            Notification.objects.create_warning_for_admins(
                "DHCP is not enabled on any VLAN.  This will prevent "
                "machines from being able to PXE boot, unless an external "
                "DHCP server is being used.",
                ident="dhcp_disabled_all_vlans")
        super().save(*args, **kwargs)
예제 #15
0
class Event(CleanSave, TimestampedModel):
    """An `Event` represents a MAAS event.

    :ivar type: The event's type.
    :ivar node: The node of the event.
    :ivar node_hostname: The hostname of the node of the event.
    :ivar user: The user responsible for this event.
    :ivar username: The username of the user responsible for this event.
    :ivar ip_address: IP address used in the request for this event.
    :ivar endpoint: Endpoint used in the request for this event.
    :ivar user_agent: User agent used in the request for this event.
    :ivar action: The action of the event.
    :ivar description: A free-form description of the event.
    """

    type = ForeignKey('EventType',
                      null=False,
                      editable=False,
                      on_delete=PROTECT)

    node = ForeignKey('Node', null=True, editable=False, on_delete=SET_NULL)

    # Set on node deletion.
    node_hostname = CharField(max_length=255,
                              default='',
                              blank=True,
                              validators=[validate_hostname])

    user = ForeignKey(User,
                      default=None,
                      blank=True,
                      null=True,
                      editable=False,
                      on_delete=SET_NULL)

    # Set on user deletion.
    username = CharField(max_length=32, blank=True, default='')

    # IP address of the request that caused this event.
    ip_address = MAASIPAddressField(unique=False,
                                    null=True,
                                    editable=False,
                                    blank=True,
                                    default=None)

    # Endpoint of request used to register the event.
    endpoint = IntegerField(choices=ENDPOINT_CHOICES,
                            editable=False,
                            default=ENDPOINT.API)

    # User agent of request used to register the event.
    user_agent = TextField(default='', blank=True, editable=False)

    action = TextField(default='', blank=True, editable=False)

    description = TextField(default='', blank=True, editable=False)

    objects = EventManager()

    class Meta(DefaultMeta):
        verbose_name = "Event record"
        index_together = (("node", "id"), )

    @property
    def endpoint_name(self):
        return ENDPOINT_CHOICES[self.endpoint][1]

    def __str__(self):
        return "%s (node=%s, type=%s, created=%s)" % (
            self.id, self.node, self.type.name, self.created)