ansiblenetplan

Ansible regexp for netplan config dhcp4


I'm trying to edit the 01-netcfg.yml file with Ansible. Below is the file:

network:
  version: 2
  renderer: NetworkManager
  ethernets:
    enp0s31f6:
      dhcp4: no
      nameservers:
       addresses: [8.8.8.8, 8.8.4.4]
    enp12s0:
      dhcp4: no
      addresses:
        - 192.168.0.1/24
    enp8s0:
      dhcp4: no
      addresses:
        - 192.168.1.1/24
    enp9s0:
      dhcp4: no
      addresses:
        - 192.168.16.1/24

I'm trying to change the dhcp4 value from no to yes under enp0s31f6 only.

- lineinfile:
        path: /etc/netplan/01-netcfg.yaml
        regexp: '      dhcp4: no'
        line: '      dhcp4: yes'
        insertbefore: '      nameservers:'
        firstmatch: True
        state: present

When I run the above code, it is modifying the dhcp4 under enp0s31f6 and also the dhcp4 under enp12s0. I tried insertafter as well, but same result.


Solution

  • Q: "Change the dhcp4 value from no to yes under enp0s31f6 only."


    Given the file for testing

    shell> cat /tmp/netplan/01-netcfg.yml 
    network:
      version: 2
      renderer: NetworkManager
      ethernets:
        enp0s31f6:
          dhcp4: no
          nameservers:
           addresses: [8.8.8.8, 8.8.4.4]
        enp12s0:
          dhcp4: no
          addresses:
            - 192.168.0.1/24
        enp8s0:
          dhcp4: no
          addresses:
            - 192.168.1.1/24
        enp9s0:
          dhcp4: no
          addresses:
            - 192.168.16.1/24
    

    A: Do not use the module lineinfile (1) in this use_case. The module replace (2) might be a better option. The best option is to fetch, update, and copy the file (3).

    1. lineinfile

    Generally, the module lineinfile is not able to update any neplane interface's parameter. In the particular case of the given file, the task below does the job

        - lineinfile:
            path: /tmp/netplan/01-netcfg.yml
            regexp: '^(\s+)dhcp4:\s+.*$'
            backrefs: true
            line: '\1dhcp4: yes'
            firstmatch: true
    

    Running the play with the options --check --diff gives

    TASK [lineinfile] *****************************************************************************
    --- before: /tmp/netplan/01-netcfg.yml (content)
    +++ after: /tmp/netplan/01-netcfg.yml (content)
    @@ -3,7 +3,7 @@
       renderer: NetworkManager
       ethernets:
         enp0s31f6:
    -      dhcp4: no
    +      dhcp4: yes
           nameservers:
            addresses: [8.8.8.8, 8.8.4.4]
         enp0s31f6:
    

    This works because the interface enp0s31f6 is first in the ethernets keys.


    Notes:

    • Set backrefs: true. Put the leading whitespace into the group \1
    • Set firstmatch: true. The default false would match the last dhcp4 in the interface enp9s0. Quoting from regexp: "Only the last line found will be replaced."

    This doesn't work if the interface enp0s31f6 is not the first in the dictionary ethernets. For example,

    shell> cat /tmp/netplan/01-netcfg.yml 
    network:
      version: 2
      renderer: NetworkManager
      ethernets:
        eth01:
          dhcp4: no
          nameservers:
           addresses: [8.8.8.8, 8.8.4.4]
        enp0s31f6:
          dhcp4: no
          nameservers:
           addresses: [8.8.8.8, 8.8.4.4]
    

    The task below, even with the option insertafter: enp0s31f6

        - lineinfile:
            path: /tmp/netplan/01-netcfg.yml
            regexp: '^(\s+)dhcp4:\s+.*$'
            backrefs: true
            line: '\1dhcp4: yes'
            insertafter: enp0s31f6
            firstmatch: true
    

    gives

    TASK [lineinfile] *****************************************************************************
    --- before: /tmp/netplan/01-netcfg.yml (content)
    +++ after: /tmp/netplan/01-netcfg.yml (content)
    @@ -3,7 +3,7 @@
       renderer: NetworkManager
       ethernets:
         eth01:
    -      dhcp4: no
    +      dhcp4: yes
           nameservers:
            addresses: [8.8.8.8, 8.8.4.4]
         enp0s31f6:
    

    because insertafter doesn't work if regexp is matched. Quoting from regexp:

    If the regular expression is not matched, the line will be added to the file in keeping with insertbefore or insertafter settings.

    If you remove the option regexp the option backrefs can't work, and the regexp group \1 can't be used in line. Then, the task below

        - lineinfile:
            path: /tmp/netplan/01-netcfg.yml
            line: '      dhcp4: yes'
            insertafter: enp0s31f6
            firstmatch: true
    

    doesn't replace the existing parameter dhcp4: no. Instead, a new one will be added

    TASK [lineinfile] *****************************************************************************
    --- before: /tmp/netplan/01-netcfg.yml (content)
    +++ after: /tmp/netplan/01-netcfg.yml (content)
    @@ -7,6 +7,7 @@
           nameservers:
            addresses: [8.8.8.8, 8.8.4.4]
         enp0s31f6:
    +      dhcp4: yes
           dhcp4: no
           nameservers:
            addresses: [8.8.8.8, 8.8.4.4]
    

    1. replace

    The module replace seems to be a better option. The task below

        - replace:
            path: /tmp/netplan/01-netcfg.yml
            after: enp0s31f6
            before: enp12s0
            regexp: 'dhcp4:\s+.*\n'
            replace: 'dhcp4: yes\n'
    

    does the job

    TASK [replace] ********************************************************************************
    --- before: /tmp/netplan/01-netcfg.yml
    +++ after: /tmp/netplan/01-netcfg.yml
    @@ -3,7 +3,7 @@
       renderer: NetworkManager
       ethernets:
         enp0s31f6:
    -      dhcp4: no
    +      dhcp4: yes
           nameservers:
            addresses: [8.8.8.8, 8.8.4.4]
         enp12s0:
    

    The problem is that you have to provide the option before: enp12s0. There might be another interface or none at all.

    <TBD: get the value of before>


    1. Solution

    Use the module fetch to fetch the file(s) and the filter combine to update the configuration. Then, you can synchronize the file(s) with the remote host(s). This procedure provides a robust, structured, and easily extensible framework for updating multiple netplan configuration files on multiple remote hosts in parallel.

    Declare the path to netplan

      netplan_dir: /tmp/netplan
    

    and the dictionary with the updates

      netplan_update:
        01-netcfg.yml:
          network:
            ethernets:
              enp0s31f6:
                dhcp4: yes
    

    Running on the localhost for testing, fetch the file(s)

        - fetch:
            dest: /tmp/fetch
            src: "{{ netplan_dir }}/{{ item }}"
          loop: "{{ netplan_update.keys()|list }}"
    

    Take a look at the fetched file(s)

    shell> tree /tmp/fetch/
    /tmp/fetch/
    └── localhost
        └── tmp
            └── netplan
                └── 01-netcfg.yml
    
    3 directories, 1 file
    

    Read the configuration into the dictionary netplan_orig

        - set_fact:
            netplan_orig: "{{ netplan_orig|d({})|combine({item: conf}) }}"
          loop: "{{ netplan_update.keys()|list }}"
          vars:
            file: "/tmp/fetch/{{ inventory_hostname }}{{ netplan_dir }}/{{ item }}"
            conf: "{{ lookup('file', file)|from_yaml }}"
    

    gives

      netplan_orig:
        01-netcfg.yml:
          network:
            ethernets:
              enp0s31f6:
                dhcp4: false
                nameservers:
                  addresses:
                  - 8.8.8.8
                  - 8.8.4.4
              enp12s0:
                addresses:
                - 192.168.0.1/24
                dhcp4: false
              enp8s0:
                addresses:
                - 192.168.1.1/24
                dhcp4: false
              enp9s0:
                addresses:
                - 192.168.16.1/24
                dhcp4: false
            renderer: NetworkManager
            version: 2
    

    Update the configuration. Declare the combination of the dictionaries

      netplan_conf: "{{ netplan_orig|
                        combine(netplan_update, recursive=true) }}"
    

    gives

      netplan_conf:
        01-netcfg.yml:
          network:
            ethernets:
              enp0s31f6:
                dhcp4: true
                nameservers:
                  addresses:
                  - 8.8.8.8
                  - 8.8.4.4
              enp12s0:
                addresses:
                - 192.168.0.1/24
                dhcp4: false
              enp8s0:
                addresses:
                - 192.168.1.1/24
                dhcp4: false
              enp9s0:
                addresses:
                - 192.168.16.1/24
                dhcp4: false
            renderer: NetworkManager
            version: 2
    

    Update the fetched files

        - copy:
            dest: "/tmp/fetch/{{ inventory_hostname }}{{ netplan_dir }}/{{ item.key }}"
            content: "{{ item.value|to_nice_yaml(indent=2) }}"
          loop: "{{ netplan_conf|dict2items }}"
          delegate_to: localhost
    

    Take a look at the content of the file

    shell> cat /tmp/fetch/localhost/tmp/netplan/01-netcfg.yml 
    network:
      ethernets:
        enp0s31f6:
          dhcp4: true
          nameservers:
            addresses:
            - 8.8.8.8
            - 8.8.4.4
        enp12s0:
          addresses:
          - 192.168.0.1/24
          dhcp4: false
        enp8s0:
          addresses:
          - 192.168.1.1/24
          dhcp4: false
        enp9s0:
          addresses:
          - 192.168.16.1/24
          dhcp4: false
      renderer: NetworkManager
      version: 2
    

    Synchronize the file(s)

        - synchronize:
            src: "/tmp/fetch/{{ inventory_hostname }}{{ netplan_dir }}/{{ item }}"
            dest: "{{ netplan_dir }}/{{ item }}"
          loop: "{{ netplan_update.keys()|list }}"
          # notify: netplan apply
    

    Uncomment the notify directive to apply the configuration and create the handler

      handlers:
    
        - name: netplan apply
          command: netplan apply
    

    Example of a complete playbook for testing

    - hosts: localhost
    
      vars:
    
        netplan_dir: /tmp/netplan
    
        netplan_update:
          01-netcfg.yml:
            network:
              ethernets:
                enp0s31f6:
                  dhcp4: yes
    
        netplan_conf: "{{ netplan_orig|
                          combine(netplan_update, recursive=true) }}"
    
      tasks:
    
        - fetch:
            dest: /tmp/fetch
            src: "{{ netplan_dir }}/{{ item }}"
          loop: "{{ netplan_update.keys()|list }}"
          tags: netplan_fetch
    
        - set_fact:
            netplan_orig: "{{ netplan_orig|d({})|combine({item: conf}) }}"
          loop: "{{ netplan_update.keys()|list }}"
          vars:
            file: "/tmp/fetch/{{ inventory_hostname }}{{ netplan_dir }}/{{ item }}"
            conf: "{{ lookup('file', file)|from_yaml }}"
          tags: [netplan_read, netplan_update]
        - debug:
            var: netplan_orig
          tags: netplan_read
          when: debug|d(false)|bool
        - debug:
            var: netplan_conf
          when: debug|d(false)|bool
          tags: netplan_read
    
        - copy:
            dest: "/tmp/fetch/{{ inventory_hostname }}{{ netplan_dir }}/{{ item.key }}"
            content: "{{ item.value|to_nice_yaml(indent=2) }}"
          loop: "{{ netplan_conf|dict2items }}"
          delegate_to: localhost
          tags: netplan_update
    
        - synchronize:
            src: "/tmp/fetch/{{ inventory_hostname }}{{ netplan_dir }}/{{ item }}"
            dest: "{{ netplan_dir }}/{{ item }}"
          loop: "{{ netplan_update.keys()|list }}"
          # notify: netplan apply
          tags: netplan_synchronize
    
      handlers:
    
        - name: netplan apply
          command: netplan apply