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)])
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, )
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.")
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
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())
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)
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))
class MAASIPAddressFieldModel(Model): ip_address = MAASIPAddressField()
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]
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()
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
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
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)
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)