Skip to content

CRAFT SUBLIMETEXT SNIPPETS FOR ANSIBLE USER MANAGEMENT MODULES

Overview

Configuring user accounts and groups via Ansible is a common task, yet it involves a number of properties to remember.

To facilitate writing those portions of playbooks, the proposal here is to craft snippets for our favorite code editor.

Now we need to make some tech choices, my general use case is mostly to configure Linux servers from a macOS workstation, using SublimeText as code editor (abbreviated ST hereafter.) We will exclusively target Linux distributions here — configuring macOS users with Ansible has other subtleties not covered here. For testing, we're going to deploy a VM using Parallels Desktop as type-2 hypervisor, a common option on macOS.

Preparation

Create Test Environment

In Parallels Desktop, create a new Ubuntu VM, make sure to switch the network adapter to bridged mode so that our host can reach the guest out of the box.

Notice the default user created by Parallels Desktop is called "parallels", if you're using another virtualization solution, adapt the value accordingly.

Once the Ubuntu guest is up, open a terminal and get its IP address (hereafter denoted $GUEST_IP) with the following command:

# @guest
ip a s

From your host do a quick connectivity test:

# @host
ping -c1 $GUEST_IP

Copy your SSH public key to allow for password-less authentication:

# @host
ssh-copy-id parallels$@$GUEST_IP

Still on the host, create a test folder:

# @host
mkdir test

Populate it with two files for Ansible:

An inventory, inventory.yml:

---
all:
  hosts:
    $GUEST_IP:
      ansible_user: parallels

A dummy playbook, playbook.yml:

---
- hosts: all
  tasks:
    - ping:

You can test the playbook with the following command:

# @host
ansible-playbook -i inventory.yml playbook.yml -K

-K will prompt you for the guest user password which is needed with sudo for privilege escalation.

The output should look like this:

BECOME password:

PLAY [all] *************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************
ok: [$GUEST_IP]

TASK [ping] ************************************************************************************************************
ok: [$GUEST_IP]

PLAY RECAP *************************************************************************************************************
$GUEST_IP             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Review ST Snippets

This topic is detailed here, but as a quick summary:

  • On macOS, custom ST snippets are stored in ~/Library/Application Support/Sublime Text/Packages/User;

  • They are XML files and have the .sublime-snippet extension.

  • A snippet defines:

    • Some content, obviously; an interesting feature is the ability to define fields, i.e., point of customization you can cycle through once the snippet is interpolated
    • Keyword(s) triggering the interpolation of the snippet, notice that multiple snippets can have the same trigger, in which case ST will popup a menu, the snippet description will be displayed in that menu
    • A scope, i.e. the kind of files the snippet can be applied to (e.g. a "C for loop" snippet for C files)

Review Ansible Modules

Craft Ansible Snippets

Ansible tasks describe, ideally, a target state, so the filenames here reflect that.

Group is Present

ansible_linux_group_present.sublime-snippet
<snippet>
    <content><![CDATA[
- name: "${1:Linux group is present}"
  become: true
  group:
    name: "${2:GROUP_NAME}"
    ${3:gid: $4}
    state: "present"
    system: ${5:false}
$0
]]></content>
    <description>Linux group is present</description>
    <tabTrigger>group</tabTrigger>
    <scope>source.yaml</scope>
</snippet>

User is Present

ansible_linux_user_present.sublime-snippet
<snippet>
    <content><![CDATA[
- name: "${1:Linux user is present}"
  become: true
  user:
    name: "${2:USER_NAME}"
    ${3:group: "$4" # primary group name}
    ${5:groups: "$6" # comma-separated list of secondary group names
    append: false}
    ${7:uid: $8}
    state: "present"
    system: ${9: false}
$0
]]></content>
    <description>Linux user is present</description>
    <tabTrigger>user</tabTrigger>
    <scope>source.yaml</scope>
</snippet>

User is Disabled

Usage example: off-boarding a user in a corporate environment (but keep its data.)

ansible_linux_user_disabled.sublime-snippet
<snippet>
    <content><![CDATA[
- name: "${1:Linux user is disabled}"
  become: true
  user:
    name: "${2:USER_NAME}"
    password: "!" # WARNING! Linux only
$0
]]></content>
    <description>Linux user is disabled</description>
    <tabTrigger>user</tabTrigger>
    <scope>source.yaml</scope>
</snippet>

User is Purged

Used to purge a user in a test/development or other environment not requiring auditing.

ansible_linux_user_purged.sublime-snippet
<snippet>
    <content><![CDATA[
- name: "${1:Linux user is purged}"
  become: true
  user:
    name: "${2:USER_NAME}"
    state: "absent"
    remove: true
$0
]]></content>
    <description>Linux user is purged</description>
    <tabTrigger>user</tabTrigger>
    <scope>source.yaml</scope>
</snippet>

Group is Absent

ansible_linux_group_absent.sublime-snippet
<snippet>
    <content><![CDATA[
- name: "${1:Linux group is absent}"
  become: true
  group:
    name: "${2:GROUP_NAME}"
    state: "absent"
$0
]]></content>
    <description>Linux group is absent</description>
    <tabTrigger>group</tabTrigger>
    <scope>source.yaml</scope>
</snippet>

Test

Edit playbook.yml and uses the snippets:

  • Invoke the snippet "group is present" (start typing "group" in ST to get a pop-up, select the appropriate entry then press tab to interpolate), cycle through the fields:
    • replace GROUP_NAME by "sales"
    • on gid:, press shift+ctrl+k to delete the line as we let the system assign a gid
  • Invoke the snippet "user is present" (start typing "user" then proceed as above), cycle through the fields:
    • replace USER_NAME by "jdoe"
    • on group:, press shift+ctrl+k
    • on groups:, press tab and set the value to "sales"
    • on uid:, press shift+ctrl+k
  • Add some logging to print the user groups:
    - command: "id jdoe"
      register: cmd
    
    - debug:
      var: cmd.stdout
    
  • Invoke the snippet "user is purged", cycle through the fields:
    • replace USER_NAME by "jdoe"
  • Invoke the snippet "group is absent", cycle through the fields:
    • replace GROUP_NAME by "sales"

Run the playbook as explained earlier, the expected output should look like this:

BECOME password:

PLAY [all] *************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************
ok: [$GUEST_IP]

TASK [ping] ************************************************************************************************************
ok: [$GUEST_IP]

TASK [Linux group is present] ******************************************************************************************
changed: [$GUEST_IP]

TASK [Linux user is present] ********************************************************************************
changed: [$GUEST_IP]

TASK [command] *********************************************************************************************************
changed: [$GUEST_IP]

TASK [debug] ***********************************************************************************************************
ok: [$GUEST_IP] => {
    "cmd.stdout": "uid=1001(jdoe) gid=1002(jdoe) groups=1002(jdoe),1001(sales)"
}

TASK [Linux user is purged] ********************************************************************************************
changed: [$GUEST_IP]

TASK [Linux group is absent] *******************************************************************************************
changed: [$GUEST_IP]

PLAY RECAP *************************************************************************************************************
$GUEST_IP             : ok=8    changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Appendix: Configuration Keys

The Ansible modules allow to update a small subset of configuration keys:

File Configuration Key Ansible Module Attribute
/etc/login.defs group:
GID_MIN gid_min:
GID_MAX gid_max:
user:
UID_MIN uid_min:
UID_MAX uid_max: