Repository: ansible/tower-cli
Pull request #315 (2017-07-06 17:38:36)

Allow related specification of inv src by short name
This is a shot at #302

However, I'm not 100% sure this will be workable. The extremely major problem is that group names are not unique globally, and that's what this is leveraging. But if this solves an immediate need, it might be acceptable.

Demo:

```
tower-cli node create -W asdf --inventory-source "Alan AWS" -v
*** DETAILS: The workflow_job_template field is given as a name; looking it up. 
*** DETAILS: Getting the record. **********************************************
GET http://localhost:8013/api/v1/workflow_job_templates/
Params: {'name': 'asdf'}

*** DETAILS: The inventory_source field is given as a name; looking it up. ****
*** DETAILS: Getting the record. **********************************************
GET http://localhost:8013/api/v1/inventory_sources/
Params: {u'name__startswith': 'Alan AWS'}

*** DETAILS: Checking for an existing record. *********************************
*** DETAILS: Writing the record. **********************************************
POST http://localhost:8013/api/v1/workflow_job_template_nodes/
Data: {'unified_job_template': 10, 'workflow_job_template': 25}

Resource changed.
== ===================== ==================== 
id workflow_job_template unified_job_template 
== ===================== ==================== 
 1                    25                   10
== ===================== ==================== 
```

Note that the workflow schema is fully built on top of these existing utilities, so when you reference something in the workflow schema command, it's doing the same thing as here.


Patch:
diff --git a/tower_cli/resources/inventory_source.py b/tower_cli/resources/inventory_source.py
index 41739d9..0be8984 100644
--- a/tower_cli/resources/inventory_source.py
+++ b/tower_cli/resources/inventory_source.py
@@ -19,6 +19,8 @@ from tower_cli import models, resources
 from tower_cli.api import client
 from tower_cli.utils import debug, types, exceptions as exc
 
+import re
+
 
 class Resource(models.MonitorableResource):
     cli_help = 'Manage inventory sources within Ansible Tower.'
@@ -26,6 +28,7 @@ class Resource(models.MonitorableResource):
     internal = True
     unified_job_type = '/inventory_updates/'
 
+    name = models.Field(unique=True)
     credential = models.Field(type=types.Related('credential'), required=False)
     source = models.Field(
         default=None,
@@ -40,7 +43,7 @@ class Resource(models.MonitorableResource):
     instance_filters = models.Field(required=False, display=False)
     group_by = models.Field(required=False, display=False)
     source_script = models.Field(type=types.Related('inventory_script'),
-                                 required=False)
+                                 required=False, display=False)
     # Boolean variables
     overwrite = models.Field(type=bool, required=False, display=False)
     overwrite_vars = models.Field(type=bool, required=False, display=False)
@@ -51,6 +54,9 @@ class Resource(models.MonitorableResource):
     timeout = models.Field(type=int, required=False, display=False,
                            help_text='The timeout field (in seconds).')
 
+    def _is_full_v1_name(self, name):
+        return bool(re.match(r'^.+\s\(.+\s-\s\d+\)$', name))
+
     @click.argument('inventory_source', type=types.Related('inventory_source'))
     @click.option('--monitor', is_flag=True, default=False,
                   help='If sent, immediately calls `monitor` on the newly '
diff --git a/tower_cli/resources/job_template.py b/tower_cli/resources/job_template.py
index 60746c3..34e6b03 100644
--- a/tower_cli/resources/job_template.py
+++ b/tower_cli/resources/job_template.py
@@ -18,8 +18,7 @@ from __future__ import absolute_import, unicode_literals
 import click
 
 from tower_cli import models, resources
-from tower_cli.utils import types, parser
-from tower_cli.api import client
+from tower_cli.utils import types
 
 
 class Resource(models.SurveyResource):
@@ -146,18 +145,3 @@ class Resource(models.SurveyResource):
         """Disassociate a notification template from this job template."""
         return self._disassoc('notification_templates_%s' % status,
                               job_template, notification_template)
-
-    @resources.command(use_fields_as_options=('extra_vars'))
-    @click.option('--host-config-key', help='Job-template-specific string used to authenticate '
-                  'host during provisioning callback.')
-    def callback(self, pk=None, host_config_key='', extra_vars=None):
-        """Contact Tower and request a configuration update using this job template."""
-        url = self.endpoint + '%s/callback/' % pk
-        if not host_config_key:
-            host_config_key = client.get(url).json()['host_config_key']
-        post_data = {'host_config_key': host_config_key}
-        if extra_vars:
-            post_data['extra_vars'] = parser.process_extra_vars(list(extra_vars), force_json=True)
-        r = client.post(url, data=post_data, auth=None)
-        if r.status_code == 201:
-            return {'changed': True}
diff --git a/tower_cli/utils/types.py b/tower_cli/utils/types.py
index 766510b..a9db760 100644
--- a/tower_cli/utils/types.py
+++ b/tower_cli/utils/types.py
@@ -135,7 +135,15 @@ class Related(click.types.ParamType):
         try:
             debug.log('The %s field is given as a name; '
                       'looking it up.' % param.name, header='details')
-            rel = resource.get(**{resource.identity[-1]: value})
+            lookup_data = {resource.identity[-1]: value}
+            # Special case to look up inventory sources by partial name
+            # TODO: Remove with v1 deprecation
+            if (self.resource_name == 'inventory_source' and
+                    not resource._is_full_v1_name(value)):
+                lookup_data = {
+                    "query": [('name__startswith', value)]
+                }
+            rel = resource.get(**lookup_data)
         except exc.MultipleResults as ex:
             raise exc.MultipleRelatedError(
                 'Cannot look up {0} exclusively by name, because multiple {0} '


Test patch:
diff --git a/tests/test_resources_inventory_source.py b/tests/test_resources_inventory_source.py
index abe982f..b0d7834 100644
--- a/tests/test_resources_inventory_source.py
+++ b/tests/test_resources_inventory_source.py
@@ -162,3 +162,17 @@ class StatusTests(unittest.TestCase):
             })
             with self.assertRaises(exc.NotFound):
                 self.res.status(1)
+
+
+class NameTests(unittest.TestCase):
+
+    def setUp(self):
+        self.res = tower_cli.get_resource('inventory_source')
+
+    def test_valid_name(self):
+        assert self.res._is_full_v1_name('foo bar (In Space - 873928)')
+
+    def test_invalid_names(self):
+        assert not self.res._is_full_v1_name('foo bar (In Space  873928)')
+        assert not self.res._is_full_v1_name('foo bar (In Space - 873928')
+        assert not self.res._is_full_v1_name('foo bar (In Space - )')
diff --git a/tests/test_resources_job_template.py b/tests/test_resources_job_template.py
index 575cbee..79cc44f 100644
--- a/tests/test_resources_job_template.py
+++ b/tests/test_resources_job_template.py
@@ -182,12 +182,3 @@ class TemplateTests(unittest.TestCase):
             self.res.disassociate_notification_template(5, 3, 'any')
             self.assertEqual(t.requests[1].body,
                              json.dumps({'disassociate': True, 'id': 3}))
-
-    def test_callback(self):
-        """Establish that a job template should be able to conduct provisioning callback
-        """
-        with client.test_mode as t:
-            t.register_json('/job_templates/5/callback/', {'host_config_key': 'foobar'})
-            t.register_json('/job_templates/5/callback/', {}, method='POST')
-            self.res.callback(5)
-            self.assertEqual(t.requests[1].body, json.dumps({"host_config_key": "foobar"}))