Blob Blame History Raw
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils import common_koji


ANSIBLE_METADATA = {
    'metadata_version': '1.0',
    'status': ['preview'],
    'supported_by': 'community'
}


DOCUMENTATION = '''
---
module: koji_tag_inheritance

short_description: Manage a Koji tag inheritance relationship
description:
   - Fine-grained management for tag inheritance relationships.
   - The `koji_tag` module is all-or-nothing when it comes to managing tag
     inheritance. When you set inheritance with `koji_tag`, the module will
     delete any inheritance relationships that are not defined there.
   - In some cases you may want to declare *some* inheritance relationships
     within Ansible without clobbering other existing inheritance
     relationships. For example, `MBS
     <https://fedoraproject.org/wiki/Changes/ModuleBuildService>`_ will
     dynamically manage some inheritance relationships of tags.
options:
   child_tag:
     description:
       - The name of the Koji tag that will be the child.
     required: true
   parent_tag:
     description:
       - The name of the Koji tag that will be the parent of the child.
     required: true
   priority:
     description:
       - The priority of this parent for this child. Parents with smaller
         numbers will override parents with bigger numbers.
       - 'When defining an inheritance relationship with "state: present", you
         must specify a priority. When deleting an inheritance relationship
         with "state: absent", you should not specify a priority. Ansible will
         simply remove the parent_tag link, regardless of its priority.'
     required: true
   maxdepth:
     description:
       - By default, a tag's inheritance chain is unlimited. This means that
         Koji will look back through an unlimited chain of parent and
         grandparent tags to determine the contents of the tag.
       - You may use this maxdepth parameter to limit the maximum depth of the
         inheritance. For example "0" means that only the parent tag itself
         will be available in the inheritance - parent tags of the parent tag
         won't be available.
       - 'To restore the default umlimited depth behavior on a tag, you can set
         ``maxdepth: null`` or ``maxdepth: `` (empty value).'
       - If you do not set any ``maxdepth`` parameter at all, koji-ansible
         will overwrite an existing tag's current maxdepth setting to "null"
         (in other words, unlimited depth). This was the historical behavior
         of the module and the easiest way to implement this in the code.
         Arguably this behavior is unexpected, because Ansible should only do
         what you tell it to do. We might change this in the future so that
         Ansible only modifies ``maxdepth`` *if* you explicitly configure it.
         Please open GitHub issues to discuss your use-case.
     required: false
     default: null (unlimited depth)
   pkg_filter:
     description:
       - Regular expression selecting the packages for which builds can be
         inherited through this inheritance link.
       - Don't forget to use ``^`` and ``$`` when limiting to exact package
         names; they are not implicit.
       - The default empty string allows all packages to be inherited through
         this link.
     required: false
     default: ''
   intransitive:
     description:
       - Prevents inheritance link from being used by the child tag's children.
         In other words, the link is only used to determine parent tags for the
         child tag directly, but not to determine "grandparent" tags for the
         child tag's children.
     required: false
     default: false
   noconfig:
     description:
       - Prevents tag options ("extra") from being inherited.
     required: false
     default: false
   state:
     description:
       - Whether to add or remove this inheritance link.
     choices: [present, absent]
     default: present
requirements:
  - "python >= 2.7"
  - "koji"
'''

EXAMPLES = '''
- name: Use devtoolset to build for Ceph Nautilus
  hosts: localhost
  tasks:
    - name: set devtoolset-7 as a parent of ceph nautilus
      koji_tag_inheritance:
        koji: kojidev
        parent_tag: sclo7-devtoolset-7-rh-release
        child_tag: storage7-ceph-nautilus-el7-build
        priority: 25

    - name: remove devtoolset-7 as a parent of my other build tag
      koji_tag_inheritance:
        parent_tag: sclo7-devtoolset-7-rh-release
        child_tag: other-storage-el7-build
        state: absent
'''

RETURN = ''' # '''


def get_ids_and_inheritance(session, child_tag, parent_tag):
    """
    Query Koji for the current state of these tags and inheritance.

    :param session: Koji client session
    :param str child_tag: Koji tag name
    :param str parent_tag: Koji tag name
    :return: 3-element tuple of child_id (int), parent_id (int),
             and current_inheritance (list)
    """
    child_taginfo = session.getTag(child_tag)
    parent_taginfo = session.getTag(parent_tag)
    child_id = child_taginfo['id'] if child_taginfo else None
    parent_id = parent_taginfo['id'] if parent_taginfo else None
    if child_id:
        current_inheritance = session.getInheritanceData(child_id)
    else:
        current_inheritance = []
    # TODO use multicall to get all of this at once:
    # (Need to update the test suite fakes to handle multicalls)
    # session.multicall = True
    # session.getTag(child_tag, strict=True)
    # session.getTag(parent_tag, strict=True)
    # session.getInheritanceData(child_tag)
    # multicall_results = session.multiCall(strict=True)
    # # flatten multicall results:
    # multicall_results = [result[0] for result in multicall_results]
    # child_id = multicall_results[0]['id']
    # parent_id = multicall_results[1]['id']
    # current_inheritance = multicall_results[2]
    return (child_id, parent_id, current_inheritance)


def generate_new_rule(child_id, parent_tag, parent_id, priority, maxdepth,
                      pkg_filter, intransitive, noconfig):
    """
    Return a full inheritance rule to add for this child tag.

    :param int child_id: Koji tag id
    :param str parent_tag: Koji tag name
    :param int parent_id: Koji tag id
    :param int priority: Priority of this parent for this child
    :param int maxdepth: Max depth of the inheritance
    :param str pkg_filter: Regular expression string of package names to include
    :param bool intransitive: Don't allow this inheritance link to be inherited
    :param bool noconfig: Prevent tag options ("extra") from being inherited
    """
    return {
        'child_id': child_id,
        'intransitive': intransitive,
        'maxdepth': maxdepth,
        'name': parent_tag,
        'noconfig': noconfig,
        'parent_id': parent_id,
        'pkg_filter': pkg_filter,
        'priority': priority}


def add_tag_inheritance(session, child_tag, parent_tag, priority, maxdepth,
                        pkg_filter, intransitive, noconfig, check_mode):
    """
    Ensure that a tag inheritance rule exists.

    :param session: Koji client session
    :param str child_tag: Koji tag name
    :param str parent_tag: Koji tag name
    :param int priority: Priority of this parent for this child
    :param int maxdepth: Max depth of the inheritance
    :param str pkg_filter: Regular expression string of package names to include
    :param bool intransitive: Don't allow this inheritance link to be inherited
    :param bool noconfig: Prevent tag options ("extra") from being inherited
    :param bool check_mode: don't make any changes
    :return: result (dict)
    """
    result = {'changed': False, 'stdout_lines': []}
    data = get_ids_and_inheritance(session, child_tag, parent_tag)
    child_id, parent_id, current_inheritance = data
    if not child_id:
        msg = 'child tag %s not found' % child_tag
        if check_mode:
            result['stdout_lines'].append(msg)
        else:
            raise ValueError(msg)
    if not parent_id:
        msg = 'parent tag %s not found' % parent_tag
        if check_mode:
            result['stdout_lines'].append(msg)
        else:
            raise ValueError(msg)

    new_rule = generate_new_rule(child_id, parent_tag, parent_id, priority,
                                 maxdepth, pkg_filter, intransitive, noconfig)
    new_rules = [new_rule]
    for rule in current_inheritance:
        if rule == new_rule:
            return result
        if rule['priority'] == priority:
            # prefix taginfo-style inheritance strings with diff-like +/-
            result['stdout_lines'].append('dissimilar rules:')
            result['stdout_lines'].extend(
                    map(lambda r: ' -' + r,
                        common_koji.describe_inheritance_rule(rule)))
            result['stdout_lines'].extend(
                    map(lambda r: ' +' + r,
                        common_koji.describe_inheritance_rule(new_rule)))
            delete_rule = rule.copy()
            # Mark this rule for deletion
            delete_rule['delete link'] = True
            new_rules.insert(0, delete_rule)

    if len(new_rules) > 1:
        result['stdout_lines'].append('remove inheritance link:')
        result['stdout_lines'].extend(
                common_koji.describe_inheritance(new_rules[:-1]))
    result['stdout_lines'].append('add inheritance link:')
    result['stdout_lines'].extend(
            common_koji.describe_inheritance_rule(new_rule))
    result['changed'] = True
    if not check_mode:
        common_koji.ensure_logged_in(session)
        session.setInheritanceData(child_tag, new_rules)
    return result


def remove_tag_inheritance(session, child_tag, parent_tag, check_mode):
    """
    Ensure that a tag inheritance rule does not exist.

    :param session: Koji client session
    :param str child_tag: Koji tag name
    :param str parent_tag: Koji tag name
    :param bool check_mode: don't make any changes
    :return: result (dict)
    """
    result = {'changed': False, 'stdout_lines': []}
    current_inheritance = session.getInheritanceData(child_tag)
    found_rule = {}
    for rule in current_inheritance:
        if rule['name'] == parent_tag:
            found_rule = rule.copy()
            # Mark this rule for deletion
            found_rule['delete link'] = True
    if not found_rule:
        return result
    result['stdout_lines'].append('remove inheritance link:')
    result['stdout_lines'].extend(
            common_koji.describe_inheritance_rule(found_rule))
    result['changed'] = True
    if not check_mode:
        common_koji.ensure_logged_in(session)
        session.setInheritanceData(child_tag, [found_rule])
    return result


def run_module():
    module_args = dict(
        koji=dict(type='str', required=False),
        child_tag=dict(type='str', required=True),
        parent_tag=dict(type='str', required=True),
        priority=dict(type='int', required=False),
        maxdepth=dict(type='int', required=False, default=None),
        pkg_filter=dict(type='str', required=False, default=''),
        intransitive=dict(type='bool', required=False, default=False),
        noconfig=dict(type='bool', required=False, default=False),
        state=dict(type='str', choices=[
                   'present', 'absent'], required=False, default='present'),
    )
    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )

    if not common_koji.HAS_KOJI:
        module.fail_json(msg='koji is required for this module')

    check_mode = module.check_mode
    params = module.params
    state = params['state']
    profile = params['koji']

    session = common_koji.get_session(profile)

    if state == 'present':
        if 'priority' not in params:
            module.fail_json(msg='specify a "priority" integer')
        result = add_tag_inheritance(session,
                                     child_tag=params['child_tag'],
                                     parent_tag=params['parent_tag'],
                                     priority=params['priority'],
                                     maxdepth=params['maxdepth'],
                                     pkg_filter=params['pkg_filter'],
                                     intransitive=params['intransitive'],
                                     noconfig=params['noconfig'],
                                     check_mode=check_mode)
    elif state == 'absent':
        result = remove_tag_inheritance(session,
                                        child_tag=params['child_tag'],
                                        parent_tag=params['parent_tag'],
                                        check_mode=check_mode)

    module.exit_json(**result)


def main():
    run_module()


if __name__ == '__main__':
    main()