How to idempotently change file attributes (e.g. immutable) with Ansible

I recently needed to force the /etc/resolv.conf file to be immutable on a set of CentOS servers, since the upstream provider's DHCP server was giving me a poorly-running set of default DNS servers, which was getting written to the resolv.conf file on every reboot.

There are a few different ways to force your own DNS servers (and override DHCP), but one of the simplest, at least for my use case, is to change the file attributes on /etc/resolv.conf to make the file immutable (unable to be overwritten, e.g. by the network service's DHCP on reboot).

Typically you would do this on the command line with:

chattr +i /etc/resolv.conf

And Ansible's file module has an attributes (alias: attr) parameter which allows the setting of attributes. For example, to set the attributes to i, you would use a task like:

- name: Ensure resolv.conf is immutable.
  file:
    path: /etc/resolv.conf
    attr: i

The problem is, this fails (at least on CentOS), because it's trying to override all the file's attributes, and CentOS also needs the e attribute. You could say attr: ie, but then you're being pretty strict about the attributes. Instead, you can use the value +i:

- name: Ensure resolv.conf is immutable.
  file:
    path: /etc/resolv.conf
    attr: +i

But here's the rub: when you do this, Ansible will always report 'changed' for this task, because it is always going to run lsattr +i on the file, even if it's already immutable. To make it so Ansible only reports a change if the file wasn't already immutable, you can register a variable and then set changed_when for the task, like so:

- name: Ensure resolv.conf is immutable.
  file:
    path: /etc/resolv.conf
    attr: +i
  register: resolv_file
  changed_when: "'i' not in resolv_file.diff.before.attributes"

Now, when you run the playbook, it will report a change on the first run, but not after that (unless someone is surreptitiously running a -i on your server and not using automation!).

Comments

pretty handy when you're on linode and don't want to disable their auto-configure network helper. I know I know.

thank you.

Just wanted to say that this blog post was exactly what I was looking for. Thank you Jeff! πŸ™

Perfect! Exactly what I was aiming for. Thanks!

Clearest example you can find. Thank you Jeff.

FWIW, this doesn't work with Ansible 2.9. That means the file task return value just contains a path key in the diff.after and diff.before dictionaries.