76

I am starting with ansible and will use it, among others, to install packages on several Linux distros.

I see in the docs that the yum and apt commands are separated - what would be the easiest way to unify them and use something like this:

- name: install the latest version of Apache
  unified_install: name=httpd state=latest

instead of

- name: install the latest version of Apache on CentOS
  yum: name=httpd state=latest
  when: ansible_os_family == "RedHat"

- name: install the latest version of Apache on Debian
  apt: pkg=httpd state=latest 
  when: ansible_os_family == "Debian"

I understand that the two package managers are different, but they still have a set of common basic usages. Other orchestators (salt for instance) have a single install command.

peterh
  • 4,914
  • 13
  • 29
  • 44
WoJ
  • 3,365
  • 8
  • 46
  • 75
  • You could have three recipes: one that iterates over a common list and then one each for OS-specific lists. What I'm trying to figure out right now is how to notify a handler with an OS-specific service name after a common config item is set. good luck! – dannyman Oct 07 '14 at 19:29
  • possible duplicate of [Parametric ansible command based on facts, how to do it?](http://serverfault.com/questions/695490/parametric-ansible-command-based-on-facts-how-to-do-it) – xddsg Aug 11 '15 at 18:53

6 Answers6

81

Update: As of Ansible 2.0, there is now a generic & abstracted package module

Usage Examples:

Now when the package name is the same across different OS families, it's as simple as:

---
- name: Install foo
  package: name=foo state=latest

When the package name differs across OS families, you can handle it with distribution or OS family specific vars files:

---
# roles/apache/apache.yml: Tasks entry point for 'apache' role. Called by main.yml
# Load a variable file based on the OS type, or a default if not found.
- include_vars: "{{ item }}"
  with_first_found:
    - "../vars/{{ ansible_distribution }}-{{ ansible_distribution_major_version | int}}.yml"
    - "../vars/{{ ansible_distribution }}.yml"
    - "../vars/{{ ansible_os_family }}.yml"
    - "../vars/default.yml"
  when: apache_package_name is not defined or apache_service_name is not defined

- name: Install Apache
  package: >
    name={{ apache_package_name }}
    state=latest

- name: Enable apache service
  service: >
    name={{ apache_service_name }}
    state=started
    enabled=yes
  tags: packages

Then, for each OS that you must handle differently... create a vars file:

---
# roles/apache/vars/default.yml
apache_package_name: apache2
apache_service_name: apache2

---
# roles/apache/vars/RedHat.yml
apache_package_name: httpd
apache_service_name: httpd

---
# roles/apache/vars/SLES.yml
apache_package_name: apache2
apache_service_name: apache2

---
# roles/apache/vars/Debian.yml
apache_package_name: apache2
apache_service_name: apache2

---
# roles/apache/vars/Archlinux.yml
apache_package_name: apache
apache_service_name: httpd



EDIT: Since Michael DeHaan (creator of Ansible) has chosen not to abstract out the package manager modules like Chef does,

If you are still using an older version of Ansible (Ansible < 2.0), unfortunately you'll need to handle doing this in all of your playbooks and roles. IMHO this pushes a lot of unnecessary repetitive work onto playbook & role authors... but it's the way it currently is. Note that I'm not saying we should try to abstract package managers away while still trying to support all of their specific options and commands, but just have an easy way to install a package that is package manager agnostic. I'm also not saying that we should all jump on the Smart Package Manager bandwagon, but that some sort of package installation abstraction layer in your configuration management tool is very useful to simplify cross-platform playbooks/cookbooks. The Smart project looks interesting, but it is quite ambitious to unify package management across distros and platforms without much adoption yet... it'll be interesting to see whether it is successful. The real issue is just that package names sometimes tend to be different across distros, so we still have to do case statements or when: statements to handle the differences.

The way I've been dealing with it is to follow this tasks directory structure in a playbook or role:

roles/foo
└── tasks
    ├── apt_package.yml
    ├── foo.yml
    ├── homebrew_package.yml
    ├── main.yml
    └── yum_package.yml

And then have this in my main.yml:

---
# foo: entry point for tasks
#                 Generally only include other file(s) and add tags here.

- include: foo.yml tags=foo

This in foo.yml (for package 'foo'):

---
# foo: Tasks entry point. Called by main.yml
- include: apt_package.yml
  when: ansible_pkg_mgr == 'apt'
- include: yum_package.yml
  when: ansible_pkg_mgr == 'yum'
- include: homebrew_package.yml
  when: ansible_os_family == 'Darwin'

- name: Enable foo service
  service: >
    name=foo
    state=started
    enabled=yes
  tags: packages
  when: ansible_os_family != 'Darwin'

Then for the different package managers:

Apt:

---
# tasks file for installing foo on apt based distros

- name: Install foo package via apt
  apt: >
    name=foo{% if foo_version is defined %}={{ foo_version }}{% endif %}
    state={% if foo_install_latest is defined and foo_version is not defined %}latest{% else %}present{% endif %}
  tags: packages

Yum:

---
# tasks file for installing foo on yum based distros
- name: Install EPEL 6.8 repos (...because it's RedHat and foo is in EPEL for example purposes...)
  yum: >
    name={{ docker_yum_repo_url }}
    state=present
  tags: packages
  when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int == 6

- name: Install foo package via yum
  yum: >
    name=foo{% if foo_version is defined %}-{{ foo_version }}{% endif %}
    state={% if foo_install_latest is defined and foo_version is not defined %}latest{% else %}present{% endif %}
  tags: packages

- name: Install RedHat/yum-based distro specific stuff...
  yum: >
    name=some-other-custom-dependency-on-redhat
    state=latest
  when: ansible_os_family == "RedHat"
  tags: packages

Homebrew:

---
- name: Tap homebrew foobar/foo
  homebrew_tap: >
    name=foobar/foo
    state=present

- homebrew: >
    name=foo
    state=latest

Note that this is awfully repetitive and not D.R.Y., and although some things might be different on the different platforms and will have to be handled, generally I think this is verbose and unwieldy when compared to Chef's:

package 'foo' do
  version node['foo']['version']
end

case node["platform"]
when "debian", "ubuntu"
  # do debian/ubuntu things
when "redhat", "centos", "fedora"
  # do redhat/centos/fedora things
end

And yes, there is the argument that some package names are different across distros. And although there is currently a lack of easily accessible data, I'd venture to guess that most popular package names are common across distros and could be installed via an abstracted package manager module. Special cases would need to be handled anyway, and would already require extra work making things less D.R.Y. If in doubt, check pkgs.org.

TrinitronX
  • 1,087
  • 9
  • 13
  • With Ansible 2 you can use the package module to abstract all this http://docs.ansible.com/ansible/package_module.html – Guido Dec 29 '15 at 19:00
  • @GuidoGarcía: Very nice! Adding a note about this for Ansible 2.0 – TrinitronX Dec 31 '15 at 19:56
  • Maybe also worth mentioning that you can specify a comma separated list or just a list of packages. – Wes Turner Dec 12 '18 at 03:54
  • Regarding the mention of how to abstract out differing package names with the 2.0+ "package" module... is this documented somewhere besides here? The official docs for ansible.builtin.package say nothing at all about the issue of differing package names. And took quite a bit of searching to find this answer on google. Most results are just about how to use "package" or yum/apt/etc – Kelly Clowers Mar 02 '22 at 22:26
  • @KellyClowers There's no "official" way documented by Ansible, at least that I know of. The separate vars files plus `include` & `when` pattern seems to be the most popular way that I've seen it done in the community roles. – TrinitronX Mar 06 '22 at 23:18
14

You can abstract out package managers via facts

- name: Install packages
  with_items: package_list
  action: "{{ ansible_pkg_mgr }} state=installed name={{ item }}"

All you need is some logic that sets ansible_pkg_mgr to apt or yum etc.

Ansible are also working on doing what you want in a future module.

xddsg
  • 3,202
  • 2
  • 26
  • 33
  • 2
    Ansible sets `ansible_pkg_mgr` itself for any packager it knows about. It's not necessary for you to do anything. I use this particular construct everywhere. – Michael Hampton Nov 15 '15 at 22:00
  • The syntax is still quite useful for those who want to optimise the run of their playbooks. The generic package module [does not yet provide optimisation for with_items](https://github.com/ansible/ansible/issues/24581) so it's much slower when it is used to install multiple packages at once. – Danila Vershinin May 13 '17 at 23:39
  • @DanielV. Note that the github issue does provide a workaround for that. – Michael Hampton Dec 14 '17 at 01:36
6

From Ansible 2.0 there is the new Package-modul.

http://docs.ansible.com/ansible/package_module.html

You can then use it like your proposal:

- name: install the latest version of Apache
  package: name=httpd state=latest

You still have to consider name differences.

Tvartom
  • 123
  • 2
  • 6
3

Check out Ansible's documentation on Conditional Imports.

One task to ensure that apache is running even if the service names is different across each OS.

---
- hosts: all
  remote_user: root
  vars_files:
    - "vars/common.yml"
    - [ "vars/{{ ansible_os_family }}.yml", "vars/os_defaults.yml" ]
  tasks:
  - name: make sure apache is running
    service: name={{ apache }} state=running
David Vasandani
  • 246
  • 3
  • 13
2

You do not want to do that because certain package names differ between distros. For example on RHEL-related distros the popular web server package is named httpd, where as on Debian-related distros it's named apache2. Similarly with a huge list of other system and supporting libraries.

There might be a set of common basic parameters, but then there's also a number of more advanced parameters that are different between package managers. And you don't want to be in an ambiguous situation where for some commands you use one syntax and for other commands you use another syntax.

Mxx
  • 2,312
  • 2
  • 26
  • 40
  • This is more or less what I was expecting (unfortunately :)) so I wonder how `salt` manages to unify both package managers. Anyway, I will resort to a double configuration, then. – WoJ Apr 10 '14 at 12:17
  • Or don't manage a distro zoo ;-) migrate to a single-distro infrastructure and live a happier life. – Mxx Apr 10 '14 at 17:12
  • The zoo is fortunately only two animals large but this is the lowest number I can go to :) – WoJ Apr 11 '14 at 07:23
  • 1
    @Mxx that's fine logic for a sysadmin but what about a software vendor or consultant that supports multiple platforms? – David H. Bennett Nov 27 '14 at 15:12
  • @David, then this needs to be taken up with distro vendors, for them to have unified package names and install tools. There is realistically no way that Ansible can have a unified mapping of ALL packages from all supported distros of all versions. – Mxx Nov 27 '14 at 17:53
  • Puppet also has a `package` type that automatically detects the package manager (`provider`) and could be overwritten if necessary. – Belmin Fernandez Feb 12 '15 at 19:57
  • the `package` abstraction in Chef and Puppet is designed in such a way as to delegate to the appropriate package manager "`provider`" for the distro. This can be overridden using the [`provider` property in Chef](https://docs.chef.io/resource_package.html#providers) or the [`provider` attribute in Puppet](http://docs.puppetlabs.com/references/stable/type.html#package-attribute-provider). I am not that familiar with SaltStack, but if it has a package abstraction, I'd assume that you can override it somehow. – TrinitronX Nov 04 '15 at 19:34
  • The original concern about how to handle packages with differing names in a cross-distro / platform-independent way is usually achievable with a `case` or `if/then/else` statement on the `platform_family`, but by far the simple `package 'foo'` idiom works fine in most general cases. – TrinitronX Nov 04 '15 at 19:36
1

The answers above all seem to link to broken pages. This looks like the correct URL: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/package_module.html

Zach Smith
  • 121
  • 8