Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ansible: append to config line when entry is not present

Tags:

regex

ansible

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?

like image 656
Trifo Avatar asked Nov 17 '25 17:11

Trifo


1 Answers

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
like image 127
Vladimir Botka Avatar answered Nov 20 '25 12:11

Vladimir Botka



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!