Ansible vs. NetworkManager

Hi,

I’m currently busy replacing all my bone-headed Bash configuration scripts by Ansible playbooks and roles, and I like it so far.

One of the usual post-install configuration steps involves fine-tuning the network configuration. As far as I’m concerned, I usually have three relatively simple scenarios for servers which I describe in this little blog article (in French, but the *nix bits are universal):

This all boils down to:

  1. Rocky Linux 8 on a local server (single NIC) behind a router.
  2. Rocky Linux 8 on a local proxy/gateway (two NICs).
  3. Rocky Linux 8 on a public-facing server (single NIC).

I just wonder if it’s reasonable to try to automate these steps using Ansible. Or is it better to stick to the KISS principle and once the basic post-configuration playbook ran on the machine, simply fire up nmtui and configure things manually?

Any suggestions?

The package rhel-system-roles provides many roles for our plays: https://access.redhat.com/articles/3050101

The most relevant there are rhel-system-roles.network and rhel-system-roles.firewall.
The example in there is for network.

More on that role is in upstream Ansible Galaxy (“Read Me”) or
in /usr/share/ansible/roles/rhel-system-roles.network/README.md

The RHEL docs do also have examples for use of “System Role” (vs other methods): https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/configuring_and_managing_networking/configuring-an-ethernet-connection_configuring-and-managing-networking#doc-wrapper


The only “scary bit” is that if you change the config completely from what installer has set up, you could kill your connection to the machine. (The same “fun” one can have with improper firewall config.)

1 Like

First of all, thanks again for taking the time to explain all that stuff, which can be quite scary if you’re a newcomer.

Before reading your post, I did some experimenting, and I got a partial success with this:

---
- name: Configure static IP address
  community.general.nmcli:
    conn_name: LAN
    ifname: enp1s0
    type: ethernet
    method4: manual
    ip4: 192.168.2.10/24
    gw4: 192.168.2.1
    method6: disabled
    state: present
    autoconnect: yes

I ran this on a VM with a single enp1s0 interface, and here’s what I have now:

# nmcli con show
NAME    UUID                                  TYPE      DEVICE 
enp1s0  a14b78bf-388c-492c-8de7-15d88c345213  ethernet  enp1s0 
LAN     aea69fe5-a97b-4821-a518-fdfae829b48d  ethernet  --

Now I’d like to activate and use the LAN connection via Ansible, but how would I go about that?

That is “exciting”.

The installer had generated a connection (con-name “enp1s0”) for device enp1s0.
Your task generates connection “LAN” same device enp1s0.
The first connection is active, but it should be removed.

If it is removed, then will the “LAN” automatically activate?
Even if it does, will your (Ansible) ssh session stay alive?
If the current (dynamic) IP address differs from the new (static) address, then the session is definitely lost.

Could Ansible complete both “add new” and “remove old” tasks before it loses access?
Would “remove old first” (unless it kills net instantly) better ensure that new connection is activated?

Could rhel-system-roles.network handle two connections more reliably than the community.general.nmcli?
(The latter used to support more tweaks than the former – before latest point update. You don’t use such tweaks.)


Another point is that you don’t really need a “new connection”. If you could rename the existing connection to “LAN” first, then the task would modify the one and only connection. Neither of those roles seems to support rename (although I did not read carefully).

This is a crude (untested) rename:

  - name: Check whether we already have LAN
    check_mode:   false
    changed_when: false
    ignore_errors: true
    ansible.builtin.shell: nmcli con show | grep LAN
    register: has_conn_LAN

  - name: Rename connection to LAN
    ansible.builtin.command: nmcli con modify enp1s0 con-name LAN
    when:
    - not has_conn_LAN.rc == 0

Finally, the name of connection is mostly documentation, but the name is not used anywhere else, is it?

1 Like

This all turns out to be quite of a nut to crack. It’s the kind of situation where you find some answers, but these make you reformulate your initial question. Anyway, here goes.

Curiously enough, it’s not a trivial matter to find out the IP address of your VM. If you use ansible_default_ipv4.address, you might be in for a big surprise on some VMs. Here’s a foolproof method I found in a book and just tested. This actually displays the IP address of the Ansible connection:

- name: Check target host IP address
  ansible.builtin.debug:
    msg: "{{ ansible_facts['env']['SSH_CONNECTION'].split(' ')[2] }}"

Now I’m facing the following problem, but I don’t know (yet) how to translate this into Ansible-specific syntax:

  1. Find out network parameters of the existing Ansible connection (see above).
  2. Find out the corresponding name of the connection in NetworkManager.
  3. Rename this connection to LAN. (We’ll cross the WAN bridge when we get there.)
  4. Turn it into a static configuration using the detected IP address, gateway and DNS server(s).
  5. Don’t forget to make this declarative and idempotent.

So it looks like the question is clear now. Only I’m a bit clueless about how to put this into practice with Ansible. Ideally, if I could make this work with the vanilla community.general.nmcli module, it would be wonderful.

Any suggestions ?

You can get the output to pass to other sections lower down by using the register option, so:

- name: Check target host IP address
  ansible.builtin.debug:
    msg: "{{ ansible_facts['env']['SSH_CONNECTION'].split(' ')[2] }}"
  register: server_ip

you can change server_ip to whatever name you want. Then in other tasks you reference {{ server_ip }} so:

- name: Add my IP to trusted source
  firewalld:
    zone: trusted
    source: {{ server_ip }}
    state: enabled
    permanent: yes
    immediate: yes

maybe not a great example, but it at least shows the context on how to do it using the register method. You can incorporate that with the specific tasks that you want to do. That example would resolve points 1 and 4 pretty much. The remainder you’d most likely pull the info using the Ansible network manager module that you were mentioning earlier.

You can use the register option for the IP of the server, as well the when you get the DNS config, default route also, and use the variables later to apply the new network config. From ansible facts you can get ansible_dns['nameservers'], same for obtaining gateway. I’ve personally never had the problem with the default_ipv4 facts option, but perhaps that can be something specific to certain situations.

Assuming that the ansible_facts command you used returns the IP without any additional info, then all should be OK in that instance.

1 Like

OK, I spent a couple hours experimenting some more, and I think I found a rather elegant solution to replace the default DHCP NetworkManager connection by a static counterpart called LAN. It’s tested, and it works as expected. And so far I’ve done things “the Ansible way”, meaning I could avoid any shell or command calls.

- name: Configure static connection using NetworkManager
  community.general.nmcli:
    type: ethernet
    conn_name: LAN
    ifname: "{{ ansible_default_ipv4.interface }}"
    method4: manual
    ip4: "{{ ansible_default_ipv4.address }}/{{ ansible_default_ipv4.prefix }}"
    gw4: "{{ ansible_default_ipv4.gateway }}"
    dns4: "{{ ansible_dns.nameservers }}"
    dns4_search: "{{ ansible_dns.search }}"
    state: present
    autoconnect: true

- name: Deactivate default NetworkManager connection
  community.general.nmcli:
    type: ethernet
    conn_name: "{{ ansible_default_ipv4.interface }}"
    state: absent

Cheers,

Niki

2 Likes

And here’s the final version after some more fiddling:

- name: Disable DNS processing in NetworkManager configuration
  ansible.builtin.copy:
    dest: /etc/NetworkManager/conf.d/90-dns-none.conf
    mode: 0644
    content: |
      [main]
      dns=none
  notify: Reload NetworkManager configuration

- name: Reflect manual DNS configuration in comments
  ansible.builtin.replace:
    path: /etc/resolv.conf
    regexp: "Generated by NetworkManager"
    replace: "/etc/resolv.conf"

# NetworkManager keeps updating resolv.conf even if we tell it not to
- name: Ensure resolv.conf is immutable
  file:
    path: /etc/resolv.conf
    attr: +i
  register: resolv_file
  changed_when: "'i' not in resolv_file.diff.before.attributes"

- name: Convert dynamic to static network configuration
  community.general.nmcli:
    type: ethernet
    conn_name: LAN
    ifname: "{{ ansible_default_ipv4.interface }}"
    method4: manual
    ip4: "{{ ansible_default_ipv4.address }}/{{ ansible_default_ipv4.prefix }}"
    gw4: "{{ ansible_default_ipv4.gateway }}"
    method6: disabled
    state: present
    autoconnect: true

- name: Clean up initial NetworkManager connection
  community.general.nmcli:
    type: ethernet
    conn_name: "{{ ansible_default_ipv4.interface }}"
    state: absent

Source : https://twitter.com/microlinux_eu/status/1663786705277599745 :upside_down_face: