Role in collection cannot find module in the same collection

ab203972
Tue, 14 Apr 2026 10:00:28 +0000
Get Help

We have an internal collection with roles, one of which lists available updates, and another that actually performs the updates, excluding necessary packages. I am trying to tweak the role that lists updates so that it doesn’t list the packages that are excluded in the actual update. I was unable to find a combination of Ansible functions/filters to accomplish this, so I have resorted to trying to write a module within this collection that helps with some string formatting. However, the role cannot find the module, even though they are within the same collection.

Below you can find my directory structure, role contents, and module contents:

# Working directory
├── CHANGELOG.md
├── README.md
├── galaxy.yml
├── meta
│   └── runtime.yml
├── plugins
│   └── modules
│       ├── __init__.py
│       └── add_string.py
├── renovate.json
└── roles
    ├── list_linux_updates
    │   ├── handlers
    │   │   └── main.yml
    │   ├── meta
    │   │   ├── argument_specs.yml
    │   │   └── main.yml
    │   └── tasks
    │       └── main.yml
    └── update_linux_packages
        ├── defaults
        │   └── main.yml
        ├── meta
        │   ├── argument_specs.yml
        │   └── main.yml
        └── tasks
            └── main.yml

# list_linux_updates role
---
- name: List updates
  block:
    - name: Updates for RedHat/SUSE and derived distributions
      when: ansible_facts.pkg_mgr == 'dnf'
      block:
        - name: Prepare submitted packages to be excluded
          company.update_roles.add_string:
            in_list: list_linux_updates_packages_exclude
            in_string: "--exclude"
            prepend: true
          register: list_linux_updates_packages_exclude_prepped

        - name: Store submitted packages to be excluded
          ansible.builtin.set_fact:
            list_linux_updates_packages_exclude_joined: "{{ list_linux_updates_packages_exclude_prepped.output | join(' ') }}"

        - name: Gather available package updates for RedHat/SUSE systems
          ansible.builtin.command:
            cmd: "dnf check-update --quiet {{ list_linux_updates_packages_exclude_joined }}"
          register: list_linux_updates_updatable_packages_raw_RH
          changed_when: list_linux_updates_updatable_packages_raw_RH.rc in [0, 100]
          failed_when: list_linux_updates_updatable_packages_raw_RH.rc not in [0, 100]
          notify:
            - Write RedHat packages to variable

    - name: Updates for Debian/Ubuntu and derived distributions
      when: ansible_facts.pkg_mgr == 'apt'
      block:
        - name: Gather available package updates for Debian/Ubuntu systems
          ansible.builtin.command:
            cmd: apt-get upgrade --dry-run
          register: list_linux_updates_updatable_packages_raw_DB
          changed_when: list_linux_updates_updatable_packages_raw_DB.rc == 0
          notify:
            - Write Debian packages to variable

# Module
#!/usr/bin/python
# Created using the official module in a collection example:
# https://docs.ansible.com/projects/ansible/latest/dev_guide/developing_modules_general.html#creating-a-module-in-a-collection
DOCUMENTATION = r'''
---
module: add_string
author: department_X
version_added: "1.0.0"
short_description: Prepends or appends a string to each string item in a list.
description:
  - Prepends or appends a string to each string item in a list.
  - It makes no changes on any host, and is purely needed for some formatting of text.
options:
    in_list:
        description: This is a list of strings to which the string must be added.
        required: true
        type: list
    in_string:
        description: This is the string to add to each element in the list.
        required: true
        type: str
    prepend:
        description: Prepends string to each element. If false, it will append instead.
        required: false
        type: bool
        default: true
'''

EXAMPLES = r'''
# Base usage
- name: Add "added" to every object in the list
  company.update_roles.add_string:
    in_list: ['ring', 'around', 'the', 'rosie']
    in_string: 'added'
  register: prepend_text

- name: Print output
  ansible.builtin.debug:
    var: prepend_text.output
# returns: ['added ring','added around','added the','added rosie']

# Append instead of prepend
- name: Add "added" to every object in the list
  company.update_roles.add_string:
    in_list: ['ring', 'around', 'the', 'rosie']
    in_string: 'added'
    prepend: false

- name: Print output
  ansible.builtin.debug:
    var: append_text.output
# returns: ['ring added','around added','the added','rosie added']
'''

RETURN = r'''
# These are examples of possible return values, and in general should use other names for return values.
input:
    description: The original name param that was passed in.
    type: list
    returned: always
    sample: ['ring','around','the','rosie']
output:
    description: The output message that the test module generates.
    type: list
    returned: always
    sample: ['added ring','added around','added the','added rosie']
'''

from ansible.module_utils.basic import AnsibleModule

def run_module():
    # define available arguments/parameters a user can pass to the module
    module_args = dict(
        in_list=dict(type='list', required=True),
        in_string=dict(type='str', required=True),
        prepend=dict(type='bool', required=False, default=True)
    )

    result = dict(
        changed=False,
        input=[],
        output=[]
    )

    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )

    if module.check_mode:
        module.exit_json(**result)

    result['input'] = module.params['in_list']
    result['output'] = add_string(module.params['in_list'], module.params['in_string'], module.params['prepend'])

    if module.params['in_list'] != result['output']:
        result['changed'] = True

    module.exit_json(**result)

def add_string(in_list: list, in_string: str, prepend: bool):
    if in_list == []:
        return []
    else:
        out_list = []
        for item in in_list:
            if prepend:
                out_list.append(str(item).strip() + " " + in_string.strip())
            else:
                out_list.append(in_string.strip() + " " + str(item).strip())
        return out_list

def main():
    run_module()

if __name__ == '__main__':
    main()

I have tried to test it via command line, but it yields this error:

ansible localhost -m company.update_roles.add_string -a "in_list=['one','two','three'] in_string=word" --playbook-dir=$PWD

# result:
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: Error loading plugin 'company.update_roles.add_string': No module named 'ansible_collections.company'
[ERROR]: Task failed: Cannot resolve 'company.update_roles.add_string' to an action or module. Does anyone here have an inkling of what I might be missing?

Task failed.
Origin: <adhoc 'company.update_roles.add_string' task>

{'action': 'company.update_roles.add_string', 'args': {'in_list': "['one','two','three']", 'in_string': 'word'}, [...]

<<< caused by >>>

Cannot resolve 'company.update_roles.add_string' to an action or module.
Origin: <CLI option '-m'>

company.update_roles.add_string

localhost | FAILED! => {
    "changed": false,
    "msg": "Task failed: Cannot resolve 'company.update_roles.add_string' to an action or module."
}

Which is pretty similar to the error I get when running ansible-lint:

$ ansible-lint
WARNING  Unable to load module company.update_roles.add_string at roles/list_linux_updates/tasks/main.yml:7 for options validation
WARNING  Invalid value (None)for resolved_fqcn attribute of company.update_roles.add_string module.
WARNING  Listing 1 violation(s) that are fatal
# Rule Violation Summary
  1 syntax-check profile:min tags:core,unskippable
Failed: 1 failure(s), 0 warning(s) on 27 files.
syntax-check[unknown-module]: couldn't resolve module/action 'company.update_roles.add_string'. This often indicates a misspelling, missing collection, or incorrect module path.
roles/list_linux_updates/tasks/main.yml:7:11

I’m sure that there are plenty of things wrong with the module itself, but so long as I cannot test it, I won’t know and learn. I also have a bit of a feeling that this may not be the intended use of modules, or even the best way to tackle the issue at hand, but it’s the only one that I could think of that has proven somewhat fruitful. Nevertheless, I do want to know how I can use modules within the same collection.

10 posts - 7 participants