ansible-collections-openstack/docs/contributing.md
Jakob Meng f507465c9e Pivoted docs to generic resource{,s} modules and StateMachine class
Change-Id: Iebcd45d0eae79ab3911fd97c63d17b42d238f875
2023-01-26 13:35:45 +01:00

14 KiB

Development Guide for Ansible OpenStack Collection

Ansible OpenStack collection is a set of Ansible modules for interacting with the OpenStack API as either an admin or an end user.

We, and the OpenStack community in general, use OpenDev for its development. Patches are submitted to OpenDev Gerrit. Pull requests submitted through GitHub will be ignored. Please read OpenStack's Developer Workflow for details.

For hacking on the Ansible OpenStack collection it helps to prepare a DevStack environment first.

Hosting

Branches

For rationale behind our master and stable/1.0.0 branches and details on our relation to openstacksdk, please read our branching docs.

Examples

Naming

  • This collection is named openstack.cloud. There is no need for further namespace prefixing.
  • Name any module that a cloud consumer would expect from openstackclient (OSC), for example server instead of nova. This naming convention acknowledges that the end user does not care which service manages the resource - that is a deployment detail. For example, cloud consumers may not know whether their floating ip address are managed by Nova or Neutron.

Interface

  • If the resource being managed has an id, it should be returned.
  • If the resource being managed has an associated object more complex than an id, that should be returned instead of the id.
  • Modules should return a value of type dict, list or other primitive data types. For example, floating_ips in self.exit_json(changed=False, floating_ips=floating_ips) should to be a list of dicts. Use to_dict() on openstacksdk objects to convert resources to dictionaries. Setting its parameters such as computed to False will drop computed attributes from the resulting dict. Read to_dict's docstring for more parameters.
  • Module results have to be documented in RETURN docstring.
  • We should document which attribute cannot be updated in DOCUMENTATION variable. For example, insert 'This attribute cannot be updated.' to DOCUMENTATION like we did for the server module and others.
  • Sorting module options in DOCUMENTATION, attributes in RETURN, entries in argument_spec and expected fields in integration tests will make reviewing easier and faster.

Interoperability

  • It should be assumed that the cloud consumer does not know details about the deployment choices their cloud provider made. A best effort should be made to present one sane interface to the Ansible user regardless of deployer choices.
  • It should be assumed that a user may have more than one cloud account that they wish to combine as part of a single Ansible-managed infrastructure.
  • All modules should work appropriately against all existing versions of OpenStack regardless of upstream EOL status. The reason for this is that the Ansible modules are for consumers of cloud APIs who are not in a position to impact what version of OpenStack their cloud provider is running. It is known that there are OpenStack Public Clouds running rather old versions of OpenStack, but from a user point of view the Ansible modules can still support these users without impacting use of more modern versions.

Coding Guidelines

  • Modules should
    • be idempotent (not being idempotent requires a solid reason),
    • return whether something has changed,
    • support check mode,
    • be based on (be subclasses of) OpenStackModule in ansible_collections.openstack.cloud.plugins.module_utils.openstack,
    • should include extends_documentation_fragment: openstack in their DOCUMENTATION docstring,
    • be registered in meta/action_groups.yml for enabling the variables to be set in group level.
  • Complex functionality, cloud interaction or interoperability code should be moved to openstacksdk.
  • OpenStack API interactions should happen via openstacksdk and not via OpenStack component libraries. The OpenStack component libraries do no have end users as a primary audience, they are for intra-server communication.
  • When a resource exist and should be deleted (absent), then pass the resource to the delete_* function, not its name. Passing a name requires openstacksdk to find that resource again, doing a unnecessary api call, because we queried the resource before.
  • *_info modules never raise exceptions when resources cannot be found. When resources cannot be found, then a *_info module returns an empty list instead. For example, module openstack.cloud.neutron_rbac_policies_info will return an empty list when no project with name given in module parameter project can be found.
  • When a id is given in *_info modules, then we do not need nor want extra code to handle that. Instead most openstacksdk resources allow to pass ids as query arguments to OpenStack API. For example, identity.identity_providers() can be used for both cases: Where an id is given and where no id is given. No need to call get_identity_provider().
  • EXAMPLES docstring in modules (and Ansible's own modules) consist of a list of tasks. They do not contain YAML directives end marker line (---) and do not define playbooks (e.g. hosts keyword). They shall be simple, e.g. do not do fancy loops, heavy use of variables or use Ansible directives for no apparent reason such as ignore_errors or register.
  • self.params.get('...') can be replaced with self.params['...'] because parameters from argument_spec will always be in self.params. If not defined differently, they have a default value of None.
  • Writing code to check that some options cannot be updated and to fail if user still tries to update that value is most often not worth it. It would require much more code to catch all cases where updates are impossible and we would have to implement it consistently across modules. Atm we are fine with documenting which attribute cannot be updated in DOCUMENTATION variable. We could simply drop these checks and insert 'This attribute cannot be updated.' to DOCUMENTATION like we did for the server module and others.
  • openstacksdk functions often accept IDs but no names, e.g. find_address_scope() and create_address_scope() accept a project_id parameter. Most modules in our collection use names for finding resources, so we want to support the same for resources attributes such as project_id in AddressScope.
  • Constraints for module parameters and error handling can often be implemented in argument_spec or module_kwargs module_kwargs allows to define dependencies between module options such as mutually_exclusive, required_together, required_if etc..
  • When using openstacksdk's find_* functions (self.conn.*.find_*), then pass ignore_missing=False instead of checking its return value and failing with self.fail_json() if it is None.
  • Use module option names which match attribute names used in openstacksdk, e.g. use is_shared instead of shared. When refactoring modules, keep old option names as aliases to keep backward compatibility. Using openstacksdk names provides two benefits:
  • Use functions from openstacksdk's proxy layer instead of its cloud layer, if possible. For example, use self.conn.network.find_network(), not self.conn.get_network(). As a guideline use this decision tree:
    • If a functionality requires a single api call (to the OpenStack API), then use functions from openstacksdk's proxy layer.
    • If a functionality requires several api calls (to the OpenStack API), e.g. when creating and attaching a floating ip to a server, then use functions from openstacksdk's cloud layer.
    • When unsure which of openstacksdk's layers to use, then first go to proxy layer, then to its cloud layer and if this is not sufficient, then use its resource layer. Mainly, this applies to functions retrieving information, i.e. all calls where we get info about cloud resources should be changed to openstacksdk functions which return proxy resources.
    • It is perfectly fine to use openstacksdk's cloud layer for functionality which is not provided by openstacksdk's proxy layer. SDK's cloud layer is not going away. For example, list_* functions from openstacksdk's cloud layer such as search_users() allow to filter retrieved results with function parameter filters. openstacksdk's proxy layer does not provide an equivalent and thus the use of search_users() is perfectly fine.

Testing

  • Modules have to be tested with CI integration tests (if possible).
  • Each module has a corresponding Ansible role containing integration tests in ci/roles directory.
  • Ensure role names of integration tests in ci/roles match the module names. Only exception are *_info modules: Their integration tests are located in the same Ansible roles as their non-*_info equivalents (to reduce redundant code). For example, tests for both modules federation_mapping and federation_mapping_info can be found in role federation_mapping.
  • Zuul CI jobs are defined in .zuul.yaml.
  • Add assertions on return values from Ansible modules in integration tests. For an example, refer to ci/roles/floating_ip/tasks/main.yml. We need those checks to validate return values from openstacksdk, which might change across releases. Adding those assertions will be done in minutes, while checking the output manually during code reviews takes much more time.
  • Our Zuul CI jobs will run ansible-test for sanity checking.
  • Use tox -elinters_latest to run various linters against your code.

Upload

  • Study our Review Guidelines before submitting a patch.
  • Use Gerrit's work-in-progress feature to mark the status of the patch. A minus workflow (-w) will be reset when a new patchset is uploaded and hence easy to miss.
  • When you edit a patch, first rebase your patch on top of the current branch. Sometimes we replace code in all modules which might cause merge conflicts for you otherwise. For example, we dropped all options with default values from argument_spec such as required=False.

Release

Read Release Guide on how to publish new releases.

Permissions