I got myself a bunch of Raspberry Pi 3s a short while ago:

Stack of Raspberry Pis

...the idea being that I can use those to run a few experiments on. After the experiment, I would need to be able to somehow remove the stuff that I put on them again. The most obvious way to do that is to reimage them after every experiment; but the one downside of the multi-pi cases that I've put them in is that removing the micro-SD cards from the Pis without removing them from the case, while possible, is somewhat fiddly. That, plus the fact that I cared more about price than about speed when I got the micro-SD cards makes "image all the cards of these Raspberry Pis" something I don't want to do every day.

So I needed a different way to reset them to a clean state, and I decided to use ansible to get it done. It required a bit of experimentation, but eventually I came up with this:

---
- name: install package-list fact
  hosts: all
  remote_user: root
  tasks:
  - name: install libjson-perl
    apt:
      pkg: libjson-perl
      state: latest
  - name: create ansible directory
    file:
      path: /etc/ansible
      state: directory
  - name: create facts directory
    file:
      path: /etc/ansible/facts.d
      state: directory
  - name: copy fact into ansible facts directory
    copy:
      dest: /etc/ansible/facts.d/allowclean.fact
      src: allowclean.fact
      mode: 0755

- name: remove extra stuff
  hosts: pis
  remote_user: root
  tasks:
  - apt: pkg={{ item }} state=absent purge=yes
    with_items: "{{ ansible_local.allowclean.cleanable_packages }}"

- name: remove package facts
  hosts: pis
  remote_user: root
  tasks:
  - file:
      path: /etc/ansible/facts.d
      state: absent
  - apt:
      pkg: libjson-perl
      state: absent
      autoremove: yes
      purge: yes

This ansible playbook has three plays. The first play installs a custom ansible fact (written in perl) that emits a json file which defines cleanable_packages as a list of packages to remove. The second uses that fact to actually remove those packages; and the third removes the fact (and the libjson-perl library we used to produce the JSON).

Which means, really, that all the hard work is done inside the allowclean.fact file. That looks like this:

#!/usr/bin/perl

use strict;
use warnings;
use JSON;

open PACKAGES, "dpkg -l|";

my @packages;

my %whitelist = (
    ...
);

while(<PACKAGES>) {
    next unless /^ii\s+(\S+)/;

    my $pack = $1;

    next if(exists($whitelist{$pack}));

    if($pack =~ /(.*):armhf/) {
        $pack = $1;
    }

    push @packages, $1;
}

my %hash;

$hash{cleanable_packages} = \@packages;

print to_json(\%hash);

Basically, we create a whitelist, and compare the output of dpkg -l against that whitelist. If a certain package is in the whitelist, then we don't add it to our "packages" list; if it isn't, we do.

The whitelist is just a list of package => 1 tuples. We just need the hash keys and don't care about the data, really; I could equally well have made it package => undef, I suppose, but whatever.

Creating the whitelist is not that hard. I did it by setting up one raspberry pi to the minimum that I wanted, running

dpkg -l|awk '/^ii/{print "\t\""$2"\" => 1,"}' > packagelist

and then adding the contents of that packagelist file in place of the ... in the above perl script.

Now whenever we apply the ansible playbook above, all packages (except for those in the whitelist) are removed from the system.

Doing the same for things like users and files is left as an exercise to the reader.

Side note: ... is a valid perl construct. You can write it, and the compiler won't complain. You can't run it, though.