F5 Ansible 1.0.0

Current Page

User Documentation

Module Documentation

Developer Documentation

Cloud Docs Home > F5 Ansible Index

Code Conventions

The F5 modules attempt to follow a set of coding conventions that apply to all new and existing modules.

These conventions help new contributors quickly develop new modules. Additionally, they help existing contributors maintain the current modules.

Style checking

Where possible, we try to automate the validation of these coding conventions so that you are aware of mistakes and able to fix them yourself without having to have the maintainers intervene.

For more information on what tools perform these checks, refer to the tests page.

Module Conventions

When writing your modules and their accompanying tests and docs, please follow the below coding conventions.

Use the complex/structure map format

In reference to Jeff Geerling’s page here, this format looks like this.

- name: Create a UCS
  bigip_ucs_fetch:
     dest: "/tmp/{{ ucs_name }}"
     password: "{{ bigip_password }}"
     server: "{{ inventory_hostname }}"
     src: "{{ ucs_name }}"
     user: "{{ bigip_username }}"
     validate_certs: "{{ validate_certs }}"
  register: result

There are several reasons that we use this format. Among them are Geerling’s reasons.

  • The structure is all valid YAML, using the structured list/map syntax mentioned in the beginning of this post.
  • Strings, booleans, integers, octals, etc. are all preserved (instead of being converted to strings).
  • Each parameter must be on its own line, so you can’t chain together mode: 0755, owner: root, user: root to save space.
  • YAML syntax highlighting works slightly better for this format than key=value, since each key will be highlighted, and values will be displayed as constants, strings, etc.

In addition to those reasons, there are also some situations that, if you use the simple key=value format will raise syntax errors. Finally, it saves on space and, in the maintainers opinion, is easier to read and know (by looking) what the arguments to the module are.

Alphabetize your module parameters

The parameters to your modules in the Roles and Playbooks that are developed here must be in alphabetic order.

GOOD

- name: My task
  bigip_module:
      alpha: "foo"
      beta: "bar"
      gamma: "baz"

BAD

- name: My task
  bigip_module:
      alpha: "foo"
      gamma: "baz"
      beta: "bar"

This provides for consistency amongst module usage as well as provides a way to see at a glance if a module has the correct parameters.

Double-quotes for Strings, no quotes for Numbers

Ansible supports a simple form of typing for your parameters. If there is a value that is a string, it should be represented as a string using double quotes.

GOOD

- name: My task
  bigip_module:
      alpha: "foo"
      beta: "bar"

BAD

- name: My task
  bigip_module:
      alpha: foo
      beta: bar

For numeric characters, you should not use any quotes because this can cause some modules to raise ‘type’ errors if the expected value is a number and you provide it with a number wrapped in quotes

GOOD

- name: My task
  bigip_module:
      alpha: 1
      beta: 100

BAD

- name: My task
  bigip_module:
      alpha: "1"
      beta: "100"

Begin YAML files with a triple-dash

A YAML file usually begins with three dashes. As such, you should have that be a part of your own YAML files.

GOOD

---

- name: My task
  bigip_module:
      alpha: 1
      beta: 100

BAD

- name: My task
  bigip_module:
      alpha: "1"
      beta: "100"

All tasks should have a name

When your Playbooks encounter errors, the name of the task is always called out in the failure. If you do not provide a name, then Ansible creates a name for you using the module call itself.

Naming your tasks allows you to quickly reference where a failure occurred.

GOOD

- name: My task
  bigip_module:
      alpha: 1
      beta: 100

BAD

- bigip_module:
      alpha: "1"
      beta: "100"

All modules must have a DOCUMENTATION variable

The DOCUMENTATION variable is also required by Ansible upstream as it serves as the source of the module documentation that is generated on their site.

Good documentation is essential to people being able to use the module so it must be included.

GOOD

DOCUMENTATION = '''
---
module: bigip_device_ntp
short_description: Manage NTP servers on a BIG-IP
description:
  - Manage NTP servers on a BIG-IP
version_added: "2.1"
options:
...
'''

BAD

Missing DOCUMENTATION variable

All modules must have an EXAMPLES variable

Useful and valid examples are crucial for people new to Ansible and to the module itself.

When providing examples, be mindful of what you provide. If you developed the module with a specific use case in mind, be sure to include that use case. It may be applicable to a large majority of users and, therefore, may eliminate a significant portion of their time that they would otherwise spend figuring out what is or is not needed.

GOOD

EXAMPLES = '''
- name: Set the banner for the SSHD service from a string
  bigip_device_sshd:
      banner: "enabled"
      banner_text: "banner text goes here"
      password: "admin"
      server: "bigip.localhost.localdomain"
      user: "admin"
  delegate_to: localhost
'''

BAD

Missing EXAMPLES variable

All modules must have a RETURN variable

The RETURN variable provides documentation essential to determining what, if any, information is returned by the operation of the module.

End users of the module will reference this documentation when they want to use the register keyword.

The RETURN field should include the parameters that have been changed by your module. If nothing has been changed, then no values need be returned.

GOOD

RETURN = '''
full_name:
    description: Full name of the user
    returned: changed
    type: string
    sample: "John Doe"
'''

BAD

Missing RETURN variable

If your module does not return any information, then an empty YAML string is sufficient

GOOD

..code-block:: python

RETURN = ‘’‘# ‘’‘

The author field must be a list

There is a good possibility that multiple people will work to maintain the module over time, so it is a good idea to make the author keyword in your module a list.

GOOD

author:
  - Tim Rupp (@caphrim007)

BAD

author: Tim Rupp (@caphrim007)

Author field should be Github handle

Both Ansible and this repository are maintained on Github. Therefore, for maintenance reasons we require your Github handle. Additionally, your email address may change over time.

GOOD

author:
  - Tim Rupp (@caphrim007)

BAD

author:
  - Tim Rupp <caphrim007@gmail.com>

Use 2 spaces in the DOCUMENTATION, EXAMPLES, and RETURN

This is a simple spacing convention to ensure that everything is properly spaced over.

GOOD

options:
  server:
    description:
      - BIG-IP host
    required: true
  user:
^^

BAD

options:
    server:
        description:
            - BIG-IP host
        required: true
    user:
^^^^

Use ansible lookup plugins where appropriate

Ansible provides existing facilities that can be used to read in file contents to a module’s parameters.

If your module can accept a string or a file containing a string, then assume that users will be using the lookup plugins.

For example, SSL files are typically strings. SSH keys are also strings even if they are contained in a file. Therefore, you would delegate the fetching of the string data to a lookup plugin.

There should be no need to use the python open facility to read in the file.

GOOD

some_module:
    string_param: "{{ lookup('file', '/path/to/file') }}"

BAD

some_module:
    param: "/path/to/file"

Always expand lists in the various documentation variables

When listing examples or documentation in any of the following variables,

  • DOCUMENTATION
  • RETURN
  • EXAMPLES

be sure to always expand lists of values if that key takes a list value.

GOOD

options:
  state:
    description:
      - The state of things
    choices:
      - present
      - absent

BAD

options:
  state:
    description:
      - The state of things
    choices: ['enabled', 'disabled']

Support for 12.0.0 or greater at this time

In the DOCUMENTATION section notes, you should specify what version of BIG-IP the module requires.

At this time, that version is 12.0.0, so your DOCUMENTATION string should reflect that.

GOOD

notes:
  - Requires BIG-IP version 12.0.0 or greater

BAD

Any version less than 12.0.0.

If your module requires functionality greater than 12.0.0 it is also acceptable to specify that in the DOCUMENTATION block.

Never raise a general Exception

General Exceptions are bad because they hide unknown errors from you, the developer. If a bug report comes in and is being caused by an exception that you do not handle, it will be exceedingly difficult to debug it.

Instead, only catch the F5ModuleError exception that is provided by the f5-sdk. Specifically raise this module and handle those errors. If an unknown error occurs, a full traceback will be produced that will more easily allow you to debug the problem.

GOOD

try:
    // do some things here that can cause an Exception
except bigsuds.OperationFailed as e:
    raise F5ModuleError('Error on setting profiles : %s' % e)

GOOD

if foo:
    // assume something successful happens here
else:
    raise F5ModuleError('Error on baz')

BAD

try:
    // do some things here that can cause an Exception
except bigsuds.OperationFailed as e:
    raise Exception('Error on setting profiles : %s' % e)

BAD

if foo:
    // assume something successful happens here
else:
    raise Exception('Error on baz')

All modules must support check mode

Check-mode allows Ansible to run your Playbooks in a dry-mode sort of operation. This is very handy when you want to run a set of tasks but are not sure what will happen when you do.

Since BIG-IPs are usually considered a sensitive device to handle, there should always be a check-mode implemented in your module.

Do not use local_action in your EXAMPLES

Some folks like local_action and some folks like delegation. Delegation is more applicable to general-purpose Ansible, so for that reason I want to get people in the habit of using and understanding it.

Therefore, do not use local_action when defining examples. Instead, use delegate_to.

GOOD

- name: Reset the initial setup screen
  bigip_sys_db:
      user: "admin"
      password: "secret"
      server: "lb.mydomain.com"
      key: "setup.run"
      state: "reset"
  delegate_to: localhost

BAD

- name: Reset the initial setup screen
  local_action:
      module: "bigip_sys_db"
      user: "admin"
      password: "secret"
      server: "lb.mydomain.com"
      key: "setup.run"
      state: "reset"

Default EXAMPLE parameters

For consistency, always using the following values for the given parameters

  • user: “admin”
  • password: “secret”
  • server: “lb.mydomain.com”

This allows you to not have to overthink the inclusion of your example.

GOOD

- name: Reset the initial setup screen
  bigip_sys_db:
      user: "admin"
      password: "secret"
      server: "lb.mydomain.com"
      key: "setup.run"
      state: "reset"
  delegate_to: localhost

BAD

- name: Reset the initial setup screen
  bigip_sys_db:
      user: "joe_user"
      password: "admin"
      server: "bigip.host"
      key: "setup.run"
      state: "reset"
  delegate_to: localhost

Assign before returning

To enable easier debugging when something goes wrong, ensure that you assign values before you return those values.

GOOD

def exists(self):
    result = self.client.api.tm.gtm.pools.pool.exists(
        name=self.want.name,
        partition=self.want.partition
    )
    return result

BAD

def exists(self):
    return self.client.api.tm.gtm.pools.pool.exists(
        name=self.want.name,
        partition=self.want.partition
    )

The reason that the above BAD example is considered bad is that when it comes time to debug the value of a variable, it requires that you change the code to do an assignment operation anyway.

For example, using q to debug the value of the above requires that you implicitly assign the value of the API call before you do this,

...
result = self.client.api....
q.q(result)
...

When the code does not do a assignment, then you are required to change the code before you are able to debug the code.

Fixed Github issues should have an associated issue-xxxxx.yaml file

When a developer takes on a new issue that requires changes to code to get working, these changes should be tested with a new functional test yaml file located in the module’s test/integration/PRODUCT/targets directory.

For example.

Consider the Github Issue 59 which is relevant to the bigip_virtual_server module.

The developer needed to add new code to the module. So to verify that the new code is tested, the developer should add a new file to the module’s targets directory here

  • test/functional/bigip/bigip_virtual_server/tasks

The name of the file should be

  • issue-59.yaml

And inside of the file should be any and all work that is required to,

  • Setup the test
  • Perform the test
  • Teardown the test

Any issues that are reported on github should follow the same pattern, however the filenames of those modules should be

  • ansible-xxxxx.yaml

So-as not to step on the numeric namespace that is used natively in the f5-ansible repository.

RETURN value when there is no return

The correct way to set a RETURN variable whose module has no returnable things is like this according to bcoca.

Excluding code from unit test coverage

Ansible’s test runner makes use of pytest, so the acceptable way of excluding lines from code coverage is documented here.

The cases where you would want to use this include the various *_on_device and *_from_device methods in modules that make direct calls to the remote BIG-IPs.

Exception message should be on a new line

This convention is done to eliminate the total number of columns in use, but also to increase readability when long lines tend to scroll off screen. Even with a 160 column limit for this project, long lines, and many lines, can begin to grow less compact.

BAD

GOOD

List contents should start on a new line

For the same reason given above concerning compactness, lists should follow the same rule. The ending bracket should be on a new line as well, aligned with the beginning of the variable name

BAD

GOOD

License header

Each module requires a license header which includes the GPL3 license.

Here is the common license header.

If the module under development is your original work, then you can include your name in the copyright above.

If you are only contributing an existing module, then it is not necessary to include a copyright line at the top. Instead, accepting the F5 CLA is sufficient to get code merged into our branch.

The ANSIBLE_METADATA variable

This variable should be included first in your module. It specifies metadata for the module itself. It can always look the same. Here is it as would be defined in code.

ANSIBLE_METADATA = {'status': ['preview'],
                    'supported_by': 'community',
                    'version': '1.0'}

The stubber will create this for you automatically.

Do not include required key for non-required parameters

This convention comes to us courtesy of Ansible module authoring rules. This convention is used to limit the amount of verbosity in module code. Additionally, there is a risk of conflict (who is right? docs? or code?) that can occur if this convention is not followed.

Ansible, by default, make a parameter not required. Therefore, it is also redundant to provide it again in your documentation.

BAD

...
login:
  description:
    - Specifies, when checked C(enabled), that the system accepts SSH
      communications.
  choices:
    - enabled
    - disabled
  required: False
...

GODE

...
login:
  description:
    - Specifies, when checked C(enabled), that the system accepts SSH
      communications.
  choices:
    - enabled
    - disabled
...

Do not include default key for parameters without defaults

Another convention from Ansible, similar to the required: False convention, this convention is applying the rule to the default. Since default: None is already the value that Ansible uses (in code), it is redundant to provide it again in the docs.

BAD

...
login:
  description:
    - Specifies, when checked C(enabled), that the system accepts SSH
      communications.
  choices:
    - enabled
    - disabled
  default: None
...

GODE

...
login:
  description:
    - Specifies, when checked C(enabled), that the system accepts SSH
      communications.
  choices:
    - enabled
    - disabled
...