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 (domain is a nice short example), you will notice a few differences to a “regular” Ansible module:

  • Instead of AnsibleModule, we use ForemanEntityAnsibleModule (and a few others, see plugins/module_utils/foreman_helper.py) which provides an abstraction layer for talking with the Foreman API

  • Instead of Ansible’s argument_spec, we provide an enhanced version called foreman_spec. It handles the translation from Ansible module arguments to Foreman API parameters, as nobody wants to write organization_ids in their playbook when they can write organizations

  • In addition to Ansible’s validation options, we provide required_plugins which 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 ForemanEntityAnsibleModule for your module called ForemanMyEntityModule to work with MyEntity foreman resource and use this one for your module definition. Eg: If the foreman entity is named Architecture:

    [...]
    
    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 architecture In some cases, you will have to handle some custom workflows/validations, you can see some examples in bookmark, compute_attribute, hostgroup, 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 with flat_name=<entity>_id. If no flat_name is provided, fallback to <entity>_id where entity is the foreman_spec key. eg default_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 with flat_name=<entity>_ids. If no flat_name is provided, fallback to singularize(<entity>)_ids where entity is the foreman_spec key. eg organizations=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 in domain for an example. The sub entities must be described by foreman_spec=<sub_entity>_spec.

  • type='invisible' The parameter is available to the API call, but it will be excluded from Ansible’s argument_spec.

  • search_by='login': Used with type='entity' or type='entity_list'. Field used to search the sub entity. Defaults to value provided by ENTITY_KEYS or ‘name’ if no value found.

  • search_operator='~': Used with type='entity' or type='entity_list'. Operator used to search the sub entity. Defaults to ‘=’. For fuzzy search use ‘~’.

  • resource_type='organizations': Used with type='entity' or type='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 subnet which needs the Discovery plugin:

required_plugins=[
    ('discovery', ['discovery_proxy']),
]