Johnathan Kupferer
2020-03-17 a2ff7304131a5c50fbd98725417430c1c1d84d5d
Dynamic Role Loading (#1186)

* Add dynamic config

* Remove dynamic config

* Fix agnosticd_dynamic_role_git_sources var usage
8 files added
3 files modified
273 ■■■■■ changed files
.gitignore 3 ●●●●● patch | view | raw | blame | history
ansible/ansible.cfg 2 ●●● patch | view | raw | blame | history
ansible/install_galaxy_roles.yml 4 ●●●● patch | view | raw | blame | history
ansible/roles/agnosticd_dynamic/README.adoc 35 ●●●●● patch | view | raw | blame | history
ansible/roles/agnosticd_dynamic/defaults/main.yml 6 ●●●●● patch | view | raw | blame | history
ansible/roles/agnosticd_dynamic/filter_plugins/agnosticd_dynamic.py 46 ●●●●● patch | view | raw | blame | history
ansible/roles/agnosticd_dynamic/tasks/install-galaxy-source-to-cache.yaml 30 ●●●●● patch | view | raw | blame | history
ansible/roles/agnosticd_dynamic/tasks/install-galaxy-sources-to-dynamic-roles-dir.yaml 23 ●●●●● patch | view | raw | blame | history
ansible/roles/agnosticd_dynamic/tasks/install-git-source.yaml 37 ●●●●● patch | view | raw | blame | history
ansible/roles/agnosticd_dynamic/tasks/main.yml 44 ●●●●● patch | view | raw | blame | history
ansible/roles/agnosticd_dynamic/test_plugins/agnosticd_dynamic.py 43 ●●●●● patch | view | raw | blame | history
.gitignore
@@ -31,3 +31,6 @@
.vscode/settings.json
__pycache__
*.pyc
dynamic-cache
dynamic-roles
ansible/ansible.cfg
@@ -1,6 +1,6 @@
[defaults]
nocows                  = 1
roles_path              = roles-infra:ansible/roles-infra:roles:ansible/roles
roles_path              = dynamic-roles:roles-infra:ansible/roles-infra:roles:ansible/roles
forks                   = 50
become                  = False
gathering               = smart
ansible/install_galaxy_roles.yml
@@ -44,3 +44,7 @@
        and r_requirements_content | length > 0
        and r_requirements_content is mapping
        and "collections" in r_requirements_content
    - name: Install dynamic sources
      include_role:
        name: agnosticd_dynamic
ansible/roles/agnosticd_dynamic/README.adoc
New file
@@ -0,0 +1,35 @@
= agnosticd_dynamic
This Ansible role is used during AgnosticD setup to download dynamic sources from Ansible Galaxy and Git.
== Variables
`agnosticd_dynamic_roles_dir` - Directory in which to install Ansible roles.
This directory should be included in the `ANSIBLE_ROLES_PATH`, which may be set by the `roles_path` in the `[defaults]` section of `ansible.cfg`.
Default value `{{ playbook_dir }}/dynamic-roles`.
`agnosticd_dynamic_cache_dir` - Directory in which to install sources which should be saved between executions.
Default value `{{ playbook_dir }}/dynamic-cache`, but should be configured outside of the playbook directory to preserve data when executing inside Ansible Tower.
`agnosticd_dynamic_role_galaxy_sources` - List of Ansible Galaxy role sources.
Values are specified as in an Ansible `requirements.yml` (https://galaxy.ansible.com/docs/using/installing.html).
If the version is specified with a semantic version syntax then the role source is saved to a cache directory and a link is created from the roles directory.
--------------------
agnosticd_dynamic_role_galaxy_sources:
  - name: k8s_config
    src: redhat-cop.k8s_config
    version: 0.2.0
--------------------
`agnosticd_dynamic_role_git_sources` - List of Git sources to install.
Each source is specified using options to the `git` module (https://docs.ansible.com/ansible/latest/modules/git_module.html).
In addition, `role_paths` may be specified as a dictionary of names and paths within the repository:
--------------------
agnosticd_dynamic_role_git_sources:
  - repo: https://github.com/redhat-cop/agnosticd.git
    version: ocp4-workshop-prod-1.41
    role_paths:
      ocp4-workload-infra-nodes: ansible/roles/ocp4-workload-infra-nodes
--------------------
ansible/roles/agnosticd_dynamic/defaults/main.yml
New file
@@ -0,0 +1,6 @@
---
agnosticd_dynamic_cache_dir: "{{ playbook_dir }}/dynamic-cache"
agnosticd_dynamic_roles_dir: "{{ playbook_dir }}/dynamic-roles"
agnosticd_dynamic_role_galaxy_sources: []
agnosticd_dynamic_role_git_sources: []
#agnosticd_dynamic_git_executable: ...
ansible/roles/agnosticd_dynamic/filter_plugins/agnosticd_dynamic.py
New file
@@ -0,0 +1,46 @@
# Copyright (c) 2020 Red Hat
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['preview'],
    'supported_by': 'community'
}
import hashlib
import re
from ansible.errors import AnsibleFilterError
def agnosticd_dynamic_source_name(source):
    if 'name' in source:
        return source['name']
    if re.match('[\w\-]+\.([\w\-]+)$', source['src']):
        return source['src']
    m = re.search(r'/([\w\-]+)(\.git)?$', source['src'])
    if m:
        return m.group(1)
    raise AnsibleFilterError("Unable to determine source name from {}, name must be provided".format(source['src']))
def agnosticd_dynamic_source_version(source):
    return source.get('version', 'latest')
def agnosticd_dynamic_git_source_name(source):
    m = re.search(r'/([^/]+)(\.git)?$', source['repo'])
    prefix = m.group(1) + '-'
    if 'version' in source:
        prefix += source['version'] + '-'
    return prefix + hashlib.sha256((
        source['repo'] + ':' + source.get('version', '')
    ).encode('utf-8')).hexdigest()
# ---- Ansible filters ----
class FilterModule(object):
    def filters(self):
        return {
            'agnosticd_dynamic_source_name': agnosticd_dynamic_source_name,
            'agnosticd_dynamic_source_version': agnosticd_dynamic_source_version,
            'agnosticd_dynamic_git_source_name': agnosticd_dynamic_git_source_name
        }
ansible/roles/agnosticd_dynamic/tasks/install-galaxy-source-to-cache.yaml
New file
@@ -0,0 +1,30 @@
---
- when: not _source_cache_path is exists
  block:
  - name: Write dynamic requirements.yaml
    copy:
      content: |
        ---
        roles:
        - name: {{ _source_name_version | to_json }}
          src: {{ _source.src | to_json }}
        {% if _source_version != 'latest' %}
          version: {{ _source_version | to_json }}
        {% endif %}
        {% if 'scm' in _source %}
          scm: {{ _source.scm }}
        {% endif %}
      dest: "{{ _requirements_yaml }}"
  - name: Install ansible-galaxy source for {{ _source_name }}
    command: >-
      ansible-galaxy install --no-deps
      --role-file {{ _requirements_yaml | quote }}
      --roles-path {{ agnosticd_dynamic_cache_dir | quote }}
- name: Create link to role cache for {{ _source_name }}
  file:
    state: link
    path: "{{ agnosticd_dynamic_roles_dir }}/{{ _source_name }}"
    src: "{{ _source_cache_path | relpath(agnosticd_dynamic_roles_dir) }}"
ansible/roles/agnosticd_dynamic/tasks/install-galaxy-sources-to-dynamic-roles-dir.yaml
New file
@@ -0,0 +1,23 @@
---
- name: Write dynamic requirements.yaml
  copy:
    content: |
      ---
      roles:
      {% for _source in _sources %}
      - name: {{ _source | agnosticd_dynamic_source_name | to_json }}
        src: {{ _source.src | to_json }}
      {%   if 'scm' in _source %}
        scm: {{ _source.scm }}
      {%   endif %}
      {%   if 'version' in _source %}
        version: {{ _source.version }}
      {%   endif %}
      {% endfor %}
    dest: "{{ agnosticd_dynamic_roles_dir }}/requirements.yaml"
- name: Install ansible-galaxy sources
  command: >-
    ansible-galaxy install --no-deps
    --role-file {{ (agnosticd_dynamic_roles_dir ~ '/requirements.yaml') | quote }}
    --roles-path {{ agnosticd_dynamic_roles_dir | quote }}
ansible/roles/agnosticd_dynamic/tasks/install-git-source.yaml
New file
@@ -0,0 +1,37 @@
---
- name: Git clone for {{ _source_name }}
  git:
    dest: "{{ _install_path }}"
    # Pass-through options to git module
    accept_hostkey: "{{ _source.accept_hostkey | default(omit) }}"
    #archive - not appropriate for this usage
    #bare - not appropriate for this usage
    #clone - always default to yes
    depth: "{{ _source.depth | default(omit) }}"
    executable: "{{ agnosticd_dynamic_git_executable | default(omit) }}"
    force: "{{ _source.force | default(omit) }}"
    gpg_whitelist: "{{ _source.gpg_whitelist | default(omit) }}"
    key_file: "{{ _source.key_file | default(omit) }}"
    recursive: "{{ _source.recursive | default(omit) }}"
    reference: "{{ _source.reference | default(omit) }}"
    refspec: "{{ _source.refspec | default(omit) }}"
    remote: "{{ _source.remote | default(omit) }}"
    repo: "{{ _source.repo | default(omit) }}"
    #separate_git_dir - not appropriate for this usage
    ssh_opts: "{{ _source.ssh_opts | default(omit) }}"
    track_submodules: "{{ _source.track_submodules | default(omit) }}"
    umask: "{{ _source.umask | default(omit) }}"
    update: "{{ _source.get('update', omit) }}"
    verify_commit: "{{ _source.verify_commit | default(omit) }}"
    version: "{{ _source.version | default(omit) }}"
  when: not _install_path is exists
- name: Create role links to git repo for {{ _source_name }}
  file:
    state: link
    path: "{{ agnosticd_dynamic_roles_dir }}/{{ _role.key }}"
    src: "{{ (_install_path ~ '/' ~ _role.value | default('')) | relpath(_install_dir) }}"
  loop: "{{ _source.role_paths | default({}) | dict2items }}"
  loop_control:
    loop_var: _role
    label: "{{ _role.key }}"
ansible/roles/agnosticd_dynamic/tasks/main.yml
New file
@@ -0,0 +1,44 @@
---
- name: Create dynamic-cache and dynamic-roles directories
  file:
    path: "{{ _dir }}"
    state: directory
  loop:
  - "{{ agnosticd_dynamic_cache_dir }}"
  - "{{ agnosticd_dynamic_roles_dir }}"
  loop_control:
    loop_var: _dir
- name: Install ansible-galaxy sources to dynamic roles dir
  include_tasks: install-galaxy-sources-to-dynamic-roles-dir.yaml
  vars:
    _sources: >-
      {{ agnosticd_dynamic_role_galaxy_sources | select('agnosticd_dynamic_cache_disabled') | list }}
  when: _sources != []
- name: Install ansible-galaxy sources to cache
  include_tasks: install-galaxy-source-to-cache.yaml
  loop: >-
    {{ agnosticd_dynamic_role_galaxy_sources | select('agnosticd_dynamic_cache_enabled') | list }}
  loop_control:
    loop_var: _source
    label: "{{ _source_name }}"
  vars:
    _source_name: "{{ _source | agnosticd_dynamic_source_name }}"
    _source_version: "{{ _source | agnosticd_dynamic_source_version }}"
    _source_name_version: "{{ _source_name ~ '-' ~ _source_version }}"
    _source_cache_path: "{{ agnosticd_dynamic_cache_dir }}/{{ _source_name_version }}"
    _requirements_yaml: "{{ agnosticd_dynamic_roles_dir }}/{{ _source_name }}-requirements.yaml"
- name: Install git sources
  include_tasks: install-git-source.yaml
  loop: "{{ agnosticd_dynamic_role_git_sources }}"
  loop_control:
    loop_var: _source
    label: "{{ _source_name }}"
  vars:
    _source_name: "{{ _source | agnosticd_dynamic_role_git_source_name }}"
    _install_dir: >-
      {{ agnosticd_dynamic_cache_dir if _source is agnosticd_dynamic_cache_enabled else agnosticd_dynamic_roles_dir }}
    _install_path: >-
      {{ _install_dir }}/{{ _source_name }}
ansible/roles/agnosticd_dynamic/test_plugins/agnosticd_dynamic.py
New file
@@ -0,0 +1,43 @@
# Copyright (c) 2020 Red Hat
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['preview'],
    'supported_by': 'community'
}
import re
from ansible import errors
def agnosticd_dynamic_cache_enabled(source):
    '''
    Return whether cache is enabled for this source.
    Cache may be enabled explicitly or otherwise detected from the version string.
    Versions that appear to use semantic versioning will be enabled for cache.
    '''
    if 'cache' in source:
        return bool(source['cache'])
    if 'version' not in source:
        return False
    elif re.search(r'\d+\.\d+\.\d+', source['version']):
        return True
    else:
        return False
def agnosticd_dynamic_cache_disabled(source):
    '''
    Return whether cache is disbled for this source.
    '''
    return not agnosticd_dynamic_cache_enabled(source)
class TestModule(object):
    def tests(self):
        return dict(
            agnosticd_dynamic_cache_enabled=agnosticd_dynamic_cache_enabled,
            agnosticd_dynamic_cache_disabled=agnosticd_dynamic_cache_disabled
        )