Ansible: update rsyslog.conf multiline entry - Stack Overflow

admin2025-04-17  3

I need a way to update one entry in rsyslog.conf on RHEL 8/9/10 systems, using Ansible (I'm using 2.16). Unfortunately, the new rsyslog format uses multiline entries that are not easily manipulated with something like lineinfile.

Original (this is from RHEL 10 beta; RHEL 8 and 9 are similar but not identical):

module(load="imjournal"             # provides access to the systemd journal
       StateFile="imjournal.state") # File to store the position in the journal

Desired:

module(load="imjournal"             # provides access to the systemd journal
       StateFile="imjournal.state"  # File to store the position in the journal
       Ratelimit.Interval="300"
       Ratelimit.Burst="30000")

It would be OK to lose the comments, or change the format of the module config, as long as the semantics are preserved. It is also acceptable to replace existing parameters with the factory defaults.

What I've looked at so far:

  • ansible.builtin.lineinfile. I can't see how it would work since it only operates on single lines.
  • ansible.builtin.replace: I'm not sure how to craft a regexp that would deal with the comments, and preserve existing parameters without duplicating the new ones on each run.
  • ansible.builtin.blockinfile: This seems to be my best bet, if I find a way to inject the markers in the right place.
  • Using lineinfile to inject the markers, then blockinfile to update the entry, using factory defaults. I just don't know how to put the end marker reliably in the right place.
  • Check if rsyslog has an API to update the configuration file. I didn't find anything (nor did I expect to).

I need a way to update one entry in rsyslog.conf on RHEL 8/9/10 systems, using Ansible (I'm using 2.16). Unfortunately, the new rsyslog format uses multiline entries that are not easily manipulated with something like lineinfile.

Original (this is from RHEL 10 beta; RHEL 8 and 9 are similar but not identical):

module(load="imjournal"             # provides access to the systemd journal
       StateFile="imjournal.state") # File to store the position in the journal

Desired:

module(load="imjournal"             # provides access to the systemd journal
       StateFile="imjournal.state"  # File to store the position in the journal
       Ratelimit.Interval="300"
       Ratelimit.Burst="30000")

It would be OK to lose the comments, or change the format of the module config, as long as the semantics are preserved. It is also acceptable to replace existing parameters with the factory defaults.

What I've looked at so far:

  • ansible.builtin.lineinfile. I can't see how it would work since it only operates on single lines.
  • ansible.builtin.replace: I'm not sure how to craft a regexp that would deal with the comments, and preserve existing parameters without duplicating the new ones on each run.
  • ansible.builtin.blockinfile: This seems to be my best bet, if I find a way to inject the markers in the right place.
  • Using lineinfile to inject the markers, then blockinfile to update the entry, using factory defaults. I just don't know how to put the end marker reliably in the right place.
  • Check if rsyslog has an API to update the configuration file. I didn't find anything (nor did I expect to).
Share Improve this question asked Feb 1 at 1:17 Kevin KeaneKevin Keane 1,65816 silver badges26 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 2

As a hint. Given the file for testing

shell> cat /tmp/rsyslog.conf
module(load="imjournal"             # imjournal
       StateFile="imjournal.state"  # file
       )

module(load="foo"                   # foo
       StateFile="foo.state"        # file
       )

module(load="bar"                   # bar
       StateFile="bar.state"        # file
       )
  • Create markers. Declare the list
    mark_files:
      - path: /tmp/rsyslog.conf
        markers:
          - marker: imjournal
            regex1: 'module\(load="imjournal"(.*)'
            replace1: 'module(load="imjournal"'
            regex2: \)
            replace2: )

and execute the block

    - name: Create markers
      when: markers | d(false) | bool
      block:

        - name: Create BEGIN markers
          replace:
            path: "{{ item.0.path }}"
            regexp: "{{ item.1.regex1 }}"
            replace: |-
              {{ '#' }} BEGIN ANSIBLE MANAGED BLOCK {{ item.1.marker }}
              {{ item.1.replace1 }}
          loop: "{{ mark_files | subelements('markers') }}"

        - name: Create END markers
          replace:
            path: "{{ item.0.path }}"
            regexp: '({{ item.1.regex1 }}[\s\S]*?){{ item.1.regex2 }}'
            replace: |-
              \g<1>{{ item.1.replace2 }}
              {{ '#' }} END ANSIBLE MANAGED BLOCK {{ item.1.marker }}
          loop: "{{ mark_files | subelements('markers') }}"

This creates the markers

shell> cat /tmp/rsyslog.conf
# BEGIN ANSIBLE MANAGED BLOCK imjournal
module(load="imjournal"
       StateFile="imjournal.state"  # file
       )
# END ANSIBLE MANAGED BLOCK imjournal

module(load="foo"                   # foo
       StateFile="foo.state"        # file
       )

module(load="bar"                   # bar
       StateFile="bar.state"        # file
       )
  • Update the blocks. Declare the list
    update_files:
      - path: /tmp/rsyslog.conf
        markers:
          - marker: imjournal
            block: |
              module(load="imjournal"
                     StateFile="imjournal.state"
                     Ratelimit.Interval="300"
                     Ratelimit.Burst="30000"
                     )

and execute the task

    - name: Update files
      when: update | d(false) | bool
      blockinfile:
        path: "{{ item.0.path }}"
        marker: "# {mark} ANSIBLE MANAGED BLOCK {{ item.1.marker }}"
        block: "{{ item.1.block }}"
      loop: "{{ update_files | subelements('markers') }}"

This updates the files

shell> cat /tmp/rsyslog.conf
# BEGIN ANSIBLE MANAGED BLOCK imjournal
module(load="imjournal"
       StateFile="imjournal.state"
       Ratelimit.Interval="300"
       Ratelimit.Burst="30000"
       )
# END ANSIBLE MANAGED BLOCK imjournal

module(load="foo"                   # foo
       StateFile="foo.state"        # file
       )

module(load="bar"                   # bar
       StateFile="bar.state"        # file
       )

Notes:

  • The block that creates the markers is not idempotent. See example how to check the markers.

  • The regex2/replace2 attributes in the above example work with standalone closing parenthesis only. You'll have to modify this to your needs.

  • For more details see blockinfile markers.


Example of a complete playbook for testing

- hosts: localhost

  vars:

    mark_files:
      - path: /tmp/rsyslog.conf
        markers:
          - marker: imjournal
            regex1: 'module\(load="imjournal"(.*)'
            replace1: 'module(load="imjournal"'
            regex2: \)
            replace2: )

    update_files:
      - path: /tmp/rsyslog.conf
        markers:
          - marker: imjournal
            block: |
              module(load="imjournal"
                     StateFile="imjournal.state"
                     Ratelimit.Interval="300"
                     Ratelimit.Burst="30000"
                     )

  tasks:

    - name: Create markers
      when: markers | d(false) | bool
      block:

        - name: Create BEGIN markers
          replace:
            path: "{{ item.0.path }}"
            regexp: "{{ item.1.regex1 }}"
            replace: |-
              {{ '#' }} BEGIN ANSIBLE MANAGED BLOCK {{ item.1.marker }}
              {{ item.1.replace1 }}
          loop: "{{ mark_files | subelements('markers') }}"

        - name: Create END markers
          replace:
            path: "{{ item.0.path }}"
            regexp: '({{ item.1.regex1 }}[\s\S]*?){{ item.1.regex2 }}'
            replace: |-
              \g<1>{{ item.1.replace2 }}
              {{ '#' }} END ANSIBLE MANAGED BLOCK {{ item.1.marker }}
          loop: "{{ mark_files | subelements('markers') }}"

    - name: Update files
      when: update | d(false) | bool
      blockinfile:
        path: "{{ item.0.path }}"
        marker: "# {mark} ANSIBLE MANAGED BLOCK {{ item.1.marker }}"
        block: "{{ item.1.block }}"
      loop: "{{ update_files | subelements('markers') }}"

Based on Vladimir Botka's excellent answer, here is what I came up with. This solution is idempotent without resorting to a shell construct, using strictly Ansible modules. It also fixes a small bug when the last line in the text is followed by a comment.

I also removed the loop because the order of tasks is important, and would break with a loop.

The idea of using the find module comes from Thomas M's answer at this: How can I check if a string exists in a file?

- name: Update rate limiting in rsyslog.conf
  vars:
    update_file:  /etc/rsyslog.conf
    blockname:    imjournal
    regexstart:   'module\(load="imjournal"(.*)'
    replacestart: 'module(load="imjournal"'
    regexend: \).*
    replaceend: )
    marker:      "{{ '#' }} {mark} ANSIBLE MANAGED BLOCK {{ blockname }}"
    beginmarker: "{{ '#' }} BEGIN ANSIBLE MANAGED BLOCK {{ blockname }}"
    endmarker:   "{{ '#' }} END ANSIBLE MANAGED BLOCK {{ blockname }}"
    newblock:    |
      module(load="imjournal"
             StateFile="imjournal.state"
             Ratelimit.Interval="300"
             Ratelimit.Burst="30000")
  block:
   - name: Check if BEGIN marker already exists
     ansible.builtin.find:
       name:            "{{ update_file | dirname }}"
       patterns:        "{{ update_file | basename }}"
       file_type:       file
       use_regex:       False
       read_whole_file: True
       contains:        "{{ beginmarker }}"
     register: find_result
   - name: Create BEGIN markers
     when: find_result.matched == 0
     ansible.builtin.replace:
       path:    "{{ update_file }}"
       regexp:  '{{ regexstart }}'
       replace: |-
         {{ beginmarker }}
         {{ replacestart }}

    - name: Check if END marker already exists
      ansible.builtin.find:
        name:            "{{ update_file | dirname }}"
        patterns:        "{{ update_file | basename }}"
        file_type:       file
        use_regex:       False
        read_whole_file: True
        contains:        "{{ endmarker }}"
      register: find_result
    - name: Create END markers
      when: find_result.matched == 0
      ansible.builtin.replace:
        path:    "{{ update_file }}"
        regexp:  '({{ regexstart }}[\s\S]*?){{ regexend }}'
        replace: |-
          \g<1>{{ replaceend }}
          {{ endmarker }}

    - name: Update Module Load statement
      ansible.builtin.blockinfile:
        path:    "{{ update_file }}"
        marker:  "{{ marker }}"
        block:   "{{ newblock }}"

A few things worth highlighting and explaining

  • The regexp when creating the END delimiter includes [/s/S], from Vladimir Botka's code. /s means, all whitespace characters. /S means, all characters that aren't whitespace. Combined, they mean "all characters". Importantly, unlike the . in a regexp, [/s/S] includes newline.
  • The regexp for replacing the end marker uses the *? construct. The difference to the standard * operator in a regexp is that * is greedy, while *? is non-greedy. In other words, * will include everything to the last ) in the file, spanning multiple configuration directives. *? will only include everything up to the next ) (which is the end of the module( statement)
  • This code relies on find being called immediately before replace as the find result will be overwritten. This is why this code would not work with the original loop construct. If needed, this is a solvable problem.
  • The regexp for matching the end needs to have .* added to it. Otherwise, any existing comment would end up appended to the end marker, corrupting it.
转载请注明原文地址:http://anycun.com/QandA/1744842220a88378.html