Skip to content
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

Porting Collections Off of Tower CLI #5747

Merged

Conversation

beeankha
Copy link
Contributor

SUMMARY

The related AWX issue can be found here: #5145. This is a collaborative PR between myself and @john-westcott-iv .

ISSUE TYPE
  • Feature Pull Request
COMPONENT NAME
  • Collections

@beeankha beeankha changed the title Porting Collections Off of Tower CLI [WIP] Porting Collections Off of Tower CLI Jan 23, 2020
@beeankha beeankha self-assigned this Jan 23, 2020
@beeankha beeankha added component:awx_collection issues related to the collection for controlling AWX state:in_progress labels Jan 23, 2020
if not handle_response:
return response
elif response['status_code'] == 200:
existing_return['changed'] = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

link #5474

Copy link
Contributor

@squidboylan squidboylan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left an inline comment about tower_cli.cfg default paths, another thing to note is that the tower_cli.cfg supports both yaml and key=value format, which we should make a decision on whether we need/want to support.

awx_collection/plugins/module_utils/tower_api.py Outdated Show resolved Hide resolved
except(Exception) as excinfo:
self.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo))

def update_if_needed(self, existing_item, new_item, handle_response=True, **existing_return):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this gets too far into the weeds, I want to ask about the highest priority design item, which is the consistency of how these methods are called.

It would be really good to distinguish all methods that terminate execution - that is they do exit_json or fail_json. I would also like to be very careful that they either always terminate or never terminate. You've done an excellent job fitting a solution to do that with the on_changed callback. Whether they terminate or not is so important, I almost think it should be in the method name.

On to the call arguments, the core CRUD actions involve update_if_needed, post_endpoint, and delete_endpoint. Consider the arguments for post_endpoint versus this method here.

def post_endpoint(self, endpoint, handle_return=True, item_type='item', item_name='', *args, **kwargs):

This just doesn't look the same at all. My position is that these should all have a common set of functionality - because they are terminating methods.

Likewise, for deletion, you handle the case of changed=False in the module code very elegantly, but that's not consistent with this. If we have a method update_if_needed, then we should have a delete_if_needed method so that we know that (a) it will terminate execution and (b) it could terminate with multiple types of statuses.

I come back around to the design that tower-cli used, maybe we should have a single method for CRU and another one for D. Since the module itself deals with the state parameter, this makes a lot of sense with the surface area that Ansible requires.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great feed back.

In thinking about, I'm learning towards having C, U, and D methods. In the form of:
update_if_needed
create_if_needed
delete_if_needed

I don't want to put _and_exit on the functions since you can override the exit with handle_response=False and my gut is saying we will find some use case where we might have a "compound" action we need to take. I.e. create item 'a' and then create item 'b' which has a reference to 'a'. So the first one would handle_exit=False and the second one is True.

Each of the methods above can have a corresponding hook for on_create, on_update, and on_delete. Do you think those should be "registered" like I did or just make them parameters to the if_needed methods?

The R will continue to be handled via get_one and get_all (I'm thinking we can rename get_all_endpoint to just get_all for consistency).

In addition, I think it would make sense to merged CU method called create_or_update_if_needed so that modules can get into a very simple pattern like:

if state == 'absent':
    delete_if_needed(<args>)
else:
    create_or_update_if_needed(<args>)

Another other option would be to make a single method like: put_object_in_state(old_object, new_object, state, ...) which would handle all of the CUD operations in one. Any thoughts on that? That would put even the conditional logic into the module_util so the modules would only need to:

  • resolve associated IDs
  • build the object data structures
  • define any on_* methods
  • make the put_object_in_state call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do feel partial to the CU, and D methods, exactly how your proposed code snippet is. But as long as we have consistent args with calls to _if_needed methods, then I give my 👍

my gut is saying we will find some use case where we might have a "compound" action we need to take. I.e. create item 'a' and then create item 'b' which has a reference to 'a'. So the first one would handle_exit=False and the second one is True.

Good point, we should be prepared to complex multi-action scenarios. Given that, handle_exit=False sounds like a good solution to me... probably better than the callback approach. If the actions got complex, then we would wind up in callback hell. The way tower_project waits for update could be re-written to use a handle_exit=False kwarg and not the callback.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check it out now. I left the option for a callback for "simple" things, like waiting for a project update to complete. For more complex things you can give it handle_response=False but then you are on the hook for processing the return and follow up actions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on board with the call pattern in the modules now. Having both the callback and handle_response kwarg has a pretty good justification.

else:
# If the state was present and we had a team we can see if we need to update it
# This will return on its own
module.update_if_needed(team, team_fields)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a specific example, following up to my last comment, both delete_endpoint and post_endpoint need the plural name used in the list endpoint, the string "teams" here. The method update_if_needed avoids this by using the URL in the data, but it doesn't need to. It could also do a PATCH to 'teams/{0}'.format(team['id']), and I would prefer this because it's more consistent.

The arg item_name doesn't seem to me like something we need to keep. We could default to looking for name, and then have some other means of overriding that on initialization of the class for the special cases.

The data is clearly needed in all cases.

So this points us in the direction of a call pattern like 'teams', team_fields, and you could share that pattern between all the methods from module_utils.

module.delete_if_needed(team)
elif state == 'present':
# If the state was present we can let the module build or update the existing team, this will return on its own
module.create_or_update_if_needed(team, team_fields, item_type='team'})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! I can't wait to look into check_mode and diff_mode later on. That will be so painless by comparison.

if existing_item:
return self.update_if_needed(existing_item, new_item, handle_response=handle_response, on_update=on_update)
else:
return self.create_if_needed(existing_item, new_item, handle_response=handle_response, on_create=on_create, item_type=item_type)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this need to pass in the plural endpoint? So I would expect that create_or_update_if_needed will also need to take the plural endpoint.

Alternatively, it would make sense to me to pass that into __init__. It's a general property of a resource, as long as we're only declaring it once for each module, that's about the most efficient solution we could hope for. You could even take it in optionally, and just add an "s" if it's not given.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct, this will need the endpoint passed into it and then on down to create_if_needed.

The init option would be cool but if we are trying to keep complex use cases in mind that would require an override option anyway.

Finally for pluralization, I wish we could auto-pluralize a friendly name but inventorys is not valid also; if anyone ever wanted to hit /me, /config, ping, etc auto-pluralization logic could become tricky and something that would need to be maintained. It feels like it might almost work better to do an un-pluralization to derive the friendly name from the endpoint.

But I'll go ahead and get the pass through of the endpoint working and then touchup the converted modules to use the new syntax; thanks for the review.

@beeankha beeankha force-pushed the collections branch 2 times, most recently from e16ab16 to 00b393c Compare February 2, 2020 17:22

# If the variables were spacified as a file, load them
if variables:
variables = module.load_variables_if_file_specified(variables, 'variables')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that we're still doing this, but I don't want to add one more deprecated thing into this PR.

But I might make an issue to get rid of all these instances, and wanted to ask how you would feel about that. In order to optionally take the "@" character, that means the parameter type is string, and I would much prefer that the type be a dict.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I almost said I 100% agree on this but then I started to think about it....
The only reason this notation was supported was because it had to be fed into Tower CLI that way. Since we are deprecating Tower CLI with this PR one could argue that now is the perfect time to drop this. If we wanted to do that we could provide in the release notes something like:

IF you used the @ method to import a vars file, here is how you would convert your code to have Ansible load a vars file.... one of the caveats would be that the @ syntax as is loads the file from the targeted node where as using a lookup plugin would load from the controller.

Also, as an FYI, in my test I passed in something like:
variables:
foo: bar
Ansible generates a warning that its forcing a dict into a str but other than that it works.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IF you used the @ method to import a vars file, here is how you would convert your code to have Ansible load a vars file....

probably similar to:

https://github.com/ansible/awx/blob/devel/awx_collection/plugins/modules/tower_job_template.py#L242

I'm cautiously optimistic that no one will have problems with these parameter type changes - because the change was prompted by Ansible core itself. Their CI started enforcing types, but old modules were given an exception. The variables param here technically passes their CI, but doesn't respect the spirit of enforcing types. So at minimum, we have someone else to point the finger at.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can go ahead and take this out (changes currently in progress). Do we want to raise an exception or warn that the "@" syntax is no longer supported?

elif state == 'present':
# We need to perform a check to make sure you are not trying to convert a regular inventory into a smart one.
if inventory and inventory['kind'] == '' and inventory_fields['kind'] == 'smart':
module.fail_json(msg='You cannot turn a regular inventory into a "smart" inventory.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't the API itself give a 400 in this event? If it does that, and if the 400 error is handled correctly, then I don't think we should need this.

that case can be unit tested. As long as it gives a sensible error message that says mostly the same thing, we shouldn't need this conditional.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API returns a 405 without a helpful error message:

Screen Shot 2020-02-10 at 9 32 16 AM

I filed a bug for this here: #5744

john-westcott-iv and others added 16 commits February 26, 2020 09:15
Fix up inventory_source module changes, fix import yaml sanity error, change inventory_source unit tests to comply with new structure.
Fixed typo in help documentation

Fix up sanity errors and update converted modules

Remove unnecessary test playbook file
Now supports json or yaml

Depricated multiple k=v on a single line

Remove assert statement and unused import from module_util
Unitied comment

Fix up inventory_source example, misc comment edits
Because scm_update_cache_timeout has a default and thus will always be != None
…teger

Fix linting issues, update tower_project unit test
Clear up sanity test and remove redundant import statement
AlanCoding and others added 5 commits February 26, 2020 09:26
* Allow running tests without tower_cli

* patch up test mutability

* Fix test import error, warning mock

* flake8 error

Update documentation for non-converted modules
* Lean on API validation for tower_inventory_source arg errors

used for
 - validating needed credential is given
 - missing source_project for scm sources

* Add warning when config is specified in 2 places

Fix up unit tests, address multiple comments re: backwards compatibility, redundant methods, etc.

Update new_name and variables parameters, update unit tests
Removing default of empty dict from variables param on group and host modules

Make modules comply with updated sanity tests
…end and receive modules

Update variables in tower_inventory to be in dict format
@softwarefactory-project-zuul
Copy link
Contributor

Build succeeded.

@beeankha beeankha changed the title [WIP] Porting Collections Off of Tower CLI Porting Collections Off of Tower CLI Feb 26, 2020
@softwarefactory-project-zuul
Copy link
Contributor

Build succeeded (gate pipeline).

@softwarefactory-project-zuul softwarefactory-project-zuul bot merged commit 154b9c3 into ansible:devel Feb 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component:awx_collection issues related to the collection for controlling AWX
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants