# How to write modules First of all, please have a look at the [Ansible module development](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html) guide and get familiar with the general Ansible module layout. When looking at actual modules in this repository ([`domain`](../plugins/modules/domain.py) 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`](../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`: ```python [...] class ForemanArchitectureModule(ForemanEntityAnsibleModule): pass [...] ``` * Connect to the API and run the module Eg: Like previous example, if the foreman entity is named `Architecture`: ```python [...] 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`](../plugins/modules/architecture.py) In some cases, you will have to handle some custom workflows/validations, you can see some examples in [`bookmark`](../plugins/modules/bookmark.py), [`compute_attribute`](../plugins/modules/compute_attribute.py), [`hostgroup`](../plugins/modules/hostgroup.py), [`provisioning_template`](../plugins/modules/provisioning_template.py)... ## 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 `type`s 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=_id`. If no flat_name is provided, fallback to `_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=_ids`. If no flat_name is provided, fallback to `singularize()_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`](../plugins/modules/domain.py) for an example. The sub entities must be described by `foreman_spec=_spec`. * `invisible=True` 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 ` = module.lookup_entity('')`. 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('', 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: ```python required_plugins=[ ('katello', ['*']), ] ``` Or specific parameters, like the `discovery_proxy` parameter of `subnet` which needs the Discovery plugin: ```python required_plugins=[ ('discovery', ['discovery_proxy']), ] ```