Repository: ansible/tower-cli
Pull request #544 (2018-06-09 01:25:10)

Added --ask-tower-pass
This option will use click to prompt for a tower password if present. No more typing passwords on command line for your shell history to remember.

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

Coverage increased (+0.01%) to 63.981% when pulling **28e3dd76274753396d887b8c654d6a522efe2d3c on john-westcott-iv:ask-pass** into **a3bad1e117e35a056d36c9f8f3a6bad40781b900 on ansible:master**.

AlanCoding: what if, instead of a new option, we used your callback in the event that the string value of the existing password option was "ASK"? That will avoid lengthening the help text volume, and it's an approach that Tower/AWX itself has used for password prompting.
john-westcott-iv: It would be very easy to allow prompting for all of the runtime variables. Is that something we want to do? I thought of it because I could see someone saying that want to be prompted for the username as well as the password.
AlanCoding: I would stick to password for now, it's hard to imagine a good use case for prompting for those other things.

Patch:
diff --git a/docs/source/cli_ref/index.rst b/docs/source/cli_ref/index.rst
index d459aa1..7137ab4 100644
--- a/docs/source/cli_ref/index.rst
+++ b/docs/source/cli_ref/index.rst
@@ -209,6 +209,8 @@ and ``tower-cli {resource} {action} --help`` shows details of the usage of this
       -p, --tower-password TEXT       Password to use to authenticate to Ansible
                                       Tower. This will take precedence over a
                                       password provided to `tower config`, if any.
+                                      If set to ASK you will be prompted to
+                                      input the password.
       -u, --tower-username TEXT       Username to use to authenticate to Ansible
                                       Tower. This will take precedence over a
                                       username provided to `tower config`, if any.
diff --git a/tower_cli/api.py b/tower_cli/api.py
index fb287f1..3e47378 100644
--- a/tower_cli/api.py
+++ b/tower_cli/api.py
@@ -20,12 +20,6 @@ import functools
 import json
 import stat
 import warnings
-
-try:
-    from urlparse import urljoin, urlparse
-except Exception as e:
-    from urllib.parse import urljoin, urlparse
-
 from datetime import datetime as dt
 
 from requests.exceptions import ConnectionError, SSLError
@@ -193,16 +187,9 @@ class Client(Session):
                 'Can not verify ssl with non-https protocol. Change the '
                 'verify_ssl configuration setting to continue.'
             )
-        # Validate that we have either an http or https based URL
-        url_pieces = urlparse(host)
-        if url_pieces[0] not in ['http', 'https']:
-            raise exc.ConnectionError('URL must be http(s), {} is not valid'.format(url_pieces[0]))
-
-        prefix = urljoin(host, '/api/')
+        prefix = os.path.sep.join([host.rstrip('/'), 'api', ''])
         if include_version:
-            # We add the / to the end of {} so that our URL has the ending slash.
-            prefix = urljoin(prefix, "{}/".format(CUR_API_VERSION))
-
+            prefix = os.path.sep.join([prefix.rstrip('/'), CUR_API_VERSION, ''])
         return prefix
 
     @functools.wraps(Session.request)
diff --git a/tower_cli/conf.py b/tower_cli/conf.py
index 4552f26..bdd65e9 100644
--- a/tower_cli/conf.py
+++ b/tower_cli/conf.py
@@ -23,6 +23,7 @@ import os
 import stat
 import warnings
 from functools import wraps
+from sys import argv
 
 import six
 from six.moves import configparser
@@ -340,6 +341,8 @@ settings = Settings()
 
 
 def _apply_runtime_setting(ctx, param, value):
+    if param.name == 'tower_password' and value == 'ASK' and '--help' not in argv:
+        value = click.prompt('Enter tower password', type=str, hide_input=True)
     settings.set_or_reset_runtime_param(param.name, value)
 
 
@@ -399,7 +402,8 @@ def with_global_options(method):
         '-p', '--tower-password',
         help='Password to use to authenticate to Ansible Tower. '
              'This will take precedence over a password provided to '
-             '`tower config`, if any.',
+             '`tower config`, if any. If value is ASK you will be '
+             'prompted for the password',
         required=False, callback=_apply_runtime_setting,
         is_eager=True
     )(method)
diff --git a/tower_cli/resources/project.py b/tower_cli/resources/project.py
index de1b21f..a62cf26 100644
--- a/tower_cli/resources/project.py
+++ b/tower_cli/resources/project.py
@@ -114,7 +114,7 @@ class Resource(models.Resource, models.MonitorableResource):
             # Processing the organization flag depends on version
             debug.log('Checking Organization Relationship.', header='details')
             r = client.options('/projects/')
-            if 'organization' in r.json().get('actions', {}).get('POST', {}):
+            if 'organization' in r.json()['actions']['POST']:
                 kwargs['organization'] = organization
             else:
                 post_associate = True


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index 45818a0..98cc629 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -46,24 +46,12 @@ class ClientTests(unittest.TestCase):
         with settings.runtime_values(host='33.33.33.33'):
             self.assertEqual(client.get_prefix(), 'https://33.33.33.33/api/%s/' % CUR_API_VERSION)
 
-    def test_prefix_https_protocol(self):
-        """Establish that the http protocol works
-        """
-        with settings.runtime_values(host='https://33.33.33.33'):
-            self.assertEqual(client.get_prefix(), 'https://33.33.33.33/api/%s/' % CUR_API_VERSION)
-
-    def test_prefix_http_protocol(self):
-        """Establish that the http protocol works
-        """
-        with settings.runtime_values(host='http://33.33.33.33', verify_ssl=False):
-            self.assertEqual(client.get_prefix(), 'http://33.33.33.33/api/%s/' % CUR_API_VERSION)
-
     def test_prefix_explicit_protocol(self):
-        """Establish that the prefix property can not start with non-http protocol.
+        """Establish that the prefix property returns the appropriate
+        URL prefix and don't clobber over an explicit protocol.
         """
         with settings.runtime_values(host='bogus://33.33.33.33/'):
-            with self.assertRaises(exc.ConnectionError):
-                client.get_prefix()
+            self.assertEqual(client.get_prefix(), 'bogus://33.33.33.33/api/%s/' % CUR_API_VERSION)
 
     def test_request_ok(self):
         """Establish that a request that returns a valid JSON response