Ansible: append to config line when entry is not present

I am trying to automate a config entry modification. I have AIX servers which have a file login.cfg and there is a line on which available shells are configured. It is like this:

usw:
        shells = /bin/sh,/bin/bsh,/bin/csh,/bin/ksh,/bin/tsh,/bin/ksh93,/usr/bin/sh,/usr/bin/bsh,/usr/bin/csh,/usr/bin/ksh,/usr/bin/tsh,/usr/bin/ksh93,/usr/bin/rksh,/usr/bin/rksh93,/usr/sbin/uucp/uucico,/usr/sbin/snappd,/usr/sbin/sliplogin

        maxlogins = 32767
        logintimeout = 60

My goal is to append ,/usr/bin/bash to the end of the shells line, when it is not already present. The order of shells is not uniform among the hosts, for a reason.

My attempt to achieve this modification is like this snippet below – for testing purposes I just edit login.cfg locally. It has two steps: first testing if bash is already present and then edit the line if not.

---
- name: test adding /bin/bash to the end of the line IF NOT THERE
  hosts: localhost
  tasks:
  - name: Check if bash is already there
    shell: "grep -E 'shells = .*/usr/bin/bash' {{ playbook_dir }}/login.cfg"
    ignore_errors: yes
    register: result
  - name: add entry if not there
    lineinfile:
      backrefs: yes
      line: 'g<list>,/usr/bin/bash'
      path: "{{ playbook_dir }}/login.cfg"
      regexp: "(?P<list> +shells =.*)"
    when: result.rc != 0

My question is if there is a method to avoid the testing using shell: module. Is there any more Ansible-ish way to do the same?

2

Let’s parse the content of the file first. For example, if you replace the equal signs with colons you get YAML

  _regex: 's*='
  _replace: ':'
  login_dict: "{{ lookup('file', 'login.cfg') |
                  regex_replace(_regex, _replace) |
                  from_yaml }}"

gives

  login_dict:
    usw:
      logintimeout: 60
      maxlogins: 32767
      shells: /bin/sh,/bin/bsh,/bin/csh,/bin/ksh,/bin/tsh,/bin/ksh93,/usr/bin/sh,/usr/bin/bsh,/usr/bin/csh,/usr/bin/ksh,/usr/bin/tsh,/usr/bin/ksh93,/usr/bin/rksh,/usr/bin/rksh93,/usr/sbin/uucp/uucico,/usr/sbin/snappd,/usr/sbin/sliplogin

Split the shells

 shells: "{{ login_dict.usw.shells | split(',') }}"

gives

  shells:
  - /bin/sh
  - /bin/bsh
  - /bin/csh
  - /bin/ksh
  - /bin/tsh
  - /bin/ksh93
  - /usr/bin/sh
  - /usr/bin/bsh
  - /usr/bin/csh
  - /usr/bin/ksh
  - /usr/bin/tsh
  - /usr/bin/ksh93
  - /usr/bin/rksh
  - /usr/bin/rksh93
  - /usr/sbin/uucp/uucico
  - /usr/sbin/snappd
  - /usr/sbin/sliplogin

and, as required, “append /usr/bin/bash to the end of the shells line, when it is not already present

  shells_append: /usr/bin/bash
  shells_update: "{{ (shells_append in shells) |
                     ternary(shells, shells + [shells_append]) |
                     join(',') }}"

Use shells_update in the module lineinfile

    - name: add entry if not there
      lineinfile:
        backrefs: true
        path: login.cfg
        regexp: '^(s*shells)s*=.*$'
        line: '1 = {{ shells_update }}'

Optionally, you can update the dictionary

  usw_update:
    usw:
      shells: "{{ shells_update }}"
  login_update: "{{ login_dict | combine(usw_update, recursive='true') }}"

gives

  login_update:
    usw:
      logintimeout: 60
      maxlogins: 32767
      shells: /bin/sh,/bin/bsh,/bin/csh,/bin/ksh,/bin/tsh,/bin/ksh93,/usr/bin/sh,/usr/bin/bsh,/usr/bin/csh,/usr/bin/ksh,/usr/bin/tsh,/usr/bin/ksh93,/usr/bin/rksh,/usr/bin/rksh93,/usr/sbin/uucp/uucico,/usr/sbin/snappd,/usr/sbin/sliplogin,/usr/bin/bash

and create a template

    - debug:
        msg: |
          {% for section,conf in login_update.items() %}
          {{ section }}:
          {% for k,v in conf.items() %}
                  {{ k }} = {{ v }}
          {% endfor %}
          {% endfor %}

gives

  msg: |-
    usw:
            shells = /bin/sh,/bin/bsh,/bin/csh,/bin/ksh,/bin/tsh,/bin/ksh93,/usr/bin/sh,/usr/bin/bsh,/usr/bin/csh,/usr/bin/ksh,/usr/bin/tsh,/usr/bin/ksh93,/usr/bin/rksh,/usr/bin/rksh93,/usr/sbin/uucp/uucico,/usr/sbin/snappd,/usr/sbin/sliplogin,/usr/bin/bash
            maxlogins = 32767
            logintimeout = 60

Copy the content to the file

    - name: copy content
      copy:
        dest: login.cfg
        content: |
          {% for section,conf in login_update.items() %}
          {{ section }}:
          {% for k,v in conf.items() %}
                  {{ k }} = {{ v }}
          {% endfor %}
          {% endfor %}

Both tasks are idempotent and give the same result. There might be a systemic option on how to parse this format in AIX.


Example of a complete playbook for testing

- hosts: localhost

  vars:

    _regex: 's*='
    _replace: ':'
    login_dict: "{{ lookup('file', 'login.cfg') |
                    regex_replace(_regex, _replace) |
                    from_yaml }}"

    shells: "{{ login_dict.usw.shells | split(',') }}"
    shells_append: /usr/bin/bash
    shells_update: "{{ (shells_append in shells) |
                       ternary(shells, shells + [shells_append]) |
                       join(',') }}"

    usw_update:
      usw:
        shells: "{{ shells_update }}"
    login_update: "{{ login_dict | combine(usw_update, recursive='true') }}"
    

  tasks:

    - debug:
        var: login_dict

    - debug:
        var: shells

    - debug:
        var: shells_update

    - name: add entry if not there
      lineinfile:
        backrefs: true
        path: login.cfg
        regexp: '^(s*shells)s*=.*$'
        line: '1 = {{ shells_update }}'
      when: lineinfile | d(false) | bool


    - debug:
        var: usw_update

    - debug:
        var: login_update

    - debug:
        msg: |
          {% for section,conf in login_update.items() %}
          {{ section }}:
          {% for k,v in conf.items() %}
                  {{ k }} = {{ v }}
          {% endfor %}
          {% endfor %}

    - name: copy content
      copy:
        dest: login.cfg
        content: |
          {% for section,conf in login_update.items() %}
          {{ section }}:
          {% for k,v in conf.items() %}
                  {{ k }} = {{ v }}
          {% endfor %}
          {% endfor %}
      when: content | d(false) | bool

The optimized play below will skip the task if the shell is in the list

- hosts: localhost

  vars:

    _regex: 's*='
    _replace: ':'
    login_dict: "{{ lookup('file', 'login.cfg') |
                    regex_replace(_regex, _replace) |
                    from_yaml }}"

    shells: "{{ login_dict.usw.shells | split(',') }}"
    shells_append: /usr/bin/bash
    

  tasks:

    - name: add entry if not there
      lineinfile:
        backrefs: true
        path: login.cfg
        regexp: '^(s*shells)s*=.*$'
        line: '1 = {{ (shells + [shells_append]) | join(",") }}'
      when: shells_append not in shells

This can be further simplified

- hosts: localhost

  vars:

    _regex: 's*='
    _replace: ':'
    login_conf: "{{ lookup('file', 'login.cfg') |
                    regex_replace(_regex, _replace) |
                    from_yaml }}"
    shells: "{{ login_conf.usw.shells }}"
    shells_append: /usr/bin/bash
    

  tasks:

    - name: add entry if not there
      lineinfile:
        backrefs: true
        path: login.cfg
        regexp: '^(s*shells)s*=.*$'
        line: '1 = {{ [shells, shells_append] | join(",") }}'
      when: shells_append not in shells

1

One quick and cheap way to do it could be the following:

First, it would be necessary to gather facts about the Remote Nodes. Even if it is not the best way it could simply be done via a minimal example

---
- hosts: test
  become: false
  gather_facts: false

  vars:

    FACT: "shells"
    SEARCH_STRING: "/usr/bin/bash"
    SEARCH_FILE: "login.cfg"

  tasks:

  # grep string from Remote File

  - name: Gathering Custom Facts
    shell:
      cmd: "grep {{ FACT }} {{ SEARCH_FILE }} | tr -d ' ' | cut -d '=' -f 2"
    register: shells
    # Since this is a reporting, gathering facts task
    # it needs to deliver a result in any case
    failed_when: shells.rc != 0 and shells.rc != 1
    check_mode: false
    changed_when: false

  - name: Show available remote shells
    debug:
      msg: "{{ shells.stdout }}"
    when: ansible_check_mode

resulting into an output of

TASK [Show available remote shells] *********************************************************************************************************************************
ok: [test.example.com] =>
  msg: /bin/sh,/bin/bsh,/bin/csh,/bin/ksh,/bin/tsh,/bin/ksh93,/usr/bin/sh,/usr/bin/bsh,/usr/bin/csh,/usr/bin/ksh,/usr/bin/tsh,/usr/bin/ksh93,/usr/bin/rksh,/usr/bin/rksh93,/usr/sbin/uucp/uucico,/usr/sbin/snappd,/usr/sbin/sliplogin

Q: “Is there a method to avoid the testing using the shell module?

A better approach is shown under How to search for a string in a Remote File using Ansible?, Ansible: How to append an option? or just use Custom facts directly.

Second, append entry if it is not contained already

  - name: Append entry if it is not contained already
    lineinfile:
      path: "{{ SEARCH_FILE }}"
      regexp: ".*{{ FACT }} =.*"
      line: "        {{ FACT }} = {{ shells.stdout }},{{ SEARCH_STRING }}"
    when: not shells.stdout is search(SEARCH_STRING)

My main question was how to find out if an entry is present in a config file, without using shell: module. My answer is to use lineinfile: with check_mode: yes

By the way, up to this moment I was thinking that quotation marks and apostrophes are interchangeable in Ansible playbooks. The experience shows that they are not. The line: row, which contains regex bacrefs, gave me syntax error with quotation marks, but is working fine with apostrophes

---
- name: testing the behavior of check_mode option
  hosts: localhost
  vars:
    searchstr: "/usr/bin/bash"
    logincfgfile: "{{ playbook_dir }}/login.cfg"
  become: no
  tasks:
  - name: check if line is present
    lineinfile:
      path: "{{ logincfgfile }}"
      regex: "shells = .*{{ searchstr }}.*"
      state: absent
    check_mode: yes
    register: entrypresent
  - name: add shell entry if it was not present
    lineinfile:
      backrefs: yes
      path: "{{ logincfgfile }}"
      regex: "(?P<shellsline> *shells = .*)"
      line: 'g<shellsline>,{{ searchstr }}'
      # line: "g<shellsline>,{{ searchstr }}"    # not working with quotation marks
    when: entrypresent.found == 0

For the sake of simplicity I am using a local file to experiment on. This is defined in logincfgfile. I am trying do delete the line with the shell definitions, including the shell to be added, but I do it in check_mode, so there is no real change to the file, just collecting data – and registering into entrypresent variable. The output of lineinfile has a field, called found, which holds the number of matches. If it is zero, thus no matches, the next task adds the new shell entry.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật