I'm attempting to uninstall a list of packages from our RHEL servers. However, I need to account for servers where these packages to uninstall are needed for the application. An good example of this is httpd, which is listed on our uninstall list, but it is an dependency for the application running on the server. Basically I'm managing two states with one playbook.
So here is the list of packages to remove, which is in the role's defaults/main.yml
packagesRemove:
- telnet
- nfs
- nfs-server
- nfs-utils
- named
- httpd
- rsync
- postfix
- autofs
- cups
- smb
- squid
Currently, I'm doing something basic to uninstall the packages on the first run.
- name: Check for packageRemove file
stat:
path: /root/packageRemove.txt
register: stat_result
- name: remove packages not needed
yum:
name: "{{ packagesRemove }}"
autoremove: yes
register: packageRemove_output
when: not stat_result.stat.exists
- name: create packageRemove file
template:
src: output.txt.j2
dest: /root/packageRemove.txt
owner: root
group: root
mode: 0600
when: not stat_result.stat.exists
Basically if the /root/packageRemove.txt file exits, these tasks just get skipped. How can I make this more dynamic, and remove the need for the /root/packageRemove.txt file. I would like to make the packages that are needed into some sort of inventory variables.
Right now, I just have the following to gather a list of packages installed on the server.
- name: gather installed packages
dnf:
list: installed
no_log: true
register: yum_packages
- name: make installed packages a list
set_fact:
installed_packages: "{{ yum_packages.results | map(attribute='name') | list }}"
This is now where I'm stumped, and I'm not quite sure what my next step should be or if I'm on the right track. Any help would be great.
Related
I'm trying create a Ansible playbook that will read contents of a file and use those contents to install packages on a target machine.
In simpler terms, I want to run this command converted to an ansible playbook
cat ./meta/install-list/apt | xargs apt install -y
./meta/install-list/apt
neofetch
tmux
git
./ansible/playbooks/apt.yaml
- hosts: all
become: true
tasks:
- name: Extract APT packages to install
command: cat ../../meta/install-list/apt
register: _pkgs
delegate_to: localhost
run_once: true
- name: Install APT packages
apt:
name: "{{ _pkgs.stdout_lines }}"
state: latest
./ansible.cfg
[defaults]
inventory = ./ansible/inventory/hosts.yaml
./ansible/inventory/hosts.yaml
---
all:
children:
group-machines:
hosts:
target-machine.local
Command to run playbook
ansible-playbook --ask-become-pass ./ansible/playbooks/apt.yaml --limit group-machine
When running the command, it gets stuck on Extract APT packages to install
NOTE:
these files mentioned above are to be only on machine that is running the command. If possible, I'd like to prevent copying files to target machines and then running the playbooks tasks
PS: new to ansible
I don't see anything in your "Extract APT packages to install" task that should cause it to get stuck... but you don't need that task in any case; you can combine your two tasks into a single task like this:
- hosts: all
become: true
tasks:
- name: Install APT packages
apt:
name: "{{ packages }}"
state: latest
vars:
packages: "{{ lookup('file', '../../meta/install-list/apt').splitlines() }}"
Here we're using a file lookup to read the contents of a file. Lookups always run on the local (control) host.
Note that you could write the above like this as well...
- hosts: all
become: true
tasks:
- name: Install APT packages
apt:
name: "{{ lookup('file', '../../meta/install-list/apt').splitlines() }}"
state: latest
...but I like to keep longer jinja expressions in vars in order to keep the rest of the arguments more readable.
The above answer is more than enough by #Zeitounator. But if you do some formatting to your original file of package list as below
packages:
- neofetch
- tmux
- git
After that you can simply run the playbook like below
- hosts: all
become: true
vars_files: ../../meta/install-list/apt
tasks:
- name: Install APT packages
apt:
name: "{{ packages }}"
state: latest
Now suppose if you are lazy enough to not want to do the formatting then below playbook also will do the trick. Its much cleaner and scalable in my opinion.
---
- name: SHow the packages list
hosts: localhost
become: true
tasks:
- name: View the packages list file
shell: cat ../../meta/install-list/apt
register: output
- name: Install the package
apt:
name: "{{ output.stdout_lines }}"
state: latest
(See update2 below for my final word on this.)
I'd like to have ansible generate a list of missing packages by comparing the installed packages with a list of desired packages. (The list of missing packages would then be passed to yum for installation.) It seems like this can be done using some combination of package_facts and the "difference" filter, but I can't come up with the right syntax.
For example, the following gives information about installed packages:
- name: get the rpm package facts
package_facts:
- name: show them
debug: var=ansible_facts.packages
and lists can be compared like this:
- name: Find difference of two lists
gather_facts: no
hosts: localhost
vars:
missing: "{{ input1|difference(input2) }}"
input1:
- john
- mary
input2:
- john
- alice
tasks:
- name: 'Print them out'
debug: var=missing
How can I plug these two techniques together to make a list that's usable later by yum?
Update
Zigarn's answer below gets me very close to what I want (with the caveat that it's "set_fact:" instead of "set_facts:" and "package:" instead of "packages:"). Here's what I'm trying now:
- name: Retrieve the installed packages
package_facts:
- name: Get only list of name of installed packages
set_fact:
installed_packages: "{{ ansible_facts.packages.keys() }}"
- name: Specify list of desired packages
set_fact:
desired_packages:
- emacs
- antiword
- gimp
- junk
- otherstuff
- name: Compute missing packages
set_fact:
missing_packages: "{{ desired_packages | difference (installed_packages) }}"
- name: Print list of missing packages
debug: var=missing_packages
- name: Print list of installed packages
debug: var=installed_packages
- name: Install missing packages
ignore_errors: yes
yum:
skip_broken: yes
name: "{{ item }}"
state: present
loop: "{{ missing_packages }}"
This does indeed try to install missing packages, with the odd exception of "emacs" which isn't currently installed, but also isn't put into the missing_packages list by ansible. This might just be something odd about this particular package, so I'm going to go ahead and mark zigarn's post as the answer but continue to look into the problem.
Regarding why I'm doing it this way, two reasons: First, I'm using a loop and a list instead of passing all the packages to yum at once because I want to avoid the case where a typo in the list of desired packages, or a named package that's not in the repositories, makes yum fail to install any of the listed packages. "skip_broken" doesn't help in these cases. Second, I'm installing a lot of packages (thousands) and yum is very slow, so whittling down the list to just the missing packages can save an hour on the time it takes to update a computer.
Update2
After experimenting with this for a while, I've decided that I'm not confident that I understand the data structure returned by ansible_facts.packages and the way "difference" interprets it, so the following is what I'm going to stick with for now.
- name: Get list of installed packages without arch
command: "rpm -q --qf '%{NAME}\n' -a"
args:
warn: false
register: installed_packages_noarch
- name: Get list of installed packages with arch
command: "rpm -q --qf '%{NAME}.%{ARCH}\n' -a"
args:
warn: false
register: installed_packages_arch
- name: Combine the lists of packages with and without arch
set_fact:
installed_packages: "{{ installed_packages_noarch.stdout_lines + installed_packages_arch.stdout_lines }}"
- name: Install missing packages
ignore_errors: yes
yum:
skip_broken: yes
state: present
name: "{{ item }}"
loop: "{{ desired_packages | difference (installed_packages) }}"
vars:
desired_packages:
- emacs
- gimp
- somethingelse
- anotherthing
Note that I've fallen back to just using rpm to get the list of installed packages, and that I actually have it generate two lists and combine them: A list of bare package names, and a list of packagename.architecture. This lets me specify things like libsomething.i686 or libother.x86_64 in the list of desired packages in cases where I want packages of multiple architectures installed.
First, you need to know that you don't need to compute yourself the list of missing packages as the package module (or platform-specific module yum, apt, ...) is idempotent and will anyway check itself the missing packages to install them.
But, if you need it for another purpose, here is how to do it.
package_facts is populating the ansible_facts.packages variables with a specific structured documented in https://docs.ansible.com/ansible/latest/collections/ansible/builtin/package_facts_module.html#returned-facts
If you want to compare it with a simple list of package names, you need to only use the keys of the package_facts. Then you can get the difference between the two lists:
- name: Retrieve the installed packages
package_facts:
- name: Get only list of name of installed packages
set_fact:
installed_packages: "{{ ansible_facts.packages.keys() }}"
- name: Compute missing packages
set_fact:
missing_packages: "{{ package_list_you_want | difference (installed_packages) }}"
- name: Install missing packages
package:
name: "{{ missing_packages }}"
state: present
I have to deal with new machines (same OS version on all) that have been previously managed manually by many different admins.
The purpose is to use Ansible to make all these machines sharing the same list of installed packages,
AND remove any packages not in the list that might be installed already.
Is this feasible with Ansible ?
vars:
- yum_rpm:
- tcpdump
- tmux
- psacct
tasks:
- name: "Install all package in our list"
yum:
name: "{{ yum_rpm }}"
state: absent
update_cache: no
- name: "Remove any other unexpected package already installed"
## NO IDEA
Building up on #gary lopez answer to add security and performance.
First you will need to get an actual list of all packages you want to see installed on your final machine, including the default ones that come with the system. I assume that list will be in var yum_rpm
Once you have that, the next step is to get the list of currently installed packages on the machine. To create an actual list we can reuse:
- name: Get installed packages
yum:
list: installed
register: __yum_packages
- name: Make installed packages a list of names
set_fact:
installed_packages: "{{ __yum_packages.results | map(attribute='name') | list }}"
From there, adding and removing is just a matter of making a difference on lists. The goal here is to avoid looping on the yum module package by package (because it is damn slow and listed as a bad practice on the module documentation page) and to make the install and remove operations in one go.
- name: align packages on system to expected
yum:
name: "{{ item.packages }}"
state: "{{ item.state }}"
loop:
- packages: "{{ yum_rpm | difference(installed_packages) }}"
state: present
- packages: "{{ installed_packages | difference(yum_rpm) }}"
state: absent
when: item.packages | length > 0
In the first task you need to use state: present. You could try this
vars:
- yum_rpm:
- tcpdump
- tmux
- psacct
tasks:
- name: "Install all package in our list"
yum:
name: "{{ yum_rpm }}"
state: present
update_cache: no
- name: Get packages installed
yum:
list: installed
register: __yum_packages
- name: "Remove any other unexpected package already installed"
yum:
name: "{{ item.name }}"
state: absent
with_items: "{{ __yum_packages.results }}"
But I recommend you validate packages to uninstall because you could uninstall some packages required for your OS.
I have an ansible playbook which will copy a file into a location on a remote server. It works fine. In this case, the file is an rpm. This is the way it works:
---
- hosts: my_host
tasks:
- name: mkdir /tmp/RPMS
file: path=/tmp/RPMS state=directory
- name: copy RPMs to /tmp/RPMS
copy:
src: "{{ item }}"
dest: /tmp/RPMS
mode: 0755
with_items:
[any_rpm-x86_64.rpm]
register: rpms_copied
Now, with the file successfully on the remote server, I need to start some new logic that will install the rpm that sits in /tmp/RPMS. I have run many different versions of the below (So this code is added onto the above block):
- name: install rpm from file
yum:
name: /tmp/RPMS/any_rpm-x86_64.rpm
state: present
become: true
I don't know if the formatting is incorrect, or if this is not the way. Can anyone advise as to how I can get the rpm in the directory /tmp/RPMS installed using a new few lines in the existing playbook?
Thanks.
I did not find this anywhere else, and it genuinely took me all of my working day to get to this point. For anyone else struggling:
- name: Install my package from a file on server
shell: rpm -ivh /tmp/RPMS/*.rpm
async: 1800
poll: 0
become_method: sudo
become: yes
become_user: root
I have this task in Ansible:
- name: Install mongodb
yum:
name:
- "mongodb-org-{{ mongodb_version }}"
- "mongodb-org-server-{{ mongodb_version }}"
- "mongodb-org-mongos-{{ mongodb_version }}"
- "mongodb-org-shell-{{ mongodb_version }}"
- "mongodb-org-tools-{{ mongodb_version }}"
state: present
notify: Restart mongodb
Is there a way I can indicate the version without having to use a loop like this? What is a more "elegant" way of writing this?
- name: Install mongodb
yum:
name: "{{ item }}-{{ mongodb_version }}"
state: present
loop:
- mongodb-org-server
- mongodb-org-mongos
- mongodb-org-shell
- mongodb-org-tools
notify: Restart mongodb
To my surprise I didn't find the simplest solution in all the answers, so here it is. Referring to the question title Installing multiple packages in Ansible this is (using the yum module):
- name: Install MongoDB
yum:
name:
- mongodb-org-server
- mongodb-org-mongos
- mongodb-org-shell
- mongodb-org-tools
state: latest
update_cache: true
Or with the apt module:
- name: Install MongoDB
apt:
pkg:
- mongodb-org-server
- mongodb-org-mongos
- mongodb-org-shell
- mongodb-org-tools
state: latest
update_cache: true
Both modules support inline lists!
The second part of your question is how to integrate specific version numbers into the package lists. No problem - simply remove the state: latest (since using specific version numbers together with state: latest would raise errors) and add the version numbers to the package names using a preceding = like this:
- name: Install MongoDB
yum:
name:
- mongodb-org-server=1:3.6.3-0centos1.1
- mongodb-org-mongos=1:3.6.3-0centos1.1
- mongodb-org-shell=1:3.6.3-0centos1.1
- mongodb-org-tools=1:3.6.3-0centos1.1
update_cache: true
You could also optimize further and template the version numbers. In this case don't forget to add quotation marks :)
Make this into an Ansible Role called mongo, resulting in the directory structure:
playbook.yml
roles
|-- mongo
| |-- defaults
| | |-- main.yml
| |
| |-- tasks
| | |-- main.yml
| |
| |-- handlers
| | |-- main.yml
Add the required MongoDB packages and version into the default variables file roles/mongo/defaults/main.yml:
mongo_version: 4.0
mongo_packages:
- mongodb-org-server
- mongodb-org-mongos
- mongodb-org-shell
- mongodb-org-tools
Re-write the task in the roles/mongo/tasks/main.yml file:
- name: Install mongodb
yum:
name: "{{ item }}-{{ mongo_version }}"
state: present
with_items: "{{ mongo_packages }}"
notify: Restart mongodb
Add your handler logic to restart MongoDB in the file roles/mongo/handlers/main.yml.
Write a Playbook called playbook.yml that calls the role.
---
- hosts: all
roles:
- mongo
Some people have mentioned syntax that allows passing a list, but you don't need to special-case your code for apt or yum. Here is the command that uses the generic package manager module:
- name: Install packages
become: yes
package:
name:
- tmux
- rsync
- zsh
- rsync
- wget
state: present
(Or state: latest.) This feature is only documented for Ansible 2.9, so it may be new.
I think this is best way, but as the package names should not be hard coded, it is preferred to keep in vars/main.yml
e.g
mongodb_version: 5
packages:
- "mongodb-org-shell-{{ mongodb_version }}"
- "mongodb-org-server-{{ mongodb_version }}"
Call this inside your playbook as
- name: Install mongodb packages
yum: name={{ item }}
state=latest
with_items: "{{ packages}}"
Now yum module direct accept array of packages like this
vars:
pkgs:
- screen
- rubygems
- ksh
- strace
tasks:
- name: Install pre-requisite packages
yum: name={{ pkgs }} state=installed
A more elegant way, which doesn't keep re-checking each and every package if installed every time the script is run is to use something like this:
In vars/main.yml or similar:
packages_to_install:
- mongodb-org-server
- mongodb-org-mongos
- mongodb-org-shell
- mongodb-org-tools
In tasks/main.yml or similar:
- name: "Get installed packages"
yum:
list: "installed"
register: installed_packages
- name: "Install missing packages"
package:
state: "present"
name: "{{ item }}"
with_items: "{{ packages_to_install | difference(installed_packages | json_query('results[*].name')) }}"
Essentially this checks what is already installed and only installs missing packages. Counter-intuitively this is faster. You can also use dnf instead of yum.