ApiParameters class¶
The ApiParameters
class is one of two major Adapter-based classes that routinely appears
in the F5 Modules for Ansible. This class acts as a translation layer, or adapter
between the data received from the API and the data used in the module.
For this tutorial, you should navigate to the appropriate section of the stable 2.5 source code and copy it into your working module at the same location that it exists in the stable branch.
The rest of this section discusses implementation details of this class.
Internal methods¶
Adapter classes like ApiParameters
may have any number of internal methods added to them.
In this module’s implementation, the class has one method: _remove_internal_keywords
.
Adding new internal methods is a great way to tease out common functionality that you may want to
reuse across a wide variety of modules.
The quintessential example is the fq_name
method. You know it is common, because if you
remember back to the import block section, it was included in one of the imports:
from ansible.module_utils.network.f5.common import fq_name
The history of this particular function goes back to the earliest days of the F5 module code. In fact, its original implementation was not written by F5, but by customer contributors before F5 ever became involved.
This method is used to combine a resource’s name and its partition. This behavior is common, and affects every resource on the device. Therefore, it was a great candidate for inclusion in the common methods.
This same process of deducing what is common, and then re-using it across modules, typically begins with internal methods.
Ansible’s means of supporting this inclusion is through the module_utils
area of Ansible.
What should you do in situations where your method may apply to a small subset of modules, but not all
modules? It turns out that Ansible can support that too. The module_utils
directory contains
a number of sub-directories; one of them is delegated for use by F5.
Inside F5’s directory (conveniently called f5
), module developers may add more files for
use in common subsets of modules. Examples might be “the GTM modules”, or “the monitor modules.”
The combinations may vary, but including them is all the same.
Suppose there was a common function used in all monitor-related modules. This
function is only relevant to monitors though. It makes no sense to include it in all
modules. The result is that the developer may create a new file in F5’s module_utils
directory called monitors.py
and inside of that file, put the implementation of the
function.
Usage of this method could then be done in the monitor-related modules, like this.
from ansible.module_utils.network.f5.monitors import the_function
Where the_function
is replaced with the name of the function.
Write as many internal methods as you need.
@property methods¶
When it comes right down to it, the entirety of the functionality of the ApiParameters
class is encapsulated in the numerous @property
methods that it exposes.
These methods are used whenever you call the name of the property via the self.have
object.
This object is the ApiParameters
class. You should make sure that you have as many
properties as needed to make your development easier.
All ApiParameters
inherit an additional, special, piece of functionality, which is,
they can be populated by the api_map
. If you’ll remember back to the previous section
on the base Parameters
class, one of the top-of-class variables was the api_map
variable. The ApiParameters
class is where this variable is most useful because it will
auto-map the API resource attribute name to the @property
you specify.
Some modules implement additional @property
methods that are neither mapped to the API
nor provided by the module user. The reason this is done (usually) is to get a simpler
view of data that either the API or the user provide. This simpler implementation is then used
for comparisons for validity checks.
Looking deeper into an @property method¶
To illustrate an example of a @property
method, consider the actions
property. The
implementation of this property is:
@property
def actions(self):
result = []
if self._values['actions'] is None or 'items' not in self._values['actions']:
return [dict(type='ignore')]
for item in self._values['actions']['items']:
action = dict()
self._remove_internal_keywords(item)
if 'forward' in item:
action.update(item)
action['type'] = 'forward'
del action['forward']
elif 'enable' in item:
action.update(item)
action['type'] = 'enable'
del action['enable']
result.append(action)
result = sorted(result, key=lambda x: x['name'])
return result
Remember that the purpose of a the ApiParameters
adapter is to take the content from the
API and translate it to something that is usable in the module.
The API representation of this action data is a list of dictionaries:
{
"kind": "tm:ltm:policy:rules:actions:actionscollectionstate",
"selfLink": "https://localhost/mgmt/tm/ltm/policy/~Common~sdfg/rules/foo/actions?ver=13.1.0",
"items": [
{
"kind": "tm:ltm:policy:rules:actions:actionsstate",
"name": "0",
"fullPath": "0",
"generation": 62,
"selfLink": "https://localhost/mgmt/tm/ltm/policy/~Common~sdfg/rules/foo/actions/0?ver=13.1.0",
"code": 0,
"expirySecs": 0,
"forward": true,
"length": 0,
"offset": 0,
"pool": "/Common/dfgh",
"poolReference": {
"link": "https://localhost/mgmt/tm/ltm/pool/~Common~dfgh?ver=13.1.0"
},
"port": 0,
"request": true,
"select": true,
"status": 0,
"timeout": 0,
"vlanId": 0
}
]
}
The adapter needs to take this payload and turn it into something that the module can use. A lot of thought needs to go into the “that the module can use” part, because there is no prescribed way of handling data.
The developer of this module needed to know about what was stored in the API so that they could
do an accurate comparison. These things were outlined back in the DOCUMENTATION
blob that
you wrote. If you’ll remember, that data was the following:
actions:
description:
- The actions that you want the policy rule to perform.
- The available attributes vary by the action, however, each action requires that
a C(type) be specified.
- These conditions can be specified in any order. Despite them being a list, the
BIG-IP does not treat their order as anything special.
- Available C(type) values are C(forward).
suboptions:
type:
description:
- The action type. This value controls what below options are required.
- When C(type) is C(forward), will associate a given C(pool) with this rule.
- When C(type) is C(enable), will associate a given C(asm_policy) with
this rule.
- When C(type) is C(ignore), will remove all existing actions from this
rule.
required: true
choices: [ 'forward', 'enable', 'ignore' ]
pool:
description:
- Pool that you want to forward traffic to.
- This parameter is only valid with the C(forward) type.
asm_policy:
description:
- ASM policy to enable.
- This parameter is only valid with the C(enable) type.
This documentation tells us that the module intends to receive an actions
argument.
Inside this argument will be a list. Each item in the list will be a dictionary containing
a required type
key, and then one of the two other keys: either pool
, or
asm_policy
.
So we know that the data we want to compare with should look something like this in terms of its Python representation.
[
{
'type': '...',
'pool': '...'
},
]
or
[
{
'type': '...',
'asm_policy': '...'
},
]
Additionally, the data could possibly be a combination of the above, because policies allow this. Perhaps something like this:
[
{
'type': '...',
'pool': '...'
},
{
'type': '...',
'asm_policy': '...'
},
]
Python lets us compare dictionaries pretty easily using their tuple representations, so let’s assume that we want to make the API data reflect the data structure shown above.
To do this, we need to know the type
, and one of two values: either the pool
or
asm_policy
. It turns out that the action payload shown earlier actually contains this
information. Furthermore, we can see that the actions
@property
converts the JSON
payload to a dict that resembles the intended data structure above.
First, because the module data structure wants a list, the method sets the result
local
variable to a Python empty list. This allows the method to then add values to the list later.
Next, the method checks to see if either of two conditions are true:
- Is the
actions
attribute of the LTM policy rule missing? If it is, its value will be Python’sNone
value. - Is the
actions
attribute missing theitems
key? Earlier, in the JSON payload, you saw that the actions payload will have three top-level keys:kind
,selfLink
, anditems
. If theitems
value is missing, then there are no actions to be taken.
If either of the above conditions are met, the method immediately returns a single item list
with the one item being set to a dictionary with a type
key whose value is ignore
.
This is the internal representation for how the module detects an ignore
type.
If the above does not happen, the method can safely assume that it has a number of actions that need to be discovered and formatted into usable dictionaries.
On the for item...
line, it iterates over each of these items
.
During iteration, the method will be determining the actions
that can be deduced from
the original JSON payload. Therefore, it creates a new local variable named actions
and
sets its value to an empty dictionary. If it is able to intuit actions from this payload,
they will go in this variable.
Next, the module removes any keywords that it deems internal, from the current action in
the items
list.
After removing internal (i.e., useless to the module) keywords, the method makes a judgement
call about the type
. This judgement call also says a lot about which ``type``s the module
supports.
The two decisions are:
- Does the current action have an attribute named
forward
? - Does the current action have an attribute named
enable
?
If either of those two rules is met, then the current action is added to the local action
variable, a type
key is added that is specific to the type
that the method found,
and the original key that was used to determine the type is deleted from the local action
variable. The last step is done to prevent any polluting of what is returned by the method.
Finally, the local action variable is appended to the local result
list.
The final action of the method before returning the result to the caller is that it sorts
all of the entries in the local result
variable by the name
key of the item in the
result
list.
This is a very important step because it ensures that any future comparisons will be done on lists that are in the same order. When determining “difference,” it is not enough to assume that all items in a list have the same value. Order of that list is just as important in certain circumstances. Those circumstances are usually when the data on the BIG-IP itself is *un*ordered.
If BIG-IP does not consider order important for a particular resource, then the module developer must consider it important. This is because when there is no order, the users are not expecting there to be any order, and therefore, can arrange things in any way they want. For the module developer, this is a problem because all of the following are technically the same:
[1, 2, 3, 4]
[2, 3, 1, 4]
[4, 3, 2, 1]
The module then, is responsible for assuming that all values can possibly be unordered, and ordering them sanely for comparison.
Contrast this with a situation where the above is ordered. Then, each one of those lists is a different value. And a comparison of one order would fail against another order– i.e., if the customer changes the order of an ordered list, it implies their desire to change the order of the values in the BIG-IP.
Rules in a policy are a great example of this. The rules have order. However the actions and conditions in that rule have no order.
Conclusion¶
Understanding and using the ApiParameters
class is a core tenant of understanding
the F5 Modules for Ansible. From here, you may want to go back and consider exploring the
twin of this class (but which operates on the user’s side): the ModuleParameters
class.