Example #1
0
    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'.")
Example #2
0
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 {}
Example #3
0
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'])