When is a Fact not a Fact?

When it's sorta wrong. That's when.

The :ipaddress fact is one of a bunch of core facts that Facter provides by default. The Facter documentation states that, for a Windows host, this fact is populated according to the following:

  • On Windows, it attempts to use the socket library and resolve the machine’s hostname via DNS.
  • On LDAP based hosts it tries to use either the win32/resolv library to resolve the hostname to an IP address, or on Unix, it uses the resolv library.
  • As a fall back for undefined systems, it tries to run the “host” command to resolve the machine’s hostname using the system DNS.

However, on hosts that are part of a Windows cluster (in Azure at least, I can't speak for the physical world), where an IP address resource is in use, the Cluster Resource's IP address bubbles up to the top of the list if the node happens to be the current owner. This may or may not be desirable depending on how you make use of this fact, but nonetheless the behaviour is inconsistent in my opinion.

So, how can you replace or override a core fact (or indeed any fact that may be defined for a given node) where it's appropriate, and make sure that it's left alone where it's not? The key here is in the fact resolution and its weight. Again, from the Facter docs:

A fact is a piece of information about a given node, while a resolution is a way of obtaining that information from the system. That means that every fact needs to have at least one resolution, and facts that can run on different operating systems may need to have different resolutions for each one.

In the event that more than one resolution exists for a given fact on a given node, Facter will rank those resolutions based on their weight - and the resolution with the highest weight wins. By default the weight is calculated as the number of confine statements that enclose the fact, but this can also be manually overridden using the has_weight property. The latter is arguably less elegant in that it has no inherent logic - another resolution could easily replace a fact's value simply by using a higher has_weight value. Likewise, another resolution that could be more specific (and correct) may end up not being used because it doesn't carry enough weight.

Using our Windows Cluster scenario as an example, let's see one solution for how we can ensure that the :ipaddress core fact is always that of the host and not that of the Windows Cluster IP address resource.

First of all, let's see how we might re-define the :ipaddress fact to hold the correct value for a cluster node. The Facter exec could be something like this (linebreaks added for readability):

Facter::Core::Execution.exec('powershell "(Get-NetIPAddress | 
  where {($_.AddressFamily -eq \"IPv4\") 
  -and ($_.SkipAsSource -eq $false) 
  -and ($_.InterfaceAlias.StartsWith(\"Ethernet\"))}).IPAddress"')

The key part in the above is the SkipAsSource property which controls whether or not an IP address is used for initiating outbound connections. The pattern for matching the interface name is something that will need to be tweaked depending on your environment (in Azure, the NICs are always named 'Ethernet...'

Now, we could enclose the above in a new Fact Resolution contained within a module in our Puppet environment. We could even confine it to make sure it only targets Windows operating systems, like this:

Facter.add(:ipaddress) do
  confine :kernel => 'windows'
  setcode do
    <...the code you saw above...>
  end
end

However, if we put this into our Puppet environment, it would have no effect. Why not? Remember that Facter takes into account the weight of a Fact when deciding which resolution to use - so ours is clearly not winning the fight here. As mentioned above, we could do one of two things: manually increase the weight to some arbitrary number that would ensure our resolution always wins, or make the fact more targeted by using multiple confines. Let's do the second one. Let's define a new fact that states whether or not a system has the Windows Clustering feature installed:

Facter.add(:ismsclusternode) do
  confine :kernel => 'windows'

  sysnativedir = Facter.value(:system32)
  setcode do
    if Facter::Util::Resolution.exec(sysnativedir +
        '\WindowsPowerShell\v1.0\powershell.exe "(get-windowsfeature 
        -Name Failover-Clustering).Installed"') == "True"
      true
    else
      false
    end
  end
end

We could make the above Fact even more clever by checking whether or not a cluster has an IP address resource, but this will do for now. Now we have an additional piece of information that we can use to increase the weight of our new resolution for :ipaddress - we can confine it to MSCS nodes by using the new :ismsclusternode fact we just created.

Facter.add(:ipaddress) do
  confine :kernel => 'windows'
  confine :ismsclusternode => true
  setcode do
    <...the code you saw above...>
  end
end

Now that the fact has two confines, it has more weight. And, as luck would have it, this is enough weight for our resolution to take precedence over the default resolution for this fact. Hey presto, the :ipaddress fact is correct again!