Ansible OS Abstraction

Hi,

So I’ve been scavenging through a lot of posts to figure out how to deal with heterogeneous environments.
The two approaches that I see most are either using “group_by” or using “when”.

For example:

site.yml:

  • name: whatever
    tasks:

  • group_by: key={{ansible_os_family}

  • hosts: Debian
    roles:

  • role: rolename-debian

  • hosts: Darwin
    roles:

  • role: rolename-darwin

My problem with this approach is that you are saying that a role is “OS” specific, even though it’s not. Perhaps this has to do with my definition of role, but as I see it a ‘role’ is a task that any given node can perform.
I should be able to assign the ‘role’ ‘nginx’ to a variety of hosts, how those hosts then implement that role should be defined within the ‘role’ definition.

Now supposedly you can do this using the ‘when’ conditional statement, you would then end up with something like:

roles/myRole/tasks

  • include: apt.yml
    when: ansible_os_family == “Debian”

  • include: brew.yml
    when ansible_os_family == “Darwin”

However this is rather chatty, especially when these files include files of there own. And with chatty I mean every task, even when the ‘when’ clause is not matched is being shown, now you can set 'show_skipped_hosts" in the ansible configuration, however this still shows the headers of tasks that are (not) being processed.

Should I be dealing with this in a different fashion?
What I’m trying to accomplish is having a playbook that installs a package on a bunch of machines (running differents OS’s), then configure that package based on the OS and configure the service accordingly.

IMHO the latter approach is the way to go, however the ‘chattyness’ is killing my operators.

Thanks a lot for sharing your insights.

Best regards.

Most all ansible modules already abstract out OS details.

Package managers we want you to know - not only do package names change, but the way you interact with those packages change - think of how different Apache is between Ubuntu and CentOS for instance.

You can do things like

  • include: “{{ ansible_os_family }}.yml”

And you can also do things like use “include_vars” with similar tricks where you want to maintain differences.

In most cases you’ll only differ by variables except having a few tasks with “when” statements on them keying off the OS - which will also minimize task duplication.

Sure, but that’s exactly the thing I would like to deal with within a role, within a role however you can’t perform the ‘include’ you stated in your post as “ansible_os_family” doesn’t seem to evaluate.

To see how far I could come I went with the second approach that I mentioned in my original post.
With some tinkering I managed to abstract away most of the differences, it’s way too chatty though but it works.
However, I am stuck with the Handlers. As ‘service’ isn’t supported on OSX I had the idea of defining my own Handler like:

  • name: restart someservice
    shell: launchctl {{ item }} {{ service_plist}}
    with_items:
  • stop
  • start
    when: ansible_os_family == “Darwin”

This works, except the ‘when’ clause isn’t processed, after all as per the manual, handlers are matched by name. I could then go down the lane of naming the service accordingly:

  • name: restart {{ ansible_os_family }}

however this doesn’t work either as you can’t seem to dynamically set the name of a handler, so the only approach that I can think of that does work is giving the handlers another name:
for example “restart nginx darwin / restart nginx ubuntu”.
However that means that in my generic code where I change a file I will have to make a choice which handler to notify based on the OS I’m running on; same problem as before, you can’t do:
notify:

  • restart nginx {{ ansible_os_family }}

Things would be a lot easier if ‘Service’ would be implemented for OS-X and package would be abstracted (like for example Salt does).

This is not true.

Task files in roles (main.yml) can include other files.

include_vars also works as a task as I have mentioned.

It would be better to enhance the service module to understand launchd, but that’s a different topic.

It doesn’t work for me (unless I create a file called roles/foobar/tasks/{{ansible_os_family}}.yml… :wink: ).

I abstracted away package manager things using the ansible_pkg_mgr fact:

  • module: “{{ ansible_pkg_mgr }} name=foobar,baz,foo”

To account for OS-specific package names, you can do something like this:

  • module: “{{ ansible_pkg_mgr }} name={{ pkg_foobar }},{{ pkg_baz }},{{ pkg_foo }}”

The pkg_XXX variables must be defined in vars/Debian.yml and vars/Darwin.yml, and you do this at the top of the role:

  • include_vars: ‘{{ ansible_os_family }}.yml’

“It doesn’t work for me (unless I create a file called roles/foobar/tasks/{{ansible_os_family}}.yml… :wink: ).”

So in other words, it works :slight_smile:

Also, public service announcement - Don’t use ansible_pkg_mgr like that - it won’t join mulitple package installs using with_items into single transactions. If you want to do a “when: ansible_pkg_mgr == ‘apt’” that will still keep it going.

“It doesn’t work for me (unless I create a file called roles/foobar/tasks/{{ansible_os_family}}.yml… :wink: ).”

So in other words, it works :slight_smile:

No, sorry, I meant literally “{{ansible_os_family}}.yml” – it doesn’t seem to do templating for the included file name. I think I remember this being a popular feature request some time ago, but you wrote it’s important that the list of tasks in a play is fixed for every host (and it’s determined on parsing time, not run-time). Correct me if I missed something.

Also, public service announcement - Don’t use ansible_pkg_mgr like that - it won’t join mulitple package installs using with_items into single transactions. If you want to do a “when: ansible_pkg_mgr == ‘apt’” that will still keep it going.

Yes, in my roles I am working around that by supplying comma-separated lists myself instead of using with_items. Using ansible_pkg_mgr also has the disadvantage of not being able to use apt/yum/portage-specific parameters, but for virtually all of my package-manager needs it is sufficient – and so, so, so much nicer in the output :wink:

"It doesn't work for me (unless I create a file called
roles/foobar/tasks/{{ansible_os_family}}.yml... :wink: )."

So in other words, it works :slight_smile:

No, sorry, I meant literally "{{ansible_os_family}}.yml" -- it doesn't
seem to do templating for the included file name. I think I remember this
being a popular feature request some time ago, but you wrote it's important
that the list of tasks in a play is fixed for every host (and it's
determined on parsing time, not run-time). Correct me if I missed something.

Sorry, yes, It is true you can't include off inventory scoped variables
like that.

However

- include: RedHat.yml
  when: ansible_os_family == 'RedHat'

That works fine.

include_vars is fine with such tricks, however

That’s exactly the workaround I’m using now, however you can’t do something similar for ‘notify’.

e.g.

notify:

  • reload {{ ansible_os_family }}

doesn’t work, putting a when clause on the entire block just for the sake of using different notifiers means duplication of an entire block and again will also increase the chatty-ness
What’s the preferred way of dealing with variable notifiers?

No, you cannot notify based on a fact.

Here’s the idiom:

tasks:

  • shell: foo
    notify:
  • ack
  • bar

handlers:

  • name: ack
    when: some_condition

  • name: bar
    when: not some_condition