Skip to content

Commit

Permalink
Recreating changes from PR 41 for new PR (#61)
Browse files Browse the repository at this point in the history
* Recreating changes from PR 41 for new PR - reformatting roles section
  • Loading branch information
bontreger committed Jun 29, 2022
1 parent 74c7108 commit fcb5558
Showing 1 changed file with 49 additions and 79 deletions.
128 changes: 49 additions & 79 deletions roles/README.adoc
Original file line number Diff line number Diff line change
@@ -1,63 +1,45 @@
= Roles Good Practices for Ansible

NOTE: this section has been imported "as-is" from the https://github.com/oasis-roles/meta_standards[OASIS metastandards repository] and still requires re-formatting to fit the overall structure.
NOTE: this section has been rewritten, using https://github.com/oasis-roles/meta_standards[OASIS metastandards repository] as a starting place. If you have anything to add or review, please comment.

== Background
[%collapsible]
====
The goal of the Ansible Metateam project (specifically, the https://github.com/linux-system-roles[Linux System Roles project]) is to provide a stable and consistent user interface to multiple operating systems (multiple versions of RHEL in the downstream RHEL System Roles package, additionally CentOS, Fedora at least).
Stable and consistent means that the same Ansible playbook will be usable to manage the equivalent functionality in the supported versions without the administrator (the user of the role) being forced to change anything in the playbook (the roles should serve as abstractions to shield the administrator from differences).
Of course, this means that the interface of the roles should be itself stable (i.e. changing only in a backward compatible way).
This implies a great responsibility in the design of the interface, because the interface, unlike the underlying implementation, can not be easily changed.
The differences in the underlying operating systems that the roles need to compensate for are basically of two types:
* Trivial differences like changed names of packages, services, changed location of configuration files.
Roles must deals with those by using internal variables based on the OS defaults.
This is fairly simple, but still it brings value to the user, because they then do not have to worry about keeping up with such trivial changes.
* Change of the underlying implementation of a given functionality.
Quite often, there are multiple packages/components implementing the same functionality.
Classic examples are the various MTAs (sendmail, postfix, qmail, exim), FTP daemons, etc. In the context of Linux System Roles, we call them "`providers`".
The goal of the roles is to abstract even such differences, so that when the OS changes to a different component (provider), the role continues to work.
An example is time synchronization, where RHEL used to use the ntpd package, then chrony was introduced and became the default, but both components have been shipped in RHEL 6 and RHEL 7, until finally ntpd was dropped from RHEL 8, leaving only chrony.
A role covering time synchronization should therefore support both components with the same interface, and on systems which ship both components, both should be supported.
The appropriate supported component should be automatically selected on systems that ship only one of them.
This covers several related use cases:
** Users that want to manage multiple major releases of the system simultaneously with a single playbook.
** Users that want to migrate to a new version of the system without changing their automation (playbook).
** Users who want to switch to a different provider in the same version of the OS (like switching from ntpd to chrony to RHEL 7) and keep the same playbook.
Designing the interface in the latter case is difficult because it has to be sufficiently abstract to cover different providers.
We, for example, do not provide an email role in the Linux System Roles project, only a postfix role, because the underlying implementations (sendmail, postfix) were deemed to be too divergent.
Generally, an abstract interface should be something that should be always aimed for though, especially if there are multiple providers in use already, and in
particular when the default provider is changing or is known to be likely to change in the next major releases.
====

== Basics
== Role design considerations

=== Basic design
[%collapsible]
====
* Every repository in the AGP-roles namespace should be a valid Ansible Galaxy compatible role with the exception of any whose names begin with "meta_", such as this one.
* New roles should be initiated in line with the skeleton directory, which has standard boilerplate code for a Galaxy-compatible Ansible role and some enforcement around these standards
* Use https://semver.org/[semantic versioning] for Git release tags.
Use 0.y.z before the role is declared stable (interface-wise).
Although it has not been a problem so far for linux system roles, since they use strict X.Y.Z versioning, you should be aware that there are some
https://github.com/ansible/ansible/issues/67512[restrictions] for Ansible
Galaxy and Automation Hub.
The versioning must be in strict X.Y.Z[ab][W] format, where X, Y, and Z are integers.
====
Explanations:: Design roles focused on the functionality provided, not the software implementation.
== Interface design considerations
Rationale::
Try to design roles focused on the functionality, not on the software implementation behind it.
This will help abstracting differences between different providers, and help the user to focus on the functionality, not on technical details.
What should a role do and how can a user tell it what to do.
Examples::
For an example, designing a role to implement an NTP configuration on a server would be a role.
The role internally would have the logic to decide whether to use ntpd, chronyd, and the ntp site configurations.
However, when the underlying implementations become too divergent, for example implementing an email server with postfix or sendmail, then
separate roles are encouraged.
=== Basic design
====

=== Role Structure
[%collapsible]
====
Try to design the interface focused on the functionality, not on the software implementation behind it.
This will help abstracting differences between different providers (see above), and help the user to focus on the functionality, not on technical details.
Explanations:: New roles should be initiated in line, with the skeleton directory, which has standard boilerplate code for a Galaxy-compatible
Ansible role and some enforcement around these standards
Rationale:: A consistent file tree structure will help drive consistency and reusability across the entire environment.
====

=== Naming things
=== Role Distribution
[%collapsible]
====
Explanations:: Use https://semver.org/[semantic versioning] for Git release tags.
Use 0.y.z before the role is declared stable (interface-wise).
Rationale:: There are some https://github.com/ansible/ansible/issues/67512[restrictions] for Ansible Galaxy and Automation Hub.
The versioning must be in strict X.Y.Z[ab][W] format, where X, Y, and Z are integers.
=== Naming parameters
[%collapsible]
====
* All defaults and all arguments to a role should have a name that begins with the role name to help avoid collision with other names.
Expand Down Expand Up @@ -99,23 +81,21 @@ Moreover, after a major upgrade of their systems, it may force the users to chan
Exporting `$ROLENAME_provider_os_default` allows the users to set `$ROLENAME_provider: "{{ $ROLENAME_provider_os_default }}"` (thanks to the lazy variable evaluation in Ansible) and thus get a consistent setting for all the systems of the given OS version without having to decide what the actual value is - the decision is delegated to the role).
====
== Implementation considerations

=== Role Structure
=== Distributions and Versions
[%collapsible]
====
Avoid testing for distribution and version in tasks.
Rather add a variable file to "vars/" for each supported distribution and version with the variables that need to change according to the distribution and version.
This way it is easy to add support to a new distribution by
simply dropping a new file in to "vars/", see below
<<supporting-multiple-distributions-and-versions,Supporting multiple distributions and versions>>.
See also <<vars-vs-defaults,Vars vs Defaults>> which mandates "Avoid embedding large lists or 'magic values' directly into the playbook."
Explanations:: Avoid testing for distribution and version in tasks.
Rather add a variable file to "vars/" for each supported distribution and version with the variables that need to change according to the distribution and version.

Rationale::
This way it is easy to add support to a new distribution by simply dropping a new file in to "vars/", see below <<supporting-multiple-distributions-and-versions,Supporting multiple distributions and versions>>.
See also <<vars-vs-defaults,Vars vs Defaults>> which mandates "Avoid embedding large lists or 'magic values' directly into the playbook."
Since distribution-specific values are kind of "magic values", it applies to them.
The same logic applies for providers: a role can load a provider-specific variable file, include a provider-specific task file, or both, as needed.
Consider making paths to templates internal variables if you need different templates for different distributions.
====
=== Check Mode and Idempotency Issues
=== Check Mode
[%collapsible]
====
* The role should work in check mode, meaning that first of all, they should not fail check mode, and they should also not report changes when there are no changes to be done.
Expand All @@ -140,6 +120,13 @@ Consider making paths to templates internal variables if you need different temp
If this is to be supported, see https://github.com/linux-system-roles/timesync/issues/27#issuecomment-472466223[hhaniel's proposal], which seems to properly guard even against such cases.
====
=== Idempotency
[%collapsible]
====
Explanations:: Reporting changes properly is related to the other requirement: *idempotency*. Roles should not perform changes when applied a second time to the same system with the same parameters, and it should not report that changes have been done if they have not been done. Due to this, using `command:` is problematic, as it always reports changes. Therefore, override the result by using `changed_when:`
Rationale:: Additional automation or other integrations, such as with external ticketing systems, should rely on the idempotence of the ansible role to report changes accurately
====
=== Supporting multiple distributions and versions
Use Cases::
* The role developer needs to be able to set role variables to different values depending on the OS platform and version. For example, if the name of a service is different between EL8 and EL9, or a config file location is different.
Expand All @@ -154,27 +141,10 @@ If it is desirable to use roles that require facts, but fact gathering is expens
==== Platform specific variables
[%collapsible]
====
You normally use `vars/main.yml` (automatically included) to set variables used by your role.
If some variables need to be parameterized according to distribution and version (name of packages, configuration file paths, names of services), do the following:
Explanations:: You normally use `vars/main.yml` (automatically included) to set variables used by your role.
If some variables need to be parameterized according to distribution and version (name of packages, configuration file paths, names of services), use this in the beginning of your `tasks/main.yml`:
Examples::

* Add the following to `vars/main.yml` (create the file if it does not exist):
+
[source,yaml]
----
# ansible_facts required by the role
__rolename_required_facts:
- distribution
- distribution_major_version
- distribution_version
- os_family
----
+
You may need to add more facts, depending on what Ansible facts your role requires.
Try to add only the facts required by the role.
If you add more facts, you may need to change `gather_subset: min` - see below.
* Create a file called `tasks/set_vars.yml` with the following contents:
+
[source,yaml]
----
- name: Ensure ansible_facts used by role
Expand Down Expand Up @@ -274,7 +244,7 @@ For example, if the role developer wants to pre-populate a VM with the packages
In this way, the role developer does not have to copy and maintain a separate list of role packages.
====
==== Platform specific tasks
=== Platform specific tasks
[%collapsible]
====
Platform specific tasks, however, are different.
Expand Down

0 comments on commit fcb5558

Please sign in to comment.