def update_dynamic_hostname(self, sip, hostname): """Creates or updates the DHCP hostname for a StaticIPAddress. The hostname will be coerced into a valid hostname before being saved, since DHCP clients may report hostnames with embedded spaces, etc. :param sip: a StaticIPAddress of alloc_type DISCOVERED :param hostname: the hostname provided by the DHCP client. """ assert sip.ip is not None assert sip.alloc_type == IPADDRESS_TYPE.DISCOVERED hostname = coerce_to_valid_hostname(hostname) self.release_dynamic_hostname(sip, but_not_for=hostname) dnsrr, created = self.get_or_create(name=hostname) if created: dnsrr.ip_addresses.add(sip) log.msg("Added dynamic hostname '%s' for IP address '%s'." % (dnsrr.fqdn, sip.ip)) else: if dnsrr.has_static_ip(): log.msg("Skipped adding dynamic hostname '%s' for IP address " "'%s': already exists in DNS with a static IP." % (dnsrr.fqdn, sip.ip)) else: if sip in dnsrr.ip_addresses.all(): return dnsrr.ip_addresses.add(sip) log.msg(f"Updated dynamic hostname '{dnsrr.fqdn}'." f" Added IP address 'sip.ip'.")
def update_lease( action, mac, ip_family, ip, timestamp, lease_time=None, hostname=None ): """Update one DHCP leases from a cluster. :param action: DHCP action taken on the cluster as found in :py:class`~provisioningserver.rpc.region.UpdateLease`. :param mac: MAC address for the action taken on the cluster as found in :py:class`~provisioningserver.rpc.region.UpdateLease`. :param ip_family: IP address family for the action taken on the cluster as found in :py:class`~provisioningserver.rpc.region.UpdateLease`. :param ip: IP address for the action taken on the cluster as found in :py:class`~provisioningserver.rpc.region.UpdateLease`. :param timestamp: Epoch time for the action taken on the cluster as found in :py:class`~provisioningserver.rpc.region.UpdateLease`. :param lease_time: Number of seconds the lease is active on the cluster as found in :py:class`~provisioningserver.rpc.region.UpdateLease`. :param hostname: Hostname of the machine for the lease on the cluster as found in :py:class`~provisioningserver.rpc.region.UpdateLease`. Based on the action a DISCOVERED StaticIPAddress will be either created or updated for a Interface that matches `mac`. Actions: commit - When a new lease is given to a client. `lease_time` is required for this action. `hostname` is optional. expiry - When a lease has expired. Occurs when a client fails to renew their lease before the end of the `lease_time`. release - When a client explicitly releases the lease. :raises NoSuchCluster: If the cluster identified by `cluster_uuid` does not exist. """ # Check for a valid action. if action not in ["commit", "expiry", "release"]: raise LeaseUpdateError("Unknown lease action: %s" % action) # Get the subnet for this IP address. If no subnet exists then something # is wrong as we should not be recieving message about unknown subnets. subnet = Subnet.objects.get_best_subnet_for_ip(ip) if subnet is None: raise LeaseUpdateError("No subnet exists for: %s" % ip) # Check that the subnet family is the same. subnet_family = subnet.get_ipnetwork().version if ip_family == "ipv4" and subnet_family != IPADDRESS_FAMILY.IPv4: raise LeaseUpdateError( "Family for the subnet does not match. Expected: %s" % ip_family ) elif ip_family == "ipv6" and subnet_family != IPADDRESS_FAMILY.IPv6: raise LeaseUpdateError( "Family for the subnet does not match. Expected: %s" % ip_family ) created = datetime.fromtimestamp(timestamp) log.msg( "Lease update: %s for %s on %s at %s%s%s" % ( action, ip, mac, created, " (lease time: %ss)" % lease_time if lease_time is not None else "", " (hostname: %s)" % hostname if _is_valid_hostname(hostname) else "", ) ) # We will recieve actions on all addresses in the subnet. We only want # to update the addresses in the dynamic range. dynamic_range = subnet.get_dynamic_range_for_ip(IPAddress(ip)) if dynamic_range is None: # Do nothing. return {} interfaces = list(Interface.objects.filter(mac_address=mac)) if len(interfaces) == 0 and action == "commit": # A MAC address that is unknown to MAAS was given an IP address. Create # an unknown interface for this lease. unknown_interface = UnknownInterface( name="eth0", mac_address=mac, vlan_id=subnet.vlan_id ) unknown_interface.save() interfaces = [unknown_interface] elif len(interfaces) == 0: # No interfaces and not commit action so nothing needs to be done. return {} sip = None # Delete all discovered IP addresses attached to all interfaces of the same # IP address family. old_family_addresses = StaticIPAddress.objects.filter_by_ip_family( subnet_family ) old_family_addresses = old_family_addresses.filter( alloc_type=IPADDRESS_TYPE.DISCOVERED, interface__in=interfaces ) for address in old_family_addresses: # Release old DHCP hostnames, but only for obsolete dynamic addresses. if address.ip != ip: if address.ip is not None: DNSResource.objects.release_dynamic_hostname(address) address.delete() else: # Avoid recreating a new StaticIPAddress later. sip = address # Create the new StaticIPAddress object based on the action. if action == "commit": # Interfaces received a new lease. Create the new object with the # updated lease information. # Hostname sent from the cluster is either blank or can be "(none)". In # either of those cases we do not set the hostname. sip_hostname = None if _is_valid_hostname(hostname): sip_hostname = hostname # Use the timestamp from the lease to create the StaticIPAddress # object. That will make sure that the lease_time is correct from # the created time. sip, _ = StaticIPAddress.objects.update_or_create( defaults=dict( subnet=subnet, lease_time=lease_time, created=created, updated=created, ), alloc_type=IPADDRESS_TYPE.DISCOVERED, ip=ip, ) for interface in interfaces: interface.ip_addresses.add(sip) if sip_hostname is not None: # MAAS automatically manages DNS for node hostnames, so we cannot # allow a DHCP client to override that. hostname_belongs_to_a_node = Node.objects.filter( hostname=coerce_to_valid_hostname(sip_hostname) ).exists() if hostname_belongs_to_a_node: # Ensure we don't allow a DHCP hostname to override a node # hostname. DNSResource.objects.release_dynamic_hostname(sip) else: DNSResource.objects.update_dynamic_hostname(sip, sip_hostname) elif action == "expiry" or action == "release": # Interfaces no longer holds an active lease. Create the new object # to show that it used to be connected to this subnet. if sip is None: # XXX: There shouldn't be more than one StaticIPAddress # record here, but it can happen be due to bug 1817305. sip = StaticIPAddress.objects.filter( alloc_type=IPADDRESS_TYPE.DISCOVERED, ip=None, subnet=subnet, interface__in=interfaces, ).first() if sip is None: sip = StaticIPAddress.objects.create( alloc_type=IPADDRESS_TYPE.DISCOVERED, ip=None, subnet=subnet, ) else: sip.ip = None sip.save() for interface in interfaces: interface.ip_addresses.add(sip) return {}
def create_node(macs, arch, power_type, power_parameters, domain=None, hostname=None): """Create a Node on the region and return its system_id. :param macs: A list of MAC addresses belonging to the node. :param arch: The node's architecture, in the form 'arch/subarch'. :param power_type: The node's power type as a string. :param power_parameters: The power parameters for the node, as a dict. :param domain: The domain the node should join. """ if hostname is not None: hostname = coerce_to_valid_hostname(hostname) for elapsed, remaining, wait in retries(15, 5, reactor): try: client = getRegionClient() break except NoConnectionsAvailable: yield pause(wait, reactor) else: maaslog.error("Can't create node, no RPC connection to region.") return # De-dupe the MAC addresses we pass. We sort here to avoid test # failures. macs = sorted(set(macs)) try: response = yield client(CreateNode, architecture=arch, power_type=power_type, power_parameters=json.dumps(power_parameters), mac_addresses=macs, hostname=hostname, domain=domain) except NodeAlreadyExists: # The node already exists on the region, so we log the error and # give up. maaslog.error( "A node with one of the mac addresses in %s already exists.", macs) returnValue(None) except UnhandledCommand: # The region hasn't been upgraded to support this method # yet, so give up. maaslog.error("Unable to create node on region: Region does not " "support the CreateNode RPC method.") returnValue(None) except UnknownRemoteError as e: # This happens, for example, if a ValidationError occurs on the region. # (In particular, we see this if the hostname is a duplicate.) # We should probably create specific exceptions for these, so we can # act on them appropriately. maaslog.error( "Unknown error while creating node %s: %s (see regiond.log)", macs, e.description) returnValue(None) else: returnValue(response['system_id'])