Reusable fact-derived variables

I need to set a variable that is derived from some facts, with a little logic.

For example: default_interface, as in, the name of the network interface to which the main IP address is assigned.

It can be computed by using the new fact ansible_ipv4_address and iterating over all interface facts trying to match it to their IPs:

for iface in ansible_interfaces:

somehow access ansible_${iface}

if ansible_ipv4_address == ansible_$iface.ipv4.address:
default_interface = ansible_$iface.device

This leads to three questions:

  1. How do I iterate over all interfaces’ information, or more specifically, how do I construct fact names?
    In the example above, how do I access ansible_${iface}? Obviously, this is not a valid expression
    If I can’t do that, what’s the use of the ansible_interfaces fact?

  2. Where can I perform such logic, if at all?
    In general, where can any logic be applied to facts to derive new variables?

  3. What’s the “ansible approach” towards all this?
    If I need a variable that can be computed from available facts, should I compute it, or should ansible add it as a new fact?
    In this example, new ansible_ipv4_interface and ansible_ipv6_interface (corresponding to ansible_ipv4_address and ansible_ipv6_address) facts would be trivial to add to the setup module. But is that the only way to go? How does ansible want to deal with such cases?

In the example above, how do I access ansible_${iface}? Obviously, this is not a valid expression

The template doesn't currently make Jinja2 "context" available, though you do have access to all variables through {{ hostvars["your hostname here"] }}
I won't say it's optimal, but it's there.

I really don't expect a lot of folks to be doing complex template iteration, but you can do it if you really want to. Our goal should be, of course, to minimize the levels of complex magic you have to do in templates. Nobody likes a complicated template system.

If I can't do that, what's the use of the ansible_interfaces fact?

Well, from the API, setup facts are useful for a lot more than playbooks. For instance, you could easily use an API script to build a basic inventory system, especially when combined with your own modules that also return facts.

logic to make an ansible_default_interface fact (in the core setup module) that has the value of the the default interface seems nicest to me, that way everyone could use it. It could have the same data format as ansible_eth0 (or any other interface) does.

--Michael

Cool. I will get on it. But first…
There are a few options here:

  1. I was thinking of just adding two simple top-level facts similar to ansible_ipv4/6_address: ansible_ipv4/6_interface. Though getting any other details besides the address and the interface would be a little tricky. Also, this may bring up the interface vs. alias issue again. I guess this option is out.

  2. A single ansible_eth0-like structure: ansible_default_interface. But what if the main IPv4 interface is different from the main IPv6 interface? (not sure if this happens in real life, but can’t think of why it can’t happen.)

  3. ansible_ipv4_interface and ansible_ipv6_interface as two ansible_eth0-like structures. They could be exact copies of the corresponding interface structures, with each listing both their IPv4 and IPv6 information along with mac address, device, … etc. Easy to implement, consistent with other interface facts, and confusion-resistant!

Options 2 and 3 will obsolete ansible_ipv4_address, which would be available as ansible_ipv4_interface.ipv4.address, but not ansible_ipv6_address, because ansible_ipv6_interface may have multiple addresses.

3. ansible_ipv4_interface and ansible_ipv6_interface as two
ansible_eth0-like structures. They could be exact copies of the
corresponding interface structures, with each listing both their IPv4 and
IPv6 information along with mac address, device, ... etc. Easy to implement,
consistent with other interface facts, and confusion-resistant!

yep, this would good if they were exact copies.

Options 2 and 3 will obsolete ansible_ipv4_address, which would be available
as ansible_ipv4_interface.ipv4.address, but not ansible_ipv6_address,
because ansible_ipv6_interface may have multiple addresses.

Not sure how it obsoletes a fact that isn't there now, and ipv6 stuff
is already a list. Seems fine to me.

Ah, I see my confusion, ipv4_address and ipv6_address are not
documented on the modules page, and probably introduced recently
without any advertising.

It seems fine to get rid of them with the "exact copy" idea.

This seems like a good option. One word of caution. Right now ipv4_address and ipv6_address are set to the first address discovered by parse_ip_addr(). In my case, that selected the wrong addresses as the default. One option is to consider the route table when selecting a default.

Also, would it be worth highlighting the globally-scoped ipv6 address in some way?

sf

It seems fine to get rid of them with the “exact copy” idea.

Not exactly. We can get rid of the ipv4_address as it will be available reliably as ipv4_interface.ipv4.address, but ipv6_interface may have multiple addresses, in which case it is not clear what ipv6_address is.
What I am trying to say is, in the presence of a new ipv4_interface and ipv6_interface complete structures, it is not clear what should happen to the recently implemented ipv4_address and ipv6_address: ipv4_address will be redundant, but ipv6_address will not.

This seems like a good option. One word of caution. Right now ipv4_address and ipv6_address are set to the first address discovered by parse_ip_addr(). In my case, that selected the wrong addresses as the default. One option is to consider the route table when selecting a default.

This was a possibility. Now we know it is an issue. Thanks for letting me know. I will change it to use the default route interface and address, which brings the naming issue: shall it be called ansible_ipv4_default_interface?

> This seems like a good option. One word of caution. Right now ipv4_address and ipv6_address are set to the first address discovered by parse_ip_addr(). In my case, that selected the wrong addresses as the default. One option is to consider the route table when selecting a default.

This was a possibility. Now we know it is an issue. Thanks for letting me know. I will change it to use the default route interface and address, which brings the naming issue: shall it be called ansible_ipv4_default_interface?

Yes.

It seems fine to get rid of them with the “exact copy” idea.

Not exactly. We can get rid of the ipv4_address as it will be available reliably as ipv4_interface.ipv4.address, but ipv6_interface may have multiple addresses, in which case it is not clear what ipv6_address is.
What I am trying to say is, in the presence of a new ipv4_interface and ipv6_interface complete structures, it is not clear what should happen to the recently implemented ipv4_address and ipv6_address: ipv4_address will be redundant, but ipv6_address will not.

And what do we do with the recently implemented ipv4_address and ipv6_address? Keep them? Remove them? Keep only ipv6_address?

Remove them both. They are singular in the ipv6 case and don't include the rest of the interface info in either case.

How about this structure for network interface facts:

“interfaces”: {
“eth0”: {
“macaddress”: xxxx…,
“mtu”: 1500,
“ipv4”: [
{
“address”: “192.168.1.1”,
“netmask”: “255.255.255.0”,
“network”: “192.168.1.0”,
“alias”: “eth0”
},
{
“address”: “192.168.1.2”,
“netmask”: “255.255.255.0”,
“network”: “192.168.1.0”,
“alias”: “eth0”
},
{
“address”: “192.168.1.3”,
“netmask”: “255.255.255.0”,
“network”: “192.168.1.0”,
“alias”: “eth0:0”
}
],
“ipv6”: [
{

No changes

“address”: … ,
“prefix”: … ,
“scope”: …
},
{

}
]
}, # close interfaces.eth0
“eth1”: {

},
“default_ipv4”: {
“interface”: “eth0”,
“address”: “192.168.1.3”,
“netmask”: “255.255.255.0”,
“alias”: “eth0:0”
},
“default_ipv6”: {
“interface”: “eth1”,
“address”: … ,
“prefix”: … ,
“scope”: …
}
} # close interfaces

Advantages:

1. Backwords compatible
You can still iterate over ‘ansible_interfaces’ as before to get the same result. This does not affect ‘ansible_eth0’-type facts, which can be deprecated but kept available for a couple more releases.

2. Easy to iterate over all interfaces’ information
Currently it is not straightforward to iterate over all addresses of all interfaces, because each interface has its own ansible_$iface fact. With this new structure, each interface can be explored using ansible_interfaces[$iface].

3. More natural representation of aliased interfaces
With this new structure, all IPv4 addresses assigned to an interface, whether as additional addresses for the same interface via ‘ip addr add’, or as interface aliases (eth0:0), are listed together in one list, with an alias key to be able to tell aliased interfaces apart. This structures resembles the output of the ‘ip addr’ command:

2: venet0: <BROADCAST,POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
link/void
inet 127.0.0.2/32 scope host venet0
inet <…>/32 scope global venet0:0

4. Differentiates multiple IPv4 addresses assigned to one interface from aliased interfaces
The current structure represents additional IPv4 addresses that are assigned to one interface as if they were assigned to an alias: it creates eth0:0 for the second IPv4 assigned to eth0, although there may not be a et0:0 in the underlying system. Worse, if there were an actual eth0:0, it would be named eth0:1, because eth0:0 had already been used.
The new structure lists all those addresses under the main interface, and distinguishes those assigned to an aliased interface using the ‘alias’ key.

5. Easier queries
Get all IPv4 addresses assigned to eth0:
[i[‘address’] for i in ansible_interfaces[‘eth0’][‘ipv4’]]

Get all IPv4 addresses assigned only to eth0, and not to any of its aliases:
[i[‘address’] for i in ansible_interfaces[‘eth0’][‘ipv4’] if i[‘alias’] == ‘eth0’]

6. Straightforward default interface information
With the current structure, using an exact copy of an interface tree for ‘default_ipv6’ is not enough, because that tree may have multiple addresses. Also, getting the actual IPv4 default interface is a little awkward:
ansible_default_ipv4_interface[‘ipv4’][‘device’]

Compared to:
ansible_interfaces[‘default_ipv4’][‘interface’]

7. No “device”
Avoid using the ‘device’ key which was used to map aliased interfaces to their real interfaces. ‘device’ vs. ‘interface’ can get confusing.

8. Consistent: ipv4 vs. ipv6, multi-ip vs. alias
More overall consistency. IPv4 data has the same structure as IPv6. Multiple IPv4 addresses assigned to the same interface are listed together, whether they are assigned to the interface or one of its aliases.

9. Portability to BSD
https://github.com/ansible/ansible/issues/842#issuecomment-7656871

Let me know if this structure is missing anything, or if it breaks any currently working use cases.

This discussion has gone on a bit long. We have a large list of 250+
plus people and few people actually seem interested in this, plus we
can't really be rearranging facts at all, so I'd like be finished with
it.

We have a documented structure already, and while it may not be
conceptually super-ideal to all people (nothing is), iteration over
interfaces in templates is not something that comes up a lot.

Most folks just need the ip address for a given interface, and I agree
it would be nice if they didn't have to care what the name was, which
is where ansible_ipv4_interface comes in -- if they just need to know
from dbserver A what the IP address of all the webservers are... you
can do that.

We should just add the ansible_ipv4_interface fact -- we do not need
an 'ipv4' subkey, and then this is done.

I am not willing to have a parallel structure of facts -- takes up
unneccessary space and feels redundant -- and I'm not willing to break
backwards compatibility either.

--Michael

Oh well… I felt good about it, being backwords compatible and all. I had already implemented most of it when you sent this message.

Besides, it does fix a broken scenario: when eth0 has two IPs, the current implementation will create eth0 and eth0:0 interfaces. When it then finds a real eth0:0, it will name it eth0:1.

Anyway, it’s done and ready to try if you are interested. I’ll send a pull request anyway. The ball is in your court.