diff --git a/koji-tags/README.md b/koji-tags/README.md
new file mode 100644
index 0000000..e6bd167
--- /dev/null
+++ b/koji-tags/README.md
@@ -0,0 +1,40 @@
+# CentOS Infra koji tags tool
+
+## Purpose
+This ansible playbook will just use a list of tags/targets/tags to be inherited and update the koji instance to reflect the needed changes.
+
+## Prerequisites on the machine used to control koji
+
+ * ansible (tested with 2.9.x but should work with previous versions)
+ * git clone of this repository
+ * koji cli installed *and* configured (with a profile, see below)
+ * [koji-ansible](https://github.com/ktdreyer/koji-ansible) : For you convenience, we have included needed tools here, so that this git repo is self-contained
+
+## How to use it
+
+Assuming that we have a koji profile named "mbox" , the following should work before even trying to use the playbook:
+
+```
+koji -p mbox moshimoshi
+```
+
+If it doesn't work, don't even try to use this tool, and configure koji properly (config file, mandatory x509 tls cert/private key etc)
+
+This tool also uses the variable `koji_profile` to know which koji profile to use and also which variables file to source with lists.
+In our case with "mbox", it will source the file vars/mbox.yml (apply same logic for other profiles if needed)
+
+There is an example.yml (so for profile "example") that is provided for reference
+Once you have your vars/{{ koji_profile }}.yml ready, you can start using the tool :
+
+```
+ansible-playbook create_koji_tags.yml
+
+```
+
+This playbook will parse the variables file containings lists and will :
+
+ * create needed tags that would need to be inherited from default list
+ * create koji target (including build tag and dest tags)
+ * modify build tags based on inheritance, either coming from default list, or another list that can be used
+
+
diff --git a/koji-tags/create_koji_tags.yml b/koji-tags/create_koji_tags.yml
new file mode 100644
index 0000000..b8b6a04
--- /dev/null
+++ b/koji-tags/create_koji_tags.yml
@@ -0,0 +1,65 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ koji_profile: mbox # can be updated with --extra-vars
+ tasks:
+ - name: "Loading first variables for koji instance {{ koji_profile }}"
+ include_vars:
+ file: "vars/{{ koji_profile}}.yml"
+ tags:
+ - inheritance
+
+ - name: "Creating first default tags for inheritance on [{{ koji_profile }}] koji instance"
+ koji_tag:
+ koji: "{{ koji_profile }}"
+ name: "{{ item.name }}"
+ arches: "{{ koji_arches }}"
+ state: present
+ with_items: "{{ default_tag_inheritance_list }}"
+ loop_control:
+ label: "{{ item.name }}"
+
+ - name: "Creating needed build tags for targets on [{{ koji_profile }}] koji instance"
+ koji_tag:
+ koji: "{{ koji_profile }}"
+ name: "{{ item.build_tag }}"
+ arches: "{{ koji_arches }}"
+ state: present
+ with_items: "{{ koji_targets }}"
+ loop_control:
+ label: "{{ item.build_tag }} => {{ item.name }}"
+
+ - name: "Creating needed dest tags for targets on [{{ koji_profile }}] koji instance"
+ koji_tag:
+ koji: "{{ koji_profile }}"
+ name: "{{ item.dest_tag }}"
+ arches: "{{ koji_arches }}"
+ state: present
+ with_items: "{{ koji_targets }}"
+ loop_control:
+ label: "{{ item.dest_tag }} => {{ item.name }}"
+
+ - name: "Creating koji targets on on [{{ koji_profile }}] koji instance"
+ koji_target:
+ koji: "{{ koji_profile }}"
+ name: "{{ item.name }}"
+ build_tag: "{{ item.build_tag }}"
+ dest_tag: "{{ item.dest_tag }}"
+ with_items: "{{ koji_targets }}"
+ loop_control:
+ label: "{{ item.name }}"
+
+ - name: "Adding inheritance when needed"
+ koji_tag_inheritance:
+ koji: "{{ koji_profile }}"
+ parent_tag: "{{ item.1.name }}"
+ child_tag: "{{ item.0.name }}"
+ priority: "{{ item.1.priority }}"
+ with_subelements:
+ - "{{ koji_build_tags }}"
+ - inheritance_list
+ loop_control:
+ label: "{{ item.1.name }} => {{ item.0.name }}"
+ tags:
+ - inheritance
+
diff --git a/koji-tags/library/koji_archivetype.py b/koji-tags/library/koji_archivetype.py
new file mode 100644
index 0000000..c4bae35
--- /dev/null
+++ b/koji-tags/library/koji_archivetype.py
@@ -0,0 +1,115 @@
+#!/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_archivetype
+
+short_description: Create and manage Koji archive types
+description:
+ - Create and manage Koji archive types
+ - Your Koji Hub must be version 1.20 or newer in order to use the new
+ ``addArchiveType`` RPC.
+
+options:
+ name:
+ description:
+ - The name of the Koji archive type to create and manage.
+ - 'Example: "deb".'
+ required: true
+ description:
+ description:
+ - The human-readable description of this Koji archive type. Koji uses
+ this value in the UI tooling that display a build's files.
+ - 'Example: "Debian packages".'
+ required: true
+ extensions:
+ description:
+ - The file extensions for this Koji archive type.
+ - 'Example: "deb" means Koji will apply this archive type to files that
+ end in ".deb".'
+ required: true
+requirements:
+ - "python >= 2.7"
+ - "koji"
+'''
+
+EXAMPLES = '''
+- name: Add deb archive types into koji
+ hosts: localhost
+ tasks:
+ - name: Add deb archive type
+ koji_archivetype:
+ name: deb
+ description: Debian packages
+ extensions: deb
+ state: present
+
+ - name: Add dsc archive type
+ koji_archivetype:
+ name: dsc
+ description: Debian source control files
+ extensions: dsc
+ state: present
+'''
+
+RETURN = ''' # '''
+
+
+def run_module():
+ module_args = dict(
+ koji=dict(type='str', required=False),
+ name=dict(type='str', required=True),
+ description=dict(type='str', required=True),
+ extensions=dict(type='str', required=True),
+ 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
+ profile = params['koji']
+ name = params['name']
+ description = params['description']
+ extensions = params['extensions']
+ state = params['state']
+
+ session = common_koji.get_session(profile)
+
+ result = {'changed': False}
+
+ if state == 'present':
+ if not session.getArchiveType(type_name=name):
+ result['changed'] = True
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.addArchiveType(name, description, extensions)
+ elif state == 'absent':
+ module.fail_json(msg="Cannot remove Koji archive types.",
+ changed=False, rc=1)
+
+ module.exit_json(**result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/koji-tags/library/koji_btype.py b/koji-tags/library/koji_btype.py
new file mode 100644
index 0000000..e541380
--- /dev/null
+++ b/koji-tags/library/koji_btype.py
@@ -0,0 +1,94 @@
+#!/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_btype
+
+short_description: Create and manage Koji build types
+description:
+ - Every build in Koji has a "type". This module allows you to define
+ entirely new build types in Koji. These are typically in support of
+ `content generators
+ `_.
+ - Koji only supports adding new build types, not deleting them.
+
+options:
+ name:
+ description:
+ - The name of the Koji build type to create.
+ - 'Example: "debian".'
+ required: true
+requirements:
+ - "python >= 2.7"
+ - "koji"
+'''
+
+EXAMPLES = '''
+- name: create a debian btype in koji
+ hosts: localhost
+ tasks:
+ - name: Create a koji debian btype
+ koji_btype:
+ name: debian
+ state: present
+'''
+
+RETURN = ''' # '''
+
+
+def run_module():
+ module_args = dict(
+ koji=dict(type='str', required=False),
+ name=dict(type='str', required=True),
+ 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
+ profile = params['koji']
+ name = params['name']
+ state = params['state']
+
+ session = common_koji.get_session(profile)
+
+ result = {'changed': False}
+
+ if state == 'present':
+ btype_data = session.listBTypes()
+ btypes = [data['name'] for data in btype_data]
+ if name not in btypes:
+ result['changed'] = True
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.addBType(name)
+ elif state == 'absent':
+ module.fail_json(msg="Cannot remove Koji build types.",
+ changed=False, rc=1)
+
+ module.exit_json(**result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/koji-tags/library/koji_call.py b/koji-tags/library/koji_call.py
new file mode 100644
index 0000000..9eff758
--- /dev/null
+++ b/koji-tags/library/koji_call.py
@@ -0,0 +1,170 @@
+#!/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_call
+
+short_description: Make low-level Koji API calls
+description:
+ - Call Koji's RPC API directly.
+ - Why would you use this module instead of the higher level modules like
+ koji_tag, koji_target, etc? This koji_call module has two main
+ uses-cases.
+ - 1. You may want to do something that the higher level modules do not yet
+ support. It can be easier to use this module to quickly prototype out
+ your ideas for what actions you need, and then write the Python code to
+ do it in a better way later. If you find that you need to use koji_call
+ to achieve functionality that is not yet present in the other
+ koji-ansible modules, please file a Feature Request issue in GitHub with
+ your use case.
+ - 2. You want to write some tests that verify Koji's data at a very low
+ level. For example, you may want to write an integration test to verify
+ that you've set up your Koji configuration in the way you expect.
+ - 'Note that this module will always report "changed: true" every time,
+ because it simply sends the RPC to the Koji Hub on every ansible run.
+ This module cannot understand if your chosen RPC actually "changes"
+ anything.'
+options:
+ name:
+ description:
+ - The name of the Koji RPC to send.
+ - 'Example: "getTag"'
+ required: true
+ args:
+ description:
+ - The list or dict of arguments to pass into the call.
+ - 'Example: ["f29-build"]'
+ required: false
+ login:
+ description:
+ - Whether to authenticate to Koji for this API call or not.
+ Authentication is an extra round-trip to the Hub, so it slower and
+ more load on the database. You should not authenticate if this call
+ is read-only (one of the "get" API calls). If you are doing some
+ create or write operation, you must authenticate. The default
+ behavior is to do an anonymous (non-authenticated) call.
+ choices: [true, false]
+ default: false
+requirements:
+ - "python >= 2.7"
+ - "koji"
+'''
+
+RETURNS = '''
+data:
+ description: Koji's representation of the call result
+ returned: always
+ type: various, depending on the call. Koji could return a dict, or int, or
+ None.
+ sample: {'...'}
+'''
+
+EXAMPLES = '''
+- name: call the Koji API
+ hosts: localhost
+ tasks:
+
+ - name: call the API
+ koji_call:
+ name: getTag
+ args: [f29-build]
+ register: call_result
+
+ - debug:
+ var: call_result.data
+'''
+
+
+def describe_call(name, args):
+ """ Return a human-friendly description of this call. """
+ description = '%s()' % name
+ if args:
+ if isinstance(args, dict):
+ description = '%s(**%s)' % (name, args)
+ elif isinstance(args, list):
+ description = '%s(*%s)' % (name, args)
+ return description
+
+
+def check_mode_call(name, args):
+ """
+ Describe what would have happened if we executed a Koji RPC.
+ """
+ result = {'changed': True}
+ description = describe_call(name, args)
+ result['stdout_lines'] = 'would have called %s' % description
+ return result
+
+
+def do_call(session, name, args, login):
+ """
+ Execute a Koji RPC.
+
+ :param session: Koji client session
+ :param str name: Name of the RPC
+ :param args: list or dict of arguments to this RPC.
+ :param bool login: Whether to log in for this call or not.
+ """
+ result = {'changed': True}
+ if login:
+ common_koji.ensure_logged_in(session)
+ call = getattr(session, name)
+ if isinstance(args, dict):
+ data = call(**args)
+ else:
+ data = call(*args)
+ result['data'] = data
+ return result
+
+
+def run_module():
+ module_args = dict(
+ koji=dict(type='str', required=False),
+ name=dict(type='str', required=True),
+ args=dict(type='raw', required=False, default=[]),
+ login=dict(type='bool', required=False, default=False),
+ )
+ 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')
+
+ params = module.params
+ profile = params['koji']
+ name = params['name']
+ args = params['args']
+ login = params['login']
+
+ if args and not isinstance(args, (dict, list)):
+ msg = "args must be a list or dictionary, not %s" % type(args)
+ module.fail_json(msg=msg, changed=False, rc=1)
+
+ session = common_koji.get_session(profile)
+
+ if module.check_mode:
+ result = check_mode_call(name, args)
+ else:
+ result = do_call(session, name, args, login)
+
+ module.exit_json(**result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/koji-tags/library/koji_cg.py b/koji-tags/library/koji_cg.py
new file mode 100644
index 0000000..60ff399
--- /dev/null
+++ b/koji-tags/library/koji_cg.py
@@ -0,0 +1,181 @@
+#!/usr/bin/python
+import sys
+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_cg
+
+short_description: Create and manage Koji content generators
+description:
+ - This module can grant or revoke access to a `content generator
+ `_ for a user account.
+ - Your Koji Hub must be version 1.19 or newer in order to use the new
+ `listCGs `_ RPC.
+
+
+options:
+ name:
+ description:
+ - The name of the Koji content generator.
+ - 'Example: "debian".'
+ required: true
+ user:
+ description:
+ - The name of the Koji user account.
+ - This user account must already exist in Koji's database. For example,
+ you may run an authenticated "koji hello" command to create the
+ account database entry.
+ - 'Example: "cguser".'
+ required: true
+requirements:
+ - "python >= 2.7"
+ - "koji"
+'''
+
+EXAMPLES = '''
+- name: Grant a user access to a content generator.
+ hosts: localhost
+ tasks:
+ - name: Grant access to the rcm/debbuild account
+ koji_cg:
+ name: debian
+ user: rcm/debbuild
+ state: present
+'''
+
+RETURN = ''' # '''
+
+
+class UnknownCGsError(Exception):
+ """ We cannot know what CGs are present """
+ pass
+
+
+def list_cgs(session):
+ """ Return the result of listCGs, or raise UnknownCGsError """
+ koji_profile = sys.modules[session.__module__]
+ try:
+ return session.listCGs()
+ except koji_profile.GenericError as e:
+ if str(e) == 'Invalid method: listCGs':
+ # Kojihub before version 1.20 will raise this error.
+ raise UnknownCGsError
+ raise
+
+
+def ensure_cg(session, user, name, state, cgs, check_mode):
+ """
+ Ensure that a content generator and user is present or absent.
+
+ :param session: koji ClientSession
+ :param str user: koji user name
+ :param str name: content generator name
+ :param str state: "present" or "absent"
+ :param dict cgs: existing content generators and users
+ :param bool check_mode: if True, show what would happen, but don't do it.
+ :returns: result
+ """
+ result = {'changed': False}
+ if state == 'present':
+ if name not in cgs or user not in cgs[name]['users']:
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.grantCGAccess(user, name, create=True)
+ result['changed'] = True
+ elif state == 'absent':
+ if name in cgs and user in cgs[name]['users']:
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.revokeCGAccess(user, name)
+ result['changed'] = True
+ return result
+
+
+def ensure_unknown_cg(session, user, name, state):
+ """
+ Ensure that a content generator and user is present or absent.
+
+ This method is for older versions of Koji where we do not have the listCGs
+ RPC. This method does not support check_mode.
+
+ :param session: koji ClientSession
+ :param str user: koji user name
+ :param str name: content generator name
+ :param str state: "present" or "absent"
+ :returns: result
+ """
+ result = {'changed': False}
+ koji_profile = sys.modules[session.__module__]
+ common_koji.ensure_logged_in(session)
+ if state == 'present':
+ # The "grant" method will at least raise an error if the permission
+ # was already granted, so we can set the "changed" result based on
+ # that.
+ try:
+ session.grantCGAccess(user, name, create=True)
+ result['changed'] = True
+ except koji_profile.GenericError as e:
+ if 'User already has access to content generator' not in str(e):
+ raise
+ elif state == 'absent':
+ # There's no indication whether this changed anything, so we're going
+ # to be pessimistic and say we're always changing it.
+ session.revokeCGAccess(user, name)
+ result['changed'] = True
+ return result
+
+
+def run_module():
+ module_args = dict(
+ koji=dict(type='str', required=False),
+ name=dict(type='str', required=True),
+ user=dict(type='str', required=True),
+ 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
+ profile = params['koji']
+ name = params['name']
+ user = params['user']
+ state = params['state']
+
+ session = common_koji.get_session(profile)
+
+ try:
+ cgs = list_cgs(session)
+ result = ensure_cg(session, user, name, state, cgs, check_mode)
+ except UnknownCGsError:
+ if check_mode:
+ msg = 'check mode does not work without listCGs'
+ result = {'changed': False, 'msg': msg}
+ else:
+ result = ensure_unknown_cg(session, user, name, state)
+
+ module.exit_json(**result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/koji-tags/library/koji_external_repo.py b/koji-tags/library/koji_external_repo.py
new file mode 100644
index 0000000..4ca103d
--- /dev/null
+++ b/koji-tags/library/koji_external_repo.py
@@ -0,0 +1,148 @@
+#!/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_external_repo
+
+short_description: Create and manage Koji external repos
+description:
+ - Create and manage Koji external repos
+options:
+ name:
+ description:
+ - The name of the Koji external repo to create and manage.
+ required: true
+ url:
+ description:
+ - The URL to the Koji external repo.
+ - Note, this uses "$arch", not the common "$basearch" you may find in a
+ typical Yum repository file.
+ - For idempotency, please ensure your url always ends with a "/"
+ character. If you leave it out, Koji Hub will automatically add a "/"
+ slash when storing this value in the database, and every subsequent
+ Ansible run will appear to be "changing" the external repo's URL.
+ required: true
+requirements:
+ - "python >= 2.7"
+ - "koji"
+'''
+
+EXAMPLES = '''
+- name: create a koji tag with an external repo.
+ hosts: localhost
+ tasks:
+ - name: Create an external repo for CentOS "CR"
+ koji_external_repo:
+ name: centos7-cr
+ url: http://mirror.centos.org/centos/7/cr/$arch/
+ state: present
+
+ - name: Create a koji tag that uses the CentOS CR repo
+ koji_tag:
+ name: storage7-ceph-nautilus-el7-build
+ state: present
+ external_repos:
+ - repo: centos7-cr
+ priority: 5
+'''
+
+RETURN = ''' # '''
+
+
+def ensure_external_repo(session, name, check_mode, url):
+ """
+ Ensure that this external repo exists in Koji.
+
+ :param session: Koji client session
+ :param name: Koji external repo name
+ :param check_mode: don't make any changes
+ :param url: URL to this external repo
+ """
+ repoinfo = session.getExternalRepo(name)
+ result = {'changed': False, 'stdout_lines': []}
+ if not repoinfo:
+ if check_mode:
+ result['stdout_lines'].append('would create repo %s' % name)
+ result['changed'] = True
+ return result
+ common_koji.ensure_logged_in(session)
+ repoinfo = session.createExternalRepo(name, url)
+ result['stdout_lines'].append('created repo id %d' % repoinfo['id'])
+ result['changed'] = True
+ return result
+ if repoinfo['url'] != url:
+ result['stdout_lines'].append('set url to %s' % url)
+ result['changed'] = True
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.editExternalRepo(info=repoinfo['id'], url=url)
+ return result
+
+
+def delete_external_repo(session, name, check_mode):
+ """ Ensure that this external_repo is deleted from Koji. """
+ repoinfo = session.getExternalRepo(name)
+ result = dict(
+ stdout='',
+ changed=False,
+ )
+ if repoinfo:
+ result['stdout'] = 'deleted external repo %s' % name
+ result['changed'] = True
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.deleteExternalRepo(name)
+ return result
+
+
+def run_module():
+ module_args = dict(
+ koji=dict(type='str', required=False),
+ name=dict(type='str', required=True),
+ state=dict(type='str', choices=[
+ 'present', 'absent'], required=False, default='present'),
+ url=dict(type='str', required=False, default=None),
+ )
+ 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
+ profile = params['koji']
+ name = params['name']
+ state = params['state']
+ url = params['url']
+
+ session = common_koji.get_session(profile)
+
+ if state == 'present':
+ if not url:
+ module.fail_json(msg='you must set a url for this external_repo')
+ result = ensure_external_repo(session, name, check_mode, url)
+ elif state == 'absent':
+ result = delete_external_repo(session, name, check_mode)
+
+ module.exit_json(**result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/koji-tags/library/koji_host.py b/koji-tags/library/koji_host.py
new file mode 100644
index 0000000..9e43574
--- /dev/null
+++ b/koji-tags/library/koji_host.py
@@ -0,0 +1,234 @@
+#!/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_host
+
+short_description: Create and manage Koji build hosts
+description:
+ - This module can add new hosts and manage existing hosts.
+ - 'Koji only supports adding new hosts, not deleting them. Once they are
+ defined, you can enable or disable the hosts with "state: enabled" or
+ "state: disabled".'
+
+options:
+ name:
+ description:
+ - The name of the Koji builder.
+ - 'Example: "builder1.example.com".'
+ required: true
+ arches:
+ description:
+ - The list of arches this host supports.
+ - 'Example: [x86_64]'
+ required: true
+ channels:
+ description:
+ - The list of channels this host should belong to.
+ - If you specify a completely new channel here, Ansible will create the
+ channel on the hub. For example, when you set up OSBS with Koji, you
+ must add a builder host to a new "container" channel. You can simply
+ specify "container" in the list here, and Ansible will create the new
+ "container" channel when it adds your host to that channel.
+ - 'Example: [default, createrepo]'
+ required: false
+ state:
+ description:
+ - Whether to set this host as "enabled" or "disabled". If unset, this
+ defaults to "enabled".
+ krb_principal:
+ description:
+ - Set a non-default krb principal for this host. If unset, Koji will
+ use the standard krb principal scheme for builder accounts.
+ capacity:
+ description:
+ - Total task weight for this host. This is a float value. If unset,
+ Koji will use the standard capacity for a host (2.0).
+ - 'Example: 10.0'
+ description:
+ description:
+ - Human-readable description for this host.
+ comment:
+ description:
+ - Human-readable comment explaining the current state of the host. You
+ may write a description here explaining how this host was set up, or
+ why this host is currently offline.
+requirements:
+ - "python >= 2.7"
+ - "koji"
+'''
+
+EXAMPLES = '''
+- name: create a koji host
+ hosts: localhost
+ tasks:
+ - name: Add new builder1 host
+ koji_host:
+ name: builder1.example.com
+ arches: [x86_64]
+ state: enabled
+ channels:
+ - createrepo
+ - default
+
+ - name: Add new builder host for OSBS
+ koji_host:
+ name: containerbuild1.example.com
+ arches: [x86_64]
+ state: enabled
+ channels:
+ # This will automatically create the "container" channel
+ # if it does not already exist:
+ - container
+'''
+
+
+def ensure_channels(session, host_id, host_name, check_mode, desired_channels):
+ """
+ Ensure that given host belongs to given channels (and only them).
+
+ :param session: Koji client session
+ :param int host_id: Koji host ID
+ :param int host_name: Koji host name
+ :param bool check_mode: don't make any changes
+ :param list desired_channels: channels that the host should belong to
+ """
+ result = {'changed': False, 'stdout_lines': []}
+ common_koji.ensure_logged_in(session)
+ current_channels = session.listChannels(host_id)
+ current_channels = [channel['name'] for channel in current_channels]
+ for channel in current_channels:
+ if channel not in desired_channels:
+ if not check_mode:
+ session.removeHostFromChannel(host_name, channel)
+ result['stdout_lines'].append('removed host from channel %s' % channel)
+ result['changed'] = True
+ for channel in desired_channels:
+ if channel not in current_channels:
+ if not check_mode:
+ session.addHostToChannel(host_name, channel, create=True)
+ result['stdout_lines'].append('added host to channel %s' % channel)
+ result['changed'] = True
+ return result
+
+
+def ensure_host(session, name, check_mode, state, arches, krb_principal,
+ channels, **kwargs):
+ """
+ Ensure that this host is configured in Koji.
+
+ :param session: Koji client session
+ :param str name: Koji builder host name
+ :param bool check_mode: don't make any changes
+ :param str state: "enabled" or "disabled"
+ :param list arches: list of arches for this builder.
+ :param str krb_principal: custom kerberos principal, or None
+ :param list chanels: list of channels this host should belong to.
+ :param **kwargs: Pass remaining kwargs directly into Koji's editHost RPC.
+ """
+ result = {'changed': False, 'stdout_lines': []}
+ host = session.getHost(name)
+ if not host:
+ result['changed'] = True
+ result['stdout_lines'].append('created host')
+ if check_mode:
+ return result
+ common_koji.ensure_logged_in(session)
+ id_ = session.addHost(name, arches, krb_principal)
+ host = session.getHost(id_)
+ if state == 'enabled':
+ if not host['enabled']:
+ result['changed'] = True
+ result['stdout_lines'].append('enabled host')
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.enableHost(name)
+ elif state == 'disabled':
+ if host['enabled']:
+ result['changed'] = True
+ result['stdout_lines'].append('disabled host')
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.disableHost(name)
+ edits = {}
+ if ' '.join(arches) != host['arches']:
+ edits['arches'] = ' '.join(arches)
+ for key, value in kwargs.items():
+ if value is None:
+ continue # Ansible did not set this parameter.
+ if key in host and kwargs[key] != host[key]:
+ edits[key] = value
+ if edits:
+ result['changed'] = True
+ for edit in edits.keys():
+ result['stdout_lines'].append('edited host %s' % edit)
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.editHost(name, **edits)
+
+ # Ensure host is member of desired channels.
+ if channels not in (None, ''):
+ channels_result = ensure_channels(session, host['id'],
+ name, check_mode, channels)
+ if channels_result['changed']:
+ result['changed'] = True
+ result['stdout_lines'].extend(channels_result['stdout_lines'])
+
+ return result
+
+
+def run_module():
+ module_args = dict(
+ koji=dict(type='str', required=False),
+ name=dict(type='str', required=True),
+ arches=dict(type='list', required=True),
+ channels=dict(type='list', required=False, default=None),
+ krb_principal=dict(type='str', required=False, default=None),
+ capacity=dict(type='float', required=False, default=None),
+ description=dict(type='str', required=False, default=None),
+ comment=dict(type='str', required=False, default=None),
+ state=dict(type='str', choices=[
+ 'enabled', 'disabled'], required=False, default='enabled'),
+ )
+ 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
+ profile = params['koji']
+ name = params['name']
+ state = params['state']
+
+ session = common_koji.get_session(profile)
+
+ result = ensure_host(session, name, check_mode, state,
+ arches=params['arches'],
+ channels=params['channels'],
+ krb_principal=params['krb_principal'],
+ capacity=params['capacity'],
+ description=params['description'],
+ comment=params['comment'])
+ module.exit_json(**result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/koji-tags/library/koji_tag.py b/koji-tags/library/koji_tag.py
new file mode 100644
index 0000000..5e78359
--- /dev/null
+++ b/koji-tags/library/koji_tag.py
@@ -0,0 +1,590 @@
+#!/usr/bin/python
+from ansible.module_utils.basic import AnsibleModule
+from collections import defaultdict
+from ansible.module_utils import common_koji
+from ansible.module_utils.six import string_types
+
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.0',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+
+DOCUMENTATION = '''
+---
+module: koji_tag
+
+short_description: Create and manage Koji tags
+description:
+ - Create and manage Koji tags
+options:
+ name:
+ description:
+ - The name of the Koji tag to create and manage.
+ required: true
+ inheritance:
+ description:
+ - How to set inheritance. what happens when it's unset.
+ external_repos:
+ description:
+ - list of Koji external repos to set for this tag. Each element of the
+ list should have a "repo" (the external repo name) and "priority"
+ (integer).
+ packages:
+ description:
+ - dict of package owners and the a lists of packages each owner
+ maintains.
+ groups:
+ description:
+ - A tag's "groups" tell Koji what packages will be present in the
+ tag's buildroot. For example, the "build" group defines the packages
+ that Koji will put into a "build" task's buildroot. You may set other
+ package groups on a tag as well, like "srpm-build" or
+ "applicance-build".
+ - This should be a dict of groups and packages to set for this tag.
+ Each dict key will be the name of the group. Each dict value should
+ be a list of package names to include in the comps for this group.
+ - If a group or package defined in this field is already applicable for
+ a tag due to inheritance, Koji will not allow it to be added to the
+ tag, but will instead silently ignore it. Conversely, groups and
+ packages that are inherited in this field are not removed if they are
+ left unspecified. Therefore, this field will only have an effect if it
+ includes groups and packages that are unique to this tag (i.e., not
+ inherited).
+ - This does not support advanced comps group operations, like
+ configuring extra options on groups, or blocking packages in groups.
+ If you need that level of control over comps groups, you will need
+ to import a full comps XML file, outside of this Ansible module.
+ arches:
+ description:
+ - space-separated string of arches this Koji tag supports.
+ - Note, the order in which you specify architectures does matter in a
+ few subtle cases. For example, the SRPM that Koji includes in the
+ build is the one built on the first arch in this list. Likewise,
+ rpmdiff compares RPMs built on the first arch with RPMs built on
+ other arches.
+ perm:
+ description:
+ - permission (string or int) for this Koji tag.
+ locked:
+ description:
+ - whether to lock this tag or not.
+ choices: [true, false]
+ default: false
+ maven_support:
+ description:
+ - whether Maven repos should be generated for the tag.
+ choices: [true, false]
+ default: false
+ maven_include_all:
+ description:
+ - include every build in this tag (including multiple versions of the
+ same package) in the Maven repo.
+ choices: [true, false]
+ default: false
+ extra:
+ description:
+ - set any extra parameters on this tag.
+requirements:
+ - "python >= 2.7"
+ - "koji"
+'''
+
+EXAMPLES = '''
+- name: create a main koji tag and candidate tag
+ hosts: localhost
+ tasks:
+ - name: Create a main product koji tag
+ koji_tag:
+ koji: kojidev
+ name: ceph-3.1-rhel-7
+ arches: x86_64
+ state: present
+ packages:
+ kdreyer:
+ - ansible
+ - ceph
+ - ceph-ansible
+
+ - name: Create a candidate koji tag
+ koji_tag:
+ koji: kojidev
+ name: ceph-3.1-rhel-7-candidate
+ state: present
+ inheritance:
+ - parent: ceph-3.1-rhel-7
+ priority: 0
+
+ - name: Create a tag that uses an external repo
+ koji_tag:
+ koji: kojidev
+ name: storage7-ceph-nautilus-el7-build
+ state: present
+ external_repos:
+ - repo: centos7-cr
+ priority: 5
+
+ - name: Create a tag that uses comps groups
+ koji_tag:
+ name: foo-el7-build
+ groups:
+ srpm-build:
+ - rpm-build
+ - fedpkg
+'''
+
+RETURN = ''' # '''
+
+
+class DuplicateNameError(Exception):
+ """ The user specified two external repos with the same name. """
+ pass
+
+
+class DuplicatePriorityError(Exception):
+ """ The user specified two external repos with the same priority. """
+ pass
+
+
+def validate_repos(repos):
+ """Ensure that each external repository has unique name and priority
+ values.
+
+ This prevents the user from accidentally specifying two or more external
+ repositories with the same name or priority.
+
+ :param repos: list of repository dicts
+ :raises: DuplicatePriorityError if two repos have the same priority.
+ """
+ names = set()
+ priorities = set()
+ for repo in repos:
+ name = repo['repo']
+ priority = repo['priority']
+ if name in names:
+ raise DuplicateNameError(name)
+ if priority in priorities:
+ raise DuplicatePriorityError(priority)
+ names.add(name)
+ priorities.add(priority)
+
+
+def normalize_inheritance(inheritance):
+ """
+ Transform inheritance module argument input into the format returned by
+ getInheritanceData(). Only includes supported fields (excluding child_id
+ and parent_id), renames "parent" to "name", chooses correct defaults for
+ missing fields, and performs some limited type correction for maxdepth and
+ priority.
+
+ :param inheritance: list of inheritance dicts
+ """
+ normalized_inheritance = []
+
+ for rule in inheritance:
+ # maxdepth: treat empty strings the same as None
+ maxdepth = rule.get('maxdepth')
+ if maxdepth == '':
+ maxdepth = None
+ if isinstance(maxdepth, string_types):
+ maxdepth = int(maxdepth)
+
+ normalized_inheritance.append(dict(
+ # we don't know child_id yet
+ intransitive=rule.get('intransitive', False),
+ maxdepth=maxdepth,
+ name=rule['parent'],
+ noconfig=rule.get('noconfig', False),
+ # we don't know parent_id yet
+ pkg_filter=rule.get('pkg_filter', ''),
+ priority=int(rule['priority']),
+ ))
+
+ return sorted(normalized_inheritance, key=lambda i: i['priority'])
+
+
+def ensure_inheritance(session, tag_name, tag_id, check_mode, inheritance):
+ """
+ Ensure that these inheritance rules are configured on this Koji tag.
+
+ :param session: Koji client session
+ :param str tag_name: Koji tag name
+ :param int tag_id: Koji tag ID
+ :param bool check_mode: don't make any changes
+ :param list inheritance: ensure these rules are set, and no others
+ """
+ result = {'changed': False, 'stdout_lines': []}
+
+ # resolve parent tag IDs
+ rules = []
+ for rule in normalize_inheritance(inheritance):
+ parent_name = rule['name']
+ parent_taginfo = session.getTag(parent_name)
+ if not parent_taginfo:
+ msg = "parent tag '%s' not found" % parent_name
+ if check_mode:
+ result['stdout_lines'].append(msg)
+ # spoof to allow continuation
+ parent_taginfo = {'id': 0}
+ else:
+ raise ValueError(msg)
+ parent_id = parent_taginfo['id']
+ rules.append(dict(rule, child_id=tag_id, parent_id=parent_id))
+
+ current_inheritance = session.getInheritanceData(tag_name)
+ if current_inheritance != rules:
+ result['stdout_lines'].extend(
+ ('current inheritance:',)
+ + common_koji.describe_inheritance(current_inheritance)
+ + ('new inheritance:',)
+ + common_koji.describe_inheritance(rules))
+ result['changed'] = True
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.setInheritanceData(tag_name, rules, clear=True)
+ return result
+
+
+def ensure_external_repos(session, tag_name, check_mode, repos):
+ """
+ Ensure that these external repos are configured on this Koji tag.
+
+ :param session: Koji client session
+ :param str tag_name: Koji tag name
+ :param bool check_mode: don't make any changes
+ :param list repos: ensure these external repos are set, and no others.
+ """
+ result = {'changed': False, 'stdout_lines': []}
+ validate_repos(repos)
+ current_repo_list = session.getTagExternalRepos(tag_name)
+ current = {repo['external_repo_name']: repo for repo in current_repo_list}
+ current_priorities = {
+ str(repo['priority']): repo for repo in current_repo_list
+ }
+ for repo in sorted(repos, key=lambda r: r['priority']):
+ repo_name = repo['repo']
+ repo_priority = repo['priority']
+ if repo_name in current:
+ # The repo is present for this tag.
+ # Now ensure the priority is correct.
+ if repo_priority == current[repo_name]['priority']:
+ continue
+ result['changed'] = True
+ msg = 'set %s repo priority to %i' % (repo_name, repo_priority)
+ result['stdout_lines'].append(msg)
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.editTagExternalRepo(tag_name, repo_name, repo_priority)
+ continue
+ elif str(repo_priority) in current_priorities:
+ # No need to check for name equivalence here; it would already
+ # have happened
+ result['changed'] = True
+ msg = 'set repo at priority %i to %s' % (repo_priority, repo_name)
+ result['stdout_lines'].append(msg)
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ same_priority_repo = current_priorities.get(
+ str(repo_priority)).get('external_repo_name')
+ session.removeExternalRepoFromTag(tag_name, same_priority_repo)
+ session.addExternalRepoToTag(
+ tag_name, repo_name, repo_priority)
+ # Prevent duplicate attempts
+ del current_priorities[str(repo_priority)]
+ continue
+ result['changed'] = True
+ msg = 'add %s external repo to %s' % (repo_name, tag_name)
+ result['stdout_lines'].append(msg)
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.addExternalRepoToTag(tag_name, repo_name, repo_priority)
+ # Find the repos to remove from this tag.
+ repo_names = [repo['repo'] for repo in repos]
+ current_names = current.keys()
+ repos_to_remove = set(current_names) - set(repo_names)
+ for repo_name in repos_to_remove:
+ result['changed'] = True
+ msg = 'removed %s repo from %s tag' % (repo_name, tag_name)
+ result['stdout_lines'].append(msg)
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.removeExternalRepoFromTag(tag_name, repo_name)
+ return result
+
+
+def ensure_packages(session, tag_name, tag_id, check_mode, packages):
+ """
+ Ensure that these packages are configured on this Koji tag.
+
+ :param session: Koji client session
+ :param str tag_name: Koji tag name
+ :param int tag_id: Koji tag ID
+ :param bool check_mode: don't make any changes
+ :param dict packages: ensure these packages are set (?)
+ """
+ result = {'changed': False, 'stdout_lines': []}
+ # Note: this in particular could really benefit from koji's
+ # multicalls...
+ common_koji.ensure_logged_in(session)
+ current_pkgs = session.listPackages(tagID=tag_id)
+ current_names = set([pkg['package_name'] for pkg in current_pkgs])
+ # Create a "current_owned" dict to compare with what's in Ansible.
+ current_owned = defaultdict(set)
+ for pkg in current_pkgs:
+ owner = pkg['owner_name']
+ pkg_name = pkg['package_name']
+ current_owned[owner].add(pkg_name)
+ for owner, owned in packages.items():
+ for package in owned:
+ if package not in current_names:
+ # The package was missing from the tag entirely.
+ if not check_mode:
+ session.packageListAdd(tag_name, package, owner)
+ result['stdout_lines'].append('added pkg %s' % package)
+ result['changed'] = True
+ else:
+ # The package is already in this tag.
+ # Verify ownership.
+ if package not in current_owned.get(owner, []):
+ if not check_mode:
+ session.packageListSetOwner(tag_name, package, owner)
+ result['stdout_lines'].append('set %s owner %s' %
+ (package, owner))
+ result['changed'] = True
+ # Delete any packages not in Ansible.
+ all_names = [name for names in packages.values() for name in names]
+ delete_names = set(current_names) - set(all_names)
+ for package in delete_names:
+ result['stdout_lines'].append('remove pkg %s' % package)
+ result['changed'] = True
+ if not check_mode:
+ session.packageListRemove(tag_name, package, owner)
+ return result
+
+
+def ensure_groups(session, tag_id, check_mode, desired_groups):
+ """
+ Ensure that these groups are configured on this Koji tag.
+
+ :param session: Koji client session
+ :param int tag_id: Koji tag ID
+ :param bool check_mode: don't make any changes
+ :param dict desired_groups: ensure these groups are set (?)
+ """
+ result = {'changed': False, 'stdout_lines': []}
+ common_koji.ensure_logged_in(session)
+ current_groups = session.getTagGroups(tag_id)
+ for group in current_groups:
+ if group['tag_id'] == tag_id and group['name'] not in desired_groups:
+ if not check_mode:
+ session.groupListRemove(tag_id, group['name'])
+ result['stdout_lines'].append('removed group %s' % group['name'])
+ result['changed'] = True
+ for group_name, desired_pkgs in desired_groups.items():
+ for group in current_groups:
+ if group['name'] == group_name:
+ current_pkgs = {entry['package']: entry['tag_id']
+ for entry in group['packagelist']}
+ break
+ else:
+ current_pkgs = {}
+ if not check_mode:
+ session.groupListAdd(tag_id, group_name)
+ result['stdout_lines'].append('added group %s' % group_name)
+ result['changed'] = True
+
+ for package, pkg_tag_id in current_pkgs.items():
+ if pkg_tag_id == tag_id and package not in desired_pkgs:
+ if not check_mode:
+ session.groupPackageListRemove(tag_id, group_name, package)
+ result['stdout_lines'].append('removed pkg %s from group %s' % (package, group_name))
+ result['changed'] = True
+ for package in desired_pkgs:
+ if package not in current_pkgs:
+ if not check_mode:
+ session.groupPackageListAdd(tag_id, group_name, package)
+ result['stdout_lines'].append('added pkg %s to group %s' % (package, group_name))
+ result['changed'] = True
+ return result
+
+
+def compound_parameter_present(param_name, param, expected_type):
+ if param not in (None, ''):
+ if not isinstance(param, expected_type):
+ raise ValueError(param_name + ' must be a '
+ + expected_type.__class__.__name__
+ + ', not a ' + param.__class__.__name__)
+ return True
+ return False
+
+
+def ensure_tag(session, name, check_mode, inheritance, external_repos,
+ packages, groups, **kwargs):
+ """
+ Ensure that this tag exists in Koji.
+
+ :param session: Koji client session
+ :param name: Koji tag name
+ :param check_mode: don't make any changes
+ :param inheritance: Koji tag inheritance settings. These will be translated
+ for Koji's setInheritanceData RPC.
+ :param external_repos: Koji external repos to set for this tag.
+ :param packages: dict of packages to add ("whitelist") for this tag.
+ If this is an empty dict, we don't touch the package list
+ for this tag.
+ :param groups: dict of comps groups to set for this tag.
+ :param **kwargs: Pass remaining kwargs directly into Koji's createTag and
+ editTag2 RPCs.
+ """
+ taginfo = session.getTag(name)
+ result = {'changed': False, 'stdout_lines': []}
+ if not taginfo:
+ if check_mode:
+ result['stdout_lines'].append('would create tag %s' % name)
+ result['changed'] = True
+ return result
+ common_koji.ensure_logged_in(session)
+ if 'perm' in kwargs and kwargs['perm']:
+ kwargs['perm'] = common_koji.get_perm_id(session, kwargs['perm'])
+ id_ = session.createTag(name, parent=None, **kwargs)
+ result['stdout_lines'].append('created tag id %d' % id_)
+ result['changed'] = True
+ taginfo = {'id': id_} # populate for inheritance management below
+ else:
+ # The tag name already exists. Ensure all the parameters are set.
+ edits = {}
+ edit_log = []
+ for key, value in kwargs.items():
+ if taginfo[key] != value and value is not None:
+ edits[key] = value
+ edit_log.append('%s: changed %s from "%s" to "%s"'
+ % (name, key, taginfo[key], value))
+ # Find out which "extra" items we must explicitly remove
+ # ("remove_extra" argument to editTag2).
+ if 'extra' in kwargs and kwargs['extra'] is not None:
+ for key in taginfo['extra']:
+ if key not in kwargs['extra']:
+ if 'remove_extra' not in edits:
+ edits['remove_extra'] = []
+ edits['remove_extra'].append(key)
+ if 'remove_extra' in edits:
+ edit_log.append('%s: remove extra fields "%s"'
+ % (name, '", "'.join(edits['remove_extra'])))
+ if edits:
+ result['stdout_lines'].extend(edit_log)
+ result['changed'] = True
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.editTag2(name, **edits)
+
+ # Ensure inheritance rules are all set.
+ if compound_parameter_present('inheritance', inheritance, list):
+ inheritance_result = ensure_inheritance(session, name, taginfo['id'],
+ check_mode, inheritance)
+ if inheritance_result['changed']:
+ result['changed'] = True
+ result['stdout_lines'].extend(inheritance_result['stdout_lines'])
+
+ # Ensure external repos.
+ if compound_parameter_present('external_repos', external_repos, list):
+ repos_result = ensure_external_repos(session, name, check_mode,
+ external_repos)
+ if repos_result['changed']:
+ result['changed'] = True
+ result['stdout_lines'].extend(repos_result['stdout_lines'])
+
+ # Ensure package list.
+ if compound_parameter_present('packages', packages, dict):
+ packages_result = ensure_packages(session, name, taginfo['id'],
+ check_mode, packages)
+ if packages_result['changed']:
+ result['changed'] = True
+ result['stdout_lines'].extend(packages_result['stdout_lines'])
+
+ # Ensure group list.
+ if compound_parameter_present('groups', groups, dict):
+ groups_result = ensure_groups(session, taginfo['id'],
+ check_mode, groups)
+ if groups_result['changed']:
+ result['changed'] = True
+ result['stdout_lines'].extend(groups_result['stdout_lines'])
+
+ return result
+
+
+def delete_tag(session, name, check_mode):
+ """ Ensure that this tag is deleted from Koji. """
+ taginfo = session.getTag(name)
+ result = dict(
+ stdout='',
+ changed=False,
+ )
+ if taginfo:
+ result['stdout'] = 'deleted tag %d' % taginfo['id']
+ result['changed'] = True
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.deleteTag(name)
+ return result
+
+
+def run_module():
+ module_args = dict(
+ koji=dict(type='str', required=False),
+ name=dict(type='str', required=True),
+ state=dict(type='str', choices=[
+ 'present', 'absent'], required=False, default='present'),
+ inheritance=dict(type='raw', required=False, default=None),
+ external_repos=dict(type='raw', required=False, default=None),
+ packages=dict(type='raw', required=False, default=None),
+ groups=dict(type='raw', required=False, default=None),
+ arches=dict(type='str', required=False, default=None),
+ perm=dict(type='str', required=False, default=None),
+ locked=dict(type='bool', required=False, default=False),
+ maven_support=dict(type='bool', required=False, default=False),
+ maven_include_all=dict(type='bool', required=False, default=False),
+ extra=dict(type='dict', required=False, default=None),
+ )
+ 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
+ profile = params['koji']
+ name = params['name']
+ state = params['state']
+
+ session = common_koji.get_session(profile)
+
+ if state == 'present':
+ result = ensure_tag(session, name,
+ check_mode,
+ inheritance=params['inheritance'],
+ external_repos=params['external_repos'],
+ packages=params['packages'],
+ groups=params['groups'],
+ arches=params['arches'],
+ perm=params['perm'] or None,
+ locked=params['locked'],
+ maven_support=params['maven_support'],
+ maven_include_all=params['maven_include_all'],
+ extra=params['extra'])
+ elif state == 'absent':
+ result = delete_tag(session, name, check_mode)
+
+ module.exit_json(**result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/koji-tags/library/koji_tag_inheritance.py b/koji-tags/library/koji_tag_inheritance.py
new file mode 100644
index 0000000..53ebc84
--- /dev/null
+++ b/koji-tags/library/koji_tag_inheritance.py
@@ -0,0 +1,329 @@
+#!/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
+ `_ 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()
diff --git a/koji-tags/library/koji_target.py b/koji-tags/library/koji_target.py
new file mode 100644
index 0000000..14b0505
--- /dev/null
+++ b/koji-tags/library/koji_target.py
@@ -0,0 +1,150 @@
+#!/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_target
+
+short_description: Create and manage Koji targets
+description:
+ - Create, update, and delete targets within Koji.
+options:
+ name:
+ description:
+ - The name of the Koji target to create and manage.
+ required: true
+ build_tag:
+ description:
+ - The name of the "build" or "buildroot" tag. The latest builds in
+ this tag will be available in the buildroot when you build an RPM or
+ container for this Koji target.
+ - 'Example: "f29-build"'
+ required: true
+ dest_tag:
+ description:
+ - The name of the "destination" tag. When Koji completes a build for
+ this target, it will tag that build into this destination tag.
+ - 'Example: "f29-updates-candidate"'
+ required: true
+requirements:
+ - "python >= 2.7"
+ - "koji"
+'''
+
+EXAMPLES = '''
+- name: create a koji target
+ hosts: localhost
+ tasks:
+
+ - name: Configure CBS target
+ koji_target:
+ name: storage7-ceph-nautilus-el7
+ build_tag: storage7-ceph-nautilus-el7-build
+ dest_tag: storage7-ceph-nautilus-candidate
+'''
+
+
+def ensure_target(session, name, check_mode, build_tag, dest_tag):
+ """
+ Ensure that this target exists in Koji.
+
+ :param session: Koji client session
+ :param name: Koji target name
+ :param check_mode: don't make any changes
+ :param build_tag: Koji build tag name, eg. "f29-build"
+ :param dest_tag: Koji destination tag name, eg "f29-updates-candidate"
+ """
+ targetinfo = session.getBuildTarget(name)
+ result = {'changed': False, 'stdout_lines': []}
+ if not targetinfo:
+ result['changed'] = True
+ if check_mode:
+ result['stdout_lines'].append('would create target %s' % name)
+ return result
+ common_koji.ensure_logged_in(session)
+ session.createBuildTarget(name, build_tag, dest_tag)
+ targetinfo = session.getBuildTarget(name)
+ result['stdout_lines'].append('created target %s' % targetinfo['id'])
+ # Ensure the build and destination tags are set for this target.
+ needs_edit = False
+ if build_tag != targetinfo['build_tag_name']:
+ needs_edit = True
+ result['stdout_lines'].append('build_tag_name: %s' % build_tag)
+ if dest_tag != targetinfo['dest_tag_name']:
+ needs_edit = True
+ result['stdout_lines'].append('dest_tag_name: %s' % dest_tag)
+ if needs_edit:
+ result['changed'] = True
+ if check_mode:
+ return result
+ common_koji.ensure_logged_in(session)
+ session.editBuildTarget(name, name, build_tag, dest_tag)
+ return result
+
+
+def delete_target(session, name, check_mode):
+ """ Ensure that this tag is deleted from Koji. """
+ targetinfo = session.getBuildTarget(name)
+ result = dict(
+ stdout='',
+ changed=False,
+ )
+ if targetinfo:
+ result['stdout'] = 'deleted target %d' % targetinfo['id']
+ result['changed'] = True
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ session.deleteBuildTarget(targetinfo['id'])
+ return result
+
+
+def run_module():
+ module_args = dict(
+ koji=dict(type='str', required=False),
+ name=dict(type='str', required=True),
+ state=dict(type='str', choices=[
+ 'present', 'absent'], required=False, default='present'),
+ build_tag=dict(type='str', required=True),
+ dest_tag=dict(type='str', required=True),
+ )
+ 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
+ profile = params['koji']
+ name = params['name']
+ state = params['state']
+ build_tag = params['build_tag']
+ dest_tag = params['dest_tag']
+
+ session = common_koji.get_session(profile)
+
+ if state == 'present':
+ result = ensure_target(session, name, check_mode, build_tag, dest_tag)
+ elif state == 'absent':
+ result = delete_target(session, name, check_mode)
+
+ module.exit_json(**result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/koji-tags/library/koji_user.py b/koji-tags/library/koji_user.py
new file mode 100644
index 0000000..aa46f31
--- /dev/null
+++ b/koji-tags/library/koji_user.py
@@ -0,0 +1,154 @@
+#!/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_user
+
+short_description: Create and manage Koji user accounts
+description:
+ - This module can add new users and manage existing users.
+ - 'Koji only supports adding new users, not deleting them. Once they are
+ defined, you can enable or disable the users with "state: enabled" or
+ "state: disabled".'
+
+options:
+ name:
+ description:
+ - The name of the Koji user.
+ - 'Example: "kdreyer".'
+ required: true
+ state:
+ description:
+ - Whether to set this user as "enabled" or "disabled". If unset, this
+ defaults to "enabled".
+ permissions:
+ description:
+ - A list of permissions for this user.
+ - 'Example: [admin]'
+ krb_principal:
+ description:
+ - Set a non-default krb principal for this user. If unset, Koji will
+ use the standard krb principal scheme for user accounts.
+ - Warning, Koji only allows you to set this one time, at the point at
+ which you create the new account. You cannot edit the krb_principal
+ for an existing account.
+requirements:
+ - "python >= 2.7"
+ - "koji"
+'''
+
+EXAMPLES = '''
+- name: create a koji user
+ hosts: localhost
+ tasks:
+ - name: Add new kdreyer user
+ koji_user:
+ name: kdreyer
+ state: enabled
+ permissions: [admin]
+'''
+
+
+def ensure_user(session, name, check_mode, state, permissions, krb_principal):
+ """
+ Ensure that this user is configured in Koji.
+
+ :param session: Koji client session
+ :param str name: Koji builder user name
+ :param bool check_mode: don't make any changes
+ :param str state: "enabled" or "disabled"
+ :param list permissions: list of permissions for this user.
+ :param str krb_principal: custom kerberos principal, or None. Used only at
+ account creation time.
+ """
+ result = {'changed': False, 'stdout_lines': []}
+ if state == 'enabled':
+ desired_status = common_koji.koji.USER_STATUS['NORMAL']
+ else:
+ desired_status = common_koji.koji.USER_STATUS['BLOCKED']
+ user = session.getUser(name)
+ if not user:
+ result['changed'] = True
+ result['stdout_lines'] = ['created %s user' % name]
+ if check_mode:
+ return result
+ common_koji.ensure_logged_in(session)
+ id_ = session.createUser(name, desired_status, krb_principal)
+ user = session.getUser(id_)
+ if user['status'] != desired_status:
+ result['changed'] = True
+ result['stdout_lines'] = ['%s %s user' % (state, name)]
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ if state == 'enabled':
+ session.enableUser(name)
+ else:
+ session.disableUser(name)
+ if not permissions:
+ return result
+ current_perms = session.getUserPerms(user['id'])
+ to_grant = set(permissions) - set(current_perms)
+ to_revoke = set(current_perms) - set(permissions)
+ if to_grant or to_revoke:
+ result['changed'] = True
+ if not check_mode:
+ common_koji.ensure_logged_in(session)
+ for permission in to_grant:
+ result['stdout_lines'].append('grant %s' % permission)
+ if not check_mode:
+ session.grantPermission(name, permission, True)
+ for permission in to_revoke:
+ result['stdout_lines'].append('revoke %s' % permission)
+ if not check_mode:
+ session.revokePermission(name, permission)
+ return result
+
+
+def run_module():
+ module_args = dict(
+ koji=dict(type='str', required=False),
+ name=dict(type='str', required=True),
+ permissions=dict(type='list', required=True),
+ krb_principal=dict(type='str', required=False, default=None),
+ state=dict(type='str', choices=[
+ 'enabled', 'disabled'], required=False, default='enabled'),
+ )
+ 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
+ profile = params['koji']
+ name = params['name']
+ state = params['state']
+
+ session = common_koji.get_session(profile)
+
+ result = ensure_user(session, name, check_mode, state,
+ permissions=params['permissions'],
+ krb_principal=params['krb_principal'])
+
+ module.exit_json(**result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/koji-tags/module_utils/common_koji.py b/koji-tags/module_utils/common_koji.py
new file mode 100644
index 0000000..ecf9740
--- /dev/null
+++ b/koji-tags/module_utils/common_koji.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+import os
+try:
+ import koji
+ from koji_cli.lib import activate_session
+ HAS_KOJI = True
+except ImportError:
+ HAS_KOJI = False
+
+
+def get_profile_name(profile):
+ """
+ Return a koji profile name.
+
+ :param str profile: profile name, like "koji" or "cbs", or None. If None,
+ we will use return the "KOJI_PROFILE" environment
+ variable. If we could find no profile name, raise
+ ValueError.
+ :returns: str, the profile name
+ """
+ if profile:
+ return profile
+ profile = os.getenv('KOJI_PROFILE')
+ if profile:
+ return profile
+ raise ValueError('set a profile "koji" argument for this task, or set '
+ 'the KOJI_PROFILE environment variable')
+
+
+def get_session(profile):
+ """
+ Return an anonymous koji session for this profile name.
+
+ :param str profile: profile name, like "koji" or "cbs". If None, we will
+ use a profile name from the "KOJI_PROFILE" environment
+ variable.
+ :returns: anonymous koji.ClientSession
+ """
+ profile = get_profile_name(profile)
+ # Note, get_profile_module() raises koji.ConfigurationError if we
+ # could not find this profile's name in /etc/koji.conf.d/*.conf and
+ # ~/.koji/config.d/*.conf.
+ mykoji = koji.get_profile_module(profile)
+ # Workaround https://pagure.io/koji/issue/1022 . Koji 1.17 will not need
+ # this.
+ if '~' in str(mykoji.config.cert):
+ mykoji.config.cert = os.path.expanduser(mykoji.config.cert)
+ if '~' in str(mykoji.config.ca):
+ mykoji.config.ca = os.path.expanduser(mykoji.config.ca)
+ # Note, Koji has a grab_session_options() method that can also create a
+ # stripped-down dict of our module's (OptParse) configuration, like:
+ # opts = mykoji.grab_session_options(mykoji.config)
+ # The idea is that callers then pass that opts dict into ClientSession's
+ # constructor.
+ # There are two reasons we don't use that here:
+ # 1. The dict is only suitable for the ClientSession(..., opts), not for
+ # activate_session(..., opts). activate_session() really wants the full
+ # set of key/values in mykoji.config.
+ # 2. We may call activate_session() later outside of this method, so we
+ # need to preserve all the configuration data from mykoji.config inside
+ # the ClientSession object. We might as well just store it in the
+ # ClientSession's .opts and then pass that into activate_session().
+ opts = vars(mykoji.config)
+ # Force an anonymous session (noauth):
+ opts['noauth'] = True
+ session = mykoji.ClientSession(mykoji.config.server, opts)
+ # activate_session with noauth will simply ensure that we can connect with
+ # a getAPIVersion RPC. Let's avoid it here because it just slows us down.
+ # activate_session(session, opts)
+ return session
+
+
+def ensure_logged_in(session):
+ """
+ Authenticate this Koji session (if necessary).
+
+ :param session: a koji.ClientSession
+ :returns: None
+ """
+ if not session.logged_in:
+ session.opts['noauth'] = False
+ # Log in ("activate") this session:
+ # Note: this can raise SystemExit if there is a problem, eg with
+ # Kerberos:
+ activate_session(session, session.opts)
+
+
+# inheritance display utils
+
+
+def describe_inheritance_rule(rule):
+ """
+ Given a dictionary representing a koji inheritance rule (i.e., one of the
+ elements of getInheritanceData()'s result), return a tuple of strings to be
+ appended to a module's stdout_lines array conforming to the output of
+ koji's taginfo CLI command, e.g.:
+ 0 .... a-parent-tag
+ 10 M... another-parent-tag
+ maxdepth: 1
+ 100 .F.. yet-another-parent-tag
+ package filter: ^prefix-
+ """
+ # koji_cli/commands.py near the end of anon_handle_taginfo()
+ flags = '%s%s%s%s' % (
+ 'M' if rule['maxdepth'] not in ('', None) else '.',
+ 'F' if rule['pkg_filter'] not in ('', None) else '.',
+ 'I' if rule['intransitive'] else '.',
+ 'N' if rule['noconfig'] else '.',
+ )
+
+ result = ["%4d %s %s" % (rule['priority'], flags, rule['name'])]
+
+ if rule['maxdepth'] not in ('', None):
+ result.append(" maxdepth: %d" % rule['maxdepth'])
+ if rule['pkg_filter'] not in ('', None):
+ result.append(" package filter: %s" % rule['pkg_filter'])
+
+ return tuple(result)
+
+
+def describe_inheritance(rules):
+ """
+ Given a sequence of dictionaries representing koji inheritance rules (i.e.,
+ getInheritanceData()'s result), return a tuple of strings to be appended to
+ a module's stdout_lines array conforming to the output of koji's taginfo
+ CLI command. See describe_inheritance_rule for sample output.
+ """
+
+ # each invocation of describe_inheritance_rule yields a tuple of strings
+ # to be appended to a module's stdout_lines result, so concatenate them:
+ # sum(…, tuple()) will flatten tuples of tuples into just the child tuples
+ # > sum( ((1, 2), (3, 4)), tuple() ) ⇒ (1, 2) + (3, 4) + (,) ⇒ (1, 2, 3, 4)
+ return sum(tuple(map(describe_inheritance_rule, rules)), tuple())
+
+
+# permission utils
+
+
+perm_cache = {}
+
+
+def get_perms(session):
+ global perm_cache
+ if not perm_cache:
+ perm_cache = dict([
+ (perm['name'], perm['id']) for perm in session.getAllPerms()
+ ])
+ return perm_cache
+
+
+def get_perm_id(session, name):
+ perms = get_perms(session)
+ return perms[name]
+
+
+def get_perm_name(session, id_):
+ perms = get_perms(session)
+ for perm_name, perm_id in perms.items():
+ if perm_id == id_:
+ return perm_name
diff --git a/koji-tags/vars/example.yml b/koji-tags/vars/example.yml
new file mode 100644
index 0000000..61e608d
--- /dev/null
+++ b/koji-tags/vars/example.yml
@@ -0,0 +1,39 @@
+# This vars file will be included for koji profile [example]
+# It has to exist on execution node (and koji configured properly)
+
+# List of architectures supported on this koji instance
+koji_arches: x86_64 ppc64le aarch64
+
+# List of tags automatically added in build tags if no list is provided
+default_tag_inheritance_list:
+ - name: el8_1
+ priority: 5
+ - name: dist-c8-updates
+ priority: 10
+ - name: dist-c8
+ priority: 15
+ - name: module-javapackages-tools-201801-8000020190620195035-211ef2cd
+ priority: 20
+ - name: module-pki-deps-10.6-8000020190624155206-d1bfb1e2
+ priority: 21
+
+# List of koji targets, where we'll submit builds
+koji_targets:
+ - name: dist-c8_1-updates
+ build_tag: dist-c8_1-updates-build
+ dest_tag: dist-c8-updates
+ - name: dist-c8_2-updates
+ build_tag: dist-c8_2-updates-build
+ dest_tag: dist-c8-updates
+
+# List of build tags used in targets, but also configured with default inheritance or overriding it
+koji_build_tags:
+ - name: dist-c8_2-updates-build #inheriting default_tag_inheritance_list
+ inheritance_list: "{{ default_tag_inheritance_list }}"
+ - name: zzz-rh-ruby999-rh
+ inheritance_list:
+ - name: storage7-gluster-7-testing
+ priority: 2
+ - name: test77-common-candidate
+ priority: 5
+
diff --git a/koji-tags/vars/mbox.yml b/koji-tags/vars/mbox.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/koji-tags/vars/mbox.yml