5

I am modifying /boot/cmdline.txt to add container features to a Raspberry Pi, so I need to add cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory into the file, within the same line.

I am trying to do it with the lineinfile module without much success:

- hosts: mypi
  become: yes
  tasks:
  - name: Enable container features
    lineinfile:
      path: /boot/cmdline.txt
      regex: " cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory"
      line: " cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory"
      insertafter: EOF
      state: present

I have been trying modifying the insertafter to BOF, using insertbefore too, using a regex to match the last word... But it ends up adding a carriage return. I have been unable to find some way to not add a new line.

Oliver Blaha
  • 98
  • 1
  • 4
Geiser
  • 1,054
  • 1
  • 12
  • 28
  • I do not quite understand. You want to add those three values to the beginning of the first line in the file? – Jack May 23 '20 at 13:39
  • I do not mind. If you check /boot/cmdline.txt it is just contains key-value pairs in the same line, so I don't mind whether they are be in the end or at the beginning. The only requirement is that they should be whitespaced. This is an example of my Pi: `console=serial0,115200 console=tty1 root=PARTUUID=ea7d04d6-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait` – Geiser May 23 '20 at 14:08
  • Just the one line in the file? – Jack May 23 '20 at 14:13

4 Answers4

9

Since you only have the one line in the file, you can do that with either replace or lineinfile. Here is the replace version:

  - name: Enable container features
    replace:
      path: cmdline.txt
      regexp: '^([\w](?!.*\b{{ item }}\b).*)$'
      replace: '\1 {{ item }}'
    with_items:
    - "cgroup_enable=cpuset"
    - "cgroup_memory=1"
    - "cgroup_enable=memory"

Stole the answer from here

Jack
  • 5,801
  • 1
  • 15
  • 20
8

As Vladimir pointed out, Jack's answer unfortunately is not sufficient for empty files and also fails if the desired argument already exists at the beginning of the line. The following suggested solution should address those issues. In particular, it is supposed to

  • support empty files,
  • support existing arguments at any position within the string,
  • be robust even with multi-line files (just in case...),
  • be idempotent, and
  • optionally update existing keys with the desired value.

Original version May 2021:

# cmdline.yml

- name: read cmdline.txt
  become: true
  slurp: "src={{ cmdline_txt_path }}"
  register: result_cmdline

- name: generate regular expression for existing arguments
  set_fact:
    regex_existing: '{{ "\b" + key|string + "=" + ("[\w]*" if update else value|string) + "\b" }}'
    key_value_pair: '{{ key|string + "=" + value|string }}'

- name: generate regular expression for new arguments
  set_fact:
    regex_add_missing: '{{ "^((?!(?:.|\n)*" + regex_existing + ")((?:.|\n)*))$" }}'

- name: update cmdline.txt
  become: true
  copy:
    content: '{{ result_cmdline.content
        | b64decode
        | regex_replace(regex_existing, key_value_pair)
        | regex_replace(regex_add_missing, key_value_pair + " \1")
      }}'
    dest: "{{ cmdline_txt_path }}"

Updated version August 2023:

Adds support for keys without values and allows removing keys.

# cmdline.yml

- name: read cmdline.txt
  slurp: "src={{ cmdline_txt_path }}"
  register: result_cmdline

- name: generate regular expression for existing arguments
  set_fact:
    regex_existing: '{{ "\b" + key|string + "(?:=" + ("[\w]*" if update|default(true) else value|string) + ")?(?![\w=])" }}'
    key_value_pair: '{{ "" if remove|default(false) else ("" + key|string + (("=" + value|string) if value is defined else "")) }}'

- name: generate regular expression for new arguments
  set_fact:
    regex_add_missing: '{{ "^((?!(?:.|\n)*" + regex_existing + ")((?:.|\n)*))$" }}'

- name: update cmdline.txt
  copy:
    content: '{{ result_cmdline.content
        | b64decode
        | regex_replace(regex_existing, key_value_pair)
        | regex_replace(regex_add_missing, key_value_pair + ("\1" if remove|default(false) else " \1"))
        | regex_replace("\s+", " ") | trim # in case you don't like extra whitespaces
      }}'
    dest: "{{ cmdline_txt_path }}"

Usage:

- set_fact:
    cmdline_txt_path: /boot/cmdline.txt

- include_tasks: cmdline.yml
  vars:
    key: cgroup_enable
    value: memory
    update: false
    # will add the argument if the key-value-pair doesn't exist

- include_tasks: cmdline.yml
  vars:
    key: cgroup_enable
    value: cpu
    update: false

- include_tasks: cmdline.yml
  vars:
    key: cgroup_memory
    value: 1
    update: true
    # will replace the value of the first matching key, if found;
    # will add it if it's not found

# The following examples need the updated version from August 2023:

- include_tasks: cmdline.yml
  vars:
    key: quiet
    # will add key without "=" and without value if not present

- include_tasks: cmdline.yml
  vars:
    key: quiet
    remove: true
    # will remove all matching keys

I didn't have a lot of time to test the updated version. Feel free to try it out and give feedback. Thanks!

Oliver Blaha
  • 98
  • 1
  • 4
  • 1
    This works brilliantly. One thing to call out as it very briefly caught me out is that the two cgroup_enable's _must_ be `update: false` otherwise they overwrite each other (its not just to demondstrate that it _can_ be false ‍♂️). – DanielM Jun 29 '21 at 22:21
  • Unfortunately this solution does not support keys without a value, for example `quiet`. – Björn Kahlert Aug 21 '23 at 21:58
  • Thanks Björn, I've added an updated version that should cover your requirement. Please let me know if it works for you. – Oliver Blaha Aug 24 '23 at 19:19
2

Q: "Ansible lineinfile module: Do not add new line. Find some way to not add a new line."

A: It's not possible. New line will be always added by module lineinfile. See source for example

b_lines.insert(index[1], b_line + b_linesep)

This is how a new line is added. Such additions will be terminated with b_linesep. See how the variable is defined

b_linesep = to_bytes(os.linesep, errors='surrogate_or_strict')

The os.linesep is used when you want to iterate through the lines of a text file. The internal scanner recognizes the os.linesep and replaces it with a single "\n".

See What is os.linesep for?.


The task with the module replace doesn't solve this problem either. Neither it creates the line without a newline, nor it modifies existing one this way. In addition to this it's not idempotent.
  - name: Enable container features
    replace:
      path: cmdline.txt
      regexp: '^([\w](?!.*\b{{ item }}\b).*)$'
      replace: '\1 {{ item }}'
    loop:
      - "cgroup_enable=cpuset"
      - "cgroup_memory=1"
      - "cgroup_enable=memory"

It will do nothing if the file is empty

TASK [Enable container features]
ok: [localhost] => (item=cgroup_enable=cpuset)
ok: [localhost] => (item=cgroup_memory=1)
ok: [localhost] => (item=cgroup_enable=memory)

If the line is present in the file this task will change it

shell> cat cmdline.txt 
cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory
ASK [Enable container features] *****************************************
ok: [localhost] => (item=cgroup_enable=cpuset)
--- before: cmdline.txt
+++ after: cmdline.txt
@@ -1 +1 @@
-cgroup_memory=1 cgroup_enable=memory cgroup_enable=cpuset
+cgroup_memory=1 cgroup_enable=memory cgroup_enable=cpuset cgroup_memory=1

Vladimir Botka
  • 58,131
  • 4
  • 32
  • 63
2

I followed all of the different strategies laid out above, but in the end, I wanted something simple, as this is my first playbook, and I need to understand it now, and when I pick it up again later,.

My cmdline.txt contained multiple lines:

cat /boot/cmdline.txt -E 
console=serial0,115200 console=tty1 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=memory cgroup_enable=cpuset cgroup_memory=1$
dtoverlay=pi3-disable-bt$ 
dtoverlay=pi3-disable-wifi$

So, the approach I was looking for:

  • Would ignore the other configurations in the cmdline.txt
  • Would only add the add a specific key=value if it was missing
  • It had to be idempotent

I settled on a simple regex to decide if this was the row I wanted to edit:

  • If the row contaied console= (as this is the row I'm after)
  • AND.. If the row does not contain cgroup_memory=1

- name: Adding cgroup_enable=memory to boot parameters for k3s
  lineinfile:
    path: /boot/cmdline.txt
    state: present
    regexp: '^((?!.*cgroup_enable=memory).*console.*)$'
    line: '\1 cgroup_enable=memory'
    backrefs: yes
  notify: reboot

- name: Adding cgroup_enable=cpuset to boot parameters for K3s
  lineinfile:
    path: /boot/cmdline.txt
    state: present
    regexp: '^((?!.*cgroup_enable=cpuset).*console.*)$'
    line: '\1 cgroup_enable=cpuset'
    backrefs: yes
  notify: reboot

- name: Adding cgroup_memory=1 to boot parameters for K3s
  lineinfile:
    path: /boot/cmdline.txt
    state: present
    regexp: '^((?!.*cgroup_memory=1).*console.*)$'
    line: '\1 cgroup_memory=1'
    backrefs: yes
  notify: reboot

And at some point in future, I'll probably condense all three of these into a single loop task. But not today.

Matt Law
  • 81
  • 3