On Linux, network interface device naming has been somewhat chaotic: depending on a number of factors, eth0 today might not be eth0 tomorrow. For me, this has never been a problem in practice. At work, our physical servers have a bank of on-mainboard network ports, all managed by the same driver and so are assigned names predictably. For our virtual machines, the same is true: 99% of the time they have one network interface, but when they have more than one, they are of the same type and so are assigned predictably. At home, even on desktops and laptops with multiple network devices being added and removed at a time, I've never actually hit a problem where one was mis-identified.

However, it must have been a problem for some people because it has been solved three times now. Sadly, these incompatible solutions cause headaches in all of my use cases.

At work we have a puppet snippet that relies on knowing the interface name for a host's "primary" interface. In our case, "primary" means something like "the one the default route is over". We used to hardcode eth0 into this recipe. I don't feel too bad about that, as the puppet folks do the exact same thing in many of their scripts.

This breaks for newer physical hosts where one of the three schemes are in play and the interfaces are named/numbered em1 and counting. We worked around it by parameterizing the interface name in our recipe and defining it in our nodes.pp for the troublesome hosts, but that just moves the problem around.

My more permanent solution is to define a facter fact primary_interface that tells puppet what the correct interface should be. Here's my first attempt:

Facter.add("primary_interface") do
  setcode do
    ifs = Dir.new("/sys/class/net").each.to_a - \
      Dir.new("/sys/devices/virtual/net").each.to_a

      # filter out ones with no link
      ifs.reject! do |i|
        begin
          File.read("/sys/class/net/#{i}/carrier") != "1\n"
        rescue Errno::EINVAL
          true
        end
      end
    ifs[0]
  end
end

Of course, there is plenty of room for improvement with this solution: if ifs has more than one element at the end, a lexographic sort may prove more accurate; if there is an existing network default route up, that might be a strong clue too. It's also very Linux specific. My question though is whether this is a sensible approach in the first place?


Comments

comment 1

Your method is clever to generate a list of link active network devices. But if there are more than one then there isn't a way to know which one is primary. But it all depends upon what you need to know that information for and what you will do with it.

Perhaps just looking at what the default route is using is sufficient.

ip route show | awk '/^default/{print$NF}'

Of course if you are looking to determine the primary interface so that you can configure an appropriate default route then it is a circular dependency and something else is needed.

Comment by Bob Proulx
comment 2

Hi Bob, thanks for your comment.

In my case, situations where there are more than one link-active interface are very rare, and in all cases the "natural" order would provide the correct answer (so if I have eth0 and eth1 wired in, by convention I'm using eth0 as the "primary" interface and eth1 for something else). The persistent naming schemes all try to preserve "natural" order, even when they use different names: so em1 will always be the first on-board NIC port, before em2; likewise for systemd's forthcoming en1 notation, etc.

I think for most people the exceptions would be rare enough to special-case (i.e., don't use $primary_interface in your puppet recipes for those situations, or override it).

Indeed there is a circular issue. The default route might be over a bridge in which case the "primary interface" is something else. There may be more than one default route in the routing table (but in that situation I think "ip route show" prints them in order so the top-most is the relevant one. Sad that this data is not exposed in sysfs). You may not have a default route at all if the recipe you are trying to run is one that fixes up the networking.

I think I'll insert a lexographic sort into the mix to make it a bit more robust.

jon
comment 3

For the sake of completion, here's my latest version. Internally I distribute this as a puppet module, although it's farcically simple, just this one snippet. However I will probably put that up somewhere public shortly.

Facter.add("primary_interface") do
  setcode do
    ifs = Dir.new("/sys/class/net").each.to_a - \
      Dir.new("/sys/devices/virtual/net").each.to_a

      # filter out ones with no link
      ifs.reject! do |i|
        begin
          File.read("/sys/class/net/#{i}/carrier") != "1\n"
        rescue Errno::EINVAL
          true
        end
      end

    ifs.sort[0]
  end
end
jon