Dev Guide
- Developer Guide
- Ansible markup
- `ansible-core` project branches and tags
- Developer Guide
- Debugging modules
- Python API
- Developing collections
- Generating changelogs and porting guide entries in a collection
- Contributing to collections
- Creating collections
- Distributing collections
- Documenting collections
- Migrating Ansible content to a different collection
- Ansible collection creator path
- Resource reporting
- Using shared resources in collections
- Collection structure
- Testing collections
- Developing `ansible-core`
- Developing dynamic inventory
- Adding modules and plugins locally<span id="using_local_modules_and_plugins"></span>
- Using and developing module utilities
- Should you develop a module?<span id="developing_modules"></span>
- Conventions, tips, and pitfalls<span id="developing_modules_best_practices"></span>
- Contributing your module to an existing Ansible collection<span id="developing_modules_checklist"></span>
- Module format and documentation<span id="developing_modules_documenting"></span>
- Developing modules<span id="developing_modules_general"></span>
- Windows module development walkthrough
- Creating a new collection
- Developing plugins<span id="developing_plugins"></span>
- Ansible module architecture<span id="flow_modules"></span>
- Ansible and Python 3
- Rebasing a pull request
- Handling secrets in modules and plugins
- Migrating Roles to Roles in Collections on Galaxy
- The lifecycle of an Ansible module or plugin
- Ansible architecture
- Adjacent YAML documentation files
- Basic rules
- Grammar and Punctuation
- Ansible documentation style guide
- Preferred terminology
- Resources
- Writing documentation so search can find it
- Spelling - Word Usage - Common Words and Phrases to Use and Avoid
- Trademark Usage
- Voice Style
- Testing Ansible
- action-plugin-docs
- ansible-doc
- ansible-requirements
- bin-symlinks
- boilerplate
- changelog
- compile
- deprecated-config
- empty-init
- future-import-boilerplate
- ignores
- import
- Sanity Tests
- integration-aliases
- line-endings
- mypy
- no-assert
- no-basestring
- no-dict-iteritems
- no-dict-iterkeys
- no-dict-itervalues
- no-get-exception
- no-illegal-filenames
- no-main-display
- no-smart-quotes
- no-tests-as-filters
- no-unicode-literals
- no-unwanted-files
- obsolete-files
- package-data
- pep8
- pslint
- pylint
- pymarkdown
- release-names
- replace-urlopen
- required-and-default-attributes
- runtime-metadata
- shebang
- shellcheck
- symlinks
- test-constraints
- update-bundled
- use-argspec-type-path
- use-compat-six
- validate-modules
- yamllint
- Testing plugin documentation<span id="testing_module_documentation"></span>
- httptester
- Integration tests
- Testing Ansible and Collections
- Sanity Tests
- Unit Tests
- Unit Testing Ansible Modules
Developer Guide
Making Open Source More Inclusive
Red Hat is committed to replacing problematic language in our code, documentation, and web properties. We are beginning with these four terms: master, slave, blacklist, and whitelist. We ask that you open an issue or pull request if you come upon a term that we have missed. For more details, see our CTO Chris Wright's message.
Welcome to the Ansible Developer Guide!
Who should use this guide?
If you want to extend Ansible by using a custom module or plugin locally, creating a module or plugin, adding functionality to an existing module, or expanding test coverage, this guide is for you. We've included detailed information for developers on how to test and document modules, as well as the prerequisites for getting your module or plugin accepted into the main Ansible repository.
Find the task that best describes what you want to do:
-
I'm looking for a way to address a use case:
- I want to
add a custom plugin or module locally <developing_locally>. - I want to figure out if
developing a module is the right approach <module_dev_should_you>for my use case. - I want to understand
what a successful collection creator path looks like <developing_collections_path>. - I want to
develop a collection <developing_collections>. - I want to
contribute to an Ansible-maintained collection <contributing_maintained_collections>. - I want to
contribute to a community-maintained collection <hacking_collections>. - I want to
migrate a role to a collection <migrating_roles>.
- I want to
-
I've read the info above, and I'm sure I want to develop a module:
-
What do I need to know before I start coding?
-
I want to
set up my Python development environment <environment_setup>. -
I want to
get started writing a module <developing_modules_general>. -
I want to write a specific kind of module:
- a
network module <developing_modules_network> - a
Windows module <developing_modules_general_windows>. - an
Amazon module <ansible_collections.amazon.aws.docsite.dev_guide_intro>. - an oVirt/RHV module.
- a
VMware module <ansible_collections.community.vmware.docsite.vmware_ansible_devguide>.
- a
-
I want to
write a series of related modules <developing_modules_in_groups>that integrate Ansible with a new product (for example, a database, cloud provider, network platform, and so on).
-
-
I want to refine my code:
- I want to
debug my module code <debugging_modules>. - I want to
add tests <developing_testing>. - I want to
document my module <module_documenting>. - I want to
improve documentation by using Ansible markup <ansible_markup>. - I want to
document my set of modules for a network platform <documenting_modules_network>. - I want to follow
conventions and tips for clean, usable module code <developing_modules_best_practices>. - I want to
make sure my code runs on Python 2 and Python 3 <developing_python_3>.
- I want to
-
I want to work on other development projects:
- I want to
write a plugin <developing_plugins>. - I want to
connect Ansible to a new source of inventory <developing_inventory>. - I want to
deprecate an outdated module <deprecating_modules>.
- I want to
-
I want to contribute back to the Ansible project:
- I want to
understand how to contribute to Ansible <ansible_community_guide>. - I want to
contribute my module or plugin <developing_modules_checklist>. - I want to
understand the DCO agreement <developer_certificate_of_origin>for contributions to the Ansible Core and Ansible Documentation repositories.
- I want to
If you prefer to read the entire guide, here's a list of the pages in order.
developing_locally developing_modules developing_modules_general developing_modules_checklist developing_modules_best_practices developing_python_3 debugging developing_modules_documenting ansible_markup sidecar developing_modules_general_windows developing_modules_in_groups testing module_lifecycle developing_plugins developing_inventory developing_core developing_program_flow_modules developing_api developing_rebasing developing_module_utilities developing_secret_masking developing_collections_path developing_collections migrating_roles collections_galaxy_meta overview_architecture
Ansible markup
Ansible markup allows you to format and structure documentation for Ansible modules, plugins, and roles. It lets you add basic formatting to text, such as bold, italics, code, and horizontal lines, as well as create various references, such as URLs, hyperlinks, Ansible module references, and RST references. The Ansible markup language was extended in 2023, starting with ansible-core 2.15. It now lets you apply semantic markup for values, module/plugin options, return values, environment variables, and for referencing plugins.
This page documents the currently supported Ansible markup.
Semantic markup within module documentation
Use the semantic markup to highlight option names, option values, and environment variables. The markup processor formats these highlighted terms in a uniform way. With semantic markup, we can modify how the output looks without changing underlying code.
The correct formats for semantic markup are as follows:
O()for option names, whether mentioned alone or with values. For example:Required if O(state=present).andUse with O(force) to require secure access.V()for option values when mentioned alone. For example:Possible values include V(monospace) and V(pretty).RV()for return value names, whether mentioned alone or with values. For example:The module returns RV(changed=true) in case of changes.andUse the RV(stdout) return value for standard output.E()for environment variables. For example:If not set, the environment variable E(ACME_PASSWORD) will be used.
The parameters for these formatting functions can use escaping with backslashes: V(foo(bar="a\\b"\), baz) results in the formatted value foo(bar="a\b"), baz).
Rules for using O() and RV()
Rules for using O() and RV() are very strict. You must follow syntax rules so that documentation renderers can create hyperlinks for the options and return values, respectively.
The allowed syntaxes are as follows:
- To reference an option for the current plugin/module, or the entrypoint of the current role (inside role entrypoint documentation), use
O(option)andO(option=value). - To reference an option for another entrypoint
entrypointfrom inside role documentation, useO(entrypoint:option)andO(entrypoint:option=name). The entrypoint information can be ignored by the documentation renderer, turned into a link to that entrypoint, or even directly to the option of that entrypoint. - To reference an option for another plugin/module
plugin.fqcn.nameof typetype, useO(plugin.fqcn.name#type:option)andO(plugin.fqcn.name#type:option=value). For modules, usetype=module. The FQCN and plugin type can be ignored by the documentation renderer, turned into a link to that plugin, or even directly to the option of that plugin. - To reference an option for entrypoint
entrypointof another rolerole.fqcn.name, useO(role.fqcn.name#role:entrypoint:option)andO(role.fqcn.name#role:entrypoint:option=value). The FQCN and entrypoint information can be ignored by the documentation renderer, turned into a link to that entrypoint, or even directly to the option of that entrypoint. - To reference options that do not exist (for example, options that were removed in an earlier version), use
O(ignore:option)andO(ignore:option=value). Theignore:part will not be shown to the user by documentation rendering.
Option names can refer to suboptions by listing the path to the option separated by dots. For example, if you have an option foo with suboption bar, then you must use O(foo.bar) to reference that suboption. You can add array indications like O(foo[].bar) or even O(foo[-1].bar) to indicate specific list elements. Everything between [ and ] pairs will be ignored to determine the real name of the option. For example, O(foo[foo | length - 1].bar[]) results in the same link as O(foo.bar), but the text foo[foo | length - 1].bar[] displays instead of foo.bar.
The same syntaxes can be used for RV(), except that these will refer to return value names instead of option names; for example RV(ansible.builtin.service_facts#module:ansible_facts.services) refers to the ansible.builtin.service_facts#module:ansible_facts.services fact returned by the ansible.builtin.service_facts module <ansible.builtin.service_facts#module>.
Linking within module documentation
You can link from your module documentation to other module docs, other resources on docs.ansible.com, and resources elsewhere on the internet with the help of some pre-defined macros. The correct formats for these macros are:
R()for cross-references with a heading (supported since Ansible 2.10). For example:See R(Cisco IOS Platform Guide,ios_platform_options). Use the RST anchor for the cross-reference. Seeadding_anchors_rstfor details.- For links outside of your collection, use
R()if available. Otherwise, useU()orL()with full URLs (not relative links). - To refer to a group of modules in a collection, use
R(). When a collection is not the right granularity, useC(..), for example:Refer to the R(kubernetes.core collection, plugins_in_kubernetes.core) for information on managing kubernetes clusters.The C(win_*) modules (spread across several collections) allow you to manage various aspects of windows hosts.
- For links outside of your collection, use
L()for links with a heading. For example:See L(Ansible Automation Platform,https://www.ansible.com/products/automation-platform).As of Ansible 2.10, do not useL()for relative links between Ansible documentation and collection documentation.U()for URLs. For example:See U(https://www.ansible.com/products/automation-platform) for an overview.M()for module names. For example:See also M(ansible.builtin.yum) or M(community.general.apt_rpm).- FQCNs MUST be used, short names will create broken links; use
ansible.builtinfor modules in ansible-core.
- FQCNs MUST be used, short names will create broken links; use
P()for plugin names (supported since ansible-core 2.15). For example:See also P(ansible.builtin.file#lookup) or P(community.general.json_query#filter).- This can also reference roles:
P(community.sops.install#role). - FQCNs must be used, short names will create broken links; use
ansible.builtinfor plugins in ansible-core.
- This can also reference roles:
O()andRV()can also link, seethe section on their syntax <option_return_value_link_syntax>.
If you are creating your own documentation site, you will need to use the intersphinx extension to convert R(), M(), P(), O(), and RV() to the correct links.
Format macros within module documentation
While it is possible to use standard Ansible formatting macros to control the look of other terms in module documentation, you should do so sparingly.
Possible macros include the following:
C()formonospace(code) text. For example:This module functions like the unix command C(foo).B()for bold text.I()for italic text.HORIZONTALLINEfor a horizontal rule (the<hr>html tag) to separate long descriptions.
Note that C(), B(), and I() do not allow escaping, and thus cannot contain the value ) as it always ends the formatting sequence. If you need to use ) inside C(), we recommend to use V() instead; see the above section on semantic markup.
`ansible-core` project branches and tags
devel branch
All new development on the next version of ansible-core occurs exclusively in the devel branch, and all bugfixes to prior releases must first be merged to devel before being backported to one or more stable branches for inclusion in servicing releases. Around the Beta 1 milestone, a new stable-X.Y branch is cut from devel, which is then updated to host development of the X.Y+1 release. External automated testing of Ansible content from devel is not generally recommended.
stable-X.Y branches
All ansible-core X.Y.Z releases are created from a corresponding stable-X.Y branch. A release's stable branch is typically cut from devel around X.Y.0 beta 1 (when the release is feature complete). All further bugfixes (no enhancements!) must be made against devel and backported to applicable stable branches.
vX.Y.Z tags
Each ansible-core vX.Y.Z release is tagged from the release commit in the corresponding stable-X.Y branch, allowing access to the exact source used to create the release. As of ansible-core 2.13, the auto-generated GitHub tarball of the tag contents is considered the official canonical release artifact.
milestone branch
A milestone branch is a slow-moving stream of the devel branch, intended for external testing of ansible-core features under active development. As described in the ansible_core_roadmaps for a given release, development is typically split into three phases of decreasing duration, with larger and more invasive changes targeted to be merged to devel in earlier phases. The milestone branch is updated to the contents of devel at the end of each development phase. This allows testing of semi-stable unreleased features on a predictable schedule without the exposure to the potential instability of the daily commit "fire hose" from devel. When a release reaches the Beta 1 milestone, the milestone branch will be updated to the first devel commit after the version number has been increased. Further testing of the same release should be done from the new stable-X.Y branch that was created. If a severe issue that significantly affects community testing or stability is discovered in the milestone branch, the branch contents may require unscheduled adjustment, but not in a way that prevents fast-forward updates (for example, milestone-only commits will not be created or cherry-picked from devel).
The following example is for illustrative purposes only. See the ansible_core_roadmaps for accurate dates. For example, the milestone branch in 2.13 ansible-core roadmap updated as follows:
- 27-Sep-2021: 2.13 Development Phase 1 begins;
milestonecontents are updated to 2.12.0b1 with version number set to2.13.0.dev0. Automated content testing that includes version-specific ignore files (e.g.,ignore-2.12.txt) should copy them for the current version (e.g.,ignore-2.13.txt) before this point to ensure that automated sanity testing against themilestonebranch will continue to pass. - 13-Dec-2021: 2.13 Development Phase 2 begins;
milestonecontents are updated to the final commit from Development Phase 1 - 14-Feb-2022: 2.13 Development Phase 3 begins;
milestonecontents are updated to the final commit from Development Phase 2 - 11-Apr-2022:
stable-2.13branch created with results from Development Phase 3 and freeze.2.13.0b1is released fromstable-2.13. Automated content testing should continue 2.13 series testing against the new branch. Thedevelversion number is updated to2.14.0.dev0, andmilestoneis updated to that point.
Developer Guide
Making Open Source More Inclusive
Red Hat is committed to replacing problematic language in our code, documentation, and web properties. We are beginning with these four terms: master, slave, blacklist, and whitelist. We ask that you open an issue or pull request if you come upon a term that we have missed. For more details, see our CTO Chris Wright's message.
Welcome to the Ansible Developer Guide!
Who should use this guide?
If you want to extend Ansible by using a custom module or plugin locally, creating a module or plugin, adding functionality to an existing module, or expanding test coverage, this guide is for you. We've included detailed information for developers on how to test and document modules, as well as the prerequisites for getting your module or plugin accepted into the main Ansible repository.
Find the task that best describes what you want to do:
-
I'm looking for a way to address a use case:
- I want to
add a custom plugin or module locally <developing_locally>. - I want to figure out if
developing a module is the right approach <module_dev_should_you>for my use case. - I want to understand
what a successful collection creator path looks like <developing_collections_path>. - I want to
develop a collection <developing_collections>. - I want to
contribute to an Ansible-maintained collection <contributing_maintained_collections>. - I want to
contribute to a community-maintained collection <hacking_collections>. - I want to
migrate a role to a collection <migrating_roles>.
- I want to
-
I've read the info above, and I'm sure I want to develop a module:
-
What do I need to know before I start coding?
-
I want to
set up my Python development environment <environment_setup>. -
I want to
get started writing a module <developing_modules_general>. -
I want to write a specific kind of module:
- a
network module <developing_modules_network> - a
Windows module <developing_modules_general_windows>.
- a
-
I want to
write a series of related modules <developing_modules_in_groups>that integrate Ansible with a new product (for example, a database, cloud provider, network platform, and so on).
-
-
I want to refine my code:
- I want to
debug my module code <debugging_modules>. - I want to
add tests <developing_testing>. - I want to
document my module <module_documenting>. - I want to
improve documentation by using Ansible markup <ansible_markup>. - I want to
document my set of modules for a network platform <documenting_modules_network>. - I want to follow
conventions and tips for clean, usable module code <developing_modules_best_practices>. - I want to
make sure my code runs on Python 2 and Python 3 <developing_python_3>.
- I want to
-
I want to work on other development projects:
- I want to
write a plugin <developing_plugins>. - I want to
connect Ansible to a new source of inventory <developing_inventory>. - I want to
deprecate an outdated module <deprecating_modules>.
- I want to
-
I want to contribute back to the Ansible project:
- I want to
understand how to contribute to Ansible <ansible_community_guide>. - I want to
contribute my module or plugin <developing_modules_checklist>. - I want to
understand the DCO agreement <developer_certificate_of_origin>for contributions to the Ansible Core and Ansible Documentation repositories.
- I want to
If you prefer to read the entire guide, here's a list of the pages in order.
developing_locally developing_modules developing_modules_general developing_modules_checklist developing_modules_best_practices developing_python_3 debugging developing_modules_documenting ansible_markup sidecar developing_modules_general_windows developing_modules_in_groups testing module_lifecycle developing_plugins developing_inventory developing_core developing_program_flow_modules developing_api developing_rebasing developing_module_utilities developing_secret_masking developing_collections_path developing_collections migrating_roles collections_galaxy_meta overview_architecture
Debugging modules
Detailed debugging steps
Ansible modules are put together as a zip file consisting of the module file and the various Python module boilerplate inside of a wrapper script. To see what is actually happening in the module, you need to extract the file from the wrapper. The wrapper script provides helper methods that let you do that.
The following steps use localhost as the target host, but you can use the same steps to debug against remote hosts as well. For a simpler approach to debugging without using the temporary files, see simple debugging <simple_debugging>.
-
Set
ANSIBLE_KEEP_REMOTE_FILESto1on the control host so Ansible will keep the remote module files instead of deleting them after the module finishes executing. Use the-vvvoption to make Ansible more verbose. This will display the file name of the temporary module file.$ ANSIBLE_KEEP_REMOTE_FILES=1 ansible localhost -m ping -a 'data=debugging_session' -vvv <127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: badger <127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1461434734.35-235318071810595 `" && echo "` echo $HOME/.ansible/tmp/ansible-tmp-1461434734.35-235318071810595 `" )' <127.0.0.1> PUT /var/tmp/tmpjdbJ1w TO /home/badger/.ansible/tmp/ansible-tmp-1461434734.35-235318071810595/AnsiballZ_ping.py <127.0.0.1> EXEC /bin/sh -c 'LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /home/badger/.ansible/tmp/ansible-tmp-1461434734.35-235318071810595/AnsiballZ_ping.py && sleep 0' localhost | SUCCESS => { "changed": false, "invocation": { "module_args": { "data": "debugging_session" }, "module_name": "ping" }, "ping": "debugging_session" } -
Navigate to the temporary directory from the previous step. If the previous command was run against a remote host, connect to that host first before trying to navigate to the temporary directory.
$ ssh remotehost # only if not debugging against localhost $ cd /home/badger/.ansible/tmp/ansible-tmp-1461434734.35-235318071810595 -
Run the wrapper's
explodecommand to turn the string into some Python files that you can work with.$ python AnsiballZ_ping.py explode Module expanded into: /home/badger/.ansible/tmp/ansible-tmp-1461434734.35-235318071810595/debug_dirIf you want to examine the wrapper file you can. It will show a small Python script with a large base64 encoded string. The string contains the module to execute.
-
When you look into the temporary directory you'll see a structure like this:
├── AnsiballZ_ping.py └── debug_dir ├── ansible │ ├── __init__.py │ ├── module_utils │ │ ├── __init__.py │ │ ├── _text.py │ │ ├── basic.py │ │ ├── common │ │ ├── compat │ │ ├── distro │ │ ├── parsing │ │ ├── pycompat24.py │ │ └── six │ └── modules │ ├── __init__.py │ └── ping.py └── argsAnsiballZ_ping.pyis the Python script with the module code stored in a base64 encoded string. It contains various helper functions for executing the module.ping.pyis the code for the module itself. You can modify this code to see what effect it would have on your module, or for debugging purposes.- The
argsfile contains a JSON string. The string is a dictionary containing the module arguments and other variables that Ansible passes into the module to change its behavior. Modify this file to change the parameters passed to the module. - The
ansibledirectory contains the module code inmodulesas well as code fromansible.module_utilsthat is used by the module. Ansible includes files for anyansible.module_utilsimports in the module but not any files from any other module. If your module usesansible.module_utils.urlAnsible will include it for you. But if your module includes requests, then you'll have to make sure that the Python requests library is installed on the system before running the module.
You can modify files in this directory if you suspect that the module is having a problem in some of this boilerplate code rather than in the module code you have written.
-
Once you edit the code or arguments in the exploded tree, use the
executesubcommand to run it:$ python AnsiballZ_ping.py execute {"invocation": {"module_args": {"data": "debugging_session"}}, "changed": false, "ping": "debugging_session"}This subcommand inserts the absolute path to
debug_diras the first item insys.pathand invokes the script using the arguments in theargsfile. You can continue to run the module like this until you understand the problem. Then you can copy the changes back into your real module file and test that the real module works by using theansibleoransible-playbook.
Simple debugging
The easiest way to run a debugger in a module, either local or remote, is to use madbg. Add import madbg; madbg.set_trace() in the module code on the control node at the desired break point. To connect to the debugger, run madbg connect. See the madbg documentation for how to specify the host and port. If connecting to a remote node, make sure to use a port that is allowed by any firewall between the control node and the remote node.
This technique should work with any remote debugger, but we do not guarantee any particular remote debugging tool will work.
The q library is another very useful debugging tool.
Since print() statements do not work inside modules, raising an exception is a good approach if you just want to see some specific data. Put raise Exception(some_value) somewhere in the module and run it normally. Ansible will handle this exception, pass the message back to the control node, and display it.
Python API
Topics
Attention
The Ansible API is intended for internal Ansible use. Ansible may make changes to this API at any time that could break backward compatibility with older versions of the API. Because of this, external use is not supported by Ansible. If you want to use Python API only for executing playbooks or modules, consider ansible-runner first.
If you would like to use Ansible programmatically from a language other than Python, trigger events asynchronously, or have access control and logging demands, please see the AWX project.
developing_inventory
Developing dynamic inventory integrations
developing_modules_general
Getting started on developing a module
developing_plugins
How to develop plugins
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Developing collections
Collections are a distribution format for Ansible content. You can package and distribute playbooks, roles, modules, and plugins using collections. A typical collection addresses a set of related use cases. For example, the cisco.ios collection automates management of Cisco IOS devices.
You can create a collection and publish it to Ansible Galaxy or to a private Automation Hub instance. You can publish certified collections to the Red Hat Automation Hub, part of the Red Hat Ansible Automation Platform.
Examine the developing_collections_path to understand how to go from creating a collection to having it included in the Ansible package distribution.
developing_collections_creating developing_collections_shared developing_collections_testing developing_collections_resource_reporting developing_collections_distributing developing_collections_documenting
developing_collections_migrating developing_collections_contributing developing_collections_changelogs
developing_collections_structure collections_galaxy_meta
For instructions on developing modules, see developing_modules_general.
collections
Learn how to install and use collections in playbooks and roles
contributing_maintained_collections
Guidelines for contributing to selected collections
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Generating changelogs and porting guide entries in a collection
You can create and share changelog and porting guide entries for your collection. If your collection is part of the Ansible Community package, we recommend that you use the antsibull-changelog tool to generate Ansible-compatible changelogs. The Ansible changelog uses the output of this tool to collate all the collections included in an Ansible release into one combined changelog for the release.
Ansible here refers to the Ansible 2.10 or later release that includes a curated set of collections.
Understanding antsibull-changelog
The antsibull-changelog tool allows you to create and update changelogs for Ansible collections that are compatible with the combined Ansible changelogs. This is an update to the changelog generator used in prior Ansible releases. The tool adds three new changelog fragment categories: breaking_changes, security_fixes and trivial. The tool also generates the changelog.yaml file that Ansible uses to create the combined CHANGELOG.rst file and Porting Guide for the release.
See changelogs_how_to and the antsibull-changelog documentation for complete details.
The collection maintainers set the changelog policy for their collections. See the individual collection contributing guidelines for complete details.
Generating changelogs
To initialize changelog generation:
- Install
antsibull-changelog:pip install antsibull-changelog. - Initialize changelogs for your repository:
antsibull-changelog init <path/to/your/collection>. - Optionally, edit the
changelogs/config.yamlfile to customize the location of the generated changelog.rstfile or other options. See Bootstrapping changelogs for collections for details.
To generate changelogs from the changelog fragments you created:
- Optionally, validate your changelog fragments:
antsibull-changelog lint. - Generate the changelog for your release:
antsibull-changelog release [--version version_number].
Add the --reload-plugins option if you ran the antsibull-changelog release command previously and the version of the collection has not changed. antsibull-changelog caches the information on all plugins and does not update its cache until the collection version changes.
Porting Guide entries from changelog fragments
The Ansible changelog generator automatically adds several changelog fragment categories to the Ansible Porting Guide:
major_changesbreaking_changesdeprecated_featuresremoved_features
Including collection changelogs into Ansible
If your collection is part of Ansible, use one of the following three options to include your changelog into the Ansible release changelog:
- Use the
antsibull-changelogtool. - If are not using this tool, include the properly formatted
changelog.yamlfile into your collection. See the changelog.yaml format for details. - Add a link to own changelogs or release notes in any format by opening an issue at https://github.com/ansible-community/ansible-build-data/ with the HTML link to that information.
For the first two options, Ansible pulls the changelog details from Galaxy so your changelogs must be included in the collection version on Galaxy that is included in the upcoming Ansible release.
collection_changelogs
Learn how to create good changelog fragments.
collections
Learn how to install and use collections.
contributing_maintained_collections
Guidelines for contributing to selected collections
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Contributing to collections
If you want to add functionality to an existing collection, modify a collection you are using to fix a bug, or change the behavior of a module in a collection, clone the Git repository for that collection and make changes on a branch. You can combine changes to a collection with a local checkout of Ansible (source hacking/env-setup). You should first check the collection repository to see if it has specific contribution guidelines. These are typically listed in the README.md or CONTRIBUTING.md files within the repository. See collection_quickstart for more general guidelines and testing_running_locally for testing guidelines.
Contributing to a collection: community.general
These instructions apply to collections hosted in the ansible_collections GitHub organization. For other collections, especially for collections not hosted on GitHub, check the README.md of the collection for information on contributing to it.
This example uses the community.general collection. To contribute to other collections in the same GitHub org, replace the folder names community and general with the namespace and collection name of a different collection.
Prerequisites
- Include
~/dev/ansible/collections/inCOLLECTIONS_PATHS - If that path mentions multiple directories, make sure that no other directory earlier in the search path contains a copy of
community.general.
Creating a PR
- Create the directory
~/dev/ansible/collections/ansible_collections/community:
mkdir -p ~/dev/ansible/collections/ansible_collections/community
- Clone the community.general Git repository or a fork of it into the directory
general:
cd ~/dev/ansible/collections/ansible_collections/community
git clone git@github.com:ansible-collections/community.general.git general
- If you clone from a fork, add the original repository as a remote
upstream:
cd ~/dev/ansible/collections/ansible_collections/community/general
git remote add upstream git@github.com:ansible-collections/community.general.git
- Create a branch and commit your changes on the branch.
- Remember to add tests for your changes, see
testing_collections. - Push your changes to your fork of the collection and create a Pull Request.
You can test your changes by using this checkout of community.general in playbooks and roles with whichever version of Ansible you have installed locally, including a local checkout of ansible/ansible's devel branch.
collections
Learn how to install and use collections.
contributing_maintained_collections
Guidelines for contributing to selected collections
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Creating collections
To create a collection:
- Create a
new collection<creating_new_collections>, optionally using a customcollection template<creating_collection_from_custom_template>, with theansible-galaxy collection initcommand. - Add modules and other content to the collection.
- Build the collection into a collection artifact with
ansible-galaxy collection build<building_collections>. - Publish the collection artifact to Galaxy with
ansible-galaxy collection publish<publishing_collections>.
A user can then install your collection on their systems.
Naming your collection
Collection names consist of a namespace and a name, separated by a period (.). Both namespace and name should be valid Python identifiers. This means that they should consist of ASCII letters, digits, and underscores.
Usually namespaces and names use lower-case letters, digits, and underscores, but no upper-case letters.
You should check Ansible Galaxy's namespace list to ensure that your desired namespace is not already registered. If you use a namespace or full collection name that conflicts with another collection on Galaxy, then the ansible-galaxy collection install can install the wrong collection.
There are a few special namespaces:
ansible
The ansible namespace is owned by Red Hat and reserved for official Ansible collections. Two special members are the synthetic
ansible.builtinandansible.legacycollections. These cannot be found on Ansible Galaxy, but are built-in into ansible-core.
community
The community namespace is owned by the Ansible community. Collections from this namespace generally live in the GitHub ansible-collection organization. If you want to create a collection in this namespace,
request<request_coll_repo>it on the forum.
local
The local namespace does not contain any collection on Ansible Galaxy, and the intention is that this will never change. You can use the
localnamespace for collections that are locally on your machine or locally in your Git repositories, without having to fear collisions with actually existing collections on Ansible Galaxy.
To request a new namespace in Galaxy, do the following:
- Log in to the Ansible forum.
- Submit a namespace request.
The Red Hat Community and Partner Engineering team will be notified of your namespace request and create it for you.
Creating a new collection
Create your collection skeleton in a path that includes ansible_collections, for example collections/ansible_collections/.
To start a new collection, run the following command in your collections directory:
ansible_collections#> ansible-galaxy collection init my_namespace.my_collection
Both the namespace and collection names use the same strict set of requirements. Both are limited to alphanumeric characters and underscores, must have a minimum length of three characters, and cannot start with an underscore.
It will create the structure [my_namespace]/[my_collection]/[collection skeleton].
Hint
If Git is used for version control, the corresponding repository should be initialized in the collection directory.
Once the collection exists, you can populate the directories with the content you want inside the collection. See ansible-collections GitHub Org to get a better idea of what you can place inside a collection.
Reference: the ansible-galaxy collection command
Currently the ansible-galaxy collection command implements the following sub commands:
init: Create a basic collection based on the default template included with Ansible or your own template.build: Create a collection artifact that can be uploaded to Galaxy or your own repository.publish: Publish a built collection artifact to Galaxy.install: Install one or more collections.
To learn more about the ansible-galaxy command-line tool, see the ansible-galaxy man page.
Creating a collection from a custom template
The built-in collection template is a simple example of a collection that works with ansible-core, but if you want to simplify your development process you may want to create a custom collection template to pass to ansible-galaxy collection init.
A collection skeleton is a directory that looks like a collection directory but any .j2 files (excluding those in templates/ and roles/*/templates/) will be templated by ansible-galaxy collection init. The skeleton's galaxy.yml.j2 file should use the variables namespace and collection_name which are derived from ansible-galaxy init namespace.collection_name, and will populate the metadata in the initialized collection's galaxy.yml file. There are a few additional variables available by default (for example, version is 1.0.0), and these can be supplemented/overridden using --extra-vars.
An example galaxy.yml.j2 file that accepts an optional dictionary variable dependencies could look like this:
namespace: {{ namespace }}
name: {{ collection_name }}
version: {{ (version|quote) is version('0.0.0', operator='gt', version_type='semver')|ternary(version, undef('version must be a valid semantic version greater than 0.0.0')) }}
dependencies: {{ dependencies | default({}, true) }}
To initialize a collection using the new template, pass the path to the skeleton with ansible-galaxy collection init:
ansible_collections#> ansible-galaxy collection init --collection-skeleton /path/to/my/namespace/skeleton --extra-vars "@my_vars_file.json" my_namespace.my_collection
You can configure the collection skeleton using collection_skeleton in ansible.cfg. To ignore files and directories in the collection skeleton, use the collection_skeleton_ignore option in ansible.cfg.
[galaxy]
collection_skeleton = /path/to/collection_skeleton
collection_skeleton_ignore = ^.git$,^.*/.git_keep$,^\./CLAUDE\.md$
The collection_skeleton_ignore option is a list of regular expressions to match files and directories to ignore. The regex are matched against the relative path of the file or directory from the skeleton directory.
Before ansible-core 2.17, collection skeleton templating is limited to the few hardcoded variables including namespace, collection_name, and version.
The default collection skeleton uses an internal filter comment_ify that isn't accessibly to --collection-skeleton. Use ansible-doc -t filter|test --list to see available plugins.
Creating collections with ansible-creator
ansible-creator is designed to quickly scaffold an Ansible collection project.
The Ansible Development Tools package offers a convenient way to install ansible-creator along with a curated set of tools for developing automation content.
After installing ansible-creator you can initialize a project in one of the following ways:
- Use the
initsubcommand. - Use
ansible-creatorwith the Ansible extension in Visual Studio Code.
collections
Learn how to install and use collections.
collection_structure
Directories and files included in the collection skeleton
Ansible Development Tools (ADT)
Python package of tools to create and test Ansible content.
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Distributing collections
A collection is a distribution format for Ansible content. A typical collection contains modules and other plugins that address a set of related use cases. For example, a collection might automate administering a particular database. A collection can also contain roles and playbooks.
Before distributing your collection, ensure you have updated the galaxy.yml file. See collection_structure for details.
To distribute your collection and allow others to use it, you can publish your collection on one or more distribution server. Distribution servers include:
| Distribution server | Collections accepted |
|---|---|
| Ansible Galaxy | All collections |
Pulp 3 Galaxy |
All collections, supports signed collections |
| Red Hat Automation Hub | Only collections certified by Red Hat, supports signed collections |
| Privately hosted Automation Hub | Collections authorized by the owners |
Distributing collections involves four major steps:
- Initial configuration of your distribution server or servers
- Building your collection tarball
- Preparing to publish your collection
- Publishing your collection
Initial configuration of your distribution server or servers
Configure a connection to one or more distribution servers so you can publish collections there. You only need to configure each distribution server once. You must repeat the other steps (building your collection tarball, preparing to publish, and publishing your collection) every time you publish a new collection or a new version of an existing collection.
- Create a namespace on each distribution server you want to use.
- Get an API token for each distribution server you want to use.
- Specify the API token for each distribution server you want to use.
Creating a namespace
You must upload your collection into a namespace on each distribution server. If you have a login for Ansible Galaxy, your Ansible Galaxy username is usually also an Ansible Galaxy namespace.
Namespaces on Ansible Galaxy cannot include hyphens. If you have a login for Ansible Galaxy that includes a hyphen, your Galaxy username is not also a Galaxy namespace. For example, awesome-user is a valid username for Ansible Galaxy, but it is not a valid namespace.
You can create additional namespaces on Ansible Galaxy if you choose. For Red Hat Automation Hub and private Automation Hub you must create a namespace before you can upload your collection. To create a namespace:
- To create a namespace on Galaxy, see Galaxy namespaces on the Galaxy docsite for details.
- To create a namespace on Red Hat Automation Hub, see the Red Hat automation content documentation.
Specify the namespace in the galaxy.yml file for each collection. For more information on the galaxy.yml file, see collections_galaxy_meta.
Getting your API token
An API token authenticates your connection to each distribution server. You need a separate API token for each distribution server. Use the correct API token to connect to each distribution server securely and protect your content.
To get your API token:
- To get an API token for Galaxy, see the Galaxy documentation.
- To get an API token for Automation Hub, see the Red Hat automation content documentation.
Specifying your API token and distribution server
Each time you publish a collection, you must specify the API token and the distribution server to create a secure connection. You have two options for specifying the token and distribution server:
- You can configure the token in configuration, as part of a
galaxy_server_listentry in youransible.cfgfile. Using configuration is the most secure option. - You can pass the token at the command line as an argument to the
ansible-galaxycommand. If you pass the token at the command line, you can specify the server at the command line, by using the default setting, or by setting the server in configuration. Passing the token at the command line is insecure, because typing secrets at the command line may expose them to other users on the system.
Specifying the token and distribution server in configuration
By default, Ansible Galaxy is configured as the only distribution server. You can add other distribution servers and specify your API token or tokens in configuration by editing the galaxy_server_list section of your ansible.cfg file. This is the most secure way to manage authentication for distribution servers. Specify a URL and token for each server. For example:
[galaxy]
server_list = release_galaxy
[galaxy_server.release_galaxy]
url=https://galaxy.ansible.com/
token=abcdefghijklmnopqrtuvwxyz
You cannot use apt-key with any servers defined in your galaxy_server_list <galaxy_server_config>. See galaxy_server_config for complete details.
Specifying the token at the command line
You can specify the API token at the command line using the --token argument of the ansible-galaxy command. There are three ways to specify the distribution server when passing the token at the command line:
- using the
--serverargument of theansible-galaxycommand - relying on the default (https://galaxy.ansible.com)
- setting a server in configuration by creating a
GALAXY_SERVERsetting in youransible.cfgfile
For example:
ansible-galaxy collection publish path/to/my_namespace-my_collection-1.0.0.tar.gz --token abcdefghijklmnopqrtuvwxyz
Using the --token argument is insecure. Passing secrets at the command line may expose them to others on the system.
Building your collection tarball
After configuring one or more distribution servers, build a collection tarball. The collection tarball is the published artifact, the object that you upload and other users download to install your collection. To build a collection tarball:
- Review all settings in your
galaxy.ymlfile. Seecollections_galaxy_metafor details. Ensure you have updated the version number. Each time you publish your collection, it must have a new version number. You cannot make changes to existing versions of your collection on a distribution server. If you try to upload the same collection version more than once, the distribution server returns the errorCode: conflict.collection_exists. Collections follow semantic versioning rules. For more information on versions, seecollection_versions. For more information on thegalaxy.ymlfile, seecollections_galaxy_meta. - Run
ansible-galaxy collection buildfrom inside the top-level directory of the collection. For example:
collection_dir#> ansible-galaxy collection build
This command builds a tarball of the collection in the current directory, which you can upload to your selected distribution server:
my_collection/
├── galaxy.yml
├── ...
├── my_namespace-my_collection-1.0.0.tar.gz
└── ...
* To reduce the size of collections, certain files and folders are excluded from the collection tarball by default. See ignoring_files_and_folders_collections if your collection directory contains other files you want to exclude. * The current Galaxy maximum tarball size is 20 MB.
You can upload your tarball to one or more distribution servers. You can also distribute your collection locally by copying the tarball to install your collection directly on target systems.
Ignoring files and folders
You can exclude files from your collection with either build_ignore <build_ignore> or manifest_directives. For more information on the galaxy.yml file, see collections_galaxy_meta.
Include all, with explicit ignores
By default, the build step includes all the files in the collection directory in the tarball except for the following:
galaxy.yml*.pyc*.retrytests/output- previously built tarballs in the root directory
- various version control directories such as
.git/
To exclude other files and folders from your collection tarball, set a list of file glob-like patterns in the build_ignore key in the collection's galaxy.yml file. These patterns use the following special characters for wildcard matching:
*: Matches everything?: Matches any single character[seq]: Matches any character in sequence[!seq]:Matches any character not in sequence
For example, to exclude the sensitive folder within the playbooks folder as well any .tar.gz archives, set the following in your galaxy.yml file:
build_ignore:
- playbooks/sensitive
- '*.tar.gz'
The build_ignore feature is only supported with ansible-galaxy collection build in Ansible 2.10 or newer.
Manifest Directives
2.14
The galaxy.yml file supports manifest directives that are historically used in Python packaging, as described in MANIFEST.in commands.
The use of manifest requires installing the optional distlib Python dependency.
The manifest feature is only supported with ansible-galaxy collection build in ansible-core 2.14 or newer, and is mutually exclusive with build_ignore.
For example, to exclude the sensitive folder within the playbooks folder as well as any .tar.gz archives, set the following in your galaxy.yml file:
manifest:
directives:
- recursive-exclude playbooks/sensitive **
- global-exclude *.tar.gz
By default, the MANIFEST.in style directives would exclude all files by default, but there are default directives in place. Those default directives are described below. To see the directives in use during build, pass -vvv with the ansible-galaxy collection build command.
include meta/*.yml
include *.txt *.md *.rst COPYING LICENSE
recursive-include tests **
recursive-include docs **.rst **.yml **.yaml **.json **.j2 **.txt
recursive-include roles **.yml **.yaml **.json **.j2
recursive-include playbooks **.yml **.yaml **.json
recursive-include changelogs **.yml **.yaml
recursive-include plugins */**.py
recursive-include plugins/become **.yml **.yaml
recursive-include plugins/cache **.yml **.yaml
recursive-include plugins/callback **.yml **.yaml
recursive-include plugins/cliconf **.yml **.yaml
recursive-include plugins/connection **.yml **.yaml
recursive-include plugins/filter **.yml **.yaml
recursive-include plugins/httpapi **.yml **.yaml
recursive-include plugins/inventory **.yml **.yaml
recursive-include plugins/lookup **.yml **.yaml
recursive-include plugins/netconf **.yml **.yaml
recursive-include plugins/shell **.yml **.yaml
recursive-include plugins/strategy **.yml **.yaml
recursive-include plugins/test **.yml **.yaml
recursive-include plugins/vars **.yml **.yaml
recursive-include plugins/modules **.ps1 **.yml **.yaml
recursive-include plugins/module_utils **.ps1 **.psm1 **.cs
# manifest.directives from galaxy.yml inserted here
exclude galaxy.yml galaxy.yaml MANIFEST.json FILES.json <namespace>-<name>-*.tar.gz
recursive-exclude tests/output **
global-exclude /.* /__pycache__
<namespace>-<name>-*.tar.gz is expanded with the actual namespace and name.
The manifest.directives supplied in galaxy.yml are inserted after the default includes and before the default excludes.
To enable the use of manifest directives without supplying your own, insert either manifest: {} or manifest: null in the galaxy.yml file and remove any use of build_ignore.
If the default manifest directives do not meet your needs, you can set manifest.omit_default_directives to a value of true in galaxy.yml. You then must specify a full compliment of manifest directives in galaxy.yml. The defaults documented above are a good starting point.
Below is an example where the default directives are not included.
manifest:
directives:
- include meta/runtime.yml
- include README.md LICENSE
- recursive-include plugins */**.py
- exclude galaxy.yml MANIFEST.json FILES.json <namespace>-<name>-*.tar.gz
- recursive-exclude tests/output **
omit_default_directives: true
Signing a collection
You can include a GnuPG signature with your collection on a Pulp 3 Galaxy server. See Enabling collection signing for details.
You can manually generate detached signatures for a collection using the gpg CLI using the following step. This step assume you have generated a GPG private key, but do not cover this process.
ansible-galaxy collection build
tar -Oxzf namespace-name-1.0.0.tar.gz MANIFEST.json | gpg --output namespace-name-1.0.0.asc --detach-sign --armor --local-user email@example.com -
Preparing to publish your collection
Each time you publish your collection, you must create a new version <collection_versions> on the distribution server. After you publish a version of a collection, you cannot delete or modify that version. To avoid unnecessary extra versions, check your collection for bugs, typos, and other issues locally before publishing:
- Install the collection locally.
- Review the locally installed collection before publishing a new version.
Installing your collection locally
You have two options for installing your collection locally:
- Install your collection locally from the tarball.
- Install your collection locally from your Git repository.
Installing your collection locally from the tarball
To install your collection locally from the tarball, run ansible-galaxy collection install and specify the collection tarball. You can optionally specify a location using the -p flag. For example:
collection_dir#> ansible-galaxy collection install my_namespace-my_collection-1.0.0.tar.gz -p ./collections
Install the tarball into a directory configured in COLLECTIONS_PATHS so Ansible can easily find and load the collection. If you do not specify a path value, ansible-galaxy collection install installs the collection in the first path defined in COLLECTIONS_PATHS.
Installing your collection locally from a Git repository
To install your collection locally from a Git repository, specify the repository and the branch you want to install:
collection_dir#> ansible-galaxy collection install git+https://github.com/org/repo.git,devel
You can install a collection from a git repository instead of from Galaxy or Automation Hub. As a developer, installing from a git repository lets you review your collection before you create the tarball and publish the collection. As a user, installing from a git repository lets you use collections or versions that are not in Galaxy or Automation Hub yet. This functionality is meant as a minimal shortcut for developers of content as previously described, and git repositories may not support the full set of features from the ansible-galaxy CLI. In complex cases, a more flexible option may be to git clone the repository into the correct file structure of the collection installation directory.
The repository must contain a galaxy.yml or MANIFEST.json file. This file provides metadata such as the version number and namespace of the collection.
Installing a collection from a git repository at the command line
To install a collection from a git repository at the command line, use the URI of the repository instead of a collection name or path to a tar.gz file. Use the prefix git+, unless you're using SSH authentication with the user git (for example, git@github.com:ansible-collections/ansible.windows.git). You can specify a branch, commit, or tag using the comma-separated git commit-ish syntax.
For example:
# Install a collection in a repository using the latest commit on the branch 'devel'
ansible-galaxy collection install git+https://github.com/organization/repo_name.git,devel
# Install a collection from a private GitHub repository
ansible-galaxy collection install git@github.com:organization/repo_name.git
# Install a collection from a local git repository
ansible-galaxy collection install git+file:///home/user/path/to/repo_name.git
Embedding credentials into a git URI is not secure. Use safe authentication options to prevent your credentials from being exposed in logs or elsewhere.
- Use SSH authentication
- Use netrc authentication
- Use http.extraHeader in your git configuration
- Use url.<base>.pushInsteadOf <https://git-scm.com/docs/git-config#Documentation/git-config.txt-urlltbasegtpushInsteadOf> in your git configuration
Specifying the collection location within the git repository
When you install a collection from a git repository, Ansible uses the collection galaxy.yml or MANIFEST.json metadata file to build the collection. By default, Ansible searches two paths for collection galaxy.yml or MANIFEST.json metadata files:
- The top level of the repository.
- Each directory in the repository path (one level deep).
If a galaxy.yml or MANIFEST.json file exists in the top level of the repository, Ansible uses the collection metadata in that file to install an individual collection.
├── galaxy.yml
├── plugins/
│ ├── lookup/
│ ├── modules/
│ └── module_utils/
└─── README.md
If a galaxy.yml or MANIFEST.json file exists in one or more directories in the repository path (one level deep), Ansible installs each directory with a metadata file as a collection. For example, Ansible installs both collection1 and collection2 from this repository structure by default:
├── collection1
│ ├── docs/
│ ├── galaxy.yml
│ └── plugins/
│ ├── inventory/
│ └── modules/
└── collection2
├── docs/
├── galaxy.yml
├── plugins/
| ├── filter/
| └── modules/
└── roles/
If you have a different repository structure or only want to install a subset of collections, you can add a fragment to the end of your URI (before the optional comma-separated version) to indicate the location of the metadata file or files. The path should be a directory, not the metadata file itself. For example, to install only collection2 from the example repository with two collections:
ansible-galaxy collection install git+https://github.com/organization/repo_name.git#/collection2/
In some repositories, the main directory corresponds to the namespace:
namespace/
├── collectionA/
| ├── docs/
| ├── galaxy.yml
| ├── plugins/
| │ ├── README.md
| │ └── modules/
| ├── README.md
| └── roles/
└── collectionB/
├── docs/
├── galaxy.yml
├── plugins/
│ ├── connection/
│ └── modules/
├── README.md
└── roles/
You can install all collections in this repository, or install one collection from a specific commit:
# Install all collections in the namespace
ansible-galaxy collection install git+https://github.com/organization/repo_name.git#/namespace/
# Install an individual collection using a specific commit
ansible-galaxy collection install git+https://github.com/organization/repo_name.git#/namespace/collectionA/,7b60ddc245bc416b72d8ea6ed7b799885110f5e5
Reviewing your collection
Review the collection:
- Run a playbook that uses the modules and plugins in your collection. Verify that new features and functionality work as expected. For examples and more details see
Using collections <using_collections>. - Check the documentation for typos.
- Check that the version number of your tarball is higher than the latest published version on the distribution server or servers.
- If you find any issues, fix them and rebuild the collection tarball.
Understanding collection versioning
The only way to change a collection is to release a new version. The latest version of a collection (by highest version number) is the version displayed everywhere in Galaxy and Automation Hub. Users can still download older versions.
Follow semantic versioning when setting the version for your collection. In summary:
- Increment the major version number,
xofx.y.z, for an incompatible API change. - Increment the minor version number,
yofx.y.z, for new functionality in a backwards compatible manner (for example new modules/plugins, parameters, return values). - Increment the patch version number,
zofx.y.z, for backwards compatible bug fixes.
Read the official Semantic Versioning documentation for details and examples.
Publishing your collection
The last step in distributing your collection is publishing the tarball to Ansible Galaxy, Red Hat Automation Hub, or a privately hosted Automation Hub instance. You can publish your collection in two ways:
- from the command line using the
ansible-galaxy collection publishcommand - from the website of the distribution server (Galaxy, Automation Hub) itself
Publishing a collection from the command line
To upload the collection tarball from the command line using ansible-galaxy:
ansible-galaxy collection publish path/to/my_namespace-my_collection-1.0.0.tar.gz
This ansible-galaxy command assumes you have retrieved and stored your API token in configuration. See galaxy_specify_token for details.
The ansible-galaxy collection publish command triggers an import process, just as if you uploaded the collection through the Galaxy website. The command waits until the import process completes before reporting the status back. If you want to continue without waiting for the import result, use the --no-wait argument and manually look at the import progress in your My Imports page.
Publishing a collection from the website
See the Galaxy documentation to learn how to publish your collection directly on the Galaxy website.
collections
Learn how to install and use collections.
collections_galaxy_meta
Table of fields used in the galaxy.yml file
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Documenting collections
Documenting modules and plugins
Documenting modules is thoroughly documented in module_documenting. Plugins can be documented the same way as modules, that is with DOCUMENTATION, EXAMPLES, and RETURN blocks.
Documenting roles
To document a role, you have to add a role argument spec by creating a file meta/argument_specs.yml in your role. See role_argument_spec for details. As an example, you can look at the argument specs file of the telekom_mms.icinga_director.ansible_icinga role <telekom_mms.icinga_director.ansible_icinga#role> on GitHub.
Verifying your collection documentation
You can use antsibull-docs to lint your collection documentation. See Linting collection documentation. for details.
Build a docsite with antsibull-docs
You can use antsibull-docs to build a Sphinx-based docsite for your collection:
- Create your collection and make sure you can use it with ansible-core by adding it to your
COLLECTIONS_PATHS. - Create a directory
destand runantsibull-docs sphinx-init --use-current --dest-dir dest namespace.name, wherenamespace.nameis the name of your collection. - Go into
destand runpip install -r requirements.txt. You might want to create a venv and activate it first to avoid installing this globally. - Then run
./build.sh. - Open
build/html/index.htmlin a browser of your choice.
See antsibull-docs documentation for complete details.
If you want to add additional documentation to your collection next to the plugin, module, and role documentation, see collections_doc_dir.
Migrating Ansible content to a different collection
You might decide to move content from one collection to another; for example, to extract a set of related modules out of community.general or community.network to create a more focused collection. When you migrate content between collections, you must take certain steps to ensure users can follow the transition.
Migrating content
If the collection from which you are going to migrate content is included in the Ansible community package, ensure the target collection satisfies the collections_requirements. After you satisfy the requirements, you can migrate the content as follows:
- Copy content from the source (old) collection to the target (new) collection.
- Change
M(), examples,seealso,extended_documentation_fragmentsto use actual FQCNs in moved content, old collection, and in other collections that have references to the content. - Move all related issues, pull requests, and wiki pages.
- Look through the
docs/docsitedirectory of the ansible-documentation GitHub repository (for example, using thegrepcommand-line utility) to check if there are examples using the moved modules and plugins so that you can update those FQCNs. - Deprecate the module/plugin with
removal_versionscheduled for the next major version inmeta/runtime.ymlof the old collection. The deprecation must be released after the copied content has been included in a release of the new collection. - When the next major release of the old collection is prepared:
- remove the module/plugin from the old collection
- remove related unit and integration tests
- remove specific module utils (if they are NOT used by other modules/plugins or
module_utils)- remove specific documentation fragments if there are any in the old collection
- add a changelog fragment containing entries for
removed_featuresandbreaking_changes; you can see an example of a changelog fragment in this pull request- change
meta/runtime.ymlin the old collection:
- add
redirectto the corresponding module/plugin's entry- in particular, add
redirectfor the removed module utils and documentation fragments if applicable- remove
removal_versionfrom there- remove related entries from
tests/sanity/ignore.txtfiles if exist- remove changelog fragments for removed content that are not yet part of the changelog (in other words, do not modify changelogs/changelog.yaml and do not delete files mentioned in it)
- remove requirements that are no longer required in
tests/unit/requirements.txt,tests/requirements.ymlandgalaxy.yml
To implement these changes, you need to create at least three PRs:
- Create a PR against the new collection to copy the content.
- Deprecate the module/plugin in the old collection.
- Later create a PR against the old collection to remove the content according to the schedule.
Adding the content to the new collection
Create a PR in the new collection to:
Removing the content from the old collection
Create a PR against the source collection repository to remove the modules, module_utils, plugins, and docs_fragments related to this migration:
- If you are removing an action plugin, remove the corresponding module that contains the documentation.
- If you are removing a module, remove any corresponding action plugin that should stay with it.
- Remove any entries about removed plugins from
meta/runtime.yml. Ensure they are added into the new repo. - Remove sanity ignore lines from
tests/sanity/ignore\*.txt - Remove associated integration tests from
tests/integrations/targets/and unit tests fromtests/unit/plugins/. - if you are removing from content from
community.generalorcommunity.network, remove entries from.github/BOTMETA.yml. - Carefully review
meta/runtime.ymlfor any entries you may need to remove or update, in particular deprecated entries. - Update
meta/runtime.ymlto contain redirects for EVERY PLUGIN, pointing to the new collection name.
Maintainers for the old collection have to make sure that the PR is merged in a way that it does not break user experience and semantic versioning:
- A new version containing the merged PR must not be released before the collection the content has been moved to has been released again, with that content contained in it. Otherwise the redirects cannot work and users relying on that content will experience breakage.
- Once 1.0.0 of the collection from which the content has been removed has been released, such PRs can only be merged for a new major version (in other words, 2.0.0, 3.0.0, and so on).
Updating BOTMETA.yml
The BOTMETA.yml, for example in community.general collection repository, is the source of truth for:
- ansibullbot
If the old and/or new collection has ansibullbot, its BOTMETA.yml must be updated correspondingly.
Ansibulbot will know how to redirect existing issues and PRs to the new repo. The build process for docs.ansible.com will know where to find the module docs.
$modules/monitoring/grafana/grafana_plugin.py:
migrated_to: community.grafana
$modules/monitoring/grafana/grafana_dashboard.py:
migrated_to: community.grafana
$modules/monitoring/grafana/grafana_datasource.py:
migrated_to: community.grafana
$plugins/callback/grafana_annotations.py:
maintainers: $team_grafana
labels: monitoring grafana
migrated_to: community.grafana
$plugins/doc_fragments/grafana.py:
maintainers: $team_grafana
labels: monitoring grafana
migrated_to: community.grafana
- The
migrated_to:key must be added explicitly for every file. You cannot addmigrated_toat the directory level. This is to allow module and plugin webdocs to be redirected to the new collection docs. migrated_to:MUST be added for every:- module
- plugin
- module_utils
- contrib/inventory script
- You do NOT need to add
migrated_tofor:- Unit tests
- Integration tests
- ReStructured Text docs (anything under
docs/docsite/rst/) - Files that never existed in
ansible/ansible:devel
collections
Learn how to install and use collections.
contributing_maintained_collections
Guidelines for contributing to selected collections
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Ansible collection creator path
If you are unfamiliar with Ansible collections, first take a look at the Using Ansible collections guide<collections>.
Ansible collections are a distribution format for Ansible content that can include playbooks, roles, modules, and plugins. A typical collection addresses a set of related use cases. For example, the community.dns collection includes modules and plugins to work with DNS.
You can install collections made by others or share yours with the community through a distribution server such as Ansible Galaxy. Certified collections can be published to the Red Hat Automation Hub, a part of the Red Hat Ansible Automation Platform.
Creating and sharing collections is a great way of contributing to the Ansible project.
The Ansible community package consists of ansible-core, which, among other core components, includes the ansible.builtin collection maintained by the Core team, and a set of collections maintained by the community.
The purpose of this guide is to give you as a (potential) content creator a consistent overview of the Ansible collection creator journey from an idea for the first module/role to having your collection included in the Ansible community package. The Collection development guidelines section<developing_collections> provides references to more detailed aspects of this journey. The overall journey consists of the following milestones:
Examine currently available solutions
If you have an idea for a new role or module/plugin, there is no need to reinvent the wheel if there is already a sufficient solution that solves your automation issue.
Therefore, first examine the currently available content including:
Ansible builtin modules and plugins <plugins_in_ansible.builtin>Ansible package collection index<all_modules_and_plugins>- Ansible Galaxy
- Ansible Automation Hub if you have the Ansible Automation Platform subscription
In case the solutions you found are not fully sufficient or have flaws, consider improving them rather than creating your own. Each collection includes information on where to create issues for that collection to propose your enhancement ideas.
If you already have your content written and used in your workflows, you can still consider integrating it to the existing solutions. However, if these options do not apply to your collection ideas, we encourage you to create and share your own.
Create your content
You tried <examine_existing_content> but have not found any sufficient solution for your automation issue.
Use one of the following guides:
Roles guide<playbooks_reuse_roles>: if you want to create a role.Developer guide<developer_guide>: if you want to create a new Ansible module or plugin for your personal use.
Put your content in a collection
You created <create_content> new content.
Now it is time to create a reusable and sharable collection. Use the Developing collections guide<developing_collections> to learn how.
We recommend you to use the collection_template repository as a basis for your collection.
Write good user collection documentation
Your collection README.md file should contain a quick-start installation and usage guides. You can use the community.general collection README file as an example.
If your collection contains modules or plugins, make sure their documentation is comprehensive. Use the Module format and documentation guide<developing_modules_documenting> and Ansible documentation style guide<style_guide> to learn more.
Publish your collection source code
Publish your collection on a platform for software development and version control such as GitHub.
It can be your personal repository or your organization's one. You can also request<request_coll_repo> a repository under the ansible-collections organization.
Make sure your collection contains exhaustive license information. Ansible is an open source project, so we encourage you to license it under one of open source licenses. If you plan to submit your collection for inclusion in the Ansible community package, your collection must satisfy the licensing requirements<coll_licensing_req>.
If you have used the collection_template repository we recommended earlier as a skeleton for your collection, it already contains the GNU GPL v3 license.
Follow a versioning convention
When releasing new versions of your collections, take the following recommended practices into consideration:
Understand and implement testing and CI
This section is applicable to collections containing modules and plugins.
For role testing, see the Ansible Molecule project.
Add tests
Testing your collection ensures that your code works well and integrates with other components such as ansible-core.
Take a look at the following documents:
Testing Ansible guide<developing_testing>: provides general information about testing.Testing collections guide<testing_collections>: contains collection-specific testing information.
Implement continuous integration
Now make sure when pull requests are created in your collection repository they are automatically tested using a CI tool such as GitHub Actions or Azure Pipelines.
The collection_template repository contains GitHub Actions templates you can adjust and use to enable the workflows in your repository.
Provide good contributor & maintainer documentation
See the collection_template/README.md as an example.
Publish your collection on distribution servers
To distribute your collection and allow others to conveniently use it, publish your collection on one or more distribution servers. See the Distributing collections guide<distributing_collections> to learn how.
Make your collection a part of Ansible community package
Make your collection satisfy the Ansible community package collections requirements<collections_requirements>. The Steering Committee <steering_responsibilities> selects collections for inclusion. See the collection inclusion criteria and process<steering_inclusion> for details.
Maintain
Maintain your collection. See the Ansible collection maintainer guidelines<maintainers> for details.
Communicate
Engage with the community. Take a look at the Ansible communication guide<communication> to see available communication options.
developing_collections
A set of guidelines about collection development aspects
Resource reporting
Ansible modules often interact with external systems such as clouds, hypervisors, and network controllers to manage resources that are not declared in your inventory. Resource reporting gives you structured visibility into what your automation actually touches, even for resources that never appear in the inventory.
You can add resource reporting to your Ansible collection by creating a lightweight YAML query file that maps module return values to a standardized resource taxonomy. Each entry uses a jq expression to extract resource metadata from module output.
How resource reporting works
Resource reporting uses a query file inside your collection to describe the resources that your modules manage. When a module runs, the query file tells any consuming tool how to extract three pieces of information from the module return values:
- A human-readable resource name.
- Canonical facts that uniquely identify the resource for deduplication.
- Metadata that categorizes the resource using a standardized taxonomy.
The result is machine-readable documentation about what your modules manage. For collection developers, resource reporting is like having excellent return value documentation that tools can consume automatically. For users, it creates a consistent, unified picture of automation activity across different vendors and platforms.
The query file format, resource taxonomy, and jq expression pattern are an open schema. Any automation platform, reporting tool, or custom script can consume the query file to extract structured resource data from module output.
The normalized resource taxonomy
The normalized resource taxonomy maps vendor-specific resource types to standard names. For example, a VMware VM and an AWS EC2 instance are both type virtual_machine. An Azure load balancer and an F5 BIG-IP VIP are both type load_balancer.
The taxonomy organizes resources into categories and device types. When you write a query, set the facts.device_type field to the snake_case value from the tables below.
Compute
| Resource | device_type value |
|---|---|
| Virtual machines | virtual_machine |
| Containers (managed) | container |
| Hypervisors | hypervisor |
| Bare metal | bare_metal |
| Serverless functions | serverless_function |
| Auto scaling groups | auto_scaling_group |
Networking
| Resource | device_type value |
|---|---|
| Switches | switch |
| Routers | router |
| Firewalls | firewall |
| Load balancers | load_balancer |
| Virtual private clouds | vpc |
| Subnets | subnet |
| VPNs | vpn |
| Gateways | gateway |
| DNS services | dns_service |
| Wireless access points | wireless_access_point |
| SD-WAN | sd_wan |
Storage
| Resource | device_type value |
|---|---|
| Object storage | object_storage |
| Block storage | block_storage |
| File storage | file_storage |
| Archive storage | archive_storage |
Database
| Resource | device_type value |
|---|---|
| Relational (SQL) | database_relational |
| NoSQL | database_nosql |
| Data warehouse | data_warehouse |
| In-memory or cache | database_cache |
DevOps and app integration
| Resource | device_type value |
|---|---|
| CI/CD platforms | ci_cd_platform |
| Container registries | container_registry |
| Message queues | message_queue |
| API endpoints | api_endpoint |
If your resource does not fit one of these standard types, open a topic on the Ansible forum to propose a new device type.
Adding resource reporting to a collection
To add resource reporting to your collection, create a query file and write jq expressions that extract resource metadata from your module return values.
Creating the query file
Create the file extensions/audit/event_query.yml in your collection root directory. This is the standard location for embedded query files that ship with your collection.
my_namespace/my_collection/
├── extensions/
│ └── audit/
│ └── event_query.yml
├── plugins/
├── meta/
└── galaxy.yml
Writing the query
The query file maps each module to a jq expression using the Fully Qualified Collection Name (FQCN) as the key. The jq expression runs against the module return values and must output a JSON object with the following fields:
| Field | Requirement | Description |
|---|---|---|
name |
Required | A human-readable display name for the resource, for example a VM name or switch hostname. |
canonical_facts |
Required | A dictionary of facts that uniquely identify the resource and deduplicate it across job runs. |
facts |
Optional | Metadata for categorization. Must include device_type when present. |
The following example shows a complete query file entry:
yaml/event_query_vmware.yaml
Choosing canonical facts
Select fields that are globally unique and stable for the life of the resource.
- Recommended: UUIDs, serial numbers, permanent MAC addresses.
- Conditional: Hostnames (if unique in the environment), IP addresses (static only).
- Avoid: Dynamic IPs, random session IDs.
json/canonical_facts.json
Setting the device type
Map your resource to a standard device_type value from the normalized resource taxonomy <resource_reporting_taxonomy>. You can optionally include a platform field to indicate the underlying platform, for example aws, vmware, or azure.
json/facts_device_type.json
Platform-specific examples
Different platforms require different querying strategies based on how their APIs return data.
VMware (flat structure)
VMware modules return a flat structure where the top-level key defines the node type. Map directly to the returned keys.
yaml/event_query_vmware.yaml
Azure (hierarchical structure)
Azure resources are hierarchical and use verbose resource IDs. Because the full Azure resource ID contains the resource type, you can use a jq regex to dynamically capture and categorize it.
yaml/event_query_azure.yaml
In this example, Microsoft.Compute in the resource ID is captured and lowercased to "compute".
AWS (implied types)
AWS info modules often return a list of resources, so your jq expression must iterate over them. The resource type is implied by the module you query rather than included in the resource ID.
yaml/event_query_aws.yaml
To manage the mapping from AWS module to resource type, define a mapping dictionary in your expression:
json/aws_type_mapping.json
Module matching rules
The query file uses module FQCNs as keys. You can target modules with exact matches or wildcard patterns.
Exact match
Target a specific module.
yaml/event_query_exact_match.yaml
Wildcard match
Target all modules in a collection. Use this carefully because different modules return different data structures.
yaml/event_query_wildcard_match.yaml
Testing your query
Before publishing your collection, verify your jq expression against real module output.
-
Run a playbook with your module and capture the JSON output. Register the return value or run the module with
-vvvto see the full output.yaml/test_playbook_resource_reporting.yaml
-
Save the module output to a file and test your
jqexpression from the command line.cat module_output.json | jq '{ name: .instance.hw_name, canonical_facts: { uuid: .instance.hw_uuid }, facts: { device_type: "virtual_machine" } }' -
Validate that the output JSON contains a valid
namestring and non-emptycanonical_facts.
developing_collections
Learn how to develop Ansible collections
collection_structure
Directories and files included in a collection
Ansible forum
Got questions? Need help? Want to share your ideas? Visit the Ansible forum
Using shared resources in collections
Although developing Ansible modules contained in collections is similar to developing standalone Ansible modules, you use shared resources like documentation fragments and module utilities differently in collections. You can use documentation fragments within and across collections. You can use optional module utilities to support multiple versions of ansible-core in your collection. Collections can also depend on other collections.
Using documentation fragments in collections
To include documentation fragments in your collection:
- Create the documentation fragment:
plugins/doc_fragments/fragment_name. - Refer to the documentation fragment with its FQCN.
extends_documentation_fragment:
- kubernetes.core.k8s_name_options
- kubernetes.core.k8s_auth_options
- kubernetes.core.k8s_resource_options
- kubernetes.core.k8s_scale_options
module_docs_fragments covers the basics for documentation fragments. The kubernetes.core collection includes a complete example.
If you use FQCN, you can use documentation fragments from one collection in another collection.
Leveraging optional module utilities in collections
Optional module utilities let you adopt the latest features from the most recent ansible-core release in your collection-based modules without breaking your collection on older Ansible versions. With optional module utilities, you can use the latest features when running against the latest versions, while still providing fallback behaviors when running against older versions.
This implementation, widely used in Python programming, wraps optional imports in conditionals or defensive try/except blocks, and implements fallback behaviors for missing imports. Ansible's module payload builder supports these patterns by treating any module_utils import nested in a block (e.g., if, try) as optional. If the requested import cannot be found during the payload build, it is simply omitted from the target payload and assumed that the importing code will handle its absence at runtime. Missing top-level imports of module_utils packages (imports that are not wrapped in a block statement of any kind) will fail the module payload build, and will not execute on the target.
For example, the ansible.module_utils.common.respawn package is only available in Ansible 2.11 and higher. The following module code would fail during the payload build on Ansible 2.10 or earlier (as the requested Python module does not exist, and is not wrapped in a block to signal to the payload builder that it can be omitted from the module payload):
from ansible.module_utils.common.respawn import respawn_module
By wrapping the import statement in a try block, the payload builder will omit the Python module if it cannot be located, and assume that the Ansible module will handle it at runtime:
try:
from ansible.module_utils.common.respawn import respawn_module
except ImportError:
respawn_module = None
...
if needs_respawn:
if respawn_module:
respawn_module(target)
else:
module.fail_json('respawn is not available in Ansible < 2.11, ensure that foopkg is installed')
The optional import behavior also applies to module_utils imported from collections.
Listing collection dependencies
We recommend that collections work as standalone, independent units, depending only on ansible-core. However, if your collection must depend on features and functionality from another collection, list the other collection or collections under dependencies in your collection's galaxy.yml file. Use the meta/runtime.yml file to set the ansible-core version that your collection depends on. For more information on the galaxy.yml file, see collections_galaxy_meta.
You can use Git repositories for collection dependencies during local development and testing. For example:
dependencies: {'git@github.com:organization/repo_name.git': 'devel'}
Do not use Git repositories as dependencies for published collections. Dependencies for published collections must be other published collections.
collections
Learn how to install and use collections.
contributing_maintained_collections
Guidelines for contributing to selected collections
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Collection structure
A collection is a simple data structure. None of the directories are required unless you have specific content that belongs in one of them. A collection does require a galaxy.yml file at the root level of the collection. This file contains all of the metadata that Galaxy and other tools need in order to package, build and publish the collection.
Collection directories and files
A collection can contain these directories and files:
collection/
├── docs/
├── extensions/
│ └── audit/
│ └── event_query.yml
├── galaxy.yml
├── meta/
│ └── runtime.yml
├── plugins/
│ ├── modules/
│ │ └── module1.py
│ ├── inventory/
│ └── .../
├── README.md
├── roles/
│ ├── role1/
│ ├── role2/
│ └── .../
├── playbooks/
│ ├── files/
│ ├── vars/
│ ├── templates/
│ └── tasks/
└── tests/
* Ansible only accepts .md extensions for the README file and any files in the /docs folder. * See the ansible-collections GitHub Org for examples of collection structure. * Not all directories are currently in use. Those are placeholders for future features.
galaxy.yml
A collection must have a galaxy.yml file that contains the necessary information to build a collection artifact. See collections_galaxy_meta for details.
extensions directory
Use the extensions directory to add content and metadata that extends the functionality of your collection beyond the standard plugins and roles. Each subdirectory groups related files for a specific capability. For example, the extensions/audit/event_query.yml file defines resource reporting queries that map module return values to a standardized resource taxonomy. See resource_reporting for details on adding resource reporting to your collection.
docs directory
Use the docs folder to describe how to use the roles and plugins the collection provides, role requirements, and so on.
For certified collections, Automation Hub displays documents written in markdown in the main docs directory with no subdirectories. This documentation is not published to docs.ansible.com.
For community collections included in the Ansible PyPI package, docs.ansible.com displays documents written in reStructuredText (.rst) in a docsite/rst/ subdirectory. Define the structure of your extra documentation in docs/docsite/extra-docs.yml:
---
sections:
- title: Scenario Guide
toctree:
- scenario_guide
The index page of the documentation for your collection displays the title you define in docs/docsite/extra-docs.yml with a link to your extra documentation. For an example, see the community.docker collection repo and the community.docker collection documentation.
You can add extra links to your collection index page and plugin pages with the docs/docsite/links.yml file. This populates the links under Description and Communications headings as well as links at the end of the individual plugin pages. See the collection_template links.yml file for a complete description of the structure and use of this file to create links.
Plugin and module documentation
Keep the specific documentation for plugins and modules embedded as Python docstrings. Use ansible-doc to view documentation for plugins inside a collection:
ansible-doc -t lookup my_namespace.my_collection.lookup1
The ansible-doc command requires the fully qualified collection name (FQCN) to display specific plugin documentation. In this example, my_namespace is the Galaxy namespace and my_collection is the collection name within that namespace.
The Galaxy namespace of an Ansible collection is defined in the galaxy.yml file. It can be different from the GitHub organization or repository name.
plugins directory
Add a 'per plugin type' specific subdirectory here, including module_utils which is usable not only by modules, but by most plugins by using their FQCN. This is a way to distribute modules, lookups, filters, and so on without having to import a role in every play.
Vars plugins in collections are not loaded automatically, and always require being explicitly enabled by using their fully qualified collection name. See enable_vars for details.
Cache plugins in collections may be used for fact caching, but are not supported for inventory plugins.
module_utils
When coding with module_utils in a collection, the Python import statement needs to take into account the FQCN along with the ansible_collections convention. The resulting Python import will look like from ansible_collections.{namespace}.{collection}.plugins.module_utils.{util} import {something}
The following example snippets show a Python and PowerShell module using both default Ansible module_utils and those provided by a collection. In this example the namespace is community, the collection is test_collection. In the Python example the module_util in question is called qradar such that the FQCN is community.test_collection.plugins.module_utils.qradar:
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.six.moves.urllib.parse import urlencode, quote_plus
from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible_collections.community.test_collection.plugins.module_utils.qradar import QRadarRequest
argspec = dict(
name=dict(required=True, type='str'),
state=dict(choices=['present', 'absent'], required=True),
)
module = AnsibleModule(
argument_spec=argspec,
supports_check_mode=True
)
qradar_request = QRadarRequest(
module,
headers={"Content-Type": "application/json"},
not_rest_data_keys=['state']
)
Note that importing something from an __init__.py file requires using the file name:
from ansible_collections.namespace.collection_name.plugins.callback.__init__ import CustomBaseClass
In the PowerShell example the module_util in question is called hyperv such that the FQCN is community.test_collection.plugins.module_utils.hyperv:
#!powershell
#AnsibleRequires -CSharpUtil Ansible.Basic
#AnsibleRequires -PowerShell ansible_collections.community.test_collection.plugins.module_utils.hyperv
$spec = @{
name = @{ required = $true; type = "str" }
state = @{ required = $true; choices = @("present", "absent") }
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
Invoke-HyperVFunction -Name $module.Params.name
$module.ExitJson()
roles directory
Collection roles are mostly the same as existing roles, but with a couple of limitations:
- Role names are now limited to contain only lowercase alphanumeric characters, plus
_and start with an alpha character.- Roles in a collection cannot contain plugins any more. Plugins must live in the collection
pluginsdirectory tree. Each plugin is accessible to all roles in the collection.
The directory name of the role is used as the role name. Therefore, the directory name must comply with the above role name rules. The collection import into Galaxy will fail if a role name does not comply with these rules.
You can migrate 'traditional roles' into a collection but they must follow the rules above. You may need to rename roles if they don't conform. You will have to move or link any role-based plugins to the collection specific directories.
For roles imported into Galaxy directly from a GitHub repository, setting the role_name value in the role's metadata overrides the role name used by Galaxy. For collections, that value is ignored. When importing a collection, Galaxy uses the role directory as the name of the role and ignores the role_name metadata value.
playbooks directory
In prior releases, you could reference playbooks in this directory using the full path to the playbook file from the command line. In ansible-core 2.11 and later, you can use the FQCN, namespace.collection.playbook (with or without extension), to reference the playbooks from the command line or from import_playbook. This will keep the playbook in 'collection context', as if you had added collections: [ namespace.collection ] to it.
You can have most of the subdirectories you would expect, such files/, vars/ or templates/ but not roles/ since those are handled already in the collection.
Also, playbooks within a collection follow the same guidelines as any playbooks except for these few adjustments:
- Directory: It must be in the
playbooks/directory.- Hosts: The host should be defined as a variable so the users of a playbook do not mistakenly run the plays against their entire inventory (if the host is set to all). For example -
hosts: '{{target|default("all")}}'.
To run the plays, users can now use such commands as ansible-playbook --e 'targets=webservers' or ansible-playbook --limit webservers. Either way, the collection owner should document their playbooks and how to use them in the docs/ folder or README file.
tests directory
Ansible Collections are tested much like Ansible itself, by using the ansible-test utility which is released as part of Ansible, version 2.9.0 and newer. Because Ansible Collections are tested using the same tooling as Ansible itself, by using the ansible-test, all Ansible developer documentation for testing is applicable for authoring Collections Tests with one key concept to keep in mind.
See testing_collections for specific information on how to test collections with ansible-test.
When reading the developing_testing documentation, there will be content that applies to running Ansible from source code through a Git clone, which is typical of an Ansible developer. However, it is not always typical for an Ansible Collection author to be running Ansible from source but instead from a stable release, and to create Collections it is not necessary to run Ansible from source. Therefore, when references of dealing with ansible-test binary paths, command completion, or environment variables are presented throughout the developing_testing documentation; keep in mind that it is not needed for Ansible Collection Testing because the act of installing the stable release of Ansible containing ansible-test is expected to setup those things for you.
meta directory
runtime.yml
A collection can store some additional metadata in a runtime.yml file in the collection's meta directory. The runtime.yml file supports the top level keys:
-
requires_ansible:
The version of Ansible Core (ansible-core) required to use the collection. Multiple versions can be separated with a comma.
requires_ansible: ">=2.10,<2.11"although the version is a PEP440 Version Specifier under the hood, Ansible deviates from PEP440 behavior by truncating prerelease segments from the Ansible version. This means that Ansible 2.11.0b1 is compatible with something that
requires_ansible: ">=2.11". -
plugin_routing:
Content in a collection that Ansible needs to load from another location or that has been deprecated/removed. The top level keys of
plugin_routingare types of plugins, with individual plugin names as subkeys. To define a new location for a plugin, set theredirectfield to another name. To deprecate a plugin, use thedeprecationfield to provide a custom warning message and the removal version or date. If the plugin has been renamed or moved to a new location, theredirectfield should also be provided. If a plugin is being removed entirely,tombstonecan be used for the fatal error message and removal version or date.plugin_routing: inventory: kubevirt: redirect: community.general.kubevirt my_inventory: tombstone: removal_version: "2.0.0" warning_text: my_inventory has been removed. Please use other_inventory instead. modules: my_module: deprecation: removal_date: "2021-11-30" warning_text: my_module will be removed in a future release of this collection. Use another.collection.new_module instead. redirect: another.collection.new_module podman_image: redirect: containers.podman.podman_image module_utils: ec2: redirect: amazon.aws.ec2 util_dir.subdir.my_util: redirect: namespace.name.my_util -
import_redirection
A mapping of names for Python import statements and their redirected locations.
import_redirection: ansible.module_utils.old_utility: redirect: ansible_collections.namespace_name.collection_name.plugins.module_utils.new_location -
action_groups
A mapping of groups and the list of action plugin and module names they contain. They may also have a special 'metadata' dictionary in the list, which can be used to include actions from other groups.
action_groups: groupname: # The special metadata dictionary. All action/module names should be strings. - metadata: extend_group: - another.collection.groupname - another_group - my_action another_group: - my_module - another.collection.another_module
execution-environment.yml
If your collection has requirements, you can specify them in the execution-environment.yml file in the meta directory. This ensures users do not need to add these requirements manually when building Execution Environments<getting_started_ee_index> containing your collection. See the collection-level metadata guide for details.
distributing_collections
Learn how to package and publish your collection
contributing_maintained_collections
Guidelines for contributing to selected collections
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Testing collections
Testing your collection ensures that your code works well and integrates well with the rest of the Ansible ecosystem. Your collection should pass the sanity tests for Ansible code. You should also add unit tests to cover the code in your collection and integration tests to cover the interactions between your collection and ansible-core.
Testing tools
The main tool for testing collections is ansible-test, Ansible's testing tool described in developing_testing and provided by both the ansible and ansible-core packages.
Use ansible-test tool from your collection directory, which must include ansible_collections in the path, for example collections/ansible_collections/community/general for the community.general collection. See testing_collections_guide and testing_running_locally for testing guidelines.
You can run several sanity tests, as well as run unit and integration tests for plugins using ansible-test. When you test collections, test against the ansible-core version(s) you are targeting.
You must always execute ansible-test from the root directory of a collection. You can run ansible-test in Docker containers without installing any special requirements. The Ansible team uses this approach in Azure Pipelines both in the ansible/ansible GitHub repository and in the large community collections such as community.general and community.network to automatically run the tests when pull requests are submitted.
Many collections which do not require running tests on different OS distributions use GitHub Actions as their continuous integration (CI) platform. The collection_template repository contains GitHub Actions workflow templates that collection developers are free to use to easily set up CI in their collection repositories.
The examples below demonstrate running tests in Docker containers.
Sanity tests
To run all sanity tests:
ansible-test sanity --docker default -v
See testing_sanity for more information. See the full list of sanity tests <all_sanity_tests> for details on the sanity tests and how to fix identified issues.
Adding unit tests
You must place unit tests in the appropriate tests/unit/plugins/ directory. For example, you would place tests for plugins/module_utils/foo/bar.py in tests/unit/plugins/module_utils/foo/test_bar.py or tests/unit/plugins/module_utils/foo/bar/test_bar.py. For examples, see the unit tests in community.general.
To run all unit tests for all supported Python versions:
ansible-test units --docker default -v
To run all unit tests only for a specific Python version:
ansible-test units --docker default -v --python 3.13
To run only a specific unit test:
ansible-test units --docker default -v --python 3.13 tests/unit/plugins/module_utils/foo/test_bar.py
You can specify Python requirements in the tests/unit/requirements.txt file. See testing_units for more information, especially on fixture files.
Adding integration tests
You must place integration tests in the appropriate tests/integration/targets/ directory. For module integration tests, you can use the module name alone. For example, you would place integration tests for plugins/modules/foo.py in a directory called tests/integration/targets/foo/. For non-module plugin integration tests, you must add the plugin type to the directory name. For example, you would place integration tests for plugins/connections/bar.py in a directory called tests/integration/targets/connection_bar/. For lookup plugins, the directory must be called lookup_foo, for inventory plugins, inventory_foo, and so on.
You can write two different kinds of integration tests:
- Ansible role tests run with
ansible-playbookand validate various aspects of the module. They can depend on other integration tests (usually namedprepare_barorsetup_bar, which prepare a service or install a requirement namedbarin order to test modulefoo) to set-up required resources, such as installing required libraries or setting up server services. runme.shtests run directly as scripts. They can set up inventory files, and executeansible-playbookoransible-inventorywith various settings.
For examples, see the integration tests in community.general. See also testing_integration for more details.
Since integration tests can install requirements, and set-up, start and stop services, we recommended running them in docker containers or otherwise restricted environments whenever possible. By default, ansible-test supports Docker images for several operating systems. See the list of supported docker images for all options. Use the default image mainly for platform-independent integration tests, such as those for cloud modules. The following examples use the fedora35 image.
To execute all integration tests for a collection:
ansible-test integration --docker fedora -v
If you want more detailed output, run the command with -vvv instead of -v. Alternatively, specify --retry-on-error to automatically re-run failed tests with higher verbosity levels.
To execute only the integration tests in a specific directory:
ansible-test integration --docker fedora -v connection_bar
You can specify multiple target names. Each target name is the name of a directory in tests/integration/targets/.
Collection testing with ansible-test
ansible-test has been the de facto collection testing tool since ansible 2.9. However, ansible-test is no longer receiving feature enhancements for new collection testing requirements. We ask collection maintainers and community members to join the discussion to improve collection testing tools on the Ansible Forum. Find related discussion topics via the collection-test-strat tag.
developing_testing
More resources on testing Ansible
contributing_maintained_collections
Guidelines for contributing to selected collections
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Developing `ansible-core`
Although ansible-core (the code hosted in the ansible/ansible repository on GitHub) includes a few plugins that can be swapped out by the playbook directives or configuration, much of the code there is not modular. The documents here give insight into how the parts of ansible-core work together.
core_branches_and_tags developing_program_flow_modules
developing_api
Learn about the Python API for task execution
developing_plugins
Learn about developing plugins
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Developing dynamic inventory
Ansible can pull inventory information from dynamic sources, including cloud sources, by using the supplied inventory plugins <inventory_plugins>. For details about how to pull inventory information, see dynamic_inventory. If the source you want is not currently covered by existing plugins, you can create your own inventory plugin as with any other plugin type.
In previous versions, you had to create a script or program that could output JSON in the correct format when invoked with the proper arguments. You can still use and write inventory scripts, as we ensured backwards compatibility through the script inventory plugin <script_inventory> and there is no restriction on the programming language used. If you choose to write a script, however, you will need to implement some features yourself such as caching, configuration management, dynamic variable and group composition, and so on. If you use inventory plugins <inventory_plugins> instead, you can use the Ansible codebase and add these common features automatically.
Topics
Inventory sources
Inventory sources are the input strings that inventory plugins work with. An inventory source can be a path to a file or to a script, or it can be raw data that the plugin can interpret.
The table below shows some examples of inventory plugins and the source types that you can pass to them with -i on the command line.
|
Source |
|
A comma-separated list of hosts |
|
Path to a YAML format data file |
|
Path to a YAML configuration file |
|
Path to an INI formatted data file |
|
Path to a YAML configuration file |
|
Path to an executable that outputs JSON |
Inventory plugins
Like most plugin types (except modules), inventory plugins must be developed in Python. They execute on the control node and should therefore adhere to the control_node_requirements.
Most of the documentation in developing_plugins also applies here. You should read that document first for a general understanding and then come back to this document for specifics on inventory plugins.
Normally, inventory plugins are executed at the start of a run, and before the playbooks, plays, or roles are loaded. However, you can use the meta: refresh_inventory task to clear the current inventory and execute the inventory plugins again, and this task will generate a new inventory.
If you use the persistent cache, inventory plugins can also use the configured cache plugin to store and retrieve data. Caching inventory avoids making repeated and costly external calls.
Developing an inventory plugin
The first thing you want to do is use the base class:
from ansible.plugins.inventory import BaseInventoryPlugin
class InventoryModule(BaseInventoryPlugin):
NAME = 'myplugin' # used internally by Ansible, it should match the file name but not required
If the inventory plugin is in a collection, the NAME should be in the 'namespace.collection_name.myplugin' format. The base class has a couple of methods that each plugin should implement and a few helpers for parsing the inventory source and updating the inventory.
After you have the basic plugin working, you can incorporate other features by adding more base classes:
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
NAME = 'myplugin'
For the bulk of the work in a plugin, we mostly want to deal with 2 methods verify_file and parse.
verify_file method
Ansible uses this method to quickly determine if the inventory source is usable by the plugin. The determination does not need to be 100% accurate, as there might be an overlap in what plugins can handle and by default Ansible will try the enabled plugins as per their sequence.
def verify_file(self, path):
''' return true/false if this is possibly a valid file for this plugin to consume '''
valid = False
if super(InventoryModule, self).verify_file(path):
# base class verifies that file exists and is readable by current user
if path.endswith(('virtualbox.yaml', 'virtualbox.yml', 'vbox.yaml', 'vbox.yml')):
valid = True
return valid
In the above example, from the virtualbox inventory plugin <virtualbox_inventory>, we screen for specific file name patterns to avoid attempting to consume any valid YAML file. You can add any type of condition here, but the most common one is 'extension matching'. If you implement extension matching for YAML configuration files, the path suffix <plugin_name>.<yml|yaml> should be accepted. All valid extensions should be documented in the plugin description.
The following is another example that does not use a 'file' but the inventory source string itself, from the host list <host_list_inventory> plugin:
def verify_file(self, path):
''' don't call base class as we don't expect a path, but a host list '''
host_list = path
valid = False
b_path = to_bytes(host_list, errors='surrogate_or_strict')
if not os.path.exists(b_path) and ',' in host_list:
# the path does NOT exist and there is a comma to indicate this is a 'host list'
valid = True
return valid
This method is just to expedite the inventory process and avoid unnecessary parsing of sources that are easy to filter out before causing a parse error.
parse method
This method does the bulk of the work in the plugin. It takes the following parameters:
- inventory: inventory object with existing data and the methods to add hosts/groups/variables to inventory
- loader: Ansible's DataLoader. The DataLoader can read files, auto load JSON/YAML and decrypt vaulted data, and cache read files.
- path: string with inventory source (this is usually a path, but is not required)
- cache: indicates whether the plugin should use or avoid caches (cache plugin and/or loader)
The base class does some minimal assignment for reuse in other methods.
def parse(self, inventory, loader, path, cache=True):
self.loader = loader
self.inventory = inventory
self.templar = Templar(loader=loader)
It is up to the plugin now to parse the provided inventory source and translate it into Ansible inventory. To facilitate this, the example below uses a few helper functions:
NAME = 'myplugin'
def parse(self, inventory, loader, path, cache=True):
# call base method to ensure properties are available for use with other helper methods
super(InventoryModule, self).parse(inventory, loader, path, cache)
# this method will parse 'common format' inventory sources and
# update any options declared in DOCUMENTATION as needed
config = self._read_config_data(path)
# if NOT using _read_config_data you should call set_options directly,
# to process any defined configuration for this plugin,
# if you don't define any options you can skip
#self.set_options()
# example consuming options from inventory source
mysession = apilib.session(user=self.get_option('api_user'),
password=self.get_option('api_pass'),
server=self.get_option('api_server')
)
# make requests to get data to feed into inventory
mydata = mysession.getitall()
#parse data and create inventory objects:
for colo in mydata:
for server in mydata[colo]['servers']:
self.inventory.add_host(server['name'])
self.inventory.set_variable(server['name'], 'ansible_host', server['external_ip'])
The specifics will vary depending on API and structure returned. Remember that if you get an inventory source error or any other issue, you should raise AnsibleParserError to let Ansible know that the source was invalid or the process failed.
For examples on how to implement an inventory plugin, see the source code here: lib/ansible/plugins/inventory.
inventory object
The inventory object passed to parse has helpful methods for populating inventory.
add_group adds a group to inventory if it doesn't already exist. It takes the group name as the only positional argument.
add_child adds a group or host that exists in inventory to a parent group in inventory. It takes two positional arguments, the name of the parent group and the name of the child group or host.
add_host adds a host to inventory if it doesn't already exist, optionally to a specific group. It takes the host name as the first argument and accepts two optional keyword arguments, group and port. group is the name of a group in inventory, and port is an integer.
set_variable adds a variable to a group or host in inventory. It takes three positional arguments: the name of the group or host, the name of the variable, and the value of the variable.
To create groups and variables using Jinja2 expressions, see the section on implementing constructed features below.
To see other inventory object methods, see the source code here: lib/ansible/inventory/data.py.
inventory cache
To cache the inventory, extend the inventory plugin documentation with the inventory_cache documentation fragment and use the Cacheable base class.
extends_documentation_fragment:
- inventory_cache
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
NAME = 'myplugin'
Next, load the cache plugin specified by the user to read from and update the cache. If your inventory plugin uses YAML-based configuration files and the _read_config_data method, the cache plugin is loaded within that method. If your inventory plugin does not use _read_config_data, you must load the cache explicitly with load_cache_plugin.
NAME = 'myplugin'
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
self.load_cache_plugin()
Before using the cache plugin, you must retrieve a unique cache key by using the get_cache_key method. This task needs to be done by all inventory modules using the cache, so that you don't use/overwrite other parts of the cache.
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
self.load_cache_plugin()
cache_key = self.get_cache_key(path)
The get_cache_key is a helper method that retrieves a unique key for the plugin name and the path parameter. Inventory plugins that generate multiple unique cache entries should not use this helper method because it is designed to get one key per inventory source. In general we recommend that plugin developers refactor to avoid multiple cache entries for inventory sources.
Now that you've enabled caching, loaded the correct plugin, and retrieved a unique cache key, you can set up the flow of data between the cache and your inventory using the cache parameter of the parse method. This value comes from the inventory manager and indicates whether the inventory is being refreshed (such as by the --flush-cache or the meta task refresh_inventory). Although the cache shouldn't be used to populate the inventory when being refreshed, the cache should be updated with the new inventory if the user has enabled caching. You can use self._cache like a dictionary. The following pattern allows refreshing the inventory to work in conjunction with caching.
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
self.load_cache_plugin()
cache_key = self.get_cache_key(path)
# cache may be True or False at this point to indicate if the inventory is being refreshed
# get the user's cache option too to see if we should save the cache if it is changing
user_cache_setting = self.get_option('cache')
# read if the user has caching enabled and the cache isn't being refreshed
attempt_to_read_cache = user_cache_setting and cache
# update if the user has caching enabled and the cache is being refreshed; update this value to True if the cache has expired below
cache_needs_update = user_cache_setting and not cache
# attempt to read the cache if inventory isn't being refreshed and the user has caching enabled
if attempt_to_read_cache:
try:
results = self._cache[cache_key]
except KeyError:
# This occurs if the cache_key is not in the cache or if the cache_key expired, so the cache needs to be updated
cache_needs_update = True
if not attempt_to_read_cache or cache_needs_update:
# parse the provided inventory source
results = self.get_inventory()
if cache_needs_update:
self._cache[cache_key] = results
# submit the parsed data to the inventory object (add_host, set_variable, etc)
self.populate(results)
After the parse method is complete, the contents of self._cache is used to set the cache plugin if the contents of the cache have changed.
You have three other cache methods available:
set_cache_pluginforces the cache plugin to be set with the contents ofself._cache, before theparsemethod completesupdate_cache_if_changedsets the cache plugin only ifself._cachehas been modified, before theparsemethod completesclear_cacheflushes the cache, ultimately by calling the cache plugin'sflush()method, whose implementation is dependent upon the particular cache plugin in use. Note that if the user is using the same cache backend for facts and inventory, both will get flushed. To avoid this, the user can specify a distinct cache backend in their inventory plugin configuration.
constructed features
Inventory plugins can create host variables and groups from Jinja2 expressions and variables by using features from the constructed inventory plugin. To do this, use the Constructable base class and extend the inventory plugin's documentation with the constructed documentation fragment.
extends_documentation_fragment:
- constructed
class InventoryModule(BaseInventoryPlugin, Constructable):
NAME = 'ns.coll.myplugin'
There are three main options in the constructed documentation fragment:
compose creates variables using Jinja2 expressions. This is implemented by calling the _set_composite_vars method. keyed_groups creates groups of hosts based on variable values. This is implemented by calling the _add_host_to_keyed_groups method. groups creates groups based on Jinja2 conditionals. This is implemented by calling the _add_host_to_composed_groups method.
Each method should be called for every host added to inventory. Three positional arguments are required: the constructed option, a dictionary of variables, and a host name. Calling the method _set_composite_vars first will allow keyed_groups and groups to use the composed variables.
By default, undefined variables are ignored. This is permitted by default for compose so you can make the variable definitions depend on variables that will be populated later in a play from other sources. For groups, it allows using variables that are not always present without having to use the default filter. To support configuring undefined variables to be an error, pass the constructed option strict to each of the methods as a keyword argument.
keyed_groups and groups use any variables already associated with the host (for example, from an earlier inventory source). _add_host_to_keyed_groups and add_host_to_composed_groups can turn this off by passing the keyword argument fetch_hostvars.
Here is an example using all three methods:
def add_host(self, hostname, host_vars):
self.inventory.add_host(hostname, group='all')
for var_name, var_value in host_vars.items():
self.inventory.set_variable(hostname, var_name, var_value)
strict = self.get_option('strict')
# Add variables created by the user's Jinja2 expressions to the host
self._set_composite_vars(self.get_option('compose'), host_vars, hostname, strict=True)
# Create user-defined groups using variables and Jinja2 conditionals
self._add_host_to_composed_groups(self.get_option('groups'), host_vars, hostname, strict=strict)
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host_vars, hostname, strict=strict)
By default, group names created with _add_host_to_composed_groups() and _add_host_to_keyed_groups() are valid Python identifiers. Invalid characters are replaced with an underscore _. A plugin can change the sanitization used for the constructed features by setting self._sanitize_group_name to a new function. The core engine also does sanitization, so if the custom function is less strict it should be used in conjunction with the configuration setting TRANSFORM_INVALID_GROUP_CHARS.
from ansible.inventory.group import to_safe_group_name
class InventoryModule(BaseInventoryPlugin, Constructable):
NAME = 'ns.coll.myplugin'
@staticmethod
def custom_sanitizer(name):
return to_safe_group_name(name, replacer='')
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
self._sanitize_group_name = custom_sanitizer
Common format for inventory sources
To simplify development, most plugins use a standard YAML-based configuration file as the inventory source. The file has only one required field plugin, which should contain the name of the plugin that is expected to consume the file. Depending on other common features used, you might need other fields, and you can add custom options in each plugin as required. For example, if you use the integrated caching, cache_plugin, cache_timeout and other cache-related fields could be present.
The 'auto' plugin
From Ansible 2.5 onwards, we include the auto inventory plugin <auto_inventory> and enable it by default. If the plugin field in your standard configuration file matches the name of your inventory plugin, the auto inventory plugin will load your plugin. The 'auto' plugin makes it easier to use your plugin without having to update configurations.
Inventory scripts
Even though we now have inventory plugins, we still support inventory scripts, not only for backwards compatibility but also to allow users to use other programming languages.
Inventory script conventions
Inventory scripts must accept the --list and --host <hostname> arguments. Although other arguments are allowed, Ansible will not use them. Such arguments might still be useful for executing the scripts directly.
When the script is called with the single argument --list, the script must output to stdout a JSON object that contains all the groups to be managed. Each group's value should be either an object containing a list of each host, any child groups, and potential group variables, or simply a list of hosts:
{
"group001": {
"hosts": ["host001", "host002"],
"vars": {
"var1": true
},
"children": ["group002"]
},
"group002": {
"hosts": ["host003","host004"],
"vars": {
"var2": 500
},
"children":[]
}
}
If any of the elements of a group are empty, they may be omitted from the output.
When called with the argument --host <hostname> (where <hostname> is a host from above), the script must print a JSON object, either empty or containing variables to make them available to templates and playbooks. For example:
{
"VAR001": "VALUE",
"VAR002": "VALUE"
}
Printing variables is optional. If the script does not print variables, it should print an empty JSON object.
Tuning the external inventory script
1.3
The stock inventory script system mentioned above works for all versions of Ansible, but calling --host for every host can be rather inefficient, especially if it involves API calls to a remote subsystem.
To avoid this inefficiency, if the inventory script returns a top-level element called "_meta", it is possible to return all the host variables in a single script execution. When this meta element contains a value for "hostvars", the inventory script will not be invoked with --host for each host. This behavior results in a significant performance increase for large numbers of hosts.
The data to be added to the top-level JSON object looks like this:
{
# results of inventory script as above go here
# ...
"_meta": {
"hostvars": {
"host001": {
"var001" : "value"
},
"host002": {
"var002": "value"
}
}
}
}
To satisfy the requirements of using _meta, to prevent ansible from calling your inventory with --host you must at least populate _meta with an empty hostvars object. For example:
{
# results of inventory script as above go here
# ...
"_meta": {
"hostvars": {}
}
}
If you intend to replace an existing static inventory file with an inventory script, it must return a JSON object which contains an 'all' group that includes every host in the inventory as a member and every group in the inventory as a child. It should also include an 'ungrouped' group which contains all hosts which are not members of any other group. A skeleton example of this JSON object is:
{
"_meta": {
"hostvars": {}
},
"all": {
"children": [
"ungrouped"
]
},
"ungrouped": {
"children": [
]
}
}
An easy way to see how this should look is using ansible-inventory, which also supports --list and --host parameters like an inventory script would.
developing_api
Python API to Playbooks and Ad Hoc Task Execution
developing_modules_general
Get started with developing a module
developing_plugins
How to develop plugins
AWX
REST API endpoint and GUI for Ansible, syncs with dynamic inventory
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Adding modules and plugins locally<span id="using_local_modules_and_plugins"></span>
You can extend Ansible by adding custom modules or plugins. You can create them from scratch or copy existing ones for local use. You can store a local module or plugin on your Ansible control node and share it with your team or organization. You can also share plugins and modules by including them in a collection, then publishing the collection on Ansible Galaxy.
If you are using a local module or plugin but Ansible cannot find it, this page is all you need.
If you want to create a plugin or a module, see developing_plugins, developing_modules_general and developing_collections.
Extending Ansible with local modules and plugins offers shortcuts such as:
- You can copy other people's modules and plugins.
- When writing a new module, you can choose any programming language you like.
- You do not have to clone any repositories.
- You do not have to open a pull request.
- You do not have to add tests (though we recommend that you do!).
Modules and plugins: what is the difference?
If you are looking to add functionality to Ansible, you might wonder whether you need a module or a plugin. Here is a quick overview to help you understand what you need:
Plugins <working_with_plugins>extend Ansible's core functionality. Most plugin types execute on the control node within the/usr/bin/ansibleprocess. Plugins offer options and extensions for the core features of Ansible: transforming data, logging output, connecting to inventory, and more.- Modules are a type of plugin that execute automation tasks on a 'target' (usually a remote system). Modules work as standalone scripts that Ansible executes in their own process outside of the control node. Modules interface with Ansible mostly with JSON, accepting arguments and returning information by printing a JSON string to stdout before exiting. Unlike the other plugins (which must be written in Python), modules can be written in any language; although Ansible provides modules in Python and Powershell only.
Adding modules and plugins in collections
You can add modules and plugins by creating a collection <developing_collections>. With a collection, you can use custom modules and plugins in any playbook or role. You can share your collection easily at any time through Ansible Galaxy.
The rest of this page describes other methods of using local, standalone modules or plugins.
Adding a module or plugin outside of a collection
You can configure Ansible to load standalone local modules or plugins in specific locations and make them available to all playbooks and roles (using configured paths). Alternatively, you can make a non-collection local module or plugin available only to certain playbooks or roles (with adjacent paths).
Adding standalone local modules for all playbooks and roles
To load standalone local modules automatically and make them available to all playbooks and roles, use the DEFAULT_MODULE_PATH configuration setting or the ANSIBLE_LIBRARY environment variable. The configuration setting and environment variable take a colon-separated list, similar to $PATH. You have two options:
-
Add your standalone local module to one of the default configured locations. See the
DEFAULT_MODULE_PATHconfiguration setting for details. Default locations may change without notice. -
Add the location of your standalone local module to an environment variable or configuration:
- the
ANSIBLE_LIBRARYenvironment variable - the
DEFAULT_MODULE_PATHconfiguration setting
- the
To view your current configuration settings for modules:
ansible-config dump |grep DEFAULT_MODULE_PATH
After you save your module file in one of these locations, Ansible loads it and you can use it in any local task, playbook, or role.
To confirm that my_local_module is available:
- type
ansible localhost -m my_local_moduleto see the output for that module, or - type
ansible-doc -t module my_local_moduleto see the documentation for that module
This applies to all plugin types but requires specific configuration and/or adjacent directories for each plugin type, see below.
The ansible-doc command can parse module documentation from modules written in Python or an adjacent YAML file. If you have a module written in a programming language other than Python, you should write the documentation in a Python or YAML file adjacent to the module file. adjacent_yaml_doc
Adding standalone local modules for selected playbooks or a single role
Ansible automatically loads all executable files from certain directories adjacent to your playbook or role as modules. Standalone modules in these locations are available only to the specific playbook, playbooks, or role in the parent directory.
- To use a standalone module only in a selected playbook or playbooks, store the module in a subdirectory called
libraryin the directory that contains the playbook or playbooks. - To use a standalone module only in a single role, store the module in a subdirectory called
librarywithin that role.
This applies to all plugin types but requires specific configuration and/or adjacent directories for each plugin type, see below.
Roles contained in collections cannot contain any modules or other plugins. All plugins in a collection must live in the collection plugins directory tree. All plugins in that tree are accessible to all roles in the collection. If you are developing new modules, we recommend distributing them in collections <developing_collections>, not in roles.
Adding a non-module plugin locally outside of a collection
You can configure Ansible to load standalone local plugins in a specified location or locations and make them available to all playbooks and roles. Alternatively, you can make a standalone local plugin available only to specific playbooks or roles.
Although modules are plugins, the naming patterns for directory names and environment variables that apply to other plugin types do not apply to modules. See local_modules.
Adding local non-module plugins for all playbooks and roles
To load standalone local plugins automatically and make them available to all playbooks and roles, use the configuration setting or environment variable for the type of plugin you are adding. These configuration settings and environment variables take a colon-separated list, similar to $PATH. You have two options:
-
Add your local plugin to one of the default configured locations. See
configuration settings <ansible_configuration_settings>for details on the correct configuration setting for the plugin type. Default locations may change without notice. -
Add the location of your local plugin to an environment variable or configuration:
- the relevant
ANSIBLE_plugin_type_PLUGINSenvironment variable - for example,$ANSIBLE_INVENTORY_PLUGINSor$ANSIBLE_VARS_PLUGINS - the relevant
plugin_type_PATHconfiguration setting, most of which begin withDEFAULT_- for example,DEFAULT_CALLBACK_PLUGIN_PATHorDEFAULT_FILTER_PLUGIN_PATHorBECOME_PLUGIN_PATH
- the relevant
To view your current configuration settings for non-module plugins:
ansible-config dump |grep plugin_type_PATH
After your plugin file is added to one of these locations, Ansible loads it and you can use it in any local module, task, playbook, or role. For more information on environment variables and configuration settings, see ansible_configuration_settings.
To confirm that plugins/plugin_type/my_local_plugin is available:
- type
ansible-doc -t <plugin_type> my_local_lookup_pluginto see the documentation for that plugin - for example,ansible-doc -t lookup my_local_lookup_plugin
The ansible-doc command works for most plugin types, but not for action, filter, or test plugins. See ansible-doc for more details.
Adding standalone local plugins for selected playbooks or a single role
Ansible automatically loads all plugins from certain directories adjacent to your playbook or role, loading each type of plugin separately from a directory named for the type of plugin. Standalone plugins in these locations are available only to the specific playbook, playbooks, or role in the parent directory.
- To use a standalone plugin only in a selected playbook or playbooks, store the plugin in a subdirectory for the correct
plugin_type(for example,callback_pluginsorinventory_plugins) in the directory that contains the playbooks. These directories must use the_pluginssuffix. For a full list of plugin types, seeworking_with_plugins. - To use a standalone plugin only in a single role, store the plugin in a subdirectory for the correct
plugin_type(for example,cache_pluginsorstrategy_plugins) within that role. When shipped as part of a role, the plugin is available as soon as the role is executed. These directories must use the_pluginssuffix. For a full list of plugin types, seeworking_with_plugins.
Roles contained in collections cannot contain any plugins. All plugins in a collection must live in the collection plugins directory tree. All plugins in that tree are accessible to all roles in the collection. If you are developing new plugins, we recommend distributing them in collections <developing_collections>, not in roles.
Some plugin types are needed early during Ansible execution, such as callbacks, inventory, and cache. These plugin types cannot be loaded dynamically and must exist in configured paths or be referenced by FQCN in configuration.
Using ansible.legacy to access custom versions of an ansible.builtin module
If you need to override one of the ansible.builtin modules and are using FQCN, you need to use ansible.legacy as part of the fully-qualified collection name (FQCN). For example, if you had your own copy module, you would access it as ansible.legacy.copy. See using_ansible_legacy for details on how to use custom modules with roles within a collection.
Using and developing module utilities
Ansible provides a number of module utilities, or snippets of shared code, that provide helper functions you can use when developing your own modules. The basic.py module utility provides the main entry point for accessing the Ansible library, and all Python Ansible modules must import something from ansible.module_utils. A common option is to import AnsibleModule:
from ansible.module_utils.basic import AnsibleModule
The ansible.module_utils namespace is not a plain Python package: it is constructed dynamically for each task invocation, by extracting imports and resolving those matching the namespace against a search path <ansible_search_path> derived from the active configuration.
Modules may only import from ansible.module_utils.*. Importing from other parts of the ansible namespace (for example, ansible.errors or ansible.parsing.dataloader) is not supported. The ansible package is not guaranteed to be present on the remote target where modules execute, so such imports may fail silently or unpredictably.
To reduce the maintenance burden in a collection or in local modules, you can extract duplicated code into one or more module utilities and import them into your modules. For example, if you have your own custom modules that import a my_shared_code library, you can place that into a ./module_utils/my_shared_code.py file like this:
from ansible.module_utils.my_shared_code import MySharedCodeClient
When you run ansible-playbook, Ansible will merge any files in your local module_utils directories into the ansible.module_utils namespace in the order defined by the Ansible search path <ansible_search_path>.
Naming and finding module utilities
You can generally tell what a module utility does from its name and/or its location. Generic utilities (shared code used by many different kinds of modules) live in the main ansible/ansible codebase, in the common subdirectory or in the root directory of lib/ansible/module_utils. Utilities used by a particular set of modules generally live in the same collection as those modules. For example:
lib/ansible/module_utils/urls.pycontains shared code for parsing URLsopenstack.cloud.plugins.module_utils.openstack.pycontains utilities for modules that work with OpenStack instancesansible.netcommon.plugins.module_utils.network.common.config.pycontains utility functions for use by networking modules
Following this pattern with your own module utilities makes everything easy to find and use.
Standard module utilities
Ansible ships with an extensive library of module_utils files. You can find the module utility source code in the lib/ansible/module_utils directory under your main Ansible path. We describe the most widely used utilities below. For more details on any specific module utility, please see the source code for module_utils.
LICENSING REQUIREMENTS Ansible enforces the following licensing requirements:
-
Utilities (files in
lib/ansible/module_utils/) may have one of two licenses:- A file in
module_utilsused only for a specific vendor's hardware, provider, or service may be licensed under GPLv3+. Adding a new file undermodule_utilswith GPLv3+ needs to be approved by the core team. - All other
module_utilsmust be licensed under BSD, so GPL-licensed third-party and Galaxy modules can use them. - If there's doubt about the appropriate license for a file in
module_utils, the Ansible Core Team will decide during an Ansible Core Community Meeting.
- A file in
-
All other files shipped with Ansible, including all modules, must be licensed under the GPL license (GPLv3 or later).
-
Existing license requirements still apply to content in ansible/ansible (ansible-core).
-
Content that was previously in ansible/ansible or a collection and has moved to a new collection must retain the license it had in its prior repository.
-
Copyright entries by previous committers must also be kept in any moved files.
api.py- Supports generic API modulesbasic.py- General definitions and helper utilities for Ansible modulescommon/dict_transformations.py- Helper functions for dictionary transformationscommon/file.py- Helper functions for working with filescommon/text/- Helper functions for converting and formatting textcommon/parameters.py- Helper functions for dealing with module parameterscommon/sys_info.py- Functions for getting distribution and platform informationcommon/validation.py- Helper functions for validating module parameters against a module argument specfacts/- Directory of utilities for modules that return facts. See PR 23012 for more informationjson_utils.py- Utilities for filtering unrelated output around module JSON output, like leading and trailing linespowershell/- Directory of definitions and helper functions for Windows PowerShell modulespycompat24.py- Exception workaround for Python 2.4secrets.py- Functions for registering secrets so they are masked in Ansible output. Seedeveloping_secret_maskingservice.py- Utilities to enable modules to work with Linux services (placeholder, not in use)six/__init__.py- Bundled copy of the Six Python library to aid in writing code compatible with both Python 2 and Python 3splitter.py- String splitting and manipulation utilities for working with Jinja2 templatesurls.py- Utilities for working with http and https requests
Several commonly-used utilities migrated to collections in Ansible 2.10, including:
ismount.pymigrated toansible.posix.plugins.module_utils.mount.py- Single helper function that fixes os.path.ismountknown_hosts.pymigrated tocommunity.general.plugins.module_utils.known_hosts.py- utilities for working with known_hosts file
For a list of migrated content with destination collections, see the runtime.yml file.
Should you develop a module?<span id="developing_modules"></span>
Developing Ansible modules is easy, but often it is not necessary. Before you start writing a new module, ask:
- Does a similar module already exist?
An existing module may cover the functionality you want. Ansible collections include thousands of modules. Search our list of included collections <list_of_collections> or Ansible Galaxy to see if an existing module does what you need.
- Should you use or develop an action plugin instead of a module?
An action plugin may be the best way to get the functionality you want. Action plugins run on the control node instead of on the managed node, and their functionality is available to all modules. For more information about developing plugins, read the developing plugins page <developing_plugins>.
- Should you use a role instead of a module?
A combination of existing modules may cover the functionality you want. You can write a role for this type of use case. Check out the roles documentation<playbooks_reuse_roles>.
- Should you create a collection instead of a single module?
The functionality you want may be too large for a single module. If you want to connect Ansible to a new cloud provider, database, or network platform, you may need to develop a new collection<developing_modules_in_groups>.
- Each module should have a concise and well defined functionality. Basically, follow the UNIX philosophy of doing one thing well.
- A module should not require that a user know all the underlying options of an API/tool to be used. For example, if the legal values for a required module parameter cannot be documented, that's a sign that the module would be rejected.
- Modules should typically encompass much of the logic for interacting with a resource. A lightweight wrapper around an API that does not contain much logic would likely cause users to offload too much logic into a playbook, and for this reason the module would be rejected. Instead try creating multiple modules for interacting with smaller individual pieces of the API.
If your use case isn't covered by an existing module, an action plugin, or a role, and you don't need to create multiple modules, then you're ready to start developing a new module. Choose from the topics below for next steps:
- I want to
get started on a new module <developing_modules_general>. - I want to review
tips and conventions for developing good modules <developing_modules_best_practices>. - I want to
write a Windows module <developing_modules_general_windows>. - I want
an overview of Ansible's architecture <developing_program_flow_modules>. - I want to
document my module <developing_modules_documenting>. - I want to
contribute my module to an existing Ansible collection <developing_modules_checklist>. - I want to
add unit and integration tests to my module <developing_testing>. - I want to
add Python 3 support to my module <developing_python_3>. - I want to
write multiple modules <developing_modules_in_groups>.
list_of_collections
Browse existing collections, modules, and plugins
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Conventions, tips, and pitfalls<span id="developing_modules_best_practices"></span>
Topics
As you design and develop modules, follow these basic conventions and tips for clean, usable code:
Scoping your module(s)
Especially if you want to contribute your module(s) to an existing Ansible Collection, make sure each module includes enough logic and functionality, but not too much. If these guidelines seem confusing, consider whether you really need to write a module <module_dev_should_you> at all.
- Each module should have a concise and well-defined functionality. Basically, follow the UNIX philosophy of doing one thing well.
- Do not add
get,listorinfostate options to an existing module - create a new_infoor_factsmodule. - Modules should not require that a user know all the underlying options of an API/tool to be used. For example, if the legal values for a required module option cannot be documented, the module does not belong in Ansible Core.
- Modules should encompass much of the logic for interacting with a resource. A lightweight wrapper around a complex API forces users to offload too much logic into their playbooks. If you want to connect Ansible to a complex API,
create multiple modules <developing_modules_in_groups>that interact with smaller individual pieces of the API. - Avoid creating a module that does the work of other modules; this leads to code duplication and divergence, and makes things less uniform, unpredictable and harder to maintain. Modules should be the building blocks. If you are asking 'how can I have a module execute other modules' ... you want to write a role.
Designing module interfaces
- If your module is addressing an object, the option for that object should be called
namewhenever possible, or acceptnameas an alias. - Modules accepting boolean status should accept
yes,no,true,false, or anything else a user may likely throw at them. The AnsibleModule common code supports this withtype='bool'. - Avoid
action/command, they are imperative and not declarative, there are other ways to express the same thing.
General guidelines & tips
- Each module should be self-contained in one file, so it can be auto-transferred by
ansible-core. - Module name MUST use underscores instead of hyphens or spaces as a word separator. Using hyphens and spaces will prevent
ansible-corefrom importing your module. - Always use the
hacking/test-module.pyscript when developing modules - it will warn you about common pitfalls. - If you have a local module that returns information specific to your installations, a good name for this module is
site_info. - Eliminate or minimize dependencies. If your module has dependencies, document them at the top of the module file and raise JSON error messages when dependency import fails.
- Don't write to files directly; use a temporary file and then use the
atomic_movefunction fromansible.module_utils.basicto move the updated temporary file into place. This prevents data corruption and ensures that the correct context for the file is kept. - Avoid creating caches. Ansible is designed without a central server or authority, so you cannot guarantee it will not run with different permissions, options or locations. If you need a central authority, have it on top of Ansible (for example, using bastion/cm/ci server, AWX, or the Red Hat Ansible Automation Platform); do not try to build it into modules.
- If you package your module(s) in an RPM, install the modules on the control machine in
/usr/share/ansible. Packaging modules in RPMs is optional.
Functions and Methods
- Each function should be concise and should describe a meaningful amount of work.
- "Don't repeat yourself" is generally a good philosophy.
- Function names should use underscores:
my_function_name. - The name of each function should describe what the function does.
- Each function should have a docstring.
- If your code is too nested, that's usually a sign the loop body could benefit from being a function. Parts of our existing code are not the best examples of this at times.
Python tips
- Include a
mainfunction that wraps the normal execution. - Call your
mainfunction from a conditional so you can import it into unit tests - for example:
if __name__ == '__main__':
main()
Importing and using shared code
import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
import foo
HAS_LIB = True
except:
HAS_LIB = False
LIB_IMP_ERR = traceback.format_exc()
Then in main(), just after the argspec, do
if not HAS_LIB:
module.fail_json(msg=missing_required_lib("foo"),
exception=LIB_IMP_ERR)
And document the dependency in the requirements section of your module's documentation_block.
Handling module failures
When your module fails, help users understand what went wrong. If you are using the AnsibleModule common Python code, the failed element will be included for you automatically when you call fail_json. For polite module failure behavior:
- Include a key of
failedalong with a string explanation inmsg. If you don't do this, Ansible will use standard return codes: 0=success and non-zero=failure. - Don't raise a traceback (stacktrace). Ansible can deal with stacktraces and automatically converts anything unparsable into a failed result, but raising a stacktrace on module failure is not user-friendly.
- Do not use
sys.exit(). Usefail_json()from the module object.
Handling exceptions (bugs) gracefully
- Validate upfront--fail fast and return useful and clear error messages.
- Use defensive programming--use a simple design for your module, handle errors gracefully, and avoid direct stacktraces.
- Fail predictably--if we must fail, do it in a way that is the most expected. Either mimic the underlying tool or the general way the system works.
- Give out a useful message on what you were doing and add exception messages to that.
- Avoid catchall exceptions, they are not very useful unless the underlying API gives very good error messages pertaining the attempted action.
Creating correct and informative module output
Modules must output valid JSON only. Follow these guidelines for creating correct, useful module output:
- Module return data must be encoded as strict UTF-8. Modules that cannot return UTF-8 encoded data should return the data encoded by something such as base64. Optionally modules can make the determination if they can encode as UTF-8 and utilize
errors='replace'to replace non UTF-8 characters making the return values lossy. - Make your top-level return type a hash (dictionary).
- Nest complex return values within the top-level hash.
- Incorporate any lists or simple scalar values within the top-level return hash.
- Do not send module output to standard error, because the system will merge standard out with standard error and prevent the JSON from parsing.
- Capture standard error and return it as a variable in the JSON on standard out. This is how the command module is implemented.
- Never do
print("some status message")in a module, because it will not produce valid JSON output. - Always return useful data, even when there is no change.
- Be consistent about returns (some modules are too random), unless it is detrimental to the state/action.
- Make returns reusable--most of the time you don't want to read it, but you do want to process it and re-purpose it.
- Return diff if in diff mode. This is not required for all modules, as it won't make sense for certain ones, but please include it when applicable.
- Enable your return values to be serialized as JSON with Python's standard JSON encoder and decoder library. Basic python types (strings, int, dicts, lists, and so on) are serializable.
- Do not return an object using exit_json(). Instead, convert the fields you need from the object into the fields of a dictionary and return the dictionary.
- Results from many hosts will be aggregated at once, so your module should return only relevant output. Returning the entire contents of a log file is generally bad form.
If a module returns stderr or otherwise fails to produce valid JSON, the actual output will still be shown in Ansible, but the command will not succeed.
Following Ansible conventions
Ansible conventions offer a predictable user interface across all modules, playbooks, and roles. To follow Ansible conventions in your module development:
- Use consistent names across modules (yes, we have many legacy deviations - don't make the problem worse!).
- Use consistent options (arguments) within your module(s).
- Do not use 'message' or 'syslog_facility' as an option name, because this is used internally by Ansible.
- Normalize options with other modules - if Ansible and the API your module connects to use different names for the same option, add aliases to your options so the user can choose which names to use in tasks and playbooks.
- Return facts from
*_factsmodules in theansible_factsfield of theresult dictionary<common_return_values>so other modules can access them. - Implement
check_modein all*_infoand*_factsmodules. Playbooks which conditionalize based on fact information will only conditionalize correctly incheck_modeif the facts are returned incheck_mode. Usually you can addsupports_check_mode=Truewhen instantiatingAnsibleModule. - Use module-specific environment variables. For example, if you use the helpers in
module_utils.apifor basic authentication withmodule_utils.urls.fetch_url()and you fall back on environment variables for default values, use a module-specific environment variable likeAPI_<MODULENAME>_USERNAMEto avoid conflicts between modules. - Keep module options simple and focused - if you're loading a lot of choices/states on an existing option, consider adding a new, simple option instead.
- Keep options small when possible. Passing a large data structure to an option might save us a few tasks, but it adds a complex requirement that we cannot easily validate before passing on to the module.
- If you want to pass complex data to an option, write an expert module that allows this, along with several smaller modules that provide a more 'atomic' operation against the underlying APIs and services. Complex operations require complex data. Let the user choose whether to reflect that complexity in tasks and plays or in vars files.
- Implement declarative operations (not CRUD) so the user can ignore existing state and focus on final state. For example, use
started/stopped,present/absent. - Strive for a consistent final state (aka idempotency). If running your module twice in a row against the same system would result in two different states, see if you can redesign or rewrite to achieve consistent final state. If you can't, document the behavior and the reasons for it.
- Provide consistent return values within the standard Ansible return structure, even if NA/None are used for keys normally returned under other options.
Module Security
- Avoid passing user input from the shell.
- Always check return codes.
- You must always use
module.run_command, notsubprocessorPopenoros.system. - Avoid using the shell unless absolutely necessary.
- If you must use the shell, you must pass
use_unsafe_shell=Truetomodule.run_command. - If any variables in your module can come from user input with
use_unsafe_shell=True, you must wrap them withpipes.quote(x). - When fetching URLs, use
fetch_urloropen_urlfromansible.module_utils.urls. Do not useurllib2, which does not natively verify TLS certificates and so is insecure for https. - Sensitive values marked with
no_log=Trueare automatically hidden from Ansible output. Sinceansible-core2.22 the value is preserved in the result and redacted in the output, on earlier versions the value is redacted in the result itself. Seedeveloping_secret_maskingfor details.
Contributing your module to an existing Ansible collection<span id="developing_modules_checklist"></span>
If you want to contribute a module to an existing collection, you must meet the community's objective and subjective requirements. Please read the details below, and also review our tips for module development <developing_modules_best_practices>.
Modules accepted into certain collections are included in every Ansible release on PyPI. However, contributing to one of these collections is not the only way to distribute a module - you can create your own collection <developing_collections>, embed modules in roles on Galaxy or simply share copies of your module code for local use <developing_locally>.
Contributing modules: objective requirements
To contribute a module to most Ansible collections, you must:
- write your module in either Python or Powershell for Windows
- use the
AnsibleModulecommon code - support Python 2.6 and Python 3.5 - if your module cannot support Python 2.6, explain the required minimum Python version and rationale in the requirements section in
DOCUMENTATION - use proper
Python 3 syntax <developing_python_3> - follow PEP 8 Python style conventions - see
testing_pep8for more information - license your module under the GPL license (GPLv3 or later)
- understand the
DCO agreement <developer_certificate_of_origin>, which applies to contributions to the Ansible Core and Ansible Documentation repositories. - conform to Ansible's
formatting and documentation <developing_modules_documenting>standards - include comprehensive
tests <developing_testing>for your module - minimize module dependencies
- support
check_mode <check_mode_dry>if possible - ensure your code is readable
- if a module is named
<something>_facts, it should be because its main purpose is returningansible_facts. Do not name modules that do not do this with_facts. Only useansible_factsfor information that is specific to the host machine, for example network interfaces and their configuration, which operating system and which programs are installed. - Modules that query/return general information (and not
ansible_facts) should be named_info. General information is non-host specific information, for example information on online/cloud services (you can access different accounts for the same online service from the same host), or information on VMs and containers accessible from the machine.
Additional requirements may apply for certain collections. Review the individual collection repositories for more information.
Please make sure your module meets these requirements before you submit your PR/proposal. If you have questions, visit the Ansible communication guide<communication> for information on how to reach out to the community.
Contributing to Ansible: subjective requirements
If your module meets these objective requirements, collection maintainers will review your code to see if they think it is clear, concise, secure, and maintainable. They will consider whether your module provides a good user experience, helpful error messages, reasonable defaults, and more. This process is subjective, with no exact standards for acceptance. For the best chance of getting your module accepted, follow our tips for module development <developing_modules_best_practices>.
Other checklists
Tips for module development <developing_modules_best_practices>.Windows development checklist <developing_modules_general_windows>.
Module format and documentation<span id="developing_modules_documenting"></span>
In most cases if you want to contribute your module to an Ansible collection, you should write your module in Python and follow the standard format described below. If you are writing a Windows module, you should follow the Windows guidelines <developing_modules_general_windows>.
Before you open a pull request, in addition to following these guidelines, please also review and adhere to the practices outlined in the following sections:
submission checklist <developing_modules_checklist>programming tips <developing_modules_best_practices>testing <developing_testing>before you open a pull request
Every Ansible module written in Python must begin with seven standard sections in a particular order, followed by the code. The sections in order are:
If you are curious why imports are not located at the top of the file, see the python_imports section.
If you see any discrepancies in older Ansible modules, please open a pull request with modifications that satisfy these guidelines.
Non-Python modules documentation
For modules written in languages other than Python, there are two approaches to handling documentation:
- Option one: Create a
.pyfile that contains the documentation-related sections described in this document. - Option two: Create a
.ymlfile that has the same data structure in pure YAML.- With YAML files, the examples below are easy to use by removing Python quoting and substituting
=for:, for exampleDOCUMENTATION = r''' ... '''toDOCUMENTATION: ...and removing closing quotes. Refer toadjacent_yaml_docfor details.
- With YAML files, the examples below are easy to use by removing Python quoting and substituting
Python shebang
- Begin your Ansible module with the
#!/usr/bin/pythonshebang so thatansible_python_interpreterworks.
- If you develop the module using a different scripting language, adjust the interpreter accordingly (
#!/usr/bin/<interpreter>) soansible_<interpreter>_interpretercan work for that specific language.- Binary modules do NOT require a shebang or an interpreter.
- Do NOT use
#!/usr/bin/envbecause it makesenvthe interpreter and bypassesansible_<interpreter>_interpreterlogic.- Passing arguments to the interpreter in the shebang does not work; for example,
#!/usr/bin/env python.
- Do not add an encoding comment such as
# -*- coding: utf-8 -*-. It was required for Python 2 but is unnecessary in Python 3, which treats source files as UTF-8 by default. Since ansible-core 2.17 dropped Python 2 support, existing encoding comments can be removed.
Copyright and license
- After the shebang, add a copyright line with the original copyright holder and a license declaration.
- The license declaration should be one line ONLY, not the full GPL prefix, as follows:
#!/usr/bin/python
# Copyright: Contributors to the Ansible project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
- Additions to the module MUST NOT include additional copyright lines beyond the default statement, unless the default copyright statement is missing.
# Copyright: Contributors to the Ansible project
- Any legal review will include the source control history, so an exhaustive copyright header is not necessary.
- Please do NOT include a copyright year.
- If the existing copyright statement includes a year, do NOT edit the existing copyright year.
- Do NOT modify the existing copyright header without permission from the copyright author.
DOCUMENTATION block
Before committing your module documentation, please test it at the command line and as HTML <dev_testing_module_documentation>.
After the shebang, the copyright line, and the license section comes the DOCUMENTATION block. Ansible's online module documentation is generated from the DOCUMENTATION blocks in the source code of each module.
The DOCUMENTATION block must be valid YAML. To make it easier:
- Start by copying our example documentation string.
- Write the block in an
editor with YAML syntax highlighting <other_tools_and_programs>before you include it in your Python file. - If you run into syntax issues that are difficult to resolve, use the YAML Lint website to help validate the YAML.
When writing module documentation, take the following statements into consideration:
- Module documentation should briefly and accurately define what each module and option does and how it works with others in the underlying system.
- Module documentation should be written for a broad audience and be easily understood both by experts and non-experts.
- Descriptions should always start with a capital letter and end with a full stop or period. Consistency always helps.
- For password and secret arguments
no_log=Trueshould be set and any example passwords, secrets, or hashes should start withEXAMPLEto ensure no real passwords and so on are leaked. - For arguments that seem to contain sensitive information but do not contain secrets, such as "password_length", set
no_log=Falseto disable the warning message. - If an option is only required in certain conditions, describe those conditions; for example, "Required when I(state=present)."
- If your module allows
check_mode, reflect this fact in the documentation. - To create clear, concise, consistent, and useful documentation, follow the
style guide <style_guide>.
Each documentation field is described below.
Documentation fields
- All fields in the
DOCUMENTATIONblock are lower-case. - All fields are required unless specified otherwise.
module
- The name of the module.
- Must be the same as the file name, without the
.pyextension.
short_description
- A short description which is displayed on the
list_of_collectionspage andansible-doc -l.- The
short_descriptionis displayed byansible-doc -lwithout any category grouping, so it needs enough detail to explain the module's purpose without the context of the directory structure in which it lives.- Unlike
description:,short_descriptionMUST NOT have a trailing period/full stop.- You can use
Ansible markup <ansible_markup>in this field.- Put
short_descriptionat the top of theDOCUMENTATIONblock, beforeoptions.ansible-doc -ldoes not parse the YAML; it uses the firstshort_description:line it finds, so an option with the same name that comes earlier would be used instead.
description
- A detailed description (generally two or more sentences).
- Each sentence MUST be full: start with a capital letter and end with a period.
- SHOULD NOT mention the module name.
- Make use of multiple entries rather than using one long paragraph.
- MUST NOT quote complete values unless it is required by YAML.
- You can use
Ansible markup <ansible_markup>in this field.
version_added
- This is a string, not a float, and should be quoted to avoid errors.
- For
ansible.builtin.*modules (included inansible-core), it is a version ofansible-core, for example,version_added: '2.18'- In collections, it MUST be a version of a collection (not the Ansible version) when the module was added, for example,
version_added: '1.0.0'.
- Name of the module author in the form
First Last (@GitHubID).- Use a multi-line list if there is more than one author.
- Do NOT use quotes unless it is required by YAML.
deprecated
- Marks modules that will be removed in future releases. See also
module_lifecycle.
options
- Options are often called "parameters" or "arguments". Because the documentation field is called
options, we will use that term.- If the module has no options (for example, it is a
_factsmodule), all you need is one line:options: {}.- If your module has options (in other words, accepts arguments), document them thoroughly. For each module option, include:
option-name
- Name it as a declarative operation (not CRUD) that focuses on the final state, for example
online:, rather thanis_online:.- Make the name consistent with the rest of the module, as well as other modules in the same category.
- When in doubt, look for other modules to find option names that are used for the same purpose, we like to offer consistency to our users.
- There is no explicit field
option-name. This entry is about the key of the option in theoptionsdictionary.description
- Detailed explanation of what this option does. Write it in full sentences that start with a capital letter and end with a period.
- The first entry is a description of the option itself; subsequent entries detail its use, dependencies, or format of possible values.
- Do NOT list the possible values (that's what the
choices:field is for, though it should explain what the values do if they are not obvious).- If an option is only sometimes required, describe the conditions. For example, "Required when O(state=present)."
- Mutually exclusive options MUST be documented as the final sentence on each of the options.
- You can use
Ansible markup <ansible_markup>in this field.required
- Only needed if
true.- If missing, we assume the option is not required.
default
- If
requiredis eitherfalseor missing,defaultmay be specified (assumednullif missing).- Ensure that the default value in the docs matches the default value in the code.
- The default field MUST NOT be listed as part of the description, unless it requires additional information or conditions.
- If the option is a boolean value, you can use any of the boolean values recognized by Ansible (such as
true/falseoryes/no). Document booleans astrue/falsefor consistency and compatibility with ansible-lint.choices
- List of option values.
- Do NOT use it if empty.
type
- Specifies the data type that option accepts, MUST match the
argument_specdictionary.- If an argument is
type='bool', set it totype: booland do NOT specifychoices.- If an argument is
type='list', specifyelements.elements
- Specifies the data type for list elements in case
type='list'.aliases
- List of optional name aliases.
- Generally not needed and not recommended to ensure consistency in the module usage.
version_added
- Only needed if this option was added after initial module release; in other words, this is greater than the top (module) level
version_addedfield.- This is a string, not a float, for example, for a module in ansible-core this could be
version_added: '2.18'.- In collections, this MUST be the collection version the option was added to, not the Ansible version. For example,
version_added: '1.0.0'.suboptions
- If this option takes a dict or list of dicts, you can define the structure here.
- See
azure.azcollection.azure_rm_securitygroup#module,azure.azcollection.azure_rm_azurefirewall#module, andopenstack.cloud.baremetal_node_action#modulefor examples.
requirements
- List of requirements (if applicable).
- Include minimum versions.
- You can use
Ansible markup <ansible_markup>in this field.
seealso
A list of references to other modules, documentation, or internet resources.
Because it is more prominent, use
seealsofor general references instead ofnotesor adding links to the moduledescription.References to modules MUST use the FQCN or
ansible.builtinfor modules inansible-core.Plugin references are supported since ansible-core 2.15.
You can use
Ansible markup <ansible_markup>in thedescriptionandnamefields.A reference can be one of the following formats:
seealso: # Reference by module name - module: cisco.aci.aci_tenant # Reference by module name, including description - module: cisco.aci.aci_tenant description: ACI module to create tenants on a Cisco ACI fabric. # Reference by plugin name - plugin: ansible.builtin.file plugin_type: lookup # Reference by plugin name, including description - plugin: ansible.builtin.file plugin_type: lookup description: You can use the ansible.builtin.file lookup to read files on the control node. # Reference by rST documentation anchor - ref: aci_guide description: Detailed information on how to manage your ACI infrastructure using Ansible. # Reference by rST documentation anchor (with custom title) - ref: The official Ansible ACI guide <aci_guide> description: Detailed information on how to manage your ACI infrastructure using Ansible. # Reference by Internet resource - name: APIC Management Information Model reference description: Complete reference of the APIC object model. link: https://developer.cisco.com/docs/apic-mim-ref/If you use
ref:to link to an anchor that is not associated with a title, you MUST add a title to the ref for the link to work correctly.
attributes
- A dictionary mapping attribute names to dictionaries describing that attribute.
- Usually attributes are provided by documentation fragments, for example
ansible.builtin.action_common_attributesand its sub-fragments. Modules and plugins use the appropriate docs fragments and fill in thesupport,details, and potential attribute-specific other fields.description
- Required.
- A string or a list of strings. Each string is one paragraph.
- Explanation of what this attribute does. It should be written in full sentences.
- You can use
Ansible markup <ansible_markup>in this field.details
- Generally optional, but must be provided if
supportispartial.- A string or a list of strings. Each string is one paragraph.
- Describes how support might not work as expected by the user.
- You can use
Ansible markup <ansible_markup>in this field.support
- Required.
- Must be one of
full,none,partial, orN/A.- Indicates whether this attribute is supported by this module or plugin.
membership
- MUST ONLY be provided for the attribute
action_group.- Lists the action groups this module or action is part of.
- A string or a list of strings.
platforms
- MUST ONLY be used for the attribute
platform.- Lists the platforms the module or action supports.
- A string or a list of strings.
version_added
- Only needed if this attribute's support was extended after the module/plugin was created, in other words, this is greater than the top (module) level
version_addedfield.- This is a string, and not a float, for example,
version_added: '2.3'.- In collections, this must be the collection version the attribute's support was added to, not the Ansible version. For example,
version_added: '1.0.0'.
notes
- Details of any important information that does not fit in one of the above sections.
- Do NOT list
check_modeordiffinformation undernotes. Use theattributesfield instead.- Because it stands out better, use
seealsofor general references over the use ofnotes.- You can use
Ansible markup <ansible_markup>in this field.
Documentation fragments
If you are writing multiple related modules, they may share common documentation, such as options, authentication details, file mode settings, notes: or seealso: entries. Rather than duplicate that information in each module's DOCUMENTATION block, you can save it once as a doc_fragment plugin and then include it in each module's documentation.
Modules should only use items from a doc fragment if the module will implement all of the interface documented there in a manner that behaves the same as the existing modules which import that fragment. The goal is that items imported from the doc fragment will behave identically when used in another module that imports the doc fragment.
By default, only the DOCUMENTATION property from a doc fragment is inserted into the module documentation. It is possible to define additional properties in the doc fragment in order to import only certain parts of a doc fragment or mix and match as appropriate. If a property is defined in both the doc fragment and the module, the module value overrides the doc fragment.
Here is an example doc fragment named example_fragment.py:
class ModuleDocFragment(object):
# Standard documentation
DOCUMENTATION = r'''
options:
# options here
'''
# Additional section
OTHER = r'''
options:
# other options here
'''
To insert the contents of OTHER in a module:
extends_documentation_fragment: example_fragment.other
Or use both :
extends_documentation_fragment:
- example_fragment
- example_fragment.other
2.8
Since Ansible 2.8, you can have user-supplied doc_fragments by using a doc_fragments directory adjacent to play or role, just like any other plugin.
For example, all AWS modules should include:
extends_documentation_fragment:
- aws
- ec2
docfragments_collections describes how to incorporate documentation fragments in a collection.
EXAMPLES block
Immediately after the DOCUMENTATION block comes the EXAMPLES block. Here you show users how your module works with real-world examples in multi-line plain-text YAML format. The best examples are ready for the user to copy and paste into a playbook. Review and update your examples with every change to your module.
If the module has integration tests, add the example you want to add to the integration tests to make sure it works.
Best practices are:
- Each example should include a
name:line:
EXAMPLES = r'''
- name: Ensure foo is installed
namespace.collection.modulename:
name: foo
state: present
'''
- The
name:line should be capitalized and not include a trailing dot. - Use a fully qualified collection name (FQCN) as a part of the module's name like in the example above.
- For modules in
ansible-core, use theansible.builtin.identifier, for exampleansible.builtin.debug.
- For modules in
- If your examples use boolean options, use true/false values. Since the documentation generates boolean values as true/false, having the examples use these values as well makes the module documentation more consistent.
- If your module returns facts that are often needed, consider adding an example of how to use them.
RETURN block
Right after the EXAMPLES block comes the RETURN block. This section documents the information the module returns for use by other modules.
If your module does not return anything (apart from the standard returns made by ansible-core), specify it as RETURN = r''' # ''' Otherwise, for each value returned, provide the following fields. All the fields are required unless specified otherwise:
return name
Name of the returned field.
description
Detailed description of what this value represents. Capitalized and with a trailing dot. You can use Ansible markup <ansible_markup> in this field.
returned
When this value is returned, such as always, changed or success. This is a string and can contain any human-readable content.
type
Data type.
elements
If type='list', specifies the data type of the list's elements.
sample
One or more examples.
version_added
Only needed if this return was extended after initial module release, in other words, this is greater than the top (module) level version_added field. This is a string, and not a float, for example, version_added: '2.3'.
contains
Optional. To describe nested return values, set type: dict, or type: list/elements: dict, or if you really have to, type: complex, and repeat the elements above for each sub-field.
Here are two example RETURN sections, one with three simple fields and one with a complex nested field:
RETURN = r'''
dest:
description: Destination file/path.
returned: success
type: str
sample: /path/to/file.txt
src:
description: Source file used for the copy on the target machine.
returned: changed
type: str
sample: /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source
md5sum:
description: MD5 checksum of the file after running copy.
returned: when supported
type: str
sample: 2a5aeecc61dc98c4d780b14b330e3282
'''
RETURN = r'''
packages:
description: Information about package requirements.
returned: success
type: dict
contains:
missing:
description: Packages that are missing from the system.
returned: success
type: list
elements: str
sample:
- libmysqlclient-dev
- libxml2-dev
badversion:
description: Packages that are installed but at bad versions.
returned: success
type: list
elements: dict
sample:
- package: libxml2-dev
version: 2.9.4+dfsg1-2
constraint: ">= 3.0"
'''
Python imports
Immediately after the RETURN block, add the Python imports. All modules must use Python imports in the form:
from module_utils.basic import AnsibleModule
The use of "wildcard" imports such as from module_utils.basic import * is no longer allowed.
Why don't the imports go first?
Since the DOCUMENTATION, EXAMPLES, and RETURN blocks are essentially extra docstrings for the file and are not used by the module code itself, the import statements are placed after these special variables. Positioning the imports closer to the functional code helps consolidate related elements, improving readability, debugging, and overall comprehension.
Testing module documentation
- Before committing your module documentation, please test it on the command line and as HTML as described on the
testing_module_documentationpage. - To test documentation in collections, please see
build_collection_docsite.
Developing modules<span id="developing_modules_general"></span>
A module is a reusable, standalone script that Ansible runs on your behalf, either locally or remotely. Modules interact with your local machine, an API, or a remote system to perform specific tasks like changing a database password or spinning up a cloud instance. Each module can be used by the Ansible API, or by the ansible or ansible-playbook programs. A module provides a defined interface, accepts arguments, and returns information to Ansible by printing a JSON string to stdout before exiting.
If you need functionality that is not available in any of the thousands of Ansible modules found in collections, you can easily write your own custom module. When you write a module for local use, you can choose any programming language and follow your own rules. Use this topic to learn how to create an Ansible module in Python. After you create a module, you must add it locally to the appropriate directory so that Ansible can find and execute it. For details about adding a module locally, see developing_locally.
If you are developing a module in a collection <developing_collections>, see those documents instead.
Preparing an environment for developing Ansible modules
You just need ansible-core installed to test the module. Modules can be written in any language, but most of the following guide is assuming you are using Python. Modules for inclusion in Ansible itself must be Python or Powershell.
One advantage of using Python or Powershell for your custom modules is being able to use the module_utils common code that does a lot of the heavy lifting for argument processing, logging and response writing, among other things.
Creating a standalone module
It is highly recommended that you use a venv or virtualenv for Python development.
To create a standalone module:
- Create a
librarydirectory in your workspace. Your test play should live in the same directory. - Create your new module file:
$ touch library/my_test.py. Or just open/create it with your editor of choice. - Paste the content below into your new module file. It includes the
required Ansible format and documentation <developing_modules_documenting>, a simpleargument spec for declaring the module options <argument_spec>, and some example code. - Modify and extend the code to do what you want your new module to do. See the
programming tips <developing_modules_best_practices>andPython 3 compatibility <developing_python_3>pages for pointers on writing clean and concise module code.
Creating a module in a collection
To create a new module in an existing collection called my_namespace.my_collection:
- Create your new module file:
$ touch <PATH_TO_COLLECTION>/ansible_collections/my_namespace/my_collection/plugins/modules/my_test.py. Or just create it with your editor of choice. - Paste the content below into your new module file. It includes the
required Ansible format and documentation <developing_modules_documenting>, a simpleargument spec for declaring the module options <argument_spec>, and some example code. - Modify and extend the code to do what you want your new module to do. See the
programming tips <developing_modules_best_practices>andPython 3 compatibility <developing_python_3>pages for pointers on writing clean and concise module code.
../../../../examples/scripts/my_test.py
Creating an info or a facts module
Ansible gathers information about the target machines using facts modules, and gathers information on other objects or files using info modules. If you find yourself trying to add state: info or state: list to an existing module, that is often a sign that a new dedicated _facts or _info module is needed.
In Ansible 2.8 and onwards, we have two type of information modules, they are *_info and *_facts.
If a module is named <something>_facts, it should be because its main purpose is returning ansible_facts. Do not name modules that do not do this with _facts. Only use ansible_facts for information that is specific to the host machine, for example network interfaces and their configuration, which operating system and which programs are installed.
Modules that query/return general information (and not ansible_facts) should be named _info. General information is non-host specific information, for example information on online/cloud services (you can access different accounts for the same online service from the same host), or information on VMs and containers accessible from the machine, or information on individual files or programs.
Info and facts modules, are just like any other Ansible Module, with a few minor requirements:
- They MUST be named
<something>_infoor<something>_facts, where <something> is singular. - Info
*_infomodules MUST return in the form of theresult dictionary<common_return_values>so other modules can access them. - Fact
*_factsmodules MUST return in theansible_factsfield of theresult dictionary<common_return_values>so other modules can access them. - They MUST support
check_mode <check_mode_dry>. - They MUST NOT make any changes to the system.
- They MUST document the
return fields<return_block>andexamples<examples_block>.
You can add your facts into ansible_facts field of the result as follows:
module.exit_json(changed=False, ansible_facts=dict(my_new_fact=value_of_fact))
The rest is just like creating a normal module.
Verifying your module code
After you modify the sample code above to do what you want, you can try out your module. Our debugging tips <debugging_modules> will help if you run into bugs as you verify your module code.
Verifying your module code locally
The simplest way is to use ansible adhoc command:
ANSIBLE_LIBRARY=./library ansible -m my_test -a 'name=hello new=true' remotehost
If your module does not need to target a remote host, you can quickly and easily exercise your code locally like this:
ANSIBLE_LIBRARY=./library ansible -m my_test -a 'name=hello new=true' localhost
For a module developed in an existing collection called my_namespace.my_collection (as mentioned above):
$ ansible localhost -m my_namespace.my_collection.my_test -a 'name=hello new=true' --playbook-dir=$PWD
- If you use
pdb,print(), or some other method of local debugging for faster iteration, you can avoid going through Ansible by creating an arguments file, which is a basic JSON config file that passes parameters to your module so that you can run it. Name the arguments file/tmp/args.jsonand add the following content:
{
"ANSIBLE_MODULE_ARGS": {
"name": "hello",
"new": true
}
}
- Then the module can be tested locally and directly. This skips the packing steps and uses module_utils files directly:
$ python library/my_test.py /tmp/args.json
You might also need to add your collection's path to the Python path. This tells Python to look for additional module_utils code in the given path. You can run the module code, as in the following example:
$ export PYTHONPATH=PATH_TO_COLLECTIONS:$PYTHONPATH
$ python -m ansible_collections.my_namespace.my_collection.plugins.modules.my_test /tmp/args.json
It should return output like this:
{"changed": true, "state": {"original_message": "hello", "new_message": "goodbye"}, "invocation": {"module_args": {"name": "hello", "new": true}}}
Verifying your module code in a playbook
You can easily run a full test by including it in a playbook, as long as the library directory is in the same directory as the play:
- Create a playbook in any directory:
$ touch testmod.yml - Add the following to the new playbook file:
- name: test my new module
hosts: localhost
tasks:
- name: run the new module
my_test:
name: 'hello'
new: true
register: testout
- name: dump test output
debug:
msg: '{{ testout }}'
- Run the playbook and analyze the output:
$ ansible-playbook ./testmod.yml
Testing your newly-created module
Review our testing <developing_testing> section for more detailed information, including instructions for testing module documentation <testing_module_documentation>, adding integration tests <testing_integration>, and more.
If contributing to Ansible, every new module and plugin should have integration tests, even if the tests cannot be run on Ansible CI infrastructure. In this case, the tests should be marked with the unsupported alias in aliases file <integration_aliases>.
Contributing back to Ansible
If you would like to contribute to ansible-core by adding a new feature or fixing a bug, create a fork of the ansible/ansible repository and develop against a new feature branch using the devel branch as a starting point. When you have a good working code change, you can submit a pull request to the Ansible repository by selecting your feature branch as a source and the Ansible devel branch as a target.
If you want to contribute a module to an Ansible collection <contributing_maintained_collections>, review our submission checklist <developing_modules_checklist>, programming tips <developing_modules_best_practices>, and strategy for maintaining Python 2 and Python 3 compatibility <developing_python_3>, as well as information about testing <developing_testing> before you open a pull request.
The Community Guide <ansible_community_guide> covers how to open a pull request and what happens next.
Communication and development support
Visit the Ansible communication guide<communication> for information on how to join the conversation.
Credit
Thank you to Thomas Stringer (@trstringer) for contributing source material for this topic.
Windows module development walkthrough
In this section, we will walk through developing, testing, and debugging an Ansible Windows module.
Because Windows modules are written in Powershell and need to be run on a Windows host, this guide differs from the usual development walkthrough guide.
What's covered in this section:
Windows environment setup
Unlike Python module development which can be run on the host that runs Ansible, Windows modules need to be written and tested for Windows hosts. While evaluation editions of Windows can be downloaded from Microsoft, these images are usually not ready to be used by Ansible without further modification. The easiest way to set up a Windows host so that it is ready to by used by Ansible is to set up a virtual machine using Vagrant. Vagrant can be used to download existing OS images called boxes that are then deployed to a hypervisor like VirtualBox. These boxes can either be created and stored offline or they can be downloaded from a central repository called Vagrant Cloud.
This guide will use the Vagrant boxes created by the packer-windoze repository which have also been uploaded to Vagrant Cloud. To find out more info on how these images are created, please go to the GitHub repo and look at the README file.
Before you can get started, the following programs must be installed (please consult the Vagrant and VirtualBox documentation for installation instructions):
- Vagrant
- VirtualBox
Create a Windows server in a VM
To create a single Windows Server 2016 instance, run the following:
vagrant init jborean93/WindowsServer2016
vagrant up
This will download the Vagrant box from Vagrant Cloud and add it to the local boxes on your host and then start up that instance in VirtualBox. When starting for the first time, the Windows VM will run through the sysprep process and then create a HTTP and HTTPS WinRM listener automatically. Vagrant will finish its process once the listeners are online, after which the VM can be used by Ansible.
Create an Ansible inventory
The following Ansible inventory file can be used to connect to the newly created Windows VM:
[windows]
WindowsServer ansible_host=127.0.0.1
[windows:vars]
ansible_user=vagrant
ansible_password=vagrant
ansible_port=55986
ansible_connection=winrm
ansible_winrm_transport=ntlm
ansible_winrm_server_cert_validation=ignore
The port 55986 is automatically forwarded by Vagrant to the Windows host that was created, if this conflicts with an existing local port then Vagrant will automatically use another one at random and display show that in the output.
The OS that is created is based on the image set. The following images can be used:
- jborean93/WindowsServer2012
- jborean93/WindowsServer2012R2
- jborean93/WindowsServer2016
- jborean93/WindowsServer2019
- jborean93/WindowsServer2022
When the host is online, you can access it by RDP on 127.0.0.1:3389 but the port may differ depending on whether there was a conflict. To get rid of the host, run vagrant destroy --force and Vagrant will automatically remove the VM and any other files associated with it.
While this is useful when testing modules on a single Windows instance, these host won't work without modification with domain based modules. The Vagrantfile at ansible-windows can be used to create a test domain environment to be used in Ansible. This repo contains three files which are used by both Ansible and Vagrant to create multiple Windows hosts in a domain environment. These files are:
Vagrantfile: The Vagrant file that reads the inventory setup ofinventory.ymland provisions the hosts that are requiredinventory.yml: Contains the hosts that are required and other connection information such as IP addresses and forwarded portsmain.yml: Ansible playbook called by Vagrant to provision the domain control node and join the child hosts to the domain
By default, these files will create the following environment:
- A single AD domain controller running on Windows Server 2016
- Five child hosts for each major Windows Server version joined to that domain
- A domain with the DNS name
domain.local - A local administrator account on each host with the username
vagrantand passwordvagrant - A domain admin account
vagrant-domain@domain.localwith the passwordVagrantPass1
The domain name and accounts can be modified by changing the variables domain_* in the inventory.yml file if it is required. The inventory file can also be modified to provision more or less servers by changing the hosts that are defined under the domain_children key. The host variable ansible_host is the private IP that will be assigned to the VirtualBox host only network adapter while vagrant_box is the box that will be used to create the VM.
Provisioning the environment
To provision the environment as is, run the following:
git clone https://github.com/jborean93/ansible-windows.git
cd vagrant
vagrant up
Vagrant provisions each host sequentially so this can take some time to complete. If any errors occur during the Ansible phase of setting up the domain, run vagrant provision to rerun just that step.
Unlike setting up a single Windows instance with Vagrant, these hosts can also be accessed using the IP address directly as well as through the forwarded ports. It is easier to access it over the host only network adapter as the normal protocol ports are used, for example RDP is still over 3389. In cases where the host cannot be resolved using the host only network IP, the following protocols can be access over 127.0.0.1 using these forwarded ports:
RDP: 295xxSSH: 296xxWinRM HTTP: 297xxWinRM HTTPS: 298xxSMB: 299xx
Replace xx with the entry number in the inventory file where the domain controller started with 00 and is incremented from there. For example, in the default inventory.yml file, WinRM over HTTPS for SERVER2012R2 is forwarded over port 29804 as it is the fourth entry in domain_children.
Windows new module development
When creating a new module there are a few things to keep in mind:
- Module code is in Powershell (.ps1) files while the documentation is contained in Python (.py) files of the same name
- Avoid using
Write-Host/Debug/Verbose/Errorin the module and add what needs to be returned to the$module.Resultvariable - To fail a module, call
$module.FailJson("failure message here"), an Exception or ErrorRecord can be set to the second argument for a more descriptive error message - You can pass in the exception or ErrorRecord as a second argument to
FailJson("failure", $_)to get a more detailed output - Most new modules require check mode and integration tests before they are merged into the main Ansible codebase
- Avoid using try/catch statements over a large code block, rather use them for individual calls so the error message can be more descriptive
- Try and catch specific exceptions when using try/catch statements
- Avoid using PSCustomObjects unless necessary
- Look for common functions in
./lib/ansible/module_utils/powershell/and use the code there instead of duplicating work. These can be imported by adding the line#Requires -Module *where * is the file name to import, and will be automatically included with the module code sent to the Windows target when run through Ansible - As well as PowerShell module utils, C# module utils are stored in
./lib/ansible/module_utils/csharp/and are automatically imported in a module execution if the line#AnsibleRequires -CSharpUtil *is present - C# and PowerShell module utils achieve the same goal but C# allows a developer to implement low level tasks, such as calling the Win32 API, and can be faster in some cases
- Ensure the code runs under Powershell v5.1 and higher on Windows Server 2016 and higher; if higher minimum Powershell or OS versions are required, ensure the documentation reflects this clearly
- Ansible runs modules under strictmode version 2.0. Be sure to test with that enabled by putting
Set-StrictMode -Version 2.0at the top of your dev script - Favor native Powershell cmdlets over executable calls if possible
- Use the full cmdlet name instead of aliases, for example
Remove-Itemoverrm - Use named parameters with cmdlets, for example
Remove-Item -Path C:\tempoverRemove-Item C:\temp
A very basic Powershell module win_environment incorporates best practices for Powershell modules. It demonstrates how to implement check-mode and diff-support, and also shows a warning to the user when a specific condition is met.
A slightly more advanced module is win_uri which additionally shows how to use different parameter types (bool, str, int, list, dict, path) and a selection of choices for parameters, how to fail a module and how to handle exceptions.
As part of the new AnsibleModule wrapper, the input parameters are defined and validated based on an argument spec. The following options can be set at the root level of the argument spec:
-
mutually_exclusive: A list of lists, where the inner list contains module options that cannot be set together -
no_log: Stops the module from emitting any logs to the Windows Event log -
options: A dictionary where the key is the module option and the value is the spec for that option -
required_by: A dictionary where the option(s) specified by the value must be set if the option specified by the key is also set -
required_if: A list of lists where the inner list contains 3 or 4 elements;- The first element is the module option to check the value against
- The second element is the value of the option specified by the first element, if matched then the required if check is run
- The third element is a list of required module options when the above is matched
- An optional fourth element is a boolean that states whether all module options in the third elements are required (default:
$false) or only one ($true)
-
required_one_of: A list of lists, where the inner list contains module options where at least one must be set -
required_together: A list of lists, where the inner list contains module options that must be set together -
supports_check_mode: Whether the module supports check mode, by default this is$false
The actual input options for a module are set within the options value as a dictionary. The keys of this dictionary are the module option names while the values are the spec of that module option. Each spec can have the following options set:
-
aliases: A list of aliases for the module option -
choices: A list of valid values for the module option, iftype=listthen each list value is validated against the choices and not the list itself -
default: The default value for the module option if not set -
deprecated_aliases: A list of hashtables that define aliases that are deprecated and the versions they will be removed in. Each entry must contain the keysnameandcollection_namewith eitherversionordate -
elements: Whentype=list, this sets the type of each list value, the values are the same astype -
no_log: Hides the input value from Ansible output. Onansible-core2.22 and later the value is masked in the Event Log and in output, on earlier versions it is removed frommodule_invocation. Seedeveloping_secret_masking -
removed_in_version: States when a deprecated module option is to be removed, a warning is displayed to the end user if set -
removed_at_date: States the date (YYYY-MM-DD) when a deprecated module option will be removed, a warning is displayed to the end user if set -
removed_from_collection: States from which collection the deprecated module option will be removed; must be specified if one ofremoved_in_versionandremoved_at_dateis specified -
required: Will fail when the module option is not set -
type: The type of the module option, if not set then it defaults tostr. The valid types are;bool: A boolean valuedict: A dictionary value, if the input is a JSON or key=value string then it is converted to dictionaryfloat: A float or Single valueint: An Int32 valuejson: A string where the value is converted to a JSON string if the input is a dictionarylist: A list of values,elements=<type>can convert the individual list value types if set. Ifelements=dictthenoptionsis defined, the values will be validated against the argument spec. When the input is a string then the string is split by,and any whitespace is trimmedpath: A string where values likes%TEMP%are expanded based on environment values. If the input value starts with\\?\then no expansion is runraw: No conversions occur on the value passed in by Ansiblesid: Will convert Windows security identifier values or Windows account names to a SecurityIdentifier valuestr: The value is converted to a string
When type=dict, or type=list and elements=dict, the following keys can also be set for that module option:
apply_defaults: The value is based on theoptionsspec defaults for that key ifTrueand null ifFalse. Only valid when the module option is not defined by the user andtype=dict.mutually_exclusive: Same as the root levelmutually_exclusivebut validated against the values in the sub dictoptions: Same as the root leveloptionsbut contains the valid options for the sub optionrequired_if: Same as the root levelrequired_ifbut validated against the values in the sub dictrequired_by: Same as the root levelrequired_bybut validated against the values in the sub dictrequired_together: Same as the root levelrequired_togetherbut validated against the values in the sub dictrequired_one_of: Same as the root levelrequired_one_ofbut validated against the values in the sub dict
A module type can also be a delegate function that converts the value to whatever is required by the module option. For example the following snippet shows how to create a custom type that creates a UInt64 value:
$spec = @{
uint64_type = @{ type = [Func[[Object], [UInt64]]]{ [System.UInt64]::Parse($args[0]) } }
}
$uint64_type = $module.Params.uint64_type
When in doubt, look at some of the other core modules and see how things have been implemented there.
Sometimes there are multiple ways that Windows offers to complete a task; this is the order to favor when writing modules:
- Native Powershell cmdlets like
Remove-Item -Path C:\temp -Recurse - .NET classes like
[System.IO.Path]::GetRandomFileName() - WMI objects through the
New-CimInstancecmdlet - COM objects through
New-Object -ComObjectcmdlet - Calls to native executables like
Secedit.exe
PowerShell modules support a small subset of the #Requires options built into PowerShell as well as some Ansible-specific requirements specified by #AnsibleRequires. These statements can be placed at any point in the script, but are most commonly near the top. They are used to make it easier to state the requirements of the module without writing any of the checks. Each requires statement must be on its own line, but there can be multiple requires statements in one script.
These are the checks that can be used within Ansible modules:
#Requires -Module Ansible.ModuleUtils.<module_util>: Added in Ansible 2.4, specifies a module_util to load in for the module execution.#Requires -Version x.y: Added in Ansible 2.5, specifies the version of PowerShell that is required by the module. The module will fail if this requirement is not met.#AnsibleRequires -PowerShell <module_util>: Added in Ansible 2.8, like#Requires -Module, this specifies a module_util to load in for module execution.#AnsibleRequires -CSharpUtil <module_util>: Added in Ansible 2.8, specifies a C# module_util to load in for the module execution.#AnsibleRequires -OSVersion x.y: Added in Ansible 2.5, specifies the OS build version that is required by the module and will fail if this requirement is not met. The actual OS version is derived from[Environment]::OSVersion.Version.#AnsibleRequires -Become: Added in Ansible 2.5, forces the exec runner to run the module withbecome, which is primarily used to bypass WinRM restrictions. Ifansible_become_useris not specified then theSYSTEMaccount is used instead.
The #AnsibleRequires -PowerShell and #AnsibleRequires -CSharpUtil support further features such as:
- Importing a util contained in a collection (added in Ansible 2.9)
- Importing a util by relative names (added in Ansible 2.10)
- Specifying the util is optional by adding -Optional to the import declaration (added in Ansible 2.12).
See the below examples for more details:
# Imports the PowerShell Ansible.ModuleUtils.Legacy provided by Ansible itself
#AnsibleRequires -PowerShell Ansible.ModuleUtils.Legacy
# Imports the PowerShell my_util in the my_namesapce.my_name collection
#AnsibleRequires -PowerShell ansible_collections.my_namespace.my_name.plugins.module_utils.my_util
# Imports the PowerShell my_util that exists in the same collection as the current module
#AnsibleRequires -PowerShell ..module_utils.my_util
# Imports the PowerShell Ansible.ModuleUtils.Optional provided by Ansible if it exists.
# If it does not exist then it will do nothing.
#AnsibleRequires -PowerShell Ansible.ModuleUtils.Optional -Optional
# Imports the C# Ansible.Process provided by Ansible itself
#AnsibleRequires -CSharpUtil Ansible.Process
# Imports the C# my_util in the my_namespace.my_name collection
#AnsibleRequires -CSharpUtil ansible_collections.my_namespace.my_name.plugins.module_utils.my_util
# Imports the C# my_util that exists in the same collection as the current module
#AnsibleRequires -CSharpUtil ..module_utils.my_util
# Imports the C# Ansible.Optional provided by Ansible if it exists.
# If it does not exist then it will do nothing.
#AnsibleRequires -CSharpUtil Ansible.Optional -Optional
For optional require statements, it is up to the module code to then verify whether the util has been imported before trying to use it. This can be done by checking if a function or type provided by the util exists or not.
While both #Requires -Module and #AnsibleRequires -PowerShell can be used to load a PowerShell module it is recommended to use #AnsibleRequires. This is because #AnsibleRequires supports collection module utils, imports by relative util names, and optional util imports.
C# module utils can reference other C# utils by adding the line using Ansible.<module_util>; to the top of the script with all the other using statements.
Windows module utilities
Like Python modules, PowerShell modules also provide a number of module utilities that provide helper functions within PowerShell. These module_utils can be imported by adding the following line to a PowerShell module:
#Requires -Module Ansible.ModuleUtils.Legacy
This will import the module_util at ./lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 and enable calling all of its functions. As of Ansible 2.8, Windows module utils can also be written in C# and stored at lib/ansible/module_utils/csharp. These module_utils can be imported by adding the following line to a PowerShell module:
#AnsibleRequires -CSharpUtil Ansible.Basic
This will import the module_util at ./lib/ansible/module_utils/csharp/Ansible.Basic.cs and automatically load the types in the executing process. The Ansible.Secrets C# module util provides the [Ansible.Secrets.SecretMasker] class for registering secrets and masking strings, see developing_secret_masking. C# module utils can reference each other and be loaded together by adding the following line to the using statements at the top of the util:
using Ansible.Become;
There are special comments that can be set in a C# file for controlling the compilation parameters. The following comments can be added to the script;
//AssemblyReference -Name <assembly dll> [-CLR [Core|Framework]]: The assembly DLL to reference during compilation, the optional-CLRflag can also be used to state whether to reference when running under .NET Core, Framework, or both (if omitted)//NoWarn -Name <error id> [-CLR [Core|Framework]]: A compiler warning ID to ignore when compiling the code, the optional-CLRworks the same as above. A list of warnings can be found at Compiler errors
As well as this, the following pre-processor symbols are defined;
CORECLR: This symbol is present when PowerShell is running through .NET CoreWINDOWS: This symbol is present when PowerShell is running on WindowsUNIX: This symbol is present when PowerShell is running on Unix
A combination of these flags help to make a module util interoperable on both .NET Framework and .NET Core, here is an example of them in action:
#if CORECLR
using Newtonsoft.Json;
#else
using System.Web.Script.Serialization;
#endif
//AssemblyReference -Name Newtonsoft.Json.dll -CLR Core
//AssemblyReference -Name System.Web.Extensions.dll -CLR Framework
// Ignore error CS1702 for all .NET types
//NoWarn -Name CS1702
// Ignore error CS1956 only for .NET Framework
//NoWarn -Name CS1956 -CLR Framework
The following is a list of module_utils that are packaged with Ansible and a general description of what they do:
- ArgvParser: Utility used to convert a list of arguments to an escaped string compliant with the Windows argument parsing rules.
- CamelConversion: Utility used to convert camelCase strings/lists/dicts to snake_case.
- CommandUtil: Utility used to execute a Windows process and return the stdout/stderr and rc as separate objects.
- FileUtil: Utility that expands on the
Get-ChildItemandTest-Pathto work with special files likeC:\pagefile.sys. - Legacy: General definitions and helper utilities for Ansible module.
- LinkUtil: Utility to create, remove, and get information about symbolic links, junction points and hard inks.
- SID: Utilities used to convert a user or group to a Windows SID and vice versa.
For more details on any specific module utility and their requirements, please see the Ansible module utilities source code.
PowerShell module utilities can be stored outside of the standard Ansible distribution for use with custom modules. Custom module_utils are placed in a folder called module_utils located in the root folder of the playbook or role directory.
C# module utilities can also be stored outside of the standard Ansible distribution for use with custom modules. Like PowerShell utils, these are stored in a folder called module_utils and the file name must end in the extension .cs, start with Ansible. and be named after the namespace defined in the util.
The below example is a role structure that contains two PowerShell custom module_utils called Ansible.ModuleUtils.ModuleUtil1, Ansible.ModuleUtils.ModuleUtil2, and a C# util containing the namespace Ansible.CustomUtil:
meta/
main.yml
defaults/
main.yml
module_utils/
Ansible.ModuleUtils.ModuleUtil1.psm1
Ansible.ModuleUtils.ModuleUtil2.psm1
Ansible.CustomUtil.cs
tasks/
main.yml
Each PowerShell module_util must contain at least one function that has been exported with Export-ModuleMember at the end of the file. For example
Export-ModuleMember -Function Invoke-CustomUtil, Get-CustomInfo
Exposing shared module options
PowerShell module utils can easily expose common module options that a module can use when building its argument spec. This allows common features to be stored and maintained in one location and have those features used by multiple modules with minimal effort. Any new features or bugfixes added to one of these utils are then automatically used by the various modules that call that util.
An example of this would be to have a module util that handles authentication and communication against an API. This util can be used by multiple modules to expose a common set of module options like the API endpoint, username, password, timeout, cert validation, and so on without having to add those options to each module spec.
The standard convention for a module util that has a shared argument spec would have
-
A
Get-<namespace.name.util name>Specfunction that outputs the common spec for a module- It is highly recommended to make this function name be unique to the module to avoid any conflicts with other utils that can be loaded
- The format of the output spec is a Hashtable in the same format as the
$specused for normal modules
-
A function that takes in an
AnsibleModuleobject called under the-Moduleparameter which it can use to get the shared options
Because these options can be shared across various module it is highly recommended to keep the module option names and aliases in the shared spec as specific as they can be. For example do not have a util option called password, rather you should prefix it with a unique name like acme_password.
Failure to have a unique option name or alias can prevent the util being used by module that also use those names or aliases for its own options.
The following is an example module util called ServiceAuth.psm1 in a collection that implements a common way for modules to authentication with a service.
Invoke-MyServiceResource {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateScript({ $_.GetType().FullName -eq 'Ansible.Basic.AnsibleModule' })]
$Module,
[Parameter(Mandatory=$true)]
[String]
$ResourceId,
[String]
$State = 'present'
)
# Process the common module options known to the util
$params = @{
ServerUri = $Module.Params.my_service_url
}
if ($Module.Params.my_service_username) {
$params.Credential = Get-MyServiceCredential
}
if ($State -eq 'absent') {
Remove-MyService @params -ResourceId $ResourceId
} else {
New-MyService @params -ResourceId $ResourceId
}
}
Get-MyNamespaceMyCollectionServiceAuthSpec {
# Output the util spec
@{
options = @{
my_service_url = @{ type = 'str'; required = $true }
my_service_username = @{ type = 'str' }
my_service_password = @{ type = 'str'; no_log = $true }
}
required_together = @(
,@('my_service_username', 'my_service_password')
)
}
}
$exportMembers = @{
Function = 'Get-MyNamespaceMyCollectionServiceAuthSpec', 'Invoke-MyServiceResource'
}
Export-ModuleMember @exportMembers
For a module to take advantage of this common argument spec it can be set out like
#!powershell
# Include the module util ServiceAuth.psm1 from the my_namespace.my_collection collection
#AnsibleRequires -PowerShell ansible_collections.my_namespace.my_collection.plugins.module_utils.ServiceAuth
# Create the module spec like normal
$spec = @{
options = @{
resource_id = @{ type = 'str'; required = $true }
state = @{ type = 'str'; choices = 'absent', 'present' }
}
}
# Create the module from the module spec but also include the util spec to merge into our own.
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec, @(Get-MyNamespaceMyCollectionServiceAuthSpec))
# Call the ServiceAuth module util and pass in the module object so it can access the module options.
Invoke-MyServiceResource -Module $module -ResourceId $module.Params.resource_id -State $module.params.state
$module.ExitJson()
Options defined in the module spec will always have precedence over a util spec. Any list values under the same key in a util spec will be appended to the module spec for that same key. Dictionary values will add any keys that are missing from the module spec and merge any values that are lists or dictionaries. This is similar to how the doc fragment plugins work when extending module documentation.
To document these shared util options for a module, create a doc fragment plugin that documents the options implemented by the module util and extend the module docs for every module that implements the util to include that fragment in its docs.
Windows playbook module testing
You can test a module with an Ansible playbook. For example:
- Create a playbook in any directory
touch testmodule.yml. - Create an inventory file in the same directory
touch hosts. - Populate the inventory file with the variables required to connect to a Windows host(s).
- Add the following to the new playbook file:
---
- name: test out windows module
hosts: windows
tasks:
- name: test out module
win_module:
name: test name
- Run the playbook
ansible-playbook -i hosts testmodule.yml
This can be useful for seeing how Ansible runs with the new module end to end. Other possible ways to test the module are shown below.
Windows debugging
Debugging a module currently can only be done on a Windows host. This can be useful when developing a new module or implementing bug fixes. These are some steps that need to be followed to set this up:
- Copy the module script to the Windows server
- Copy the folders
./lib/ansible/module_utils/powershelland./lib/ansible/module_utils/csharpto the same directory as the script above - Add an extra
#to the start of any#Requires -Modulelines in the module code, this is only required for any lines starting with#Requires -Module - Add the following to the start of the module script that was copied to the server:
# Set $ErrorActionPreference to what's set during Ansible execution
$ErrorActionPreference = "Stop"
# Set the first argument as the path to a JSON file that contains the module args
$args = @("$($pwd.Path)\args.json")
# Or instead of an args file, set $complex_args to the pre-processed module args
$complex_args = @{
_ansible_check_mode = $false
_ansible_diff = $false
path = "C:\temp"
state = "present"
}
# Import any C# utils referenced with '#AnsibleRequires -CSharpUtil' or 'using Ansible.;
# The $_csharp_utils entries should be the context of the C# util files and not the path
Import-Module -Name "$($pwd.Path)\powershell\Ansible.ModuleUtils.AddType.psm1"
$_csharp_utils = @(
[System.IO.File]::ReadAllText("$($pwd.Path)\csharp\Ansible.Basic.cs")
)
Add-CSharpType -References $_csharp_utils -IncludeDebugInfo
# Import any PowerShell modules referenced with '#Requires -Module`
Import-Module -Name "$($pwd.Path)\powershell\Ansible.ModuleUtils.Legacy.psm1"
# End of the setup code and start of the module code
#!powershell
You can add more args to $complex_args as required by the module or define the module options through a JSON file with the structure:
{
"ANSIBLE_MODULE_ARGS": {
"_ansible_check_mode": false,
"_ansible_diff": false,
"path": "C:\\temp",
"state": "present"
}
}
There are multiple IDEs that can be used to debug a Powershell script, two of the most popular ones are
To be able to view the arguments as passed by Ansible to the module follow these steps.
- Prefix the Ansible command with
ANSIBLE_KEEP_REMOTE_FILES=1<ANSIBLE_KEEP_REMOTE_FILES>to specify that Ansible should keep the exec files on the server. - Log onto the Windows server using the same user account that Ansible used to execute the module.
- Navigate to
%TEMP%\... It should contain a folder starting withansible-tmp-. - Inside this folder, open the PowerShell script for the module.
- In this script is a raw JSON script under
$json_rawwhich contains the module arguments undermodule_args. These args can be assigned manually to the$complex_argsvariable that is defined on your debug script or put in theargs.jsonfile.
Windows unit testing
Currently there is no mechanism to run unit tests for Powershell modules under Ansible CI.
Windows integration testing
Integration tests for Ansible modules are typically written as Ansible roles. These test roles are located in ./test/integration/targets. You must first set up your testing environment, and configure a test inventory for Ansible to connect to.
In this example we will set up a test inventory to connect to two hosts and run the integration tests for win_stat:
- Run the command
source ./hacking/env-setupto prepare your environment. - Create a copy of
./test/integration/inventory.winrm.templateand name itinventory.winrm. - Fill in entries under
[windows]and set the required variables that are needed to connect to the host. Install the required Python modules <windows_winrm>to support WinRM and a configured authentication method.- To execute the integration tests, run
ansible-test windows-integration win_stat; you can replacewin_statwith the role you want to test.
This will execute all the tests currently defined for that role. You can set the verbosity level using the -v argument just as you would with ansible-playbook.
When developing tests for a new module, it is recommended to test a scenario once in check mode and twice not in check mode. This ensures that check mode does not make any changes but reports a change, as well as that the second run is idempotent and does not report changes. For example:
- name: remove a file (check mode)
win_file:
path: C:\temp
state: absent
register: remove_file_check
check_mode: true
- name: get result of remove a file (check mode)
win_command: powershell.exe "if (Test-Path -Path 'C:\temp') { 'true' } else { 'false' }"
register: remove_file_actual_check
- name: assert remove a file (check mode)
assert:
that:
- remove_file_check is changed
- remove_file_actual_check.stdout == 'true\r\n'
- name: remove a file
win_file:
path: C:\temp
state: absent
register: remove_file
- name: get result of remove a file
win_command: powershell.exe "if (Test-Path -Path 'C:\temp') { 'true' } else { 'false' }"
register: remove_file_actual
- name: assert remove a file
assert:
that:
- remove_file is changed
- remove_file_actual.stdout == 'false\r\n'
- name: remove a file (idempotent)
win_file:
path: C:\temp
state: absent
register: remove_file_again
- name: assert remove a file (idempotent)
assert:
that:
- not remove_file_again is changed
Windows communication and development support
Join the Ansible Forum<ansible_forum> and use the windows tag for discussions about Ansible development for Windows.
Creating a new collection
Starting with Ansible 2.10, related modules should be developed in a collection. The Ansible Core team and community compiled these module development tips and tricks to help companies developing Ansible modules for their products and users developing Ansible modules for third-party products. See developing_collections for a more detailed description of the collections format and additional development guidelines.
LICENSING REQUIREMENTS Ansible enforces the following licensing requirements:
-
Utilities (files in
lib/ansible/module_utils/) may have one of two licenses:- A file in
module_utilsused only for a specific vendor's hardware, provider, or service may be licensed under GPLv3+. Adding a new file undermodule_utilswith GPLv3+ needs to be approved by the core team. - All other
module_utilsmust be licensed under BSD, so GPL-licensed third-party and Galaxy modules can use them. - If there's doubt about the appropriate license for a file in
module_utils, the Ansible Core Team will decide during an Ansible Core Community Meeting.
- A file in
-
All other files shipped with Ansible, including all modules, must be licensed under the GPL license (GPLv3 or later).
-
Existing license requirements still apply to content in ansible/ansible (ansible-core).
-
Content that was previously in ansible/ansible or a collection and has moved to a new collection must retain the license it had in its prior repository.
-
Copyright entries by previous committers must also be kept in any moved files.
Before you start coding
This list of prerequisites is designed to help ensure that you develop high-quality modules that work well with ansible-core and provide a seamless user experience.
- Read though all the pages linked off
developing_modules_general; paying particular focus to thedeveloping_modules_checklist. - We encourage PEP 8 compliance. See
testing_pep8for more information. - We encourage supporting
Python 2.6+ and Python 3.5+ <developing_python_3>. - Look at Ansible Galaxy and review the naming conventions in your functional area (such as cloud, networking, databases).
- With great power comes great responsibility: Ansible collection maintainers have a duty to help keep content up to date and release collections they are responsible for regularly. As with all successful community projects, collection maintainers should keep a watchful eye for reported issues and contributions.
- We strongly recommend unit and/or integration tests. Unit tests are especially valuable when external resources (such as cloud or network devices) are required. For more information see
developing_testing.
Naming conventions
Fully Qualified Collection Names (FQCNs) for plugins and modules include three elements:
the Galaxy namespace, which generally represents the company or group
the collection name, which generally represents the product or OS
the plugin or module name
- always in lower case
- words separated with an underscore (
_) character- singular, rather than plural, for example,
commandnotcommands
For example, community.mongodb.mongodb_linux or cisco.meraki.meraki_device.
It is convenient if the organization and repository names on GitHub (or elsewhere) match your namespace and collection names on Ansible Galaxy, but it is not required. The plugin names you select, however, are always the same in your code repository and in your collection artifact on Galaxy.
Speak to us
Circulating your ideas before coding helps you adopt good practices and avoid common mistakes. After reading the "Before you start coding" section you should have a reasonable idea of the structure of your modules. Write a list of your proposed plugin and/or module names, with a short description of what each one does. Circulate that list on the Ansible Forum<ansible_forum> so the Ansible community can review your ideas for consistency and familiarity. Names and functionality that are consistent, predictable, and familiar make your collection easier to use.
Where to get support
Ansible has a thriving and knowledgeable community of module developers that is a great resource for getting your questions answered. Visit the Ansible communication guide<communication> for details.
Required files
Your collection should include the following files to be usable:
- an
__init__.pyfile - An empty file to initialize namespace and allow Python to import the files. Required - at least one plugin, for example,
/plugins/modules/$your_first_module.py. Required - if needed, one or more
/plugins/doc_fragments/$topic.pyfiles - Code documentation, such as details regarding common arguments. Optional - if needed, one or more
/plugins/module_utils/$topic.pyfiles - Code shared between more than one module, such as common arguments. Optional
When you have these files ready, review the developing_modules_checklist again. If you are creating a new collection, you are responsible for all procedures related to your repository, including setting rules for contributions, finding reviewers, and testing and maintaining the code in your collection.
New to Git or GitHub
We realize this may be your first use of Git or GitHub. The following guides may be of use:
- How to create a fork of ansible/ansible
- How to sync (update) your fork
- How to create a Pull Request (PR)
Developing plugins<span id="developing_plugins"></span>
Plugins augment Ansible's core functionality with logic and features that are accessible to all modules. Ansible collections include a number of handy plugins, and you can easily write your own. All plugins must:
- be written in Python
- raise errors
- return strings in unicode
- conform to Ansible's configuration and documentation standards
Once you've reviewed these general guidelines, you can skip to the particular type of plugin you want to develop.
Writing plugins in Python
You must write your plugin in Python so it can be loaded by the PluginLoader and returned as a Python object that any module can use. Since your plugin will execute on the control node, you must write it in a compatible version of Python <control_node_requirements>.
Raising errors
You should return errors encountered during plugin execution by raising AnsibleError() or a similar class with a message describing the error. When wrapping other exceptions into error messages, you should always use the to_native Ansible function to ensure proper string compatibility across Python versions:
from ansible.module_utils.common.text.converters import to_native
try:
cause_an_exception()
except Exception as e:
raise AnsibleError('Something happened, this was original exception: %s' % to_native(e))
Since Ansible evaluates variables only when they are needed, filter and test plugins should propagate the exceptions jinja2.exceptions.UndefinedError and AnsibleUndefinedVariable to ensure undefined variables are only fatal when necessary.
Check the different AnsibleError objects and see which one applies best to your situation. Check the section on the specific plugin type you're developing for type-specific error handling details.
String encoding
You must convert any strings returned by your plugin into Python's unicode type. Converting to unicode ensures that these strings can run through Jinja2. To convert strings:
from ansible.module_utils.common.text.converters import to_text
result_string = to_text(result_string)
Plugin configuration & documentation standards
To define configurable options for your plugin, describe them in the DOCUMENTATION section of the python file. Callback and connection plugins have declared configuration requirements this way since Ansible version 2.4; most plugin types now do the same. This approach ensures that the documentation of your plugin's options will always be correct and up-to-date. To add a configurable option to your plugin, define it in this format:
options:
option_name:
description: describe this config option
default: default value for this config option
env:
- name: MYCOLLECTION_NAME_ENV_VAR_NAME
ini:
- section: mycollection_section_of_ansible.cfg_where_this_config_option_is_defined
key: key_used_in_ansible.cfg
vars:
- name: mycollection_name_of_ansible_var
- name: mycollection_name_of_second_var
version_added: X.x
required: True/False
secret: True/False
type: boolean/float/integer/list/none/path/pathlist/pathspec/string/tmppath
version_added: X.x
The supported configuration fields are:
env
List of environment variables that can be used to set this option. Each entry includes a name field specifying the environment variable name. The name should be in uppercase and should be prefixed with the collection name. Multiple environment variables can be listed for the same option. The last set environment variable in the list takes precedence if multiple are set. This is commonly used for plugins (especially inventory plugins) to allow configuration through environment variables. Examples: VMWARE_PORT, GRAFANA_PASSWORD
ini
List of configuration file settings that can be used to set this option. Each entry includes a section field for the configuration file section and a key field for the configuration key. Both should be in lowercase and should be prefixed with the collection name. Multiple configuration settings can be listed for the same option. The last set configuration setting in the list takes precedence if multiple are set. This allows plugins to be configured with ansible.cfg. Example: grafana_password
vars
List of Ansible variables that can be used to set this option. Each entry includes a name field specifying the variable name. The name should be in lowercase and should be prefixed with the collection name. Multiple variables can be listed for the same option. The last set variable in the list takes precedence if multiple are set. Variables follow Ansible's variable precedence rules. This allows plugins to be configured with Ansible variables. Example: ansible_vmware_port
secret
Boolean that marks the option as sensitive. When set to true, the resolved value is registered as a secret and is masked in Ansible output regardless of the source that set it. Only supported for options of type str, string, and list (string elements), and not supported on suboptions. Use it for passwords, tokens, private keys, and similar values. See plugin_config_secret for details.
2.22
General precedence rules
The precedence rules for configuration sources are listed below, starting with the highest precedence values:
- Keywords
- CLI settings
- Environment variables (
env) - Values defined in
ansible.cfg - Default value for the option, if present.
Accessing configuration settings
To access the configuration settings in your plugin, use self.get_option(<option_name>). Some plugin types handle this differently:
- Become, callback, connection and shell plugins are guaranteed to have the engine call
set_options(). - Lookup plugins always require you to handle it in the
run()method. - Inventory plugins are done automatically if you use the
base _read_config_file()method. If not, you must useself.get_option(<option_name>). - Cache plugins do it on load.
- Cliconf, httpapi and netconf plugins indirectly piggy back on connection plugins.
- Vars plugin settings are populated when first accessed (using the
self.get_option()orself.get_options()method.
If you need to populate settings explicitly, use a self.set_options() call.
Configuration sources follow the precedence rules for values in Ansible. When there are multiple values from the same category, the value defined last takes precedence. For example, in the above configuration block, if both name_of_ansible_var and name_of_second_var are defined, the value of the option_name option will be the value of name_of_second_var. Refer to general_precedence_rules for further information.
Plugins that support embedded documentation (see ansible-doc for the list) should include well-formed doc strings. If you inherit from a plugin, you must document the options it takes, either through a documentation fragment or as a copy. See module_documenting for more information on correct documentation. Thorough documentation is a good idea even if you're developing a plugin for local use.
In ansible-core 2.14 we added support for documenting filter and test plugins. You have two options for providing documentation:
- Define a Python file that includes inline documentation for each plugin.
- Define a Python file for multiple plugins and create adjacent documentation files in YAML format.
Developing particular plugin types
Action plugins
Action plugins let you integrate local processing and local data with module functionality.
To create an action plugin, create a new class with the Base(ActionBase) class as the parent:
from ansible.plugins.action import ActionBase
class ActionModule(ActionBase):
pass
From there, execute the module using the _execute_module method to call the original module. After successful execution of the module, you can modify the module return data.
module_return = self._execute_module(module_name='<NAME_OF_MODULE>',
module_args=module_args,
task_vars=task_vars, tmp=tmp)
For example, if you wanted to check the time difference between your Ansible control node and your target machine(s), you could write an action plugin to check the local time and compare it to the return data from Ansible's setup module:
#!/usr/bin/python
# Make coding more python3-ish, this is required for contributions to Ansible
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.plugins.action import ActionBase
from datetime import datetime
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
super(ActionModule, self).run(tmp, task_vars)
module_args = self._task.args.copy()
module_return = self._execute_module(module_name='setup',
module_args=module_args,
task_vars=task_vars, tmp=tmp)
ret = dict()
remote_date = None
if not module_return.get('failed'):
for key, value in module_return['ansible_facts'].items():
if key == 'ansible_date_time':
remote_date = value['iso8601']
if remote_date:
remote_date_obj = datetime.strptime(remote_date, '%Y-%m-%dT%H:%M:%SZ')
time_delta = datetime.utcnow() - remote_date_obj
ret['delta_seconds'] = time_delta.seconds
ret['delta_days'] = time_delta.days
ret['delta_microseconds'] = time_delta.microseconds
return dict(ansible_facts=dict(ret))
This code checks the time on the control node, captures the date and time for the remote machine using the setup module, and calculates the difference between the captured time and the local time, returning the time delta in days, seconds and microseconds.
For practical examples of action plugins, see the source code for the action plugins included with Ansible Core
Cache plugins
Cache plugins store gathered facts and data retrieved by inventory plugins.
Import cache plugins using the cache_loader so you can use self.set_options() and self.get_option(<option_name>). If you import a cache plugin directly in the code base, you can only access options by the ansible.constants, and you break the cache plugin's ability to be used by an inventory plugin.
from ansible.plugins.loader import cache_loader
[...]
plugin = cache_loader.get('custom_cache', **cache_kwargs)
There are two base classes for cache plugins, BaseCacheModule for database-backed caches, and BaseCacheFileModule for file-backed caches.
To create a cache plugin, start by creating a new CacheModule class with the appropriate base class. If you're creating a plugin using an __init__ method you should initialize the base class with any provided args and kwargs to be compatible with inventory plugin cache options. The base class calls self.set_options(direct=kwargs). After the base class __init__ method is called self.get_option(<option_name>) should be used to access cache options.
New cache plugins should take the options _uri, _prefix, and _timeout to be consistent with existing cache plugins.
from ansible.plugins.cache import BaseCacheModule
class CacheModule(BaseCacheModule):
def __init__(self, *args, **kwargs):
super(CacheModule, self).__init__(*args, **kwargs)
self._connection = self.get_option('_uri')
self._prefix = self.get_option('_prefix')
self._timeout = self.get_option('_timeout')
If you use the BaseCacheModule, you must implement the methods get, contains, keys, set, delete, flush, and copy. The contains method should return a boolean that indicates if the key exists and has not expired. Unlike file-based caches, the get method does not raise a KeyError if the cache has expired.
If you use the BaseFileCacheModule, you must implement _load and _dump methods that will be called from the base class methods get and set.
If your cache plugin stores JSON, use AnsibleJSONEncoder in the _dump or set method and AnsibleJSONDecoder in the _load or get method.
For example cache plugins, see the source code for the cache plugins included with Ansible Core.
Note that cache plugin implementation is an internal detail, and should not be relied upon by external uses such as interrogation or consumption in a playbook.
It is assumed that a cache produced at any point in time, is usable at any future point in time, as the underlying implementation, or information provided within may change.
If the planned use case of a cache is external interrogation or consumption, we recommend to be explicit about the fetching and storing of that data, such as creating a playbook that gathers facts and stores them in the format you need them in, and then stores that data explicitly outside of the concept of a cache, where it can be relied upon.
Callback plugins
Callback plugins add new behaviors to Ansible when responding to events. By default, callback plugins control most of the output you see when running the command line programs.
To create a callback plugin, create a new class with the Base(Callbacks) class as the parent:
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
pass
From there, override the specific methods from the CallbackBase that you want to provide a callback for. For plugins intended for use with Ansible version 2.0 and later, you should only override methods that start with v2. For a complete list of methods that you can override, please see __init__.py in the lib/ansible/plugins/callback directory.
The following is a modified example of how Ansible's timer plugin is implemented, but with an extra option so you can see how configuration works in Ansible version 2.4 and later:
# Make coding more python3-ish, this is required for contributions to Ansible
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
# not only visible to ansible-doc, it also 'declares' the options the plugin requires and how to configure them.
DOCUMENTATION = '''
name: timer
callback_type: aggregate
requirements:
- enable in configuration
short_description: Adds time to play stats
version_added: "2.0" # for collections, use the collection version, not the Ansible version
description:
- This callback just adds total play duration to the play stats.
options:
format_string:
description: format of the string shown to user at play end
ini:
- section: callback_timer
key: format_string
env:
- name: ANSIBLE_CALLBACK_TIMER_FORMAT
default: "Playbook run took %s days, %s hours, %s minutes, %s seconds"
'''
from datetime import datetime
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
"""
This callback module tells you how long your plays ran for.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'namespace.collection_name.timer'
# only needed if you ship it and don't want to enable by default
CALLBACK_NEEDS_ENABLED = True
def __init__(self):
# make sure the expected objects are present, calling the base's __init__
super(CallbackModule, self).__init__()
# start the timer when the plugin is loaded, the first play should start a few milliseconds after.
self.start_time = datetime.now()
def _days_hours_minutes_seconds(self, runtime):
''' internal helper method for this callback '''
minutes = (runtime.seconds // 60) % 60
r_seconds = runtime.seconds - (minutes * 60)
return runtime.days, runtime.seconds // 3600, minutes, r_seconds
# this is only event we care about for display, when the play shows its summary stats; the rest are ignored by the base class
def v2_playbook_on_stats(self, stats):
end_time = datetime.now()
runtime = end_time - self.start_time
# Shows the usage of a config option declared in the DOCUMENTATION variable. Ansible will have set it when it loads the plugin.
# Also note the use of the display object to print to screen. This is available to all callbacks, and you should use this over printing yourself
self._display.display(self._plugin_options['format_string'] % (self._days_hours_minutes_seconds(runtime)))
Note that the CALLBACK_VERSION and CALLBACK_NAME definitions are required for properly functioning plugins for Ansible version 2.0 and later. CALLBACK_TYPE is mostly needed to distinguish 'stdout' plugins from the rest, since you can only load one plugin that writes to stdout.
New in ansible-core 2.22, the task result a callback plugin receives has registered secrets already masked, and anything a callback writes through Display() is masked as well. A callback that writes other data, such as task names, to a file or an external service should pass it through mask_secrets() first. See developing_callbacks_masking for the details and an example.
For example callback plugins, see the source code for the callback plugins included with Ansible Core
New in ansible-core 2.11, callback plugins are notified (by the v2_playbook_on_task_start) of meta<meta_module> tasks. By default, only explicit meta tasks that users list in their plays are sent to callbacks.
There are also some tasks which are generated internally and implicitly at various points in execution. Callback plugins can opt-in to receiving these implicit tasks as well, by setting self.wants_implicit_tasks = True. Any Task object received by a callback hook will have an .implicit attribute, which can be consulted to determine whether the Task originated from within Ansible, or explicitly by the user.
Connection plugins
Connection plugins allow Ansible to connect to target hosts so it can execute tasks on them. Ansible ships with many connection plugins, but only one can be used per host at a time. The most commonly used connection plugins are native ssh, paramiko, and local. All of these can be used with ad-hoc tasks and in playbooks.
To create a new connection plugin (for example, to support SNMP, Message bus, or other transports), copy the format of one of the existing connection plugins and drop it into connection directory on your local plugin path <local_plugins>.
Connection plugins can support common options (such as the --timeout flag) by defining an entry in the documentation for the attribute name (in this case timeout). If the common option has a non-null default, the plugin should define the same default since a different default would be ignored.
For example connection plugins, see the source code for the connection plugins included with Ansible Core.
Filter plugins
Filter plugins manipulate data. They are a feature of Jinja2 and are also available in Jinja2 templates used by the template module. As with all plugins, they can be easily extended, but instead of having a file for each one you can have several per file. Most of the filter plugins shipped with Ansible reside in a core.py.
Filter plugins do not use the standard configuration system described above, but since ansible-core 2.14 can use it as plain documentation.
Since Ansible evaluates variables only when they are needed, filter plugins should propagate the exceptions jinja2.exceptions.UndefinedError and AnsibleUndefinedVariable to ensure undefined variables are only fatal when necessary.
try:
cause_an_exception(with_undefined_variable)
except jinja2.exceptions.UndefinedError as e:
raise AnsibleUndefinedVariable("Something happened, this was the original exception: %s" % to_native(e))
except Exception as e:
raise AnsibleFilterError("Something happened, this was the original exception: %s" % to_native(e))
For example filter plugins, see the source code for the filter plugins included with Ansible Core.
Inventory plugins
Inventory plugins parse inventory sources and form an in-memory representation of the inventory. Inventory plugins were added in Ansible version 2.4.
You can see the details for inventory plugins in the developing_inventory page.
Lookup plugins
Lookup plugins pull in data from external data stores. Lookup plugins can be used within playbooks both for looping --- playbook language constructs like with_fileglob and with_items are implemented through lookup plugins --- and to return values into a variable or parameter.
Lookup plugins are expected to return lists, even if just a single element.
Ansible includes many filters <playbooks_filters> which can be used to manipulate the data returned by a lookup plugin. Sometimes it makes sense to do the filtering inside the lookup plugin, other times it is better to return results that can be filtered in the playbook. Keep in mind how the data will be referenced when determining the appropriate level of filtering to be done inside the lookup plugin.
Here's a simple lookup plugin implementation --- this lookup returns the contents of a text file as a variable:
# python 3 headers, required if submitting to Ansible
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r"""
name: file
author: Daniel Hokka Zakrisson (@dhozac) <daniel@hozac.com>
version_added: "0.9" # for collections, use the collection version, not the Ansible version
short_description: read file contents
description:
- This lookup returns the contents from a file on the Ansible control node's file system.
options:
_terms:
description: path(s) of files to read
required: True
option1:
description:
- Sample option that could modify plugin behavior.
- This one can be set directly ``option1='x'`` or in ansible.cfg, but can also use vars or environment.
type: string
ini:
- section: file_lookup
key: option1
notes:
- if read in variable context, the file can be interpreted as YAML if the content is valid to the parser.
- this lookup does not understand globbing --- use the fileglob lookup instead.
"""
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.plugins.lookup import LookupBase
from ansible.utils.display import Display
display = Display()
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
# First of all populate options,
# this will already take into account env vars and ini config
self.set_options(var_options=variables, direct=kwargs)
# lookups in general are expected to both take a list as input and output a list
# this is done so they work with the looping construct 'with_'.
ret = []
for term in terms:
display.debug("File lookup term: %s" % term)
# Find the file in the expected search path, using a class method
# that implements the 'expected' search path for Ansible plugins.
lookupfile = self.find_file_in_search_path(variables, 'files', term)
# Don't use print or your own logging, the display class
# takes care of it in a unified way.
display.vvvv(u"File lookup using %s as file" % lookupfile)
try:
if lookupfile:
contents, show_data = self._loader._get_file_contents(lookupfile)
ret.append(contents.rstrip())
else:
# Always use ansible error classes to throw 'final' exceptions,
# so the Ansible engine will know how to deal with them.
# The Parser error indicates invalid options passed
raise AnsibleParserError()
except AnsibleParserError:
raise AnsibleError("could not locate file in lookup: %s" % term)
# consume an option: if this did something useful, you can retrieve the option value here
if self.get_option('option1') == 'do something':
pass
return ret
The following is an example of how this lookup is called:
---
- hosts: all
vars:
contents: "{{ lookup('namespace.collection_name.file', '/etc/foo.txt') }}"
contents_with_option: "{{ lookup('namespace.collection_name.file', '/etc/foo.txt', option1='donothing') }}"
tasks:
- debug:
msg: the value of foo.txt is {{ contents }} as seen today {{ lookup('pipe', 'date +"%Y-%m-%d"') }}
For example lookup plugins, see the source code for the lookup plugins included with Ansible Core.
For more usage examples of lookup plugins, see Using Lookups<playbooks_lookups>.
Test plugins
Test plugins verify data. They are a feature of Jinja2 and are also available in Jinja2 templates used by the template module. As with all plugins, they can be easily extended, but instead of having a file for each one you can have several per file. Most of the test plugins shipped with Ansible reside in a core.py. These are specially useful in conjunction with some filter plugins like map and select; they are also available for conditional directives like when:.
Test plugins do not use the standard configuration system described above. Since ansible-core 2.14 test plugins can use plain documentation.
Since Ansible evaluates variables only when they are needed, test plugins should propagate the exceptions jinja2.exceptions.UndefinedError and AnsibleUndefinedVariable to ensure undefined variables are only fatal when necessary.
try:
cause_an_exception(with_undefined_variable)
except jinja2.exceptions.UndefinedError as e:
raise AnsibleUndefinedVariable("Something happened, this was the original exception: %s" % to_native(e))
except Exception as e:
raise AnsibleFilterError("Something happened, this was the original exception: %s" % to_native(e))
For example test plugins, see the source code for the test plugins included with Ansible Core.
Vars plugins
Vars plugins inject additional variable data into Ansible runs that did not come from an inventory source, playbook, or command line. Playbook constructs like 'host_vars' and 'group_vars' work using vars plugins.
Vars plugins were partially implemented in Ansible 2.0 and rewritten to be fully implemented starting with Ansible 2.4. Vars plugins are supported by collections starting with Ansible 2.10.
Older plugins used a run method as their main body/work:
def run(self, name, vault_password=None):
pass # your code goes here
Ansible 2.0 did not pass passwords to older plugins, so vaults were unavailable. Most of the work now happens in the get_vars method which is called from the VariableManager when needed.
def get_vars(self, loader, path, entities):
pass # your code goes here
The parameters are:
- loader: Ansible's DataLoader. The DataLoader can read files, auto-load JSON/YAML and decrypt vaulted data, and cache read files.
- path: this is 'directory data' for every inventory source and the current play's playbook directory, so they can search for data in reference to them.
get_varswill be called at least once per available path.- entities: these are host or group names that are pertinent to the variables needed. The plugin will get called once for hosts and again for groups.
This get_vars method just needs to return a dictionary structure with the variables.
Since Ansible version 2.4, vars plugins only execute as needed when preparing to execute a task. This avoids the costly 'always execute' behavior that occurred during inventory construction in older versions of Ansible. Since Ansible version 2.10, vars plugin execution can be toggled by the user to run when preparing to execute a task or after importing an inventory source.
The user must explicitly enable vars plugins that reside in a collection. See enable_vars for details.
Legacy vars plugins are always loaded and run by default. You can prevent them from automatically running by setting REQUIRES_ENABLED to True.
class VarsModule(BaseVarsPlugin):
REQUIRES_ENABLED = True
Include the vars_plugin_staging documentation fragment to allow users to determine when vars plugins run.
DOCUMENTATION = '''
name: custom_hostvars
version_added: "2.10" # for collections, use the collection version, not the Ansible version
short_description: Load custom host vars
description: Load custom host vars
options:
stage:
ini:
- key: stage
section: vars_custom_hostvars
env:
- name: ANSIBLE_VARS_PLUGIN_STAGE
extends_documentation_fragment:
- vars_plugin_staging
'''
At times a value provided by a vars plugin will contain unsafe values. The utility function wrap_var provided by ansible.utils.unsafe_proxy should be used to ensure that Ansible handles the variable and value correctly. The use cases for unsafe data is covered in unsafe_strings.
from ansible.plugins.vars import BaseVarsPlugin
from ansible.utils.unsafe_proxy import wrap_var
class VarsPlugin(BaseVarsPlugin):
def get_vars(self, loader, path, entities):
return dict(
something_unsafe=wrap_var("{{ SOMETHING_UNSAFE }}")
)
For example vars plugins, see the source code for the vars plugins included with Ansible Core.
list_of_collections
Browse existing collections, modules, and plugins
developing_api
Learn about the Python API for task execution
developing_inventory
Learn about how to develop dynamic inventory sources
developing_modules_general
Learn about how to write Ansible modules
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
adjacent_yaml_doc
Alternate YAML files as documentation
Ansible module architecture<span id="flow_modules"></span>
If you are working on the ansible-core code, writing an Ansible module, or developing an action plugin, you may need to understand how Ansible's program flow executes. If you are just using Ansible Modules in playbooks, you can skip this section.
Types of modules
Ansible supports several different types of modules in its code base. Some of these are for backwards compatibility and others are to enable flexibility.
Action plugins
Action plugins look like modules to anyone writing a playbook. Usage documentation for most action plugins lives inside a module of the same name. Some action plugins do all the work, with the module providing only documentation. Some action plugins execute modules. The normal action plugin executes modules that don't have special action plugins. Action plugins always execute on the control node.
Some action plugins do all their work on the control node. For example, the debug <debug_module> action plugin (which prints text for the user to see) and the assert <assert_module> action plugin (which tests whether values in a playbook satisfy certain criteria) execute entirely on the control node.
Most action plugins set up some values on the control node, then invoke an actual module on the managed node that does something with these values. For example, the template <template_module> action plugin takes values from the user to construct a file in a temporary location on the control node using variables from the playbook environment. It then transfers the temporary file to a temporary file on the remote system. After that, it invokes the copy module <copy_module> which operates on the remote system to move the file into its final location, sets file permissions, and so on.
New-style modules
All of the modules that ship with Ansible fall into this category. While you can write modules in any language, all official modules (shipped with Ansible) use either Python or PowerShell.
New-style modules have the arguments to the module embedded inside of them in some manner. Old-style modules must copy a separate file over to the managed node, which is less efficient as it requires two over-the-wire connections instead of only one.
Python
New-style Python modules use the Ansiballz framework for constructing modules. These modules use imports from ansible.module_utils to pull in boilerplate module code, such as argument parsing, formatting of return values as JSON, and various file operations.
In Ansible, up to version 2.0.x, the official Python modules used the module_replacer framework. For module authors, Ansiballz is largely a superset of module_replacer functionality, so you usually do not need to understand the differences between them.
PowerShell
New-style PowerShell modules use the module_replacer framework for constructing modules. These modules get a library of PowerShell code embedded in them before being sent to the managed node.
JSONARGS modules
These modules are scripts that include the string <<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>> in their body. This string is replaced with the JSON-formatted argument string. These modules typically set a variable to that value like this:
json_arguments = """<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>"""
Which is expanded as:
json_arguments = """{"param1": "test's quotes", "param2": "\"To be or not to be\" - Hamlet"}"""
Ansible outputs a JSON string with bare quotes. Double quotes are used to quote string values, double quotes inside of string values are backslash escaped, and single quotes may appear unescaped inside of a string value. To use JSONARGS, your scripting language must have a way to handle this type of string. The example uses Python's triple quoted strings to do this. Other scripting languages may have a similar quote character that won't be confused by any quotes in the JSON or it may allow you to define your own start-of-quote and end-of-quote characters. If the language doesn't give you any of these then you'll need to write a non-native JSON module <flow_want_json_modules> or Old-style module <flow_old_style_modules> instead.
These modules typically parse the contents of json_arguments using a JSON library and then use them as native variables throughout the code.
Non-native want JSON modules
If a module has the string WANT_JSON in it anywhere, Ansible treats it as a non-native module that accepts a file name as its only command-line parameter. The file name is for a temporary file containing a JSON string containing the module's parameters. The module needs to open the file, read and parse the parameters, operate on the data, and print its return data as a JSON encoded dictionary to stdout before exiting.
These types of modules are self-contained entities. As of Ansible 2.1, Ansible only modifies them to change a shebang line if present.
Examples of Non-native modules written in ruby are in the Ansible for Rubyists repository.
Binary modules
From Ansible 2.2 onwards, modules may also be small binary programs. Ansible doesn't perform any magic to make these portable to different systems so they may be specific to the system on which they were compiled or require other binary runtime dependencies. Despite these drawbacks, you may have to compile a custom module against a specific binary library if that's the only way to get access to certain resources.
Binary modules take their arguments and return data to Ansible in the same way as want JSON modules <flow_want_json_modules>.
One example of a binary module written in go.
Old-style modules
Old-style modules are similar to want JSON modules <flow_want_json_modules>, except that the file that they take contains key=value pairs for their parameters instead of JSON. Ansible decides that a module is old-style when it doesn't have any of the markers that would show that it is one of the other types.
How modules are executed
When a user uses ansible or ansible-playbook, they specify a task to execute. The task is usually the name of a module along with several parameters to be passed to the module. Ansible takes these values and processes them in various ways before they are finally executed on the remote machine.
Executor/task_executor
The TaskExecutor receives the module name and parameters that were parsed from the playbook <playbooks> (or from the command-line in the case of /usr/bin/ansible). It uses the name to decide whether it is looking at a module or an Action Plugin <flow_action_plugins>. If it is a module, it loads the Normal Action Plugin <flow_normal_action_plugin> and passes the name, variables, and other information about the task and play to that Action Plugin for further processing.
The normal action plugin
The normal action plugin executes the module on the remote host. It is the primary coordinator of much of the work to actually execute the module on the managed machine.
- It loads the appropriate connection plugin for the task, which then transfers or executes as needed to create a connection to that host.
- It adds any internal Ansible properties to the module's parameters (for instance, the ones that pass along
no_logto the module). - It works with other plugins (connection, shell, become, other action plugins) to create any temporary files on the remote machine and cleans up afterwards.
- It pushes the module and module parameters to the remote host, although the
module_common <flow_executor_module_common>code described in the next section decides which format those will take. - It handles any special cases regarding modules (for example, async execution, or complications around Windows modules that must have the same names as Python modules, so that internal calling of modules from other Action Plugins work.)
Much of this functionality comes from the BaseAction class, which lives in plugins/action/__init__.py. It uses the Connection and Shell objects to do its work.
When tasks <tasks> are run with the async: parameter, Ansible uses the async Action Plugin instead of the normal Action Plugin to invoke it. That program flow is currently not documented. Read the source for information on how that works.
Executor/module_common.py
Code in executor/module_common.py assembles the module to be shipped to the managed node. The module is first read in, then examined to determine its type:
PowerShell <flow_powershell_modules>andJSON-args modules <flow_jsonargs_modules>are passed throughModule Replacer <module_replacer>.- New-style
Python modules <flow_python_modules>are assembled byAnsiballz. Non-native-want-JSON <flow_want_json_modules>,Binary modules <flow_binary_modules>, andOld-Style modules <flow_old_style_modules>aren't touched by either of these and pass through unchanged.
After the assembling step, one final modification is made to all modules that have a shebang line. Ansible checks whether the interpreter in the shebang line has a specific path configured with an ansible_$X_interpreter inventory variable. If it does, Ansible substitutes that path for the interpreter path given in the module. After this, Ansible returns the complete module data and the module type to the Normal Action <flow_normal_action_plugin> which continues execution of the module.
Assembler frameworks
Ansible supports two assembler frameworks: Ansiballz and the older Module Replacer.
Module Replacer framework
The Module Replacer framework is the original framework implementing new-style modules, and is still used for PowerShell modules. It is essentially a preprocessor (like the C Preprocessor for those familiar with that programming language). It does straight substitutions of specific substring patterns in the module file. There are two types of substitutions:
- Replacements that only happen in the module file. These are public replacement strings that modules can utilize to get helpful boilerplate or access to arguments.
from ansible.module_utils.MOD_LIB_NAME import *is replaced with the contents of theansible/module_utils/MOD_LIB_NAME.pyThese should only be used withnew-style Python modules <flow_python_modules>.#<<INCLUDE_ANSIBLE_MODULE_COMMON>>is equivalent tofrom ansible.module_utils.basic import *and should also only apply to new-style Python modules.# POWERSHELL_COMMONsubstitutes the contents ofansible/module_utils/powershell.ps1. It should only be used withnew-style Powershell modules <flow_powershell_modules>.
- Replacements that are used by
ansible.module_utilscode. These are internal replacement patterns. They may be used internally, in the above public replacements, but shouldn't be used directly by modules."<<ANSIBLE_VERSION>>"is substituted with the Ansible version. Innew-style Python modules <flow_python_modules>under theAnsiballzframework the proper way is to instead instantiate an AnsibleModule and then access the version from`AnsibleModule.ansible_version`."<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"is substituted with a string which is the Pythonreprof theJSONencoded module parameters. Usingrepron the JSON string makes it safe to embed in a Python file. In new-style Python modules under the Ansiballz framework this is better accessed by instantiating an AnsibleModule and then usingAnsibleModule.params.<<SELINUX_SPECIAL_FILESYSTEMS>>substitutes a string which is a comma-separated list of file systems which have a file system dependent security context in SELinux. In new-style Python modules, if you really need this you should instantiate an AnsibleModule and then useAnsibleModule._selinux_special_fs. The variable has also changed from a comma-separated string of file system names to an actual python list of file system names.<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>substitutes the module parameters as a JSON string. Care must be taken to properly quote the string as JSON data may contain quotes. This pattern is not substituted in new-style Python modules as they can get the module parameters another way.- The string
syslog.LOG_USERis replaced wherever it occurs with thesyslog_facilitywhich was named inansible.cfgor anyansible_syslog_facilityinventory variable that applies to this host. In new-style Python modules this has changed slightly. If you really need to access it, you should instantiate an AnsibleModule and then useAnsibleModule._syslog_facilityto access it. It is no longer the actual syslog facility and is now the name of the syslog facility. See thedocumentation on internal arguments <flow_internal_arguments>for details.
Ansiballz framework
The Ansiballz framework was adopted in Ansible 2.1 and is used for all new-style Python modules. Unlike the Module Replacer, Ansiballz uses real Python imports of things in ansible/module_utils instead of merely preprocessing the module. It does this by constructing a zipfile -- which includes the module file, files in ansible/module_utils that are imported by the module, and some boilerplate to pass in the module's parameters. The zipfile is then Base64 encoded and wrapped in a small Python script which decodes the Base64 encoding and places the zipfile into a temp directory on the managed node. It then extracts just the Ansible module script from the zip file and places that in the temporary directory as well. Then it sets the PYTHONPATH to find Python modules inside of the zip file and imports the Ansible module as the special name, __main__. Importing it as __main__ causes Python to think that it is executing a script rather than simply importing a module. This lets Ansible run both the wrapper script and the module code in a single copy of Python on the remote machine.
* Ansible wraps the zipfile in the Python script for two reasons:
- for compatibility with Python 2.6 which has a less functional version of Python's
-mcommand-line switch. - so that pipelining will function properly. Pipelining needs to pipe the Python module into the Python interpreter on the remote node. Python understands scripts on stdin but does not understand zip files.
* Prior to Ansible 2.7, the module was executed by a second Python interpreter instead of being executed inside of the same process. This change was made once Python-2.4 support was dropped to speed up module execution.
In Ansiballz, any imports of Python modules from the ansible.module_utils package trigger inclusion of that Python file into the zipfile. Instances of #<<INCLUDE_ANSIBLE_MODULE_COMMON>> in the module are turned into from ansible.module_utils.basic import * and ansible/module-utils/basic.py is then included in the zipfile. Files that are included from module_utils are themselves scanned for imports of other Python modules from module_utils to be included in the zipfile as well.
Passing args
Arguments are passed differently by the two frameworks:
- In
module_replacer, module arguments are turned into a JSON-ified string and substituted into the combined module file. - In
Ansiballz, the JSON-ified string is part of the script which wraps the zipfile. Just before the wrapper script imports the Ansible module as__main__, it monkey-patches the private,_ANSIBLE_ARGSvariable inbasic.pywith the variable values. When aansible.module_utils.basic.AnsibleModuleis instantiated, it parses this string and places the args intoAnsibleModule.paramswhere it can be accessed by the module's other code.
If you are writing modules, remember that the way we pass arguments is an internal implementation detail: it has changed in the past and will change again as soon as changes to the common module_utils code allow Ansible modules to forgo using ansible.module_utils.basic.AnsibleModule. Do not rely on the internal global _ANSIBLE_ARGS variable.
Very dynamic custom modules which need to parse arguments before they instantiate an AnsibleModule may use _load_params to retrieve those parameters. Although _load_params may change in breaking ways if necessary to support changes in the code, it is likely to be more stable than either the way we pass parameters or the internal global variable.
Prior to Ansible 2.7, the Ansible module was invoked in a second Python interpreter and the arguments were then passed to the script over the script's stdin.
Internal arguments
Both module_replacer and Ansiballz send additional arguments to the Ansible module beyond those which the user specified in the playbook. These additional arguments are internal parameters that help implement global Ansible features. Modules often do not need to know about these explicitly because the features are implemented in ansible.module_utils.basic. However, certain features need support from modules and some knowledge of the internal arguments is useful.
The internal arguments in this section are global. If you need to add a local internal argument to a custom module, create an action plugin for that specific module. See _original_basename in the copy action plugin for an example.
ansible_no_log
Type: bool
Set to True whenever an argument in a task or play specifies no_log. Any module that calls the AnsibleModule.log function handles this action automatically. If you have a module that implements its own logging then you need to check the value of _ansible_no_log. To access _ansible_no_log in a module, instantiate the AnsibleModule utility and then check the value of AnsibleModule.no_log.
no_log specified in a module's argument_spec is handled by a different mechanism.
ansible_debug
Type: bool
Operates verbose logging and logging of external commands that a module executes. If the module uses the AnsibleModule.debug function rather than the AnsibleModule.log function then the messages are only logged if you set the _ansible_debug argument to True. To access _ansible_debug in a module, instantiate the AnsibleModule utility and access AnsibleModule._debug. For more details, see DEFAULT_DEBUG.
ansible_diff
Type: bool
With this parameter you can configure your module to show a unified diff of changes that will be applied to the templated files. To access _ansible_diff in a module, instantiate the AnsibleModule utility and access AnsibleModule._diff. You can also access this parameter using the diff keyword in your playbook, or the relevant environment variable. For more details, see playbook_keywords and the DIFF_ALWAYS configuration option.
ansible_verbosity
Type: int
You can use this argument to control the level (0 for none) of verbosity in logging.
ansible_selinux_special_fs
Type: list Elements: strings
This argument provides modules with the names of file systems which should have a special SELinux context. They are used by the AnsibleModule methods which operate on files (changing attributes, moving, and copying).
Most modules can use the built-in AnsibleModule methods to manipulate files. To access in a module that needs to know about these special context file systems, instantiate AnsibleModule and examine the list in AnsibleModule._selinux_special_fs.
This argument replaces ansible.module_utils.basic.SELINUX_SPECIAL_FS from module_replacer. In the module replacer framework the argument was formatted as a comma-separated string of file system names. Under the Ansiballz framework it is a list. You can access _ansible_selinux_special_fs using the corresponding environment variable. For more details, see the DEFAULT_SELINUX_SPECIAL_FS configuration option.
2.1
ansible_syslog_facility
This argument controls which syslog facility the module logs to. Most modules should just use the AnsibleModule.log function which will then make use of this. If a module has to use this on its own, it should instantiate the AnsibleModule method and then retrieve the name of the syslog facility from AnsibleModule._syslog_facility. The Ansiballz code is less elegant than the module_replacer code:
# Old module_replacer way
import syslog
syslog.openlog(NAME, 0, syslog.LOG_USER)
# New Ansiballz way
import syslog
facility_name = module._syslog_facility
facility = getattr(syslog, facility_name, syslog.LOG_USER)
syslog.openlog(NAME, 0, facility)
For more details, see the DEFAULT_SYSLOG_FACILITY configuration option.
2.1
ansible_version
This argument passes the version of Ansible to the module. To access it, a module should instantiate the AnsibleModule method and then retrieve the version from AnsibleModule.ansible_version. This replaces ansible.module_utils.basic.ANSIBLE_VERSION from module_replacer.
2.1
ansible_module_name
Type: str
This argument passes the information to modules about their name. For more details see, the configuration option DEFAULT_MODULE_NAME.
ansible_keep_remote_files
Type: bool
This argument provides instructions that modules must be ready if they need to keep the remote files. For more details, see the DEFAULT_KEEP_REMOTE_FILES configuration option.
ansible_socket
This argument provides modules with a socket for persistent connections. The argument is created using the PERSISTENT_CONTROL_PATH_DIR configuration option.
ansible_shell_executable
Type: bool
This argument ensures that modules use the designated shell executable. For more details, see the ansible_shell_executable remote host environment parameter.
ansible_tmpdir
Type: str
This argument provides instructions to modules that all commands must use the designated temporary directory, if created. The action plugin designs this temporary directory.
Modules can access this parameter by using the public tmpdir property. The tmpdir property will create a temporary directory if the action plugin did not set the parameter.
The directory name is generated randomly, and the root of the directory is determined by one of these:
DEFAULT_LOCAL_TMPremote_tmp <ansible.builtin.sh#shell:remote_tmp>system_tmpdirs <ansible.builtin.sh#shell:system_tmpdirs>
As a result, using the ansible.cfg configuration file to activate or customize this setting will not guarantee that you control the full value.
ansible_remote_tmp
The module's tmpdir property creates a randomized directory name in this directory if the action plugin did not set _ansible_tmpdir. For more details, see the remote_tmp <ansible.builtin.sh#shell:remote_tmp> parameter of the shell plugin.
Module return values & Unsafe strings
At the end of a module's execution, it formats the data that it wants to return as a JSON string and prints the string to its stdout. The normal action plugin receives the JSON string, parses it into a Python dictionary, and returns it to the executor.
If Ansible templated every string return value, it would be vulnerable to an attack from users with access to managed nodes. If an unscrupulous user disguised malicious code as Ansible return value strings, and if those strings were then templated on the control node, Ansible could execute arbitrary code. To prevent this scenario, Ansible marks all strings inside returned data as Unsafe, emitting any Jinja2 templates in the strings verbatim, not expanded by Jinja2.
Strings returned by invoking a module through ActionPlugin._execute_module() are automatically marked as Unsafe by the normal action plugin. If another action plugin retrieves information from a module through some other means, it must mark its return data as Unsafe on its own.
In case a poorly-coded action plugin fails to mark its results as "Unsafe," Ansible audits the results again when they are returned to the executor, marking all strings as Unsafe. The normal action plugin protects itself and any other code that it calls with the result data as a parameter. The check inside the executor protects the output of all other action plugins, ensuring that subsequent tasks run by Ansible will not template anything from those results either.
Special considerations
Pipelining
Ansible can transfer a module to a remote machine in one of two ways:
- it can write out the module to a temporary file on the remote host and then use a second connection to the remote host to execute it with the interpreter that the module needs
- or it can use what's known as pipelining to execute the module by piping it into the remote interpreter's stdin.
Pipelining only works with modules written in Python at this time because Ansible only knows that Python supports this mode of operation. Supporting pipelining means that whatever format the module payload takes before being sent over the wire must be executable by Python through stdin.
Why pass args over stdin?
Passing arguments through stdin was chosen for the following reasons:
- When combined with
ANSIBLE_PIPELINING, this keeps the module's arguments from temporarily being saved onto disk on the remote machine. This makes it harder (but not impossible) for a malicious user on the remote machine to steal any sensitive information that may be present in the arguments. - Command line arguments would be insecure as most systems allow unprivileged users to read the full commandline of a process.
- Environment variables are usually more secure than the commandline but some systems limit the total size of the environment. This could lead to truncation of the parameters if we hit that limit.
AnsibleModule
Argument spec
The argument_spec provided to AnsibleModule defines the supported arguments for a module, as well as their type, defaults and more.
Example argument_spec:
module = AnsibleModule(argument_spec=dict(
top_level=dict(
type='dict',
options=dict(
second_level=dict(
default=True,
type='bool',
)
)
)
))
This section will discuss the behavioral attributes for arguments:
type
typeallows you to define the type of the value accepted for the argument. The default value fortypeisstr. Possible values are:
- str
- list
- dict
- bool
- int
- float
- path
- raw
- jsonarg
- json
- bytes
- bits
The
rawtype, performs no type validation or type casting, and maintains the type of the passed value.
elements
elementsworks in combination withtypewhentype='list'.elementscan then be defined aselements='int'or any other type, indicating that each element of the specified list should be of that type.
default
The
defaultoption allows sets a default value for the argument for the scenario when the argument is not provided to the module. When not specified, the default value isNone.
fallback
fallbackaccepts atuplewhere the first argument is a callable (function) that will be used to perform the lookup, based on the second argument. The second argument is a list of values to be accepted by the callable.The most common callable used is
env_fallbackwhich will allow an argument to optionally use an environment variable when the argument is not supplied.Example:
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME']))
choices
choicesaccepts a list of choices that the argument will accept. The types ofchoicesshould match thetype.
required
requiredaccepts a boolean, eitherTrueorFalsethat indicates that the argument is required. When not specified,requireddefaults toFalse. This should not be used in combination withdefault.
no_log
no_logaccepts a boolean, eitherTrueorFalse, that indicates explicitly whether or not the argument value should be masked in logs and output.How the value is hidden depends on the
ansible-coreversion. Onansible-core2.22 and later, the value is registered as a secret and masked wherever Ansible writes output, while the module result keeps the real value so later tasks can use it. On earlier versions, the value is removed from the module result and replaced withVALUE_SPECIFIED_IN_NO_LOG_PARAMETER, and is stripped from the module logs. Seedeveloping_secret_maskingfor details of the masking behavior.[!NOTE] In the absence of
no_log, if the parameter name appears to indicate that the argument value is a password or passphrase (such as "admin_password"), a warning will be shown and the value will be masked in logs but not output. To disable the warning and masking for parameters that do not contain sensitive information, setno_logtoFalse.
aliases
aliasesaccepts a list of alternative argument names for the argument, such as the case where the argument isnamebut the module acceptsaliases=['pkg']to allowpkgto be interchangeably withname. Use of aliases can make module interfaces confusing, so we recommend adding them only when necessary. If you are updating argument names to fix a typo or improve the interface, consider moving the old names todeprecated_aliasesrather than keeping them around indefinitely.
options
optionsimplements the ability to create a sub-argument_spec, where the sub options of the top level argument are also validated using the attributes discussed in this section. The example at the top of this section demonstrates use ofoptions.typeorelementsshould bedictis this case.
apply_defaults
apply_defaultsworks alongsideoptionsand allows thedefaultof the sub-options to be applied even when the top-level argument is not supplied.In the example of the
argument_specat the top of this section, it would allowmodule.params['top_level']['second_level']to be defined, even if the user does not providetop_levelwhen calling the module.
removed_in_version
removed_in_versionindicates which version of ansible-core or a collection a deprecated argument will be removed in. Mutually exclusive withremoved_at_date, and must be used withremoved_from_collection.Example:
option = { 'type': 'str', 'removed_in_version': '2.0.0', 'removed_from_collection': 'testns.testcol', },
removed_at_date
removed_at_dateindicates that a deprecated argument will be removed in a minor ansible-core release or major collection release after this date. Mutually exclusive withremoved_in_version, and must be used withremoved_from_collection.Example:
option = { 'type': 'str', 'removed_at_date': '2020-12-31', 'removed_from_collection': 'testns.testcol', },
removed_from_collection
Specifies which collection (or ansible-core) deprecates this deprecated argument. Specify
ansible.builtinfor ansible-core, or the collection's name (formatfoo.bar). Must be used withremoved_in_versionorremoved_at_date.
deprecated_aliases
Deprecates aliases of this argument. Must contain a list or tuple of dictionaries having some the following keys:
name
The name of the alias to deprecate. (Required.)
version
The version of ansible-core or the collection this alias will be removed in. Either
versionordatemust be specified.date
The a date after which a minor release of ansible-core or a major collection release will no longer contain this alias.. Either
versionordatemust be specified.collection_name
Specifies which collection (or ansible-core) deprecates this deprecated alias. Specify
ansible.builtinfor ansible-core, or the collection's name (formatfoo.bar). Must be used withversionordate.Examples:
option = { 'type': 'str', 'aliases': ['foo', 'bar'], 'deprecated_aliases': [ { 'name': 'foo', 'version': '2.0.0', 'collection_name': 'testns.testcol', }, { 'name': 'foo', 'date': '2020-12-31', 'collection_name': 'testns.testcol', }, ], },
mutually_exclusive
If
optionsis specified,mutually_exclusiverefers to the sub-options described inoptionsand behaves as inargument_spec_dependencies.
required_together
If
optionsis specified,required_togetherrefers to the sub-options described inoptionsand behaves as inargument_spec_dependencies.
required_one_of
If
optionsis specified,required_one_ofrefers to the sub-options described inoptionsand behaves as inargument_spec_dependencies.
required_if
If
optionsis specified,required_ifrefers to the sub-options described inoptionsand behaves as inargument_spec_dependencies.
required_by
If
optionsis specified,required_byrefers to the sub-options described inoptionsand behaves as inargument_spec_dependencies.
context
2.17
You can set the value of the
contextkey to a dict of custom content. This allows you to provide additional context in the argument spec. The content provided is not validated or utilized by the core engine.Example:
option = { 'type': 'str', 'context': { 'disposition': '/properties/apiType', }, 'choices': ['http', 'soap'], }
Dependencies between module options
The following are optional arguments for AnsibleModule():
module = AnsibleModule(
argument_spec,
mutually_exclusive=[
('path', 'content'),
],
required_one_of=[
('path', 'content'),
],
)
mutually_exclusive
Must be a sequence (list or tuple) of sequences of strings. Every sequence of strings is a list of option names which are mutually exclusive. If more than one options of a list are specified together, Ansible will fail the module with an error.
Example:
mutually_exclusive=[ ('path', 'content'), ('repository_url', 'repository_filename'), ],In this example, the options
pathandcontentmust not specified at the same time. Also the optionsrepository_urlandrepository_filenamemust not be specified at the same time. But specifyingpathandrepository_urlis accepted.To ensure that precisely one of two (or more) options is specified, combine
mutually_exclusivewithrequired_one_of.
required_together
Must be a sequence (list or tuple) of sequences of strings. Every sequence of strings is a list of option names which are must be specified together. If at least one of these options are specified, the other ones from the same sequence must all be present.
Example:
required_together=[ ('file_path', 'file_hash'), ],In this example, if one of the options
file_pathorfile_hashis specified, Ansible will fail the module with an error if the other one is not specified.
required_one_of
Must be a sequence (list or tuple) of sequences of strings. Every sequence of strings is a list of option names from which at least one must be specified. If none one of these options are specified, Ansible will fail module execution.
Example:
required_one_of=[ ('path', 'content'), ],In this example, at least one of
pathandcontentmust be specified. If none are specified, execution will fail. Specifying both is explicitly allowed; to prevent this, combinerequired_one_ofwithmutually_exclusive.
required_if
Must be a sequence of sequences. Every inner sequence describes one conditional dependency. Every sequence must have three or four values. The first two values are the option's name and the option's value which describes the condition. The further elements of the sequence are only needed if the option of that name has precisely this value.
If you want that all options in a list of option names are specified if the condition is met, use one of the following forms:
('option_name', option_value, ('option_a', 'option_b', ...)), ('option_name', option_value, ('option_a', 'option_b', ...), False),If you want that at least one option of a list of option names is specified if the condition is met, use the following form:
('option_name', option_value, ('option_a', 'option_b', ...), True),Example:
required_if=[ ('state', 'present', ('path', 'content'), True), ('force', True, ('force_reason', 'force_code')), ],In this example, if the user specifies
state=present, at least one of the optionspathandcontentmust be supplied (or both). To make sure that precisely one can be specified, combinerequired_ifwithmutually_exclusive.On the other hand, if
force(a boolean parameter) is set totrue,yesand so on, bothforce_reasonandforce_codemust be specified.
required_by
Must be a dictionary mapping option names to sequences of option names. If the option name in a dictionary key is specified, the option names it maps to must all also be specified. Note that instead of a sequence of option names, you can also specify one single option name.
Example:
required_by={ 'force': 'force_reason', 'path': ('mode', 'owner', 'group'), },In the example, if
forceis specified,force_reasonmust also be specified. Also, ifpathis specified, then three three optionsmode,ownerandgroupalso must be specified.
Declaring check mode support
To declare that a module supports check mode, supply supports_check_mode=True to the AnsibleModule() call:
module = AnsibleModule(argument_spec, supports_check_mode=True)
The module can determine whether it is called in check mode by checking the boolean value module.check_mode. If it evaluates to True, the module must take care not to do any modification.
If supports_check_mode=False is specified, which is the default value, the module will exit in check mode with skipped=True and message remote module (<insert module name here>) does not support check mode.
Adding file options
To declare that a module should add support for all common file options, supply add_file_common_args=True to the AnsibleModule() call:
module = AnsibleModule(argument_spec, add_file_common_args=True)
You can find a list of all file options here. It is recommended that you make your DOCUMENTATION extend the doc fragment ansible.builtin.files (see module_docs_fragments) in this case, to make sure that all these fields are correctly documented.
The helper functions module.load_file_common_arguments() and module.set_fs_attributes_if_different() can be used to handle these arguments for you:
argument_spec = {
'path': {
'type': 'str',
'required': True,
},
}
module = AnsibleModule(argument_spec, add_file_common_args=True)
changed = False
# TODO do something with module.params['path'], like update its contents
# Ensure that module.params['path'] satisfies the file options supplied by the user
file_args = module.load_file_common_arguments(module.params)
changed = module.set_fs_attributes_if_different(file_args, changed)
module.exit_json(changed=changed)
Ansible and Python 3
The ansible-core code runs Python 3 (for specific versions check Control Node Requirements <control_node_requirements>). Contributors to ansible-core and to Ansible Collections should be aware of the tips in this document so that they can write code that will run on the same versions of Python as the rest of Ansible.
We do have some considerations depending on the types of Ansible code:
- code on the control node - code that runs on the machine where you invoke
/usr/bin/ansible, only needs to support the control node's Python versions. - modules - the code which Ansible transmits to and invokes on the managed machine. Modules need to support the 'managed node' Python versions, with some exceptions.
- shared
module_utilscode - the common code that is used by modules to perform tasks and sometimes used by code on the control node. Sharedmodule_utilscode needs to support the same range of Python as the modules.
However, the three types of code do not use the same string strategy. If you're developing a module or some module_utils code, be sure to read the section on string strategy carefully.
Minimum version of Python 3.x and Python 2.x
See Control Node Requirements <control_node_requirements> and Managed Node Requirements <managed_node_requirements> for the specific versions supported.
Your custom modules can support any version of Python (or other languages) you want, but the above are the requirements for the code contributed to the Ansible project.
Developing Ansible code that supports Python 2 and Python 3
The best place to start learning about writing code that supports both Python 2 and Python 3 is Lennart Regebro's book: Porting to Python 3. The book describes several strategies for porting to Python 3. The one we're using is to support Python 2 and Python 3 from a single code base
Understanding strings in Python 2 and Python 3
Python 2 and Python 3 handle strings differently, so when you write code that supports Python 3 you must decide what string model to use. Strings can be an array of bytes (like in C) or they can be an array of text. Text is what we think of as letters, digits, numbers, other printable symbols, and a small number of unprintable "symbols" (control codes).
In Python 2, the two types for these (str <python:str> for bytes and unicode <python:unicode> for text) are often used interchangeably. When dealing only with ASCII characters, the strings can be combined, compared, and converted from one type to another automatically. When non-ASCII characters are introduced, Python 2 starts throwing exceptions due to not knowing what encoding the non-ASCII characters should be in.
Python 3 changes this behavior by making the separation between bytes (bytes <python3:bytes>) and text (str <python3:str>) more strict. Python 3 will throw an exception when trying to combine and compare the two types. The programmer has to explicitly convert from one type to the other to mix values from each.
In Python 3 it is immediately apparent to the programmer when code is mixing the byte and text types inappropriately, whereas in Python 2, code that mixes those types may work until a user causes an exception by entering non-ASCII input. Python 3 forces programmers to proactively define a strategy for working with strings in their program so that they don't mix text and byte strings unintentionally.
Ansible uses different strategies for working with strings in the code on the control node, in modules <module_string_strategy>, and in module_utils <module_utils_string_strategy> code.
Control node string strategy: the Unicode Sandwich
Until recently ansible-core supported Python 2.x and followed this strategy, known as the Unicode Sandwich (named after Python 2's unicode <python:unicode> text type). For Unicode Sandwich we know that at the border of our code and the outside world (for example, file and network IO, environment variables, and some library calls) we are going to receive bytes. We need to transform these bytes into text and use that throughout the internal portions of our code. When we have to send those strings back out to the outside world we first convert the text back into bytes. To visualize this, imagine a 'sandwich' consisting of a top and bottom layer of bytes, a layer of conversion between, and all text type in the center.
For compatibility reasons you will see a bunch of custom functions we developed (to_text/to_bytes/to_native) and while Python 2 is not a concern anymore we will continue to use them as they apply for other cases that make dealing with unicode problematic.
While we will not be using most of it anymore, the documentation below is still useful for those developing modules that still need to support both Python 2 and 3 simultaneously.
Unicode Sandwich common borders: places to convert bytes to text in control node code
This is a partial list of places where we have to convert to and from bytes when using the Unicode Sandwich string strategy. It is not exhaustive but it gives you an idea of where to watch for problems.
Reading and writing to files
In Python 2, reading from files yields bytes. In Python 3, it can yield text. To make code that's portable to both we don't make use of Python 3's ability to yield text but instead do the conversion explicitly ourselves. For example:
from ansible.module_utils.common.text.converters import to_text
with open('filename-with-utf8-data.txt', 'rb') as my_file:
b_data = my_file.read()
try:
data = to_text(b_data, errors='surrogate_or_strict')
except UnicodeError:
# Handle the exception gracefully -- usually by displaying a good
# user-centric error message that can be traced back to this piece
# of code.
pass
Much of Ansible assumes that all encoded text is UTF-8. At some point, if there is demand for other encodings we may change that, but for now it is safe to assume that bytes are UTF-8.
Writing to files is the opposite process:
from ansible.module_utils.common.text.converters import to_bytes
with open('filename.txt', 'wb') as my_file:
my_file.write(to_bytes(some_text_string))
Note that we don't have to catch UnicodeError here because we're transforming to UTF-8 and all text strings in Python can be transformed back to UTF-8.
Filesystem interaction
Dealing with file names often involves dropping back to bytes because on UNIX-like systems file names are bytes. On Python 2, if we pass a text string to these functions, the text string will be converted to a byte string inside of the function and a traceback will occur if non-ASCII characters are present. In Python 3, a traceback will only occur if the text string can't be decoded in the current locale, but it is still good to be explicit and have code which works on both versions:
import os.path
from ansible.module_utils.common.text.converters import to_bytes
filename = u'/var/tmp/くらとみ.txt'
f = open(to_bytes(filename), 'wb')
mtime = os.path.getmtime(to_bytes(filename))
b_filename = os.path.expandvars(to_bytes(filename))
if os.path.exists(to_bytes(filename)):
pass
When you are only manipulating a filename as a string without talking to the filesystem (or a C library which talks to the filesystem) you can often get away without converting to bytes:
import os.path
os.path.join(u'/var/tmp/café', u'くらとみ')
os.path.split(u'/var/tmp/café/くらとみ')
On the other hand, if the code needs to manipulate the file name and also talk to the filesystem, it can be more convenient to transform to bytes right away and manipulate in bytes.
Make sure all variables passed to a function are the same type. If you're working with something like python3:os.path.join which takes multiple strings and uses them in combination, you need to make sure that all the types are the same (either all bytes or all text). Mixing bytes and text will cause tracebacks.
Interacting with other programs
Interacting with other programs goes through the operating system and C libraries and operates on things that the UNIX kernel defines. These interfaces are all byte-oriented so the Python interface is byte oriented as well. On both Python 2 and Python 3, byte strings should be given to Python's subprocess library and byte strings should be expected back from it.
One of the main places in Ansible's control node code that we interact with other programs is the connection plugins' exec_command methods. These methods transform any text strings they receive in the command (and arguments to the command) to execute into bytes and return stdout and stderr as byte strings. Higher level functions (like action plugins' _low_level_execute_command) transform the output into text strings.
Module string strategy: Native String
In modules we use a strategy known as Native Strings. This makes things easier on the community members who maintain so many of Ansible's modules, by not breaking backwards compatibility by mandating that all strings inside of modules are text and converting between text and bytes at the borders.
Native strings refer to the type that Python uses when you specify a bare string literal:
"This is a native string"
In Python 2, these are byte strings. In Python 3 these are text strings. Modules should be coded to expect bytes on Python 2 and text on Python 3.
Module_utils string strategy: hybrid
In module_utils code we use a hybrid string strategy. Although Ansible's module_utils code is largely like module code, some pieces of it are used by the control node as well. So it needs to be compatible with modules and with the control node's assumptions, particularly the string strategy. The module_utils code attempts to accept native strings as input to its functions and emit native strings as their output.
In module_utils code:
- Functions must accept string parameters as either text strings or byte strings.
- Functions may return either the same type of string as they were given or the native string type for the Python version they are run on.
- Functions that return strings must document whether they return strings of the same type as they were given or native strings.
Module-utils functions are therefore often very defensive in nature. They convert their string parameters into text (using ansible.module_utils.common.text.converters.to_text) at the beginning of the function, do their work, and then convert the return values into the native string type (using ansible.module_utils.common.text.converters.to_native) or back to the string type that their parameters received.
Tips, tricks, and idioms for Python 2/Python 3 compatibility
Use forward-compatibility boilerplate
Use the following boilerplate code at the top of all python files to make certain constructs act the same way on Python 2 and Python 3:
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
__metaclass__ = type makes all classes defined in the file into new-style classes without explicitly inheriting from object <python3:object>.
The __future__ imports do the following:
absolute_import
Makes imports look in sys.path <python3:sys.path> for the modules being imported, skipping the directory in which the module doing the importing lives. If the code wants to use the directory in which the module doing the importing, there's a new dot notation to do so.
division
Makes division of integers always return a float. If you need to find the quotient use x // y instead of x / y.
print_function
Changes print <python3:print> from a keyword into a function.
Prefix byte strings with b_
Since mixing text and bytes types leads to tracebacks we want to be clear about what variables hold text and what variables hold bytes. We do this by prefixing any variable holding bytes with b_. For example:
filename = u'/var/tmp/café.txt'
b_filename = to_bytes(filename)
with open(b_filename) as f:
data = f.read()
We do not prefix the text strings instead because we only operate on byte strings at the borders, so there are fewer variables that need bytes than text.
Import Ansible's bundled Python six library
The third-party Python six library exists to help projects create code that runs on both Python 2 and Python 3. Ansible includes a version of the library in module_utils so that other modules can use it without requiring that it is installed on the remote system. To make use of it, import it like this:
from ansible.module_utils import six
Ansible can also use a system copy of six
Ansible will use a system copy of six if the system copy is a later version than the one Ansible bundles.
Handle exceptions with as
In order for code to function on Python 2.6+ and Python 3, use the new exception-catching syntax which uses the as keyword:
try:
a = 2/0
except ValueError as e:
module.fail_json(msg="Tried to divide by zero: %s" % e)
Do not use the following syntax as it will fail on every version of Python 3:
try:
a = 2/0
except ValueError, e:
module.fail_json(msg="Tried to divide by zero: %s" % e)
Update octal numbers
In Python 2.x, octal literals could be specified as 0755. In Python 3, octals must be specified as 0o755.
String formatting for control node code
Use str.format() for Python 2.6 compatibility
Starting in Python 2.6, strings gained a method called format() to put strings together. However, one commonly used feature of format() wasn't added until Python 2.7, so you need to remember not to use it in Ansible code:
# Does not work in Python 2.6!
new_string = "Dear {}, Welcome to {}".format(username, location)
# Use this instead
new_string = "Dear {0}, Welcome to {1}".format(username, location)
Both of the format strings above map positional arguments of the format() method into the string. However, the first version doesn't work in Python 2.6. Always remember to put numbers into the placeholders so the code is compatible with Python 2.6.
Python documentation on format strings:
Use percent format with byte strings
In Python 3.5 and later, byte strings do not have a format() method. However, it does have support for the older, percent-formatting.
b_command_line = b'ansible-playbook --become-user %s -K %s' % (user, playbook_file)
Rebasing a pull request
You may find that your pull request (PR) is out-of-date and needs to be rebased. This can happen for several reasons:
- Files modified in your PR are in conflict with changes which have already been merged.
- Your PR is old enough that significant changes to automated test infrastructure have occurred.
Rebasing the branch used to create your PR will resolve both of these issues.
Configuring your remotes
Before you can rebase your PR, you need to make sure you have the proper remotes configured. These instructions apply to any repository on GitHub, including collections repositories. On other platforms (bitbucket, gitlab), the same principles and commands apply but the syntax may be different. We use the ansible/ansible repository here as an example. In other repositories, the branch names may be different. Assuming you cloned your fork in the usual fashion, the origin remote will point to your fork:
$ git remote -v
origin git@github.com:YOUR_GITHUB_USERNAME/ansible.git (fetch)
origin git@github.com:YOUR_GITHUB_USERNAME/ansible.git (push)
However, you also need to add a remote which points to the upstream repository:
$ git remote add upstream https://github.com/ansible/ansible.git
Which should leave you with the following remotes:
$ git remote -v
origin git@github.com:YOUR_GITHUB_USERNAME/ansible.git (fetch)
origin git@github.com:YOUR_GITHUB_USERNAME/ansible.git (push)
upstream https://github.com/ansible/ansible.git (fetch)
upstream https://github.com/ansible/ansible.git (push)
Checking the status of your branch should show your fork is up-to-date with the origin remote:
$ git status
On branch YOUR_BRANCH
Your branch is up-to-date with 'origin/YOUR_BRANCH'.
nothing to commit, working tree clean
Rebasing your branch
Once you have an upstream remote configured, you can rebase the branch for your PR:
$ git pull --rebase upstream devel
This will replay the changes in your branch on top of the changes made in the upstream devel branch. If there are merge conflicts, you will be prompted to resolve those before you can continue.
After you rebase, the status of your branch changes:
$ git status
On branch YOUR_BRANCH
Your branch and 'origin/YOUR_BRANCH' have diverged,
and have 4 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
nothing to commit, working tree clean
Don't worry, this is normal after a rebase. You should ignore the git status instructions to use git pull. We'll cover what to do next in the following section.
Updating your pull request
Now that you've rebased your branch, you need to push your changes to GitHub to update your PR.
Since rebasing re-writes Git history, you will need to use a force push:
$ git push --force-with-lease
Your PR on GitHub has now been updated. This will automatically trigger testing of your changes. You should check in on the status of your PR after tests have completed to see if further changes are required.
Getting help rebasing
If you need any help with rebasing your PR, or you have other development related questions, visit the Ansible communication guide<communication> for information on how to reach out to the community.
community_development_process
Information on roadmaps, opening PRs, Ansibullbot, and more
Handling secrets in modules and plugins
2.22
ansible-core 2.22 adds a secret masking API that modules and plugins can use to register sensitive values. Any registered value is replaced with $REDACTED$ wherever Ansible writes output, without changing the value itself. This page describes the API, how registered secrets move between the controller, its worker processes, and the managed node, and what you must do in callback plugins and plugin configuration to take part in masking.
For the user-facing behavior, including which values Ansible registers automatically and how registered secrets are matched, see playbooks_secret_masking.
How masking works
Ansible keeps a process-wide registry of secret strings. Every place that Ansible writes output, referred to as an egress boundary, runs the text through the registry and replaces any registered secret with the placeholder. The boundaries are:
- The
Displayobject on the controller, which covers the screen, thelog_pathlog file, warnings, deprecation messages, errors, and tracebacks. - The
resultmapping of every task result handed to a callback plugin. AnsibleModule.log()in Python modules and theAnsible.Basiclogging in PowerShell modules, which write to syslog and the Windows Event Log on the managed node.
Registration is append-only and non-destructive. Registering a value does not change it, and code that already holds the value keeps working with it. Secrets are never removed from the registry for the life of the process.
Secret masking is best effort and does not guarantee that a secret never reaches output. Modules and plugins must still avoid writing sensitive data where they can, and use no_log where a whole result is sensitive. The public API on this page is stable, but the matching algorithm and the other behaviors described here are implementation details that may change in future releases.
The log_path log file is a handler on the Python root logger. Only messages that Ansible writes through Display are masked before they reach it. Messages that a plugin, or a library it imports, emits with the standard logging module are written to the file unmasked. Pass such messages through mask_secrets() before logging them, or avoid logging sensitive data with logging at all.
Secrets registered in a worker process, such as an action, connection, lookup, or filter plugin running inside a forked worker, are sent back to the controller together with the worker's results and registered there. Secrets registered inside a module on the managed node are returned to the controller as part of the module result and registered there as well. In both cases the secret is masked in every later task, not only in the task that registered it.
In the other direction, the controller passes any registered secrets that appear in a module's arguments to the module, so that the module-side masking in AnsibleModule.log() and Ansible.Basic also covers them. Secrets that are not present in the module arguments are not sent to the managed node. A module that obtains a sensitive value another way, such as reading it from a file or receiving it from an API, must register the value itself for module-side masking to cover it, even if the controller already knows that value.
Secrets that a module registers during its run are returned inside the module's raw JSON result and are only registered on the controller when that result is processed. Connection plugin authors must audit any code that displays the raw standard output or standard error of a module, since that text contains the new secrets in plain text before the controller knows about them. The connection plugins shipped with ansible-core only display raw module output when ANSIBLE_DEBUG is enabled. Gate such output the same way, or do not display it at all.
Propagation from a worker to the controller happens when the worker sends its results, and only workers forked after that point inherit the new secret. Workers that are already running, such as those executing the same task for other hosts, do not see it. If a plugin needs a value masked across every host in the current task, register it on the controller before the task runs, for example in a vars plugin or through the register_secret filter in an earlier task.
Masking works by searching output for the exact registered string. A registered secret is masked wherever it appears, including inside a longer word or value, so avoid registering very short or common values. See secret_masking_length_rules for the rules that apply to short, overlapping, and adjacent secrets. If you know a secret can also appear in output in another shape or format, register that form as well so it is redacted too. A base64 encoded or URL encoded copy of a registered secret is a different string and is not masked on its own. The JSON-escaped form of a secret is the one exception. Because most Ansible output and module data is JSON, the registry derives the escaped forms of each secret itself, with and without \uXXXX escapes for non-ASCII characters, and masks those as well.
The ansible.module_utils.secrets API
The ansible.module_utils.secrets module is the public API for working with secrets. It is available to modules, module utilities, and all controller-side plugins.
from ansible.module_utils.secrets import register_secret, register_secrets, mask_secrets
register_secret(value)
Register a single string as a secret and return it unchanged. The return value makes it convenient to wrap an expression in place, for example token = register_secret(response['token']).
register_secrets(values)
Register every string in an iterable. Use this when you have several values to register at once.
mask_secrets(value, *, mask_placeholder='$REDACTED$')
Return a copy of value with every registered secret replaced by mask_placeholder. Use this when you write text to a destination that is not an Ansible egress boundary, such as a file, an external logging system, or a message queue.
All three functions expect Python str values. Convert bytes with to_text() before registering them. Leading and trailing spaces, tabs, carriage returns, and newlines are stripped from a value before it is registered, so there is no need to strip a value read from a file or a subprocess yourself. There is no minimum or maximum secret length but values that are empty after stripping are silently ignored by register_secret() and register_secrets(). In both cases register_secret() still returns the value exactly as it was given.
Registering secrets in a module
Values for options declared with no_log: True in the argument_spec are registered automatically when AnsibleModule validates the arguments. Register any other sensitive value that your module discovers at run time, such as a token returned by an API or a password read from a file on the managed node, as soon as you have it:
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.secrets import register_secret
def main():
module = AnsibleModule(
argument_spec=dict(
username=dict(type='str', required=True),
password=dict(type='str', required=True, no_log=True), # registered automatically
),
)
session = login(module.params['username'], module.params['password'])
# The token is a new secret discovered by the module. Register it so it is masked
# in the module log, the callback output, and any later task on the controller.
token = register_secret(session['token'])
# The real value is still returned so it can be used by later tasks. It is only
# masked when written to output.
module.exit_json(changed=False, token=token)
if __name__ == '__main__':
main()
There is no need to strip the value from the result. The result keeps the real value, and the controller masks it when a callback or Display writes it out.
On ansible-core versions before 2.22, no_log values are removed from the module result and replaced with VALUE_SPECIFIED_IN_NO_LOG_PARAMETER, and are stripped from module logs. The secrets API is not available on those versions.
The AnsibleModule.log() method masks its message and the log_args values before writing to syslog. The module invocation log entry always replaces no_log options with $REDACTED$, regardless of the value's length, and masks any other registered secret it contains.
Modules that respawn under a different Python interpreter keep the secrets that were passed in from the controller, and any secret registered in the respawned process is returned to the controller as normal.
Registering secrets in a plugin
The same functions work in every controller-side plugin type. This example is a lookup plugin that reads a credential from an external service and registers it before returning it:
from ansible.module_utils.secrets import register_secret
from ansible.plugins.lookup import LookupBase
from ansible.utils.display import Display
display = Display()
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
self.set_options(var_options=variables, direct=kwargs)
results = []
for term in terms:
value = fetch_credential(term)
results.append(register_secret(value))
# Display output is a masked boundary, so this prints the placeholder.
display.vvv(f"fetched credential for {term}: {value}")
return results
For plugin options that are always sensitive, prefer marking the option with secret: true in the plugin documentation instead of registering the value by hand. See plugin_config_secret.
Masking output you write yourself
Anything you write outside of an egress boundary is not masked. Call mask_secrets() before writing:
from ansible.module_utils.secrets import mask_secrets
with open('/var/log/my_plugin.log', 'a') as fd:
fd.write(mask_secrets(f"request completed: {response_text}\n"))
Deprecated helpers
The following helpers are replaced by automatic masking and are deprecated:
ansible.module_utils.basic.heuristic_log_sanitize()is deprecated and will be removed inansible-core2.25.ansible.module_utils.common.parameters.remove_values()andsanitize_keys()are deprecated and will be removed inansible-core2.25.- The
liveargument ofansible.utils.cmd_functions.run_cmd()is deprecated and will be removed inansible-core2.25.
Modules that called remove_values() or sanitize_keys() on their results, or that strip no_log values from a result before returning it, no longer need to. Remove those calls and return the real values.
The live argument of run_cmd() writes subprocess output directly to standard output, which bypasses masking. Run the subprocess yourself if you need to stream its output, and mask it with mask_secrets().
The heuristics that these helpers applied are gone as well. AnsibleModule.log() no longer rewrites password-like text such as user:password@host in URLs, and run_command() no longer replaces password-like arguments such as --password=... with ******** in the cmd value of a failure result. Only registered secrets are masked. If your module passes a secret on the command line or embeds one in a URL and that secret is not a no_log option, register it with register_secret() before use.
PowerShell and C# modules
The Ansible.Secrets C# module utility provides the same API for Windows modules. Import it with #AnsibleRequires and use the static Ansible.Secrets.SecretMasker class:
#!powershell
#AnsibleRequires -CSharpUtil Ansible.Basic
#AnsibleRequires -CSharpUtil Ansible.Secrets
$spec = @{
options = @{
username = @{ type = 'str'; required = $true }
password = @{ type = 'str'; required = $true; no_log = $true } # registered automatically
}
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
$session = Connect-MyService -Username $module.Params.username -Password $module.Params.password
# Register the discovered token so it is masked in the Event Log and on the controller.
[Ansible.Secrets.SecretMasker]::RegisterSecret($session.Token)
# Mask a string yourself before writing it somewhere that is not an Ansible boundary.
$summary = [Ansible.Secrets.SecretMasker]::MaskString("Connected with token $($session.Token)")
Set-Content -Path C:\logs\my_module.log -Value $summary
$module.Result.token = $session.Token
$module.ExitJson()
The SecretMasker class exposes:
RegisterSecret(string secret) and RegisterSecret(SecureString secret)
Register a value. Prefer the SecureString overload when you already hold the value as a SecureString, so the plain text is not exposed to PowerShell AMSI notifications.
MaskString(string value) and MaskString(string value, string maskPlaceholder)
Return a copy of value with registered secrets replaced by $REDACTED$ or the placeholder you supply.
The matching rules match the Python implementation. Ansible.Basic registers no_log option values automatically, masks messages written through its event logging, and returns any newly registered secrets to the controller when the module exits.
Marking plugin configuration options as secret
Plugin configuration options can be marked with secret: true. When a marked option is resolved, its value is registered as a secret regardless of the source that set it: an environment variable, an ansible.cfg entry, an Ansible variable, a command line option, or a direct argument to the plugin.
options:
api_token:
description: Token used to authenticate to the service.
type: str
secret: true
env:
- name: MYCOLLECTION_API_TOKEN
vars:
- name: mycollection_api_token
secret is only supported for options of type str, string, and list. For a list option, each string element is registered and non-string elements are ignored. Declaring secret: true on an option of any other type is an error when the plugin's configuration is loaded, so the plugin fails to load rather than silently leaving the value unmasked.
secret is not supported on suboptions. Sub-option definitions are documentation only and are not processed by the configuration manager, so there is no point at which their values could be registered. If a sub-option holds a sensitive value, register it in the plugin with register_secret() after reading the option.
The ansible-core connection and become plugins use this keyword for their password, private key, and passphrase options. When you write a plugin that accepts a password or token, mark the option as secret instead of relying on callers to add no_log or use register_secret.
Callback plugins
The result mapping of every task result handed to a callback plugin is masked before the plugin sees it. Ansible walks CallbackTaskResult.result and replaces registered secrets at any depth, including inside loop results, so a callback never receives an unmasked secret value in the result. The walk covers:
- Strings. Every
strvalue and everystrdictionary key is passed throughmask_secrets(). When two keys mask to the same placeholder, a numeric suffix is appended to keep both entries, for example$REDACTED$and$REDACTED$ (2). - Numbers. An
intorfloatwhose text form is a registered secret is replaced with the placeholder string, so{"pin": 1234567}becomes{"pin": "$REDACTED$"}and the result stays valid when serialized. A callback that expects a number in a result must handle receiving a string instead. - Command output lines. The
stdout_linesandstderr_linesvalues are rebuilt from the maskedstdoutandstderrtext, so a secret that spans several lines is masked in the lines as well. This only happens when the lines value still matches the split text. A module that setsstdout_linesitself has each line masked on its own like any other list.
Booleans, bytes, and None are returned unchanged. Only result.result is masked. Anything else the callback can reach is not:
- The task and play names, and the
task,host,warnings,deprecations, andexceptionattributes of the result. - The statistics passed to
v2_playbook_on_stats()and the arguments of any other callback method. - Data the callback builds by combining or deriving values, for example a value it decodes, hashes, or re-encodes after receiving the result.
Output sent through Display() is masked automatically wherever the data came from. Output written anywhere else, such as a file, a socket, an HTTP request, or a database, should be passed through mask_secrets() first so that data from outside result.result is masked as well. A callback that only writes through Display() needs no changes. The junit and tree callbacks shipped with ansible-core write to files and call mask_secrets() on everything they write, which also covers the task and play names in their output:
import json
from ansible.module_utils.secrets import mask_secrets
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'namespace.collection_name.json_file'
CALLBACK_NEEDS_ENABLED = True
def v2_runner_on_ok(self, result):
# result.result is already masked, but the task name is not, so mask the
# serialized form before writing it.
entry = {'task': result.task_name, 'host': result.host.name, 'result': result.result}
entry_json = mask_secrets(json.dumps(entry, default=str))
with open('/var/log/ansible-results.jsonl', 'a') as fd:
fd.write(entry_json + '\n')
The mask_secrets() function masks a secret in its literal form and in its JSON-escaped form, so a callback can serialize data with json.dumps() first and mask the resulting text, as the example above does. A secret that contains a double quote, a backslash, a control character, or any other character that is written differently when encoded as JSON is still recognized as a secret:
import json
from ansible.module_utils.secrets import mask_secrets, register_secret
register_secret('pa"ss\\wörd!')
# The literal value is masked.
mask_secrets('pa"ss\\wörd!') # $REDACTED$
# The JSON-escaped form is masked as well.
result = {'password': 'pa"ss\\wörd!'}
result_json = json.dumps(result) # {"password": "pa\"ss\\w\u00f6rd!"}
mask_secrets(result_json) # {"password": "$REDACTED$"}
Masking the serialized text is not always enough, or not what you want, for data that does not come from result.result. Mask each value before serializing it instead when:
- The output must stay valid. A non-string value whose text form is a registered secret is replaced inside the serialized text. If
12345678is registered,{"key": 12345678}becomes{"key": $REDACTED$}, which is not valid JSON. - The serializer escapes differently from JSON. XML writes
My<Secret>asMy<Secret>, andrepr(), YAML, and other formats have their own escaping rules. Only the literal and JSON-escaped forms are registered, so a secret containing an escaped character is not found in that text. - The serializer transforms values. Base64, URL encoding, compression, or any other transform produces text that does not contain the registered form.
- Only some fields are secret. Masking the whole text can also replace the same string where it is not sensitive, such as a key name or an unrelated field that happens to hold the same value.
All of the limitations of secret masking <secret_masking_limitations> described in the user guide apply to the masked result and to any output a callback masks itself, so read them before designing how your callback handles results.
playbooks_secret_masking
User guide for secret masking, including the matching rules and limitations
developing_plugins
Developing plugins
developing_modules_general
Developing modules
developing_modules_general_windows
Developing Windows modules
Migrating Roles to Roles in Collections on Galaxy
You can migrate any existing standalone role into a collection and host the collection on Galaxy. With Ansible collections, you can distribute many roles in a single cohesive unit of reusable automation. Inside a collection, you can share custom plugins across all roles in the collection instead of duplicating them in each role's library/` directory.
You must migrate roles to collections if you want to distribute them as certified Ansible content.
If you want to import your collection to Galaxy, you need a Galaxy namespace.
See developing_collections for details on collections.
Comparing standalone roles to collection roles
Standalone roles <playbooks_reuse_roles> have the following directory structure:
role/
├── defaults
├── files
├── handlers
├── library
├── meta
├── module_utils
├── [*_plugins]
├── tasks
├── templates
├── tests
└── vars
The highlighted directories above will change when you migrate to a collection-based role. The collection directory structure includes a roles/ directory:
mynamespace/
└── mycollection/
├── docs/
├── galaxy.yml
├── plugins/
│ ├── modules/
│ │ └── module1.py
│ ├── inventory/
│ └── .../
├── README.md
├── roles/
│ ├── role1/
│ ├── role2/
│ └── .../
├── playbooks/
│ ├── files/
│ ├── vars/
│ ├── templates/
│ └── tasks/
└── tests/
You will need to use the Fully Qualified Collection Name (FQCN) to use the roles and plugins when you migrate your role into a collection. The FQCN is the combination of the collection namespace, collection name, and the content item you are referring to.
So for example, in the above collection, the FQCN to access role1 would be:
mynamespace.mycollection.role1
A collection can contain one or more roles in the roles/ directory and these are almost identical to standalone roles, except you need to move plugins out of the individual roles, and use the FQCN (Fully Qualified Collection Name) in some places, as detailed in the next section.
In standalone roles, some of the plugin directories referenced their plugin types in the plural sense; this is not the case in collections.
Migrating a role to a collection
To migrate from a standalone role that contains no plugins to a collection role:
- Create a local
ansible_collectionsdirectory andcdto this new directory. - Create a collection. If you want to import this collection to Ansible Galaxy, you need a Galaxy namespace.
$ ansible-galaxy collection init mynamespace.mycollection
This creates the collection directory structure.
- Copy the standalone role directory into the
roles/subdirectory of the collection. Roles in collections cannot have hyphens in the role name. Rename any such roles to use underscores instead.
$ mkdir mynamespace/mycollection/roles/my_role/
$ cp -r /path/to/standalone/role/mynamespace/my_role/\* mynamespace/mycollection/roles/my_role/
- Update
galaxy.ymlto include any role dependencies. - Update the collection README.md file to add links to any role README.md files.
Migrating a role that contains plugins to a collection
To migrate from a standalone role that has plugins to a collection role:
- Create a local
ansible_collections directoryandcdto this new directory. - Create a collection. If you want to import this collection to Ansible Galaxy, you need a Galaxy namespace.
$ ansible-galaxy collection init mynamespace.mycollection
This creates the collection directory structure.
- Copy the standalone role directory into the
roles/subdirectory of the collection. Roles in collections cannot have hyphens in the role name. Rename any such roles to use underscores instead.
$ mkdir mynamespace/mycollection/roles/my_role/
$ cp -r /path/to/standalone/role/mynamespace/my_role/\* mynamespace/mycollection/roles/my_role/
- Move any modules to the
plugins/modules/directory.
$ mv -r mynamespace/mycollection/roles/my_role/library/\* mynamespace/mycollection/plugins/modules/
- Move any other plugins to the appropriate
plugins/PLUGINTYPE/directory. Seemigrating_plugins_collectionfor additional steps that may be required. - Update
galaxy.ymlto include any role dependencies. - Update the collection README.md file to add links to any role README.md files.
- Change any references to the role to use the
FQCN (Fully Qualified Collection Name).
---
- name: example role by FQCN
hosts: some_host_pattern
tasks:
- name: import FQCN role from a collection
import_role:
name: mynamespace.mycollection.my_role
You can alternately use the collections keyword to simplify this:
---
- name: example role by FQCN
hosts: some_host_pattern
collections:
- mynamespace.mycollection
tasks:
- name: import role from a collection
import_role:
name: my_role
Migrating other role plugins to a collection
To migrate other role plugins to a collection:
- Move each nonmodule plugins to the appropriate
plugins/PLUGINTYPE/directory. Themynamespace/mycollection/plugins/README.mdfile explains the types of plugins that the collection can contain within optionally created subdirectories.
$ mv -r mynamespace/mycollection/roles/my_role/filter_plugins/\* mynamespace/mycollection/plugins/filter/
- Update documentation to use the FQCN. Plugins that use
doc_fragmentsneed to use FQCN (for example,mydocfragbecomesmynamespace.mycollection.mydocfrag). - Update relative imports work in collections to start with a period. For example,
./filenameand../asdfu/filestuffworks butfilenamein same directory must be updated to./filename.
If you have a custom module_utils or import from __init__.py, you must also:
- Change the Python namespace for custom
module_utilsto use theFQCN (Fully Qualified Collection Name)along with theansible_collectionsconvention. Seeupdate_module_utils_role. - Change how you import from
__init__.py. Seeupdate_init_role.
Updating module_utils
If any of your custom modules use a custom module utility, once you migrate to a collection you cannot address the module utility in the top level ansible.module_utils Python namespace. Ansible does not merge content from collections into the Ansible internal Python namespace. Update any Python import statements that refer to custom module utilities when you migrate your custom content to collections. See module_utils in collections <collection_module_utils> for more details.
When coding with module_utils in a collection, the Python import statement needs to take into account the FQCN (Fully Qualified Collection Name) along with the ansible_collections convention. The resulting Python import looks similar to the following example:
from ansible_collections.{namespace}.{collectionname}.plugins.module_utils.{util} import {something}
You need to follow the same rules in changing paths and using namespaced names for subclassed plugins.
The following example code snippets show a Python and a PowerShell module using both default Ansible module_utils and those provided by a collection. In this example the namespace is ansible_example and the collection is community.
In the Python example the module_utils is helper and the FQCN (Fully Qualified Collection Name) is ansible_example.community.plugins.module_utils.helper:
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible_collections.ansible_example.community.plugins.module_utils.helper import HelperRequest
argspec = dict(
name=dict(required=True, type='str'),
state=dict(choices=['present', 'absent'], required=True),
)
module = AnsibleModule(
argument_spec=argspec,
supports_check_mode=True
)
_request = HelperRequest(
module,
headers={"Content-Type": "application/json"},
data=data
)
In the PowerShell example the module_utils is hyperv and the FQCN (Fully Qualified Collection Name) is ansible_example.community.plugins.module_utils.hyperv:
#!powershell
#AnsibleRequires -CSharpUtil Ansible.Basic
#AnsibleRequires -PowerShell ansible_collections.ansible_example.community.plugins.module_utils.hyperv
$spec = @{
name = @{ required = $true; type = "str" }
state = @{ required = $true; choices = @("present", "absent") }
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
Invoke-HyperVFunction -Name $module.Params.name
$module.ExitJson()
Importing from __init__.py
Because of the way that the CPython interpreter does imports, combined with the way the Ansible plugin loader works, if your custom embedded module or plugin requires importing something from an __init__.py file, that also becomes part of your collection. You can either originate the content inside a standalone role or use the file name in the Python import statement. The following example is an __init__.py file that is part of a callback plugin found inside a collection named ansible_example.community.
from ansible_collections.ansible_example.community.plugins.callback.__init__ import CustomBaseClass
Example: Migrating a standalone role with plugins to a collection
In this example we have a standalone role called my-standalone-role.webapp to emulate a standalone role that contains dashes in the name (which is not valid in collections). This standalone role contains a custom module in the library/ directory called manage_webserver.
my-standalone-role.webapp
├── defaults
├── files
├── handlers
├── library
├── meta
├── tasks
├── templates
├── tests
└── vars
- Create a new collection, for example,
acme.webserver:
$ ansible-galaxy collection init acme.webserver
- Collection acme.webserver was created successfully
$ tree acme -d 1
acme
└── webserver
├── docs
├── plugins
└── roles
- Create the
webapprole inside the collection and copy all contents from the standalone role:
$ mkdir acme/webserver/roles/webapp
$ cp my-standalone-role.webapp/* acme/webserver/roles/webapp/
- Move the
manage_webservermodule to its new home inacme/webserver/plugins/modules/:
$ cp my-standalone-role.webapp/library/manage_webserver.py acme/webserver/plugins/modules/manage.py
This example changed the original source file manage_webserver.py to the destination file manage.py. This is optional but the FQCN (Fully Qualified Collection Name) provides the webserver context as acme.webserver.manage.
- Change
manage_webservertoacme.webserver.manageintasks/files in the role ( for example,my-standalone-role.webapp/tasks/main.yml) and any use of the original module name.
This name change is only required if you changed the original module name, but illustrates content referenced by FQCN (Fully Qualified Collection Name) can offer context and in turn can make module and plugin names shorter. If you anticipate using these modules independent of the role, keep the original naming conventions. Users can add the collections keyword <collections_using_playbook> in their playbooks. Typically roles are an abstraction layer and users won't use components of the role independently.
Example: Supporting standalone roles and migrated collection roles in a downstream RPM
A standalone role can co-exist with its collection role counterpart (for example, as part of a support lifecycle of a product). This should only be done for a transition period, but these two can exist in downstream in packages such as RPMs. For example, the RHEL system roles could coexist with an example of a RHEL system roles collection and provide existing backwards compatibility with the downstream RPM.
This section walks through an example creating this coexistence in a downstream RPM and requires Ansible 2.9.0 or later.
To deliver a role as both a standalone role and a collection role:
- Place the collection in
/usr/share/ansible/collections/ansible_collections/. - Copy the contents of the role inside the collection into a directory named after the standalone role and place the standalone role in
/usr/share/ansible/roles/.
All previously bundled modules and plugins used in the standalone role are now referenced by FQCN (Fully Qualified Collection Name) so even though they are no longer embedded, they can be found from the collection contents. This is an example of how the content inside the collection is a unique entity and does not have to be bound to a role or otherwise. You could alternately create two separate collections: one for the modules and plugins and another for the standalone role to migrate to. The role must use the modules and plugins as FQCN (Fully Qualified Collection Name).
The following is an example RPM spec file that accomplishes this using this example content:
Name: acme-ansible-content
Summary: Ansible Collection for deploying and configuring ACME webapp
Version: 1.0.0
Release: 1%{?dist}
License: GPLv3+
Source0: acme-webserver-1.0.0.tar.gz
Url: https://github.com/acme/webserver-ansible-collection
BuildArch: noarch
%global roleprefix my-standalone-role.
%global collection_namespace acme
%global collection_name webserver
%global collection_dir %{_datadir}/ansible/collections/ansible_collections/%{collection_namespace}/%{collection_name}
%description
Ansible Collection and standalone role (for backward compatibility and migration) to deploy, configure, and manage the ACME webapp software.
%prep
%setup -qc
%build
%install
mkdir -p %{buildroot}/%{collection_dir}
cp -r ./* %{buildroot}/%{collection_dir}/
mkdir -p %{buildroot}/%{_datadir}/ansible/roles
for role in %{buildroot}/%{collection_dir}/roles/*
do
cp -pR ${role} %{buildroot}/%{_datadir}/ansible/roles/%{roleprefix}$(basename ${role})
mkdir -p %{buildroot}/%{_pkgdocdir}/$(basename ${role})
for docfile in README.md COPYING LICENSE
do
if [ -f ${role}/${docfile} ]
then
cp -p ${role}/${docfile} %{buildroot}/%{_pkgdocdir}/$(basename ${role})/${docfile}
fi
done
done
%files
%dir %{_datadir}/ansible
%dir %{_datadir}/ansible/roles
%dir %{_datadir}/ansible/collections
%dir %{_datadir}/ansible/collections/ansible_collections
%{_datadir}/ansible/roles/
%doc %{_pkgdocdir}/*/README.md
%doc %{_datadir}/ansible/roles/%{roleprefix}*/README.md
%{collection_dir}
%doc %{collection_dir}/roles/*/README.md
%license %{_pkgdocdir}/*/COPYING
%license %{_pkgdocdir}/*/LICENSE
Using ansible.legacy to access local custom modules from collections-based roles
Some roles within a collection use local custom modules <developing_locally> that are not part of the collection itself. If there is a conflict between the custom module short name and the collection module name, you need to specify which module your tasks call. You can update the tasks to change local_module_name to ansible.legacy.local_module_name to ensure you are using the custom module.
The lifecycle of an Ansible module or plugin
Modules and plugins in the main Ansible repository have a defined life cycle, from the first introduction to final removal. The module and plugin lifecycle is tied to the Ansible release cycle <release_cycle>. A module or plugin may move through these four stages:
- When a module or plugin is first accepted into Ansible, we consider it in tech preview and will mark it as such in the documentation.
- If a module or plugin matures, the 'preview' mark in the documentation is removed. Backward compatibility for these modules and plugins is maintained but not guaranteed, which means their parameters should be maintained with stable meanings.
- If a module's or plugin's target API changes radically, or if someone creates a better implementation of its functionality, we may mark it deprecated. Modules and plugins that are deprecated are still available but they are reaching the end of their life cycle. We retain deprecated modules and plugins for 4 release cycles with deprecation warnings to help users update playbooks and roles that use them.
- When a module or plugin has been deprecated for four release cycles, it is removed and replaced with a tombstone entry in the routing configuration. Modules and plugins that are removed are no longer shipped with Ansible. The tombstone entry helps users find alternative modules and plugins.
For modules and plugins in collections, the lifecycle is similar. Since ansible-base 2.10, it is no longer possible to mark modules as 'preview' or 'stable'.
Deprecating modules and plugins in the Ansible main repository
To deprecate a module in ansible-core, you must:
- Rename the file so it starts with an
_, for example, renameold_cloud.pyto_old_cloud.py. This keeps the module available and marks it as deprecated on the module index pages. - Mention the deprecation in the relevant changelog (by creating a changelog fragment with a section
deprecated_features). - Reference the deprecation in the relevant
porting_guide_core_x.y.rst. - Add
deprecated:to the documentation with the following sub-values:
removed_in
Astring, such as"2.10"; the version of Ansible where the module will be replaced with a docs-only module stub. Usually current release +4. Mutually exclusive withremoved_at_date:.removed_at_date
(Added in ansible-base 2.10). An ISO 8601 formatted date when the module will be removed. Usually 2 years from the date the module is deprecated. Mutually exclusive withremoved_in:.why
Optional string that used to detail why this has been removed.alternatives
Inform users they should do instead, for example,Use M(whatmoduletouseinstead) instead..
- For an example of documenting deprecation, see this PR that deprecates multiple modules. Some of the elements in the PR might now be out of date.
Deprecating modules and plugins in a collection
To deprecate a module in a collection, you must:
-
Add a
deprecationentry toplugin_routinginmeta/runtime.yml. For example, to deprecate the moduleold_cloud, add:plugin_routing: modules: old_cloud: deprecation: removal_version: 2.0.0 warning_text: Use foo.bar.new_cloud instead.For other plugin types, you have to replace
modules:with<plugin_type>:, for examplelookup:for lookup plugins. When deprecating action plugins, you need to add two entries: one for the action plugin and one for the module file that contains the documentation.Instead of
removal_version, you can also useremoval_datewith an ISO 8601 formatted date after which the module will be removed in a new major version of the collection. -
Mention the deprecation in the relevant changelog. If the collection uses
antsibull-changelog, create a changelog fragment with a sectiondeprecated_features. -
Add
deprecated:to the documentation of the module or plugin with the following sub-values:
removed_in
Astring, such as"2.10"; the version of Ansible where the module will be replaced with a docs-only module stub. Usually current release +4. Mutually exclusive withremoved_at_date:.removed_at_date
(Added in ansible-base 2.10). An ISO 8601 formatted date when the module will be removed. Usually 2 years from the date the module is deprecated. Mutually exclusive withremoved_in:.why
String that used to detail why this has been removed.alternative
Inform users they should do instead, for example,Use M(whatmoduletouseinstead) instead.. Seemodule_documents_linkingfor ways to reference entities other than modules.
Changing a module or plugin name in the Ansible main repository
You can also rename a module and keep a deprecated alias to the old name by using a symlink that starts with _. This example allows the stat module to be called with fileinfo, making the following examples equivalent:
ln -s stat.py _fileinfo.py
ansible -m stat -a "path=/tmp" localhost
ansible -m fileinfo -a "path=/tmp" localhost
Renaming a module or plugin in a collection, or redirecting a module or plugin to another collection
To rename a module or plugin in a collection, or to redirect a module or plugin to another collection, you need to add a redirect entry to plugin_routing in meta/runtime.yml. For example, to redirect the module old_cloud to foo.bar.new_cloud, add:
plugin_routing:
modules:
old_cloud:
redirect: foo.bar.new_cloud
If you want to deprecate the old name, add a deprecation: entry (see above):
plugin_routing:
modules:
old_cloud:
redirect: foo.bar.new_cloud
deprecation:
removal_version: 2.0.0
warning_text: Use foo.bar.new_cloud instead.
You need to use the Fully Qualified Collection Name (FQCN) of the new module/plugin name, even if it is located in the same collection as the redirect. By using a FQCN from another collection, you redirect the module/plugin to that collection.
If you need to support Ansible 2.9, please note that Ansible 2.9 does not know about meta/runtime.yml. With Ansible 2.9 you can still rename plugins and modules inside one collection by using symbolic links. Note that ansible-base 2.10, ansible-core 2.11, and newer will prefer meta/runtime.yml entries over symbolic links.
Tombstoning a module or plugin in a collection
To remove a deprecated module or plugin from a collection, you need to tombstone it:
-
Remove the module or plugin file with related files like tests, documentation references, and documentation.
-
Add a tombstone entry in
meta/runtime.yml. For example, to tombstone the moduleold_cloud, add:plugin_routing: modules: old_cloud: tombstone: removal_version: 2.0.0 warning_text: Use foo.bar.new_cloud instead.Instead of
removal_version, you can also useremoval_datewith an ISO 8601 formatted date. The date should be the date of the next major release.
Ansible architecture
Ansible is a radically simple IT automation engine that automates cloud provisioning, configuration management, application deployment, intra-service orchestration, and many other IT needs.
Being designed for multi-tier deployments since day one, Ansible models your IT infrastructure by describing how all of your systems inter-relate, rather than just managing one system at a time.
It uses no agents and no additional custom security infrastructure, so it is easy to deploy - and most importantly, it uses a very simple language (YAML, in the form of Ansible Playbooks) that allows you to describe your automation jobs in a way that approaches plain English.
In this section, we'll give you a really quick overview of how Ansible works so you can see how the pieces fit together.
Modules
Ansible works by connecting to your nodes and pushing out scripts called "Ansible modules" to them. Most modules accept parameters that describe the desired state of the system. Ansible then executes these modules (over SSH by default), and removes them when finished. Your library of modules can reside on any machine, and there are no servers, daemons, or databases required.
You can write your own modules <developing_modules_general>, though you should first consider whether you should <developing_modules>. Typically you'll work with your favorite terminal program, a text editor, and probably a version control system to keep track of changes to your content. You may write specialized modules in any language that can return JSON (Ruby, Python, bash, and so on).
Module utilities
When multiple modules use the same code, Ansible stores those functions as module utilities to minimize duplication and maintenance. For example, the code that parses URLs is lib/ansible/module_utils/url.py. You can write your own module utilities <developing_module_utilities> as well. Module utilities may only be written in Python or in PowerShell.
Plugins
Plugins <plugins_lookup> augment Ansible's core functionality. While modules execute on the target system in separate processes (usually that means on a remote system), plugins execute on the control node within the /usr/bin/ansible process. Plugins offer options and extensions for the core features of Ansible - transforming data, logging output, connecting to inventory, and more. Ansible ships with a number of handy plugins, and you can easily write your own <developing_plugins>. For example, you can write an inventory plugin <developing_inventory> to connect to any datasource that returns JSON. Plugins must be written in Python.
Inventory
By default, Ansible represents the machines it manages in a file (INI, YAML, and so on) that puts all of your managed machines in groups of your own choosing.
To add new machines, there is no additional SSL signing server involved, so there's never any hassle deciding why a particular machine didn't get linked up due to obscure NTP or DNS issues.
If there's another source of truth in your infrastructure, Ansible can also connect to that. Ansible can draw inventory, group, and variable information from sources like EC2, Rackspace, OpenStack, and more.
Here's what a plain text inventory file looks like:
[webservers]
www1.example.com
www2.example.com
[dbservers]
db0.example.com
db1.example.com
Once inventory hosts are listed, variables can be assigned to them in simple text files (in a subdirectory called 'group_vars/' or 'host_vars/') or directly in the inventory file.
Or, as already mentioned, use a dynamic inventory to pull your inventory from data sources like EC2, Rackspace, or OpenStack.
Playbooks
Playbooks can finely orchestrate multiple slices of your infrastructure topology, with very detailed control over how many machines to tackle at a time. This is where Ansible starts to get most interesting.
Ansible's approach to orchestration is one of finely-tuned simplicity, as we believe your automation code should make perfect sense to you years down the road and there should be very little to remember about special syntax or features.
Here's what a simple playbook looks like:
---
- hosts: webservers
serial: 5 # update 5 machines at a time
roles:
- common
- webapp
- hosts: content_servers
roles:
- common
- content
The Ansible search path
Modules, module utilities, plugins, playbooks, and roles can live in multiple locations. If you write your own code to extend Ansible's core features, you may have multiple files with similar or the same names in different locations on your Ansible control node. The search path determines which of these files Ansible will discover and use on any given playbook run.
Ansible's search path grows incrementally over a run. As Ansible finds each playbook and role included in a given run, it appends any directories related to that playbook or role to the search path. Those directories remain in scope for the duration of the run, even after the playbook or role has finished executing. Ansible loads modules, module utilities, and plugins in this order:
-
Directories adjacent to a playbook specified on the command line. If you run Ansible with
ansible-playbook /path/to/play.yml, Ansible appends these directories if they exist:/path/to/modules /path/to/module_utils /path/to/plugins -
Directories adjacent to a playbook that is statically imported by a playbook specified on the command line. If
play.ymlincludes- import_playbook: /path/to/subdir/play1.yml, Ansible appends these directories if they exist:/path/to/subdir/modules /path/to/subdir/module_utils /path/to/subdir/plugins -
Subdirectories of a role directory referenced by a playbook. If
play.ymlrunsmyrole, Ansible appends these directories if they exist:/path/to/roles/myrole/modules /path/to/roles/myrole/module_utils /path/to/roles/myrole/plugins -
Directories specified as default paths in
ansible.cfgor by the related environment variables, including the paths for the various plugin types. Seeansible_configuration_settingsfor more information. Sampleansible.cfgfields:DEFAULT_MODULE_PATH DEFAULT_MODULE_UTILS_PATH DEFAULT_CACHE_PLUGIN_PATH DEFAULT_FILTER_PLUGIN_PATHSample environment variables:
ANSIBLE_LIBRARY ANSIBLE_MODULE_UTILS ANSIBLE_CACHE_PLUGINS ANSIBLE_FILTER_PLUGINS -
The standard directories that ship as part of the Ansible distribution.
Modules, module utilities, and plugins in user-specified directories will override the standard versions. This includes some files with generic names. For example, if you have a file named basic.py in a user-specified directory, it will override the standard ansible.module_utils.basic.
If you have more than one module, module utility, or plugin with the same name in different user-specified directories, the order of commands at the command line and the order of includes and roles in each play will affect which one is found and used on that particular play.
Adjacent YAML documentation files
YAML documentation for plugins
For most Ansible plugins, the documentation is in the same file as the code. This approach does not work for cases when:
- Multiple plugins are defined in the same file, such as tests and filters.
- Plugins are written in a language other than Python (modules).
These cases require plugins to provide documentation in an adjacent .py file. As of ansible-core 2.14, you can provide documentation as adjacent YAML files instead. The format of a YAML documentation file is nearly identical to its Python equivalent, except it is pure YAML.
YAML format
In Python, each section is a variable DOCUMENTATION = r""" ... """ while in YAML, it is a mapping key DOCUMENTATION: ....
Here is a longer example that shows documentation as embedded in a Python file:
DOCUMENTATION = r'''
description: something
options:
option_name:
description: describe this config option
default: default value for this config option
env:
- name: NAME_OF_ENV_VAR
ini:
- section: section_of_ansible.cfg_where_this_config_option_is_defined
key: key_used_in_ansible.cfg
vars:
- name: name_of_ansible_var
- name: name_of_second_var
version_added: X.x
required: True/False
type: boolean/float/integer/list/none/path/pathlist/pathspec/string/tmppath
version_added: X.x
'''
EXAMPLES = r'''
# TODO: write examples
'''
This example shows the same documentation in YAML format:
DOCUMENTATION:
description: something
options:
option_name:
description: describe this config option
default: default value for this config option
env:
- name: NAME_OF_ENV_VAR
ini:
- section: section_of_ansible.cfg_where_this_config_option_is_defined
key: key_used_in_ansible.cfg
vars:
- name: name_of_ansible_var
- name: name_of_second_var
version_added: X.x
required: True/False
type: boolean/float/integer/list/none/path/pathlist/pathspec/string/tmppath
version_added: X.x
EXAMPLES: # TODO: write examples
As the examples above show, Python variables already contain YAML. The main change to use YAML documentation is to simply move the YAML out of such variables.
Any adjacent YAML documentation files must be in the same directory as the plugin or module that they document. This means the documentation is available in any directory that contains the plugins or modules.
Supported plugin types
YAML documentation is mainly intended for filters, tests, and modules. While it is possible to use it with other plugin types, Ansible always recommends having documentation in the same file as the code for most cases.
list_of_collections
Browse existing collections, modules, and plugins
developing_api
Learn about the Python API for task execution
developing_inventory
Learn about how to develop dynamic inventory sources
developing_modules_general
Learn about how to write Ansible modules
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Basic rules
Use standard American English
Ansible uses Standard American English. Watch for common words that are spelled differently in American English (color vs colour, organize vs organise, and so on).
Write for a global audience
Everything you say should be understandable by people of different backgrounds and cultures. Avoid idioms and regionalism and maintain a neutral tone that cannot be misinterpreted. Avoid figurative language, metaphors, and humor.
Follow naming conventions
Always follow naming conventions and trademarks.
Use clear sentence structure
Clear sentence structure means:
- Focus each sentence on a single idea.
- Start with the important information first.
- Avoid padding/adding extra words that make the sentence harder to understand.
- Keep it short - Longer sentences are harder to understand.
- Do not start a sentence with a lowercase term. Rewrite the sentence if needed.
Some examples of improving sentences:
Bad:
The unwise walking about upon the area near the cliff edge may result in a dangerous fall and therefore it is recommended that one remains a safe distance to maintain personal safety.
Better:
Danger! Stay away from the cliff.
Bad:
Furthermore, large volumes of water are also required for the process of extraction.
Better:
Extraction also requires large volumes of water.
Use one sentence per line
Consider using a technique called one sentence per line when composing paragraphs, or use at least one line per sentence (following https://sembr.org/). This technique allows you to format RST and Markdown source in a natural and semantic way that is easy to review and edit.
Lines of text that occur next to each other are rendered as a paragraph so readers do not notice the line breaks between sentences. To create a new paragraph, add an empty line between sentences. You can still soft wrap lines of text when using one sentence per line. The important thing is that each line starts at the left margin.
Using one sentence per line reduces cognitive load and makes life much easier for people who review changes. Diffs become much more straightforward and easier to parse. For example, when multiple sentences are wrapped at a fixed column width, a change at the start of a paragraph causes the remaining lines in the paragraph to reposition.
In addition to the advantages for version control, one sentence per line makes it easier to do the following:
- Swap sentences around.
- Separate or join paragraphs.
- Comment individual sentences.
- Identify sentences that are too long or that vary widely in length.
- Spot redundant patterns in your writing.
For an example of the one sentence per line in the Ansible documentation, take a look at the RST source under docs/docsite/rst/getting_started/.
Avoid verbosity
Write short, succinct sentences. Avoid terms like:
- "...as has been said before,"
- "..each and every,"
- "...point in time,"
- "...in order to,"
Avoid negative constructions
Where possible, state what something is or does rather than what it is not. Positive statements are clearer and easier to translate.
Bad:
Do not close the editor without saving.
Better:
Save the file before closing the editor.
Preserve existing headings and anchors
Do not change existing headings or RST anchors when editing existing documentation.
Highlight menu items and commands
When documenting menus or commands, it helps to bold what is important.
For code or command snippets, use the RST code-block directive:
.. code-block:: bash
ssh my_vyos_user@vyos.example.net
show config
Grammar and Punctuation
Common Styles and Usage, and Common Mistakes
Ansible
- Write "Ansible." Not "Ansible, Inc." or "AnsibleWorks The only exceptions to this rule are when we're writing legal or financial statements.
- Never use the logotype by itself in body text. Always keep the same font you are using the rest of the sentence.
- A company is singular in the US. In other words, Ansible is an "it," not a "they."
Capitalization
If it is not a real product, service, or department at Ansible, don't capitalize it. Not even if it seems important. Capitalize only the first letter of the first word in headlines.
Do not use uppercase letters for emphasis.
Colon
A colon is generally used before a list or series: - The Triangle Area consists of three cities: Raleigh, Durham, and Chapel Hill.
But not if the list is a complement or object of an element in the sentence: - Before going on vacation, be sure to (1) set the alarm, (2) cancel the newspaper, and (3) ask a neighbor to collect your mail.
Use a colon after "as follows" and "the following" if the related list comes immediately after: wedge The steps for changing directories are as follows:
- Open a terminal.
- Type cd...
Use a colon to introduce a bullet list (or dash, or icon/symbol of your choice):
In the Properties dialog box, you'll find the following entries:
- Connection name
- Count
- Cost per item
Commas
Use serial commas, the comma before the "and" in a series of three or more items:
- "Item 1, item 2, and item 3."
It is easier to read that way and helps avoid confusion. The primary exception to this you will see is in PR, where it is traditional not to use serial commas because it is often the style of journalists.
Commas are always important, considering the vast difference in meanings of the following two statements.
- Let's eat, Grandma
- Let's eat Grandma.
Correct punctuation could save Grandma's life.
If that does not convince you, maybe this will:

Contractions
Do not use contractions in Ansible documents.
Em dashes
When possible, use em-dashes with no space on either side. When full em-dashes aren't available, use double-dashes with no spaces on either side--like this.
A pair of em dashes can be used in place of commas to enhance readability. Note, however, that dashes are always more emphatic than commas.
A pair of em dashes can replace a pair of parentheses. Dashes are considered less formal than parentheses; they are also more intrusive. If you want to draw attention to the parenthetical content, use dashes. If you want to include the parenthetical content more subtly, use parentheses.
When dashes are used in place of parentheses, surrounding punctuation should be omitted. Compare the following examples.
Upon discovering the errors (all 124 of them), the publisher immediately recalled the books.
Upon discovering the errors—all 124 of them—the publisher immediately recalled the books.
When used in place of parentheses at the end of a sentence, only a single dash is used.
After three weeks on set, the cast was fed up with his direction (or, rather, lack of direction).
After three weeks on set, the cast was fed up with his direction—or, rather, lack of direction.
Exclamation points (!)
Do not use them at the end of sentences. An exclamation point can be used when referring to a command, such as the bang (!) command.
Gender References
Do not use gender-specific pronouns in documentation. It is far less awkward to read a sentence that uses "they" and "their" rather than "he/she" and "his/hers."
It is fine to use "you" when giving instructions and "the user," "new users," and so on. in more general explanations.
Never use "one" in place of "you" when writing technical documentation. Using "one" is far too formal.
Never use "we" when writing. "We" aren't doing anything on the user side. Ansible's products are doing the work as requested by the user.
Hyphen
The hyphen's primary function is the formation of certain compound terms. Do not use a hyphen unless it serves a purpose. If a compound adjective cannot be misread or, as with many psychological terms, its meaning is established, a hyphen is not necessary.
Use hyphens to avoid ambiguity or confusion:
a little-used car
a little used-car
cross complaint
cross-complaint
high-school girl
high schoolgirl
fine-tooth comb (most people do not comb their teeth)
third-world war
third world war

In professionally printed material (particularly books, magazines, and newspapers), the hyphen is used to divide words between the end of one line and the beginning of the next. This allows for an evenly aligned right margin without highly variable (and distracting) word spacing.
Lists
Introduce every list with a complete sentence. Use bulleted lists for unordered items and numbered lists for sequential steps.
Keep the structure of bulleted lists equivalent and consistent. If one bullet is a verb phrase, they should all be verb phrases. If one is a complete sentence, they should all be complete sentences, and so on.
Capitalize the first word of each bullet. Unless it is obvious that it is just a list of items, such as a list of items like: * computer * monitor * keyboard * mouse
When the bulleted list appears within the context of other copy, (unless it is a straight list like the previous example) add periods, even if the bullets are sentence fragments. Part of the reason behind this is that each bullet is said to complete the original sentence.
In some cases where the bullets are appearing independently, such as in a poster or a homepage promotion, they do not need periods.
When giving instructional steps, use numbered lists instead of bulleted lists.
Months and States
Abbreviate months and states according to AP. Months are only abbreviated if they are used in conjunction with a day. Example: "The President visited in January 1999." or "The President visited Jan. 12."
Months: Jan., Feb., March, April, May, June, July, Aug., Sept., Nov., Dec.
States: Ala., Ariz., Ark., Calif., Colo., Conn., Del., Fla., Ga., Ill., Ind., Kan., Ky., La., Md., Mass., Mich., Minn., Miss., Mo., Mont., Neb., Nev., NH, NJ, NM, NY, NC, ND, Okla., Ore., Pa., RI, SC, SD, Tenn., Vt., Va., Wash., W.Va., Wis., Wyo.
Numbers
Numbers between one and nine are written out. 10 and above are numerals. The exception to this is writing "4 million" or "4 GB." It is also acceptable to use numerals in tables and charts.
Only
Place the word "only" immediately before the word or phrase that it modifies.
Phone Numbers
Phone number style: 1 (919) 555-0123 x002 and 1 888-GOTTEXT
Procedures
Include one action per procedure step. Start each step with an imperative verb. For procedures with many steps, include a summary at the start that explains the overall goal or the groups of steps required. Do not include system responses or expected output as separate numbered steps. Describe expected results within the step or as unnumbered text after the step. For example:
- Run the
ansible-playbookcommand. The output displays a list of completed tasks. - Verify the results.
Pronouns
Ensure every pronoun refers to a single, clear noun. If the reference is ambiguous, repeat the noun instead. Place pronouns as close as possible to the noun they reference.
Quotations (Using Quotation Marks and Writing Quotes)
"Place the punctuation inside the quotes," the editor said.
Except in rare instances, use only "said" or "says" because anything else just gets in the way of the quote itself, and also tends to editorialize.
Place the name first right after the quote:
"I like to write first-person because I like to become the character I'm writing," Wally Lamb said.
Not:
"I like to write first-person because I like to become the character I'm writing," said Wally Lamb.
Semicolon
Use a semicolon to separate items in a series if the items contain commas:
- Everyday I have coffee, toast, and fruit for breakfast; a salad for lunch; and a peanut butter sandwich, cookies, ice cream, and chocolate cake for dinner.
Use a semicolon before a conjunctive adverb (however, therefore, otherwise, namely, for example, and so on): - I think; therefore, I am.
Spacing after sentences
Use only a single space after a sentence.
One sentence per line is preferred to wrapping sentences over multiple lines. See one_sentence_per_line.
That
Include the word "that" in subordinate clauses for clarity, especially for global audiences and translation. For example, write "Verify that the file exists" rather than "Verify the file exists."
Time
- Time of day is written as "4 p.m."
Ansible documentation style guide
Welcome to the Ansible style guide! To create clear, concise, consistent, useful materials on docs.ansible.com, follow these guidelines:
Linguistic guidelines
We want the Ansible documentation to be:
- clear
- direct
- conversational
- easy to translate
We want reading the docs to feel like having an experienced, friendly colleague explain how Ansible works.
Stylistic cheat-sheet
This cheat-sheet illustrates a few rules that help achieve the "Ansible tone":
| Rule | Good example | Bad example |
|---|---|---|
| Use active voice | You can run a task by | A task can be run by |
| Use the present tense | This command creates a | This command will create a |
| Address the reader | As you expand your inventory | When the number of managed nodes grows |
| Use standard English | Return to this page | Hop back to this page |
| Use American English | The color of the output | The colour of the output |
Title and heading case
Titles and headings should be written in sentence case. For example, this section's title is Title and heading case, not Title and Heading Case or TITLE AND HEADING CASE.
Use gerunds (-ing words) for high-level task headings and imperative verbs for subtask headings. For example, use Installing Ansible for a chapter heading and Install the package for a step-level heading within it.
Avoid using Latin phrases
Latin words and phrases like e.g. or etc. are easily understood by English speakers. They may be harder to understand for others and are also tricky for automated translation.
Use the following English terms in place of Latin terms or abbreviations:
| Latin | English |
|---|---|
| i.e | in other words |
| e.g. | for example |
| etc | and so on |
| via | by/ through |
| vs./versus | rather than/against |
reStructuredText guidelines
The Ansible documentation is written in reStructuredText and processed by Sphinx. We follow these technical or mechanical guidelines on all rST pages:
Heading notation
Section headings in reStructuredText can use a variety of notations. Sphinx will 'learn on the fly' when creating a hierarchy of headings. To make our documents easy to read and to edit, we follow a standard set of heading notations. We use:
###with overline, for parts:
###############
Developer guide
###############
***with overline, for chapters:
*******************
Ansible style guide
*******************
===for sections:
Mechanical guidelines
=====================
---for subsections:
Internal navigation
-------------------
^^^for sub-subsections:
Adding anchors
^^^^^^^^^^^^^^
"""for paragraphs:
Paragraph that needs a title
""""""""""""""""""""""""""""
Syntax highlighting - Pygments
The Ansible documentation supports a range of Pygments lexers for syntax highlighting to make our code examples look good. Each code-block must be correctly indented and surrounded by blank lines.
The Ansible documentation allows the following values:
- none (no highlighting)
- ansible-output (a custom lexer for Ansible output)
- bash
- console
- csharp
- diff
- ini
- jinja
- json
- md
- powershell
- python
- rst
- sh
- shell
- shell-session
- text
- yaml
- yaml+jinja
For example, you can highlight Python code using following syntax:
.. code-block:: python
def my_beautiful_python_code():
pass
Internal navigation
Anchors (also called labels) and links work together to help users find related content. Local tables of contents also help users navigate quickly to the information they need. All internal links should use the :ref: syntax. Every page should have at least one anchor to support internal :ref: links. Long pages, or pages with multiple levels of headings, can also include a local TOC.
Avoid raw URLs. RST and sphinx allow https://my.example.com, but this is unhelpful for those using screen readers. :ref: links automatically pick up the heading from the anchor, but for external links, always use the \`link title <link-url>\`_ format.
Adding anchors
- Include at least one anchor on every page
- Place the main anchor above the main heading
- If the file has a unique title, use that for the main page anchor:
.. _unique_page::
- You may also add anchors elsewhere on the page
Adding internal links
- All internal links must use
:ref:syntax. These links both point to the anchor defined above:
:ref:`unique_page`
:ref:`this page <unique_page>`
The second example adds custom text for the link.
Adding links to modules and plugins
Use the :ansplugin: RST role to link to modules and plugins using their Fully Qualified Collection Name (FQCN):
The ansible.builtin.copy module can be linked with
:ansplugin:`ansible.builtin.copy#module`
If you want to specify an explicit type, use:
:ansplugin:`the copy module <ansible.builtin.copy#module>`
This displays as "The ansible.builtin.copy module can be linked with ansible.builtin.copy#module" and "If you want to specify an explicit type, use: the copy module <ansible.builtin.copy#module>".
Instead of #module, you can also specify #<plugin_type> to reference to a plugin of type <plugin_type>:
:ansplugin:`arista.eos.eos_config <arista.eos.eos_config#module>`
:ansplugin:`kubernetes.core.kubectl connection plugin <kubernetes.core.kubectl#connection>`
:ansplugin:`ansible.builtin.file lookup plugin <ansible.builtin.file#lookup>`
ansible.builtin is the FQCN for modules included in ansible-core.
Adding links to module and plugin options and return values
Use the :ansopt: and :ansretval: roles to reference options and return values of modules and plugins while showing the option's resp. return value's name and optionally a value. Use the :ansoptref: and :ansretvalref: roles to reference options and return values of modules and plugins while displaying a provided title.
The following example shows their usage:
:ansopt:`ansible.builtin.file#module:path` references the ``path`` parameter of the
``ansible.builtin.file`` module; :ansopt:`ansible.builtin.file#module:path=/root/.ssh/known_hosts`
shows the assignment ``path=/root/.ssh/known_hosts`` as a clickable link.
You can :ansoptref:`provide a path <ansible.builtin.file#module:path>`
to the :ansplugin:`ansible.builtin.file#module`; its value is
:ansretvalref:`returned as a return value <ansible.builtin.file#module:path>`.
:ansretval:`ansible.builtin.stat#module:stat.exists` references the ``stat.exists`` return value
of the ``ansible.builtin.stat`` module. You can also use ``=`` as for option values:
:ansretval:`ansible.builtin.stat#module:stat.exists=true` shows ``stat.exists=true``.
:ansopt:`foo` and :ansopt:`foo=bar` use the same markup for an option and an option
assignment without a link; the same is true for return values: :ansretval:`foo` and
:ansretval:`foo=bar`.
This displays as:
ansible.builtin.file#module:pathreferences thepathparameter of theansible.builtin.filemodule;ansible.builtin.file#module:path=/root/.ssh/known_hostsshows the assignmentpath=/root/.ssh/known_hostsas a clickable link.You can
provide a path <ansible.builtin.file#module:path>to theansible.builtin.file#module; its value isreturned as a return value <ansible.builtin.file#module:path>.
ansible.builtin.stat#module:stat.existsreferences thestat.existsreturn value of theansible.builtin.statmodule. You can also use=as for option values:ansible.builtin.stat#module:stat.exists=trueshowsstat.exists=true.
fooandfoo=baruse the same markup for an option and an option assignment without a link; the same is true for return values:fooandfoo=bar.
For both option and return values, . is used to reference suboptions and contained return values. Array stubs ([...]) can be used to indicate that something is a list, for example the :ansretval: argument ansible.builtin.service_facts#module:ansible_facts.services['systemd'].state references the ansible_facts.services.state return value of the ansible.builtin.service_facts module (ansible.builtin.service_facts#module:ansible_facts.services['systemd'].state).
Adding local TOCs
The page you're reading includes a local TOC. If you include a local TOC:
- place it below, not above, the main heading and (optionally) introductory text
- use the
:local:directive so the page's main heading is not included - do not include a title
The syntax is:
.. contents::
:local:
Markdown guidelines
Some Ansible ecosystem documentation is written in markdown and processed by mkdocs. We follow these technical or mechanical guidelines on all .md pages:
Heading notation
Section headings in markdown can use a variety of notations. To make our documents easy to read and to edit, we follow a standard set of heading notations. We use:
#for page titles:
# Installation
##for section headings:
## Installing on Linux
Subsections add an additional # for each subsection. We recommend not going beyond #### as that suggests a deeply nested document that could present better as multiple pages.
Linking in Markdown
Using Mkdocs, you can format internal links` using the file name of the local file instead of an external URL.
[configuration](/configuration)
You can also link directly to a heading within a file Use the lower-case form of the heading.
[dependency](/configuration/#dependency)
External links use a similar format with the external URL.
[Ansible Documentation](https://docs.ansible.com)
Code blocks
Markdown supports code blocks in the following format.
```text
docs/
index.md
user-guide/getting-started.md
user-guide/configuration-options.md
license.md
```
Accessibility guidelines
Ansible documentation has a goal to be more accessible. Use the following guidelines to help us reach this goal.
Images and alternative text
Ensure all icons, images, diagrams, and non text elements have a meaningful alternative-text description. Do not include screen captures of CLI output. Use a code block instead.
To add alt text in rst:
.. image:: path/networkdiag.png :width: 400 :alt: SpiffyCorp network diagram
To add alt text in md:

Links and hypertext
URLs and cross-reference links have descriptive text that conveys information about the content of the linked target. See style_links for how to format links in RST and see style_links_md for Markdown.
Tables
Tables have a simple, logical reading order from left to right, and top to bottom. Tables include a heading row and avoid empty or blank table cells. Label tables with a descriptive title.
For RST:
.. table:: File descriptions +----------+----------------------------+ |File |Purpose | +==========+============================+ |foo.txt |foo configuration settings | +----------+----------------------------+ |bar.txt |bar configuration settings | +----------+----------------------------+
For Markdown:
#### File descriptions |File |Purpose | |---------- | -------------------------- | |foo.txt | foo configuration settings | |bar.txt | bar configuration settings |
Colors and other visual information
- Avoid instructions that rely solely on sensory characteristics. For example, do not use
Click the square, blue button to continue.- Convey information by methods and not by color alone.
- Ensure there is sufficient contrast between foreground and background text or graphical elements in images and diagrams.
- Instructions for navigating through an interface make sense without directional indicators such as left, right, above, and below.
Accessibility resources
Use the following resources to help test your documentation changes:
- axe DevTools browser extension - Highlights accessibility issues on a website page.
- WAVE browser extension from WebAIM - another accessibility tester.
- Orca screen reader - Common tool used by people with vision impairments.
- color filter - For color-blind testing.
More resources
These pages offer more help with grammatical, stylistic, and technical rules for documentation.
basic_rules voice_style trademarks grammar_punctuation spelling_word_choice preferred_terms search_hints resources
community_documentation_contributions
How to contribute to the Ansible documentation
testing_documentation_locally
How to build the Ansible documentation
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
Preferred terminology
The following is the list of preferred terms for Ansible ecosystem projects.
Ansible
Use on all occurrences oransibleif referring to the CLI command. Preference is to use this to refer to the Ansible community package.Ansible Builder
Use on first occurrence oransible-builderif referring to the CLI command. Subsequent occurrences can be Builder.Ansible collection
Use on first occurrence. Subsequent occurrences can be collection.Ansible Content Development Kit (CDK)
Use on first occurrence. Subsequent occurrences can be Ansible CDK.Ansible Core
Use on all occurrences, though preference isansible-coreafter first occurrence to distinguish from the Ansible community package.Ansible Creator
Use on first occurrence oransible-creatorif referring to the CLI command.Ansible Execution Environment (EE)
Use on first occurrence. Subsequent occurrences can be Execution Environment or EE.Ansible Galaxy
Use on first occurrence. Subsequent occurrences can be Galaxy. Do not use Galaxy NG.Ansible Lint
Use on all occurrences oransible-lintif referring to the CLI command.Ansible Molecule
Use on first occurrence. Subsequent occurrences can be Molecule ormoleculeif referring to the CLI command.Ansible Navigator
Use first occurrence. Subsequent occurrences can be Navigator oransible-navigatorif referring to the CLI.Ansible playbook
Use on first occurrence. Subsequent occurrences can be playbook.ansible-pylibssh
Use on all occurrences.Ansible role
Use on first occurrence. Subsequent occurrences can be role.Ansible rule
Use on first occurrence. Subsequent occurrences can be rule.Ansible rulebook
Use on first occurrence. Subsequent occurrences can be rulebook.Ansible Runner
Use on first occurrence oransible-runnerif referring to the CLI command. Subsequent occurrences can be Runner.Ansible SDK
Use on all occurrences.Ansible Sign
Use on all occurrences oransible-signif referring to the CLI command.
ansible-test
Use on all occurrences as the CLI command.Ansible VSCode Extension by Red Hat
Use on first occurrence. Subsequent occurrences can be Ansible VSCode Extension.Pinakes
Use on all occurrences.Receptor
Use on all occurrences orreceptorif referring to the CLI command.
Resources
- Follow the style of the
Ansible Documentation<ansible_documentation> - Ask the community for advice. Visit the
Ansible communication guide<communication>for details. - Review these online style guides:
Writing documentation so search can find it
One of the keys to writing good documentation is to make it findable. Readers use a combination of internal site search and external search engines such as Google or DuckDuckGo.
To ensure Ansible documentation is findable, you should:
- Use headings that clearly reflect what you are documenting.
- Use numbered lists for procedures or high-level steps where possible.
- Avoid linking to GitHub blobs where possible.
Using clear headings in documentation
We all use simple English when we want to find something. For example, the title of this page could have been any one of the following:
- Search optimization
- Findable documentation
- Writing for findability
What we are really trying to describe is - how do I write documentation so search engines can find my content? That simple phrase is what drove the title of this section. When you are creating your headings for documentation, spend some time to think about what you would type in a search box to find it, or more importantly, how someone less familiar with Ansible would try to find that information. Your heading should be the answer to that question.
One word of caution - you do want to limit the size of your headings. A full heading such as How do I write documentation so search engines can find my content? is too long. Search engines would truncate anything over 50 - 60 characters. Long headings would also wrap on smaller devices such as a smart phone.
Using numbered lists for zero position snippets
Google can optimize the search results by adding a feature snippet at the top of the search results. This snippet provides a small window into the documentation on that first search result that adds more detail than the rest of the search results, and can occasionally answer the reader's questions right there, or at least verify that the linked page is what the reader is looking for.
Google returns the feature snippet in the form of numbered steps. Where possible, you should add a numbered list near the top of your documentation page, where appropriate. The steps can be the exact procedure a reader would follow, or could be a high level introduction to the documentation topic, such as the numbered list at the top of this page.
Problems with GitHub blobs on search results
Search engines do not typically return GitHub blobs in search results, at least not in higher ranked positions. While it is possible and sometimes necessary to link to GitHub blobs from documentation, the better approach would be to copy that information into an .rst page in Ansible documentation.
Other search hints
While it may not be possible to adapt your documentation to all search optimizations, keep the following in mind as you write your documentation:
- Search engines don't parse beyond the `#` in an html page. So for example, all the subheadings on this page are appended to the main page URL. As such, when I search for 'Using number lists for zero position snippets', the search result would be a link to the top of this page, not a link directly to the subheading I searched for. Using
local TOCs <local_toc>helps alleviate this problem as the reader can scan for the header at top of the page and click to the section they are looking for. For critical documentation, consider creating a new page that can be a direct search result page. - Make your first few sentences clearly describe your page topic. Search engines return not just the URL, but a short description of the information at the URL. For Ansible documentation, we do not have description metadata embedded on each page. Instead, the search engines return the first couple of sentences (140 characters) on the page. That makes your first sentence or two very important to the reader who is searching for something in Ansible.
Spelling - Word Usage - Common Words and Phrases to Use and Avoid
Acronyms
Always uppercase. An acronym is a word formed from the initial letters of a name, such as ROM for Read-only memory, SaaS for Software as a Service, or by combining initial letters or part of a series of words, such as LILO for LInux LOader.
Spell out the acronym before using it in alone text, such as "The Embedded DevKit (EDK)..."
Do not define abbreviations that appear only a few times. Spell out the full term each time instead.
Allowlist/Denylist
Use "allowlist" and "denylist." Do not use "whitelist" or "blacklist."
Applications
When used as a proper name, use the capitalization of the product, such as GNUPro or Source-Navigator. When used as a command, use lowercase as appropriate, such as "To start GCC, type gcc."
"vi" is always lowercase.
As
This is often used to mean "because", but has other connotations, for example, parallel or simultaneous actions. If you mean "because", say "because".
Asks for
Use "requests" instead.
Assure/Ensure/Insure
Assure implies a sort of mental comfort. As in "I assured my husband that I would eventually bring home beer."
Ensure means "to make sure."
Insure relates to monetary insurance.
Back up
This is a verb. You "back up" files; you do not "backup" files.
Backup
This is a noun. You create "backup" files; you do not create "back up" files.
Backward
Correct. Avoid using backwards unless you are stating that something has "backwards compatibility."
Backwards compatibility
Correct as is.
By way of
Use "using" instead.
Can/May
Use "can" to describe actions or conditions that are possible. Use "may" only to describe situations where permission is being given. If either "can," "could," or "may" apply, use "can" because it is less tentative.
CD or cd
When referring to a compact disk, use CD, such as "Insert the CD into the CD-ROM drive." When referring to the change directory command, use cd.
CD-ROM
Correct. Do not use "cdrom," "CD-Rom," "CDROM," "cd-rom" or any other variation. When referring to the drive, use CD-ROM drive, such as "Insert the CD into the CD-ROM drive." The plural is "CD-ROMs."
Command line
Correct. Do not use "command-line" or "commandline" as a noun. If used as an adjective, "command-line" is appropriate, for example "command-line arguments".
Use "command line" to describes where to place options for a command, but not where to type the command. Use "shell prompt" instead to describe where to type commands. The line on the display screen where a command is expected. Generally, the command line is the line that contains the most recently displayed command prompt.
Daylight saving time (DST)
Correct. Do not use daylight savings time. Daylight Saving Time (DST) is often misspelled "Daylight Savings", with an "s" at the end. Other common variations are "Summer Time"and "Daylight-Saving Time". (https://www.timeanddate.com/time/dst/daylight-savings-time.html)
Download
Correct. Do not use "down load" or "down-load."
e.g.
Spell it out: "For example."
Failover
When used as a noun, a failover is a backup operation that automatically switches to a standby database, server or network if the primary system fails or is temporarily shut down for servicing. Failover is an important fault tolerance function of mission-critical systems that rely on constant accessibility. Failover automatically and transparently to the user redirects requests from the failed or down system to the backup system that mimics the operations of the primary system.
Fail over
When used as a verb, fail over is two words since there can be different tenses such as failed over.
Fewer
Fewer is used with plural nouns. Think things you could count. Time, money, distance, and weight are often listed as exceptions to the traditional "can you count it" rule, often thought of a singular amounts (the work will take less than 5 hours, for example).
File name
Correct. Do not use "filename."
File extensions
Include a period before file name extensions (for example, .yml).
File system
Correct. Do not use "filesystem." The system that an operating system or program uses to organize and keep track of files. For example, a hierarchical file system is one that uses directories to organize files into a tree structure. Although the operating system provides its own file management system, you can buy separate file management systems. These systems interact smoothly with the operating system but provide more features, such as improved backup procedures and stricter file protection.
For instance
For example," instead.
For further/additional/whatever information
Use "For more information"
For this reason
Use "therefore".
Forward
Correct. Avoid using "forwards."
Gigabyte (GB)
2 to the 30th power (1,073,741,824) bytes. One gigabyte is equal to 1,024 megabytes. Gigabyte is often abbreviated as G or GB.
Got
Avoid. Use "must" instead.
High-availability
Correct. Do not use "high availability."
Highly available
Correct. Do not use highly-available."
Hostname
Correct. Do not use host name.
ID
Use ID (all uppercase) when referring to "Identification" or "Identifier."
-
Do not use "Id," "id," or other variations.
-
Examples:
- "User ID"
- "Resource ID"
-
Reason: "ID" is a standard abbreviation for "Identification" or "Identifier" and is more readable when capitalized consistently.
i.e.
Spell it out: "That is."
Installer
Avoid. Use "installation program" instead.
It's and its
"It's" is a contraction for "it is;" use "it is" instead of "it's." Use "its" as a possessive pronoun (for example, "the store is known for its low prices").
Less
Less is used with singular nouns. For example "View less details" wouldn't be correct but "View less detail" works. Use fewer when you have plural nouns (things you can count).
Linux
Correct. Do not use "LINUX" or "linux" unless referring to a command, such as "To start Linux, type linux." Linux is a registered trademark of Linus Torvalds.
Login
A noun used to refer to the login prompt, such as "At the login prompt, enter your username."
Log in
A verb used to refer to the act of logging in. Do not use "login," "loggin," "logon," and other variants. For example, "When starting your computer, you are requested to log in..."
Log on
To make a computer system or network recognize you so that you can begin a computer session. Most personal computers have no log-on procedure -- you just turn the machine on and begin working. For larger systems and networks, however, you usually need to enter a username and password before the computer system will allow you to execute programs.
Lots of
Use "Several" or something equivalent instead.
Make sure
This means "be careful to remember, attend to, or find out something." For example, "...make sure that the rhedk group is listed in the output." Try to use verify or ensure instead.
Manual/man page
Correct. Two words. Do not use "manpage"
MB
- When spelled MB, short for megabyte (1,000,000 or 1,048,576 bytes, depending on the context).
- When spelled Mb, short for megabit.
MBps
Short for megabytes per second, a measure of data transfer speed. Mass storage devices are generally measured in MBps.
MySQL
Common open source database server and client package. Do not use "MYSQL" or "mySQL."
Need to
Avoid. Use "must" instead.
Once
Use "once" to mean "one time." Do not use "once" as a conjunction meaning "after" or "when."
Primary/Replica
Use "primary" and "replica" or similar alternatives instead of "master" and "slave."
Read-only
Correct. Use when referring to the access permissions of files or directories.
Real time/real-time
Depends. If used as a noun, it is the actual time during which something takes place. For example, "The computer may partly analyze the data in real time (as it comes in) -- R. H. March." If used as an adjective, "real-time" is appropriate. For example, "XEmacs is a self-documenting, customizable, extensible, real-time display editor."
See vs Refer to
Use "See" instead of "Refer to" to indicate a reference (within a manual or website) or a cross-reference (to another manual or documentation source).
Since
This is often used to mean "because", but "since" has connotations of time, so be careful. If you mean "because", say "because".
Tells
Use "Instructs" instead.
That/which
"That" introduces a restrictive clause-a clause that must be there for the sentence to make sense. A restrictive clause often defines the noun or phrase preceding it. "Which" introduces a non-restrictive, parenthetical clause-a clause that could be omitted without affecting the meaning of the sentence. For example: The car was travelling at a speed that would endanger lives. The car, which was traveling at a speed that would endanger lives, swerved onto the sidewalk. Use "who" or "whom," rather than "that" or "which," when referring to a person.
Then/than
"Then" refers to a time in the past or the next step in a sequence. "Than" is used for comparisons.

Third-party
Correct. Do not use "third party".
Troubleshoot
Correct. Do not use "trouble shoot" or "trouble-shoot." To isolate the source of a problem and fix it. In the case of computer systems, the term troubleshoot is usually used when the problem is suspected to be hardware -related. If the problem is known to be in software, the term debug is more commonly used.
UK
Correct as is, no periods.
UNIX®
Correct. Do not use "Unix" or "unix." UNIX® is a registered trademark of The Open Group.
Unset
Don't use. Use Clear.
US
Correct as is, no periods.
User
When referring to the reader, use "you" instead of "user." For example, "The user must..." is incorrect. Use "You must..." instead. If referring to more than one user, calling the collection "users" is acceptable, such as "Other users may wish to access your database."
Username
Correct. Do not use "user name."
View
When using as a reference ("View the documentation available online."), do not use View. Use "Refer to" instead.
Within
Don't use to refer to a file that exists in a directory. Use "In".
World Wide Web
Correct. Capitalize each word. Abbreviate as "WWW" or "Web."
Webpage
Correct. Do not use "web page" or "Web page."
Web server
Correct. Do not use "webserver". For example, "The Apache HTTP Server is the default Web server..."
Website
Correct. Do not use "web site" or "Web site." For example, "The Ansible website contains ..."
Who/whom
Use the pronoun "who" as a subject. Use the pronoun "whom" as a direct object, an indirect object, or the object of a preposition. For example: Who owns this? To whom does this belong?
While
Use "while" only to refer to a period of time. Do not use "while" as a synonym for "although."
Will
Do not use future tense unless it is absolutely necessary. For example, do not use the sentence, "The next section will describe the process in more detail." Instead, use the sentence, "The next section describes the process in more detail."
Wish
Use "need" instead of "desire" and "wish." Use "want" when the reader's actions are optional (that is, they may not "need" something but may still "want" something).
x86
Correct. Do not capitalize the "x."
x86_64
Do not use. Do not use "Hammer". Always use "AMD64 and Intel® EM64T" when referring to this architecture.
You
Correct. Do not use "I," "he," or "she."
You may
Try to avoid using this. For example, "you may" can be eliminated from this sentence "You may double-click on the desktop..."
Trademark Usage
Why is it important to use the TM, SM, and ® for our registered marks?
Before a trademark is registered with the United States Patent and Trademark Office it is appropriate to use the TM or SM symbol depending whether the product is for goods or services. It is important to use the TM or SM as it is notification to the public that Ansible claims rights to the mark even though it has not yet been registered.
Once the trademark is registered, it is appropriate to use the symbol in place of the TM or SM. The symbol designation must be used in conjunction with the trademark if Ansible is to fully protect its rights. If we don't protect these marks, we run the risk of losing them in the way of Aspirin or Trampoline or Escalator.
General Rules:
Trademarks should be used on 1st references on a page or within a section.
Use Red Hat® Ansible® Automation Platform or Ansible®, on first reference when referring to products.
Use "Ansible" alone as the company name, as in "Ansible announced quarterly results," which is not marked.
Also add the trademark disclaimer. * When using Ansible trademarks in the body of written text, you should use the following credit line in a prominent place, usually a footnote.
For Registered Trademarks: - [Name of Trademark] is a registered trademark of Red Hat, Inc. in the United States and other countries.
For Unregistered Trademarks (TMs/SMs): - [Name of Trademark] is a trademark of Red Hat, Inc. in the United States and other countries.
For registered and unregistered trademarks: - [Name of Trademark] is a registered trademark and [Name of Trademark] is a trademark of Red Hat, Inc. in the United States and other countries.
Guidelines for the proper use of trademarks:
Always distinguish trademarks from surround text with at least initial capital letters or in all capital letters.
Always use proper trademark form and spelling.
Never use a trademark as a noun. Always use a trademark as an adjective modifying the noun.
Correct: Red Hat® Ansible® Automation Platform system performance is incredible.
Incorrect: Ansible's performance is incredible.
Never use a trademark as a verb. Trademarks are products or services, never actions.
Correct: "Orchestrate your entire network using Red Hat® Ansible® Automation Platform."
Incorrect: "Ansible your entire network."
Never modify a trademark to a plural form. Instead, change the generic word from the singular to the plural.
Correct: "Corporate demand for Red Hat® Ansible® Automation Platform software is surging."
Incorrect: "Corporate demand for Ansible is surging."
Never modify a trademark from its possessive form, or make a trademark possessive. Always use it in the form it has been registered.
Never translate a trademark into another language.
Never use trademarks to coin new words or names.
Never use trademarks to create a play on words.
Never alter a trademark in any way including through unapproved fonts or visual identifiers.
Never abbreviate or use any Ansible trademarks as an acronym.
The importance of Ansible trademarks
The Ansible trademark and the "A" logo in a shaded circle are our most valuable assets. The value of these trademarks encompass the Ansible Brand. Effective trademark use is more than just a name, it defines the level of quality the customer will receive and it ties a product or service to a corporate image. A trademark may serve as the basis for many of our everyday decisions and choices. The Ansible Brand is about how we treat customers and each other. In order to continue to build a stronger more valuable Brand we must use it in a clear and consistent manner.
The mark consists of the letter "A" in a shaded circle. As of 5/11/15, this was a pending trademark (registration in process).
Common Ansible Trademarks
- Ansible®
Other Common Trademarks and Resource Sites:
- Linux is a registered trademark of Linus Torvalds.
- UNIX® is a registered trademark of The Open Group.
- Microsoft, Windows, Vista, XP, and NT are registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries. https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/en-us.aspx
- Apple, Mac, Mac OS, Macintosh, Pages and TrueType are either registered trademarks or trademarks of Apple Computer, Inc. in the United States and/or other countries. https://www.apple.com/legal/intellectual-property/trademark/appletmlist.html
- Adobe, Acrobat, GoLive, InDesign, Illustrator, PostScript , PhotoShop and the OpenType logo are either registered trademarks or trademarks of Adobe Systems Incorporated in the United States and/or other countries. https://www.adobe.com/legal/permissions/trademarks.html
- Macromedia and Macromedia Flash are trademarks of Macromedia, Inc. https://www.adobe.com/legal/permissions/trademarks.html
- IBM is a registered trademark of International Business Machines Corporation. https://www.ibm.com/legal/us/en/copytrade.shtml
- Celeron, Celeron Inside, Centrino, Centrino logo, Core Inside, Intel Core, Intel Inside, Intel Inside logo, Itanium, Itanium Inside, Pentium, Pentium Inside, VTune, Xeon, and Xeon Inside are trademarks or registered trademarks of Intel Corporation or its subsidiaries in the United States and other countries. https://www.intel.com/content/www/us/en/legal/trademarks.html
Voice Style
The essence of the Ansible writing style is short sentences that flow naturally together. Mix up sentence structures. Vary sentence subjects. Address the reader directly. Ask a question. And when the reader adjusts to the pace of shorter sentences, write a longer one.
- Write how real people speak...
- ...but try to avoid slang and colloquialisms that might not translate well into other languages.
- Say big things with small words.
- Be direct. Tell the reader exactly what you want them to do.
- Be honest.
- Short sentences show confidence.
- Grammar rules are meant to be bent, but only if the reader knows you are doing this.
- Choose words with fewer syllables for faster reading and better understanding.
- Think of copy as one-on-one conversations rather than as a speech. It is more difficult to ignore someone who is speaking to you directly.
- When possible, start task-oriented sentences (those that direct a user to do something) with action words. For example: Find software... Contact support... Install the media.... and so forth. Use imperative mood for procedure steps ("Run the command"). You can use "you" in general instructional text when it fits naturally.
Active Voice
Use the active voice ("Start Linuxconf by typing...") rather than passive ("Linuxconf can be started by typing...") whenever possible. Active voice makes for more lively, interesting reading. Also avoid future tense (or using the term "will") whenever possible For example, future tense ("The screen will display...") does not read as well as an active voice ("The screen displays"). Remember, the users you are writing for most often refer to the documentation while they are using the system, not after or in advance of using the system.
Sentence length
Aim for 32 words or fewer per sentence. Varying sentence length improves readability, but most sentences should stay concise.
Precise verbs
Prefer precise, specific verbs over weak verbs such as "is", "are", "occur", or "happen." Where possible, start sentences with a real subject instead of expletive constructions such as "There is", "There are", or "It is."
Prefer single-word verbs
Where a single-word verb exists, prefer it over a phrasal verb. For example, write "click the button" rather than "click on the button", and "omit the parameter" rather than "leave out the parameter."
Avoid terms of politeness
Do not use "please", "thank you", "kindly", or similar terms of politeness in technical content. Use direct imperative instructions instead. For example, write "Configure the inventory file before running the playbook" rather than "Please configure the inventory file before running the playbook."
Avoid anthropomorphism
Do not assign human qualities to software or components. Describe what the user does or what happens, not what the software "thinks", "knows", "wants", or "decides." For example, write "The module returns a list of packages to install" rather than "The module knows which packages to install."
Testing Ansible
testing_sanity testing_units testing_units_modules testing_integration testing_httptester testing_running_locally testing_documentation
Why test your Ansible contributions?
If you're a developer, one of the most valuable things you can do is to look at GitHub issues and help fix bugs, since bug-fixing is almost always prioritized over feature development. Even for non-developers, helping to test pull requests for bug fixes and features is still immensely valuable.
Ansible users who understand how to write playbooks and roles should be able to test their work. GitHub pull requests will automatically run a variety of tests (for example, Azure Pipelines) that show bugs in action. However, contributors must also test their work outside of the automated GitHub checks and show evidence of these tests in the pull request to ensure that their work will be more likely to be reviewed and merged.
Read on to learn how Ansible is tested, how to test your contributions locally, and how to extend testing capabilities.
If you want to learn about testing collections, read testing_collections
Types of tests
At a high level, we have the following classifications of tests:
sanity
testing_sanity- Sanity tests are made up of scripts and tools used to perform static code analysis.
- The primary purpose of these tests is to enforce Ansible coding standards and requirements.
integration
testing_integration- Functional tests of modules and Ansible Core functionality.
units
testing_units- Tests directly against individual parts of the code base.
Testing within GitHub & Azure Pipelines
Organization
When Pull Requests (PRs) are created they are tested using Azure Pipelines, a Continuous Integration (CI) tool. Results are shown at the end of every PR.
When Azure Pipelines detects an error and it can be linked back to a file that has been modified in the PR then the relevant lines will be added as a GitHub comment. For example:
The test `ansible-test sanity --test pep8` failed with the following errors:
lib/ansible/modules/network/foo/bar.py:509:17: E265 block comment should start with '# '
The test `ansible-test sanity --test validate-modules` failed with the following error:
lib/ansible/modules/network/foo/bar.py:0:0: E307 version_added should be 2.4. Currently 2.3
From the above example we can see that --test pep8 and --test validate-modules have identified an issue. The commands given allow you to run the same tests locally to ensure you've fixed all issues without having to push your changes to GitHub and wait for Azure Pipelines, for example:
If you haven't already got Ansible available, use the local checkout by running:
source hacking/env-setup
Then run the tests detailed in the GitHub comment:
ansible-test sanity --test pep8
ansible-test sanity --test validate-modules
If there isn't a GitHub comment stating what's failed you can inspect the results by clicking on the "Details" button under the "checks have failed" message at the end of the PR.
Rerunning a failing CI job
Occasionally you may find your PR fails due to a reason unrelated to your change. This could happen for several reasons, including:
- a temporary issue accessing an external resource, such as a yum or Git repo
- a timeout creating a virtual machine to run the tests on
If either issue appears to be the case, you can rerun the Azure Pipelines test by:
- adding a comment with
/rebuild(full rebuild) or/rebuild_failed(rebuild only failed CI nodes) to the pull request - closing and re-opening the pull request (full rebuild)
- making another change to the branch and pushing to GitHub
If the issue persists, please contact the community. Visit the Ansible communication guide<communication> for details.
How to test a PR
Ideally, the code should add tests that prove that the code works. That's not always possible and tests are not always comprehensive, especially when a user doesn't have access to a wide variety of platforms, or is using an API or web service. In these cases, live testing against real equipment can be more valuable than automation that runs against simulated interfaces. In any case, things should always be tested manually the first time as well.
Thankfully, helping to test Ansible is pretty straightforward, assuming you are familiar with how Ansible works.
Setup: Installing Pytest and required Pytest libraries
Ansible's unit testing framework leverages the pytest library. Before diving into testing, ensure you have pytest installed alongside any additional pytest libraries such as pytest-mock and pytest-xdist.
Refer to the documentation for more information: testing_units.
Setup: Checking out a Pull Request
You can do this by:
- checking out Ansible
- fetching the proposed changes into a test branch
- testing
- commenting on that particular issue on GitHub
Here's how:
Testing source code from GitHub pull requests sent to us does have some inherent risk, as the source code sent may have mistakes or malicious code that could have a negative impact on your system. We recommend doing all testing on a virtual machine, whether a cloud instance, or locally. Some users like Vagrant or Docker for this, but they are optional. It is also useful to have virtual machines of different Linux or other flavors, since some features (for example, package managers such as apt or yum) are specific to those OS versions.
Create a fresh area to work:
git clone https://github.com/ansible/ansible.git ansible-pr-testing
cd ansible-pr-testing
Next, find the pull request you'd like to test and make a note of its number. It will look something like this:
Use os.path.sep instead of hardcoding / #65381
Only test ansible:devel
It is important that the PR request target be ansible:devel, as we do not accept pull requests into any other branch. Dot releases are cherry-picked manually by Ansible staff.
Use the pull request number when you fetch the proposed changes and create your branch for testing:
git fetch origin refs/pull/XXXX/head:testing_PRXXXX
git checkout testing_PRXXXX
The first command fetches the proposed changes from the pull request and creates a new branch named testing_PRXXXX, where the XXXX is the actual number associated with the pull request (for example, 65381). The second command checks out the newly created branch.
If the GitHub user interface shows that the pull request will not merge cleanly, we do not recommend proceeding if you are not somewhat familiar with Git and coding, as you will have to resolve a merge conflict. This is the responsibility of the original pull request contributor.
Some users do not create feature branches, which can cause problems when they have multiple, unrelated commits in their version of devel. If the source looks like someuser:devel, make sure there is only one commit listed on the pull request.
The Ansible source includes a script that allows you to use Ansible directly from source without requiring a full installation that is frequently used by developers on Ansible.
Simply source it (to use the Linux/Unix terminology) to begin using it immediately:
source ./hacking/env-setup
This script modifies the PYTHONPATH environment variables (along with a few other things), which will be temporarily set as long as your shell session is open.
Testing the Pull Request
At this point, you should be ready to begin testing!
Some ideas of what to test are:
- Create a test Playbook with the examples in and check if they function correctly
- Test to see if any Python backtraces returned (that's a bug)
- Test on different operating systems, or against different library versions
Run sanity tests
ansible-test sanity
More information: testing_sanity
Run unit tests
ansible-test units
More information: testing_units
Run integration tests
ansible-test integration -v ping
More information: testing_integration
Any potential issues should be added as comments on the pull request (and it is acceptable to comment if the feature works as well), remembering to include the output of ansible --version
Example:
Works for me! Tested on `Ansible 2.3.0`. I verified this on CentOS 6.5 and also Ubuntu 14.04.
If the PR does not resolve the issue, or if you see any failures from the unit/integration tests, just include that output instead:
This change causes errors for me.
When I ran this Ubuntu 16.04 it failed with the following:
```
some output
StackTrace
some other output
```
Code Coverage Online
The online code coverage reports is a good way to identify areas for testing improvement in Ansible. By following red colors you can drill down through the reports to find files that have no tests at all. Adding both integration and unit tests that show clearly how code should work, verify important Ansible functions and increase testing coverage in areas where there is none is a valuable way to help improve Ansible.
The code coverage reports only cover the devel branch of Ansible where new feature development takes place. Pull requests and new code will be missing from the codecov.io coverage reports so local reporting is needed. Most ansible-test commands allow you to collect code coverage, this is particularly useful to indicate where to extend testing. See testing_running_locally for more information.
Want to know more about testing?
If you'd like to know more about the plans for improving testing Ansible then why not join the Ansible community forum.
action-plugin-docs
Each action plugin should have a matching module of the same name to provide documentation.
ansible-doc
Verifies that ansible-doc can parse module documentation on all supported Python versions.
ansible-requirements
test/lib/ansible_test/_data/requirements/sanity.import-plugins.txt must be an identical copy of requirements.txt found in the project's root.
bin-symlinks
The bin/ directory in Ansible must contain only symbolic links to executable files. These files must reside in the lib/ansible/ or test/lib/ansible_test/ directories.
This is required to allow ansible-test to work with containers and remote hosts when running from an installed version of Ansible.
Symlinks for each entry point in bin/ must also be present in test/lib/ansible_test/_util/target/injector/. Each symlink should point to the python.py script in the same directory. This facilitates running with the correct Python interpreter and enabling code coverage.
boilerplate
Python files should include the following boilerplate:
from __future__ import annotations
changelog
Basic linting of changelog fragments with antsibull-changelog lint.
One or more of the following sections are required:
- major_changes
- minor_changes
- breaking_changes
- deprecated_features
- removed_features
- security_fixes
- bugfixes
- known_issues
New modules and plugins must not be included in changelog fragments.
See collection_changelogs for details.
compile
All Python source files must successfully compile using all supported Python versions.
The list of supported Python versions is dependent on the version of ansible-core that you are using. Make sure you consult the version of the documentation which matches your ansible-core version. You can find an overview for this and previous versions in ansible_core_support_matrix.
Control node code, including plugins in Ansible Collections, must support the following Python versions:
- 3.14
- 3.13
- 3.12
Code which runs on targets (modules and module_utils) must support all control node supported Python versions, as well as the additional Python versions supported only on targets:
- 3.14
- 3.13
- 3.12
- 3.11
- 3.10
- 3.9
Ansible Collections can be configured to support a subset of the target-only Python versions.
deprecated-config
DOCUMENTATION config is scheduled for removal
empty-init
The __init__.py files under the following directories must be empty. For some of these (modules and tests), __init__.py files with code won't be used. For others (module_utils), we want the possibility of using Python namespaces which an empty __init__.py will allow for.
lib/ansible/modules/lib/ansible/module_utils/test/units/
future-import-boilerplate
Most Python files should include the following boilerplate at the top of the file, right after the comment header:
from __future__ import (absolute_import, division, print_function)
This uses Python 3 semantics for absolute versus relative imports, division, and print. By doing this, we can write code which is portable between Python 2 and Python 3 by following the Python 3 semantics.
absolute_import
When Python 2 encounters an import of a name in a file like import copy it attempts to load copy.py from the same directory as the file is in. This can cause problems if there is a python file of that name in the directory and also a python module in sys.path with that same name. In that case, Python 2 would load the one in the same directory and there would be no way to load the one on sys.path. Python 3 fixes this by making imports absolute by default. import copy will find copy.py from sys.path. If you want to import copy.py from the same directory, the code needs to be changed to perform a relative import: from . import copy.
division
In Python 2, the division operator (/) returns integer values when used with integers. If there was a remainder, this part would be left off (aka, floor division). In Python 3, the division operator (/) always returns a floating point number. Code that needs to calculate the integer portion of the quotient needs to switch to using the floor division operator (//) instead.
print_function
In Python 2, python:print is a keyword. In Python 3, python3:print is a function with different parameters. Using this __future__ allows using the Python 3 print semantics everywhere.
ignores
Sanity tests for individual files can be skipped, and specific errors can be ignored.
When to Ignore Errors
Sanity tests are designed to improve code quality and identify common issues with content. When issues are identified during development, those issues should be corrected.
As development of Ansible continues, sanity tests are expanded to detect issues that previous releases could not. To allow time for existing content to be updated to pass newer tests, ignore entries can be added. New content should not use ignores for existing sanity tests.
When code is fixed to resolve sanity test errors, any relevant ignores must also be removed. If the ignores are not removed, this will be reported as an unnecessary ignore error. This is intended to prevent future regressions due to the same error recurring after being fixed.
When to Skip Tests
Although rare, there are reasons for skipping a sanity test instead of ignoring the errors it reports.
If a sanity test results in a traceback when processing content, that error cannot be ignored. If this occurs, open a new bug report for the issue so it can be fixed. If the traceback occurs due to an issue with the content, that issue should be fixed. If the content is correct, the test will need to be skipped until the bug in the sanity test is fixed.
Caution should be used when skipping sanity tests instead of ignoring them. Since the test is skipped entirely, resolution of the issue will not be automatically detected. This will prevent prevent regression detection from working once the issue has been resolved. For this reason it is a good idea to periodically review skipped entries manually to verify they are required.
Ignore File Location
The location of the ignore file depends on the type of content being tested.
Ansible Collections
Since sanity tests change between Ansible releases, a separate ignore file is needed for each Ansible major release.
The file name is tests/sanity/ignore-X.Y.txt where X.Y is the Ansible release being used to test the collection.
Maintaining a separate file for each Ansible release allows a collection to pass tests for multiple versions of Ansible.
Ansible
When testing Ansible, all ignores are placed in the test/sanity/ignore.txt file.
Only a single file is needed because ansible-test is developed and released as a part of Ansible itself.
Ignore File Format
The ignore file contains one entry per line. Each line consists of two columns, separated by a single space. Comments may be added at the end of an entry, started with a hash (#) character, which can be proceeded by zero or more spaces. Blank and comment only lines are not allowed.
The first column specifies the file path that the entry applies to. File paths must be relative to the root of the content being tested. This is either the Ansible source or an Ansible collection. File paths cannot contain a space or the hash (#) character.
The second column specifies the sanity test that the entry applies to. This will be the name of the sanity test. If the sanity test is specific to a version of Python, the name will include a dash (-) and the relevant Python version. If the named test uses error codes then the error code to ignore must be appended to the name of the test, separated by a colon (:).
Below are some example ignore entries for an Ansible collection:
roles/my_role/files/my_script.sh shellcheck:SC2154 # ignore undefined variable
plugins/modules/my_module.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/my_module.py import-3.8 # needs update to support collections.abc on Python 3.8+
It is also possible to skip a sanity test for a specific file. This is done by adding !skip after the sanity test name in the second column. When this is done, no error code is included, even if the sanity test uses error codes.
Below are some example skip entries for an Ansible collection:
plugins/module_utils/my_util.py validate-modules!skip # waiting for bug fix in module validator
plugins/lookup/my_plugin.py compile-2.6!skip # Python 2.6 is not supported on the control node
See the full list of sanity tests <all_sanity_tests>, which details the various tests and details how to fix identified issues.
Ignore File Errors
There are various errors that can be reported for the ignore file itself:
- syntax errors parsing the ignore file
- references a file path that does not exist
- references to a sanity test that does not exist
- ignoring an error that does not occur
- ignoring a file which is skipped
- duplicate entries
import
Ansible allows unchecked imports<allowed_unchecked_imports> of some libraries from specific directories. Importing any other Python library requires handling import errors<handling_import_errors>. This enables support for sanity tests such as testing_validate-modules and provides better error messages to the user.
Please see the frequently asked questions<frequently_asked_questions> for answers to common questions about resolving errors reported by this test.
Handling import errors
Ansible executes across many hosts and can use multiple Python interpreters at the same time, which may even have different versions. To ensure users get an actionable and easy to understand error we try to ensure any non-core imports in modules/plugins are guarded to avoid a traceback, which most users won't be able to understand, much less use to solve the issue.
Another reason Ansible does this is to import the code for inspection. This allows Ansible to easily test, document, configure, etc based on the code without having to install any and all requirements everywhere, especially when that is not the context in which you execute the code.
The code below shows examples of how to avoid errors on import and then use the provided missing_required_lib to ensure the user knows which LIBRARY is missing, on which HOST it is missing and the specific INTERPRETER that requires it.
In modules
Instead of using import another_library:
import traceback
from ansible.module_utils.basic import missing_required_lib
try:
import another_library
except ImportError:
HAS_ANOTHER_LIBRARY = False
ANOTHER_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_ANOTHER_LIBRARY = True
ANOTHER_LIBRARY_IMPORT_ERROR = None
The missing_required_lib import above will be used below.
Then in the module code, normally inside the main method:
module = AnsibleModule(...)
if not HAS_ANOTHER_LIBRARY:
module.fail_json(
msg=missing_required_lib('another_library'),
exception=ANOTHER_LIBRARY_IMPORT_ERROR)
In plugins
Instead of using import another_library:
try:
import another_library
except ImportError as imp_exc:
ANOTHER_LIBRARY_IMPORT_ERROR = imp_exc
else:
ANOTHER_LIBRARY_IMPORT_ERROR = None
Then in the plugin code, for example in the run method of the plugin (some plugins don't have a run method and will require it in the __init__ method instead):
if ANOTHER_LIBRARY_IMPORT_ERROR:
raise AnsibleError('another_library must be installed to use this plugin') from ANOTHER_LIBRARY_IMPORT_ERROR
When used as base classes
This solution builds on the previous two examples. Make sure to pick the appropriate one before continuing with this solution.
Sometimes an import is used in a base class, for example:
from another_library import UsefulThing
class CustomThing(UsefulThing):
pass
One option is make the entire class definition conditional:
if not ANOTHER_LIBRARY_IMPORT_ERROR:
class CustomThing(UsefulThing):
pass
Another option is to define a substitute base class by modifying the exception handler:
try:
from another_library import UsefulThing
except ImportError:
class UsefulThing:
pass
...
Allowed unchecked imports
Ansible allows the following unchecked imports from these specific directories:
- ansible-core:
- For
lib/ansible/modules/andlib/ansible/module_utils/, unchecked imports are only allowed from the Python standard library; - For
lib/ansible/plugins/, unchecked imports are only allowed from the Python standard library, from public dependencies of ansible-core, and from ansible-core itself;
- For
- collections:
- For
plugins/modules/andplugins/module_utils/, unchecked imports are only allowed from the Python standard library; - For other directories in
plugins/(see the community collection requirements for a list), unchecked imports are only allowed from the Python standard library, from public dependencies of ansible-core, and from ansible-core itself.
- For
Public dependencies of ansible-core are:
- Jinja2
- PyYAML
- MarkupSafe (as a dependency of Jinja2)
Frequently asked questions (FAQ)
Why do I get an ImportError when my module or plugin works?
The import sanity test is very different from other tests. By design, it can only see modules that are in the Python standard library. This means your module or plugin can work in your playbooks, integration tests and unit tests, while failing the import sanity test.
All occurrences of ImportError must be properly handled<handling_import_errors> in the module or plugin where it occurs.
Why isn't the test using the Python interpreter I specify?
The sanity test will use the Python interpreter you specify. However, it will create its own virtual environment using that Python interpreter.
All occurrences of ImportError must be properly handled<handling_import_errors> in the module or plugin where it occurs.
How do I use a custom virtual environment?
This is not possible, since doing so would defeat the purpose of the test.
All occurrences of ImportError must be properly handled<handling_import_errors> in the module or plugin where it occurs.
How do I specify where to find my imports?
This is not possible, since doing so would defeat the purpose of the test.
All occurrences of ImportError must be properly handled<handling_import_errors> in the module or plugin where it occurs.
How do I specify which requirements file to use?
This is not possible, since doing so would defeat the purpose of the test.
All occurrences of ImportError must be properly handled<handling_import_errors> in the module or plugin where it occurs.
How do I fix the ImportError without changing my code?
There is no way to fix an ImportError in your plugins or modules without making changes to them.
All occurrences of ImportError must be properly handled<handling_import_errors> in the module or plugin where it occurs.
Sanity Tests
The following sanity tests are available as --test options for ansible-test sanity when testing Ansible Collections. This list is also available using ansible-test sanity --list-tests --allow-disabled.
For information on how to run these tests, see sanity testing guide <testing_sanity>.
action-plugin-docs ansible-doc changelog compile deprecated-config future-import-boilerplate empty-init ignores import line-endings no-assert no-basestring no-dict-iteritems no-dict-iterkeys no-dict-itervalues no-get-exception no-illegal-filenames no-main-display no-smart-quotes no-unicode-literals pep8 pslint pylint replace-urlopen runtime-metadata shebang shellcheck symlinks update-bundled use-argspec-type-path use-compat-six validate-modules yamllint
Additional tests are available when testing Ansible Core:
ansible-requirements bin-symlinks boilerplate integration-aliases mypy no-unwanted-files obsolete-files package-data pymarkdown release-names required-and-default-attributes test-constraints
integration-aliases
Integration tests are executed by ansible-test and reside in directories under test/integration/targets/. Each test MUST have an aliases file to control test execution.
Aliases are explained in the following sections. Each alias must be on a separate line in an aliases file.
Groups
Tests must be configured to run in exactly one group. This is done by adding the appropriate group to the aliases file.
The following are examples of some of the available groups:
shippable/posix/group1shippable/windows/group2shippable/azure/group3shippable/aws/group1shippable/cloud/group1
Groups are used to balance tests across multiple CI jobs to minimize test run time. They also improve efficiency by keeping tests with similar requirements running together.
When selecting a group for a new test, use the same group as existing tests similar to the one being added. If more than one group is available, select one randomly.
Setup
Aliases can be used to execute setup targets before running tests:
setup/once/TARGET- Run the targetTARGETbefore the first target that requires it.setup/always/TARGET- Run the targetTARGETbefore each target that requires it.
Requirements
Aliases can be used to express some test requirements:
needs/privileged- Requires--docker-privilegedwhen running tests with--docker.needs/root- Requires running tests asrootor with--docker.needs/ssh- Requires SSH connections to localhost (or the test container with--docker) without a password.needs/httptester- Requires use of the http-test-container to run tests.
Dependencies
Some test dependencies are automatically discovered:
- Ansible role dependencies defined in
meta/main.ymlfiles. - Setup targets defined with
setup/*aliases. - Symbolic links from one target to a file in another target.
Aliases can be used to declare dependencies that are not handled automatically:
needs/target/TARGET- Requires use of the test targetTARGET.needs/file/PATH- Requires use of the filePATHrelative to the Git root.
Skipping
Aliases can be used to skip platforms using one of the following:
skip/freebsd- Skip tests on FreeBSD.skip/macos- Skip tests on macOS.skip/rhel- Skip tests on RHEL.skip/docker- Skip tests when running in a Docker container.
Platform versions, as specified using the --remote option with / removed, can also be skipped:
skip/freebsd11.1- Skip tests on FreeBSD 11.1.skip/rhel7.6- Skip tests on RHEL 7.6.
Windows versions, as specified using the --windows option can also be skipped:
skip/windows/2016- Skip tests on Windows Server 2016.skip/windows/2019- Skip tests on Windows Server 2019.skip/windows/2022- Skip tests on Windows Server 2022.
Aliases can be used to skip Python major versions using one of the following:
skip/python2- Skip tests on Python 2.x.skip/python3- Skip tests on Python 3.x.
For more fine grained skipping, use conditionals in integration test playbooks, such as:
when: ansible_distribution in ('Ubuntu')
Miscellaneous
There are several other aliases available as well:
destructive- Requires--allow-destructiveto run without--dockeror--remote.hidden- Target is ignored. Usable as a dependency. Automatic forsetup_andprepare_prefixed targets.retry/never- Target is excluded from retries enabled by the--retry-on-erroroption.gather_facts/yes- Enables gathering facts before running the target (default option).gather_facts/no- Disables gathering facts before running the target.
Unstable
Tests which fail sometimes should be marked with the unstable alias until the instability has been fixed. These tests will continue to run for pull requests which modify the test or the module under test.
This avoids unnecessary test failures for other pull requests, as well as tests on merge runs and nightly CI jobs.
There are two ways to run unstable tests manually:
- Use the
--allow-unstableoption foransible-test - Prefix the test name with
unstable/when passing it toansible-test.
Tests will be marked as unstable by a member of the Ansible Core Team. GitHub issues will be created to track each unstable test.
Disabled
Tests which always fail should be marked with the disabled alias until they can be fixed.
Disabled tests are automatically skipped.
There are two ways to run disabled tests manually:
- Use the
--allow-disabledoption foransible-test - Prefix the test name with
disabled/when passing it toansible-test.
Tests will be marked as disabled by a member of the Ansible Core Team. GitHub issues will be created to track each disabled test.
Unsupported
Tests which cannot be run in CI should be marked with the unsupported alias. Most tests can be supported through the use of simulators and/or cloud plugins.
However, if that is not possible then marking a test as unsupported will prevent it from running in CI.
There are two ways to run unsupported tests manually:
- Use the
--allow-unsupportedoption foransible-test - Prefix the test name with
unsupported/when passing it toansible-test.
Tests will be marked as unsupported by the contributor of the test.
Cloud
Tests for cloud services and other modules that require access to external APIs usually require special support for testing in CI.
These require an additional alias to indicate the required test plugin.
Some of the available aliases are:
cloud/awscloud/azurecloud/cscloud/digitaloceancloud/openshiftcloud/vcenter
Untested
Every module and plugin should have integration tests, even if the tests cannot be run in CI.
Issues
Tests that are marked as unstable or disabled will have an issue created to track the status of the test. Each issue will be assigned to one of the following projects:
Questions
For questions about integration tests reach out to the community on the Ansible Forum<ansible_forum> by creating a topic tagged with ansible-test, testing and other appropriate tags.
line-endings
All files must use \n for line endings instead of \r\n.
mypy
The mypy static type checker is used to check the following code against each Python version supported by the control node:
lib/ansible/test/lib/ansible_test/_internal/
Additionally, the following code is checked against Python versions supported only on managed nodes:
lib/ansible/modules/lib/ansible/module_utils/
no-assert
Do not use assert in production Ansible python code. When running Python with optimizations, Python will remove assert statements, potentially allowing for unexpected behavior throughout the Ansible code base.
Instead of using assert you should utilize simple if statements, that result in raising an exception. There is a new exception called AnsibleAssertionError that inherits from AnsibleError and AssertionError. When possible, utilize a more specific exception than AnsibleAssertionError.
Modules will not have access to AnsibleAssertionError and should instead raise AssertionError, a more specific exception, or just use module.fail_json at the failure point.
no-basestring
Do not use isinstance(s, basestring) as basestring has been removed in Python3. You can import string_types, binary_type, or text_type from ansible.module_utils.six and then use isinstance(s, string_types) or isinstance(s, (binary_type, text_type)) instead.
If this is part of code to convert a string to a particular type, ansible.module_utils.common.text.converters contains several functions that may be even better for you: to_text, to_bytes, and to_native.
no-dict-iteritems
The dict.iteritems method has been removed in Python 3. There are two recommended alternatives:
for KEY, VALUE in DICT.items():
pass
from ansible.module_utils.six import iteritems
for KEY, VALUE in iteritems(DICT):
pass
no-dict-iterkeys
The dict.iterkeys method has been removed in Python 3. Use the following instead:
for KEY in DICT:
pass
no-dict-itervalues
The dict.itervalues method has been removed in Python 3. There are two recommended alternatives:
for VALUE in DICT.values():
pass
from ansible.module_utils.six import itervalues
for VALUE in itervalues(DICT):
pass
no-get-exception
We created a function, ansible.module_utils.pycompat24.get_exception to help retrieve exceptions in a manner compatible with Python 2.4 through Python 3.6. We no longer support Python 2.4 and Python 2.5 so this is extraneous and we want to deprecate the function. Porting code should look something like this:
# Unfixed code:
try:
raise IOError('test')
except IOError:
e = get_exception()
do_something(e)
except:
e = get_exception()
do_something_else(e)
# After fixing:
try:
raise IOError('test')
except IOErrors as e:
do_something(e)
except Exception as e:
do_something_else(e)
no-illegal-filenames
Files and directories should not contain illegal characters or names so that Ansible can be checked out on any Operating System.
Illegal Characters
The following characters are not allowed to be used in any part of the file or directory name;
<>:"/\|?*- Any characters whose integer representations are in the range from 0 through to 31 like
\n
The following characters are not allowed to be used as the last character of a file or directory;
." "(just the space character)
Illegal Names
The following names are not allowed to be used as the name of a file or directory excluding the extension;
CONPRNAUXNULCOM1COM2COM3COM4COM5COM6COM7COM8COM9LPT1LPT2LPT3LPT4LPT5LPT6LPT7LPT8LPT9
For example, the file folder/COM1, folder/COM1.txt are illegal but folder/COM1-file or folder/COM1-file.txt is allowed.
no-main-display
As of Ansible 2.8, Display should no longer be imported from __main__.
Display is now a singleton and should be utilized like the following:
from ansible.utils.display import Display
display = Display()
There is no longer a need to attempt from __main__ import display inside a try/except block.
no-smart-quotes
Smart quotes (”“‘’) should not be used. Use plain ascii quotes ("') instead.
no-tests-as-filters
orphan
no-tests-as-filters
Using Ansible provided Jinja2 tests as filters will be removed in Ansible 2.9.
Prior to Ansible 2.5, Jinja2 tests included within Ansible were most often used as filters. The large difference in use is that filters are referenced as variable | filter_name while Jinja2 tests are referenced as variable is test_name.
Jinja2 tests are used for comparisons, whereas filters are used for data manipulation, and have different applications in Jinja2. This change is to help differentiate the concepts for a better understanding of Jinja2, and where each can be appropriately used.
As of Ansible 2.5 using an Ansible provided Jinja2 test with filter syntax will display a deprecation error.
no-unicode-literals
The use of from __future__ import unicode_literals has been deemed an anti-pattern. The problems with it are:
- It makes it so one can't jump into the middle of a file and know whether a bare literal string is a byte string or text string. The programmer has to first check the top of the file to see if the import is there.
- It removes the ability to define native strings (a string which should be a byte string on python2 and a text string on python3) by a string literal.
- It makes for more context switching. A programmer could be reading one file which has unicode_literals and know that bare string literals are text strings but then switch to another file (perhaps tracing program execution into a third party library) and have to switch their understanding of what bare string literals are.
no-unwanted-files
Specific file types are allowed in certain directories:
lib- All content must reside in thelib/ansibledirectory.lib/ansible- Only source code with one of the following extensions is allowed:*.cs- C#*.ps1- PowerShell*.psm1- PowerShell*.py- Python
obsolete-files
Directories in the Ansible source tree are sometimes made obsolete. Files should not exist in these directories. The new location (if any) is dependent on which directory has been made obsolete.
Below are some of the obsolete directories and their new locations:
- All of
test/runner/is now undertest/lib/ansible_test/instead. The organization of files in the new directory has changed. - Most subdirectories of
test/sanity/(with some exceptions) are now undertest/lib/ansible_test/_util/controller/sanity/instead.
This error occurs most frequently for open pull requests which add or modify files in directories which are now obsolete. Make sure the branch you are working from is current so that changes can be made in the correct location.
package-data
Verifies the contents of the built ansible-core sdist and wheel.
pep8
Python static analysis for PEP 8 style guideline compliance.
PEP 8 style guidelines are enforced by pycodestyle on all python files in the repository by default.
Running locally
The PEP 8 check can be run locally as follows:
ansible-test sanity --test pep8 [file-or-directory-path-to-check] ...
pslint
PowerShell static analysis for common programming errors using PSScriptAnalyzer.
pylint
Python static analysis for common programming errors.
pymarkdown
Check Markdown files for syntax and formatting issues.
release-names
Verifies that the most recent release name has been added to ./github/RELEASE_NAMES.yml
replace-urlopen
Use open_url from module_utils instead of urlopen.
required-and-default-attributes
Use only one of default or required with FieldAttribute.
runtime-metadata
Validates the schema for:
- ansible-core's
lib/ansible/config/ansible_builtin_runtime.yml - collection's
meta/runtime.yml
shebang
Most executable files should only use one of the following shebangs:
#!/bin/sh#!/bin/bash -eu#!/bin/bash -eux#!/usr/bin/make#!/usr/bin/env python#!/usr/bin/env bash
This does not apply to Ansible modules, which should not be executable and must always use #!/usr/bin/python.
Some exceptions are permitted. Ask if you have questions.
shellcheck
Static code analysis for shell scripts using the excellent shellcheck tool.
symlinks
Symbolic links are only permitted for files that exist to ensure proper tarball generation during a release.
If other types of symlinks are needed for tests they must be created as part of the test.
test-constraints
Constraints for test requirements should be in test/lib/ansible_test/_data/requirements/constraints.txt.
update-bundled
Check whether any of our known bundled code needs to be updated for a new upstream release.
This test can error in the following ways:
- The bundled code is out of date with regard to the latest release on pypi. Update the code to the new version and update the version in BUNDLED_METADATA to solve this.
- The code is lacking a BUNDLED_METADATA variable. This typically happens when a bundled version is updated and we forget to add a BUNDLED_METADATA variable to the updated file. Once that is added, this error should go away.
- A file has a BUNDLED_METADATA variable but the file isn't specified in
test/sanity/code-smell/update-bundled.py. This typically happens when a new bundled library is added. Add the file to the get_bundled_libs() function in the update-bundled.py test script to solve this error.
BUNDLED_METADATA has the following fields:
pypi_name
Name of the bundled package on pypi
version
Version of the package that we are including here
version_constraints
Optional PEP440 specifier for the version range that we are bundling. Currently, the only valid use of this is to follow a version that is compatible with the Python stdlib when newer versions of the pypi package implement a new API.
use-argspec-type-path
The AnsibleModule argument_spec knows of several types beyond the standard python types. One of these is path. When used, type path ensures that an argument is a string and expands any shell variables and tilde characters.
This test looks for use of os.path.expanduser <python:os.path.expanduser> in modules. When found, it tells the user to replace it with type='path' in the module's argument_spec or list it as a false positive in the test.
use-compat-six
Use six from module_utils instead of six.
validate-modules
Analyze modules for common issues in code and documentation.
Usage
cd /path/to/ansible/source
source hacking/env-setup
ansible-test sanity --test validate-modules
Help
Type ansible-test sanity --test validate-modules -h to display help for using this sanity test.
Extending validate-modules
The validate-modules tool has a schema.py that is used to validate the YAML blocks, such as DOCUMENTATION and RETURNS.
Codes
| Error Code | Type | Level | Sample Message |
| ansible-deprecated-module | Documentation | Error | A module is deprecated and supposed to be removed in the current or an earlier Ansible version |
| collection-deprecated-module | Documentation | Error | A module is deprecated and supposed to be removed in the current or an earlier collection version |
| ansible-deprecated-version | Documentation | Error | A feature is deprecated and supposed to be removed in the current or an earlier Ansible version |
| ansible-module-not-initialized | Syntax | Error | Execution of the module did not result in initialization of AnsibleModule |
| collection-deprecated-version | Documentation | Error | A feature is deprecated and supposed to be removed in the current or an earlier collection version |
| deprecated-date | Documentation | Error | A date before today appears as removed_at_date or in deprecated_aliases |
| deprecation-mismatch | Documentation | Error | Module marked as deprecated or removed in at least one of the file name, its metadata, or in DOCUMENTATION (setting DOCUMENTATION.deprecated for deprecation or removing all Documentation for removed) but not in all three places. |
| doc-choices-do-not-match-spec | Documentation | Error | Value for "choices" from the argument_spec does not match the documentation |
| doc-choices-incompatible-type | Documentation | Error | Choices value from the documentation is not compatible with type defined in the argument_spec |
| doc-default-does-not-match-spec | Documentation | Error | Value for "default" from the argument_spec does not match the documentation |
| doc-default-incompatible-type | Documentation | Error | Default value from the documentation is not compatible with type defined in the argument_spec |
| doc-elements-invalid | Documentation | Error | Documentation specifies elements for argument, when "type" is not list. |
| doc-elements-mismatch | Documentation | Error | Argument_spec defines elements different than documentation does |
| doc-missing-type | Documentation | Error | Documentation doesn't specify a type but argument in argument_spec use default type (str) |
| doc-required-mismatch | Documentation | Error | argument in argument_spec is required but documentation says it is not, or vice versa |
| doc-type-does-not-match-spec | Documentation | Error | Argument_spec defines type different than documentation does |
| documentation-error | Documentation | Error | Unknown DOCUMENTATION error |
| documentation-syntax-error | Documentation | Error | Invalid DOCUMENTATION schema |
| import-before-documentation | Imports | Error | Import found before documentation variables. All imports must appear below DOCUMENTATION/EXAMPLES/RETURN |
| import-error | Documentation | Error | Exception attempting to import module for argument_spec introspection |
| attributes-check-mode | Documentation | Error | If a module documents the check_mode attribute, its support value must be compatible with the supports_check_mode parameter of AnsibleModule |
| attributes-check-mode-details | Documentation | Error | If a module documents the check_mode attribute with support values partial or N/A, it must provide details |
| import-placement | Locations | Warning | Imports should be directly below DOCUMENTATION/EXAMPLES/RETURN |
| imports-improper-location | Imports | Error | Imports should be directly below DOCUMENTATION/EXAMPLES/RETURN |
| incompatible-choices | Documentation | Error | Choices value from the argument_spec is not compatible with type defined in the argument_spec |
| incompatible-default-type | Documentation | Error | Default value from the argument_spec is not compatible with type defined in the argument_spec |
| invalid-argument-name | Documentation | Error | Argument in argument_spec must not be one of 'message', 'syslog_facility' as it is used internally by Ansible Core Engine |
| invalid-argument-spec | Documentation | Error | Argument in argument_spec must be a dictionary/hash when used |
| invalid-argument-spec-options | Documentation | Error | Suboptions in argument_spec are invalid |
| invalid-documentation | Documentation | Error | DOCUMENTATION is not valid YAML |
| invalid-documentation-markup | Documentation | Error | DOCUMENTATION or RETURN contains invalid markup |
| invalid-documentation-options | Documentation | Error | DOCUMENTATION.options must be a dictionary/hash when used |
| invalid-examples | Documentation | Error | EXAMPLES is not valid YAML |
| invalid-extension | Naming | Error | Official Ansible modules must have a .py extension for python modules or a .ps1 for powershell modules |
| invalid-module-schema | Documentation | Error | AnsibleModule schema validation error |
| invalid-removal-version | Documentation | Error | The version at which a feature is supposed to be removed cannot be parsed (for collections, it must be a semantic version) |
| invalid-requires-extension | Naming | Error | Module #AnsibleRequires -CSharpUtil should not end in .cs, Module #Requires should not end in .psm1 |
| missing-doc-fragment | Documentation | Error | DOCUMENTATION fragment missing |
| missing-existing-doc-fragment | Documentation | Warning | Pre-existing DOCUMENTATION fragment missing |
| missing-documentation | Documentation | Error | No DOCUMENTATION provided |
| missing-examples | Documentation | Error | No EXAMPLES provided |
| missing-gplv3-license | Documentation | Error | GPLv3 license header not found |
| missing-module-utils-basic-import | Imports | Warning | Did not find ansible.module_utils.basic import |
| missing-module-utils-import-csharp-requirements | Imports | Error | No Ansible.ModuleUtils or C# Ansible util requirements/imports found |
| missing-powershell-interpreter | Syntax | Error | Interpreter line is not #!powershell |
| missing-python-interpreter | Syntax | Error | Interpreter line is not #!/usr/bin/python |
| missing-return | Documentation | Error | No RETURN documentation provided |
| missing-return-legacy | Documentation | Warning | No RETURN documentation provided for legacy module |
| missing-suboption-docs | Documentation | Error | Argument in argument_spec has sub-options but documentation does not define sub-options |
| module-incorrect-version-added | Documentation | Error | Module level version_added is incorrect |
| module-invalid-version-added | Documentation | Error | Module level version_added is not a valid version number |
| module-utils-specific-import | Imports | Error | module_utils imports should import specific components, not * |
| multiple-utils-per-requires | Imports | Error | Ansible.ModuleUtils requirements do not support multiple modules per statement |
| multiple-csharp-utils-per-requires | Imports | Error | Ansible C# util requirements do not support multiple utils per statement |
| no-default-for-required-parameter | Documentation | Error | Option is marked as required but specifies a default. Arguments with a default should not be marked as required |
| no-log-needed | Parameters | Error | Option name suggests that the option contains a secret value, while no_log is not specified for this option in the argument spec. If this is a false positive, explicitly set no_log=False |
| nonexistent-parameter-documented | Documentation | Error | Argument is listed in DOCUMENTATION.options, but not accepted by the module |
| option-incorrect-version-added | Documentation | Error | version_added for new option is incorrect |
| option-invalid-version-added | Documentation | Error | version_added for option is not a valid version number |
| parameter-invalid | Documentation | Error | Argument in argument_spec is not a valid python identifier |
| parameter-invalid-elements | Documentation | Error | Value for "elements" is valid only when value of "type" is list |
| implied-parameter-type-mismatch | Documentation | Error | Argument_spec implies type="str" but documentation defines it as different data type |
| parameter-type-not-in-doc | Documentation | Error | Type value is defined in argument_spec but documentation doesn't specify a type |
| parameter-alias-repeated | Parameters | Error | argument in argument_spec has at least one alias specified multiple times in aliases |
| parameter-alias-self | Parameters | Error | argument in argument_spec is specified as its own alias |
| parameter-documented-multiple-times | Documentation | Error | argument in argument_spec with aliases is documented multiple times |
| parameter-list-no-elements | Parameters | Error | argument in argument_spec "type" is specified as list without defining "elements" |
| parameter-state-invalid-choice | Parameters | Error | Argument state includes get, list or info as a choice. Functionality should be in an _info or (if further conditions apply) _facts module. |
| python-syntax-error | Syntax | Error | Python SyntaxError while parsing module |
| removal-version-must-be-major | Documentation | Error | According to the semantic versioning specification (https://semver.org/), the only versions in which features are allowed to be removed are major versions (x.0.0) |
| return-syntax-error | Documentation | Error | RETURN is not valid YAML, RETURN fragments missing or invalid |
| return-invalid-version-added | Documentation | Error | version_added for return value is not a valid version number |
| subdirectory-missing-init | Naming | Error | Ansible module subdirectories must contain an __init__.py |
| try-except-missing-has | Imports | Warning | Try/Except HAS_ expression missing |
| undocumented-parameter | Documentation | Error | Argument is listed in the argument_spec, but not documented in the module |
| unidiomatic-typecheck | Syntax | Error | Type comparison using type() found. Use isinstance() instead |
| unknown-doc-fragment | Documentation | Warning | Unknown pre-existing DOCUMENTATION error |
| use-boto3 | Imports | Error | boto import found, new modules should use boto3 |
| use-fail-json-not-sys-exit | Imports | Error | sys.exit() call found. Should be exit_json/fail_json |
| use-module-utils-urls | Imports | Error | requests import found, should use ansible.module_utils.urls instead |
| use-run-command-not-os-call | Imports | Error | os.call used instead of module.run_command |
| use-run-command-not-popen | Imports | Error | subprocess.Popen used instead of module.run_command |
| use-short-gplv3-license | Documentation | Error | GPLv3 license header should be the short form <copyright> for new modules |
| mutually_exclusive-type | Documentation | Error | mutually_exclusive entry contains non-string value |
| mutually_exclusive-collision | Documentation | Error | mutually_exclusive entry has repeated terms |
| mutually_exclusive-unknown | Documentation | Error | mutually_exclusive entry contains option which does not appear in argument_spec (potentially an alias of an option?) |
| required_one_of-type | Documentation | Error | required_one_of entry contains non-string value |
| required_one_of-collision | Documentation | Error | required_one_of entry has repeated terms |
| required_one_of-unknown | Documentation | Error | required_one_of entry contains option which does not appear in argument_spec (potentially an alias of an option?) |
| required_together-type | Documentation | Error | required_together entry contains non-string value |
| required_together-collision | Documentation | Error | required_together entry has repeated terms |
| required_together-unknown | Documentation | Error | required_together entry contains option which does not appear in argument_spec (potentially an alias of an option?) |
| required_if-is_one_of-type | Documentation | Error | required_if entry has a fourth value which is not a bool |
| required_if-requirements-type | Documentation | Error | required_if entry has a third value (requirements) which is not a list or tuple |
| required_if-requirements-collision | Documentation | Error | required_if entry has repeated terms in requirements |
| required_if-requirements-unknown | Documentation | Error | required_if entry's requirements contains option which does not appear in argument_spec (potentially an alias of an option?) |
| required_if-unknown-key | Documentation | Error | required_if entry's key does not appear in argument_spec (potentially an alias of an option?) |
| required_if-key-in-requirements | Documentation | Error | required_if entry contains its key in requirements list/tuple |
| required_if-value-type | Documentation | Error | required_if entry's value is not of the type specified for its key |
| required_by-collision | Documentation | Error | required_by entry has repeated terms |
| required_by-unknown | Documentation | Error | required_by entry contains option which does not appear in argument_spec (potentially an alias of an option?) |
| version-added-must-be-major-or-minor | Documentation | Error | According to the semantic versioning specification (https://semver.org/), the only versions in which features are allowed to be added are major and minor versions (x.y.0) |
yamllint
Check YAML files for syntax and formatting issues.
Testing plugin documentation<span id="testing_module_documentation"></span>
A quick test while developing is to use ansible-doc -t <plugin_type> <name> to see if it renders, you might need to add -M /path/to/module if the module is not somewhere Ansible expects to find it.
Before you submit a plugin for inclusion in ansible-core, you must run tests to ensure that the argspec matches the documentation in your Python file, and that the argspec's and documentation's structure is correct.
The community pages offer more information on testing reStructuredText documentation <testing_documentation_locally> if you extend the Ansible documentation with additional RST files.
To ensure that your module documentation matches your argument_spec:
-
Install required Python packages (drop '--user' in venv/virtualenv):
pip install --user -r test/lib/ansible_test/_data/requirements/sanity.txt -
run the
validate-modulestest:ansible-test sanity --test validate-modules mymodule
If you have Docker or Podman installed, you can also use them with the --docker option, which uses an image that already has all requirements installed:
ansible-test sanity --docker --test validate-modules mymodule
For other plugin types the steps are similar, just adjusting names and paths to the specific type.
httptester
Topics
Overview
httptester is a docker container used to host certain resources required by testing_integration. This is to avoid CI tests requiring external resources (such as Git or package repos) which, if temporarily unavailable, would cause tests to fail.
HTTP Testing endpoint which provides the following capabilities:
- httpbin
- nginx
- SSL
- SNI
- Negotiate Authentication
Source files can be found in the http-test-container repository.
Extending httptester
If you have sometime to improve httptester, open an issue in the http-test-container repository.
Integration tests
Topics
The Ansible integration Test system.
Tests for playbooks, by playbooks.
Some tests may require credentials. Credentials may be specified with credentials.yml.
Some tests may require root.
Every new module and plugin should have integration tests, even if the tests cannot be run on Ansible CI infrastructure. In this case, the tests should be marked with the unsupported alias in aliases file <integration_aliases>.
Quick Start
It is highly recommended that you install and activate the argcomplete Python package. It provides tab completion in bash for the ansible-test test runner.
Configuration
ansible-test command
The example below assumes bin/ is in your $PATH. An easy way to achieve that is to initialize your environment with the env-setup command:
source hacking/env-setup
ansible-test --help
You can also call ansible-test with the full path:
bin/ansible-test --help
integration_config.yml
Making your own version of integration_config.yml can allow for setting some tunable parameters to help run the tests better in your environment. Some tests (for example, cloud tests) will only run when access credentials are provided. For more information about supported credentials, refer to the various cloud-config-*.template files in the tests/integration/ directory.
Prerequisites
Some tests assume things like hg, svn, and git are installed, and in path. Some tests (such as those for Amazon Web Services) need separate definitions, which will be covered later in this document.
(Complete list pending)
Non-destructive Tests
These tests will modify files in subdirectories, but will not do things that install or remove packages or things outside of those test subdirectories. They will also not reconfigure or bounce system services.
Running integration tests within containers
To protect your system from any potential changes caused by integration tests, and to ensure a sensible set of dependencies are available we recommend that you always run integration tests with the --docker option, for example --docker ubuntu. Get the list of supported container images by running ansible-test integration --help. You can find them in the target docker images section of the output. The default image is used for sanity and unit tests, as well as for platform independent integration tests such as those for cloud modules.
Run as follows for all POSIX platform tests executed by our CI system in a Fedora 34 container:
ansible-test integration shippable/ --docker fedora
You can exclude a specific test as well, such as for individual modules:
ansible-test integration --exclude git
You can target a specific test as well, such as for individual modules:
ansible-test integration ping
You can use the -v option to make the output more verbose:
ansible-test integration lineinfile -vvv
Use the following command to list all the available targets:
ansible-test integration --list-targets
Bash users
If you use bash with argcomplete, obtain a full list by doing: ansible-test integration <tab><tab>
Destructive Tests
These tests are allowed to install and remove some trivial packages. You will likely want to devote these to a virtual environment, such as Docker. They won't reformat your filesystem:
ansible-test integration destructive/ --docker fedora
Windows Tests
These tests exercise the winrm connection plugin and Windows modules. You'll need to define an inventory with a remote Windows Server to use for testing, and enable PowerShell Remoting to continue.
Running these tests may result in changes to your Windows host, so don't run them against a production/critical Windows environment.
Enable PowerShell Remoting (run on the Windows host by a Remote Desktop):
Enable-PSRemoting -Force
Define Windows inventory:
cp inventory.winrm.template inventory.winrm
${EDITOR:-vi} inventory.winrm
Run the Windows tests executed by our CI system:
ansible-test windows-integration -v shippable/
Tests in containers
If you have a Linux system with Docker or Podman installed, running integration tests using the same containers used by the Ansible continuous integration (CI) system is recommended.
Podman
By default, Podman will only be used if the Docker CLI is not installed. If you have Docker installed but want to use Podman, you can change this behavior by setting the environment variable ANSIBLE_TEST_PREFER_PODMAN.
Docker on non-Linux
Using Docker Engine to run Docker on a non-Linux host (such as macOS) is not recommended. Some tests may fail, depending on the image used for testing. Using the --docker-privileged option when running integration (not network-integration or windows-integration) may resolve the issue.
Running Integration Tests
To run all CI integration test targets for POSIX platforms in a Ubuntu container:
ansible-test integration shippable/ --docker ubuntu
You can also run specific tests or select a different Linux distribution. For example, to run tests for the ping module on a Ubuntu container:
ansible-test integration ping --docker ubuntu
Container Images
Container images are updated regularly. To see the current list of container images:
ansible-test integration --help
The list is under the target docker images and supported python version heading.
Other configuration for Cloud Tests
To run some tests, you must provide access credentials in a file named cloud-config-aws.yml or cloud-config-cs.ini in the tests/integration directory. Corresponding .template files are available for syntax help. The newer AWS tests now use the file tests/integration/cloud-config-aws.yml.
IAM policies for AWS
Ansible needs fairly wide ranging powers to run the tests in an AWS account. These rights can be provided to a dedicated user. These need to be configured before running the test.
testing-policies
The GitHub repository mattclay/aws-terminator contains two sets of policies used for all existing AWS module integration tests. The hacking/aws_config/setup_iam.yml playbook can be used to setup two groups:
- ansible-integration-ci will have the policies applied necessary to run any integration tests not marked as unsupported and are designed to mirror those used by Ansible's CI.
- ansible-integration-unsupported will have the additional policies applied necessary to run the integration tests marked as unsupported including tests for managing IAM roles, users and groups.
Once the groups have been created, you'll need to create a user and make the user a member of these groups. The policies are designed to minimize the rights of that user. Please note that while this policy does limit the user to one region, this does not fully restrict the user (primarily due to the limitations of the Amazon ARN notation). The user will still have wide privileges for viewing account definitions, and will also be able to manage some resources that are not related to testing (for example, AWS lambdas with different names). Tests should not be run in a primary production account in any case.
Other Definitions required
Apart from installing the policy and giving it to the user identity running the tests, a lambda role ansible_integration_tests has to be created which has lambda basic execution privileges.
Network Tests
For guidance on writing network test see testing_resource_modules.
Where to find out more
If you'd like to know more about the plans for improving testing Ansible, join the Ansible community forum
Testing Ansible and Collections
This document describes how to run tests using ansible-test.
Setup
Before running ansible-test, set up your environment for testing_an_ansible_collection or testing_ansible_core, depending on which scenario applies to you.
If you use git for version control, make sure the files you are working with are not ignored by git. If they are, ansible-test will ignore them as well.
Testing an Ansible Collection
If you are testing an Ansible Collection, you need a copy of the collection, preferably a Git clone. For example, to work with the community.windows collection, follow these steps:
-
Clone the collection you want to test into a valid collection root:
git clone https://github.com/ansible-collections/community.windows ~/dev/ansible_collections/community/windowsThe path must end with
/ansible_collections/{collection_namespace}/{collection_name}where{collection_namespace}is the namespace of the collection and{collection_name}is the collection name. -
Clone any collections on which the collection depends:
git clone https://github.com/ansible-collections/ansible.windows ~/dev/ansible_collections/ansible/windowsIf your collection has any dependencies on other collections, they must be in the same collection root, since
ansible-testwill not use your configured collection roots (or other Ansible configuration).See the collection's
galaxy.ymlfor a list of possible dependencies. -
Switch to the directory where the collection to test resides:
cd ~/dev/ansible_collections/community/windows
Testing ansible-core
If you are testing ansible-core itself, you need a copy of the ansible-core source code, preferably a Git clone. Having an installed copy of ansible-core is not sufficient or required. For example, to work with the ansible-core source cloned from GitHub, follow these steps:
-
Clone the
ansible-corerepository:git clone https://github.com/ansible/ansible ~/dev/ansible -
Switch to the directory where the
ansible-coresource resides:cd ~/dev/ansible -
Add
ansible-coreprograms to yourPATH:source hacking/env-setupYou can skip this step if you only need to run
ansible-test, and not otheransible-coreprograms. In that case, simply runbin/ansible-testfrom the root of theansible-coresource.If you have an installed version of
ansible-coreand are trying to runansible-testfrom yourPATH, make sure the program found by your shell is the one from theansible-coresource:which ansible-test
Commands
The most commonly used test commands are:
ansible-test sanity- Run sanity tests (mostly linters and static analysis).ansible-test integration- Run integration tests.ansible-test units- Run unit tests.
Run ansible-test --help to see a complete list of available commands.
For detailed help on a specific command, add the --help option after the command.
Environments
Most ansible-test commands support running in one or more isolated test environments to simplify testing.
Containers
Containers are recommended for running sanity, unit and integration tests, since they provide consistent environments. Unit tests will be run with network isolation, which avoids unintentional dependencies on network resources.
The --docker option runs tests in a container using either Docker or Podman.
If both Docker and Podman are installed, Docker will be used. To override this, set the environment variable ANSIBLE_TEST_PREFER_PODMAN to any non-empty value.
Choosing a container
Without an additional argument, the --docker option uses the default container. To use another container, specify it immediately after the --docker option.
The default container is recommended for all sanity and unit tests.
To see the list of supported containers, use the --help option with the ansible-test command you want to use.
The list of available containers is dependent on the ansible-test command you are using.
You can also specify your own container. When doing so, you will need to indicate the Python version in the container with the --python option.
Custom containers
When building custom containers, keep in mind the following requirements:
- The
USERshould beroot. - Use an
initprocess, such assystemd. - Include
sshdand accept connections on the default port of22. - Include a POSIX compatible
shshell which can be found onPATH. - Include a
sleeputility which runs as a subprocess. - Include a supported version of Python.
- Avoid using the
VOLUMEstatement.
Docker and SELinux
Using Docker on a host with SELinux may require setting the system in permissive mode. Consider using Podman instead.
Docker Desktop with WSL2
These instructions explain how to use ansible-test with WSL2 and Docker Desktop without systemd support.
If your WSL2 environment includes systemd support, these steps are not required.
Configuration requirements
- Open Docker Desktop and go to the Settings screen.
- On the General tab:
- Uncheck the Start Docker Desktop when you log in checkbox.
- Check the Use the WSL 2 based engine checkbox.
- On the Resources tab under the WSL Integration section:
- Enable distros you want to use under the Enable integration with additional distros section.
- Click Apply and restart if changes were made.
Setup instructions
If all WSL instances have been stopped, these changes will need to be re-applied.
-
Verify Docker Desktop is properly configured (see
configuration_requirements). -
Quit Docker Desktop if it is running:
- Right click the Docker Desktop taskbar icon.
- Click the Quit Docker Desktop option.
-
Stop any running WSL instances with the command:
wsl --shutdown -
Verify all WSL instances have stopped with the command:
wsl -l -v -
Start a WSL instance and perform the following steps as
root:- Verify the
systemdsubsystem is not registered:-
Check for the
systemdcgroup hierarchy with the following command:grep systemd /proc/self/cgroup -
If any matches are found, re-check the
configuration_requirementsand follow thesetup_instructionsagain.
-
- Mount the
systemdcgroup hierarchy with the following commands:
mkdir /sys/fs/cgroup/systemd mount cgroup -t cgroup /sys/fs/cgroup/systemd -o none,name=systemd,xattr - Verify the
-
Start Docker Desktop.
You should now be able to use ansible-test with the --docker option.
Linux cgroup configuration
These changes will need to be re-applied each time the container host is booted.
For certain container hosts and container combinations, additional setup on the container host may be required. In these situations ansible-test will report an error and provide additional instructions to run as root:
mkdir /sys/fs/cgroup/systemd
mount cgroup -t cgroup /sys/fs/cgroup/systemd -o none,name=systemd,xattr
If you are using rootless Podman, an additional command must be run, also as root. Make sure to substitute your user and group for {user} and {group} respectively:
chown -R {user}:{group} /sys/fs/cgroup/systemd
Podman
When using Podman, you may need to stop existing Podman processes after following the linux_cgroup_configuration instructions. Otherwise Podman may be unable to see the new mount point.
You can check to see if Podman is running by looking for podman and catatonit processes.
Remote virtual machines
Remote virtual machines are recommended for running integration tests not suitable for execution in containers.
The --remote option runs tests in a cloud hosted ephemeral virtual machine.
An API key is required to use this feature, unless running under an approved Azure Pipelines organization.
To see the list of supported systems, use the --help option with the ansible-test command you want to use.
The list of available systems is dependent on the ansible-test command you are using.
Python virtual environments
Python virtual environments provide a simple way to achieve isolation from the system and user Python environments. They are recommended for unit and integration tests when the --docker and --remote options cannot be used.
The --venv option runs tests in a virtual environment managed by ansible-test. Requirements are automatically installed before tests are run.
Composite environment arguments
The environment arguments covered in this document are sufficient for most use cases. However, some scenarios may require the additional flexibility offered by composite environment arguments.
The --controller and --target options are alternatives to the --docker, --remote and --venv options.
When using the shell command, the --target option is replaced by three platform specific options.
Add the --help option to your ansible-test command to learn more about the composite environment arguments.
Additional Requirements
Some ansible-test commands have additional requirements. You can use the --requirements option to automatically install them.
When using a test environment managed by ansible-test the --requirements option is usually unnecessary.
Environment variables
When using environment variables to manipulate tests there some limitations to keep in mind. Environment variables are:
-
Not propagated from the host to the test environment when using the
--dockeror--remoteoptions. -
Not exposed to the test environment unless enabled in
test/lib/ansible_test/_internal/util.pyin thecommon_environmentfunction.Example:
ANSIBLE_KEEP_REMOTE_FILES=1can be set when runningansible-test integration --venv. However, using the--dockeroption would require runningansible-test shellto gain access to the Docker environment. Once at the shell prompt, the environment variable could be set and the tests executed. This is useful for debugging tests inside a container by following thedebugging_modulesinstructions.
Interactive shell
Use the ansible-test shell command to get an interactive shell in the same environment used to run tests. Examples:
ansible-test shell --docker- Open a shell in the default docker container.ansible-test shell --venv --python 3.13- Open a shell in a Python 3.13 virtual environment.
Code coverage
Code coverage reports make it easy to identify untested code for which more tests should be written. Online reports are available but only cover the devel branch (see developing_testing). For new code local reports are needed.
Add the --coverage option to any test command to collect code coverage data. If you aren't using the --venv or --docker options which create an isolated python environment then you may have to use the --requirements option to ensure that the correct version of the coverage module is installed:
ansible-test coverage erase
ansible-test units --coverage apt
ansible-test integration --coverage aws_lambda
ansible-test coverage html
Reports can be generated in several different formats:
ansible-test coverage report- Console report.ansible-test coverage html- HTML report.ansible-test coverage xml- XML report.
To clear data between test runs, use the ansible-test coverage erase command.
Sanity Tests
Topics
Sanity tests are made up of scripts and tools used to perform static code analysis. The primary purpose of these tests is to enforce Ansible coding standards and requirements.
Tests are run with ansible-test sanity. All available tests are run unless the --test option is used.
Set up your environment
Install ansible-core<installation_guide>that provides theansible-testtool.
- If you want to run checks available in the development version of
ansible-core,install it from source code<from_source>.
- Run
source hacking/env-setupfrom its source code directory in the same terminal session you run your tests.
- Install
podmanordockerto avoid installing all the dependencies on your system. - If you test files in a collection:
- Ensure you have your collection installed in the following path in your home directory:
~/ansible_collections/<NAMESPACE>/<COLLECTION_NAME>. For instance, in case of thecommunity.generalcollection, it will be~/ansible_collections/community/general- If your collection is hosted on a remote server such as GitHub, clone it to that path as follows:
git clone <COLLECTION_REPO_URL> ~/ansible_collections/<NAMESPACE>/<COLLECTION_NAME>
How to run
To run sanity tests using podman or docker, always use the default docker image by passing the --docker argument without specifying the image name.
- When testing files in a collection, change your location to your collection directory you created while
setting up your environment<set_up_env_to_run_tests>:
cd ~/ansible_collections/<NAMESPACE>/<COLLECTION_NAME>
- To run all sanity tests in a container:
ansible-test sanity --docker
- To run a
specific test<all_sanity_tests>, add the--test <NAME>argument, for example,--test validate-modules.- To list available tests, run:
ansible-test sanity --list-tests
- To list available tests, run:
- To include disabled tests, add the
--allow-disabledargument.
Available Tests
See the full list of sanity tests <all_sanity_tests>, which also details how to fix identified issues.
Unit Tests
Unit tests are small isolated tests that target a specific library or module. Unit tests in Ansible are currently the only way of driving tests from python within Ansible's continuous integration process. This means that in some circumstances the tests may be a bit wider than just units.
Topics
Available Tests
Unit tests can be found in test/units. Notice that the directory structure of the tests matches that of lib/ansible/.
Running Tests
To run unit tests using docker, always use the default docker image by passing the --docker or --docker default argument.
The Ansible unit tests can be run across the whole code base by doing:
cd /path/to/ansible/source
source hacking/env-setup
ansible-test units --docker -v
Against a single file by doing:
ansible-test units --docker -v apt
Or against a specific Python version by doing:
ansible-test units --docker -v --python 3.13 apt
If you are running unit tests against things other than modules, such as module utilities, specify the whole file path:
ansible-test units --docker -v test/units/module_utils/basic/test_imports.py
For advanced usage see the online help:
ansible-test units --help
You can also run tests in Ansible's continuous integration system by opening a pull request. This will automatically determine which tests to run based on the changes made in your pull request.
Installing dependencies
If you are running ansible-test with the --docker or --venv option you do not need to install dependencies manually.
Otherwise you can install dependencies using the --requirements option, which will install all the required dependencies needed for unit tests. For example:
ansible-test units --python 3.12 --requirements apache2_module
The list of unit test requirements can be found at test/units/requirements.txt.
This does not include the list of unit test requirements for ansible-test itself, which can be found at test/lib/ansible_test/_data/requirements/units.txt.
See also the constraints applicable to all test commands.
Extending unit tests
What a unit test isn't
If you start writing a test that requires external services then you may be writing an integration test, rather than a unit test.
Structuring Unit Tests
Ansible drives unit tests through pytest. This means that tests can either be written a simple functions which are included in any file name like test_<something>.py or as classes.
Here is an example of a function:
#this function will be called simply because it is called test_*()
def test_add():
a = 10
b = 23
c = 33
assert a + b == c
Here is an example of a class:
import unittest
class AddTester(unittest.TestCase):
def SetUp():
self.a = 10
self.b = 23
# this function will
def test_add():
c = 33
assert self.a + self.b == c
# this function will
def test_subtract():
c = -13
assert self.a - self.b == c
Both methods work fine in most circumstances; the function-based interface is simpler and quicker and so that's probably where you should start when you are just trying to add a few basic tests for a module. The class-based test allows more tidy set up and tear down of pre-requisites, so if you have many test cases for your module you may want to refactor to use that.
Assertions using the simple assert function inside the tests will give full information on the cause of the failure with a trace-back of functions called during the assertion. This means that plain asserts are recommended over other external assertion libraries.
A number of the unit test suites include functions that are shared between several modules, especially in the networking arena. In these cases a file is created in the same directory, which is then included directly.
Module test case common code
Keep common code as specific as possible within the test/units/ directory structure. Don't import common unit test code from directories outside the current or parent directories.
Don't import other unit tests from a unit test. Any common code should be in dedicated files that aren't themselves tests.
Fixtures files
To mock out fetching results from devices, or provide other complex data structures that come from external libraries, you can use fixtures to read in pre-generated data.
You can check how fixtures are used in CPU_INFO_TEST_SCENARIOS
If you are simulating APIs you may find that Python placebo is useful. See testing_units_modules for more information.
Code Coverage For New or Updated Unit Tests
New code will be missing from the codecov.io coverage reports (see developing_testing), so local reporting is needed. Most ansible-test commands allow you to collect code coverage; this is particularly useful when to indicate where to extend testing.
To collect coverage data add the --coverage argument to your ansible-test command line:
ansible-test units --coverage apt
ansible-test coverage html
Results will be written to test/results/reports/coverage/index.html
Reports can be generated in several different formats:
ansible-test coverage report- Console report.ansible-test coverage html- HTML report.ansible-test coverage xml- XML report.
To clear data between test runs, use the ansible-test coverage erase command. See testing_running_locally for more information about generating coverage reports.
testing_units_modules
Special considerations for unit testing modules
testing_running_locally
Running tests locally including gathering and reporting coverage data
Python 3 documentation - 26.4. unittest — Unit testing framework
The documentation of the unittest framework in python 3
Python 2 documentation - 25.3. unittest — Unit testing framework
The documentation of the earliest supported unittest framework - from Python 2.6
pytest: helps you write better programs
The documentation of pytest - the framework actually used to run Ansible unit tests
Unit Testing Ansible Modules
Topics
Introduction
This document explains why, how and when you should use unit tests for Ansible modules. The document doesn't apply to other parts of Ansible for which the recommendations are normally closer to the Python standard. There is basic documentation for Ansible unit tests in the developer guide testing_units. This document should be readable for a new Ansible module author. If you find it incomplete or confusing, please open a bug or ask for help on the Ansible Forum<ansible_forum>.
What Are Unit Tests?
Ansible includes a set of unit tests in the test/units directory. These tests primarily cover the internals but can also cover Ansible modules. The structure of the unit tests matches the structure of the code base, so the tests that reside in the test/units/modules/ directory are organized by module groups.
Integration tests can be used for most modules, but there are situations where cases cannot be verified using integration tests. This means that Ansible unit test cases may extend beyond testing only minimal units and in some cases will include some level of functional testing.
Why Use Unit Tests?
Ansible unit tests have advantages and disadvantages. It is important to understand these. Advantages include:
- Most unit tests are much faster than most Ansible integration tests. The complete suite of unit tests can be run regularly by a developer on their local system.
- Unit tests can be run by developers who don't have access to the system which the module is designed to work on, allowing a level of verification that changes to core functions haven't broken module expectations.
- Unit tests can easily substitute system functions allowing testing of software that would be impractical. For example, the
sleep()function can be replaced and we check that a ten minute sleep was called without actually waiting ten minutes. - Unit tests are run on different Python versions. This allows us to ensure that the code behaves in the same way on different Python versions.
There are also some potential disadvantages of unit tests. Unit tests don't normally directly test actual useful valuable features of software, instead just internal implementation
- Unit tests that test the internal, non-visible features of software may make refactoring difficult if those internal features have to change (see also naming in How below)
- Even if the internal feature is working correctly it is possible that there will be a problem between the internal code tested and the actual result delivered to the user
Normally the Ansible integration tests (which are written in Ansible YAML) provide better testing for most module functionality. If those tests already test a feature and perform well there may be little point in providing a unit test covering the same area as well.
When To Use Unit Tests
There are a number of situations where unit tests are a better choice than integration tests. For example, testing things which are impossible, slow or very difficult to test with integration tests, such as:
- Forcing rare / strange / random situations that can't be forced, such as specific network failures and exceptions
- Extensive testing of slow configuration APIs
- Situations where the integration tests cannot be run as part of the main Ansible continuous integration running in Azure Pipelines.
Providing quick feedback
Example:
A single step of the rds_instance test cases can take up to 20 minutes (the time to create an RDS instance in Amazon). The entire test run can last for well over an hour. All 16 of the unit tests complete execution in less than 2 seconds.
The time saving provided by being able to run the code in a unit test makes it worth creating a unit test when bug fixing a module, even if those tests do not often identify problems later. As a basic goal, every module should have at least one unit test which will give quick feedback in easy cases without having to wait for the integration tests to complete.
Ensuring correct use of external interfaces
Unit tests can check the way in which external services are run to ensure that they match specifications or are as efficient as possible even when the final output will not be changed.
Example:
Package managers are often far more efficient when installing multiple packages at once rather than each package separately. The final result is the same: the packages are all installed, so the efficiency is difficult to verify through integration tests. By providing a mock package manager and verifying that it is called once, we can build a valuable test for module efficiency.
Providing specific design tests
By building a requirement for a particular part of the code and then coding to that requirement, unit tests can_ sometimes improve the code and help future developers understand that code.
Unit tests that test internal implementation details of code, on the other hand, almost always do more harm than good. Testing that your packages to install are stored in a list would slow down and confuse a future developer who might need to change that list into a dictionary for efficiency. This problem can be reduced somewhat with clear test naming so that the future developer immediately knows to delete the test case, but it is often better to simply leave out the test case altogether and test for a real valuable feature of the code, such as installing all of the packages supplied as arguments to the module.
How to unit test Ansible modules
There are a number of techniques for unit testing modules. Beware that most modules without unit tests are structured in a way that makes testing quite difficult and can lead to very complicated tests which need more work than the code. Effectively using unit tests may lead you to restructure your code. This is often a good thing and leads to better code overall. Good restructuring can make your code clearer and easier to understand.
Naming unit tests
Unit tests should have logical names. If a developer working on the module being tested breaks the test case, it should be easy to figure what the unit test covers from the name. If a unit test is designed to verify compatibility with a specific software or API version then include the version in the name of the unit test.
As an example, test_v2_state_present_should_call_create_server_with_name() would be a good name, test_create_server() would not be.
Use of Mocks
Mock objects (from https://docs.python.org/3/library/unittest.mock.html) can be very useful in building unit tests for special / difficult cases, but they can also lead to complex and confusing coding situations. One good use for mocks would be in simulating an API. As for 'six', the 'mock' python package is bundled with Ansible (use import units.compat.mock).
Ensuring failure cases are visible with mock objects
Functions like module.fail_json are normally expected to terminate execution. When you run with a mock module object this doesn't happen since the mock always returns another mock from a function call. You can set up the mock to raise an exception as shown above, or you can assert that these functions have not been called in each test. For example:
module = MagicMock()
function_to_test(module, argument)
module.fail_json.assert_not_called()
This applies not only to calling the main module but almost any other function in a module which gets the module object.
Mocking of the actual module
The setup of an actual module is quite complex (see Passing Arguments below) and often isn't needed for most functions which use a module. Instead you can use a mock object as the module and create any module attributes needed by the function you are testing. If you do this, beware that the module exit functions need special handling as mentioned above, either by throwing an exception or ensuring that they haven't been called. For example:
class AnsibleExitJson(Exception):
"""Exception class to be raised by module.exit_json and caught by the test case"""
pass
# you may also do the same to fail json
module = MagicMock()
module.exit_json.side_effect = AnsibleExitJson(Exception)
with self.assertRaises(AnsibleExitJson) as result:
results = my_module.test_this_function(module, argument)
module.fail_json.assert_not_called()
assert results["changed"] == True
API definition with unit test cases
API interaction is usually best tested with the function tests defined in Ansible's integration testing section, which run against the actual API. There are several cases where the unit tests are likely to work better.
Defining a module against an API specification
This case is especially important for modules interacting with web services, which provide an API that Ansible uses but which are beyond the control of the user.
By writing a custom emulation of the calls that return data from the API, we can ensure that only the features which are clearly defined in the specification of the API are present in the message. This means that we can check that we use the correct parameters and nothing else.
Example: in rds_instance unit tests a simple instance state is defined:
def simple_instance_list(status, pending):
return {u'DBInstances': [{u'DBInstanceArn': 'arn:aws:rds:us-east-1:1234567890:db:fakedb',
u'DBInstanceStatus': status,
u'PendingModifiedValues': pending,
u'DBInstanceIdentifier': 'fakedb'}]}
This is then used to create a list of states:
rds_client_double = MagicMock()
rds_client_double.describe_db_instances.side_effect = [
simple_instance_list('rebooting', {"a": "b", "c": "d"}),
simple_instance_list('available', {"c": "d", "e": "f"}),
simple_instance_list('rebooting', {"a": "b"}),
simple_instance_list('rebooting', {"e": "f", "g": "h"}),
simple_instance_list('rebooting', {}),
simple_instance_list('available', {"g": "h", "i": "j"}),
simple_instance_list('rebooting', {"i": "j", "k": "l"}),
simple_instance_list('available', {}),
simple_instance_list('available', {}),
]
These states are then used as returns from a mock object to ensure that the await function waits through all of the states that would mean the RDS instance has not yet completed configuration:
rds_i.await_resource(rds_client_double, "some-instance", "available", mod_mock,
await_pending=1)
assert(len(sleeper_double.mock_calls) > 5), "await_pending didn't wait enough"
By doing this we check that the await function will keep waiting through potentially unusual that it would be impossible to reliably trigger through the integration tests but which happen unpredictably in reality.
Defining a module to work against multiple API versions
This case is especially important for modules interacting with many different versions of software; for example, package installation modules that might be expected to work with many different operating system versions.
By using previously stored data from various versions of an API we can ensure that the code is tested against the actual data which will be sent from that version of the system even when the version is very obscure and unlikely to be available during testing.
Ansible special cases for unit testing
There are a number of special cases for unit testing the environment of an Ansible module. The most common are documented below, and suggestions for others can be found by looking at the source code of the existing unit tests or asking the community<communication>.
Module argument processing
There are two problems with running the main function of a module:
- Since the module is supposed to accept arguments on
STDINit is a bit difficult to set up the arguments correctly so that the module will get them as parameters. - All modules should finish by calling either the
module.fail_jsonormodule.exit_json, but these won't work correctly in a testing environment.
Passing Arguments
To pass arguments to a module correctly, use the set_module_args method which accepts a dictionary as its parameter. Module creation and argument processing is handled through the AnsibleModule object in the basic section of the utilities. Normally this accepts input on STDIN, which is not convenient for unit testing. When the special variable is set it will be treated as if the input came on STDIN to the module. Simply call that function before setting up your module:
import json
from units.modules.utils import set_module_args
from ansible.module_utils.common.text.converters import to_bytes
def test_already_registered(self):
set_module_args({
'activationkey': 'key',
'username': 'user',
'password': 'pass',
})
Handling exit correctly
The module.exit_json function won't work properly in a testing environment since it writes error information to STDOUT upon exit, where it is difficult to examine. This can be mitigated by replacing it (and module.fail_json) with a function that raises an exception:
def exit_json(*args, **kwargs):
if 'changed' not in kwargs:
kwargs['changed'] = False
raise AnsibleExitJson(kwargs)
Now you can ensure that the first function called is the one you expected simply by testing for the correct exception:
def test_returned_value(self):
set_module_args({
'activationkey': 'key',
'username': 'user',
'password': 'pass',
})
with self.assertRaises(AnsibleExitJson) as result:
my_module.main()
The same technique can be used to replace module.fail_json (which is used for failure returns from modules) and for the aws_module.fail_json_aws() (used in modules for Amazon Web Services).
Running the main function
If you do want to run the actual main function of a module you must import the module, set the arguments as above, set up the appropriate exit exception and then run the module:
# This test is based around pytest's features for individual test functions
import pytest
import ansible.modules.module.group.my_module as my_module
def test_main_function(monkeypatch):
monkeypatch.setattr(my_module.AnsibleModule, "exit_json", fake_exit_json)
set_module_args({
'activationkey': 'key',
'username': 'user',
'password': 'pass',
})
my_module.main()
Handling calls to external executables
Module must use AnsibleModule.run_command in order to execute an external command. This method needs to be mocked:
Here is a simple mock of AnsibleModule.run_command (taken from test/units/modules/packaging/os/test_rhn_register.py):
with patch.object(basic.AnsibleModule, 'run_command') as run_command:
run_command.return_value = 0, '', '' # successful execution, no output
with self.assertRaises(AnsibleExitJson) as result:
my_module.main()
self.assertFalse(result.exception.args[0]['changed'])
# Check that run_command has been called
run_command.assert_called_once_with('/usr/bin/command args')
self.assertEqual(run_command.call_count, 1)
self.assertFalse(run_command.called)
A Complete Example
The following example is a complete skeleton that reuses the mocks explained above and adds a new mock for Ansible.get_bin_path:
import json
from units.compat import unittest
from units.compat.mock import patch
from ansible.module_utils import basic
from ansible.module_utils.common.text.converters import to_bytes
from ansible.modules.namespace import my_module
def set_module_args(args):
"""prepare arguments so that they will be picked up during module creation"""
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
basic._ANSIBLE_ARGS = to_bytes(args)
class AnsibleExitJson(Exception):
"""Exception class to be raised by module.exit_json and caught by the test case"""
pass
class AnsibleFailJson(Exception):
"""Exception class to be raised by module.fail_json and caught by the test case"""
pass
def exit_json(*args, **kwargs):
"""function to patch over exit_json; package return data into an exception"""
if 'changed' not in kwargs:
kwargs['changed'] = False
raise AnsibleExitJson(kwargs)
def fail_json(*args, **kwargs):
"""function to patch over fail_json; package return data into an exception"""
kwargs['failed'] = True
raise AnsibleFailJson(kwargs)
def get_bin_path(self, arg, required=False):
"""Mock AnsibleModule.get_bin_path"""
if arg.endswith('my_command'):
return '/usr/bin/my_command'
else:
if required:
fail_json(msg='%r not found !' % arg)
class TestMyModule(unittest.TestCase):
def setUp(self):
self.mock_module_helper = patch.multiple(basic.AnsibleModule,
exit_json=exit_json,
fail_json=fail_json,
get_bin_path=get_bin_path)
self.mock_module_helper.start()
self.addCleanup(self.mock_module_helper.stop)
def test_module_fail_when_required_args_missing(self):
with self.assertRaises(AnsibleFailJson):
set_module_args({})
my_module.main()
def test_ensure_command_called(self):
set_module_args({
'param1': 10,
'param2': 'test',
})
with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command:
stdout = 'configuration updated'
stderr = ''
rc = 0
mock_run_command.return_value = rc, stdout, stderr # successful execution
with self.assertRaises(AnsibleExitJson) as result:
my_module.main()
self.assertFalse(result.exception.args[0]['changed']) # ensure result is changed
mock_run_command.assert_called_once_with('/usr/bin/my_command --value 10 --name test')
Restructuring modules to enable testing module set up and other processes
Often modules have a main() function which sets up the module and then performs other actions. This can make it difficult to check argument processing. This can be made easier by moving module configuration and initialization into a separate function. For example:
argument_spec = dict(
# module function variables
state=dict(choices=['absent', 'present', 'rebooted', 'restarted'], default='present'),
apply_immediately=dict(type='bool', default=False),
wait=dict(type='bool', default=False),
wait_timeout=dict(type='int', default=600),
allocated_storage=dict(type='int', aliases=['size']),
db_instance_identifier=dict(aliases=["id"], required=True),
)
def setup_module_object():
module = AnsibleAWSModule(
argument_spec=argument_spec,
required_if=required_if,
mutually_exclusive=[['old_instance_id', 'source_db_instance_identifier',
'db_snapshot_identifier']],
)
return module
def main():
module = setup_module_object()
validate_parameters(module)
conn = setup_client(module)
return_dict = run_task(module, conn)
module.exit_json(**return_dict)
This now makes it possible to run tests against the module initiation function:
def test_rds_module_setup_fails_if_db_instance_identifier_parameter_missing():
# db_instance_identifier parameter is missing
set_module_args({
'state': 'absent',
'apply_immediately': 'True',
})
with self.assertRaises(AnsibleFailJson) as result:
my_module.setup_json
See also test/units/module_utils/aws/test_rds.py
Note that the argument_spec dictionary is visible in a module variable. This has advantages, both in allowing explicit testing of the arguments and in allowing the easy creation of module objects for testing.
The same restructuring technique can be valuable for testing other functionality, such as the part of the module which queries the object that the module configures.
Traps for maintaining Python 2 compatibility
If you use the mock library from the Python 2.6 standard library, a number of the assert functions are missing but will return as if successful. This means that test cases should take great care not use functions marked as new_ in the Python 3 documentation, since the tests will likely always succeed even if the code is broken when run on older versions of Python.
A helpful development approach to this should be to ensure that all of the tests have been run under Python 2.6 and that each assertion in the test cases has been checked to work by breaking the code in Ansible to trigger that failure.
Maintain Python 2.6 compatibility
Please remember that modules need to maintain compatibility with Python 2.6 so the unittests for modules should also be compatible with Python 2.6.
Communication<communication>
Got questions? Need help? Want to share your ideas? Visit the Ansible communication guide
testing_units
Ansible unit tests documentation
testing_running_locally
Running tests locally including gathering and reporting coverage data
developing_modules_general
Get started developing a module
Python 3 documentation - 26.4. unittest — Unit testing framework
The documentation of the unittest framework in python 3
Python 2 documentation - 25.3. unittest — Unit testing framework
The documentation of the earliest supported unittest framework - from Python 2.6
pytest: helps you write better programs
The documentation of pytest - the framework actually used to run Ansible unit tests
Testing Your Code (from The Hitchhiker's Guide to Python!)
General advice on testing Python code
Uncle Bob's many videos on YouTube
Unit testing is a part of the of various philosophies of software development, including Extreme Programming (XP), Clean Coding. Uncle Bob talks through how to benefit from this
"Why Most Unit Testing is Waste"
An article warning against the costs of unit testing
'A Response to "Why Most Unit Testing is Waste"'
An response pointing to how to maintain the value of unit tests