-
Notifications
You must be signed in to change notification settings - Fork 23.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added Ansible Tower Inventory Plugin. #41816
Conversation
The test
The test
The test
The test
|
# Before you execute the following commands, you should make sure this file is in your plugin path, | ||
# and you enabled this plugin. | ||
# Set environment variables: | ||
# export TOWER_HOST=YOUR_TOWER_HOST_ADDRESS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than limiting this to environment variables you could utilize config to allow a more flexible use case. That would let you document each option individually and mark them as required so you don't have to manually check in the plugin that they were provided.
e.g.
- document options with their environment components:
options:
tower_host:
description: ...
env:
- name: TOWER_HOST
required: true
...
- allow parsing of a file called tower_inventory by calling self._read_config_data(path)
- access those options with self.get_option('tower_host') after either calling self._read_config_data(path) (which will merge options provided in the tower_inventory file with the environment vars in a self._options dict)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could keep the use case of no inventory config file if the path doesn't exist by calling self.set_options yourself (rather than happening in _read_config_data()) to have access to the environment variables with get_option()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @s-hertel ! I will make these modification.
self._read_config_data(path) | ||
# Read inventory from tower server. | ||
# Note the environment variables will be handled automatically by InventoryManager. | ||
inventory = self.read_tower_inventory(str(self.get_option('tower_host')), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than str, use to_text from ansible.module_utils._text to ensure compatibility with python 2 and 3: to_text(option, errors='surrogate_or_strict')
Looks good to me besides that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reviewing this PR @s-hertel! Will do.
plugin_type: inventory | ||
authors: | ||
- Matthew Jones (@matburt) | ||
- Yunfan Zhang <yunfzhan@redhat.com> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want to add your github username? (@YunfanZhang42)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem!
@s-hertel Do you mind to take a look again? |
Looks good to me. I'm not sure if there is a use case for users to create their own tower groups, but it would be easy to leverage constructed features if so. You can check out the virtualbox inventory for an example (in a nutshell, add the constructed documentation fragment, update InventoryModule to @bcoca Do you mind reviewing this? |
- Yunfan Zhang (@YunfanZhang42) | ||
short_description: Ansible dynamic inventory plugin for Ansible Tower. | ||
description: | ||
- Imports inventories from Ansible Tower. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not really an 'import', 'uses tower as inventory source'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will change it, thanks for pointing out.
NAME = 'tower' | ||
# Stays backward compatible with tower inventory script. | ||
# If the user supplies 'tower_inventory' as path, the plugin will read from environment variables. | ||
read_all_configs_from_env_var = False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why force either/or? you should be able to read some things from the config file, others from env vars, user might not wan't all eggs in one basket
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't force either/or. Just indicates when the plugin needs to call set_options (or have self._read_config_data do it) if the inventory file doesn't exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
direct only gets used for inventory plugins if there's a config file. Otherwise get_option suffices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@s-hertel Thanks for the clarification! That is my intention.
''' | ||
The default get_option cannot cast int to string, even if the type is set. | ||
We override that behavior and force all numbers into string. | ||
''' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should fix the ensure_type
function to do this, instead of working around it in every plugin
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this still has not changed, what is the actual issue you hit to require this 'type enforcement'?
self.get_option('tower_username'), | ||
self.get_option('tower_password'), | ||
self.get_option('tower_inventory'), | ||
os.environ.get('TOWER_LICENSE_TYPE', 'enterprise'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this ONLY an env var?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would really like to pull @matburt into the decision-making on this matter. It's not just the sourcing from the env var here, but also the use of the license_type
parameter once it's received. What does this mean in practice? I think users would expect to be able to use the plugin as a part of the Ansible core CLI with any given type of target AWX or Tower instance.
# export TOWER_INVENTORY=THE_ID_OF_TARGETED_INVENTORY | ||
# Read the inventory specified in TOWER_INVENTORY from Ansible Tower, and list them. | ||
# The inventory path must always be tower_inventory if you are reading all settings from environment variables. | ||
# ansible-inventory -i tower_inventory --list |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can also just have a config file with plugin: tower
as it's only entry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey Brian thanks for your comments! The reason we did it this way is this plugin is adapted from an inventory script of Ansible Tower, so the we want to keep this "read environment variable only, do not require any config file" behavior to be backward compatible with the old way of reading inventory from Ansible Tower.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are other plugins supporting any kind of file-less use? It seems like that's more of where this was trying to go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. They need to have a config file with plugin: plugin_name
in it, and then read everything else from env var.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my other issue is if user has an actual 'tower_inventory' file in cwd ... i would use something more like @tower_inventory
or some other symbol to disambiguate from 'actual file'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If they have an actual file, I think that file will end with .yml
so this plugin will not mistakenly read it?
default: True | ||
env: | ||
- name: TOWER_VERIFY_SSL | ||
required: False |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
|
||
try: | ||
import requests | ||
from requests.auth import HTTPBasicAuth |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not a blocker: i would use the internal url functions in lib/ansilbe/module_utils/urls.py
as that is always present and has less issues with different versions than requests does
# If the JSON parse fails, print the ValueError | ||
raise AnsibleParserError("Failed to parse json from host: {err}".format(err=e)) | ||
except requests.ConnectionError as e: | ||
raise AnsibleParserError("Connection to remote host failed: {err}".format(err=e)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wrap exceptions in to_native
when stringifying them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Will do!
type: string | ||
env: | ||
- name: TOWER_PASSWORD | ||
required: True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also might be good to compare these options to the options in
ansible/lib/ansible/utils/module_docs_fragments/tower.py
Lines 23 to 42 in cdd21e2
DOCUMENTATION = ''' | |
options: | |
tower_host: | |
description: | |
- URL to your Tower instance. | |
tower_username: | |
description: | |
- Username for your Tower instance. | |
tower_password: | |
description: | |
- Password for your Tower instance. | |
tower_verify_ssl: | |
description: | |
- Dis/allow insecure connections to Tower. If C(no), SSL certificates will not be validated. | |
This should only be used on personally controlled sites using self-signed certificates. | |
type: bool | |
default: 'yes' | |
tower_config_file: | |
description: | |
- Path to the Tower config file. See notes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AlanCoding Thanks for the dropping in! It seems we are using same names for items in yaml config file, while keeping the environment variable names the same as the they were in inventory script, so we should be fine.
I think we let the users to specify tower_config_file
at run time, so this option is no longer needed right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we let the users to specify tower_config_file at run time, so this option is no longer needed right?
What do you mean by this?
I think the current state breaks congruence with the tower modules in that it won't accept the .tower_cli.cfg
file. I'm not going to push for that right now, because I don't have some kind of grand unification plan that would allow both to take the same forms of credentials, although you could maybe just import the options processing code from the tower modules and have it work.
That may be too ambitious, and I'm braced for this being somewhat inconsistent of a specification.
If you look at my openstack usage example:
You can see that the native configuration file format is clouds.yml
, and the inventory file can do nothing more than point to the native configuration file.
Granted, I hate this, but it seems to be the canonical way of writing inventory plugins. If you extrapolated this pattern, then yes, I think you would allow the entry of tower_config_file
inside of the tower_inventory.yml
file. @bcoca please correct me if I have horribly misunderstood your standards here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its fine to have a 'separate pre-existing file', I would just use it as a fallback if other methods are not populated and i would not create one if it were not already standard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i actually don't think you should force having the tower_config_file
entry, but it could be posed as an option and default to the 'normal location'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the openstack plugin is not really the best example of standards to follow, you might be better off looking at aws_ec2 which also has 'pre existing configs' via boto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I agree that if tower_config_file
is added as an option it should be optional. As a first-pass, maybe we don't need it, but have a plan for it in the future.
I still need to get synced up about the 2 methods of specification that this PR current does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So let me propose this, if just using the 'name' (no inventory source file) or the file contains a tower_config_file entry, use that first if they exist (in parse method):
super(InventoryModule, self).parse(inventory, loader, path)
if self.read_all_configs_from_env_var: # I would change name to 'no config file supplied' or similar
if os.path.exists('~/.tower_cli.cfg'):
# pick up options from the file
self._set_options_from_tower_cli('~/.tower_cli.ccfg')
self.set_options() # this will update with env vars to override file or pick up others
else:
self._read_config_data(path)
if self.get_option('tower_cli_config'): # check for user defined
self._set_options_from_tower_cli(self.get_option('tower_cli_config')
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our current decision is to keep this plugin simple and do not let it read from tower_cli_config
. In the future we might add this option.
# export TOWER_HOST=YOUR_TOWER_HOST_ADDRESS | ||
# export TOWER_USERNAME=YOUR_TOWER_USERNAME | ||
# export TOWER_PASSWORD=YOUR_TOWER_PASSWORD | ||
# export TOWER_INVENTORY=THE_ID_OF_TARGETED_INVENTORY |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
def verify_file(self, path): | ||
if path.endswith('tower_inventory'): | ||
self.read_all_configs_from_env_var = True | ||
return True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with the other comments that this is hard to follow, this should look for vars from options first, and then check for the env var if the option is not provided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this is tricky. I think if the user want to read everything from environment variables, they still needs to provide a path
in the command line to trigger the plugin, so I put tower_inventory as an option if they do not want to use any config file. I will switch to other implements if there is a better way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the first-pass, I wouldn't balk at requiring a file with plugin: tower
in it.
The question I'm more interested in is "do other plugins offer this in this type of format?"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all other inventory plugins currently use a file. Since most employ their own verify_file and don't always call super(InventoryModule, self).verify_file(path)
there is kind of a dichotomy though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, host_list and advance_host_list use an 'inventory string' , which is why they dont call super version of verify_file as 'its not a file'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, okay.
Maybe if non-file inventory is to be supported, a more transparent way of doing this would be to remove self.read_all_configs_from_env_var, which does sound like it's indicating an either/or scenario and then put in parse() something like...
if os.path.isfile(path):
try:
config = self.loader.load_from_file(path)
except Exception as e:
raise AnsibleParserError(to_native(e))
self.set_options(direct=config)
Oh, nevermind, I guess I see why you did that. The user could have a tower_inventory/@tower_inventory file for some reason. +1 to using @tower_inventory or something similar though, to make that more unlikely.
edit edit:
Hm. I guess just need a solution for that corner case. Or check verify_file that the file exists first, and move the path.endswith(... check to the elif.
type: string | ||
env: | ||
- name: TOWER_HOST | ||
required: True |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
@YunfanZhang42 did you rebase against devel? This is looking good, I have tested:
All of the issues are resolved with this testing so far. I am accumulating a list of non-blocking enhancements to table for later:
I would still like to do a little bit more testing tomorrow morning. |
raise AnsibleParserError(to_native('Connection to remote host failed: {err}'.format(err=e))) | ||
|
||
def verify_file(self, path): | ||
if path.endswith('@tower_inventory'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this really be 'endswith' or more like equal? path.strip() == '@tower_inventory'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On my computer it gives /Users/yunfzhan/ansible/@tower_inventory
, so we probably should stick with endswith
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, its seeing it as a file name ... weird since host_list 'works' .. i should look into that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is likely to be the use pattern of Tower / AWX, so I think allow this may be important. It sounds like this implementation may be subject to revision if another general fix is put in, but is there anything blocking this PR @bcoca for the time being?
tower_host: your_tower_server_network_address | ||
tower_username: your_username | ||
tower_password: your_password | ||
tower_inventory: the_ID_of_targeted_inventory |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one thing that does bother me is that this isn't called tower_inventory_id
, and likewise for the environment variable. If there is to be any sense of parity with the tower modules, then we would expect specification by inventory and organization names. Thus, we will add tower_inventory_name
and probably tower_inventory_organization
. In the syntax of the modules, either tower_inventory
or tower_inventory_organization
would implicitly refer to the name.
Also, it seems that there would be no problem for implementing this other env name as far as AWX server changes are involved.
plugin: tower | ||
tower_host: your_tower_server_network_address | ||
tower_username: your_username | ||
tower_password: your_password |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry if I'm being a broken record about this, but I see lots of hazards for us in the future. Maintaining this could be a large undertaking if there's not a good deal of congruency. I now see three different forms of configuration files:
inventory plugin
plugin: tower
tower_host: https://localhost:8043/
tower_username: admin
tower_password: password1
tower_inventory: 6
tower_verify_ssl: False
tower modules
(docs actually call out key=value
syntax, but docs are poorly thought out in several ways currently)
parameters match what are directly passed into the modules
host: https://localhost:8043/
username: admin
password: password1
verify_ssl: False
tower-cli config
[general]
host = http://localhost:8013/
verify_ssl = false
oauth_token = xxxxxxxxxxxxxxx
For the most part, I agree with the environment variable keys used here. However, I know this is painful, but it seems that the inventory config format is the odd-one out here. The tower-cli code can be adjusted to accept the YAML format. The modules can be free to ignore other variables like "plugin", but we can't go back once we've introduced the long names in the inventory plugin config.
I withdraw suggestions I previously made regarding the module_utils code. I looked further into it, and even if we have parity with the parameter names, what it does it fairly trivial for our purposes, and would conflict with the plugin class |
For general development reference, I want to call out the fact that the tower-cli methods will take the The tower modules apply the settings directly, so they deal exclusively with the un-prefixed namespace. |
plugin_type: inventory | ||
authors: | ||
- Matthew Jones (@matburt) | ||
- Yunfan Zhang (@YunfanZhang42) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ready to merge once you add missing version_added: "2.7"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem, will add version in.
Tested with the most recent version:
Commenting out username and specifying via env var The only thing that sticks out me me is that it appears that the env vars are lower precedence than the vars defined in the file. I didn't expect this, but it appears that this is consistent with the behavior of the modules, so this is actually desirable. With this, the PR has my final 👍 |
The test
|
error with inventory with ungrouped host
|
for group_name, group_content in six.iteritems(inventory): | ||
if group_name != 'all' and group_name != '_meta': | ||
# First add hosts to groups | ||
for host_name in group_content['hosts']: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in all cases, I would default group_contents.get('hosts', [])
for host_name in group_content['hosts']: | ||
self.inventory.add_host(host_name, group_name) | ||
# Then add the parent-children group relationships. | ||
for child_group_name in group_content['children']: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also default children list to []
self.inventory.add_child(group_name, child_group_name) | ||
# Set the group vars. Note we should set group var for 'all', but not '_meta'. | ||
if group_name != '_meta': | ||
for var_name, var_value in six.iteritems(group_content['vars']): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would default vars to {}
verify_ssl=self.get_option('verify_ssl')) | ||
# To start with, create all the groups. | ||
for group_name in inventory: | ||
if group_name != '_meta': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little iffy about the ungrouped or all group here. I would skip ungrouped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is fine it seems. Our API never gives ungrouped
, but inventory.add_host(host_name)
in Ansible Inventory API will automatically add an ungroup
to the hosts.
Signed-off-by: Yunfan Zhang <yz322@duke.edu>
ungrouped case fixed
|
SUMMARY
This PR will allow the users to import an inventory from Ansible Tower using ansible inventory plugin. Example usage:
ansible-inventory -i @tower_inventory --list
. Note this plugin will read from environment variables instead of a config file, sotower_inventory
is a keyword for triggering this plugin, not a path for the actual configuration file path.ISSUE TYPE
COMPONENT NAME
lib/ansible/plugins/inventory/tower.py
ANSIBLE VERSION
ADDITIONAL INFORMATION
The reason that this plugin reads from environment variable instead of a configuration file is we used to have a ansible inventory script for tower that reads configuration from environment variables, and this plugin is a port of that inventory script, so we want to keep the consistency between the two.
Sample Usage: