How to write modules¶
First of all, please have a look at the Ansible module development guide and get familiar with the general Ansible module layout.
When looking at actual modules in this repository (foreman_domain is a nice short example), you will notice a few differences to a “regular” Ansible module:
Instead of
AnsibleModule, we useForemanEntityAnsibleModule(and a few others, seeplugins/module_utils/foreman_helper.py) which provides an abstraction layer for talking with the Foreman APIInstead of Ansible’s
argument_spec, we provide an enhanced version calledforeman_spec. It handles the translation from Ansible module arguments to Foreman API parameters, as nobody wants to writeorganization_idsin their playbook when they can writeorganizationsIn addition to Ansible’s validation options, we provide
required_pluginswhich will check for installed Foreman plugins should the module require any.
The rest of the module is usually very minimalistic:
Create a Sub class of
ForemanEntityAnsibleModulefor your module calledForemanMyEntityModuleto work withMyEntityforeman resource and use this one for your module definition. Eg: If the foreman entity is namedArchitecture:[...] class ForemanArchitectureModule(ForemanEntityAnsibleModule): pass [...]
Connect to the API and run the module Eg: Like previous example, if the foreman entity is named
Architecture:[...] def main(): module = ForemanArchitectureModule( argument_spec=dict( [...] ), foreman_spec=dict( [...] ), ) with module.api_connection(): module.run() if __name__ == '__main__': main()
You can see a complete example of simple module in foreman_architecture
In some cases, you will have to handle some custom workflows/validations, you can see some examples in foreman_bookmark, foreman_compute_attribute, foreman_hostgroup, foreman_provisioning_template…
Specification of the foreman_spec¶
The foreman_spec can be seen as an extended version of Ansible’s argument_spec. It understands more parameters (e.g. flat_name) and supports more types than the original version. An argument_spec will be generated from an foreman_spec. Any parameters not directly known or consumed by foreman_spec will be passed directly to the argument_spec.
In addition to Ansible’s argument_spec, foreman_spec understands the following types:
type='entity'The referenced value is another Foreman entity. This is usually combined withflat_name=<entity>_id. If no flat_name is provided, fallback to<entity>_idwhere entity is the foreman_spec key. egdefault_organization=dict(type='entity')=>flat_name=default_organization_id.type='entity_list'The referenced value is a list of Foreman entities. This is usually combined withflat_name=<entity>_ids. If no flat_name is provided, fallback tosingularize(<entity>)_idswhere entity is the foreman_spec key. egorganizations=dict(type='entity_list')=>flat_name=organization_ids.type='nested_list'The referenced value is a list of Foreman entities that are not included in the main API call. The module must handle the entities separately. See domain parameters inforeman_domainfor an example. The sub entities must be described byforeman_spec=<sub_entity>_spec.type='invisible'The parameter is available to the API call, but it will be excluded from Ansible’sargument_spec.search_by='login': Used withtype='entity'ortype='entity_list'. Field used to search the sub entity. Defaults to value provided byENTITY_KEYSor ‘name’ if no value found.search_operator='~': Used withtype='entity'ortype='entity_list'. Operator used to search the sub entity. Defaults to ‘=’. For fuzzy search use ‘~’.resource_type='organizations': Used withtype='entity'ortype='entity_list'. Resource type used to build API resource PATH. Defaults to pluralized entity key.resolve=False: Defaults to ‘True’. If set to false, the sub entity will not be resolved automatically.ensure=False: Defaults to ‘True’. If set to false, it will be removed before sending data to the foreman server.scope=['organization']: Defaults to ‘[]’. A list of entities that are used to build the lookup scope for this one.
flat_name provides a way to translate the name of a module argument as known to Ansible to the name understood by the Foreman API.
You can add new or override generated Ansible module arguments, by specifying them in the argument_spec as usual.
Entity lookup¶
Sometimes you need to access entities before module.run() can take over.
You can trigger the automatic lookup of entities via <entity_variable> = module.lookup_entity('<entity_name>').
If you only need the entity to be used as a scope parameter, it is enough to call scope = module.scope_for('organization').
In case, the automatic lookup process is unable perform the proper find for a specific entity type, it must be looked up manually and then set via module.set_entity('<entity_name>', search_result) to prevent the automatism from trying.
In instances of ForemanEntityAnsibleModule the main entity is references as ‘entity’ in the above context.
required_plugins¶
A module can pass an optional required_plugins list to ForemanAnsibleModule, which will indicate whether the module needs any Foreman plugins to be installed to work.
You can either specify that the whole module needs a specific plugin, like Katello modules:
required_plugins=[
('katello', ['*']),
]
Or specific parameters, like the discovery_proxy parameter of foreman_subnet which needs the Discovery plugin:
required_plugins=[
('discovery', ['discovery_proxy']),
]