Add support for user-defined hosts (depends on !123)
ORIGINAL PROPOSAL (read the updated SUMMARY below summarizing the whole discussion)
I marked this as RFC, since there are a few details that I still need to address:
- builds don't work yet (I suspect because the list of machines is defined as a var file which is passed directly to Ansible)
- error handling from ansible-runner needs to be improved
However, I'd like to get some feedback on whether we can at least agree on the concept and the architecture before I dive into resolving the issues ^above. You can still test "update" with custom hosts.
TL;DR:
- uses the ansible-runner package and wraps it
- exposes a way under ~/.config/lcitool to specify user inventories
- probes user hosts for facts and determines distros
- maps the user host hostnames to our distro-named host_vars
[Background]
lcitool is heavily based on Ansible for which we build the command line and execute it directly. In order to satisfy Ansible's requirements, the ansible
directory hierarchy in this repository is constructed in a way that resembles a standard Ansible-based project. It all works well for the original intended use case - provide means/environment for local build rather than wait for online services to do it for you.
[The "why"]
There's a catch though - what if you want a local build on a powerfull remote machine that you have at your disposal? You can't configure where the workloads runs, because we have defined a static inventory of distros that we can install as VMs locally. So, the whole point of this series is to be able to prepare the same environment we prepare for VMs on a physical machine somewhere to be able to run a build for a project, or run a comprehensive test suite which depends on the upstream build of the project. Naturally, this whole series revolves around changing the way we interact with Ansible - why? Because once we tell users we use Ansible to prepare hosts and they can bring their own, naturally users would like to make use of all the various ways Ansible allows hosts to be specified (even dynamic inventories). Which is kind of a problem since we parse the inventory ourselves and thus allow only a single format and even there we're very picky about it.
[Architecture]
The problem with building automation around Ansible is that it doesn't provide a stable API that could be consumed. However, Ansible provides a wrapper library in Python that is advertised as the stable, backwards compatible intended way of interacting with Ansible - ansible-runner
The runner provides a way how Ansible can be configured via an object, not only can you check what the exit status of the Ansible process was, but you can also register a callback to receive events of the whole execution. But setting up everything can be a little messy so I created another wrapper class around it, so that the whole Ansible interaction and setup is contained within a single module and only APIs for common scenarios are exposed:
- inventory parsing
- fact probing
- playbook execution
- fallback to direct Ansible runner usage by passing through all the options as
**kwargs
and exposing arun
method that maps directly to the one exposed by the Runner object from the ansible-runner package.
The Inventory
class has been reworked so that inventory probing is now done by ansible-inventory
which not only processes all the inventory sources, but also any group/host vars that are defined. This is achieved by passing the ansible.cfg
via an environment variable because ansible-runner doesn't provide any means for the ansible.cfg
to be specified. Here comes the tricky design part - we'll need to use the same host vars for the user-defined hosts, otherwise we'd need to rework the whole Ansible machinery we already have. And since ansible-runner mandates its own way (directory hierarchy) of configuration I had a few options how to approach this:
- Take the directory hierarchy from our repo and basically copy it to a temporary directory which the runner needs anyway because it produces runtime data (artifacts) + create host vars entries for user-defined hosts and copy content from the existing host vars.
- Only create symlinks to files that are needed for a specific Ansible execution inside the temporary directory but the idea remains the same as with 1.
- Keep everything as is by passing over our
ansible.cfg
(and hence use the hierarchy within the repo) but dump a new YAML inventory which would contain the host vars inside it to the temporary directory.
I went with the last option, because it seems to me like the least intrusive one. Then we still need to probe the user-defined hosts for facts in order to figure out what distro they're running so that we can map the host to an existing set of host-vars. To sum it up with a single flow diagram:
"run update" --> parse inventories + host/group vars --> probe user hosts for Ansible facts --> map user hosts to our host vars -> dump a new YAML inventory to <runner_tempdir> --> execute playbook
SUMMARY OF THE DESIGN DISCUSSION BELOW
- our static inventory will be adjusted such that instead of a single
[default]
group we have a[<target-OS>]
for each target OS we care about - our current
host_vars
will becomegroup_vars
to better reflect the fact that all hosts from a specific[<target-OS>]
group share the same facts - switch our direct Ansible usage to the ansible-runner Python package
- for Dockerfile generating purposes, adjust our simple inventory parsing code so that instead of parsing a static inventory we'll load the YAML fact configs from
group_vars
(this will give us both the list of supported targets as well as the corresponding facts) - for VM purposes, we'll copy
group_vars
to ansible-runner's runtime directory and leave Ansible to handle inventories and variables (we won't parse any inventory ourselves) - a new subcommand
targets
will be introduced tolcitool
which takes over the behaviour of the currenthosts
subcommand - the
hosts
subcommand will list hosts compiled from multiple inventory sources - users will be allowed to add their own custom inventory (static or dynamic) under
~/.config/lcitool/inventory
which must group the nodes by the distro the nodes run. At the same time, the aforementioned group names must conform to what thelcitool targets
reports