.. _module_creation:

Module creation
***************

Much of ``cloud-init``'s functionality is provided by :ref:`modules<modules>`.
All modules follow a similar layout in order to provide consistent execution
and documentation. Use the example provided here to create a new module.

.. _module_creation-Guidelines:

Your Python module
==================

Modules are located in the ``cloudinit/config/`` directory, where the naming
convention for modules is to use ``cc_<module_name>`` (with underscores as the
separators).

The handle function
-------------------

Your module must include a ``handle`` function. The arguments are:

- ``name``: The module name specified in the configuration.
- ``cfg``: A configuration object that is the result of the merging of
  cloud-config configuration with any datasource-provided configuration.
- ``cloud``: A cloud object that can be used to access various datasource
  and paths for the given distro and data provided by the various datasource
  instance types.
- ``args``: An argument list. This is usually empty and is only populated
  if the module is called independently from the command line or if the
  module definition in :file:`/etc/cloud/cloud.cfg[.d]` has been modified
  to pass arguments to this module.

Schema definition
-----------------

If your module introduces any new cloud-config keys, you must provide a schema
definition in `cloud-init-schema.json`_.

- The ``meta`` variable must exist and be of type `MetaSchema`_.

  - ``id``: The module ID. In most cases this will be the filename without
    the ``.py`` extension.
  - ``distros``: Defines the list of supported distros. It can contain
    any of the values (not keys) defined in the `OSFAMILIES`_ map or
    ``[ALL_DISTROS]`` if there is no distro restriction.
  - ``frequency``: Defines how often module runs. It must be one of:

    - ``PER_ALWAYS``: Runs on every boot.
    - ``ONCE``: Runs only on first boot.
    - ``PER_INSTANCE``: Runs once per instance. When exactly this happens
      is dependent on the datasource, but may be triggered any time there
      would be a significant change to the instance-data. An example
      could be an instance being moved to a different subnet.

  - ``activate_by_schema_keys``: Optional list of cloud-config keys that will
    activate this module. When this list not empty, the config module will be
    skipped unless one of the ``activate_by_schema_keys`` are present in merged
    cloud-config instance-data.

Example module.py file
======================

.. code-block:: python

    # This file is part of cloud-init. See LICENSE file for license information.
    """Example Module: Shows how to create a module"""

    import logging
    from cloudinit.cloud import Cloud
    from cloudinit.config import Config
    from cloudinit.config.schema import MetaSchema
    from cloudinit.distros import ALL_DISTROS
    from cloudinit.settings import PER_INSTANCE

    LOG = logging.getLogger(__name__)

    meta: MetaSchema = {
        "id": "cc_example",
        "distros": [ALL_DISTROS],
        "frequency": PER_INSTANCE,
        "activate_by_schema_keys": ["example_key, example_other_key"],
    } # type: ignore

    def handle(
        name: str, cfg: Config, cloud: Cloud, args: list
    ) -> None:
        LOG.debug(f"Hi from module {name}")

Module documentation
====================

Every module has a folder in the ``doc/module-docs/`` directory, containing
a ``data.yaml`` file, and one or more ``example*.yaml`` files.

- The ``data.yaml`` file contains most of the documentation fields. At a
  minimum, your module should be provided with this file. Examples are not
  strictly required, but are helpful to readers of the documentation so it is
  preferred for at least one example to be included.
- The ``example*.yaml`` files are illustrative demonstrations of using the
  module, but should be self-contained and in correctly-formatted YAML. These
  will be automatically tested against the defined schema.

Example data.yaml file
----------------------

.. code-block:: yaml

   cc_module_name:
     description: >
       This module provides some functionality, which you can describe here.

       For straightforward text examples, use a greater-than (``>``) symbol
       next to ``description: `` to ensure proper rendering in the
       documentation. Empty lines will be respected, but line-breaks are
       folded together to create proper paragraphs.

       If you need to use call-outs or code blocks, use a pipe (``|``) symbol
       instead of ``>`` so that reStructuredText formatting (e.g. for
       directives, which take varying levels of indentation) is respected.
     examples:
     - comment: |
         Example 1: (optional) description of the expected behavior of the example
       file: cc_module_name/example1.yaml
     - comment: |
         Example 2: (optional) description of a second example.
       file: cc_module_name/example2.yaml
     name: Module Name
     title: Very brief (1 sentence) tag line describing what your module does

Rendering the module docs
-------------------------

The module documentation is auto-generated via the
:file:`doc/rtd/reference/modules.rst` file.

For your module documentation to be shown in the cloud-init docs, you will
need to add an entry to this page. Modules are listed in alphabetical order.
The entry should be in the following reStructuredText format:

.. code-block:: text

   .. datatemplate:yaml:: ../../module-docs/cc_ansible/data.yaml
      :template: modules.tmpl

The template pulls information from both your ``module.py`` file, and from its
corresponding entry in the the ``module-docs`` directory.

Module execution
================

For a module to be run, it must be defined in a module run section in
:file:`/etc/cloud/cloud.cfg` or :file:`/etc/cloud/cloud.cfg.d` on the launched
instance. The three module sections are
`cloud_init_modules`_, `cloud_config_modules`_, and `cloud_final_modules`_,
corresponding to the :ref:`Network<boot-Network>`, :ref:`Config<boot-Config>`,
and :ref:`Final<boot-Final>` boot stages respectively.

Add your module to `cloud.cfg.tmpl`_ under the appropriate module section.
Each module gets run in the order listed, so ensure your module is defined
in the correct location based on dependencies. If your module has no particular
dependencies or is not necessary for a later boot stage, it should be placed
in the ``cloud_final_modules`` section before the ``final-message`` module.

Benefits of including your config module in upstream cloud-init
===============================================================

Config modules included in upstream cloud-init benefit from ongoing
maintenance,
compatibility with the rest of the codebase, and security fixes by the upstream
development team.

If this is not possible, one can add
:ref:`custom out-of-tree config modules<custom_configuration_module>`
to cloud-init.

.. _MetaSchema: https://github.com/canonical/cloud-init/blob/3bcffacb216d683241cf955e4f7f3e89431c1491/cloudinit/config/schema.py#L58
.. _OSFAMILIES: https://github.com/canonical/cloud-init/blob/3bcffacb216d683241cf955e4f7f3e89431c1491/cloudinit/distros/__init__.py#L35
.. _settings.py: https://github.com/canonical/cloud-init/blob/3bcffacb216d683241cf955e4f7f3e89431c1491/cloudinit/settings.py#L66
.. _cloud-init-schema.json: https://github.com/canonical/cloud-init/blob/main/cloudinit/config/schemas/versions.schema.cloud-config.json
.. _cloud.cfg.tmpl: https://github.com/canonical/cloud-init/blob/main/config/cloud.cfg.tmpl
.. _cloud_init_modules: https://github.com/canonical/cloud-init/blob/b4746b6aed7660510071395e70b2d6233fbdc3ab/config/cloud.cfg.tmpl#L70
.. _cloud_config_modules: https://github.com/canonical/cloud-init/blob/b4746b6aed7660510071395e70b2d6233fbdc3ab/config/cloud.cfg.tmpl#L101
.. _cloud_final_modules: https://github.com/canonical/cloud-init/blob/b4746b6aed7660510071395e70b2d6233fbdc3ab/config/cloud.cfg.tmpl#L144
