Ansible Adventures

In this case it is because I’m delivering raw commands to a switch which is a pain no matter what you’re using.

Doing things on actual hosts that have Python installed is where you save on time/complexity… but those things need a functional switch first so here I am at the moment.

If you were to do this in a shell script, you’d need to use expect which… no thank you.

1 Like

ah ok. thanks for clarification. :slight_smile:
I only had once a MikroTik ( CCR2004-1G-12S+2XS ) for a few days for testing.

1 Like

there is an expect module in ansible too… fyi

1 Like

Still no thanks! Lol

The RouterOS Ansible module is barebones but better than using expect in any form imo.

Also, I just got basic network working on it with VLANs and IPv4. Still need to add MTU and some DHCP client stuff but I’ve got good boilerplate now that I can build on.

2 Likes

Really keen to get into ansible but limited time, anyone’s thoughts on the best place to start?

1 Like

@geerlingguy wrote the book on it which I highly recommend.

3 Likes

There’s also a perpetual 50% off coupon for A4D here: Outreach: Books and Materials · ansible/community Wiki · GitHub :slight_smile:

7 Likes

Ah i’ve already bought the book on Apple books. Will try and have a proper read this weekend; its a long weekend here.

1 Like
# The nmcli module insists that certain parameters are set in the
# presence of others, so an interface cannot be configured gradually,
# one parameter at a time. It must be configured in its entirety. To
# work around this limitation, ternary and omit are used extensively
# in one task.
- name: "Configure interface {{ nm_name_pretty }}"
  community.general.nmcli:
    conn_name: "{{ nm_con_name }}"
    type: "{{ nm_iface_type }}"
    method6: ignore
    autoconnect: true
    state: present
    ifname: "{{ ifname_var | default(omit) }}"
    vlandev: "{{ vlandev_var | default(omit) }}"
    vlanid: "{{ vlanid_var | default(omit) }}"
    method4: "{{  method4_disabled_var
                  | default(method4_auto_var)
                  | default(method4_manual_var)
                  | default(omit) }}"
    dhcp_client_id: "{{ dhcp_client_id_var | default(omit) }}"
    ip4: "{{ ip4_var | default(omit) }}"
    routes4: "{{ routes_var | default(omit) }}"
    mtu: "{{ iface['mtu'] | default(omit) }}"
  become: true
  vars:
    ifname_var: "{{ ( iface['type'] == 'phy' )
                    | ternary( iface['dev'], omit ) }}"
    vlandev_var: "{{  ( iface['type'] == 'vlan' )
                      | ternary( iface_item['dev'], omit ) }}"
    vlanid_var: "{{   ( iface['type'] == 'vlan' )
                      | ternary( iface['subnet_id'], omit) }}"
    method4_disabled_var: "{{ ( ( iface['ip4'] is undefined
                                  or iface['prefix4'] is undefined )
                                and not iface['dhcp4'] )
                              | ternary('disabled', omit) }}"
    method4_auto_var: "{{ iface['dhcp4'] | ternary('auto', omit) }}"
    method4_manual_var: "{{ ( not iface['dhcp4']
                              and iface['ip4'] is defined
                              and iface['prefix4'] is defined )
                            | ternary('manual', omit) }}"
    dhcp_client_id_var: >-
      {{  ( iface['dhcp4'] and iface['dhcp4_duid'] )
          | ternary( 'ff:' + iface['dhcp_client_id'], omit ) }}
    ip4_var: "{{  ( not iface['dhcp4']
                    and iface['ip4'] is defined
                    and iface['prefix4'] is defined )
                  | ternary( iface['ip4'] + '/' + iface['prefix4'], omit ) }}"
    routes4_var: "{{  ( iface['routes'] is defined and routes4_var != [] )
                      | ternary(  iface['routes'].keys()
                                  | zip( iface['routes'].values() )
                                  | map('ansible.netcommon.ipv4')
                                  | map('join', ' '),
                                  omit ) }}"

3 Likes

I was processing each interface on my hosts in a loop like so:

- name: Configure network interfaces and their children interfaces
  ansible.builtin.include_tasks: cfg_iface.yml
  loop: "{{ ifaces }}"
  loop_control:
    loop_var: iface_item

In my case, the ifaces variable is a list of dictionaries. This would effectively cause the role to execute the loop serially as it appears to only parallelize loops where hosts share the same loop variable values. Since each interface is unique to the host, they would execute in serial which eats up a lot of time.

An easy fix for this is to use an index variable instead.

- name: Configure network interfaces and their children interfaces
  ansible.builtin.include_tasks: cfg_iface.yml
  loop: "{{ range( 0, ifaces | length ) | list }}"
  loop_control:
    loop_var: iface_index_item

Now the hosts’ interfaces execute in parallel.

1 Like

This is a weird one. to_nice_yaml is sorting a list of dictionaries differently on each playbook run which is preventing it from being idempotent. It’s literally reading in yaml from hostvars, and then spitting it back out to hostvars in a different order. All of the logic that would change the values isn’t be executed. It’s just in one way and out another way.

I have sorted the data manually now in the role and used the sort_keys=false option in to_nice_yaml which solve the idempotency issue of the list be reordered each time, but it weirdly ignores the manual sorting that I’ve applied to the list. I literally debug print the list right before writing it to the file with to_nice_yaml and it’s in a different order. But now it at least sticks to that order…

I don’t understand why the sort_keys value has any effect at all. I am not concerned with the order of the keys in each dict. I am concerned with the order of the list of dicts themselves.

- name: Write network interface device definitions to the host variables file
  ansible.builtin.blockinfile:
    path: "{{ inventory_dir }}/host_vars/{{ inventory_hostname }}.yml"
    insertbefore: "{{ safe_zone_comment | regex_escape }}"
    marker: '# {mark} ANSIBLE MANAGED BLOCK: Network Interfaces'
    block: |
      {{ inv_iface_comment | comment }}
      ifaces:
        {{  ifaces
            | to_nice_yaml(width=1023, indent=2, sort_keys=false)
            | indent(2) }}
    backup: true
  delegate_to: 127.0.0.1
1 Like

wtf man

TASK [o0_o.host.inventory : Configure mirrors for each OS group] ***********************
fatal: [bootstrap.mgmt.hq.example.com]: FAILED! => {"changed": false, "msg": "Path /Users/o0-o/BootTest/inventory/group_vars/debian_11.yml does not exist !", "rc": 257}

PLAY RECAP *****************************************************************************
bootstrap.mgmt.hq.example.com : ok=78   changed=1    unreachable=0    failed=1    skipped=7    rescued=0    ignored=0

> stat /Users/o0-o/BootTest/inventory/group_vars/debian_11.yml
16777220 188652207 -rw-r--r-- 1 o0-o staff 0 286 "Aug  6 12:22:29 2022" "Aug  6 12:22:27 2022" "Aug  6 12:22:27 2022" "Aug  6 12:22:27 2022" 4096 8 0 /Users/o0-o/BootTest/inventory/group_vars/debian_11.yml

derp, left out the delegate_to line…

2 Likes

Ansible’s synchronize module (rsync wrapper) is nice to have but it runs on localhost by default (wtf?) and is push by default (wtf again). Seems like a very opinionated implementation. IMO, they should not have changed the default delegation behavior and had it run on the remote host like everything else. And they should have made the push/pull mode a mandatory field instead of defaulting to push. This kind of implementation is really hazardous. The intention needs to be explicit when you’re moving data around.

1 Like

Ansible has retry/until functionality, but you cannot loop through a list and exit on the first success, which is what you want to do if you are setting up a mirror and have failover sources.

Recursion in a rescue block does the trick though.

# down_mirror.yml
- name: Try each mirror until one succeeds
  block:

    - name: Download mirror contents
      ansible.posix.synchronize:
        src: "rsync://{{ os_mirrors | first }}/{{ path_item }}"
        dest: "{{ http_root }}/pub/{{ mirror_item['os'] }}/{{ path_item }}"
        archive: true
        delete: true
        mode: pull
      become: true
      delegate_to: "{{ inventory_hostname }}" #runs on localhost by default

  rescue:

    - name: Mirror failed, remove from list
      ansible.builtin.set_fact:
        os_mirrors: "{{ os_mirrors[1:] }}"

    - name: The mirror list has not been exhausted
      ansible.builtin.assert:
        that: os_mirrors | length > 0
        quiet: true

    - name: Run tasks to download mirror contents (recursive)
      ansible.builtin.include_tasks: down_mirror.yml
2 Likes

I got caught by confusing block behavior again. I might have mentioned this before, but a when declaration on a block is distributed across the tasks in that block. So the conditions are evaluated on each task in the block. Contrast this to include_tasks which evaluates its when conditions once and then runs all tasks within the file that it references.

However, if you call include_tasks within a block, the when condition of the block is re-evaluated on each task including the ones in the file referenced by include_tasks.

This can be very frustrating when running simple data gathering operations like “if variables X, Y and Z are undefined, go do things to define them” in which case, the block would execute tasks up to the point that one of those variables is declared and then skip the rest. As a result, you really should embrace splitting up tasks into relatively small tasks files.

As a rule of thumb, I try to limit my tasks files to around 120 lines unless they require lengthy commenting, and now I think I’ll completely avoid nesting include_tasks in blocks.

2 Likes

So I’ve come across a difficult situation where I have Wireguard tunnels and Wireguard clients and I want to propagate routes (Allowed IPs in Wireguard parlance) between all of them. To do this in Ansible, I have to traverse a tree of interfaces across multiple hosts, propagating routes both up and down the tree. Basically writing a simple routing protocol into Ansible :face_vomiting:

So instead, I’m just going to run a nested loop defining each Wireguard interface on each host until the definitions stop changing. It’s so so so inefficient. I’ll have to revisit once I actually figure out how to write Ansible plugins…

Luckily it’s not like I have dozens of sites with nested Wireguard tunnels, but I do need to traverse at least 2 layers at the moment, so might as well do something that will scale, albeit at a snails pace.

---
# def_wg_peers.yml

- name: >-
    Define a list of Wireguard interfaces across all hosts before defining
    peers
  ansible.builtin.set_fact:
    all_ifaces_wg_before_peers: >-
      {{ hostvars.values() | map(attribute='ifaces_wg') | flatten }}

- name: Run tasks to define peers on each Wireguard interface
  ansible.builtin.include_tasks: def_wg_peers_iface.yml
  loop: "{{ range( 0, ifaces_wg | length ) | list }}"
  loop_control:
    loop_var: iface_index_item

- name: >-
    Define a list of Wireguard interfaces across all hosts after defining peers
  ansible.builtin.set_fact:
    all_ifaces_wg_after_peers: >-
      {{ hostvars.values() | map(attribute='ifaces_wg') | flatten }}

# TODO: this is horribly inefficient, traverse the tree properly
- name: >-
    Re-run tasks to define peers on each Wireguard interfaces recursively
    until no values change
  ansible.builtin.include_tasks: def_wg_peers.yml
  when: all_ifaces_wg_before_peers != all_ifaces_wg_after_peers
1 Like

I have migrated my NetworkManager role from using the community.general.nmcli module to templating key files in /etc/NetworkManager/system-connections. In doing so, I have eliminated hundreds of lines of code, dramatically simplifying the whole process.

I think the lesson is to not become enamored with a module just because it exists, especially if there is an underlying configuration file that can be templated.

2 Likes

FYI

ive noticed that the nmcli module when run from rhel8 (ansible 2.12) throws errors but when i run the nmcli module from rhel7 (ansible version 2.9 (iirc)) works fine…
wtf

2 Likes

Interesting. It has run fine for me from macOS on latest Ansible versions targeting everything from Debian to Fedora but it’s missing a lot of features (most notably, Wireguard).

Anyone know how to use a variable to set other variables?

Theres got to be a way to do it without using lookup plugin… right?

1 Like