- Ansible Runner Python Example
- Call Ansible Playbook From Python
- Ansible Runner Python Free
- Ansible Runner Python Interview
- Ansible Runner Example
I’ve been trying to get into the habit of managing an ansible playbook to keep track of any manual installations or config tweaks I do on my local machine. No matter how vigilant you are though, there will always be packages you install that slip through the cracks.
Ansible Runner is a tool and python library that helps when interfacing with Ansible directly or as part of another system whether that be through a container image interface, as a standalone tool, or as a Python module that can be imported. The goal is to provide a stable and consistent interface abstraction to Ansible. Ansible is written in its own API so you have a considerable amount of power across the board. This chapter discusses the Python API. The Python API is very powerful, and is how the ansible CLI and ansible-playbook are implemented. In version 2.0 the core ansible got rewritten and the API was mostly rewritten.
- The Top 2 Python Ansible Runner Open Source Projects on Github. Topic Ansible Runner.
- # this addes the path endpoints that allow ansible work be run from a # virtualenv when ansible-runner calls ansible. Setting the path is not # necessary if ansible is installed in system python. Path = '/home/sprygada/Workspaces/ansible/bin:/home/sprygada/.virtualenvs/ansible/bin:' path += os.environ.get('PATH', ') # setup various environment vars to be set prior to starting the ansible.
It would be great for example if you could run an ansible playbook which runs all the relevant install tasks, and then provides you a list of these “unmanaged packages” that are not handled by the playbook:
You can imagine how such a script might be useful if you want to ensure that all system package installations are performed using the playbook, or to audit systems to ensure only automatically installed packages are installed.
What follows is an interesting exercise (adventure if you will) of trying to create such a script using internal ansible python objects. I hesitate to call this an API (even though the official docs do), as that would imply stable, object level references and documentation - but there is none of that, and even what little official documentation there is uses examples with plenty of calls to methods starting with _
. Thankfully the code is easy to read and understand, and I certainly learned a lot about how Ansible works along the way. I hope you do too.
Before we begin I should mention that all the code snippets use internal objects from Ansible v2.9. These are very likely to change between versions so I do not expect them to work for other versions. I have also only focused on making this work on Ubuntu/Debian, as this is what I use personally.
0. Background
For this exercise, I wanted the end result to be a standalone python script that:
- I can run without any dependencies (other than the ansible pip package(s)) and without editing/creating any files or configs.
- Will execute any playbook against on all the relevant hosts, based on task level args as well as inventory, host and group args just like a regular playbook execution would.
- Will accept any other valid options for
ansible-playbook
(i.e act as a wrapper for ansible-playbook) - Ensure that the playbook executes in
--check/dry-run
mode, so that nothing is actually changed on the host(s) - Print only the unmanaged packages for each host where packages are installed.
This limited my options somewhat, and I arrived at this approach:
- Create a set of tasks to find manually installed packages on Ubuntu/Debian
- Create a script which can execute any playbook using the Python API
- Prepend the tasks defined in our short playbook in 1. to the playbook being executed by the script in 2.
- Create a callback plugin that will read stdout on successful execution of each task in the target playbook, and keep track of any packages that will be installed, and compare this to the output of the manually installed packages to generate the list of unmanaged packages for each host.
1. Find manually installed packages (on Debian/Ubuntu)
The first step is to figure out which packages have been installed manually on the system. We are only interested in packages that have been manually installed by the user - for eg. by running apt-get install <package name>
. For this, all we have to do is call on our trusty package management system of choice, to filter out package dependency installs. I’ll only be covering Ubuntu and Debian systems, as I use Ubuntu, but you can find a [similar solution]()[1] for other distros. Judging by the amount of duplicate questions for this topic on stack overflow (for eg. see this[3], this[4], this[5] ) and this[6]) this is a common task people want to do.
Here are a few ways of doing it:
- Check manually installed packages using apt
apt
marks all dependencies of manually installed packages as “automatic”, so that if you run apt-get autoremove
, it removes any dangling dependencies. This is useful because it can provide a list of packages that were installed by the user manually, rather than by the system automatically to satisfy dependencies. However, apt also marks packages that were installed as part of the Ubuntu install as manual, which is not ideal for our use case.
- Look through the apt history files (
/var/log/apt/history.log.*.gz
) and check for install entries:
One disadvantage of this option is that on most systems logrotate may have already cleaned up these logs, so there is no guarantee they’ll still be there to check. The main problem though is that we’d also need to check which packages were removed manually and disregard the package if the uninstall happened after the install. This is probably do-able, but not worth my time.
Both of these options are not really satisfactory by themselves. The first option however, can be improved by filtering out all of the packages that were installed as part of the Ubuntu install process. If the install is recent, logs can sometimes be found at /var/log/installer/
, but this is not always reliable as they may have been moved and not available on all versions. A good enough solution is to grab the manifest of the installation packages from releases.ubuntu.com:
This is still not perfect as it reports packages such as libsigc++-2.0-0v5
and libido3-0.1-0
which I definitely did not install manually, but its probably accurate enough for our purposes.
Converting the above script to an ansible playbook yields the following:
We use set -o pipefail
with the shell module to ensure that the pipeline raises a non-zero exit code if any of the commands within the pipeline fail. This means that we need to use the /bin/bash
executable (as the default shell ansible uses is /bin/sh
, which does not support the pipefail option). Not also that we saved the final list of manual packages within ansible’s facts, so that it can be easily retrieved when this playbook is executed with other plays.
2. Running an Ansible Playbook using the Python API
Now that we’ve figured out how to check installed packages and created a playbook to do so, we need to find a way to execute this playbook along with our target playbook on the same hosts. The usual and sensible way is to include the play within the playbook:
However, for our use case, we want a standalone script with no other file dependencies. This requires us to use ansible’s internal python objects to customise the execution of both of the playbooks.
2.1 Argument Parsing
Firstly, we need our script to act as a wrapper for ansible-playbook, and accept all of the arguments it accepts and pass these on to the ansible context so it can be used by our playbook executor.
When you run any ansible command, it calls a stub cli script. This stub then invokes the appropriate cli object depending on the command. For eg. ansible-playbook
invokes the ansible.cli.playbook.PlaybookCLI
class. All of the CLI objects can be found in ansible.cli.*
and they each extend the ansible.cli.CLI
interface.
Our script will only ever execute playbooks, so we just need to invoke the PlaybookCLI
class and run the parse()
method to parse the cli arguments:
We also need to ensure that any playbook that is executed, is run using --check
mode (also known as dry-run), so that nothing is actually changed on the hosts.
When the arguments are parsed into context.CLIARGS
, they are stored in an ImmutableDict class. It is definitely possible to hack around it to modify the contents and add --check
if necessary. But, it is easier to just do this before the args are parsed:
2.2 Preparing for Execution
Next, we prepare to execute the playbook by performing all of the necessary pre-execution actions Ansible performs such as filtering hosts from the inventory file, checking to make sure playbook files exist, parsing playbook yaml files to extract tasks, loading any plugins, handling passwords, substituting in any variable values etc.
Luckily for us, all of this work is done for us in the run()
method of ansible.cli.playbook.PlaybookCLI
. We will add this to the script as is (from v2.9.1):
2.3 Executing the playbook
Now that we have everything in place to execute the playbook, we can go ahead and execute it. Ansible uses the PlaybookExecutor class to perform this set of actions.
We can refer back to the PlaybookCLI
class to see how it is called:
We don’t really need to do anything with the results, because all of the useful information we need will be reported to stdout, and we can capture this using Callback Plugins (more on this in section 2.5).
Ansible Runner Python Example
If we save this script as check_packages.py
and run it, it should work just like ansible-playbook
. For example, if we used it to run our playbook which checks manually installed packages, we should get:
Great! We’ve literally just created a crappier version of ansible-playbook. How is this helpful I hear you ask? Let’s find out.
2.4 Executing Plays
The whole point of figuring out how to execute a playbook using the Python API was to execute the playbook for finding manually installed packages (from section 1) along with any target playbook.
If we don’t want to read the playbook tasks from a yaml file, the alternative is to skip the yaml parsing, and just provide the executor with the parsed tasks which we can hardcode in our script.
Lets have a look at how Ansible parses the playbook file into the internal data structure the executor uses. The ansible.parsing.dataloader.DataLoader class is responsible for parsing the yaml into json that the executor can execute. We can get the parsed json with a bit of debugging:
This should give us the following output:
You may have noticed that the json above has the hosts: all
directive. This will mean that these tasks will run on all of the hosts defined in the inventory file. And because of the Implicit Localhost behaviour, if a local connection is not specified in the inventory file, the tasks will not run on localhost. This is not exactly the behaviour we want, but we need to cover more ground be able to fix this, so we’ll come back to this in section 3.7
We now have the tasks, how do we pass it to the executor to run? The only example in the ansible python API docs provides us with this answer, including some helpful comments. First we convert the json above to a python dict and hardcode it in our script as the play_source
variable. Next, we copy a few lines from the example in the docs to get:
If we run this however, our tasks will be skipped. This is because we are running this right after running a PlaybookExecutor
object that has used the same inventory, variable_manager and loader objects. If we peek at the code for the PlaybookExecutor, we can see that some of these objects are cleaned up after the run:
This causes ansible to lose state. If we want to regain this we need to reinstantiate these objects before creating the TaskQueueManager
object:
Before we can call this section done, there is one other loose end to tie up. Since we are not explicitly passing in any cli arg options to the TaskQueueManager
, it will use the options that have been parsed into the ansible context (context.CLIARGS['args']
).
In section 2.1 we made sure that the context args contains --check
, but now we want to run the manual installation check tasks outside of --check
mode. Remember that context.CLIARGS['args']
is an ImmutableDict, so we can’t edit it in place. Instead, we can get the currently parsed options as a plain dictionary, modify it and create a new ImmutableDict object to override the current context:
The script should now execute the playbook tasks in --check
mode and the manually installed package tasks without the --check
option.
3. Writing a Callback Plugin
The final piece of the puzzle is to gather the results of the tasks from each host and print them in a pretty format at the end. Ansible provides callbacks at each stage of execution for developers to execute custom code. These are called Callback Plugins:
Callback plugins enable adding new behaviors to Ansible when responding to events.
You can activate a custom callback by either dropping it into a callback_plugins directory adjacent to your play, inside a role, or by putting it in one of the callback directory sources configured in ansible.cfg.
We don’t really want to create or modify the ansible conf or create additional directories if we can avoid it. Ideally this script should be runnable without creating any artifacts or modifying the hosts in ay way. We can define our plugin within our script and override the default stdout_plugin to be our one. Let’s go through this step by step:
3.1 Defining a new Callback Plugin
The Ansible developer docs provide examples of various callback plugins, but no real object-level api reference. Luckily the code is straightforward and easy to follow. To implement a Callback Plugin, we need to extend the CallbackBase abstract class. The docstring within the class says:
This is a base ansible callback class that does nothing. New callbacks should use this class as a base and override any callback methods they wish to execute custom actions.
There is no real documentation on what triggers each of the callback methods, but most can be guessed from the name. For eg.
v2_runner_on_ok
: is called on successful completion of a taskv2_runner_on_failed
: is called when a task failsv2_runner_on_skipped
: is called when the task is skippedv2_runner_on_unreachable
: is called when the host is unreachable
This is probably a good time to mention that I am running Ansible v2.9.1, and all the code referenced here is for that version. The methods above are also specifically for versions > 2, but they will call the relevant v1 methods if necessary.
3.2 Plugin Naming
The callback plugin class must be named CallbackModule
, as this is what the executor expects all plugins to be called when reading from one of the plugin directories. This is not mandatory however, if we are using the internal plumbing to execute the playbook ourselves (more on this in the next section).
Let’s define the basics of any callback plugin following one of the examples:
It is prudent to define CALLBACK_TYPE
because the docs say:
You can only have one plugin be the main manager of your console output. If you want to replace the default, you should define CALLBACK_TYPE = stdout in the subclass
3.3 Plugin Documentation
Now let’s add some doucmentation for the plugin before the class definition:
This will appear when running a user searches for the plugin documentation using ansible-doc. For example, check out the docs for the default callback plugin:
We won’t see the doco for our plugin using ansible-doc
yet because we haven’t saved it to one of the ansible plugin directories (for eg. ~/.ansible/plugins/). We don’t plan on doing this, but it is probably best practice to do this anyway in case we decide to later on.
3.4 Override Plugin Callback Methods
We want the behaviour our module to be exactly the same as the default stdout callback plugin, with the exception of the v2_runner_on_ok
and v2_playbook_on_stats
(called when tasks are complete and stats about to be displayed).
So, instead of implementing all the other methods ourselves, we can just extend the default plugin and override the above methods. So our plugin class now looks like:
Now to override our methods. First lets look at the callback for a successful task. We are interested in any successful package installs performed by a task in the target playbook. To filter these out, we can have a look at the object that is passed to the callback method as the result
arg. This is a TaskResult object. Having a look through the code, the self._task_fields
object seems promising, and could have the info we’re looking for to filter our task results. If we create a simple override method as follows and run an example playbook with it:
It should yield something like the following for a package install:
We can see that the action
attribute provides the module name of the task, which is useful to filter task info for package installs.
Now to handle the additional tasks we run to get manually installed packages. We can parse these from the package_check - get real manually installed packages
task from section 2.5:
The callback plugin will now build up a data structure containing packages that are installed by the playbook, as well as manually installed packages for each successful play (i.e host).
3.5 Using the custom callback plugin
So now that we have a (hopefully) working plugin, how do we test it with our script?
First we must define the callback class within the script, directly after the imports. Then we must initialise the plugin:
Call Ansible Playbook From Python
Then, we have to ensure that the PlaybookExecutor (used to run the target playbook - see 2.3) uses this plugin:
We also have to ensure that when we execute the plays for manually installed package checks (see 2.4), we also use this plugin by passing the stdout_callback
option to the TaskQueueManager:
The final step is to actually print out the summary of unmanaged packages being built up in the callback plugin:
If we run this, we should finally get what we are after:
3.6 Using Verbosity to Control Output
We now have a working script, but I think we can improve it. We are only really interested in the summary of packages, and reports of any failures or unreachable hosts. It would be nice if we can prevent everything else from being displayed unless the user provides the verbose flag.
Handily, there is a global Display()
object available through the CallbackBase object which provides a verbosity
flag based on the args in the current context. So we can override the callback methods responsible for printing lines we are not interested in and get them to do nothing unless this flag is set:
3.7 Restricting Tasks to Successful Hosts
There is one last loose end to tie up before we can call it a day. You may remember that back in section 2.4 we were using the directive hosts: all
to run the tasks associated with manual installs. This is not ideal because we only want these tasks to run on the hosts which the target playbook successfully installs packages on. We want to ignore any hosts that have been skipped, or are unreachable. We have already built up such a list of hosts in the callback module object. So we can use this ro restrict the hosts on which the tasks are run on:
And voila! We have finally achieved the mildly useful. You can find the complete script here. If you’ve made it this far, I hope you’ve learned a bit about the guts of Ansible along the way. I know I have.
Как я могу запустить playbook в скрипте python? Что эквивалентно следующему использованию модуля ansible в python:
Я смотрел на их документирование в http://docs.ansible.com/developing_api.html но у них очень ограниченные примеры.
3 ответов
уведомление об устаревании: этот пост не работает с ansible 2. API был изменен.
это покрыто Ansible documentation в разделе ' API Python.'
например, ansible -i hosts dbservers -m setup
реализуется через:
есть куча недокументированных параметров в __init__
метод бегуна (от ansible.runner
). Есть слишком много, чтобы перечислить inline, но я включил некоторые из параметров в это сообщение как догадка о том, что вы конкретно ищете.
например, приведенная выше команда, указывающая пользователя sudo и pass, будет:
для playbooks, посмотрите в playbook.PlayBook, который принимает аналогичный набор инициализаторов:
и может быть выполнен с помощью .run()
метод. например:
более надежное использование можно найти в ansible-playbook
файл.
насколько я знаю, перевод playbooks в модули Python немного сложнее, но документация, перечисленная выше, должна охватить вас, и вы можете повторно использовать парсер YAML, встроенный в Ansible для преобразования playbooks в переменные.
Я ответил на вопрос здесьПубликация здесь причина публикации ссылок не рекомендуется в сообществе. Надеюсь, это поможет.
документация на удивление отсутствует, и вам придется начать здесь
это, как говорится, Вот быстрый сценарий, который я взломал вместе, что удается запустить playbook.
Ansible Runner Python Free
вы смотрите на то, что официально не поддерживается или не рекомендуется, поэтому мало документации.
тем не менее, если вы действительно хотите продолжить этот курс, я бы начал с взлома сценария ansible-playbook в bin и обратного проектирования, что вы хотите сделать.