Repository: ansible/tower-cli
Pull request #278 (2017-03-22 13:51:56)

implement relaunch of jobs like in the UI

coveralls: 
[![Coverage Status](https://coveralls.io/builds/10719469/badge)](https://coveralls.io/builds/10719469)

Coverage decreased (-0.2%) to 90.985% when pulling **563236b2e0beaae0e242618a71bc2c931b646065 on evgeni:job-relaunch** into **9e1bc5f902164ef9a2867cb360f9b964c93967bc on ansible:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/10736389/badge)](https://coveralls.io/builds/10736389)

Coverage decreased (-0.3%) to 90.722% when pulling **6fe54c86272ea7c7aefdcf92c0577a0b561013c8 on evgeni:job-relaunch** into **499f74ffe092d27844288f6098cdd289c0c72afd on ansible:master**.


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index e9720ec..9f92079 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,11 +1,6 @@
 Release History
 ===============
 
-3.1.3 (2017-03-22)
-------------------
-
-- Fixed a bug where extra_vars were dropped in some commands.
-
 3.1.2 (2017-03-21)
 ------------------
 
diff --git a/VERSION b/VERSION
index ff365e0..ef538c2 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-3.1.3
+3.1.2
diff --git a/packaging/rpm/ansible-tower-cli.spec b/packaging/rpm/ansible-tower-cli.spec
index ae272c5..8715933 100644
--- a/packaging/rpm/ansible-tower-cli.spec
+++ b/packaging/rpm/ansible-tower-cli.spec
@@ -1,7 +1,7 @@
 %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
 
 Name:           ansible-tower-cli
-Version:        3.1.3
+Version:        3.1.2
 Release:        1%{?dist}
 Summary:        Commandline interface for Ansible Tower
 Group:          Development/Tools
diff --git a/tower_cli/models/base.py b/tower_cli/models/base.py
index 8fc1f86..ba636a1 100644
--- a/tower_cli/models/base.py
+++ b/tower_cli/models/base.py
@@ -484,8 +484,7 @@ class ResourceMethods(BaseResource):
         click is unfortunately bad at the way it sends through unspecified
         defaults."""
         for key, value in copy(kwargs).items():
-            # options with multiple=True return a tuple
-            if value is None or value == ():
+            if value is None:
                 kwargs.pop(key)
             if hasattr(value, 'read'):
                 kwargs[key] = value.read()
@@ -1190,6 +1189,33 @@ class ExeResource(MonitorableResource):
         # Return a success.
         return {'status': 'canceled', 'changed': changed}
 
+    @resources.command
+    def relaunch(self, pk=None, **kwargs):
+        """Relaunch a stopped job.
+
+        Fails with a non-zero exit status if the job cannot be relaunched.
+        You must provide either a pk or parameters in the job's identity.
+        """
+        # Search for the record if pk not given
+        if not pk:
+            existing_data = self.get(**kwargs)
+            pk = existing_data['id']
+
+        relaunch_endpoint = '%s%s/relaunch/' % (self.endpoint, pk)
+        data = {}
+        # Attempt to relaunch the job.
+        answer = {}
+        try:
+            result = client.post(relaunch_endpoint, data=data).json()
+            if 'id' in result:
+                answer.update(result)
+            answer['changed'] = True
+        except exc.MethodNotAllowed:
+            answer['changed'] = False
+
+        # Return the answer.
+        return answer
+
 
 class Resource(ResourceMethods):
     """This is the parent class for all standard resources."""
@@ -1205,14 +1231,18 @@ class Resource(ResourceMethods):
                   help='If used, if a match is found on unique fields, other '
                        'fields will be updated to the provided values. If '
                        'False, a match causes the request to be a no-op.')
-    def create(self, **kwargs):
+    def create(self, fail_on_found=False, force_on_exists=False, **kwargs):
         """Create an object.
 
         Fields in the resource's `identity` tuple are used for a lookup;
         if a match is found, then no-op (unless `force_on_exists` is set) but
         do not fail (unless `fail_on_found` is set).
         """
-        return self.write(create_on_missing=True, **kwargs)
+        return super(Resource, self).write(
+            create_on_missing=True,
+            fail_on_found=fail_on_found, force_on_exists=force_on_exists,
+            **kwargs
+        )
 
     @resources.command(ignore_defaults=True)
     @click.option('--create-on-missing', default=False,
diff --git a/tower_cli/resources/job_template.py b/tower_cli/resources/job_template.py
index 34e6b03..1da228b 100644
--- a/tower_cli/resources/job_template.py
+++ b/tower_cli/resources/job_template.py
@@ -99,12 +99,17 @@ class Resource(models.SurveyResource):
         help_text='On write commands, perform extra POST to the '
                   'survey_spec endpoint.')
 
-    def write(self, *args, **kwargs):
+    @resources.command
+    def create(self, fail_on_found=False, force_on_exists=False,
+               extra_vars=None, **kwargs):
+        """Create a job template."""
         # Provide a default value for job_type, but only in creation of JT
-        if (kwargs.get('create_on_missing', False) and
-                (not kwargs.get('job_type', None))):
+        if not kwargs.get('job_type', False):
             kwargs['job_type'] = 'run'
-        return super(Resource, self).write(*args, **kwargs)
+        return super(Resource, self).create(
+            fail_on_found=fail_on_found, force_on_exists=force_on_exists,
+            **kwargs
+        )
 
     @resources.command(use_fields_as_options=False)
     @click.option('--job-template', type=types.Related('job_template'))


Test patch:
diff --git a/tests/test_resources_job.py b/tests/test_resources_job.py
index 8891003..6d1d0c7 100644
--- a/tests/test_resources_job.py
+++ b/tests/test_resources_job.py
@@ -667,3 +667,22 @@ class CancelTests(unittest.TestCase):
             t.register('/jobs/42/cancel/', '', method='POST', status_code=405)
             with self.assertRaises(exc.TowerCLIError):
                 self.res.cancel(42, fail_if_not_running=True)
+
+
+class RelaunchTests(unittest.TestCase):
+    """A set of tasks to establish that the job relaunch command works in the
+    way that we expect.
+    """
+    def setUp(self):
+        self.res = tower_cli.get_resource('job')
+
+    def test_standard_relaunch(self):
+        """Establish that a standard relaunch command works in the way
+        we expect.
+        """
+        with client.test_mode as t:
+            data = {'id': 43}
+            t.register_json('/jobs/42/relaunch/', data, method='POST')
+            result = self.res.relaunch(42)
+            self.assertTrue(t.requests[0].url.endswith('/jobs/42/relaunch/'))
+            self.assertTrue(result['changed'])
diff --git a/tests/test_resources_workflow.py b/tests/test_resources_workflow.py
index be83bbf..df92417 100644
--- a/tests/test_resources_workflow.py
+++ b/tests/test_resources_workflow.py
@@ -90,5 +90,7 @@ class NodeModelTests(unittest.TestCase):
             mck.__name__ = 'create'
             self.res.create(workflow_job_template=1,
                             job_template=5)
-            self.assertEqual(mck.call_args[1]['unified_job_template'], 5)
-            self.assertEqual(mck.call_args[1]['workflow_job_template'], 1)
+            mck.assert_called_once_with(
+                create_on_missing=True, fail_on_found=False,
+                force_on_exists=False, unified_job_template=5,
+                workflow_job_template=1)