204

The same way there is a module lineinfile to add one line in a file, is there a way to add several lines?

I do not want to use a template because you have to provide the whole file. I just want to add something to an existing file without necessarily knowing what the file already contains so a template is not an option.

Baptiste Mille-Mathias
  • 2,144
  • 4
  • 31
  • 37
Michael
  • 8,357
  • 20
  • 58
  • 86
  • I understand you don't want to use `template`, but using `lineinfile` is an [antipattern](https://en.wikipedia.org/wiki/Anti-pattern). It's also a strong red flag that you "don't know what is in the file", which leads to substantial risk of unknown failures. – tedder42 Jun 20 '14 at 21:22
  • 52
    It's not an anti-pattern. The point of lineinfile is to support multiple sources managing the same file, which is sometimes unavoidable. Most configuration files have a fixed format and logic to avoid conflicts is usually not too substantial. – Doug F Nov 20 '14 at 20:26
  • I don't know what's in the vast majority of files on my PC; doesn't mean I want to nuke them all! – DylanYoung Nov 15 '18 at 16:24
  • You could also use "\n" – John Ronald Sep 01 '22 at 09:35

10 Answers10

286

You can use the lineinfile built-in in a loop. Here's an example:

- name: Set some kernel parameters
  lineinfile:
    dest: /etc/sysctl.conf
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
  loop:
    - { regexp: '^kernel.shmall', line: 'kernel.shmall = 2097152' }
    - { regexp: '^kernel.shmmax', line: 'kernel.shmmax = 134217728' }
    - { regexp: '^fs.file-max', line: 'fs.file-max = 65536' }
Ben Whaley
  • 32,811
  • 7
  • 87
  • 85
  • 1
    MAKE SURE you have the argument to line= and regexp= *in quotes*. I did not, and I kept getting `msg: this module requires key=value arguments`. The example given does have this correct -- I just didn't follow the example. – JDS Nov 10 '14 at 20:01
  • 1
    May I ask how to do a single backup before the the first change? maybe item.backup? :D – tdihp Oct 27 '16 at 12:18
  • 10
    This was probably voted up before Ansible 2.0. A better answer is now: https://stackoverflow.com/a/28306576/972128 – kkurian Jul 05 '17 at 20:10
  • 2
    @kkurian Surely only if you're inserting, not if you're replacing? – ndtreviv Jun 01 '18 at 12:11
  • 10
    @kkurian The blockinfile solution will not work if you e.g. need to add some lines to a json file and do not want any markers. While you can set markers to "", ansible blockinfile will still look for markers, not find any, and insert the block again. Thus, blockinfile without markers is not idempotent, lineinfile with a loop is. – absurd Sep 04 '18 at 13:16
  • 1
    Worth mentioning that the regexp argument to lineinfile is optional. It'll just fallback to a simple string search. – Dustin Oprea Feb 05 '21 at 07:18
  • Change `loop:` to `with_items:` for newer version of Ansible – micksatana Oct 23 '21 at 11:45
  • As noted in [the docs](https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html), `loop` is the preferred syntax, although `with_*` remains valid. This solution works with either syntax. – Ben Whaley Nov 10 '21 at 22:23
234

You can try using blockinfile instead.

You can do something like

- blockinfile: |
    dest=/etc/network/interfaces backup=yes
    content="iface eth0 inet static
        address 192.168.0.1
        netmask 255.255.255.0"
Boden Garman
  • 2,455
  • 2
  • 20
  • 17
Soichi Hayashi
  • 3,384
  • 2
  • 22
  • 15
31

Here is a noise-free version of the solution which is to use with_items:

- name: add lines
  lineinfile: 
    dest: fruits.txt
    line: '{{ item }}'
  with_items:
    - 'Orange'
    - 'Apple'
    - 'Banana' 

For each item, if the item exists in fruits.txt no action is taken.

If the item does not exist it will be appended to the end of the file.

Easy-peasy.

Joshua Grigonis
  • 748
  • 5
  • 18
Rick O'Shea
  • 1,410
  • 19
  • 15
25

If you need to configure a set of unique property=value lines, I recommend a more concise loop. For example:

- name: Configure kernel parameters
  lineinfile:
    dest: /etc/sysctl.conf
    regexp: "^{{ item.property | regex_escape() }}="
    line: "{{ item.property }}={{ item.value }}"
  with_items:
    - { property: 'kernel.shmall', value: '2097152' }
    - { property: 'kernel.shmmax', value: '134217728' }
    - { property: 'fs.file-max', value: '65536' }

Using a dict as suggested by Alix Axel and adding automatic removing of matching commented out entries,

- name: Configure IPV4 Forwarding
  lineinfile:
    path: /etc/sysctl.conf
    regexp: "^#? *{{ item.key | regex_escape() }}="
    line: "{{ item.key }}={{ item.value }}"
  with_dict:
    'net.ipv4.ip_forward': 1
Nicholas Sushkin
  • 13,050
  • 3
  • 30
  • 20
5

It's not ideal, but you're allowed multiple calls to lineinfile. Using that with insert_after, you can get the result you want:

- name: Set first line at EOF (1/3)
  lineinfile: dest=/path/to/file regexp="^string 1" line="string 1"
- name: Set second line after first (2/3)
  lineinfile: dest=/path/to/file regexp="^string 2" line="string 2" insertafter="^string 1"
- name: Set third line after second (3/3)
  lineinfile: dest=/path/to/file regexp="^string 3" line="string 3" insertafter="^string 2"
Ramon de la Fuente
  • 8,044
  • 3
  • 32
  • 31
  • 5
    yes but it's still one line at a time. If I have 15 lines, I would prefer add them with only one command. It does not seem to be possible. – Michael Jun 20 '14 at 20:44
5

I was able to do that by using \n in the line parameter.

It is specially useful if the file can be validated, and adding a single line generates an invalid file.

In my case, I was adding AuthorizedKeysCommand and AuthorizedKeysCommandUser to sshd_config, with the following command:

- lineinfile: dest=/etc/ssh/sshd_config line='AuthorizedKeysCommand /etc/ssh/ldap-keys\nAuthorizedKeysCommandUser nobody' validate='/usr/sbin/sshd -T -f %s'

Adding only one of the options generates a file that fails validation.

Penz
  • 5,428
  • 5
  • 31
  • 28
  • 12
    This will create the line an additional time each time the playbook is run--it doesn't correctly recognize that the line already exists. At least, that's the case for me on Ansible 1.7.1 – David Oct 07 '14 at 14:13
  • 1
    I reported a [bug](https://github.com/ansible/ansible/issues/20413), but the Ansible guys have no interest to fix it. – ceving Jan 19 '17 at 22:06
  • 1
    There is a new blockinfile module that should be better than that solution now. (http://docs.ansible.com/ansible/blockinfile_module.html) – Penz Jan 20 '17 at 11:45
2

To add multiple lines you can use blockfile:

- name: Add mappings to /etc/hosts
  blockinfile:
    path: /etc/hosts
    block: |
      '10.10.10.10  server.example.com'
      '10.10.10.11  server1.example.com'

to Add one line you can use lininfile:

- name: server.example.com in /etc/hosts
  lineinfile:
    path: /etc/hosts
    line: '192.0.2.42 server.example.com server'
    state: present
Ahmed Taha
  • 21
  • 1
2

To add multiple lines you can use lineinfile module with with_items also including variable vars here to make it simple :)

---
- hosts: localhost  #change Host group as par inventory
  gather_facts: no
  become: yes
  vars:
    test_server: "10.168.1.1"
    test_server_name: "test-server"
    file_dest: "/etc/test/test_agentd.conf"

  - name: configuring test.conf
    lineinfile:
      dest: "{{ item.dest }}"
      regexp: "{{ item.regexp }}"
      line: "{{ item.line }}"
    with_items:
      - { dest: '"{{ file_dest }}"', regexp: 'Server=', line: 'Server="{{test_server}}"' }
      - { dest: '"{{ file_dest }}"', regexp: 'ServerActive=', line: 'ServerActive="{{test_server}}"' }
      - { dest: '"{{ file_dest }}"', regexp: 'Hostname=', line: 'Hostname="{{test_server_name}}"' }
1

To add multiple Lines in a configuration file you can use " " instead of ' ' and escape sequence \n for the new line in lineinfile ansible module:

- name: Configure core-site.xml
  lineinfile:
    path: /etc/hadoop/core-site.xml
    insertafter: '^<configuration>'
    line: "Line 1 \n Line 2 \n Line 3"
MattAllegro
  • 6,455
  • 5
  • 45
  • 52
0

The solution that fit my use-case (network automation):

- name: "create block in own file per host"
  blockinfile:
     path: "/dir/logs/{{ inventory_hostname }}.txt"
     create: true
     block: "some commands"
     ...

- name: "add more line in block (even if already executed on another host)"
  lineinfile:
     line: "{{ item }}"
     ...
  loop: "{{ more_commands_1 | default([]) +
    more_commands_2 | default([]) }}" 

- name: "assemble all files in one"
  assemble:
    src: "/dir/logs/"
    dest: "/dir/logs/all_hosts.txt"
    ...

note: I used those modules with "check_mode: false" and "delegate_to: localhost"

Would be glad to use a more clever solution if it exists.

tintin
  • 1
  • 2