Skip to content

Instantly share code, notes, and snippets.

@AlanCoding
Last active March 4, 2020 14:38
Show Gist options
  • Save AlanCoding/79bae67d2a1b36614fb17b0029c9b093 to your computer and use it in GitHub Desktop.
Save AlanCoding/79bae67d2a1b36614fb17b0029c9b093 to your computer and use it in GitHub Desktop.
collection migration notes

AWX collection migration story

This gist is about a one-off process to convert the tower_* modules in Ansible core into the AWX collection, and some miscelaneous tasks like cherry picking content in the interium period.

We started out using the content collector tool, with some modifications. This extremely hacky bash script is what I used to document my steps. In the end, we did not use the tree created from this tool, but I did use it to check that what we had is right by diffing between the files it created and what my more manual process created.

Preserving git commits

To keep the commits from the original modules, I used this Stack Overflow copy pasta:

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- lib/ansible/modules/web_infrastructure/ansible_tower lib/ansible/module_utils/ansible_tower.py lib/ansible/plugins/inventory/tower.py' --prune-empty -- --all

This took 2 hours and 16 minutes to finish running. From output of that, I created this repo:

https://github.com/AlanCoding/ansible-tower-content-export

To merge this content back into AWX, I first did it this way:

git checkout devel
git checkout -b awx_modules_merge
git remote add module_content https://github.com/AlanCoding/ansible-tower-content-export.git
git merge module_content/devel --allow-unrelated-histories

I did not like the git "unrelated history", and rebased the branch. This re-parented the top commit so it looked like a more ordinary AWX change. Obviously, if you are starting fresh with a new collection repo, this shouldn't matter to you.

After the merge / rebase was finished, the files had to be moved into the standard directory structure for collections (to allow building). This was a bit of a headache, but eventually I got one nice clean commit to move all the files to where the needed to be, via git mv commands.

Another topic - how do I list all new Ansible commits that modified any relevant files or directories? I never got a good answer to that. Best available option was to filter PRs by label.

Cherry-picking

There were several commits we have pulled in because they were merged into Ansible core after the AWX collection was created. Introducing the collection meant that we were basically maintaining a fork.

The cherry picking process involved 2 repos:

  • Ansible itself is added as a remote to the content-export repo
    • commit is cherry-picked there
    • substantial modifications are made to remove content from other places think of docfragments, other modules, other parts of Ansible, etc.
  • The content-export repo is added as an AWX remote
    • the 2nd version of the commit is cherry-picked there

This worked surprisingly well, because the diff would ride through the file renames. In addition to porting merged fixes, we were also able to adopt a few high-quality open and closed PRs from Ansible core.

Fixing Imports

In our case, the module_utils imports had to be changed as:

# old imports
from ansible.module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode
# what it became
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode

I believe it is probably a good idea for all collection maintainers to use this syntax.

Building, installing, and testing

So far, ensuring that we have the collection version of the modules installed has been a manual process. I apply this diff to the Ansible install that I'm using and verify that a module run will show this output:

diff --git a/lib/ansible/modules/web_infrastructure/ansible_tower/tower_organization.py b/lib/ansible/modules/web_infrastructure/ansible_tower/tower_organization.py
index bba58d8894..f3e9586e69 100644
--- a/lib/ansible/modules/web_infrastructure/ansible_tower/tower_organization.py
+++ b/lib/ansible/modules/web_infrastructure/ansible_tower/tower_organization.py
@@ -88,6 +88,7 @@ def main():
             module.fail_json(msg='Failed to update the organization: {0}'.format(excinfo), changed=False)
 
     json_output['changed'] = result['changed']
+    json_output['this_is_the_old_one'] = True
     module.exit_json(**json_output)
 

Then I install the collection and verify that the output is not there. This is not ideal, and other collection maintainers have pointed this out too.

When testing, I prefer to use custom directories local to the project.

# In module source directory
ansible-galaxy collection build --output-path=alan
ansible-galaxy collection install alan/awx-awx-0.0.1.tar.gz -p alan
# In directory with test playbook
ANSIBLE_COLLECTIONS_PATHS=<rel_path>/alan/ ansible-playbook -i localhost, -e ansible_python_interpreter=$(which python) tower_module.yml

To test that it worked, I used a simple example playbook.

Your mileage may vary, but that's the general pattern. I expect that --output-path in the build command is not necessary in most cases. We used the same directory for the build and install here (but not the source...) use whatever you want it shouldn't matter.

You can also create a directory structured symlinked to your source so that you do not have to either build or install it. That's useful for testing with playbooks (but not anything via ansible-test). A proposal was eventually made to have a Makefile target which setups up the symlink from ~/.ansible/collections to source, via make symlink_collection.

The ansible-doc command should also be tested wherever you did this build / install stuff.

ANSIBLE_COLLECTIONS_PATHS=alan ansible-doc -M alan/ansible_collections/awx/awx/plugins/modules/ -t module awx.awx.tower_organization

See my issue about using non-FQCN module names. I've pretty much given up on that and accepted that any use of ansible-doc, in particular, will require awx.awx.tower_organization and not tower_organization.

FQCN - Fully Qualified Collection Name

Here again, you should hack something into Ansible source code so that you know that it is picking up your modules and not the un-migrated ones.

Templating, namespace and doc fragments

Originally we wanted to keep the doc fragments as extends_documentation_fragment: tower. However, from issues on this subject, there seems to be no plans at all for a "relative" reference to doc fragments. I also held out hope that maybe we could just leave the primary doc fragment inside of core. But I got this repo to test with:

https://github.com/bcoca/ansible-minmal

This sends a pretty clear message, all of the content will be purged. So a solution for the doc fragments has to be developed.

In our case, we had an additional complication that we wanted to be able to change the namespace. From the testing perspective, this is so that I could test upload to:

https://galaxy.ansible.com/alancoding/awx

In other words, both of these cases required some dynamic process of replacing lines in files before building. 2 different solutions were used for both:

  • To substitute multiple values, we used a galaxy.yml.j2 template file
  • To cope with the doc fragment issue, we used a general find + replace in the modules directory

Both of these solutions were implemented in an Ansible playbook, inside the awx_collection folder in the AWX PR.

Publishing

With installing and manual testing taken care of, that leads us into publishing. Some of this is lightly documented in the AWX folder. More ancilary tasks related to this are documented in this other gist.

the good stuff is in AlanCoding/utility-playbooks, roles/publish-collection/tasks/main.yml. This will run back over all the templating steps, then run the ansible-galaxy commands of build and then publish.

Some of that is still in flux, and I will be adding the doc_fragments templating to it.

Deprecating old modules

At first I did this wrong, trying to mark deprecated, and that became very confusing. The process converged on adding a simple migrated_to key in the .github/BOTMETA.yml file. A Pull Request for this was opened and merged. In 2020, that is now concretely the standard practice.

Integrated Unit Testing

For AWX, I developed a wild hacky solution for testing that combines virtual environments, mocks requests, and uses the existing unit test suite to build responses. Pretty soon this will be simplified, because the need for installing tower-cli will be eliminated.

I would consider that closer to "unit" testing. Some collections have integration tests, and some have unit tests like sensu.go. That sensu.go link is a good example of how another collection is using a test client for unit tests. This is most similar to what is being done in AWX, but with some additional piping.

Sanity testing

A Makefile target for sanity testing was added which is make test_collection_sanity. Over time, the relevance of this has increased. We have had several issues where Ansible devel introduced a new rule. This is going to continue to be a thing, since ansible-test is the best available megaphone to broadcast needed changes to collections for deprecations and quality standards.

The construction of this sanity test target is ugly. Several issues have to be worked around. I put a great amount of detail in the issue about ignoring git content which is not checked in. It is still true that you can run sanity tests outside a git repo and have it work. The primary intention is probably that you first build, then install, then run the tests.

In the early days of collections, there were a couple of other issues, related to not recognizing collection module_utils right. It also took time to get it compatible with relative imports. But those have since been resolved, and I have been able to remove several hacks related to replacing the relative import.

What other collection projects are out there?

Notable ones I have found are:

Some notable differences...

module imports can just be done with the fully-qualified namespace in most cases.

from ansible_collections.sensu.sensu_go.plugins.module_utils.base import sensu_argument_spec, AnsibleSensuClient

This should not cause any problems.

Also, doc fragments are rarely set up in a correct way. One notable exception:

extends_documentation_fragment:
  - sensu.sensu_go.base

Full list of collections: https://galaxy.ansible.com/api/v2/collections/

The f5 collection does several things differently than our DOCUMENTION string.

notes:
  - Requires BIG-IP >= 13.1.0
extends_documentation_fragment: f5

The documentation fragment probably isn't going to work. Also, the requirement differs from our pattern, which is:

requirements:
- ansible-tower-cli >= 3.0.2

On the requirements issue, I have no idea who is right, or if it's even possible to be right.

Most repos seem to hard-code in their version into galaxy.yml. For f5, they mark special versions like:

  • 0.9.0-alpha.build3
  • 1.0.0-rc1

The AWX collection used a -beta series for a while before the official first release.

If you want to dive much deeper into collection-to-collection dependencies, I made a test repo with several examples. If you use a -beta version number, dependency resolution will not work, so be warned. If you subsequently release a A.B.C version afterwards, it doesn't pose any problems. There are also a few other scenarios that prevent resolving dependencies or installing as a dependency. So certainly collections need to be blacklisted if you were considering them as a dependency, or else your own collection will be un-installable. The actual use of collection dependencies is relatively niche, so this shouldn't actually affect anyone in practice.

https://github.com/AlanCoding/collection-dependencies-demo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment