Krish
Krish

Reputation: 21

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.

Upvotes: 1

Views: 263

Answers (1)

Vladimir Botka
Vladimir Botka

Reputation: 68189

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

Upvotes: 0

Related Questions