1

I'm trying to solve a problem where I want to add a setting 'pci=noaer' to all kernel definitions in /boot/grub/grub.conf, but only if the setting doesn't exist. I've managed to get the basic functionality to work with the replace module, in that the module WILL add the setting to the relevant lines, but the problem is in NOT adding the setting to lines where it already exists. In other words, if I run the playbook once, it'll add the setting as needed, but every other run of the playbook after that adds another instance of the setting to each line. The following is an example of the line I want to update and the regexp and replace lines I currently have configured to update it:

Line to change:
kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro root=/dev/sda3

After the playbook runs the above line should look like:
kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro root=/dev/sda3 pci=noaer

Relevant replace module parameters:

regexp: '(^\s+kernel /vmlinuz.+(?!pci=noaer).*$)'  
replace: '\1 pci=noaer'

I've tried using the \b sequences to clearly define the boundaries of pci=noaer (e.g. (?!\bpci=noaer\b)), but with no luck. I've tested this a bit on regex101 but haven't really been able to definitively understand why the negative lookahead isn't doing what it's supposed to be doing (looking for cases where the pci=noaer string isn't there) - or how I'm using it within the replace module. I've had similar issues with negative lookaheads/lookbehinds in lineinfile, and I'm perfectly willing to admit this is total user error, given that I'm fairly new to ansible and python, and my regex skills are fairly limited thus far. But I've searched and searched and searched and I can't seem to find any good indication of why the negative lookaround sequences don't work the way I expect them to within these modules. Could this be a case of not properly managing the greediness of my regex configuration?

For what it's worth, I have seen that lookahead/lookbehind might have issues in some of the 2.x versions of Ansible, so perhaps this is a version problem? I'm running Ansible 2.6.20 with an underlying Python version of 2.6.6. If this is indeed an issue with my version, is there a way around it without having to update to something more recent?

A bit of additional information: This is being done across multiple CentOS 6-based systems where I don't feel like I can necessarily expect the kernel lines in the grub.conf file to be the same from one system to the next (perhaps they should be, but that's a topic for a different discussion). That's why the fairly non-specific regex. Also, since it's CentOS 6, there is no equivalent of update-grub or grub2-mkconfig for that OS. It's just a matter of editing the /etc/grub.conf or /boot/grub/grub.conf file and rebooting.

2
  • Could you not add a pre-task/task to perform an egrep for that kernel param and then use the return code in a "when" statement for the actual replace? That way it will only add it when it doesn't find an instance of it in the grub.conf Commented Aug 4, 2021 at 18:42
  • @BrettLevene Sure, that could work, but it kind of undermines the whole point of using Ansible for things that Ansible should be able to do, does it not? Yes, there are cases where Ansible can't do what you want it to and you have to resort to external scripts and system calls to get the job done. But in this case the replace and lineinfile modules should be able to do this natively, so I feel obligated to do the work to learn how to use them properly.
    – ahuie
    Commented Aug 9, 2021 at 15:47

1 Answer 1

1

Your regexp can't work because greedy .+ in front of the lookahead assertion will consume the line till the very end. The next problem is that you require at least one space at the beginning of the line \s+.

Try this

        regexp: '^(?!.* pci=noaer.*)(\s*kernel /vmlinuz.*)$'
        replace: '\1 pci=noaer'

It's important to understand that regexp in Ansible module replace uses MULTILINE and does not use DOTALL. Quoting:

Uses MULTILINE mode, which means ^ and $ match the beginning and end of the file, as well as the beginning and end respectively of each line of the file. Does not use DOTALL, which means the . special character matches any character except newlines.


If it does not work below is a brute force solution. Given the testing file

shell> cat grub.cfg
kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro root=/dev/sda3
kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro recovery root=/dev/sda3
kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro root=/dev/sda3
kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro recovery root=/dev/sda3

get the lines

    - command: cat grub.cfg
      register: grub
      check_mode: false

For simplicity, I used command but you might want to use fetch, slurp, or any other suitable module instead.

Test it. In debug, iterate the matched lines, split the properties of the line, and find the missing properties

    - debug:
        msg:
          - "{{ item }}"
          - "{{ missing }}"
      loop: "{{ grub.stdout_lines|select('match', match) }}"
      loop_control:
        label: "{{ line.1 }}"
      when: missing|length > 0
      vars:
        match: '^\s*kernel /vmlinuz.+$'
        line: "{{ item.split() }}"
        missing: "{{ props|difference(line) }}"
        props:
          - pci=noaer

gives

ok: [localhost] => (item=/vmlinuz-2.6.32-279.22.1.el6.x86_64) => 
  msg:
  - kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro root=/dev/sda3
  - - pci=noaer
ok: [localhost] => (item=/vmlinuz-2.6.32-279.22.1.el6.x86_64) => 
  msg:
  - kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro recovery root=/dev/sda3
  - - pci=noaer
ok: [localhost] => (item=/vmlinuz-2.6.32-279.22.2.el6.x86_64) => 
  msg:
  - kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro root=/dev/sda3
  - - pci=noaer
ok: [localhost] => (item=/vmlinuz-2.6.32-279.22.2.el6.x86_64) => 
  msg:
  - kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro recovery root=/dev/sda3
  - - pci=noaer

If the configuration seems to be working use the module replace in check mode

    - replace:
        path: grub.cfg
        regexp: "{{ item }}"
        replace: "{{ (line + missing)|join(' ') }}"
      loop: "{{ grub.stdout_lines|select('match', match) }}"
      loop_control:
        label: "{{ line.1 }}"
      when: missing|length > 0
      vars:
        match: '^\s*kernel /vmlinuz.+$'
        line: "{{ item.split() }}"
        missing: "{{ props|difference(line) }}"
        props:
          - pci=noaer

gives

--- before: grub.cfg
+++ after: grub.cfg
@@ -1,4 +1,4 @@
-kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro root=/dev/sda3
+kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro root=/dev/sda3 pci=noaer
 kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro recovery root=/dev/sda3
 kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro root=/dev/sda3
 kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro recovery root=/dev/sda3

changed: [localhost] => (item=/vmlinuz-2.6.32-279.22.1.el6.x86_64)
--- before: grub.cfg
+++ after: grub.cfg
@@ -1,4 +1,4 @@
 kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro root=/dev/sda3
-kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro recovery root=/dev/sda3
+kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro recovery root=/dev/sda3 pci=noaer
 kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro root=/dev/sda3
 kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro recovery root=/dev/sda3

changed: [localhost] => (item=/vmlinuz-2.6.32-279.22.1.el6.x86_64)
--- before: grub.cfg
+++ after: grub.cfg
@@ -1,4 +1,4 @@
 kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro root=/dev/sda3
 kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro recovery root=/dev/sda3
-kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro root=/dev/sda3
+kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro root=/dev/sda3 pci=noaer
 kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro recovery root=/dev/sda3

changed: [localhost] => (item=/vmlinuz-2.6.32-279.22.2.el6.x86_64)
--- before: grub.cfg
+++ after: grub.cfg
@@ -1,4 +1,4 @@
 kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro root=/dev/sda3
 kernel /vmlinuz-2.6.32-279.22.1.el6.x86_64 ro recovery root=/dev/sda3
 kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro root=/dev/sda3
-kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro recovery root=/dev/sda3
+kernel /vmlinuz-2.6.32-279.22.2.el6.x86_64 ro recovery root=/dev/sda3 pci=noaer

changed: [localhost] => (item=/vmlinuz-2.6.32-279.22.2.el6.x86_64)

Run the play if this is what you want.


In case of grub2 you should modify GRUB_CMDLINE_LINUX or GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub and notify handler update-grub. For example

tasks:
  - name: Configure /etc/default/grub
    ansible.builtin.lineinfile:
      dest: /etc/default/grub
      regexp: '^\s*{{ item.var }}\s*=(.*)$'
      line: "{{ item.var }}={{ item.value }}"
      backup: true
    loop:
      - var: GRUB_CMDLINE_LINUX_DEFAULT
        value: "quiet splash pci=noaer"
    notify: update grub

handlers:
  - name: update grub
    ansible.builtin.command:
      cmd: >
        /usr/sbin/update-grub

Fit the configuration data to your needs.

3
  • I appreciate you taking the time to answer. That might work if it were a grub2 system, and every system I was dealing with was expected to have the same kernel configuration parameters. But doesn't really apply in my case, unfortunately. I'll try to keep this solution in mind for my grub2 systems, though, as it could have a good use there.
    – ahuie
    Commented Aug 3, 2021 at 22:20
  • I've added a regexp solution. Commented Aug 4, 2021 at 8:33
  • Thank you. That solution appears to work perfectly! And I appreciate the guidance on how to do testing and debugging. That will definitely help going forward!
    – ahuie
    Commented Aug 9, 2021 at 15:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.