Code conventions¶
The F5 modules follow a set of coding conventions that apply to all new and existing modules. These conventions help new contributors quickly develop new modules, and they help existing contributors maintain the current modules.
Where possible, F5 tries to automate the validation of these coding conventions so you are aware of mistakes and can fix them yourself.
For more information on the tools that perform these checks, refer to the Tests page.
When you write modules and their accompanying tests and docs, follow these 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 }}"
src: "{{ ucs_name }}"
provider:
password: "{{ bigip_password }}"
server: "{{ inventory_hostname }}"
user: "{{ bigip_username }}"
validate_certs: "{{ validate_certs }}"
register: result
F5 uses this format for several reasons, including Geerling’s.
- The structure is all valid YAML that uses the structured list/map syntax.
- Strings, booleans, integers, octals, etc. are all preserved (instead of 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 it
highlights each key and displays values as constants, strings, etc.
In addition, some situations will raise syntax errors if you use the simple key=value
format.
And finally, it saves space and is easier to read and know what the arguments to the module are.
Alphabetize the module’s parameters¶
The parameters 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 consistency between module usage and a way to see at a glance if a module has the correct parameters.
Use double quotes for strings¶
Ansible supports a simple parameter format. If a value is a string, represent it as a string by using double quotes.
GOOD
- name: My task
bigip_module:
alpha: "foo"
beta: "bar"
BAD
- name: My task
bigip_module:
alpha: foo
beta: bar
Do not use quotes for numbers¶
For numeric characters, do not use quotes. If the expected value is a number and you provide a number wrapped in quotes, some modules will raise ‘type’ errors.
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 as 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"
Give each task 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, Ansible creates a name by 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"
Always include a DOCUMENTATION variable¶
Ansible requires the DOCUMENTATION variable; it serves as the source of the module documentation that appears on their website.
Good documentation is essential to others being able to use the module, so you must include it.
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
Always include an EXAMPLES variable¶
Useful and valid examples are crucial for people new to Ansible and for 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 may eliminate a significant portion of 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
Always include a RETURN variable¶
The RETURN variable provides documentation essential to determining what, if any, information the module returns.
Other users will reference this documentation when they want to use the register
keyword.
The RETURN
field should include the parameters that your module has changed. If nothing has changed,
then the module does not need to return any values.
GOOD
RETURN = '''
full_name:
description: Full name of the user
returned: changed
type: string
sample: "John Doe"
'''
BAD
Missing RETURN variable
According to bcoca, the correct way to set a RETURN variable when a module does not return any information is the following.
GOOD
RETURN = '''
# only common fields returned
'''
Make the author field a list¶
Multiple people will probably 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)
Use GitHub handle for the author name¶
Both Ansible and the F5 Ansible repository are on GitHub. Therefore, for maintenance reasons, F5 requires 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 DOCUMENTATION, EXAMPLES, and RETURN¶
Follow this simple spacing convention to ensure that everything is properly spaced.
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 you can use 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 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 you list examples or documentation in any of the following variables:
- DOCUMENTATION
- RETURN
- EXAMPLES
Always expand lists of values if the 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']
Specify the BIG-IP version¶
In the DOCUMENTATION
section notes, you should specify which version of BIG-IP the module requires.
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 an exception that you do not handle causes the exception, the issue will be exceedingly difficult to debug.
Instead, only catch the F5ModuleError exception that the f5-sdk provides. Specifically raise this module and handle those errors. If an unknown error occurs, a full traceback will allow you to debug the problem more easily.
GOOD
try:
// do some things here that can cause an Exception
except IOError 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 IOError as e:
raise Exception('Error on setting profiles : %s' % e)
BAD
if foo:
// assume something successful happens here
else:
raise Exception('Error on baz')
Support check mode¶
Check mode allows Ansible to run your Playbooks in a dry-run sort of operation. This is handy when you want to run a set of tasks but are not sure what will happen when you do.
Because BIG-IPs are usually considered a sensitive device to handle, you should always implement a check mode.
http://www.jeffgeerling.com/blog/yaml-best-practices-ansible-playbooks-tasks
Do not use local_action in your EXAMPLES¶
Some people prefer local_action and some people prefer delegation. Delegation is more applicable to general-purpose Ansible, so you should get 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"
Set default EXAMPLE parameters¶
For consistency, always use the following values for the given parameters, so you do not have to over-think the inclusion of your example:
- user: “admin”
- password: “secret”
- server: “lb.mydomain.com”
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 values before returning them¶
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
)
In the bad example, when it comes time to debug the value of the variable, you must change the code to do an assignment operation anyway.
For example, if you use q to debug the value, you must 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 an assignment, then you must change the code before you are able to debug the code.
Create a functional test for each code fix¶
When you fix an issue and it requires changes to code, you should create a new functional test YAML file in the module’s test/integration/PRODUCT/targets directory.
For example, consider Github Issue 59, which is relevant to the bigip_virtual_server module.
The developer added new code to the module. To verify that someone tested the new code, 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 the file, you should include any and all work to:
- Set up the test
- Perform the test
- Teardown the test
Any issues that you report on GitHub should follow the same pattern. However, the filenames of those modules should be:
- ansible-xxxxx.yaml
This way, they will not conflict with the numeric namespace in the f5-ansible repository.
Exclude code from unit test coverage¶
Ansible’s test runner makes use of pytest, so the acceptable way of excluding lines from code coverage is here:
You should use this to include the various *_on_device and *_from_device methods in modules that make direct calls to the remote BIG-IPs.
Put exception message on a new line¶
This convention helps eliminate the total number of columns in use, but also increases 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.
GOOD
...
raise F5ModuleError(
'"{0}" is not a supported filter. '
'Supported key values are: {1}'.format(key, ', '.join(keys)))
)
BAD
...
raise F5ModuleError('"{0}" is not a supported filter. '
'Supported key values are: {1}'.format(key, ', '.join(keys)))
Put list contents on a new line¶
Lists should also be on a new line. The ending bracket should be on a new line as well, aligned with the beginning of the variable name.
GOOD
...
mylist = [
'foo', 'bar',
'baz', 'biz'
]
BAD
...
mylist = ['foo', 'bar',
'baz', 'biz']
Include the license header¶
Each module requires a license header that includes the GPL3 license.
Here is the common license header.
# Copyright 2016 F5 Networks Inc.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
If the module under development is your original work, then you can include your name in the copyright above.
If you are only contributing to 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 the F5 branch.
Include the ANSIBLE_METADATA variable¶
The ANSIBLE_METADATA variable should be first in your module. It specifies metadata for the module itself. It can always look the same.
Here is how it’s defined in code.
ANSIBLE_METADATA = {'status': ['preview'],
'supported_by': 'community',
'version': '1.0'}
The stubber creates 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 limits the amount of verbosity in module code. Additionally, conflict can occur if you do not follow this convention (who is right? docs or code?).
Ansible, by default, makes parameters not required. It is redundant to provide it again in your documentation.
GOOD
...
login:
description:
- Specifies, when checked C(enabled), that the system accepts SSH
communications.
choices:
- enabled
- disabled
...
BAD
...
login:
description:
- Specifies, when checked C(enabled), that the system accepts SSH
communications.
choices:
- enabled
- disabled
required: False
...
Do not include default key for parameters without defaults¶
Another convention from Ansible, similar to the required: False convention, is applying the rule to the default value. Since default: None is already the value that Ansible uses (in code), it is redundant to provide it again in the docs.
GOOD
...
login:
description:
- Specifies, when checked C(enabled), that the system accepts SSH
communications.
choices:
- enabled
- disabled
...
BAD
...
login:
description:
- Specifies, when checked C(enabled), that the system accepts SSH
communications.
choices:
- enabled
- disabled
default: None
...
Do not decompose to a *_device method if the using method is itself a *_device method¶
This convention is in place to limit the total amount of function decomposition that you will inevitably try to put into the code.
Some level of decomposition is good because it isolates the code that targets the device (called *_device methods) from the code that does not communicate with the device.
This method of isolation is how you extend modules when the API code diverges, or when the means of transporting information from and to the device changes.
You can take this decomposition too far, though. Refer to the examples below for an illustration of this. When you go to far, the correction is to merge the two methods.
GOOD
...
def import_to_device(self):
self.client.api.tm.asm.file_transfer.uploads.upload_file(self.want.file)
tasks = self.client.api.tm.asm.tasks
result = tasks.import_policy_s.import_policy.create(
name=self.want.name, filename=name
)
return result
...
BAD
...
def upload_to_device(self):
self.client.api.tm.asm.file_transfer.uploads.upload_file(self.want.file)
def import_to_device(self):
self.upload_to_device()
tasks = self.client.api.tm.asm.tasks
result = tasks.import_policy_s.import_policy.create(
name=self.want.name, filename=name
)
return result
...
This convention remains valid when the code you are using is a single line. Therefore, if you use the upload_file line in many places in the code, it is still correct to merge the methods instead of having a different method for it.
The only time when it would be correct to decompose it is if the “other” methods were not *_device methods.