Getting started - Bash script actions "converted" into a playbook

Hi All,

I’m managing a set of about 60 Debian computers with a deployment script.
Purpose is to copy some system files to configure the system (login manager, …) and have a default /home/user directory (bookmarks, pinneds apps, …).

I’d like to switch to Ansible to avoid having to run the script manually or each system, and to benefit from “idempotence” (I love the word :heart_eyes:).

I’m not asking to do the job on my behalf, just would like to confirm that my script building blocks properly translate into playbook entries.

Software updates

Script

apt update
apt upgrade -y
apt install -y rsync openssh-client openssh-server net-tools
apt purge -y --autoremove gnome-games

Ansible

  tasks:
    - name: updates
      block:
        - name: update
          apt:
            update_cache: yes
        - name: upgrade
          apt:
            upgrade: dist
        - name: install
          apt:
            name:
              - rsync
              - openssh-client
              - openssh-server
              - net-tools
            state: present
        - name: remove
          apt:
            name: gnome-games
            state: absent
            autoremove: yes

Disabling passwords

Script

passwd -d guest

Ansible

    - name: Désactivation du mot de passe pour l'utilisateur guest
      user:
        name: guest
        password: ""  # Mot de passe vide

Remove some files and copy some others

Script

rm -Rf /usr/share/wayland-sessions/*
rm -Rf /usr/share/xsessions/*
rm -Rf /etc/skel/*
rm -Rf /home/guest/*
rm -Rf /home/guest/.*
rsync -az --progress --ignore-times root@$SKELETON_SERVER:$SKELETON_PATH/ /etc/skel/
rsync -az --progress --ignore-times /etc/skel/ /

Ansible

    - name: Copie du skeleton depuis le serveur
      block:
        - name: Suppression des sessions Wayland et X existantes
          file:
            path: "{{ item }}"
            state: absent
          loop:
            - /usr/share/wayland-sessions/
            - /usr/share/xsessions/
        - name: Copie du skeleton depuis le serveur distant
          synchronize:
            src: "root@{{ skeleton_server }}:{{ skeleton_path }}/"
            dest: "{{ local_dest }}etc/skel/"
            mode: push
            archive: yes
            compress: yes
        - name: Application du skeleton à la racine
          synchronize:
            src: "{{ local_dest }}etc/skel/"
            dest: "{{ local_dest }}/"
            mode: pull
            archive: yes
            compress: yes

Ownership and permission changes

Script

chown -R guest:guest /home/guest
chmod +x /usr/share/sddm/scripts/Xstop

Ansible

        - name: Changement de propriétaire pour /home/guest
          file:
            path: /home/guest
            owner: guest
            group: guest
            recurse: yes

Other actions required

  1. I don’t know how to change permissions on folders
  2. I don’t know how to invoke extra specific cxommands such as
systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
sudo dconf update #pour couvrir la partie Cinnamon
sudo update-grub
systemctl restart ssh

Can you please help me confirm that my building blocks look OK?
Are there ways to run commands such as the ones at the bottom of my list?

Ultimately, if I have a working script, should I consider copying the script and simply running it on my clients via Ansible, and avoid converting the whole script into a playbook?

Thanks a lot!

3 Likes

Hi @cndpansible

Welcome to the Ansible forum! :slightly_smiling_face:

You got a really good starting point here. Under your Ansible heading, for your playbook tasks I’d recommend using the Fully Qualified Collection Name (FQCN) for the modules used in each task, this helps with knowing which collection the modules are from and if another collection has an apt module then it avoids potential clashes in the future.

A small example:

---
- name: Example Playbook
  hosts: some_hosts
  tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true

You can find module FQCNs using the ansible-doc command like: ansible-doc apt or you can use the online doc site: ansible.builtin.apt module – Manages apt-packages — Ansible Community Documentation

  1. I don’t know how to change permissions on folders

You can use the mode parameter with the file module to change permissions (RWX):

- name: Change permissions for /home/guest
  ansible.builtin.file:
    path: /home/guest
    owner: guest
    group: guest
    recurse: true
    state: directory
    mode: u=rwx,g=rx,o=

The above mode is the equivalent of: chmod 750.

  1. I don’t know how to invoke extra specific cxommands such as
systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target

You could use the ansible.builtin.systemd_service module – Manage systemd units — Ansible Community Documentation to mask these and restart services like ssh.

Hope this helps :slightly_smiling_face: - Based on what you’ve said, I think Ansible would be ideal for this.

Here is some links which might help you further:

2 Likes

I don’t know how to invoke extra specific commands such as …

Out of mind, for the systemd services you could try

    - name: Mask service
      systemd:
        name: "{{ item }}"
        masked: true
      loop:
        - sleep.target
        - suspend.target
        - hibernate.target
        - hybrid-sleep.target

and

    - name: Restart ssh daemon
      systemd:
        name: sshd
        daemon_reload: true
        state: restarted

For dconf There is also a module ansible.builtin.debconf .

1 Like

For update-grub I think the only option is to use the command module, there is no grub module:

- name: Grub configuration updated
  ansible.builtin.command:
    cmd: update-grub
  changed_when: true

I suggest setting changed_when to true because otherwise you would probably have to do something like use the shell module and write a Bash script that writes the output somewhere on each run and compare the latest run output to the previous output to see if they differ, I don’t think there is a simply way of knowing if running update-grub changed anything as it is a command with few options:

update-grub -h
Usage: grub-mkconfig [OPTION]
Generate a grub config file

  -o, --output=FILE       output generated config to FILE [default=stdout]
  -h, --help              print this message and exit
  -V, --version           print the version information and exit

Report bugs to <bug-grub@gnu.org>.
1 Like

Thanks a million @chris @dbrennand !

So many details and pointers, I had looked at the doc, but clearly had overlooked some stuff.

Allow me to play devil’s advocate: I do see the value of Ansible, but can you please help me understand what would be the upsides and downsides of trying to “simply” trigger my deployment script via Ansible, rather than fully translating it into playbooks?

I believe this might be a “too easy way out”, and maybe I would regret it in the future, but on the short term, I feel tempted to take the shortcut :wink:

Thanks again!

1 Like

And thank you @U880D too (I could only tag 2 people per post :wink: )

1 Like

I started using Ansible to run the Bash commands I’d be using for years, it’s fine to start there! Take it one step at a time and see how you get on.

1 Like

Well, from my point of view there is no right or wrong at the beginning. Since Ansible is agentless, you are almost writing a kind of documentation upfront, which can then be read and executed by a human or by a machine, it will depend on the complexity of the tasks and its dependencies.

For simple tasks without complexity it might be OK to use command or shell. But they may lack error handling then. Also be non-technical human may have problems to understand what the task is about. If there is more complexity which needs to be abstracted, error handling necessary, or dealing with data structures and formatting output, specific Ansible modules will gain advantage fast.

An example for such could be accessing REST APIs versus command with curl or the uri module. A lot of Ansible modules are just abstractions for REST API but make it very simple the write and read them, but it will also introduce a dependency to such modules, there availability and maintenance.

For the beginning one could start with commands and introduce more and more specific modules later during daily development and operations.

2 Likes

The upside is your existing script is already written. That’s kind of a huge thing. Within the scope of your question, there hardly is any downside. You can use ansible as an “easy button” to continue doing things the way you’ve been doing them. Even so, Ansible brings some things to the table. For example, how are you deploying the script to your hosts? An Asible project can act as the canonical source for your script (or its equivalent functionality in tasks), an automatic deployment mechanism, plus a trigger to run it, along with a lot of flexibility for specifying which hosts to deploy and run it on. That’s a lot of gain simply by using Ansible to “simply” trigger your script.

The downsides for only doing that, though, include missing out on a lot of error handling. Shell scripts are notoriously difficult to write in a way that properly handles errors. It’s possible, but rarely done. Breaking out that functionality into tasks gives you that error detection almost for free. You still have to decide what to do about errors, maybe, eventually, but at least you’ll have a reasonable chance of becoming aware of them. There’s a high likelihood that you wouldn’t find out about errors in your current script until you tried to use one of these new hosts.

Beyond the scope of this script or its equivalent functionality, though, you’ll likely come up with additional things you’ll need to do on, with, or to these or other hosts. Creating additional playbooks in the project fits perfectly into Ansible’s organized framework for accomplishing “do A, B, and C to hosts X, Y, and Z”-type activities. That’s so much cleaner than adding yet another script-without-a-home that only you know about that has to be deployed and run a certain way but not on Tuesday…

You can certainly start using Ansible as a simple trigger, but it quickly becomes a solution to a range of problems you may not have realized you have. You’ll soon find you aren’t “doing things the way you’ve been doing them” any more. You’ll still accomplish those things, but within a manageable, scalable framework that a bunch of scripts lacks.

So, you’re on the right track. Keep looking for additional things that Ansible can do for you, and by all means keep asking good questions when you get stuck. We’re all here to help each other succeed.

3 Likes

Thank you so much @utoddl !
I feel that I would miss a lot if I would just use Ansible to trigger my script, but your help made me realize it’s not an “all or nothing” thing. I’ll explore and progress!
Thanks a lot to you and the splendid community :smile:

3 Likes

Hi All,

I just wanted to proudly share my current progress on script commands that were now translated into small Ansible playbooks :heart_eyes:

Sharing in case it can help other people. Thanks again for the kind and responsive Ansible community!

FYI:

  1. I will shortly regroup my tasks into a single playbook, but using many small playbooks helped me a lot to understand, write, and test each type of action.
  2. I have currently relied on ansible.builtin.command for the following actions:
  • Perform rsync (todo: investigate ansible.posix.synchronize)
  • Perform chmod and chown (todo: investigate ansible.builtin.file with owner, group, state, mode)
  • Perform sudo dconf update and sudo update-grub
  1. I still need to reflect on how to handle the end of a playbook run. When I was running the script, there was a prompt “do you want to sudo reboot?”. It helped me see if there were any errors before doing the unique reboot needed. With Ansible (made to automate, so certainly not to make interactive stuff), I wonder if I will simply rely on Ansible output and consider initiating a reboot via another tool. Any feedback is welcome!

1. Debian APT operations

Script

apt update
apt upgrade -y
apt install -y rsync openssh-client openssh-server net-tools debconf-utils sddm libreoffice-l10n-fr firefox-esr-l10n-fr
apt purge -y --autoremove gnome-games

Ansible playbook

---
- name: apt operations
  hosts: test
  tasks:
    - name: updates
      block:
        - name: update
          apt:
            update_cache: yes
        - name: upgrade
          apt:
            upgrade: dist
        - name: install
          apt:
            name:
              - rsync
              - openssh-client
              - openssh-server
              - net-tools
              - emacs
            state: present
        - name: remove
          apt:
            name: gnome-games
            state: absent
            autoremove: yes

2. Removal of user password with passwd

Script

passwd -d guest

Ansible playbook

---
- name: Change guest user password
  hosts: test
  tasks:
    - name: Define a new password for user guest (here: none)
      ansible.builtin.user:
        name: guest
        password: "{{ '' | password_hash('sha512') }}"

3. Files deletion (recursive, including directories)

Script

rm -Rf /home/guest/*
rm -Rf /home/guest/.*

Ansible playbook

---
- name: Empty directories
  hosts: test
  tasks:
    - name:  Find content of /home/guest/
      ansible.builtin.find:
        paths: /home/guest/
        hidden: yes
        recurse: yes
        file_type: any
      register: contenu_rep
    - name: Delete all found content
      ansible.builtin.file:
        path: "{{ item.path }}"
        state: absent
      loop: "{{ contenu_rep.files }}"

4. rsync

Script

rsync -az --progress --ignore-times root@192.168.64.1:/root/latest/etc/skel/ /etc/skel/
rsync -az --progress --ignore-times /etc/skel/ /

Ansible playbook

---
- name: Run rsync commands
  hosts: test
  tasks:
    - name: rsync master to client /etc/skel
      ansible.builtin.command: rsync -az --progress --ignore-times root@$192.168.64.1:.root/latest/skel/ /etc/skel/
    - name: rsync local from /etc/skel to /
      ansible.builtin.command: rsync -az --progress --ignore-times /etc/skel/ /

5. Change files permissions and ownership

Script

chown -R guest:guest /home/guest
chmod +x /usr/share/sddm/scripts/Xstop

Ansible playbook

---
- name: Fix permissions on required files
  hosts: test
  tasks:
    - name: chown on /home/guest for guest (in case rsync messed up permissions/ownership)
      ansible.builtin.command: chown -R guest:guest /home/guest
    - name: chmod +x (cross/execute) Xstop 
      ansible.builtin.command: chmod +x /usr/share/sddm/scripts/Xstop

6. Mask (disable) some systemd services

Script

systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target

Ansible playbook

---
- name: With systemctl, mask sleep related services
  hosts: test
  become: yes
  tasks:
    - name: disabled (mask) sleep related services
      ansible.builtin.systemd:
        name: "{{ item }}"
        masked: yes
      loop:
        - sleep.target
        - suspend.target
        - hibernate.target
        - hybrid-sleep.target

7. Update some system config (dconf and grub)

Script

sudo dconf update
sudo update-grub

Ansible playbook

---
- name: Update config grub et dconf
  hosts: test
  become: yes
  tasks:
    - name: Update dconf
      ansible.builtin.command: sudo dconf update
    - name: Update grub
      ansible.builtin.command: sudo update-grub

8. Restart SSH Daemon

Script

systemctl restart ssh

Ansible playbook

---
- name: Restart SSH daemon
  hosts: test
  become: yes
  tasks:
    - name: restart sshd
      ansible.builtin.systemd:
        name: sshd
        state: restarted
3 Likes

You can prompt for reboots like in the following. I included a - false in my list of conditions because I didn’t want to actually reboot my local machine. Take that line out if you really do want to reboot.

---
# pause.yml
- name: Demonstrate using pause with prompt to reboot
  hosts: localhost
  gather_facts: false
  become: true
  tasks:
    - name: Pause with prompt
      ansible.builtin.pause:
        prompt: "Enter 'REBOOT' to reboot"
      register: prompt

    - name: Did user ask for a reboot?
      ansible.builtin.debug:
        msg: 'Reboot requested: {{ prompt.user_input == "REBOOT" }}'

    - name: Reboot if requested
      ansible.builtin.reboot:
      when:
        - prompt.user_input == "REBOOT"
        - false  # Remove this line to reboot for realz!!
      register: reboot_status

    - name: Report reboot status
      ansible.builtin.debug:
        var: reboot_status
3 Likes

Thanks a lot @utoddl !

Small one-off playbooks are great for trying out techniques, but integrated playbook development (or later, role development) is a technique in itself.

I got a little carried away integrating your tasks into a playbook that uses tags to execute only the part(s) you’re interested in running. In fact, if you don’t specify tags, it will stop with a message to that effect:

    During development, this playbook requires run tags
    that are a subset of
    - apt
    - guestpw
    - cleanup
    - rsync
    - services
    - updates
    - sshd
    - reboot
    - all

The other habit I highly recommend developing is to run ansible-lint early and often.

Here’s my take on your current tasks integrated into a single playbook. I called it setup-test-host.yml, and you’d run it like

ansible-playbook setup-test-host.yml -vv --tags=guestpw,rsync,sshd -K
---
# setup-test-host.yml
- name: Demonstrate using tags in playbook development
  hosts: test
  gather_facts: true
  become: true
  tasks:
    - name: Ensure user specified valid tags
      # This is only used during playbook development
      ansible.builtin.assert:
        that:
          - ansible_run_tags | intersect(valid_tags) | length > 0
        fail_msg: |
          During development, this playbook requires run tags
          that are a subset of
          {{ valid_tags | to_nice_yaml }}
      vars:
        valid_tags:
          - apt
          - guestpw
          - cleanup
          - rsync
          - services
          - updates
          - sshd
          - reboot
          - all    # in case you want to run everything
      tags: always

    - name: Manage packages
      tags: apt
      block:
        - name: Block for updates
          block:
            - name: Update the apt cache
              ansible.builtin.apt:
                update_cache: true
            - name: Distribution upgrade
              ansible.builtin.apt:
                upgrade: dist
            - name: Install selected packages
              ansible.builtin.apt:
                name:
                  - rsync
                  - openssh-client
                  - openssh-server
                  - net-tools
                  - emacs
                state: present
            - name: Remove unwanted packages
              ansible.builtin.apt:
                name: gnome-games
                state: absent
                autoremove: true

    - name: Define a new password for user guest
      ansible.builtin.user:
        name: guest
        password: "{{ '' | password_hash('sha512') }}"
      tags: guestpw

    - name: Block for directory cleanup
      tags: cleanup
      block:
        - name: Find content of /home/guest/
          ansible.builtin.find:
            paths: /home/guest/
            hidden: true
            recurse: true
            file_type: any
          register: contenu_rep
        - name: Delete all found content
          ansible.builtin.file:
            path: "{{ item.path }}"
            state: absent
          loop: "{{ contenu_rep.files }}"

    - name: Block for rsyncs
      tags: rsync
      block:
        - name: Sync master to client /etc/skel  # noqa no-changed-when command-instead-of-module
          ansible.builtin.command: rsync -az --progress --ignore-times root@$192.168.64.1:.root/latest/skel/ /etc/skel/
        - name: Sync local from /etc/skel to /  # noqa no-changed-when command-instead-of-module
          ansible.builtin.command: rsync -az --progress --ignore-times /etc/skel/ /
        - name: Ownership of /home/guest for guest  # noqa no-changed-when
          ansible.builtin.command: chown -R guest:guest /home/guest
        - name: Mode of Xstop  # noqa no-changed-when
          ansible.builtin.command: chmod +x /usr/share/sddm/scripts/Xstop

    - name: Block for system services
      tags: services
      block:
        - name: Disabled (mask) sleep-related services
          ansible.builtin.systemd:
            name: "{{ item }}"
            masked: true
          loop:
            - sleep.target
            - suspend.target
            - hibernate.target
            - hybrid-sleep.target

    - name: Block for grub et dconf
      tags: updates
      block:
        - name: Update dconf  # noqa no-changed-when
          ansible.builtin.command: sudo dconf update
        - name: Update grub   # noqa no-changed-when
          ansible.builtin.command: sudo update-grub

    - name: Restart sshd
      ansible.builtin.systemd:
        name: sshd
        state: restarted
      tags: sshd

    - name: Block for prompted reboot
      tags: reboot
      block:
        - name: Pause with prompt
          ansible.builtin.pause:
            prompt: "Enter 'REBOOT' to reboot"
          register: prompt

        - name: Reboot if requested
          ansible.builtin.reboot:
          when:
            - prompt.user_input == "REBOOT"
          register: reboot_status

        - name: Report reboot status
          ansible.builtin.debug:
            var: reboot_status
2 Likes

Thank you so much @utoddl !
I didn’t know well how to use tags yet, so great learning thanks to you, and it helps me a lot to understand stuff I didn’t play with yet and need to understand (ansible-lint, those blocks instead of just a sequence of names…)
I just bought the O’Reilly book on Ansible, will study it starting with pointers you have kindly shared with me.
Thanks again!