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.

2 Likes

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

6 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 ) }}"

2 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
1 Like