Salt: Grain Request: Primary IP Address

Created on 8 Oct 2015  路  24Comments  路  Source: saltstack/salt

Hi,

We seem to have a great need to find out the primary IP address of our servers from grains. This is all well and good when you know your primary network device is going to be called eth0, however with the new naming scheme in RHEL/CentOS 7 and some other systemd-based distributions, eth0 is no longer guaranteed to exist. I've not been able to find out a reliable way of getting this information from grains.

I was initially looking at the ipv4 grain for this, but looking at the source for that grain it seems it simply runs a sort on the IP addresses, so there is no guarantee that the IP you pick is going to be the primary one (e.g. index 1 if loopback is index 0).

I've ended up writing a custom grain for this, but it's really something that would be useful in core as grains['ip_interfaces']['eth0'][0] isn't reliably going to work on newer distros.

The custom grain I'm using is here: https://gist.github.com/Xiol/f8a27b24af936293c862. It probably needs a lot of love and less shelling out, and is very Linux specific.

Thanks

Feature Grains team-core

Most helpful comment

This feature available in every mature automation system, it should be in Salt. I fully agree with @ChrisLundquist here, even if it's not 100% accurate it's a good starting point for many deployments.

All 24 comments

@Xiol, it all depends on your definition of what constitutes a 'primary' IP address.

We have TONS of different network setups and different interface names, almost none of which would have the 'primary' ip address of eth0 (think bonding configurations, VLANs, Privacy Extensions, secondary IP's which are the real production IP's etc.).

Look at the network module (https://docs.saltstack.com/en/latest/ref/modules/all/salt.modules.network.html#module-salt.modules.network); there's a ton of usefull stuff in there that could deliver your idea of the 'primary' IP address.

Another idea is to go for the grains fqdn_ip4 and fqdn_ip6; it resolves to the IPs designated to the servers FQDN, which is usually the one you're looking for.

Yeah, I had considered that the meaning of primary IP would be specific to an organisation or use case. For us it's simply the first IP on the first interface which in almost all cases is also the IP we'd use for any management, essentially it'll be the IP on the interface with the default route.

Chef does have a similiar thing to what I'm asking for, as their ipaddress value in Ohai (their pillar, essentially) is defined as:

The IP address for a node. If the node has a default route, this is the IPV4 address for the interface. If the node does not have a default route, the value for this attribute should be nil. The IP address for default route is the recommended default value.

Puppet also has an ipaddress fact, along with Ansible, which has this information available in ansible_default_ipv4.address which is what we used to use when we used Ansible (source).

I had considered the network module, but I didn't see anything in there that would easily return this information.

network.default_route will deliver your default gateway.

network.get_route <gateway> will deliver the outbound ip.

So you'd end up with something like

res = {}
for family in ('inet', 'inet6')
    gw = __salt__['network.default_route'](family)[0]['gateway']
    out = __salt__['network.get_route'](gw)['source']
    res[family + '_default_ip'] = out

return res

Of course grains don't have access to the modules, so you'd have to initialize __salt__ manually (see the grains for examples of this).

Another idea is to look at salt.utils; there's a ton of useful network stuff in there as well for these kinds of purposes; for the most part network module wraps around these utils

I like the idea of the default route.... However lets assume salt master is in the management network, therefore the IP address used by the minion to talk to the master could also be consider 'Management IP' you could have a gain with the minion_address_ipv4 minion_address_ipv6.

@damon-atkins This would work for us, however we exclusively use the SSH transport, so that would also have to be taken into consideration ;)

@damon-atkins the 'default route' idea is probably not such a bad one, but keep in mind that for example with Privacy Extensions, the outgoing IP (e.g. the 'default route') would by definition not be what the OP considers the 'Primary IP'.
There are other examples like that; what if you for example have multihomed systems with multiple 'default' gateways dependent on outgoing hardware interface etc., not even to mention multiple route tables, dynamic injections, firewall-based modifications etc.

in summary, I think one of SaltStack's main strengths is that it doesn't try to derive these kinds of things for you, but rather grants you all the tools do so very easily for yourself depending on your specific conditions.
I'd vote to keep it that way. I'd rather use my own definition of 'Primary IP' than trip over someone else's and end up having to define 'our_actual_primary_ip' regardless.

SSHD exports variable SSH_CONNECTION='Src SrcPort Dest DestPort'
However if you are using ssh it should know the IP address for the minion. Assume master starts the ssh session. I agree it would be handy information to have so monitoring and other management tools could re-use the information.

Nuance belies much of this topic and concept more significantly and formatively than others. I am persuaded by the desire to find and assist a convention within salt as the other tools @Xiol has mentioned, but @The-Loeki describes very good reasons this often is not or cannot be conventional.

This really feels like a "batteries not included" answer for the common case.

Yes, it won't be the right IP for everyone, in all cases.
It _does_ seem like it would work for _most_ cases.

E.G. If I wanted to have nginx, memcached, or any other service bind on IP address other than 127.0.0.1 or 0.0.0.0, picking the src ip for the default route would _usually_ work.

Shouldn't we offer an experience that makes it easy to get started?

Naturally, if this grain wasn't correct for advanced setups like routers, or complex docker hosts, the user could implement their own grain or custom logic.

It seems like this feature doesn't make the hard things any harder, only keeps the easy things easy.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

If this issue is closed prematurely, please leave a comment and we will gladly reopen the issue.

This feature available in every mature automation system, it should be in Salt. I fully agree with @ChrisLundquist here, even if it's not 100% accurate it's a good starting point for many deployments.

Thanks for following up here everyone. I will re-open!

@Ch3LL Looping you in here - can you follow up on this issue when you get a moment? Just so someone currently at Salt can re-triage this as needed.

Thank you for updating this issue. It is no longer marked as stale.

Thank you for updating this issue. It is no longer marked as stale.

You need to thumbs up the original opening comment so it is sorted higher.
Issues Sorted by Top Thumbs Up

@rallytime i think we should be open to adding this and keep it as a feature request. does anyone want to give it a go?

I like this idea.

I appreciate how difficult it would be to support all user cases since networks can be very different.

I'm interested to get the IP address that the minion actually use to access the salt server.

Our current workaround:

network/default-ip4.sls

{% macro interface() -%}
{% for route in salt['network.routes']() -%}
  {%- if route['destination'] == '0.0.0.0' -%}
    {{ route['interface'] }}
  {%- endif %}
{%- endfor %}
{%- endmacro %}

Use it like:

{%- import "network/default-ip4.sls" as default_ip4 -%}
{%- set ip = salt['grains.get']('ip4_interfaces:' ~ default_ip4.interface(), {})[0] %}

EDIT: I found a solution specific to my problem, but maybe it will help someone else, too. Tested on Debian Linux with only one IPv4 address configured:

{% for ip in grains['ipv4'] %}{% if grains['ipv4'] != '127.0.0.1' %}{{ ip }}{% endif %}{% endfor %}`

_Original comment:_
@morph027 Thanks for your suggestion. I tried it on a fresh Debian 9 stretch amd64. But I receive the following error:

2019-02-06 14:48:32,679 [salt.utils.templates:181 ][ERROR   ][14358] Rendering exception occurred
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/salt/utils/templates.py", line 393, in render_jinja_tmpl
    output = template.render(**decoded_context)
  File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 1008, in render
    return self.environment.handle_exception(exc_info, True)
  File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 780, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python3/dist-packages/jinja2/_compat.py", line 37, in reraise
    raise value.with_traceback(tb)
  File "<template>", line 10, in top-level template code
  File "/srv/pillar/network/default-ip4.sls", line 4, in template
    {% for route in salt['network.routes']() -%}
  File "/usr/lib/python3/dist-packages/salt/modules/network.py", line 1619, in routes
    routes_ = _ip_route_linux()
  File "/usr/lib/python3/dist-packages/salt/modules/network.py", line 518, in _ip_route_linux
    address_mask = convert_cidr(comps[0])
  File "/usr/lib/python3/dist-packages/salt/modules/network.py", line 1143, in convert_cidr
    network_info = salt.ext.ipaddress.ip_network(cidr)
AttributeError: module 'salt.ext' has no attribute 'ipaddress'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/salt/utils/templates.py", line 170, in render_tmpl
    output = render_str(tmplstr, context, tmplpath)
  File "/usr/lib/python3/dist-packages/salt/utils/templates.py", line 442, in render_jinja_tmpl
    trace=tracestr)
salt.exceptions.SaltRenderError: Jinja error: module 'salt.ext' has no attribute 'ipaddress'
/srv/pillar/network/default-ip4.sls(4):
---
# https://github.com/saltstack/salt/issues/27790#issuecomment-442754969

{% macro interface() -%}
{% for route in salt['network.routes']() -%}    <======================
  {%- if route['destination'] == '0.0.0.0' -%}
    {{ route['interface'] }}
  {%- endif %}
{%- endfor %}
{%- endmacro %}
[...]
---
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/salt/utils/templates.py", line 393, in render_jinja_tmpl
    output = template.render(**decoded_context)
  File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 1008, in render
    return self.environment.handle_exception(exc_info, True)
  File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 780, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python3/dist-packages/jinja2/_compat.py", line 37, in reraise
    raise value.with_traceback(tb)
  File "<template>", line 10, in top-level template code
  File "/srv/pillar/network/default-ip4.sls", line 4, in template
    {% for route in salt['network.routes']() -%}
  File "/usr/lib/python3/dist-packages/salt/modules/network.py", line 1619, in routes
    routes_ = _ip_route_linux()
  File "/usr/lib/python3/dist-packages/salt/modules/network.py", line 518, in _ip_route_linux
    address_mask = convert_cidr(comps[0])
  File "/usr/lib/python3/dist-packages/salt/modules/network.py", line 1143, in convert_cidr
    network_info = salt.ext.ipaddress.ip_network(cidr)
AttributeError: module 'salt.ext' has no attribute 'ipaddress'


2019-02-06 14:48:32,682 [salt.pillar      :741 ][CRITICAL][14358] Rendering SLS 'zabbix.agent' failed, render error:
Jinja error: module 'salt.ext' has no attribute 'ipaddress'
/srv/pillar/network/default-ip4.sls(4):
---
# https://github.com/saltstack/salt/issues/27790#issuecomment-442754969

{% macro interface() -%}
{% for route in salt['network.routes']() -%}    <======================
  {%- if route['destination'] == '0.0.0.0' -%}
    {{ route['interface'] }}
  {%- endif %}
{%- endfor %}
{%- endmacro %}
[...]
---

This work around does not work for me.

{% for ip in grains['ipv4'] %}{% if grains['ipv4'] != '127.0.0.1' %}{{ ip }}{% endif %}{% endfor %}`

Coming from Chef and Puppet background, getting the ip is so simple. Really wish Salt could be more intelligent about this.
On all my systems, I have 3 networks. I just want to get the ip excluding 'lo' and 'docker0'.

Ubuntu 18.04

  • lo: 127.0.0.1
  • eno1: 192.x.x.x
  • docker0: 172.17.0.1

Ubuntu 14.04

  • lo: 127.0.0.1
  • eth0: 192.x.x.x
  • docker0: 172.17.0.1

I end up writing custom grain with same logic as Ansible one. Works for us in AWS, but won't work if you have complex networking.

# -*- coding: utf-8 -*-
#!/usr/bin/env python

from __future__ import absolute_import
import logging

log = logging.getLogger(__name__)

try:
    import netifaces
except ImportError as err:
    log.error("Unable to load 'netifaces' library. Please make sure it's installed")
    raise


def get_default_ipv4():
    grains = {'default_ipv4': None}

    gws = netifaces.gateways()
    default_gw_iface = gws['default'][netifaces.AF_INET][1]
    default_iface_ips = netifaces.ifaddresses(default_gw_iface)[netifaces.AF_INET]
    grains['default_ipv4'] = default_iface_ips[0]['addr']

    return grains

It's possible to get rid of external dependency but I wanted this code to be small and simple and without it, it will be black magic.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

If this issue is closed prematurely, please leave a comment and we will gladly reopen the issue.

Unstale.

Thank you for updating this issue. It is no longer marked as stale.

Was this page helpful?
0 / 5 - 0 ratings